summaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
* fix: Tab back from a decomp jump returns to the listing cleanlyblasty2026-07-231-2/+11
| | | | | | | | | | | | | | | | | | | | | | | | Regression from the decomp-jump feature: _open_decomp_entry clears _decomp_return, so Tab out of a jumped-to pseudocode view hit the fallback that just re-showed the (stale) listing widget WITHOUT reopening it and left _cur.view == "decomp". Two visible symptoms: * the listing showed the wrong/old function, and * a subsequent rename ran _reload_active_code -> _open_entry(cur), which saw view == "decomp" and bounced back into the decompiler; the rename also appeared not to propagate (you were looking at stale pseudocode). Fixes: * Tab from decomp with no F5 snapshot now opens the listing at the current pseudocode line's address (a real listing view, _cur.view = "listing"), so the correct function shows. * _reload_active_code forces _cur.view = "listing" when refreshing the listing, keeping the entry consistent with what's on screen. Verified: F5 -> follow ref in decomp -> Tab -> listing shows the TARGET function; renaming a label there stays in the listing and reflects the new name (RELABELED: and 'jnz RELABELED'). view_toggle/decomp_nav/decomp_follow_self/ follow_xrefs/xref_labels/rename/rename_history/disasm_nav/search/mouse/ listing_view/continuous_view/func_banners 70/0.
* nav: jumping from the decompiler stays in the decompilerblasty2026-07-231-8/+61
| | | | | | | | | | | | | | | | | | | | Following a reference or picking an xref target from the pseudocode dropped you back into the linear listing. Now such jumps stay in the decompiler when the target is a decompilable function, landing on the pseudocode line that matches the target address; non-decompilable targets still fall back to the listing. * NavEntry gains a `view` field ("listing"/"decomp"); _open_entry restores an entry in whichever view it was seen in (so back/forward also keep pseudocode). * _do_navigate/_goto_ea take prefer_decomp; when set and the target function decompiles, _open_decomp_entry opens it in the decompiler as a real nav-history push (snapshotting the source pseudocode position so 'back' returns to it). * _follow_decomp and _on_xref_chosen pass prefer_decomp when the source view is the decompiler. Verified: follow a ref from decomp -> lands in the target's pseudocode; 'back' returns to the source pseudocode; listing follow still lands in the listing. decomp_nav/decomp_follow_self/follow_xrefs/xref_labels/view_toggle/disasm_nav/ search/mouse/listing_view/func_banners/continuous_view 62/0.
* listing: on-screen jumps just move the cursor, don't re-scrollblasty2026-07-231-4/+10
| | | | | | | | | | | | Extend the jump-context behaviour: if the target is already visible in the current viewport (same listing model, cursor index within [top, top+height)), leave the scroll offset untouched and only move the cursor to the target. Only off-screen jumps re-scroll (target parked _JUMP_CONTEXT lines below the top). Verified: an in-viewport jump keeps scroll_top constant while moving the cursor onto the target; off-screen jumps still get the 4-line context margin. scroll_restore/disasm_nav/follow_xrefs/xref_labels/mouse/search/listing_view/ continuous_view/func_banners 54/0.
* listing: keep a few lines of context above a jump targetblasty2026-07-231-0/+6
| | | | | | | | | | | | | | | Jumping (goto/follow/xref/open) parked the target's first line on the very top row of the viewport, which is cramped. Now a fresh navigation scrolls so the target sits _JUMP_CONTEXT (4) lines down, leaving surrounding context above it; the cursor still lands on the target's first instruction. Only applies when the scroll is being derived (NavEntry.scroll_y == -1). Back/ forward restores carry their own saved scroll_y, so they still return to the exact previous viewport. Verified: jumps to sub_3720/sub_2060/main land the cursor on the entry with a 4-line margin above; scroll_restore/disasm_nav/follow_xrefs/mouse/search/ listing_view/continuous_view/func_banners 51/0.
* fix: follow/double-click a label lands on the label, not the function entryblasty2026-07-232-2/+33
| | | | | | | | | | | | | | | | | resolve(name) went through lookup_funcs, which for a mid-function label (loc_/locret_) does get_name_ea -> label ea, then get_function(ea) and reports the CONTAINING function's entry address. So double-clicking (or following) a label jumped to the top of the function instead of the label's own address. Add a server tool resolve_names that returns the address a name actually denotes via idaapi.get_name_ea (functions, labels and data alike), and route Program.resolve() through it, keeping lookup_funcs only as a fallback for the "did you mean ..." suggestion on an unknown name. Verified: resolve('loc_2040') -> 0x2040 (was the function entry); double-click follow on a label row lands on the label ea. Regression sweep follow_xrefs/xref_labels/rename/rename_history/decomp_follow_self/search/ listing_view/disasm_nav/mouse 50/0. Needs a supervisor restart (new tool).
* fix: don't let a late navigation steal focus from an open promptblasty2026-07-232-4/+28
| | | | | | | | | | | | | | | | | | | | | | | | Real, data-loss-capable bug: opening a function that is already the current one (e.g. after scrolling away from it) schedules an async re-navigation. If the user opens the search prompt ('/') before that navigation lands, the navigation's _show_active() called lst.focus() and yanked focus OUT of the search box. The next typed characters then routed to the listing as verbs -- and 'u' = undefine, silently deleting a function. (This is what produced the flaky "search finds nothing" + "no function for <ea>" cascade in the pilot.) Fix: _show_active() no longer grabs focus for the code view while any prompt overlay (search/rename/comment/retype/goto/func-filter) is visible -- those own the keyboard until dismissed. View visibility is unchanged; only the focus grab is suppressed. Also harden the pilot's open(): wait for the listing cursor to actually LAND on the target address, not merely for _cur.ea to match (which is stale-true when re-opening the same function), so tests can't proceed on a not-yet-moved cursor. Verified: the exact failing order disasm_nav->search->follow_xrefs is green; broad focus-sensitive sweep (startup/disasm_nav/view_toggle/hex/search/rename/ comment_func/retype/follow_xrefs/decomp_nav/mouse/listing*/continuous_view/ func_banners/region_define) 89/0.
* listing: restore full-segment search load (find every match)blasty2026-07-231-5/+29
| | | | | | | | | | | | | | | | | | Revert the "search only the loaded portion" shortcut from f5aaf50 and go back to a single, guarded background load_all before matching, so a search finds every occurrence in the segment (not just whatever streamed in so far). The guard (_search_loading + queued callbacks) shares ONE load across per-keystroke updates instead of spawning/cancelling a worker each keypress. The wedge that shortcut was meant to avoid turned out to be unrelated to the search load: it is a pre-existing navigation bug (opening a function right after scrolling to the bottom of another leaves the listing cursor out of range and '/' fails to focus the search input, so typed chars leak into the view as listing verbs, one of which is 'u'=undefine). Reproduces at 0c097f5, before any of this work; tracked separately. Verified in isolation: search 3.1s, matches found; search/mouse/func_banners/ listing_view/continuous_view/decomp_nav/follow_xrefs 45/0.
* views: highlight all occurrences of the token under the cursorblasty2026-07-231-1/+51
| | | | | | | | | | | | | | | | | | | Editor-style highlight-all: whenever the cursor lands on an identifier, every other occurrence of that same whole-word token is highlighted across the visible listing AND the decompiler view (IDA's identifier-highlight behaviour). * new _word_occurrences() finds whole-word spans in a line (reuses the same char==cell alignment search already relies on). * ColumnCursor gains _hl_word + _refresh_hl(): after any cursor move (h/l, w/b, 0/eol, j/k, mouse click, search jump) it recomputes the token under the cursor and repaints the viewport only when it changed. Tokens must be >=2 chars and start with a letter/underscore. * ListingView.render_line and DecompView.render_line overlay every occurrence with the existing _S_WORD background; the block cursor still marks the current. Verified: cursor on 'rbp' highlights 5 visible listing rows; cursor on 'v23' highlights it across the decompiler; mouse/disasm_nav/decomp_nav/listing_view/ func_banners/follow_xrefs/continuous_view/decomp_follow_self 42/0.
* listing: code labels on their own line; search loaded portion (no wedge)blasty2026-07-233-19/+24
| | | | | | | | | | | | | | | | | Code labels (loc_XXX/jump targets) get their OWN line at depth 0, like IDA, instead of inline with the instruction: the heads walker (annotate) emits a kind=label row ('loc_XXX:') before a named non-function-start code head and strips the name from the instruction. ListingModel doesn't index label rows (goto/xref/follow still land on the code); ListingView renders them at depth 0. Also fix a search regression the extra rows exposed: search no longer forces a blocking full-segment load_all (which stalled/thrashed the worker and wedged the session on the larger annotated listing). It searches the loaded portion; the background grower streams the rest and unloaded lines are skipped until they arrive. Verified: labels on their own line; search 2.9s (was 51.9s+wedge); search/mouse/ rename_history/continuous_view/func_banners/follow_xrefs 26/0.
* listing: two-level indent, address on headers, drop 'near', opcode cap+cycleblasty2026-07-233-27/+53
| | | | | | | | | | | | | | | Polish per feedback: proc header drops the meaningless 'near' ('name proc'); the function-name/proc header now shows the address like every line; two-level indent (function names at depth 0, opcode bytes + instruction text one level deeper, matching IDA's label/instruction columns); 'o' now CYCLES the opcode column off->limited->full and is shown in the footer; 'limited' mode caps long runs at the first 8 bytes + ellipsis so 15-byte x86-64 insns don't blow out the column (width capped to match). _line_plain/render_line share the layout so cursor/search stay aligned. Verified func_banners+disasm_nav/mouse/region_define/listing_view/ listing_struct_expand/listing_name_addr/search/continuous_view green. Needs a supervisor restart.
* listing: IDA-style function boundary banners in the unified viewblasty2026-07-234-3/+77
| | | | | | | | | | | | | | | | | Each function gets clear boundary annotations in the continuous listing: a blank + '; ==== S U B R O U T I N E ====' separator and a 'name proc near' header before the entry, and a 'name endp' + rule after the last item. The function name moves to the proc header (stripped from the inline entry instruction). Server: heads gains an annotate flag (default off, so DisasmModel/test_domain stay 1:1 with instructions); when on, _rows_for emits kind=sep/funchdr rows at function starts/ends. ListingModel requests annotate=true and does NOT index banner rows by ea, so goto/xref/follow land on real code. ListingView renders sep (dim) and funchdr (bold gold); new _S_SEP/_S_FUNCHDR styles. Verified: func_banners 5/0; region_define/listing_view/listing_name_addr/ listing_struct_expand/continuous_view/follow_xrefs/disasm_nav/scroll_restore/ paging green. Needs a supervisor restart (heads tool changed).
* unify: the continuous listing is the one code view; deprecate DisasmViewblasty2026-07-234-286/+227
| | | | | | | | | | | | | | | | | | | | 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).
* listing: F5/Tab decompiles the function under the cursor (IDA-style) — step 2blasty2026-07-232-9/+76
| | | | | | | | | | | In the continuous listing, F5 (or Tab) decompiles the defined function the cursor is inside; F5/Tab again returns to the linear listing exactly where you left. Not-in-a-function -> a message; decompile failure falls back to the listing (not the bounded disasm). Listing position is snapshotted so the round-trip is lossless. Verified: continuous_view F5 round-trip + view_toggle/disasm_nav/follow_xrefs/ region_define/listing_view 45/0.
* listing: render opcode bytes (shared engine with disasm) — M4/UX step 1blasty2026-07-233-9/+96
| | | | | | | | | | | | | | | The flat listing was a subset of the disasm view (no opcodes). Now the listing carries an opcode-bytes column with the same format + o toggle: Head gains a raw field, ListingModel fills it for code heads via one bulk read per page (bounded, variable-length safe) + tracks the widest opcode; ListingView renders/pads the column, settling width as pages stream in. render_line/_line_plain share the addr+opcodes+label+mnem/text layout with DisasmView. Test harness all_funcs() reloads the index if a prior bump_items() cleared it (was crashing biggest() with max([])). Verified: code heads carry raw (max_raw_len 11); listing suite + continuous_view opcode-parity 26/0.
* app: 'L' opens the continuous segment listing (functions+data interleaved)blasty2026-07-232-0/+65
| | | | | | | | | | | | Function views are bounded to one function; 'L' from any code view opens the whole-segment ListingView at the cursor instead -- one long flat listing where functions/data/undefined interleave and you scroll straight from one function into the next. Reuses the region listing path (_goto_continuous). New continuous_view scenario: open a function, press L, assert continuous listing opens, cursor at the origin function, listing spans past the function bounds, code interleaves with data. 22/0 with disasm_nav/view_toggle/ region_define (L is additive).
* unify: back the function disasm view on the heads listing walker (M4 item 4)blasty2026-07-232-13/+29
| | | | | | | | | | | | | | | The function disasm view is now a filtered listing: DisasmModel sources its lines from the heads walker bounded to [func.start, func.end) instead of the disasm tool, so both code views render from the one listing mechanism. DisasmView UI (opcode bytes, o-toggle, Tab, rename) preserved -> no test churn. total() stays on disasm include_total: paging heads for the count hit the MCP response-size limit (count=1000 truncated -> wrong count) and corrupted prime. include_total is one fast exact call; for a code function it equals the heads row count backing the lines. _fetch_block uses count=257 (safe). Verified: disasm-heavy scenarios 56/0; full suite 127/1 (1 = flaky filter). M4 complete.
* listing: expand struct-typed globals into member rows (M4 item 3)blasty2026-07-233-5/+94
| | | | | | | | | | | A struct-typed data item rendered as one dense 'MyS <...>' line. Now the heads walker emits indented member rows after the summary (from get_udt_details): +off name type, each a head of kind member at ea+offset. ListingView renders them indented/dimmed; _line_plain matches for cursor/search. Verified: a struct global expands to +0 a int / +4 b char[4] / +8 c __int16, member rows carry field addresses. New listing_struct_expand scenario; listing suite 22/0 (in-process).
* TODO: M4 progress — string auto-detect + streaming done; ↵blasty2026-07-231-3/+11
| | | | struct-expand/unify deferred
* perf: stream the listing incrementally instead of load_all on open (M4)blasty2026-07-232-5/+60
| | | | | | | | | | | | | | Opening a listing walked the WHOLE segment (load_all) before showing anything -- a blank pane for seconds on a big .text. Now _prime loads just the viewport around the cursor (appears instantly) and _grow streams the rest in the background, growing virtual_size as pages land (throttled). Search finishes loading first (needs the whole segment). ListingModel gains a _load_lock so the grower and an in-view search can both drive page loads without double-fetching; new public load_next_page. Verified: ensure(1100)->partial(1500,incomplete), load_all->full(5036,complete); 3 concurrent loaders -> 0 duplicate heads; UI listing+search 30/0 (in-process).
* app: 'a' — make string literal in the listing (M4 richness)blasty2026-07-234-1/+102
| | | | | | | | | | | | IDA's 'A' key. New make_string server tool (ida_bytes.create_strlit, auto-length to the terminator, undefines items in the way first, returns size + decoded contents). Program.make_string wraps it; 'a' on a listing head routes through the existing edit plumbing (EditItemRequested kind=string -> _do_edit_item -> make_string -> bump_items -> reopen). Verified: make_string at a .rodata addr creates the literal and IDA auto-names it (aUsage for 'usage'). New listing_make_string scenario drives it through the UI; listing suite 15/0. Needs a supervisor restart.
* perf: derive segment map from file_regions, not survey_binary (hex open ~3400x)blasty2026-07-232-25/+47
| | | | | | | | | | | | | | | | | | The slow hex load wasn't the byte reads (read_raw fixed those) -- it was opening the pane at all: hex_model -> image_range -> sections called survey_binary, which computes function counts/strings/stats and takes ~24s on libcrypto, blocking the first hex open (and section_of/segment_bounds/ region_label in the listing). sections/file_regions/image_range now share one cheap _segments source backed by the injected file_regions tool (a plain segment walk, ~7ms), which gains a name field so it fully replaces survey_binary for the segment map. Falls back to survey_binary only if the tool is missing. Measured (libcrypto): segment map 24240ms -> 7.1ms (~3400x); hex open goes from ~24s to ~11ms. Correctness verified (names, section_of/segment_bounds/ file_offset); hex+listing scenarios 24/0, full suite 123/1 flaky. Needs a supervisor restart.
* app: listing 'n' names the address (can name a bare/undefined byte)blasty2026-07-232-0/+123
| | | | | | | | | | | | | | | | Repro: shrink a u16 data field to u8 in the listing; the freed byte at addr+1 becomes undefined, but you couldn't name it. 'n' posted RenameRequested with word_under_cursor(), and _do_rename treats that as an existing symbol to rename -- a 'db ?' line has no symbol, so it failed. In the ListingView, 'n' now names the ADDRESS under the cursor: opens the name prompt prefilled with the head's current name (empty for an unnamed byte) and applies it via rename {data:{addr,new}} (the server's address form of the data rename, which set_name's the ea even when nothing is defined there), then bump_items + reopen so the label shows. Code-view symbol rename is unchanged. Server already supported it; new listing_name_addr scenario reproduces the full workflow. Full suite 123 pass / 1 pre-existing flaky (filter).
* app: on startup, land on main() (else pop the symbol picker)blasty2026-07-232-3/+70
| | | | | | | | | | | | | | | Instead of staring at an empty pane after the function list loads, jump somewhere useful: once the index is complete and nothing's open yet, open main() (tries main/_main/wmain/WinMain/wWinMain in order); if there's no entry function, pop the fuzzy symbol picker so you can pick one. Fires exactly once (guarded by _cur + _did_auto_land) and never steals focus from a user who already navigated during load. Tests: new auto_land scenario covers both branches (jump-to-main when present, picker fallback otherwise) + the idempotency guard. Boot/startup now key "load complete" off _func_index.complete instead of the status string (auto-land legitimately overwrites the "N functions" status with the landed fn). Full suite 120 pass / 1 pre-existing flaky (filter).
* fix: crash on an unnamed function (None name) in the symbol paletteblasty2026-07-232-1/+10
| | | | | | | | | | | | | | | | `Ctrl+N` then typing crashed with `AttributeError: 'NoneType' object has no attribute 'lower'` when a function had no name: Func.from_raw took `d["name"]` verbatim, so a server-returned null/missing name became None and blew up _fuzzy (and would break sort/rename-prefill too). Two-layer fix: * Func.from_raw synthesizes IDA's `sub_<ADDR>` for a null/empty/missing name (and tolerates a missing size) so `name` is always a str for every consumer; and * _fuzzy guards against a falsy name defensively. Verified: Func.from_raw({addr, name:null}) -> "sub_1000"; _fuzzy(None,'m') -> None (no raise). Pilot palette + startup 9/0.
* perf: read_raw tool — bulk byte reads for the hex view (5–8x)blasty2026-07-232-2/+62
| | | | | | | | | | | | | | | | | | | | | | The hex view's lazy-load was dominated by the byte-read path, which was slow on two axes: * server: get_bytes uses read_bytes_bss_safe, a per-byte loop (is_loaded + get_byte = 2 IDA calls/byte → ~8k calls for a 4KB block), and encodes the result as "0x00 0x01 ..." text (~5x wire bloat); * client: read_bytes re-parsed that with int(tok,16) per byte; * and get_bytes silently TRUNCATES ≥16KB responses to "0x..." (wrong data). New injected `read_raw` tool does a single bulk ida_bytes.get_bytes (C-speed) and only re-checks is_loaded for the sparse 0xFF bss-sentinel bytes, returning one contiguous hex string. Client decodes with bytes.fromhex (C-speed). domain read_bytes uses it with a transparent fallback to get_bytes on older servers (cached _no_read_raw flag). HEX_BLOCK 4096→16384 now that big blocks are cheap and no longer truncate → 4x fewer round-trips while scrolling. Measured (echo, warm): 4KB 22.7ms→4.7ms (4.9x), 8KB 51.6ms→6.6ms (7.8x), and 16KB works (15.7ms) where get_bytes truncates outright. Byte-exact match vs the old path; disasm opcode bytes (also read_bytes) benefit too. Pilot hex + disasm_nav 12/0. Needs a supervisor restart (new server tool).
* launch: `ida-tui foo.elf` one-shot wrapper (server + locks + session)blasty2026-07-235-0/+280
| | | | | | | | | | | | | | | | | | | | | | | | | A caveman entry point so you don't hand-craft the plumbing every time. It: * ensures the ida-pro-mcp supervisor is up — starts spawn.sh detached (start_new_session, tmux-free) and waits for the port if it's down; * recovers a binary wedged by a hard-killed worker — sweeps the stale unpacked .id0/.id1/.id2/.nam/.til next to the .i64 and retries (the packed .i64 is never touched); * adopts an already-open session for the same binary (idempotent), else idb_opens it with a sane idle-TTL; * launches the TUI attached to that session with keepalive on. ./ida-tui /path/to/binary # open a binary and drive it ./ida-tui # attach to the sole session ./ida-tui --db <id> # attach to a specific session Pieces: idatui/launch.py (logic, reuses pane.py's server probe), a repo-root `ida-tui` sh wrapper (resolves ~/ida-venv python, keeps idatui importable from any cwd), and an `ida-tui` console-script in pyproject. --help works without textual (app imported late). README documents both the one-liner and the manual recovery. gitignore bin/ (binary targets, like targets/). Verified live: ensure_server up-detection, open, adopt (same session id), and lock-sweep all work against a running supervisor.
* app: 'd' — define typed data in the listing (make_data, M3)blasty2026-07-224-3/+144
| | | | | | | | | | | | | | Press 'd' on a listing head to define typed data at that address via a prompt (the ".data type definitions" backlog item). Mirrors the retype prompt flow: MakeDataRequested -> #makedata Input (prefilled with a size-appropriate default type: unsigned __int8/16/32/64 or char[N]) -> _do_make_data worker -> Program.make_data -> bump_items -> reopen the listing in place. Accepts any C type IDA's SetType understands: int, char[16], my_struct, T *arr[4], ... Pilot: listing_view gains a deterministic sub-test — undefine a data head to synthesize an unknown run, assert it renders as `unknown`, then 'd' char[4] over it and assert it becomes a `data` head. 118 pass / 1 pre-existing flaky (filter); rpc_smoke 29/0.
* server: coalesce undefined byte runs in the heads walkerblasty2026-07-221-6/+41
| | | | | | | | | | | | | | | | The M2 fix (step by get_item_end so undefined bytes render and arbitrary addresses land exactly) emits one row per undefined byte, which would explode a large .bss/undefined region into millions of rows and make load_all crawl. Collapse a run of consecutive undefined bytes into a single `db N dup(?)` row (its end found in O(1) via next_head, which skips undefined). A single stray undefined byte still renders normally (shows its value), so exact-address navigation and the c/p/u workflow are unchanged: a 42-byte gap is one row at the run start, and 'p' there auto-analyzes the function. Verified: undefining a function yields one `db 42 dup(?)` row (not 42); echo .text stays 5036 heads; data segments unaffected. region_define + listing_view + test_domain[listing] all green. Needs a supervisor restart.
* app: ListingView — virtualized flat code+data listing for regions (M2)blasty2026-07-225-37/+378
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* server+domain: heads walker + ListingModel — flat code/data listing (M1)blasty2026-07-223-1/+299
| | | | | | | | | | | | | | | | | | | | | | | | | | | The keystone for a real disassembly-listing view (unlike DisasmModel, which is one function, code-only). Two pieces: server/patch_server.py: inject a `heads` tool. It walks item heads over a segment with next_head/prev_head and renders each via generate_disasm_line, so it returns a flat listing where code, data (db/dw/dd, strings, jump tables) and undefined bytes all appear as typed rows {ea,kind,size,text,name}. Unlike `disasm` (code-only, bails at the first data byte) it shows the whole segment. Address-paged: chain forward via cursor.next, page up with back=true (returns the N heads ending before addr, in forward order, + cursor.prev). domain.py: ListingModel — a lazily-grown, segment-scoped head index (FunctionIndex-style forward paging via cursor.next; line index == position in the walked list). ensure_ea() gives random access to an address (resolving a mid-item byte to its containing head). New Head dataclass, Program.listing() (cached per segment) + segment_bounds(); section_of() now derives from it; bump_items() clears the listing cache too. Verified live: heads renders strings/jump-tables/unknown correctly, forward chaining + back-paging work; ListingModel walks echo .text (5036 heads, ~276ms) with cached windows and mid-item address resolution. tests/test_domain gains a [listing] section (27 passed). Pilot hex/region_define/disasm_nav green. Note: adding a server tool needs a supervisor restart (workers respawn).
* app: non-function regions — open a flat listing + c/p/u edit verbs (M0)blasty2026-07-224-6/+200
| | | | | | | | | | | | | | | | | | | | | | | | | Navigating to an address not inside a function no longer refuses ("no function contains X"): _do_navigate now opens a region view (flat listing anchored at the EA, forced to disasm since there's no pseudocode). The server's no-function disasm already walks heads to the segment end, so DisasmModel is reused as-is; NavEntry gains is_region. Adds the IDA c/p/u structure-edit verbs on the disasm view: c = define_code (undefine-first, since create_insn won't carve a live item) p = define_func u = undefine wired via EditItemRequested -> _do_edit_item worker -> Program.define_code/ define_func/undefine. define_func upgrades the region to a real function view in place. New Program.bump_items() invalidates the item/function structure caches (disasm blocks, decomp, function indices + name gen) — broader than bump_names(), which only covers renames. Pilot: new `region_define` scenario undefines a small function, navigates to the bare region via the 'g' prompt (asserts it opens, not refused), then 'p' recreates the function and upgrades the view; restores the IDB in a finally. 107 passed / 1 pre-existing flaky (filter, fails on baseline).
* pane: reap leaked idalib workers; drive: friendlier name resolution (fixes ↵blasty2026-07-123-6/+143
| | | | spawn-hang + bare KeyError)
* TODO: opcodes in disas view doneblasty2026-07-111-1/+1
|
* disasm: render opcode bytes (toggle 'o'), padded to widest insnblasty2026-07-116-10/+227
| | | | | | | | | | | | | | Fetch per-instruction opcode bytes alongside the disasm listing and show them in a column between the address and the mnemonic. Instruction length comes from consecutive addresses (variable-length safe: x86 movabs shows its full 10 bytes); the block over-fetches one instruction for the last line's boundary, falling back to the function end for the final insn. The column pads to the widest instruction across the whole function: _fetch_block tracks a running max, and on load a background scan_bytes() settles a stable width so padding doesn't jump as blocks stream in. _op_field is shared by the rendered strip, _line_plain, and search _fmt so cursor/match offsets stay aligned. 'o' toggles the column.
* docs: add READMEblasty2026-07-111-0/+97
|
* spawn.sh: run location-independent + env-configurable host/port/targetblasty2026-07-111-3/+15
|
* drive: make `dis` drive the live UI too (generalize _show_decomp -> _show_view)blasty2026-07-111-7/+11
|
* fix: don't hang drive pc on undecompilable functionsblasty2026-07-113-4/+55
| | | | | | | | | | | | | | | | 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.
* drive pc: render pseudocode in the TUI, not just the driverblasty2026-07-111-3/+32
| | | | | | | | The pc command called only the read-only pseudocode RPC (runs off the UI loop, never touches the screen), so LLM-driven sessions showed nothing on the live pane. Compose goto + toggle_view + search so the real TUI navigates to the function, makes the decomp pane the visibly-active view, and jumps the cursor to the needle — then return the same text as before.
* docs: blueprint for generalizing the TUI-driving layer (e.g. gdb)blasty2026-07-101-0/+355
| | | | | | | | | | A design reference for bringing the idatui spawn/drive/introspect experience to other terminal apps. Captures the invariants worth keeping, the reusable core vs per-target Adapter split, a target taxonomy (embedded / control- channel / black-box PTY), the three hard problems (injection, settle, introspection) with technique gradients, tmux as the universal substrate, and a concrete gdb worked example (TUI pane + gdb-Python plugin as the adapter). No code.
* app: multi-line comments via literal \\n (long notes were clipping)blasty2026-07-103-5/+17
| | | | | | | | | The comment prompt is single-line, so a long note rendered as one giant '//' line that ran off the right edge and got clipped. Interpret a literal '\\n' (backslash-n) in the comment text as a real newline in _do_comment (the single choke point for the ';' prompt and the RPC 'comment' verb) — Hex-Rays renders each as its own '//' line. Verified live and locked in the comment_func scenario (multi-line render); suite 104 green.
* drive: add save + retype; point docs/skills at the ergonomic frontendblasty2026-07-102-4/+24
| | | | | | | | Round out idatui.drive with 'save' and 'retype <fn> <proto>' so the whole common RE loop (orient/understand/act/persist) has a terse command. Update docs/RPC.md and both skills (idatui, idatui-rpc) to recommend idatui.drive as the day-to-day driving surface, with rpcclient/raw as the fallback for unwrapped verbs.
* drive: ergonomic RE helper over the RPC socket (terse text, auto socket)blasty2026-07-102-0/+228
| | | | | | | | | | | | | | | | Driving via rpcclient meant re-exporting the socket every fresh shell, long invocations, and piping ~every call through python -c to pull one field or grep pseudocode. idatui.drive fixes that: it auto-resolves the socket (the single live pane from the registry), prints compact text instead of JSON, and bundles the common gestures: where | go | pc <fn> [substr] | dis <fn> [n] | callees/callers <fn> | names <substr> | rename <old> <new> | mv old=new... | note <fn> <text> | screen | raw <method> k=v So 'goto+rename+parse' becomes 'drive rename old new', and 'pseudocode | python -c grep' becomes 'drive pc fn needle'. rpcclient stays the raw transport. rpc_smoke drives it end-to-end (29 green).
* app: comment on a no-address line -> function comment (was refused)blasty2026-07-102-2/+42
| | | | | | | | | | Pressing ';' on the decompiler's signature or local-declaration lines did nothing ('no address on this line to comment') because those lines carry no /*0xEA*/ marker. Fall back to the current function's entry ea so commenting the header annotates the function itself; the prompt says 'function comment @ ...'. Body-line comments are unchanged. New scenario comment_func locks it (suite 104 green).
* app: drop the Header (keep the Footer); rpc: function-scope xrefs_fromblasty2026-07-104-5/+35
| | | | | | | | | | | | | | 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.
* pane: auto-start the ida-pro-mcp supervisor if it's downblasty2026-07-102-1/+164
| | | | | | | | | | | | 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.
* rpc: cursor_on + word= edits, single-driver gate, colored screenblasty2026-07-103-17/+118
| | | | | | | | | | | | | | 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.
* pane: spawn/stop/list idatui TUI panes in tmux for agent-driven REblasty2026-07-101-0/+243
| | | | | | | | | | | | python -m idatui.pane spawn --open <bin> (or --db <session>) 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 <s>' (graceful quit -> kill-pane -> unlink). A small registry in $XDG_RUNTIME_DIR tracks panes for 'list'/'stop'. Verified end-to-end: spawn -> goto/pseudocode -> list -> stop.
* rpc: readiness, quit, methods discovery, not-ready guardblasty2026-07-102-4/+94
| | | | | | | | | | | 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).
* rpc: more verbs — structured reads, modal select, in-view search, saveblasty2026-07-103-0/+174
| | | | | | | | | | | | 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).