<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git/idatui/pane.py, branch pre-codemode</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/idatui/pane.py?h=pre-codemode</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/idatui/pane.py?h=pre-codemode'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/'/>
<updated>2026-08-06T13:11:54Z</updated>
<entry>
<title>pane: drive panes under zellij as well as tmux</title>
<updated>2026-08-06T13:11:54Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-08-06T13:11:54Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=cad5a4f9f78c437e9ae80173118abb31df0954d4'/>
<id>urn:sha1:cad5a4f9f78c437e9ae80173118abb31df0954d4</id>
<content type='text'>
The multiplexer is auto-detected ($ZELLIJ then $TMUX) and every pane
command works the same under both. Pane ids are self-identifying, so a
mixed set of tmux and zellij panes can be tracked at once. zellij has no
-l, so --size is ignored there, it always focuses a new pane (--detached
is emulated), and it leaves an EXITED husk behind that stop/reap now
clear. The pane tests skip on neither multiplexer rather than on no TMUX.
</content>
</entry>
<entry>
<title>rpc: make a raw firmware image drivable (load options, define, bulk symbols)</title>
<updated>2026-08-01T15:18:57Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-01T15:18:57Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=14ca7c7e5c56c3fdd059d168957a2b2e3dbe504e'/>
<id>urn:sha1:14ca7c7e5c56c3fdd059d168957a2b2e3dbe504e</id>
<content type='text'>
Opening a headerless blob was the one workflow that fell out of the driving
surface entirely, and each gap hid the next:

- `pane spawn` couldn't pass --processor/--base/--ida-args, so the pane came up
  "ready" with zero functions (x86 at 0) and the only way through was to
  hand-write a project file. It now forwards them to idatui.launch.
- c/p/t/T (code, function, ARM&lt;-&gt;Thumb, vector scan) existed as listing
  bindings with no verb, so a driver had to guess raw keys -- and raw keys are
  swallowed by whatever modal happens to be up. `define {kind,target?}` goes
  through the app's own edit worker and reports what IDA actually did.
- every name went through the typed rename prompt: a navigation (listing page +
  decompile) plus two prompt round-trips each. A 427-symbol map took tens of
  minutes of driving. `rename_many {items|file}` hands IDA's rename tool the
  whole list in one call (371 symbols in 3s) and refreshes the caches and the
  function table once.

drive gains `define &lt;kind&gt; [target...]` and `syms &lt;file.json&gt;`.

Verified live against a real pane (tests/test_rawimage_rpc.py, 13 checks:
spawn load options, define thumb/func + unknown-kind rejection, rename_many
from a file and inline, with resolve/functions readback).
</content>
</entry>
<entry>
<title>rpc: stop lying to the driver about renames, modals and teardown</title>
<updated>2026-07-31T07:14:41Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-31T07:14:41Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=f720a16f8e4ac538e6b880960fd411357114656d'/>
<id>urn:sha1:f720a16f8e4ac538e6b880960fd411357114656d</id>
<content type='text'>
Five defects found while an agent drove a long RE session over the socket.
Each one was reproduced on a live spawned pane first (an in-process pilot
would not have shown any of them), then fixed:

pane stop truncated the save. `stop` asked the app to quit, slept 400 ms, then
unconditionally killed the pane. Quitting runs App.on_unmount, which writes
every dirty database; a 90 MB .i64 takes tens of seconds, so the kill landed
mid-write and a whole session's annotations went to /dev/null with a cheerful
{"stopped": [...]} on stdout. Now it waits for the pane to actually exit
(--timeout, default 600 s) and only force-kills on timeout, saying so.

The quit verb bypassed the dirty check. It called app.exit() directly rather
than the path a human gets, so the "unsaved changes" logic never ran. It now
routes through _on_quit_choice and reports {saving, dirty}.

Naming a function start from the listing never reached the function index.
`goto &lt;addr&gt;` puts the cursor on the address token, so `n` takes the
name-an-address path, which called bump_items() but left FunctionIndex holding
the old name. Result: the rename response snapshot showed the section label,
and functions()/names()/the palette all reported the rename had not happened —
so a driver that trusts its readbacks redoes work it already did. Twice, in
the session that prompted this. _do_name_addr now updates the index, the nav
stack and the table cell when the address is a function start.

