| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Mechanical part of the port: the 27-file patch was cut against a base ~148
commits behind us, so it did not apply. Resolved 11 conflicts (all of them
diff drift, not semantic clashes) and the three file deletions:
- app.py: the patch re-inserted _do_rename/_do_name_addr/_seek_split etc. as
"theirs" because our tree moved them to edit_ctl.py/trace_ctl.py. Kept ours
and applied the real intent (WorkerClient->CodeModeClient, .call->.invoke,
_open_worker_client->_open_database_client) at their current homes.
- domain.py: kept Head as a NamedTuple -- the patch reverted it to a frozen
dataclass, which the perf work measured at 2.9us vs 1.9us per row on a
quarter-million-row walk. Dropped _fetch_output (no download_url under Code
Mode) and its now-dead urllib/json imports.
- pane.py: the patch's deletion swallowed our zellij support along with the
worker-reaping block it meant to remove. Kept zellij, removed the reaping.
- test_scenarios.py: the idb_save->save_database teardown hunk belongs to
tests/_fixtures.py in our tree; applied it there and kept our pc_num_format
scenario that the drift landed on.
Three defects in the patch itself, fixed here:
- It made "import idatui" hard-require ida_codemode, so every offline suite
died at import -- including the pure ones (graph/index/trace) that are the
house rule for "tests/run.py --fast". The import is now deferred and gated
on the binding, which is also what lets the port's own contract tests
inject a fake DatabaseHandle.
- project.stage() inlined an ida_codemode.registry import and treated "library
not installed" as "someone owns this database", which broke IDA-free project
staging. Ownership lookup moved to codemode_client.database_owner().
- tests/test_codemode_client.py had no NEEDS_IDA marker, which tests/run.py
rejects outright.
Offline suite: 301 passed, 0 failed. Against master's 344 the whole delta is
accounted for: -40 worker_client (module deleted), -18 launch sweep checks
(behaviour deliberately removed) +3 guarding that it stays removed, +2 pool
(GUI-save semantics), +13 new codemode_client contract tests.
NOT yet done, and the port is not functional without it: the adapter is
missing five operations our tree grew since the patch's base (flowchart,
op_format, pc_nums, pc_num_format, survey_binary) and its "heads" predates
back-walking and digest/expect.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A TUI must not die because one background load failed, so this codebase catches
broadly -- ~50 `except Exception` sites, two dozen resolving to `pass`. Right
policy, one bad consequence: with 44 `@work(thread=True)` workers, a failure in
a background load leaves no trace whatsoever. The view stays empty and there is
nothing to read afterwards, because the app owns the screen.
kittygfx already solved this for itself with $IDATUI_KITTY_LOG. idatui/diag.py
is the same idea for everything else: $IDATUI_LOG writes every swallowed error
plus its traceback to a file, and the last 50 are kept in memory regardless so a
driver can ask a live app what went wrong. Unset, it costs an environ lookup.
Wired in where losing the error changes a DECISION rather than just a pixel:
* rename: a resolve() that throws renames as DATA instead of as a function.
* name: a function_of() that throws means we never learn the address is a
function start, so the index keeps the old name and every readback says the
rename didn't happen.
* retype: a resolve() that throws retypes the ENCLOSING function instead.
* decompile: a failed full-body fetch silently returns CLIPPED pseudocode.
* trail: a failed decomp_map stops the pseudocode being painted, silently.
Deliberately NOT wired into the query_one guards -- a modal owning the screen is
normal and constant, and logging it would bury the real entries in noise.
New RPC verb `diag {n?, clear?}`, documented in docs/RPC.md: the answer to "the
verb reported success and the pane shows nothing".
Also a flake, same shape as the others: follow_xrefs waited on the nav depth but
asserted on _cur, and a follow pushes the source entry BEFORE opening the
target -- so the check could run in between and see the function it jumped
from. About one run in ten. It waits on the postcondition it asserts now; three
clean full runs since.
833 checks; --fast is 344 in 3.5s.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
_active was a bare string with 49 comparisons across four modules and a fifth
value nobody meant to keep. "disasm" was assigned on exactly one path -- a
decompile that failed with nowhere to return to -- and named the same widget as
"listing". Four sites understood it; five compared against "listing" alone and
silently took the wrong branch:
* Tab out of a failed decompile set "listing" instead of "decomp", so the
first press appeared to do nothing.
* rpc.py carried a workaround for a mode change that never arrived, keyed on
being ALREADY in the ghost state -- so it fired in the rare case and not in
the common one. Now keyed on LISTING, which is the case that happens.
* drive.py asked the socket to show it "disasm", a value the app will now
never report, and would have toggled twice and given up.
ViewMode is a StrEnum on purpose: _active goes straight to drivers as
cursor.kind and the pilot compares it to plain strings, so members being strings
keeps every payload and comparison working. What it buys is one place that says
which modes exist, and an AttributeError instead of silence on a typo.
Read it through is_listing/is_decomp/is_hex/is_graph/in_code rather than ==.
The bare comparisons are what let the ghost hide, and they are what the next
mode would have to hunt down -- adding "graph" already cost one crash that way
(_active_code_view returning None when a prompt closed).
view_modes_all_handled walks the enum and asks the app the questions it asks
itself. Verified it bites: adding a fifth unhandled member fails it twice.
746 checks, 142.3s.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
First cut at the 4000-line class. Trace is the cleanest seam: 348 contiguous
lines, one coherent job (where we are in time and everything that moves us),
and two suites already covering it.
TraceController owns the state now -- the trace, the timestamp, the trail maps.
IdaTui keeps the keys, because Textual only merges BINDINGS from DOMNode
subclasses and a mixin's would be silently dropped, and it keeps the @work entry
points, because the worker machinery wants a DOMNode host. Both are one-line
delegates.
_trace/_t/_trail_map/_trail_map_ea/_trail_line_of stay readable on the app as
properties: the pilot suite and rpc.py read the position by those names, and a
property means one owner rather than a copy that can drift. rpc.py itself now
goes through the controller.
The parallel line-map that _apply_split_map used to poke into five attributes is
now one adopt_map() call -- same single shared index, but the sharing is stated
rather than implied by two places assigning the same fields.
731 checks, unchanged.
|
| |
|
|
|
|
|
|
| |
It reports STRUCTURE -- blocks, typed edges, ranks, box geometry, the
cursor -- and not the box-drawing characters, which is what a driver
actually wants; screen still gives you the drawing. show is a pure read.
The line-oriented verbs refuse in the graph rather than reporting a
(block, row) cursor as a line index some later edit would trust.
|
| |
|
|
|
|
|
|
| |
mode is cycle/back/show or an explicit format. 'show' reports the current
format and the stops on offer without editing, which is what a driver
needs: the rendered text alone can't be trusted (a listing read before an
ARM/Thumb switch shows the old decoding). 'word' puts the cursor on a
token first, so a literal can be named instead of steered to.
|
| |
|
|
|
|
|
|
|
|
| |
Hex-Rays caches per function and does not notice that a *callee* was renamed;
worse, that cache is persisted in the .i64, so a bulk import left pseudocode
calling sub_98C0 forever while the listing and every readback said memset --
the exact readback disagreement a driver cannot detect. Batch now calls
force_recompile before bumping the local caches.
Test extended: decompile, rename via rename_many, read the pseudocode back.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Opening a headerless blob was the one workflow that fell out of the driving
surface entirely, and each gap hid the next:
- `pane spawn` couldn't pass --processor/--base/--ida-args, so the pane came up
"ready" with zero functions (x86 at 0) and the only way through was to
hand-write a project file. It now forwards them to idatui.launch.
- c/p/t/T (code, function, ARM<->Thumb, vector scan) existed as listing
bindings with no verb, so a driver had to guess raw keys -- and raw keys are
swallowed by whatever modal happens to be up. `define {kind,target?}` goes
through the app's own edit worker and reports what IDA actually did.
- every name went through the typed rename prompt: a navigation (listing page +
decompile) plus two prompt round-trips each. A 427-symbol map took tens of
minutes of driving. `rename_many {items|file}` hands IDA's rename tool the
whole list in one call (371 symbols in 3s) and refreshes the caches and the
function table once.
drive gains `define <kind> [target...]` and `syms <file.json>`.
Verified live against a real pane (tests/test_rawimage_rpc.py, 13 checks:
spawn load options, define thumb/func + unknown-kind rejection, rename_many
from a file and inline, with resolve/functions readback).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two fixes to the comment verb:
Drop the per-character typing delay. Rename/retype/goto use a 35 ms delay for
the visual effect (the agent's keystrokes appear one by one on the livestream),
but comments can be long — a 200-char annotation blocked the driver for 7 s of
pure animation. Comments now type instantly (delay=0); the aesthetic delay is
kept for rename, retype and goto where values are short.
Escape literal newlines before injecting into the prompt. The Input widget is
single-line, so a real 0x0a sent as a keystroke was silently swallowed. The
app's _do_comment already converts the two-char sequence '\\n' into a real
newline for IDA, so the RPC layer now does text.replace('\\n', '\\\\n')
before typing — both literal newlines from the caller and explicit \\n in the
text reach IDA as multi-line comments (each line gets its own // prefix in the
decompiler).
Verified on a live pane: a comment with an embedded newline now renders as two
// lines in the pseudocode, and long comments appear without the multi-second
typing pause.
tests/test_scenarios.py: 212 passed, 0 failed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
goto/open ran `settle(app, pred, timeout)` and threw the result away. On a large
database the listing build routinely outruns the default 20 s, so the verb
returned a normal snapshot while the view had not moved. Every subsequent
rename/comment then applied to wherever the caller *used* to be.
Reproduced on a live pane against a 4 MB Go binary: `goto 0x1002019b0`
returned ok with the view still at 0x100001000, and the following
`rename main_inflate_zlib` renamed internal/abi.BoundsDecode instead — then the
rename's own snapshot showed `main_inflate @ 0x1002019b0`, because by the time
it was taken the goto had finally landed. Success reported, right-looking
readback, wrong function edited, and it survived a save.
This is what made an agent session stamp net_writeFull onto main_usage and
conclude the tooling was flaky.
goto/open now raise TimeoutError naming the target and where we actually are,
suggesting a larger timeout=. _press() does the same for follow/toggle_view/
hex/xrefs/structs, which had the identical "predicate ignored" shape.
tests/test_scenarios.py: 212 passed, 0 failed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Five defects found while an agent drove a long RE session over the socket.
Each one was reproduced on a live spawned pane first (an in-process pilot
would not have shown any of them), then fixed:
pane stop truncated the save. `stop` asked the app to quit, slept 400 ms, then
unconditionally killed the pane. Quitting runs App.on_unmount, which writes
every dirty database; a 90 MB .i64 takes tens of seconds, so the kill landed
mid-write and a whole session's annotations went to /dev/null with a cheerful
{"stopped": [...]} on stdout. Now it waits for the pane to actually exit
(--timeout, default 600 s) and only force-kills on timeout, saying so.
The quit verb bypassed the dirty check. It called app.exit() directly rather
than the path a human gets, so the "unsaved changes" logic never ran. It now
routes through _on_quit_choice and reports {saving, dirty}.
Naming a function start from the listing never reached the function index.
`goto <addr>` puts the cursor on the address token, so `n` takes the
name-an-address path, which called bump_items() but left FunctionIndex holding
the old name. Result: the rename response snapshot showed the section label,
and functions()/names()/the palette all reported the rename had not happened —
so a driver that trusts its readbacks redoes work it already did. Twice, in
the session that prompted this. _do_name_addr now updates the index, the nav
stack and the table cell when the address is a function start.
A stripped binary with no entry function started up *inside a modal*.
_auto_land pushed the symbol palette when main() was missing, while ping still
answered ready:true. Every keystroke an RPC driver injected went into the
palette's search box and was silently swallowed. It now lands on the first
function instead and hints at Ctrl+N.
Verbs that inject keystrokes now refuse when a modal is on top, naming it,
instead of failing with "'goto' prompt did not open (word under cursor?)" —
a message that blamed the cursor for what was always a focus problem.
Also: `drive raw` passes k=v values through as strings, so `view lines=8` died
with "'<' not supported between instances of 'int' and 'str'". Numeric params
are now coerced centrally rather than at each call site.
tests/test_scenarios.py: 212 passed, 0 failed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Driving the trace viewer needed the same treatment as everything else: seeking by
hand through a few hundred keypresses to reach an interesting timestamp is not a
way to test it.
trace {seek: 120} absolute timestamp
trace {seek: "!50"} halfway through, like Tenet's timestamp shell
trace {goto: "main"} first execution of a name or 0xADDR
trace {step: 20} relative, negative goes back
trace {step: 5, over: true}
Returns the usual state snapshot plus {idx, length, pc, changed} so a driver can
see where it landed and what that instruction wrote.
pane spawn --trace FILE passes it through to the launcher, so a trace pane is one
command.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Three things, all following from phase 3 making cross-binary jumps ordinary.
**A cross-binary jump was a one-way door.** Nav history is per-binary, so
arriving in another binary — a project search hit, or now following an import
into the library that implements it — landed you in an empty history with nothing
to take you back. _switch_then_goto records the binary it came FROM, and
action_back falls through to that hop once local history is spent: Esc walks back
through the function you were in, then the binary you were in. Manual Ctrl+O
switching records nothing, because that isn't navigation.
**Pre-warm follows the linkage graph, not list order.** _prewarm_provider warms
the binary providing the most of this one's imports — where a follow is most
likely to go, so its startup is paid before you ask for it. "Next in the list"
would have been arbitrary; phase 3 gave us something better to ask.
pool.prewarm() refuses rather than making room. Evicting a binary the user
visited to speculatively load one they haven't is a straight downgrade, and it
throws away that binary's caches as well; at a tight budget pre-warm just does
nothing. The cost of a worker that doesn't exist yet can only be estimated, so it
uses the largest resident one (same program, different database) — and if that
estimate proves wrong, the speculative worker is the one evicted, never a chosen
one.
**Driving a project.** pane spawn --project FILE [--open BIN]; `binaries` lists
the inventory (active / resident / indexed / where Esc returns to) and `switch
{binary,addr?}` makes another active — with an address it takes the search-hit
path, so it records a hop. state gains `binary` and `hops`, which it should have
had the moment project mode existed.
Verified on real sessions: drive binaries/switch against an echo+cat project
pane; Esc crossing back from a switch; and prewarm on echo+libc picking libc
(provider of echo's imports) and warming it after an evict.
tests: +5 pool (prewarm warms, no-ops when resident, refuses at budget, evicts
nothing when refusing, ignores unknown labels) and +4 project UI (jump records
the hop, Esc crosses back, hop consumed). Confirmed the Esc-back checks fail with
the branch removed. 195/0 scenarios, 27/0 project UI, 36/0 index, 27/0 pool,
33/0 project.
Left open: project-level persistence across sessions.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The app carried three overlapping ideas of the current pane: _active, _pref, and
Textual focus. 190e28b tied focus to _active in split; this removes _pref, which
turns out never to have been a variable at all.
_pref was assigned "listing" in __init__ and "listing" on a project binary
switch. Nothing else ever wrote it. But _code_view() branched on it:
return self.query_one(DecompView if self._pref == "decomp" else ListingView)
so it always returned the listing, whatever you were reading. Its two callers put
focus back after the goto prompt closes — so cancelling `g` while in the
pseudocode focused the HIDDEN listing, and the pane you were looking at stopped
answering the keyboard. Arrows did nothing until you clicked. (It also explains
why routing follow through _code_view() earlier made Enter a dead key: the helper
had been quietly lying the whole time.)
_code_view() now returns the active code pane. _pref is gone from the app,
BinaryState and the RPC snapshot keeps "pref" for wire compat, sourced from
_code_mode() — the one place that answers "which code view do we return to from
hex", and a constant by design in the unified layout.
Verified on a live pane both ways: g then Esc in pseudocode, then two Downs.
Fixed, the cursor moves 0 -> 2; with the old lookup restored it sits at 0 with
focus=ListingView. Same check added to the view_toggle scenario, and it fails
without the fix. 195/0, project UI 23/23.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
ida-pro-mcp's xref_query only classifies xrefs as code/data (xr.iscode). Add an
injected `xref_types` tool (server/patch_server.py) that mirrors xref_query's
query/envelope shape but derives a fine `kind` from the IDA xref type:
call/jump/flow for code (fl_CF/CN/JF/JN/F), read/write/offset/text/info for data
(dr_R/W/O/T/I). The worker self-injects it on startup like the other custom tools.
* domain: Xref gains a `kind` field; _parse_xrefs reads it; xrefs_to() now calls
xref_types (falling back to xref_query if absent). xrefs_from is unchanged.
* app: the `x` dialog shows the kind as an aligned column after the address
(`000034F4 read sub_34F0+0x4`).
* rpc: the structured xrefs_to read carries `kind` too.
Verified live over the worker/RPC harness on targets/echo: sub_2C00 callers ->
call; __progname -> offset (GOT), read (sub_34F0), write (sub_3500); stdout ->
read/offset.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Full IDA-style unification. The function-bounded DisasmView is gone from the UI:
navigation opens ONE continuous segment listing (functions+data+undefined
interleaved) at the target; F5/Tab decompiles the function under the cursor and
back. The listing gained the opcode column (Head.raw, 'o' toggle) so rendering
matches disasm.
Routing: _do_navigate/_open_function/_open_entry target the listing;
_show_active drops disasm; _active in {listing,decomp,hex}; _pref=listing.
Decomp is a per-function toggle with a listing fallback on failure. Edits
reload in place (_reload_active_code); bump_names() drops the listing cache so
renames refresh. Listing 'n' is symbol-aware. ListingModel gains
cached_line/lines shims; Head.label. DisasmView removed from compose/handlers;
rpc updated. DisasmModel kept (domain/test_domain).
Tests migrated (c.dis->ListingView; open(decomp) F5s; xref/search re-pointed).
Per-scenario green across view_toggle/rename/follow_xrefs/xref_labels/decomp_*/
search/mouse/startup/continuous_view (30/1, 1 cold-decompile flake).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The real disassembly-listing view. ListingView is a line-virtualized
ScrollView (reusing ColumnCursor/SearchMixin/NavMixin) backed by
ListingModel, rendering code, data (db/dw/dd, strings, jump tables) and
undefined bytes interleaved with per-kind styling. Non-function regions now
open in it instead of the M0 DisasmModel stopgap.
Integration (IdaTui): a new `listing` active mode. _open_entry routes region
entries to ListingView; _show_active/compose add it; follow/xrefs/comment and
the c/p/u edit verbs handle it (define_func upgrades a region straight to the
function view); backslash reaches hex and returns via _code_mode (so leaving
hex from a region lands back on the listing, not a func view); Tab explains
there's no pseudocode for a region. rpc._active_widget learns `listing`.
Server fix (the important one): the `heads` walker now steps by get_item_end
instead of next_head. next_head SKIPS undefined bytes, so after undefining a
function its start address wasn't even a listing line and the cursor snapped to
the next defined item; stepping by item-end renders undefined bytes as `db ?`
lines (IDA-accurate) and makes navigation land exactly on any address —
essential for "go to unmarked bytes and hit c". Needs a supervisor restart.
Tests: region_define rewritten to assert the ListingView path + segment-wide
listing + p-upgrade; new listing_view scenario (data-segment listing renders
data heads, cursor reports head ea, backslash->hex->back). Pilot 115 pass / 1
pre-existing flaky (filter); rpc_smoke 29/0; test_domain 27/0.
|
| |
|
|
| |
spawn-hang + bare KeyError)
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
toggle_view's settle predicate (lambda: app._active != before) never
fired when tabbing toward pseudocode on a function Hex-Rays can't
decompile: App._apply_decomp snaps the view back to disasm, so _active
returns to its prior value -> full 20s settle timeout (x2 in _show_decomp,
~40s for drive pc). Recognize the decomp-failed fallback as settled.
Also harden two amplifiers surfaced by the same case:
- rpcclient: the CLI socket had no read timeout and would block forever
on any server slowness; add a bounded settimeout (IDATUI_RPC_TIMEOUT,
default 90s) with a clear error.
- domain.decompile: pass a bounded 15s timeout and cache failures, so a
failing decompile can't sit at the 30s client default or be re-run by
transport retries.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
The clock/title Header added noise (and made screen() non-deterministic);
remove it from the app itself — the Footer stays. Layout shifts up a row;
scenario suite still 101 green.
xrefs_from on a function was near-useless: the server's xrefs_from is
address-scoped, so passing the entry ea only returned the fall-through
from the first instruction. For a function target it now returns
whole-body references from the decompiler (callees + string/data refs:
{to,name,string,is_func,type}); an explicit 0xADDR stays address-scoped.
Verified live: xrefs_from main now lists setlocale/getopt_long/sub_* etc.
rpc_smoke: 25 green.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
cursor_on {word,line?,occurrence?} places the cursor on a token, verified
with the app's own tokenizer (so 'main' won't match inside 'domain');
rename/retype/follow gain an optional word= that runs it first — the
ergonomic way to edit a named symbol without a manual view+cursor dance.
Single-driver gate: the socket now serves one client at a time; a second
concurrent connection is refused with 'busy' (no multi-driver support yet,
by request). Sequential connections are unaffected.
screen gains format=text|html|svg (html/svg are colored — for an
out-of-band web viewer). rpc_smoke: 24 green.
|
| |
|
|
|
|
|
|
|
|
|
| |
Spawn-and-drive needs to know when the freshly-launched TUI is usable:
ping/state now carry {ready,functions,complete} (cheap; no health call)
and ping adds the module name. Add a graceful 'quit' verb (acks, then
exits on a short delay so the reply still flushes) and a self-documenting
'methods' table. Program-dependent verbs are refused with a clear
'not ready' instead of exploding in a worker while still loading.
rpc_smoke covers readiness, methods and quit (21 green).
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Introspection (offloaded to a thread so a big fetch can't freeze the
render): pseudocode (full body), disassembly (bounded), xrefs_to/from
(structured), resolve (name->ea). These let an agent reason about a
function and the call graph without scraping screen().
Semantic: select (choose the highlighted/nth item in the open xrefs or
symbol-palette list and activate it — the natural follow-up to xrefs/
symbols), search (/ or ? incremental in the active code view), save
(Ctrl+S). rpc_smoke exercises all of them end-to-end (18 green).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Layer high-level verbs over the raw injection: goto/open, rename, comment,
retype, follow, back, toggle_view, hex, xrefs, symbols, structs, close,
plus fast move/cursor. Editing/goto verbs type through the real prompts
with a per-char delay (default 35ms) so viewers see it typed; movement
uses bare keypresses with a pump-only settle to stay snappy. Each verb
settles on an op-specific predicate where one exists (goto lands on the
target, follow grows the nav stack, toggle flips _active, modals open),
so the returned state is never stale.
rpc_smoke now drives the semantic path end-to-end (rename round-trip via
the prompt-fill seam), 11 green.
|
|
|
Run the TUI with --rpc <sock> and it renders normally while an asyncio
listener on the same loop lets another process drive it. Handlers touch
the UI directly (same loop, no thread hop), so every injected key has the
identical on-screen effect a keyboard would — the point for livestreaming.
Newline-delimited JSON. v1 methods: keys/text (raw injection, via the same
_press_keys the pilot uses; text can interleave wait:<ms> for a typed-out
look), state/view/screen/functions (introspection; screen() is a full
plain-text render of exactly what the viewer sees). No auth by design —
the socket is 0600, local only.
tests/rpc_smoke.py drives it end-to-end over a real socket (7 green).
|