| Commit message (Collapse) | Author | Files | Lines |
|
Two fixes to the comment verb:
Drop the per-character typing delay. Rename/retype/goto use a 35 ms delay for
the visual effect (the agent's keystrokes appear one by one on the livestream),
but comments can be long — a 200-char annotation blocked the driver for 7 s of
pure animation. Comments now type instantly (delay=0); the aesthetic delay is
kept for rename, retype and goto where values are short.
Escape literal newlines before injecting into the prompt. The Input widget is
single-line, so a real 0x0a sent as a keystroke was silently swallowed. The
app's _do_comment already converts the two-char sequence '\\n' into a real
newline for IDA, so the RPC layer now does text.replace('\\n', '\\\\n')
before typing — both literal newlines from the caller and explicit \\n in the
text reach IDA as multi-line comments (each line gets its own // prefix in the
decompiler).
Verified on a live pane: a comment with an embedded newline now renders as two
// lines in the pseudocode, and long comments appear without the multi-second
typing pause.
tests/test_scenarios.py: 212 passed, 0 failed.
|
|
goto/open ran `settle(app, pred, timeout)` and threw the result away. On a large
database the listing build routinely outruns the default 20 s, so the verb
returned a normal snapshot while the view had not moved. Every subsequent
rename/comment then applied to wherever the caller *used* to be.
Reproduced on a live pane against a 4 MB Go binary: `goto 0x1002019b0`
returned ok with the view still at 0x100001000, and the following
`rename main_inflate_zlib` renamed internal/abi.BoundsDecode instead — then the
rename's own snapshot showed `main_inflate @ 0x1002019b0`, because by the time
it was taken the goto had finally landed. Success reported, right-looking
readback, wrong function edited, and it survived a save.
This is what made an agent session stamp net_writeFull onto main_usage and
conclude the tooling was flaky.
goto/open now raise TimeoutError naming the target and where we actually are,
suggesting a larger timeout=. _press() does the same for follow/toggle_view/
hex/xrefs/structs, which had the identical "predicate ignored" shape.
tests/test_scenarios.py: 212 passed, 0 failed.
|
|
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 <addr>` 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 "'<' 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.
|
|
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.
|
|
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.
|
|
The app carried three overlapping ideas of the current pane: _active, _pref, and
Textual focus. 190e28b tied focus to _active in split; this removes _pref, which
turns out never to have been a variable at all.
_pref was assigned "listing" in __init__ and "listing" on a project binary
switch. Nothing else ever wrote it. But _code_view() branched on it:
return self.query_one(DecompView if self._pref == "decomp" else ListingView)
so it always returned the listing, whatever you were reading. Its two callers put
focus back after the goto prompt closes — so cancelling `g` while in the
pseudocode focused the HIDDEN listing, and the pane you were looking at stopped
answering the keyboard. Arrows did nothing until you clicked. (It also explains
why routing follow through _code_view() earlier made Enter a dead key: the helper
had been quietly lying the whole time.)
_code_view() now returns the active code pane. _pref is gone from the app,
BinaryState and the RPC snapshot keeps "pref" for wire compat, sourced from
_code_mode() — the one place that answers "which code view do we return to from
hex", and a constant by design in the unified layout.
Verified on a live pane both ways: g then Esc in pseudocode, then two Downs.
Fixed, the cursor moves 0 -> 2; with the old lookup restored it sits at 0 with
focus=ListingView. Same check added to the view_toggle scenario, and it fails
without the fix. 195/0, project UI 23/23.
|
|
ida-pro-mcp's xref_query only classifies xrefs as code/data (xr.iscode). Add an
injected `xref_types` tool (server/patch_server.py) that mirrors xref_query's
query/envelope shape but derives a fine `kind` from the IDA xref type:
call/jump/flow for code (fl_CF/CN/JF/JN/F), read/write/offset/text/info for data
(dr_R/W/O/T/I). The worker self-injects it on startup like the other custom tools.
* domain: Xref gains a `kind` field; _parse_xrefs reads it; xrefs_to() now calls
xref_types (falling back to xref_query if absent). xrefs_from is unchanged.
* app: the `x` dialog shows the kind as an aligned column after the address
(`000034F4 read sub_34F0+0x4`).
* rpc: the structured xrefs_to read carries `kind` too.
Verified live over the worker/RPC harness on targets/echo: sub_2C00 callers ->
call; __progname -> offset (GOT), read (sub_34F0), write (sub_3500); stdout ->
read/offset.
|
|
Full IDA-style unification. The function-bounded DisasmView is gone from the UI:
navigation opens ONE continuous segment listing (functions+data+undefined
interleaved) at the target; F5/Tab decompiles the function under the cursor and
back. The listing gained the opcode column (Head.raw, 'o' toggle) so rendering
matches disasm.
Routing: _do_navigate/_open_function/_open_entry target the listing;
_show_active drops disasm; _active in {listing,decomp,hex}; _pref=listing.
Decomp is a per-function toggle with a listing fallback on failure. Edits
reload in place (_reload_active_code); bump_names() drops the listing cache so
renames refresh. Listing 'n' is symbol-aware. ListingModel gains
cached_line/lines shims; Head.label. DisasmView removed from compose/handlers;
rpc updated. DisasmModel kept (domain/test_domain).
Tests migrated (c.dis->ListingView; open(decomp) F5s; xref/search re-pointed).
Per-scenario green across view_toggle/rename/follow_xrefs/xref_labels/decomp_*/
search/mouse/startup/continuous_view (30/1, 1 cold-decompile flake).
|
|
The real disassembly-listing view. ListingView is a line-virtualized
ScrollView (reusing ColumnCursor/SearchMixin/NavMixin) backed by
ListingModel, rendering code, data (db/dw/dd, strings, jump tables) and
undefined bytes interleaved with per-kind styling. Non-function regions now
open in it instead of the M0 DisasmModel stopgap.
Integration (IdaTui): a new `listing` active mode. _open_entry routes region
entries to ListingView; _show_active/compose add it; follow/xrefs/comment and
the c/p/u edit verbs handle it (define_func upgrades a region straight to the
function view); backslash reaches hex and returns via _code_mode (so leaving
hex from a region lands back on the listing, not a func view); Tab explains
there's no pseudocode for a region. rpc._active_widget learns `listing`.
Server fix (the important one): the `heads` walker now steps by get_item_end
instead of next_head. next_head SKIPS undefined bytes, so after undefining a
function its start address wasn't even a listing line and the cursor snapped to
the next defined item; stepping by item-end renders undefined bytes as `db ?`
lines (IDA-accurate) and makes navigation land exactly on any address —
essential for "go to unmarked bytes and hit c". Needs a supervisor restart.
Tests: region_define rewritten to assert the ListingView path + segment-wide
listing + p-upgrade; new listing_view scenario (data-segment listing renders
data heads, cursor reports head ea, backslash->hex->back). Pilot 115 pass / 1
pre-existing flaky (filter); rpc_smoke 29/0; test_domain 27/0.
|
|
spawn-hang + bare KeyError)
|
|
toggle_view's settle predicate (lambda: app._active != before) never
fired when tabbing toward pseudocode on a function Hex-Rays can't
decompile: App._apply_decomp snaps the view back to disasm, so _active
returns to its prior value -> full 20s settle timeout (x2 in _show_decomp,
~40s for drive pc). Recognize the decomp-failed fallback as settled.
Also harden two amplifiers surfaced by the same case:
- rpcclient: the CLI socket had no read timeout and would block forever
on any server slowness; add a bounded settimeout (IDATUI_RPC_TIMEOUT,
default 90s) with a clear error.
- domain.decompile: pass a bounded 15s timeout and cache failures, so a
failing decompile can't sit at the 30s client default or be re-run by
transport retries.
|
|
The clock/title Header added noise (and made screen() non-deterministic);
remove it from the app itself — the Footer stays. Layout shifts up a row;
scenario suite still 101 green.
xrefs_from on a function was near-useless: the server's xrefs_from is
address-scoped, so passing the entry ea only returned the fall-through
from the first instruction. For a function target it now returns
whole-body references from the decompiler (callees + string/data refs:
{to,name,string,is_func,type}); an explicit 0xADDR stays address-scoped.
Verified live: xrefs_from main now lists setlocale/getopt_long/sub_* etc.
rpc_smoke: 25 green.
|
|
cursor_on {word,line?,occurrence?} places the cursor on a token, verified
with the app's own tokenizer (so 'main' won't match inside 'domain');
rename/retype/follow gain an optional word= that runs it first — the
ergonomic way to edit a named symbol without a manual view+cursor dance.
Single-driver gate: the socket now serves one client at a time; a second
concurrent connection is refused with 'busy' (no multi-driver support yet,
by request). Sequential connections are unaffected.
screen gains format=text|html|svg (html/svg are colored — for an
out-of-band web viewer). rpc_smoke: 24 green.
|
|
Spawn-and-drive needs to know when the freshly-launched TUI is usable:
ping/state now carry {ready,functions,complete} (cheap; no health call)
and ping adds the module name. Add a graceful 'quit' verb (acks, then
exits on a short delay so the reply still flushes) and a self-documenting
'methods' table. Program-dependent verbs are refused with a clear
'not ready' instead of exploding in a worker while still loading.
rpc_smoke covers readiness, methods and quit (21 green).
|
|
Introspection (offloaded to a thread so a big fetch can't freeze the
render): pseudocode (full body), disassembly (bounded), xrefs_to/from
(structured), resolve (name->ea). These let an agent reason about a
function and the call graph without scraping screen().
Semantic: select (choose the highlighted/nth item in the open xrefs or
symbol-palette list and activate it — the natural follow-up to xrefs/
symbols), search (/ or ? incremental in the active code view), save
(Ctrl+S). rpc_smoke exercises all of them end-to-end (18 green).
|
|
Layer high-level verbs over the raw injection: goto/open, rename, comment,
retype, follow, back, toggle_view, hex, xrefs, symbols, structs, close,
plus fast move/cursor. Editing/goto verbs type through the real prompts
with a per-char delay (default 35ms) so viewers see it typed; movement
uses bare keypresses with a pump-only settle to stay snappy. Each verb
settles on an op-specific predicate where one exists (goto lands on the
target, follow grows the nav stack, toggle flips _active, modals open),
so the returned state is never stale.
rpc_smoke now drives the semantic path end-to-end (rename round-trip via
the prompt-fill seam), 11 green.
|
|
Run the TUI with --rpc <sock> and it renders normally while an asyncio
listener on the same loop lets another process drive it. Handlers touch
the UI directly (same loop, no thread hop), so every injected key has the
identical on-screen effect a keyboard would — the point for livestreaming.
Newline-delimited JSON. v1 methods: keys/text (raw injection, via the same
_press_keys the pilot uses; text can interleave wait:<ms> for a typed-out
look), state/view/screen/functions (introspection; screen() is a full
plain-text render of exactly what the viewer sees). No auth by design —
the socket is 0600, local only.
tests/rpc_smoke.py drives it end-to-end over a real socket (7 green).
|