A stripped binary with no entry function started up *inside a modal*.
_auto_land pushed the symbol palette when main() was missing, while ping still
answered ready:true. Every keystroke an RPC driver injected went into the
palette's search box and was silently swallowed. It now lands on the first
function instead and hints at Ctrl+N.

Verbs that inject keystrokes now refuse when a modal is on top, naming it,
instead of failing with "'goto' prompt did not open (word under cursor?)" —
a message that blamed the cursor for what was always a focus problem.

Also: `drive raw` passes k=v values through as strings, so `view lines=8` died
with "'&lt;' not supported between instances of 'int' and 'str'". Numeric params
are now coerced centrally rather than at each call site.

tests/test_scenarios.py: 212 passed, 0 failed.
</content>
</entry>
<entry>
<title>rpc: a `trace` verb, and --trace for pane spawn</title>
<updated>2026-07-27T20:03:08Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-27T20:03:08Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=a8d39781a7b03504556b5df6ff73ab2fd145efaf'/>
<id>urn:sha1:a8d39781a7b03504556b5df6ff73ab2fd145efaf</id>
<content type='text'>
Driving the trace viewer needed the same treatment as everything else: seeking by
hand through a few hundred keypresses to reach an interesting timestamp is not a
way to test it.

  trace {seek: 120}        absolute timestamp
  trace {seek: "!50"}      halfway through, like Tenet's timestamp shell
  trace {goto: "main"}     first execution of a name or 0xADDR
  trace {step: 20}         relative, negative goes back
  trace {step: 5, over: true}

Returns the usual state snapshot plus {idx, length, pc, changed} so a driver can
see where it landed and what that instruction wrote.

pane spawn --trace FILE passes it through to the launcher, so a trace pane is one
command.
</content>
</entry>
<entry>
<title>projects phase 4: cross-binary back, linkage-guided pre-warm, project verbs</title>
<updated>2026-07-25T21:03:10Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-25T21:03:10Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=b8d9aa4fce78f4523cffaba8b46095d2dfb66310'/>
<id>urn:sha1:b8d9aa4fce78f4523cffaba8b46095d2dfb66310</id>
<content type='text'>
Three things, all following from phase 3 making cross-binary jumps ordinary.

**A cross-binary jump was a one-way door.** Nav history is per-binary, so
arriving in another binary — a project search hit, or now following an import
into the library that implements it — landed you in an empty history with nothing
to take you back. _switch_then_goto records the binary it came FROM, and
action_back falls through to that hop once local history is spent: Esc walks back
through the function you were in, then the binary you were in. Manual Ctrl+O
switching records nothing, because that isn't navigation.

**Pre-warm follows the linkage graph, not list order.** _prewarm_provider warms
the binary providing the most of this one's imports — where a follow is most
likely to go, so its startup is paid before you ask for it. "Next in the list"
would have been arbitrary; phase 3 gave us something better to ask.

pool.prewarm() refuses rather than making room. Evicting a binary the user
visited to speculatively load one they haven't is a straight downgrade, and it
throws away that binary's caches as well; at a tight budget pre-warm just does
nothing. The cost of a worker that doesn't exist yet can only be estimated, so it
uses the largest resident one (same program, different database) — and if that
estimate proves wrong, the speculative worker is the one evicted, never a chosen
one.

**Driving a project.** pane spawn --project FILE [--open BIN]; `binaries` lists
the inventory (active / resident / indexed / where Esc returns to) and `switch
{binary,addr?}` makes another active — with an address it takes the search-hit
path, so it records a hop. state gains `binary` and `hops`, which it should have
had the moment project mode existed.

