Port of the grr client of tai.py: client, SSE stream, session, store, router, shell, views,
chat, panes, file viewer and the unix control socket; GrrTui::run is the --tui entry.
Verified against tai.py --tui on a real grr server over a pty: 236 steps, screen text and
per-cell styles identical except clocks, spinner frames and server-made ids.
docs/tui.md holds the symbol table and gaps, docs/wiring/tui.md the integrator hooks.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Rules first in logging.md: R1-R12 plus a 27-event registry, every
site validated against the source before implementation.
- DebugLog: file/stdout/stderr sinks, sentence/json formats,
wipe-on-first-write for roots, append for TAI_CHILD, per-row
commit, prune to 20000 rows, never raises.
- Vault logs table with six indexes plus FTS triggers, sealed at
rest, read back through Store.log_query.
- Hooks: turns, completions (backend, model, ms, tokens, texts),
tool dispatch, approvals, vault mutations, scheduler, serve,
CAP turns, telegram updates, uncaught exceptions.
- Secrets never logged: redact plus LOG_MASKED_ARGS.
- 13 new tests (1222 + 16 green).
The loop now sleeps until woken (no 1 s heartbeat); SIGCHLD and a drained pty writer wake it, the latter also fixes a busy writer callback. Partial layout no longer climbs the whole tree for all-auto panels: arrange() skips panels whose box is unchanged and that nothing under them asked about (a pending flag that survives hidden ancestors), measured() keeps measures until a touch drops them, and only really arranged panels are diffed; one leaf growing in a 301-panel all-auto tree costs 0.29 ms instead of 4.6 ms. A scroll offset written out of range arranges that panel for its clamp. TuiWatched is the base for state objects (TuiStyle and TuiSpan are ones): a write to a public field touches every panel holding it, directly or through a watched list, dict or set. TuiTerminal keeps converted screen rows keyed by content (200x50 scroll-by-one frame 16.0 ms to 1.8 ms). The terminal scroll uses left/right margins (DECLRMM) when the startup DECRQM probe says the terminal has them, so the sidebar beside a transcript is not touched (3-line scroll at 80x24: 1354 B to 734 B in the emulator); TuiVtScreen implements the margins and answers DECRQM. The scroll cost model counts runs and style switches the way diff_row writes them and its constants were swept over 1200 recorded frames. animate() of x, y, w, h now starts from the drawn place and writes whole cells.
Not verified on a real terminal: the margin path (tmux has no DECLRMM). Remaining gaps are listed in tui.md.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Each bash/zsh/fish instance resolves a stable dated id (no install hook);
transcripts and turn state scope per session so every new shell starts
fresh, while vault data stays shared. Parent links accrete in the
shell_sessions table with stub rows for outer shells, and /ctx renders
the ancestor path plus children. TAI_SESSION pins or overrides the scope.
Closes every gap listed under "Lazy rendering gaps" in tui.md.
State a panel keeps in a list, dict or set (nested, depth cap 4) or a TuiStyle is watched, so an in-place mutation repaints the panel with no touch(). Layout arranges only the subtrees that asked, from the nearest ancestor with a fixed size, and a touched auto-sized panel is measured again so text that grows relays out; app.verify checks every partial pass against a full arrange. A log that scrolls moves the terminal's rows (DECSTBM, IND, RI) when an exact cost count says it writes less, and keeps each line's converted row (cap 4096), so a 3-line scroll at 200x50 writes 1.8 KB instead of 7.9 KB and a line appended while pinned costs 0.6 ms. The main loop sleeps until a timer, an animation, a half-read key or a wake() (touch from any thread, pty, SIGWINCH, quit, end of input): 5 s idle at 200x50 is 7 loop iterations and 4 ms CPU instead of 147 and 20 ms. The chat composer resizes from a TuiChange event and its spinner timer only exists during a turn, instead of 0.2 s and 0.08 s polls.
Found by the new random-tree storm and fixed: a padding, border or alignment change that left the rect the same left stale cells (it was in v1.34.0); a bordered, padded log built a pad row too wide. Known and documented, not changed: animate() on x, y, w, h cannot work because positions and sizes read ints only.
Tests: TuiObservedTests (every mutator), TuiLayoutTests (random trees, partial equals full), TuiScrollTests (VT replay, overlays, resizes), TuiLoopTests (the real _main on pipes: wakeups, latency, timers, signals); twelve mutants of the new code were each caught. VERSION is left to the pending 1.35.0 work in the same file.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>