Files
ai-harness/docs/07-tui.md
T
Erik Simon 8b16348b4c M0: scaffold Cargo workspace, core types, event bus, permission engine, storage actor
Seven-crate workspace per docs/01-architecture.md. harness-core gets the
domain types (Session/Message/Part/ToolState), a broadcast EventBus, the
last-match-wins wildcard permission evaluator, and a SQLite storage actor
(dedicated thread + mpsc, JSON-blob rows) with roundtrip tests. All other
crates are compiling stubs. CI runs fmt/clippy -D warnings/test.
2026-07-08 16:28:42 +02:00

2.9 KiB

07 — TUI

harness-tui, binary harness. Single ratatui app over the engine; zero business logic — it renders AppEvents and calls EngineHandle.

Layout

┌─ header: session title · agent · provider/model ─────────────┐
│ chat viewport (scroll)                                       │
│   user block / assistant markdown / tool cards               │
│   (tool card: status icon, title, collapsible output, diff)  │
├─ jobs strip when active: ● exp-1 running · ✓ fix-1 done      │
├─ input (tui-textarea, multiline, / command completion)       │
└─ status: tokens in/out · cost · spinner · keymap hints ──────┘

Event loop

tokio::select! over:

  1. crossterm::event::EventStream (keys, resize)
  2. bus.subscribe()AppEvents
  3. 33 ms render tick

Events mutate AppState { sessions, current: SessionView { messages: Vec<MessageView> }, modal: Option<Modal>, jobs, input } and set a dirty flag; render at most once per tick.

Streaming markdown rendering

  • MessageView caches rendered Vec<Line> per part, keyed (part_id, revision, width).
  • Finished parts render once through the pulldown-cmark → ratatui renderer (headings, bold/italic, fenced code, lists, blockquotes; syntect highlighting optional in M6).
  • The in-flight tail part re-renders only itself on PartDelta; only the portion after the last blank line gets full markdown treatment — earlier paragraphs of the same part are frozen into the cache. This bounds per-delta work.
  • Risk: ratatui has no incremental layout. The cache + tail-only strategy is the mitigation; fall back to a plain-text tail if profiling shows stutter.

Modals

  • Permission dialog (on PermissionAsked): permission key, pattern list, metadata — for edits, a colorized unified diff. Keys: y allow once, a allow always, n reject → engine.permission_reply(id, …). Multiple pending asks queue FIFO.
  • Device-code login (on AuthPrompt): shows user code + verification URL; dismisses when the flow completes.
  • Pickers (shared fuzzy-filter list widget): sessions Ctrl+S, agents (mode: Primary|All) Ctrl+A, models (grouped by provider) Ctrl+M.
  • Jobs pane Ctrl+J: lists JobRecords (alias, agent, state, age, objective); Enter drills into the child session view — a read-only live follow of its stream (trivial since all sessions publish to the same bus).

Keybinds & commands

  • Esc aborts the running turn / closes modal; Ctrl+C twice quits.
  • Slash commands: /model, /agent, /new, /sessions, /login <provider>, /compact, plus markdown-defined commands with completion.

Terminal hygiene

  • Logging via tracing to ~/.local/share/ai-harness/log/ (the TUI owns the terminal — nothing writes to stdout).
  • Panic hook + Drop guard restore the terminal (disable raw mode, leave alternate screen).