Verified on real sessions: drive binaries/switch against an echo+cat project
pane; Esc crossing back from a switch; and prewarm on echo+libc picking libc
(provider of echo's imports) and warming it after an evict.

tests: +5 pool (prewarm warms, no-ops when resident, refuses at budget, evicts
nothing when refusing, ignores unknown labels) and +4 project UI (jump records
the hop, Esc crosses back, hop consumed). Confirmed the Esc-back checks fail with
the branch removed. 195/0 scenarios, 27/0 project UI, 36/0 index, 27/0 pool,
33/0 project.

Left open: project-level persistence across sessions.
</content>
</entry>
<entry>
<title>mcp: delete the ida-pro-mcp transport, supervisor, and mcp-only tests</title>
<updated>2026-07-24T12:53:56Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-24T12:53:56Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=22ffea91210610d1def042e230ffe8d223022b30'/>
<id>urn:sha1:22ffea91210610d1def042e230ffe8d223022b30</id>
<content type='text'>
The idalib worker is the only backend now, so remove the dead HTTP/supervisor
surface entirely (~2200 lines):

* deleted idatui/client.py (the IDAClient HTTP/JSON-RPC transport + session
  manager), idatui/tui.py (the old mcp TUI entry, superseded by launch.py),
  spawn.sh, and systemd/ (the supervisor unit).
* deleted the mcp-only tests (stress_client, smoke_client, test_keepalive,
  stress_paging, rpc_smoke, serverctl.sh, pane_smoke, test_domain) -- the worker
  pilot (tests/test_scenarios.py) supersedes them.
* migrated the tmux RPC harness (idatui/pane.py) to the worker: it spawns
  `idatui.launch &lt;binary&gt; --rpc &lt;sock&gt;` instead of the mcp `idatui.tui`, drops
  the supervisor auto-start/ensure machinery, and reaps our own worker
  (idatui/worker.py) instead of ida_pro_mcp.idalib_server. --db/--url/--no-
  ensure-server are gone; --open is required.
* __init__ / __main__ / domain no longer import client (exceptions come from
  errors.py, the domain client hint is WorkerClient); pyproject points both
  console scripts at idatui.launch; README + ida-tui header describe the
  worker-only flow.

What stays (by design): the ida_pro_mcp *package* (the worker reuses its @tool
functions in-process) and server/patch_server.py (the worker injects its custom
tools on startup). Verified: whole package imports + IdaTui constructs + pilot
lists 31 scenarios. The worker pilot (134 pass / 2 known flakes) is the E2E gate.
</content>
</entry>
<entry>
<title>pane: reap leaked idalib workers; drive: friendlier name resolution (fixes spawn-hang + bare KeyError)</title>
<updated>2026-07-12T13:37:31Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-12T13:37:31Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=6b744031b24cc0d8257b1365c3197c9cf0252be9'/>
<id>urn:sha1:6b744031b24cc0d8257b1365c3197c9cf0252be9</id>
<content type='text'>
</content>
</entry>
<entry>
<title>pane: auto-start the ida-pro-mcp supervisor if it's down</title>
<updated>2026-07-10T13:50:30Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-10T13:50:30Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=23a63681a61c2b522576d77dfd140f2174273a95'/>
<id>urn:sha1:23a63681a61c2b522576d77dfd140f2174273a95</id>
<content type='text'>
Before creating the TUI pane, spawn probes 127.0.0.1:8745 and, if nothing
is listening, launches ./spawn.sh in its own detached tmux pane and waits
for the port (server_started/server_pane are reported in the JSON). Only
for a local server, never a duplicate (spawn.sh binds the port), and it's
not killed on 'stop' (shared across TUIs). --no-ensure-server opts out;
IDATUI_SERVER_CMD overrides the launch command (used by the test).

tests/pane_smoke.py exercises the machinery with a dummy port-binder in a
pane (start / detect / idempotent / remote-guard), 5 green.
</content>
</entry>
<entry>
<title>pane: spawn/stop/list idatui TUI panes in tmux for agent-driven RE</title>
<updated>2026-07-10T13:37:45Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-10T13:37:45Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=d28cec909cc52ae4f08aaf9ec3b60f1cca306d52'/>
<id>urn:sha1:d28cec909cc52ae4f08aaf9ec3b60f1cca306d52</id>
<content type='text'>
python -m idatui.pane spawn --open &lt;bin&gt;  (or --db &lt;session&gt;) splits a
tmux pane running the TUI on a fresh RPC socket, blocks until it reports
ready (ping.ready; --open runs full auto-analysis so the default wait is
generous), and prints {sock,pane,ready,functions,module} as JSON. The
agent then drives it via idatui.rpcclient and tears it down with
'stop --sock &lt;s&gt;' (graceful quit -&gt; kill-pane -&gt; unlink). A small
registry in $XDG_RUNTIME_DIR tracks panes for 'list'/'stop'.

Verified end-to-end: spawn -&gt; goto/pseudocode -&gt; list -&gt; stop.
</content>
</entry>
</feed>
