summaryrefslogtreecommitdiffstats
path: root/idatui/domain.py (follow)
Commit message (Collapse)AuthorAge
* retype: 'y' now retypes globals too, not just prototypes and localsblasty2026-07-25
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | In the decompiler, 'y' on a local variable already worked (func_types -> lvars -> set_lvar_type), but a GLOBAL fell through every case and silently retyped the ENCLOSING FUNCTION'S PROTOTYPE — worse than not working, since the prompt said "prototype" while you thought you were typing a variable. * server/patch_server.py: new data_type tool — {addr,name,type,size,is_func} for a data item, so the prompt can prefill the current type and the caller can tell a global from a function. * domain: Program.data_type() + set_data_type() (set_type with kind="global"). * app: _prepare_retype gains the data case between "function" and the current-function fallback, with a size-based prefill when the global is still untyped; _do_retype routes kind="data" to set_data_type. Classification verified on echo/main: 'v3' -> lvar (prefill 'char *'), 'stdout' -> data (prefill 'FILE *'), 'main' -> func prototype, an unresolvable token -> the enclosing prototype (unchanged fallback). Also fixes a latent crash found while probing this: on_listing/decomp_view_ cursor_moved called self.query_one(ListingView), but App.query_one searches the TOP screen — a cursor-moved message landing while any modal is up (loading overlay, project switch) raised NoMatches out of a message handler and killed the app. Both handlers now go through _try_view(). Pilot `retype` extended to 9 checks covering all three flavours, each asserting the other targets are left alone. Two of the new checks needed settles: applying a retype recompiles asynchronously, so scanning/indexing the pseudocode without waiting reads text that's about to be replaced (this also cut the scenario from 30s to 2.6s of previously-wasted timeout). Full suite 174/2-flake.
* strings: browse every string in the binary and jump to it (IDA's Shift+F12)blasty2026-07-25
| | | | | | | | | | | | | | | | | | | | | | | ida-pro-mcp exposes no full strings list (only a filtered/capped "interesting" survey), so this is a new injected tool plus a filterable browser. * server/patch_server.py: list_strings(offset,count,min_len,refresh) — every literal from idautils.Strings() as {addr,text,len,type}, paginated, with a module-level cache keyed by min_len (rebuilding is O(n) and the browser pages the whole list). * domain: StrLit dataclass + Program.strings() — pages the full list once and caches it. * app: StringsPalette modal (mirrors SymbolPalette) — case-insensitive substring filter with the match highlighted, addr/len/text columns, ↑↓/Enter/Esc. Bodies are sanitized to one printable line (\n/\r/\t escaped, non-printables dropped, long strings clipped) so control chars can't break the layout; display strings are pre-rendered+pre-lowered once since filtering runs per keystroke. Enter jumps to the literal in the unified listing via _goto_ea. Bound to '"' and Shift+F12, plus a "Strings…" command-palette entry. Verified on echo: 150 strings listed with addr/len/text, filtering 'usage' narrows to 2 (case-insensitive), Enter lands the listing cursor on the literal. Pilot `strings` scenario 6/6; full suite 165/2-flake.
* split-view phase 3: rich per-line instruction region highlightblasty2026-07-24
| | | | | | | | | | | | | | | | | | | | | The Ghidra "region band": moving the pseudocode cursor now lights up EVERY instruction that C line owns, not just one. * server/patch_server.py: new decomp_map tool — sweeps cfunc.get_line_item across each pseudocode line's columns and collects the ea from each item's dstr() ('EA: desc', matching the /*ea*/ marker source so it aligns with the display lines). Returns {addr, lines:[{ea, eas:[...]}]}. (First tried item.get_ea(), which reports a different ea and didn't align — dstr() is the right source.) * domain: Program.decomp_map(ea) -> per-line ea lists, cached by name-gen. * app: _load_split_map fetches it off-thread into _split_eamap/_split_ea2line; _sync_split bands the full instruction region for a C line (decomp drives) and uses the exact ea->line inverse (listing drives), falling back to the single marker until the map lands. Maps cleared on leaving split. Pilot split_view gains: decomp_map returns/aligns with the markers, and a multi-instruction C line bands >1 listing row (13/13). Full suite 154/2-flake. The idalib spike ran on the pilot's own worker (the standalone worker kept getting reaped in this sandbox).
* xrefs: show fine-grained kind (call/jump/read/write/offset) in the dialogblasty2026-07-24
| | | | | | | | | | | | | | | | | | 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.
* mcp: delete the ida-pro-mcp transport, supervisor, and mcp-only testsblasty2026-07-24
| | | | | | | | | | | | | | | | | | | | | | | | | | 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 <binary> --rpc <sock>` 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.
* refactor: extract shared error hierarchy + Session into idatui/errors.pyblasty2026-07-24
| | | | | | | | | | | | | The IDAError/IDAConnectionError/IDAToolError/... exceptions and the Session dataclass were defined in client.py (the ida-pro-mcp HTTP client), but the idalib worker path (worker_client/domain/app) needs them without the HTTP transport. Move them to a transport-agnostic errors.py; client.py re-exports them so the deprecated mcp tooling and stress tests are unchanged (verified: errors.IDAToolError IS client.IDAToolError, so cross-module `except` still works). worker_client, domain (TYPE_CHECKING-guarded IDAClient hint), app, and __init__ now import the shared types from errors.py. This decouples the worker path from client.py at runtime -- the prerequisite for deleting the mcp transport.
* fix: follow/double-click a label lands on the label, not the function entryblasty2026-07-23
| | | | | | | | | | | | | | | | | 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).
* listing: code labels on their own line; search loaded portion (no wedge)blasty2026-07-23
| | | | | | | | | | | | | | | | | 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: IDA-style function boundary banners in the unified viewblasty2026-07-23
| | | | | | | | | | | | | | | | | 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-23
| | | | | | | | | | | | | | | | | | | | 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: render opcode bytes (shared engine with disasm) — M4/UX step 1blasty2026-07-23
| | | | | | | | | | | | | | | 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.
* unify: back the function disasm view on the heads listing walker (M4 item 4)blasty2026-07-23
| | | | | | | | | | | | | | | 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.
* perf: stream the listing incrementally instead of load_all on open (M4)blasty2026-07-23
| | | | | | | | | | | | | | 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-23
| | | | | | | | | | | | 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-23
| | | | | | | | | | | | | | | | | | 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.
* fix: crash on an unnamed function (None name) in the symbol paletteblasty2026-07-23
| | | | | | | | | | | | | | | | `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-23
| | | | | | | | | | | | | | | | | | | | | | 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).
* app: 'd' — define typed data in the listing (make_data, M3)blasty2026-07-22
| | | | | | | | | | | | | | 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+domain: heads walker + ListingModel — flat code/data listing (M1)blasty2026-07-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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-22
| | | | | | | | | | | | | | | | | | | | | | | | | 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 ↵user2026-07-12
| | | | spawn-hang + bare KeyError)
* disasm: render opcode bytes (toggle 'o'), padded to widest insnblasty2026-07-11
| | | | | | | | | | | | | | 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.
* fix: don't hang drive pc on undecompilable functionsuser2026-07-11
| | | | | | | | | | | | | | | | 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.
* hex view: file offsets + 'g' goto (dedicated input)blasty2026-07-10
| | | | | | | | | | | | | Add a file_regions server tool (get_fileregion_offset per segment) and Program.file_regions/file_offset so a virtual address maps to its raw on-disk offset without a format-specific header parser. The hex view now shows a VA column and a file-offset column side by side, and the status line reports file+off. Goto moves to its own top-level #goto Input instead of overloading the function-filter box (which is hidden with the names pane, so goto was unreachable there). In the hex view 'g' jumps the cursor to an address (bounds-checked against the image); elsewhere it navigates as before.
* hex view: raw image bytes (VA-addressed), synced to the code cursorblasty2026-07-10
| | | | | | | | | | | | | | | | New HexView: a line-virtualized 16-bytes/row dump of the whole loaded image, addressed by virtual address. Format-agnostic — the range/segments come from IDA (image_range over sections()), not any file header; gaps read back as zeros. Backslash toggles it, synced to the address under the disasm/pseudocode cursor (see the naked bytes of some code/data). In hex: hjkl/paging navigate a byte cursor (status shows the section), Enter jumps the code view to the byte, Tab/Esc/backslash return to the preferred code view. A third _active state; block -cached + prefetched like the disasm model. domain: HexModel + Program.hex_model/read_bytes/image_range (get_bytes regions). Fixed a self-deadlock (hex_model held _lock while sections() re-locked). Pilot: sync, actual bytes, byte-step, return. full suite 98/98.
* retype: set variable/function types with 'y' (IDA-style), via structured toolsblasty2026-07-10
| | | | | | | | | | | | | | | | Instead of parsing pseudocode text, add structured server tools (server/ patch_server.py, alongside del_type): - func_types(addr): prototype + local variables (name/type/is_arg) - set_lvar_type(addr,var,type): retype a decompiler local, working on auto/ register vars too (stock set_type only updates already-user-modified lvars) 'y' in a code view retypes what's under the cursor: a local variable (prompt prefilled with its current type) or a function (prompt prefilled with its full prototype). Applies via set_lvar_type / set_type, then recompiles + refreshes. Copy-line moved off 'y' to Ctrl+Y. domain: Program.func_types / set_function_type / set_lvar_type (LVar/FuncTypes). Pilot: function-prototype retype (prefill + apply). full suite 94/94.
* struct editor: C-style CRUD for local types (Ctrl+T)blasty2026-07-10
| | | | | | | | | | | | | | | | | New StructEditor overlay: left is the list of local structs/unions, right is an editable C definition. Enter loads a struct (reconstructed as C from its member layout via type_inspect), Ctrl+S declares it (create or update via declare_type), Ctrl+N starts a new one, Del deletes, Esc returns to the list then closes. Domain: Struct model + Program.list_structs / struct_source / declare_type / delete_type (anonymous $-types filtered). Delete needs a 'del_type' tool the ida-pro-mcp server doesn't currently expose; delete_type detects its absence and reports a clear message instead of failing. Create/read/update work fully. Pilot checks: open, list, view C, create, update-in-place, delete(-or-report), close. full suite 84/84.
* xrefs: informative labels — function+offset, and section instead of '?'blasty2026-07-09
| | | | | | | | | | | | | | | The xref list showed just the containing function name, so multiple sites in the same function were indistinguishable ('sub_2300' repeated), and references not in any function collapsed to a bare '?'. - In-function sites now show fn_name+offset (e.g. main+0xb08), so each site is distinct and you can see where in the function it is. - Non-function sites (GOT/reloc data slots, loose thunks) show the section they live in (.got, .data.rel.ro, .text, LOAD, ...) instead of '?'. Add Program.sections()/section_of() over survey_binary's segment map (fetched once, cached lazily). Pilot checks for both. full suite 67/67.
* comments: ';' sets a comment on the current line (disasm + pseudocode)blasty2026-07-09
| | | | | | | | | | | | | Bind ';' (NavMixin, so both code views) to a comment prompt for the line under the cursor. Resolve the line's address (_cursor_ea in disasm, the /*0xEA*/ marker in pseudocode), prefill any existing comment (parsed from the rendered '//' text), and apply via the set_comments tool; an empty value clears it. After setting, reuse the name-generation invalidation (bump_names) so Hex-Rays is force_recompiled lazily and the pseudocode/disasm refresh in place at the saved position -- the new '// comment' shows on the line. Marks the IDB dirty (Ctrl+S). Pilot checks for the pseudocode round-trip. full suite 63/63.
* fix: pseudocode names not refreshing across history after renameblasty2026-07-09
| | | | | | | | force_recompile takes 'items=[{addr}]', not 'addr' — my call raised and was swallowed, so the Hex-Rays cache was never cleared and a caller's cached pseudocode kept showing the old callee name on 'back' (disasm refreshed because its names are live). Use the correct params. History test now also caches and verifies the caller's pseudocode. pilot suite 52/52.
* fix input visibility (search/rename) + refresh stale names across historyblasty2026-07-09
| | | | | | | | | | | | | | - Visibility: #search/#rename/#status were dock:bottom like the Footer and got placed on the SAME row (y=39) as the Footer, which drew over them, so the typed text was invisible. Drop the docking; they now sit in normal flow just above the Footer (y=38). Verified: input region is above the footer. - Stale names on 'back': a rename only invalidated the current function, so popping back to a caller (cached earlier) showed the old name. Add Program.bump_names(): a rename bumps a name-generation, clears all disasm block caches (disasm names are live), and decompilation is generation-checked and force-recompiled lazily on next view. _after_rename now invalidates globally. - verified: rename a callee, 'back' to the caller shows the new name. - pilot suite 51/51.
* rename symbol under cursor ('n') + save ('Ctrl+S')blasty2026-07-09
| | | | | | | | | | | | | | | | | - 'n' on the token under the cursor opens a rename prompt prefilled with the current name. The symbol is classified and routed to the right rename batch: * resolves to a function start -> func * decompiler ref (global/string) -> data * pseudocode token, not a ref -> local (Hex-Rays lvar) * disasm var_/arg_ -> stack tool errors (e.g. name collisions) are surfaced in the status line. - after a rename: force_recompile + cache invalidation, reload the current views (preserving cursor), update the renamed function's row in place (avoids racing the initial stream), and update nav history names. - 'Ctrl+S' persists the IDB (idb_save); edits mark the session dirty. - 'n'/'N' search-repeat dropped for 'n'=rename (repeat stays on '/'+Enter / '?'). - verified live: func rename (list + disasm update), local var rename (pseudocode update), save. pilot suite 44/44; domain 17/17.
* xrefs + follow-under-cursor (disasm & pseudocode)blasty2026-07-09
| | | | | | | | | | | | | | | - Enter follows the reference under the cursor: disasm uses xref_query(from) to find the code target; pseudocode matches a decompiler ref name on the line. Navigates to the containing function at the exact line (address-history push). - x opens an xrefs popup (XrefsScreen modal): xrefs_to the subject under cursor (call target if any, else the current item); Enter jumps to a referencing site, Esc closes. - domain: Program.function_of (mid-addr -> containing func via lookup_funcs), xrefs_from/xrefs_to (xref_query), Xref model, DisasmModel ea->index map for landing on an exact address. - fix: goto to a mid-function address now lands on the right line (was opening it as a bogus function start). - pilot suite 32/32; domain 17/17.
* function-name filter: incremental + highlight + clearblasty2026-07-09
| | | | | | | | | | | | - Incremental client-side filter over the cached function list (no per-keystroke server round-trips): narrows + highlights the matched substring in each name as you type, debounced ~80ms. Substring or glob (*/?), smartcase. - Clear the filter: Esc in the filter box, OR Esc on the focused list, OR empty Enter. Status shows 'filter <q>: N/total'. - streaming pauses while a filter is active; re-applied over the full set once load completes. Row selection resolves the name from the index (cells can now be styled Text). - FunctionIndex.all_loaded(); pilot suite 27/27.
* decompiler view: Tab/Shift+Tab toggle disasm<->pseudocode, full bodiesblasty2026-07-09
| | | | | | | | | | | - domain.decompile now fetches the FULL body: server caps responses at 50KB and clips strings to 1KB, but caches the full output and exposes it at _meta.ida_mcp.download_url -> we GET that so pseudocode is complete (main: 43257 chars, not a 1KB stub). include_addresses gives per-line /*0xEA*/. - DecompView (read-only TextArea, cpp highlighting best-effort, line numbers) - Tab and Shift+Tab toggle the code pane; lazy decompile in a worker, cached; hard-failures ('decompilation failed') shown gracefully with disasm fallback - pilot suite 14/14 (adds tab->pseudocode, full-body, shift+tab->disasm)
* Phase 1 TUI: two-pane functions<->disasm, virtualized listing, nav backboneblasty2026-07-09
| | | | | | | | | | | | | | | - idatui/app.py: keyboard-first Textual app * FunctionsPanel: lazy-streamed function list (DataTable) with glob filter * DisasmView: line-VIRTUALIZED ScrollView -- paints from domain block cache, background-fetches misses, prefetches neighbors. Proven: 52,120-insn func, jump-to-bottom in 0.31s with only ~6 blocks (~1.5k lines) ever materialized. * address-history nav stack (Enter follow / Esc back); goto by name or 0xADDR * bump_idle_ttl + KeepAlive on startup so the session never idles out * all MCP work on Textual worker threads; UI never blocks - idatui/tui.py: launcher (python -m idatui.tui / console script 'idatui') - tests/test_tui.py: headless Pilot test, 9/9 (boot, load 10k funcs, open, scroll, bg-cache, goto-bottom, filter) - pyproject: textual extra + idatui script entry
* domain/paging layer: FunctionIndex, block-cached DisasmModel, decompile, resolveblasty2026-07-09
- Program: model registry + small prefetch pool; cache invalidation hooks - FunctionIndex: lazy pagination (advance by len, clamp page=500), filter globs, viewport windows; full 10,092-func enumerate matches survey in ~3.1s - DisasmModel: block-cached windowed disasm (256/block), forward+back prefetch, cached instruction totals; deep window revisit 196ms -> 0ms (cache proven) - Decompilation: truncation-aware, hard-failure surfaced as data (monster funcs) - resolve(): int/hex/symbol via lookup_funcs (string-array query shape) - tests/test_domain.py: 17/17 on both ls (~290 funcs) and libcrypto (10k+52k-insn) - smoke_client no longer assumes 'main' exists (works on libraries) - doc: idle_ttl worker self-exit finding ("not reachable" needs re-open)