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.
2.9 KiB
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:
crossterm::event::EventStream(keys, resize)bus.subscribe()→AppEvents- 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
MessageViewcaches renderedVec<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:yallow once,aallow always,nreject →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: listsJobRecords (alias, agent, state, age, objective);Enterdrills into the child session view — a read-only live follow of its stream (trivial since all sessions publish to the same bus).
Keybinds & commands
Escaborts the running turn / closes modal;Ctrl+Ctwice quits.- Slash commands:
/model,/agent,/new,/sessions,/login <provider>,/compact, plus markdown-defined commands with completion.
Terminal hygiene
- Logging via
tracingto~/.local/share/ai-harness/log/(the TUI owns the terminal — nothing writes to stdout). - Panic hook +
Dropguard restore the terminal (disable raw mode, leave alternate screen).