aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-08-07 15:29:37 +0200
committerblasty <blasty@local>2026-08-07 15:29:49 +0200
commit8fef561903e9501281ebcc58cdb33ee4d9116fd0 (patch)
tree0edd3a430134fdddd5a4826b835360591e632507
parentREADME: pure markdown, and a logo sized for a README (diff)
downloadida-tui-8fef561903e9501281ebcc58cdb33ee4d9116fd0.tar.gz
ida-tui-8fef561903e9501281ebcc58cdb33ee4d9116fd0.tar.xz
ida-tui-8fef561903e9501281ebcc58cdb33ee4d9116fd0.zip
docs: drop two stale files, correct the two the README links, cut sharp edges
README: "Known sharp edges" removed. docs/GRAPH_VIEW.md pointed the graph's one backend call at server/patch_server.py, a file the port deleted. It is idatui/codemode_client.py now. This matters because GRAPH_VIEW.md is one of only two docs the README links -- a wrong path in a linked doc is worse than no doc. docs/RPC.md was missing five verbs rpc.py serves: ping/methods/quit (now a Session table) and trace/binaries/switch (with the semantic verbs). The trace verb drives a whole feature the README advertises, so its absence was the real gap. Every verb in rpc.py's METHODS table is now documented, checked mechanically -- the remainder of what looked missing turned out to be values of the "move" verb's dir parameter, not verbs. Deleted: - docs/CODEMODE_PORT.md. Written mid-port, and its Result section claims the port covered "the complete current TUI feature set". It did not: the graph view was dead, the operand-format keys were dead, pseudocode comments never appeared, and bulk rename was broken. Keeping a doc that states the opposite of what we found is worse than having none, and docs/CODEMODE_UPSTREAM.md carries the accurate account of the API limits. - docs/TUI_DRIVING_BLUEPRINT.md. A design sketch for generalizing the drive-a-TUI-over-a-socket pattern to other programs, with gdb as the main example -- which now exists for real in ~/dev/gdb-driver. Superseded by the thing it proposed. Nothing linked either file. Both remain in git history.
Diffstat (limited to '')
-rw-r--r--README.md10
-rw-r--r--docs/CODEMODE_PORT.md175
-rw-r--r--docs/GRAPH_VIEW.md4
-rw-r--r--docs/RPC.md10
-rw-r--r--docs/TUI_DRIVING_BLUEPRINT.md355
5 files changed, 12 insertions, 542 deletions
diff --git a/README.md b/README.md
index e551a8e..5eb00f8 100644
--- a/README.md
+++ b/README.md
@@ -161,16 +161,6 @@ flake, and the four ways a test here wastes minutes.
- [`docs/RPC.md`](docs/RPC.md) — the RPC protocol, verb by verb
- [`docs/GRAPH_VIEW.md`](docs/GRAPH_VIEW.md) — how the graph is laid out
-## Known sharp edges
-
-- PLT/import stubs: following a libc symbol lands on `extrn X:near`, which
- Hex-Rays can't decompile. No pretty-printing yet.
-- Xref-select can land on the wrong line in the `x` popup.
-- No "discard changes and quit": a managed worker saves when its last lease
- closes, so the TUI can't promise your edits weren't persisted.
-- A GUI user editing the same database won't invalidate our caches — Code Mode
- has no change feed yet (see `docs/CODEMODE_UPSTREAM.md`).
-
[sl0p.foo](https://sl0p.foo)
diff --git a/docs/CODEMODE_PORT.md b/docs/CODEMODE_PORT.md
deleted file mode 100644
index 4b4bc45..0000000
--- a/docs/CODEMODE_PORT.md
+++ /dev/null
@@ -1,175 +0,0 @@
-# ida-tui → IDA Code Mode port
-
-This port is an experiment: can ida-tui be implemented as an ordinary client of
-`ida_codemode`, sharing GUI databases and managed idalib workers instead of
-owning a private worker and depending on ida-pro-mcp tool functions?
-
-## Result
-
-Yes for the database lifecycle and the complete current TUI feature set, with a
-small number of operations implemented using IDAPython inside Code Mode's
-`execute_python` sandbox because ida-domain does not yet expose the required
-behavior.
-
-The old components are gone:
-
-- `idatui/worker.py` (private pickle/socket idalib process)
-- `idatui/worker_client.py`
-- `server/patch_server.py` (ida-pro-mcp tool injection)
-
-The replacement is `idatui/codemode_client.py`.
-
-## Lifecycle mapping
-
-`CodeModeClient.connect()` calls `ida_codemode.client.DatabaseHandle.open()`.
-Resolution is therefore Code Mode's resolution, not ida-tui's:
-
-1. Match a registered GUI by executable path.
-2. Otherwise match the owner of the expected IDB.
-3. Otherwise serialize creation and start a managed `ida-codemode-worker`.
-4. Establish an authenticated SSE lease.
-5. Wait through the public autoanalysis route.
-
-The handle's registry entry supplies the backend, PID, executable path, IDB path,
-and record ID used by the status/pool layers.
-
-Closing ida-tui closes only its lease. It never closes a GUI or kills an idalib
-process. A managed worker saves and exits under Code Mode's own policy after its
-last lease disappears. A second agent or TUI can keep using the same instance.
-
-This also changes project pooling semantics. `DatabasePool` is an LRU pool of
-leases, not process ownership. Managed-IDB save-on-evict remains; budget eviction
-does not implicitly save a GUI. Eviction cannot force a shared worker to exit,
-and GUI process memory is only advisory.
-
-## ida-domain coverage
-
-The remote snippets receive Code Mode's preloaded `db` (`ida_domain.Database`).
-The following TUI needs map to public ida-domain entities:
-
-| TUI need | ida-domain surface |
-|---|---|
-| Function paging, lookup, names, sizes | `db.functions` |
-| Segments and names | `db.segments` |
-| Instructions and plain disassembly | `db.instructions`, `db.functions.get_instructions()` |
-| Heads and item classification | `db.heads`, `db.bytes` |
-| Bytes and strings | `db.bytes`, `db.strings` |
-| Symbol resolution and rename | `db.names`, `db.functions` |
-| Comments | `db.comments` |
-| Imports and exports | `db.imports`, `db.entries` |
-| Xrefs and fine type predicates | `db.xrefs` / `XrefInfo` |
-| Named types, members, parse/apply | `db.types` |
-| Function prototypes and local variables | `db.pseudocode`, `PseudocodeFunction.local_variables` |
-| Decompilation text and object references | `db.pseudocode` |
-
-All values are reduced to JSON primitives inside the database process. No SWIG
-or ida-domain object crosses the Code Mode boundary.
-
-## Remaining IDAPython gaps
-
-Code Mode intentionally allows regular Python imports, so these features still
-work, but they identify useful additions to ida-domain:
-
-1. **Rich continuous listing**
- - ida-domain enumerates defined heads and renders plain disassembly.
- - ida-tui also needs coalesced undefined runs, IDA colour-tag spans, function
- banners, code-label rows, file-region offsets, and expanded struct members.
- - The `heads` operation uses `ida_bytes`, `ida_lines`, and related modules for
- this presentation model.
-
-2. **Instruction/function carving**
- - Creating an instruction and walking a speculative decode run requires
- `ida_ua.create_insn` and processor flow/return checks.
- - Function creation exists in ida-domain; the explicit-end fallback still
- needs lower-level item boundaries.
-
-3. **ARM/Thumb state**
- - T-register ranges and segment addressing use `ida_segregs`, `ida_idp`, and
- `ida_segment`. There is no equivalent ida-domain operation.
-
-4. **Detailed decompiler diagnostics and line maps**
- - Pseudocode text, ctree objects, and the address map are available through
- ida-domain.
- - Reproducing IDA's per-rendered-line coverage uses
- `cfunc.get_line_item`; obtaining the exact Hex-Rays failure description
- uses `hexrays_failure_t`.
-
-5. **A few type/item primitives**
- - Deleting a named local type and some exact item-undefinition/data-creation
- behavior still use `ida_typeinf`/`ida_bytes` directly.
-
-These uses are isolated in `idatui/codemode_client.py`; the paging and Textual
-layers do not import IDAPython.
-
-## API limitations exposed by the port
-
-### No rollback or close-without-save
-
-A Code Mode lease has no rollback operation. Closing a GUI handle leaves the GUI
-state as-is. A managed idalib worker currently saves when its final lease closes.
-Consequently ida-tui's old “discard & quit” guarantee cannot be implemented.
-The UI now labels this choice “leave as-is & quit” and does not explicitly save,
-but managed-worker policy may still persist the changes.
-
-A true discard action would need a Code Mode/database API for transaction-like
-rollback, a close policy on a newly-owned worker, or a TUI-managed disposable DB
-copy.
-
-### Typed loader options only
-
-`DatabaseHandle.open()` supports processor, natural loading address, file type,
-output database, and fresh-database selection. It does not support ida-tui's
-arbitrary `ida_args` escape hatch. The adapter rejects unsupported switches
-rather than silently loading at the wrong architecture/base.
-
-### No database-change notification stream
-
-The lease reports liveness, not mutations. If a GUI user or another Code Mode
-client renames/retypes content while ida-tui is open, already-materialized TUI
-caches are not invalidated automatically. TUI-originated edits invalidate their
-own caches correctly. A database revision counter or change feed would make
-shared interactive editing robust.
-
-### Discovery requires a path for ambiguity
-
-`ida-tui` with no path attaches automatically when exactly one database is
-registered. With several registrations it lists them and requires an explicit
-executable/IDB path. There is not yet a pre-connection database picker in the
-Textual UI.
-
-### `DatabaseHandle` import stability
-
-The usable library primitive currently lives at
-`ida_codemode.client.DatabaseHandle`; `ida_codemode.__init__` exports nothing.
-The port therefore depends on a submodule path. Exporting the handle and public
-client exceptions from the package root would make the supported library API
-clearer.
-
-## Safety differences
-
-ida-tui no longer removes `.id0/.id1/.id2/.nam/.til` files before opening. That
-was only defensible when the TUI exclusively owned a private process; it is
-unsafe when a GUI or another client may own the database. Code Mode registry
-locks, health probes, and IDA itself now arbitrate ownership.
-
-The old pane “reap private workers” behavior is obsolete. A TUI crash closes its
-lease at the socket/kernel boundary; Code Mode decides whether a managed worker
-still has clients and when it should stop.
-
-## Verification surfaces
-
-The non-IDA suite verifies project staging, LRU lease behavior, load-option
-translation, and adapter response/error normalization. The existing live suites
-remain the end-to-end contract:
-
-```sh
-uv run python tests/test_codemode_client.py
-uv run python tests/test_pool.py
-uv run python tests/test_project.py
-uv run python tests/test_scenarios.py /path/to/binary
-```
-
-For GUI reuse, open the same binary in an IDA with the Code Mode plugin, confirm
-it appears in `ida_codemode.registry.discover_instances()`, then launch
-`ida-tui /path/to/binary`. The TUI status/`CodeModeClient.backend` should report
-`gui`, and closing the TUI must leave IDA open.
diff --git a/docs/GRAPH_VIEW.md b/docs/GRAPH_VIEW.md
index afd0f40..29b280e 100644
--- a/docs/GRAPH_VIEW.md
+++ b/docs/GRAPH_VIEW.md
@@ -52,8 +52,8 @@ every editing verb work inside a box because they are working on listing rows.
Growing a second disassembly renderer for graph mode would have been the real
cost.
-The backend adds exactly one tool, `flowchart(addr)` in
-`server/patch_server.py`, which returns block ranges and typed edges — **not**
+The backend adds exactly one operation, `flowchart(addr)` in
+`idatui/codemode_client.py`, which returns block ranges and typed edges — **not**
text.
## Layout (`idatui/graph.py`)
diff --git a/docs/RPC.md b/docs/RPC.md
index e433551..d13e8cd 100644
--- a/docs/RPC.md
+++ b/docs/RPC.md
@@ -49,6 +49,13 @@ Sequential connections (e.g. one `rpcclient` invocation per call) are fine.
## Methods
+### Session
+| method | params | notes |
+|--------|--------|-------|
+| `ping` | — | `{ok, proto, module, ready, functions, complete}` — also the default when `method` is omitted. Use it to wait for the index to finish loading. |
+| `methods` | — | this table, as data. |
+| `quit` | — | close the TUI gracefully (releases the database lease). |
+
### Raw injection (max fidelity)
| method | params | notes |
|--------|--------|-------|
@@ -88,6 +95,9 @@ predicate so the returned state is final.
| `back` | — | Escape: pop the nav stack. |
| `toggle_view` | — | Tab: disasm ⇄ pseudocode. |
| `hex` | — | `\`: hex view. |
+| `trace` | `seek` \| `goto` \| `step`, `over?` | navigate an execution trace (needs `--trace`). `seek` takes a timestamp, or `"!50"` for a percentage; `step` moves one instruction, `over=true` steps over a call. |
+| `binaries` | — | project binaries as `{label, active, resident, indexed}` (project mode only). |
+| `switch` | `label` | make another project binary active. |
| `graph` | `action?=show`, `target?`, `blocks?` | the control-flow graph (Space). `show` is a pure read — blocks, typed edges, ranks, box geometry and the cursor, **not** the box-drawing characters; `screen` gives you the drawing. `open`/`close`/`toggle` switch mode, `zoom` cycles full→compact→collapsed, `entry` jumps to the entry block, `succ`/`pred` follow one edge (`J`/`K`), `block target=<id\|0xADDR>` puts the cursor in a block. `blocks=false` omits the per-block list. See `docs/GRAPH_VIEW.md`. |
| `xrefs` | — | `x`: open the xref picker. |
| `symbols` | `query?` | Ctrl+N palette, optionally pre-typed. |
diff --git a/docs/TUI_DRIVING_BLUEPRINT.md b/docs/TUI_DRIVING_BLUEPRINT.md
deleted file mode 100644
index cb16d73..0000000
--- a/docs/TUI_DRIVING_BLUEPRINT.md
+++ /dev/null
@@ -1,355 +0,0 @@
-# Blueprint: a generic TUI-driving layer
-
-How to bring the "spawn a pane, drive the real TUI over a socket, read structured
-state back" experience (built for **idatui**, see `docs/RPC.md`) to *other* TUI
-software — most notably **gdb**, but the pattern generalizes to any terminal app
-(vim, k9s, lazygit, htop, a REPL, …).
-
-This is a design reference, not code. It captures the invariants worth keeping,
-the parts that are reusable as-is, and the one hard problem you re-solve per
-target (knowing when the UI has *settled*).
-
----
-
-## 1. The reference implementation (what we're generalizing)
-
-idatui is a Textual app we **own**, so we embedded an RPC server directly in its
-asyncio event loop. The stack that emerged:
-
-```
- tmux pane A (viewers see this) tmux pane B (the agent)
- ┌────────────────────────────┐ unix ┌────────────────────────┐
- │ the TUI, rendering normally │◀socket──▶│ ergonomic client (drive)│
- │ + in-process RPC server │ JSONL │ + pane manager │
- └────────────────────────────┘ └────────────────────────┘
-```
-
-Pieces (all reusable ideas): a **unix-socket JSONL server**, **three method
-tiers** (raw keys / semantic verbs / structured introspection), a **settle**
-primitive (block until the UI is quiescent before returning state), a
-**single-driver gate**, a **tmux pane manager** (spawn/stop/list + backend
-auto-start + readiness polling), and an **ergonomic client** (auto socket
-discovery, terse text, composite gestures).
-
----
-
-## 2. Invariants worth preserving (why it felt good)
-
-1. **Drive the *real* UI, not a hidden API.** Every action goes through the same
- input path a keyboard would, so the tmux pane shows it happening (livestream /
- debuggability). Bypassing the renderer to mutate state directly is faster but
- defeats the purpose.
-2. **Three tiers, always.** `raw` (keystrokes) for fidelity/coverage; `semantic`
- verbs (typed, with an optional per-char delay for the hand-typed look) for
- ergonomics; `introspection` (structured reads) so the agent reasons over data,
- not screen-scraping.
-3. **Every mutating call returns *after* the UI settled**, and returns fresh
- state. The driver never acts on a half-rendered frame.
-4. **Single-driver by default.** One client at a time; a second concurrent
- connection is refused. No multi-driver coordination to reason about.
-5. **Self-documenting surface.** A `methods` call returns the verb table; a
- `state`/`screen` call returns what's happening. The agent can discover the API.
-6. **Lifecycle is first-class.** Spawn a pane, wait until *ready*, drive, tear it
- down; never leak panes/processes. Auto-start the backing daemon if it's down.
-7. **An ergonomic layer on top.** The raw JSON transport is correct but verbose;
- a thin CLI that auto-resolves the socket and prints terse text is what you
- actually use all day.
-
----
-
-## 3. Generic architecture
-
-Split into a **reusable core** and a per-target **Adapter**. Only the adapter
-changes between targets.
-
-```
- ┌──────────────── reusable core ────────────────┐
-client ─┤ transport (unix sock, JSONL, single-driver) │
- (drive)│ dispatch (method table, tiers, error framing) │
- │ pane manager (tmux spawn/stop/list, readiness) │
- └───────────────────────┬───────────────────────┘
- │ Adapter interface
- ┌────────────┴────────────┐
- │ Target Adapter │ ← the only per-target code
- │ inject / settle / │
- │ snapshot / screen / │
- │ semantic verbs │
- └──────────────────────────┘
-```
-
-### The Adapter interface (the contract)
-
-Everything a target must provide. Keep it small:
-
-| method | purpose |
-|--------|---------|
-| `inject_keys(keys[])` | push keystrokes into the app's real input path |
-| `inject_text(str, delay_ms)` | type a literal string (visible typing) |
-| `settle(pred?, timeout) -> bool` | block until quiescent, then until `pred` (if given) |
-| `snapshot() -> dict` | structured "where am I / what's on screen" state |
-| `screen(fmt) -> {w,h,text}` | full render (plain / colored) of the pane |
-| `verbs` | target-specific semantic ops (each = compose inject + settle + a predicate) |
-| `ready() -> bool` | is the target drivable yet |
-| `quit()` | graceful shutdown |
-
-The core turns these into the wire method table and handles transport, framing,
-the single-driver gate, and the pane lifecycle.
-
----
-
-## 4. Target taxonomy — how much the app cooperates
-
-The adapter's implementation depends entirely on how much access you get. Three
-levels, best to worst:
-
-### Level 1 — **Embedded** (you own or can patch the app)
-Run an RPC server *inside* the app's event loop (idatui). Injection uses the
-app's own key API; `settle` uses its message pump + task/worker state; `snapshot`
-reads app state directly. **Highest fidelity, cheapest settle** (you have real
-signals). Use when you can add ~200 lines to the target.
-
-### Level 2 — **Control-channel** (app has a machine interface)
-Many serious TUIs expose a second, structured channel alongside the visual one:
-gdb (**GDB/MI** + a **Python API**), tmux (**control mode**, `-CC`), vim
-(**channels / `--remote-expr`**), lldb, many REPLs. Strategy: the **visual** pane
-is driven with keystrokes for fidelity; **semantic** verbs and **introspection**
-go through the control channel, which is request/response (so quiescence is
-*given* — a command is done when the channel replies). This is the sweet spot for
-gdb (see §8).
-
-### Level 3 — **Black-box PTY** (you can't modify or query it)
-The universal fallback: the app is just bytes in / a screen out (vim without
-channels, htop, an arbitrary curses app). You inject keystrokes into its PTY and
-read state by scraping the rendered screen. **Settle is the hard part** (no
-completion signal — you debounce on screen changes). Everything is a keystroke
-macro + screen assertion.
-
-> **Most targets are Level 2 or 3.** Design the core so an adapter can *mix*
-> levels: e.g. gdb = Level 2 for `break/step/bt/read-mem`, Level 3 (screen
-> scrape) for whatever the control channel doesn't expose.
-
----
-
-## 5. The three hard problems, generically
-
-### 5a. Input injection — "how do keystrokes get in"
-- **Embedded:** call the app's key-press API (idatui used Textual's `_press_keys`,
- the same path its own tests use). Supports a `wait:<ms>` token → free typed delay.
-- **Control-channel:** structured actions bypass keys entirely (gdb `-exec-*`);
- but *also* keep a keystroke path for TUI navigation.
-- **Black-box:** **`tmux send-keys -t <pane> …`** is the universal injector — no
- PTY plumbing. It even names special keys (`Enter`, `C-c`, `Escape`). For a
- literal string, `send-keys -l 'text'`. For the typed aesthetic, send char-by-char
- with a sleep between.
-
-### 5b. Settle / quiescence — "when did the UI finish reacting" *(the crux)*
-Pick the strongest signal the target offers. A taxonomy, best → worst:
-
-1. **Completion signal** *(embedded / control-channel).* The app tells you it's
- done: a worker/task queue drains (idatui: message pump + `WorkerManager`), or a
- request/response channel returns (`^done` in GDB/MI). Deterministic; prefer this.
-2. **Predicate wait.** Poll a cheap boolean that means "the thing I asked for
- happened" (idatui: `dec.loaded_ea == target`). Always pair a semantic verb with
- its own predicate where one exists.
-3. **Prompt / marker detection** *(black-box, structured-ish).* The screen returns
- to a known prompt regex, or the app emits a sentinel you injected (e.g. drive it
- to `echo <nonce>` and wait for `<nonce>` on screen). Robust when a prompt exists.
-4. **Output debounce** *(black-box fallback).* Snapshot the screen every ~20 ms;
- consider it settled once it's unchanged for N consecutive samples (e.g. 3× =
- ~60 ms) or a hard timeout. Flakiest; tune N per app; combine with (3).
-
-Factor settle into a shared helper that takes the *yield/poll* strategy and an
-optional predicate (idatui's `_sync.settle(app, pred)` is exactly this — reused by
-both the live driver and the test suite). That sharing is high-value: your tests
-and your driver then agree on "settled."
-
-### 5c. Introspection — "what is on screen / what is the state"
-Same best→worst gradient:
-1. **Native state** (embedded): read the app's model directly → richest snapshot.
-2. **Control-channel queries** (gdb: frames, registers, `-data-read-memory`,
- breakpoints as JSON) → structured, no scraping.
-3. **Screen scrape** (black-box): render the pane to a text grid and parse it.
- `tmux capture-pane -p -t <pane>` (add `-e` to keep colors) is the universal
- screen read. For a machine-parseable grid without a real terminal, feed the PTY
- stream through a headless emulator (e.g. **pyte**) and read its buffer.
-
-Always expose a raw `screen()` too (plain + colored). It's the agent's "look with
-your eyes" fallback and a great debugging aid (idatui added `format=text|html|svg`;
-`html` is nice to pipe to an out-of-band web viewer).
-
----
-
-## 6. tmux as the universal substrate
-
-For **Level 2/3** targets, don't own the PTY yourself — let **tmux** own it and
-drive through tmux. This collapses three problems into three commands and reuses
-the pane manager we already built:
-
-| need | tmux primitive |
-|------|----------------|
-| a visible pane running the target | `tmux split-window -P -F '#{pane_id}' '<cmd>'` |
-| inject keystrokes | `tmux send-keys -t <pane> …` (named keys, `-l` literal) |
-| read the screen | `tmux capture-pane -p [-e] -t <pane>` |
-| is the pane alive | `tmux list-panes -a -F '#{pane_id}'` |
-| tear down | `tmux kill-pane -t <pane>` |
-
-A black-box adapter can be built *entirely* on these four. The pane manager
-(`spawn/stop/list`, a small registry in `$XDG_RUNTIME_DIR`, backend auto-start,
-readiness polling) carries over unchanged — only "what counts as ready" and "what
-command to spawn" are per-target.
-
-Caveat: `capture-pane` gives you the *rendered* grid but not semantic structure;
-and `send-keys` is fire-and-forget (no completion signal) → you lean on §5b (3)/(4)
-for settle. That's the price of black-box.
-
----
-
-## 7. Reusable vs. per-target
-
-**Reuse verbatim** (target-independent):
-- Transport: unix socket, newline-delimited JSON, `{id,method,params}` →
- `{id,result|error}`; single-driver gate; per-request dispatch + error framing.
-- Pane manager: `spawn/stop/list`, registry, backend `_ensure_server`, readiness
- poll, graceful `quit` (answer then exit).
-- Client + ergonomic CLI: socket auto-resolution (single live pane), terse text
- output, composite verbs, `raw <method> k=v` passthrough, self-documenting
- `methods`.
-- The settle *shape* (`wait_for(pred, tick)` + `settle(pred?)`), even though the
- concrete signals differ.
-
-**Write per-target** (the adapter):
-- Input injection binding (app key API / control channel / `tmux send-keys`).
-- Settle signal (which of §5b applies).
-- `snapshot()` and `screen()` sources.
-- The semantic verb set (the app's real vocabulary).
-- `ready()` and the spawn command.
-
-Rule of thumb: **~80% reuse, ~20% adapter.** Keep the adapter interface narrow so
-that stays true.
-
----
-
-## 8. Worked example: **gdb**
-
-gdb is a *Level 2* target with a great control channel, so aim for a hybrid.
-
-**Layout.** A tmux pane runs gdb in TUI mode (`gdb -q -tui` / `layout src`,
-`layout asm`, `layout regs`) — that's what viewers see. Alongside it, a **gdb
-Python plugin** (loaded with `-x driver.py`, running inside gdb's own process)
-opens the unix socket and *is* the adapter. gdb's Python runs on gdb's thread, so
-handlers touch gdb state directly — the embedded pattern, for free, inside a
-program you didn't write.
-
-**Injection.**
-- *Semantic* verbs call the API directly (visible in the TUI because gdb echoes
- and repaints): `gdb.execute("break main", to_string=True)`, `-exec-run`,
- `-exec-next`, `-exec-continue`, `-exec-finish`. Or GDB/MI via a second channel
- if you prefer strict JSON.
-- *Raw* verbs (for TUI-only navigation: `C-x o` to switch windows, PgUp/PgDn in
- the source window, `C-x 2` layouts) go through **`tmux send-keys`** to the pane.
-
-**Settle.** Mostly *given*: `gdb.execute(..., to_string=True)` and MI commands are
-synchronous — they return when the command completed, so a semantic verb is
-settled the moment the call returns. For *async* execution (`-exec-continue` while
-the inferior runs), settle = wait for the next **stop event** (`gdb.events.stop`)
-or the MI `*stopped` async record. For raw `send-keys` TUI moves, fall back to
-`capture-pane` debounce (§5b-4).
-
-**Introspection** (all structured, no scraping):
-- `state()` → `{running|stopped, pc, function, file:line, thread, selected_frame}`
- from `gdb.selected_frame()`, `gdb.selected_thread()`.
-- `backtrace()` → walk `gdb.newest_frame()` → `[{level,pc,func,file,line,args}]`.
-- `regs()` → `frame.read_register(...)` for the ABI set.
-- `mem(addr,len)` → `gdb.selected_inferior().read_memory(...)` (hex/ascii).
-- `locals()`, `breakpoints()` (`gdb.breakpoints()` → JSON), `disas(addr?)`.
-- `screen()` → `tmux capture-pane -ep` of the TUI pane (the "as a viewer sees it"
- read), *plus* the structured reads above for reasoning.
-
-**Semantic verb set** (the gdb vocabulary): `run/start`, `cont`, `next`, `step`,
-`finish`, `until`, `break <loc>`, `tbreak`, `delete <n>`, `watch <expr>`,
-`bt [n]`, `frame <n>`, `up/down`, `print <expr>`, `x/<fmt> <addr>`, `set var`,
-`layout <src|asm|regs|split>`, `focus <win>`, `raw keys …`.
-
-**Spawn / readiness.** `spawn --bin ./prog [--args …]` → tmux pane runs
-`gdb -q -tui -x driver.py --args ./prog …`; **ready** when the plugin's socket is
-up *and* gdb reached its prompt (the plugin can signal readiness once loaded).
-`stop` = graceful `quit` verb (plugin calls `gdb.execute("quit")`) then kill-pane.
-
-This gives the same feel as idatui: `drive where` → `#3 main at foo.c:42`,
-`drive bt`, `drive break foo`, `drive cont`, `drive x/16xb $sp`, `drive screen` —
-terse, socket auto-resolved, every action visible in the gdb TUI pane.
-
----
-
-## 9. Method-surface conventions (keep these consistent across targets)
-
-- **Tiers, named the same everywhere:** `keys`/`text` (raw); `state`/`view`/
- `screen`/`<structured reads>` (introspection); `<semantic verbs>` (per target);
- `ping`/`methods`/`quit` (lifecycle).
-- **`ping`** returns `{ok, proto, ready, …}`; **`methods`** returns the verb table
- (self-documentation); **`quit`** answers *then* exits (so the reply flushes).
-- Mutating verbs **settle then return fresh `state`**. Read verbs never settle.
-- Program-dependent verbs return a clean **`error: not ready`** before init.
-- Typed verbs accept **`delay_ms`** (visible typing; `0` = fast). Movement verbs
- are fast (no delay, light settle).
-- Errors are data (`{id,error:{message}}`), never drop the connection.
-
-## 10. Client ergonomics (the part you use all day)
-
-- **Auto-resolve the socket**: if exactly one live pane, use it; else `--sock` /
- env; with several, list and ask. Kills the "copy the socket path everywhere"
- tax — the single biggest quality-of-life win.
-- **Terse text out**, not raw JSON (`where` → `main @ 0x… [src] L42`); keep a
- `raw <method> k=v` passthrough for the long tail.
-- **Composite gestures** for the common multi-step flows (idatui's
- `rename old new` = goto+cursor+type; gdb's `break-and-run`, `stepn N`).
-- Fire calls **sequentially** (single-driver); each invocation opens/closes its
- own connection.
-
-## 11. Security & operations
-
-- **Unix socket, mode 0600, local only.** No auth by design (whoever can r/w the
- socket drives it). Add a first-line shared token only if you bind TCP.
-- **Single-driver gate** prevents two clients interleaving mutations.
-- **Never leak panes/daemons.** Registry + `list --prune`; auto-start shared
- backends idempotently (probe before spawn; a port/socket already-in-use guards
- duplicates); don't kill shared backends on a per-pane `stop`.
-- Note the **stale-pane** failure mode: a pane can outlive its backend/session;
- `list` should check liveness, not just tmux presence.
-
-## 12. Onboarding a new target — checklist
-
-1. **Classify** it (§4): can you embed? does it have a control channel? else
- black-box.
-2. Pick the **injection** binding (§5a) and the **settle** signal (§5b) — decide
- this first; everything else is easy.
-3. Implement the **Adapter** (§3): `inject_keys/text`, `settle`, `snapshot`,
- `screen`, `ready`, `quit`, and the **semantic verbs** (the app's real
- vocabulary — don't invent; mirror what a power user types).
-4. Wire the **spawn command** + **readiness** into the pane manager.
-5. Reuse transport, client, and ergonomic CLI unchanged.
-6. Add a **regression test** that drives the surface end-to-end (idatui uses a
- headless Textual Pilot suite, `tests/test_scenarios.py`, sharing the settle
- logic with the live RPC server via `_sync.py`).
-
-## 13. Pitfalls & lessons (paid for once already)
-
-- **Settle is where the bugs live.** A verb that "passes" by timing out then
- reading stale state is the classic false-green (idatui hit this: a 25 s hang that
- a check passed *trivially*). Prefer a real signal/predicate; print per-op timing
- so a suddenly-slow verb (a hidden timeout) is visible.
-- **Newlines / control bytes** matter when injecting via a line-based reader —
- pick an escaping convention (idatui: literal `\n` → real newline at apply time).
-- **Black-box screen scraping is lossy** — no semantic structure, colors optional,
- non-deterministic chrome (a live clock makes frames differ). Crop chrome; prefer
- structured reads when a control channel exists.
-- **Match on what's shown, resolve by canonical id.** Names can render differently
- than they're stored (idatui: `.init_proc` shows as `init_proc`).
-- **Keep the app visibly driven.** If you ever bypass the UI for speed, gate it
- behind an explicit `fast:true` — the default should always render, because
- "shown as if a user did it" is the whole point.
-
----
-
-*Reference implementation: `idatui/{rpc,rpcclient,drive,pane,_sync}.py`,
-`docs/RPC.md`, `tests/test_scenarios.py` in this repo.*