aboutsummaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
...
* fix stale top frame on jump-back; test the actual repaintblasty2026-07-092-1/+17
| | | | | | | | | | | | | The pane could keep showing the top (scroll 0) after jumping back even though scroll_offset was correct: the deferred call_after_refresh set the scroll AFTER the paint and didn't force a repaint. Now the deferred callback re-applies the scroll AND refreshes, so the pane is repainted at the restored offset. Crucially, the prior test only checked scroll_offset.y (which was correct even when the bug was visible). Added a render-level assertion: trace render_line and assert the last repaint happened at the restored scroll. (Also: my earlier scratch repros used a non-existent function name so 'back' was a no-op -- the real suite test uses a valid second function.) pilot suite 53/53.
* smoother scroll restore on jump-back; non-degenerate testblasty2026-07-092-7/+20
| | | | | | | | | | | | | The restored scroll was correct in the final state but load() zeroed virtual_size (snapping scroll to 0) and only a deferred call_after_refresh jumped to the target -> a visible flash to the top before settling. Now: don't zero virtual_size on load, and apply the target scroll synchronously in _on_primed (plus the deferred call as a fallback for un-sized regions). Also hardened the scroll test to use a MID-viewport cursor (rel>0). The prior test had the cursor at the viewport top (rel=0), where scroll-into-view derives the same scroll -> it would have passed even if scroll restore were broken. pilot suite 52/52.
* stop piling up idalib workers; raise max-workers headroomblasty2026-07-093-9/+29
| | | | | | | | | | | | | | | Root cause of 'Maximum idalib worker count reached': the app bumped idle_ttl to ~never on every open, so sessions never freed and hit the default cap of 4. - app: drop the immortal bump_idle_ttl(1e9); rely on the KeepAlive heartbeat (120s) to keep the running session warm, and open with a moderate idle_ttl (1800s) so the worker self-exits and frees its slot after the TUI closes. - spawn.sh: set IDA_MCP_MAX_WORKERS (default 8) for headroom. - doc: supervisor prunes unreachable workers before counting, so killing an idle worker frees a slot on the next open. (Verified separately: big/truncated decompile results propagate renames fine via the earlier force_recompile fix.)
* fix: pseudocode names not refreshing across history after renameblasty2026-07-092-2/+14
| | | | | | | | 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-094-21/+106
| | | | | | | | | | | | | | - 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.
* paging: preserve the cursor's viewport-relative row (IDA-style)blasty2026-07-092-12/+46
| | | | | | | PageUp/Down and Ctrl+U/D now move the scroll top and the cursor by the same (clamped) delta, so the cursor keeps its relative screen row instead of snapping to an edge; at the very top/bottom the cursor jumps to the extreme. Shared _page_scroll in ColumnCursor, used by both views. pilot suite 49/49.
* search: land the cursor on the match's starting columnblasty2026-07-092-0/+11
| | | | | | | _goto_line now sets cursor_x to the first match range's start on the target line (and _hscroll's it into view for the pseudocode view), so jumping to a search hit positions the cursor exactly on the match, not the old column. Search cancel/empty also restore the origin column. pilot suite 47/47.
* preserve scroll offset in jump history (both views)blasty2026-07-093-23/+91
| | | | | | | | | | | | Restoring only the cursor re-derived the viewport via scroll-into-view, so back landed on the right line but scrolled differently. NavEntry now stores scroll_y (disasm) and dec_scroll_y/dec_scroll_x (pseudocode); a single _save_current_pos() snapshots cursor+scroll of both views right before navigating away, and load/show restore the exact scroll. Disasm defers scroll_to via call_after_refresh (its virtual_size is only set in _on_primed, so an immediate scroll_to clamps to 0). Also routes rename's reload through the same save/restore path. pilot suite 46/46.
* fix: decompiler follow/xrefs of a name not listed in refsblasty2026-07-092-0/+34
| | | | | | | | | | _follow_decomp / _xrefs_decomp only matched the pseudocode ref list, so a function name under the cursor that wasn't in refs (self-reference, or refs truncated on big functions) did nothing — while disasm worked because it also resolves the symbol. Both decomp paths now fall back to program.resolve(word) like disasm does. pilot suite 45/45.
* rename symbol under cursor ('n') + save ('Ctrl+S')blasty2026-07-093-2/+218
| | | | | | | | | | | | | | | | | - '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.
* sortable function list: click column headers (addr/name/size)blasty2026-07-092-0/+49
| | | | | | | - on_data_table_header_selected sorts the cached function list by the clicked column (address / name / size); clicking the same header reverses. Header label shows a ▲/▼ arrow. Sorting composes with the active filter. - pilot suite 41/41.
* track pseudocode-view cursor position across jumpsblasty2026-07-092-7/+90
| | | | | | | | | | NavEntry now also stores dec_cursor/dec_cursor_x. DecompView posts a CursorMoved (with the line's /*0xEA*/ address) on move/click; the app keeps the top-of-history pseudocode position current and DecompView.show restores it when returning to a function. So Esc after a jump lands on the exact pseudocode line+column too, independent of the disasm position. pilot suite 38/38.
* preserve cursor line+column in the jump historyblasty2026-07-092-4/+18
| | | | | | | | | NavEntry now stores cursor_x too; the top-of-history position is kept current (line + column) on every disasm cursor move, and DisasmView.load restores the column (clamped once the line is cached). So Esc after a jump lands exactly on the symbol you left from, not the line start. pilot suite 37/37 (adds 'back restores the exact line + column').
* mouse: click to place cursor, double-click to followblasty2026-07-092-0/+69
| | | | | | | | | | | - ColumnCursor.on_click maps the click to a virtual (line, column) via event.get_content_offset (handles padding/border) + scroll_offset, then places the line+column cursor (disasm posts CursorMoved for the status line). - double-click (event.chain>=2) = place cursor + follow the symbol under it, identical to selecting it and pressing Enter. - works in both views (verified: disasm click on 'call sub_XXXX' lands on the exact token; pseudocode click maps columns with no padding). - pilot suite 36/36.
* column cursor: real left/right movement + follow the ref UNDER the cursorblasty2026-07-092-41/+264
| | | | | | | | | | | | | | - ColumnCursor mixin for both views: h/l/left/right, w/b word-hops, 0/$ line ends; block cursor cell + word-under-cursor highlight rendered on the cursor line (region-refresh so it stays cheap). vertical moves clamp the column. - follow/xrefs now use the token under the cursor, so a line with multiple refs follows the one you're pointing at (proven: 'return sub_A(sub_B,..)' -> cursor on sub_B follows sub_B). decomp matches a decompiler ref by exact name; disasm resolves the symbol, falling back to the line's from-xref (address column / hex operands are excluded so the default position still follows the target). - horizontal scroll follows the cursor in the pseudocode view. - dropped the f/b paging aliases (b is now word-back). - pilot suite 34/34.
* xrefs + follow-under-cursor (disasm & pseudocode)blasty2026-07-093-8/+305
| | | | | | | | | | | | | | | - 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-093-28/+137
| | | | | | | | | | | | - 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.
* search UX: visible prompt + incremental (as-you-type) highlightingblasty2026-07-092-14/+109
| | | | | | | | | | | | | - Fix overlap: #search, #status and Footer all docked bottom, and Input defaults to a 3-row bordered widget squeezed into 1 row. Now the search bar is a clean 1-row (border:none) that REPLACES the status line while active (status hidden), so the query is always visible. - Incremental search: highlight all matches + preview-jump from the origin on every keystroke (Input.Changed), not just on submit. Disasm indexes once then each keystroke is instant. Enter keeps; empty Enter repeats last; Esc cancels and restores the origin cursor + status line. - pilot suite 23/23 (adds: bar-visible/no-overlap, incremental-before-Enter, Esc-cancel).
* vim-style search in disasm + pseudocode viewsblasty2026-07-092-8/+277
| | | | | | | | | | | | | - SearchMixin shared by both views: '/' search forward, '?' backward, empty query repeats last; n/N next/prev. Smartcase. All matches highlighted (substring overlay), cursor lands on current match; status shows k/N. - disasm indexes line text lazily in a worker (handles huge funcs); pseudocode has text in hand. Substring highlight via crop/join Strip overlay. - app: bottom search prompt, SearchRequested routing; '/' is context-sensitive (filters when the function table is focused, searches in code views). - gotcha fixed: Textual only merges BINDINGS from DOMNode subclasses, so mixin bindings are ignored -> each view lists SEARCH_BINDINGS explicitly. - pilot suite 20/20 (adds search finds/hl/repeat-fwd/repeat-back).
* fix decompiler highlighting + cursor jankblasty2026-07-094-21/+175
| | | | | | | | | | | | | | | | | | Highlighting: Textual has NO C/C++ tree-sitter grammar (and the syntax extra wasn't installed), so language='cpp' was a silent no-op. Replace TextArea with a virtualized, Pygments-highlighted ScrollView: - idatui/highlight.py: CLexer -> Rich styles, per-line Segment lists - DecompView: lines highlighted ONCE at load, cached as Strips; O(1) scroll Jank: profiling showed our code is ~3.6ms/move (render_line 0.018ms) -- the cost was terminal repaint volume, since every cursor move refreshed the whole ~43-line viewport. Now: - cursor reactive repaint=False (no implicit full refresh) - region-limited refresh: in-place moves repaint only the 2 changed rows (measured 43 -> 2 render_line calls/move), full refresh only on scroll Applied to both DisasmView and DecompView. pilot suite 15/15 (adds highlighting assertion).
* decompiler view: Tab/Shift+Tab toggle disasm<->pseudocode, full bodiesblasty2026-07-093-18/+134
| | | | | | | | | | | - 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)
* tui: --open PATH to create/attach a session for an arbitrary binaryblasty2026-07-092-6/+38
| | | | | | | - app: when open_path is set, idb_open (idle_ttl=1e9, long timeout) and pin the returned session; clean status on failure (e.g. non-writable dir) - tui.py: --open flag + usage docs (attach vs open) - verified: --open targets/cat -> new session, 161 funcs loaded
* add Ctrl+B toggle to show/hide the functions paneblasty2026-07-092-1/+24
| | | | | | - action_toggle_functions: collapse/restore left pane, disasm takes full width; focus follows (disasm when hidden, table when shown) - pilot test covers the toggle (11/11)
* Phase 1 TUI: two-pane functions<->disasm, virtualized listing, nav backboneblasty2026-07-095-2/+632
| | | | | | | | | | | | | | | - 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
* keep idle workers alive: bump_idle_ttl + KeepAlive heartbeatblasty2026-07-094-8/+186
| | | | | | | | | | | | | | | | Root cause: idalib workers run an idle watchdog (worker_lifecycle.py) that self-exits after idle_ttl_sec (default 600s) of no requests -- deliberate resource hygiene for a shared/ephemeral supervisor, wrong for an interactive TUI. CLI-opened startup binary always gets 600s (no flag). Fix (both verified against a 12s-TTL session): - IDAClient.bump_idle_ttl(1e9): re-open (idempotent) re-applies TTL, no upper cap -> effectively immortal. Primary fix, call once at startup. - IDAClient.keepalive()/KeepAlive: background server_health heartbeat resets the watchdog; covers adopted sessions too. Safety net. tests/test_keepalive.py: 4/4 (heartbeat + bump both keep a short-TTL worker alive past 2x TTL). docs updated with why + fix.
* domain/paging layer: FunctionIndex, block-cached DisasmModel, decompile, resolveblasty2026-07-095-3/+582
| | | | | | | | | | | | | - 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)
* stress paging on 10k-func binary; document scale constraintsblasty2026-07-093-0/+254
| | | | | | | | | | | | | | | Measured against libcrypto.so.3 (10,092 funcs, biggest 52,120 insns): - list_* count cap ~700, disasm max_instructions cap ~500, then SILENT collapse to 10 (not clamped) -> must clamp client-side (use 500). - pagination must advance by len(data); next_offset=offset+count skips data. - disasm offset paging is O(offset): 6ms@0 -> 180ms@50k, no resumable cursor -> domain layer must cache windows + prefetch + over-fetch. - include_total scans whole func (~217ms on monster) -> fetch once, cache. - decompile hard-fails on huge funcs as a soft error (code=null) -> handle. - idb_open needs a writable path for the .i64. tests/stress_paging.py: durable paging benchmark harness docs/PAGING_FINDINGS.md: constraints that drive the paging layer
* harden client: transparent session auto-recovery + stress suiteblasty2026-07-094-11/+593
| | | | | | | | | | | - auto-recover stale db ("Session not found" after server restart): drop pin, re-resolve sole session, retry once. Only when db was auto-injected; never silently switches an explicitly-pinned db (auto_recover_session gate). - tests/serverctl.sh: start/stop/kill9/ready control over spawn.sh - tests/stress_client.py: 6 adversarial scenarios, 24 assertions: dead-port fail-fast, tight-timeout recovery, worker crash, full restart (naive auto-recovery + no-autoswitch), 300/16 concurrency, kill-under-churn. All clean: no hangs, no deadlocks, no non-IDAError leaks.
* persistent MCP client: warm handshake, keep-alive pool, error taxonomy, ↵blasty2026-07-097-0/+780
thread-safe - IDAClient: single handshake, ~7ms warm calls (vs ~60ms cold CLI) - keep-alive connection pool over http.client; concurrent-safe (40 threads OK) - grounded error taxonomy: IDAToolError on isError, soft per-item errors as data - session-expiry recovery (404 -> re-handshake -> retry), bounded transport retries - auto db-injection + resolve_db (ignores stale empty-id sessions) - live smoke test: 13/13 pass