# 07 — TUI `harness-tui`, binary `harness`. Single ratatui app over the engine; zero business logic — it renders `AppEvent`s 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()` → `AppEvent`s 3. 33 ms render tick Events mutate `AppState { sessions, current: SessionView { messages: Vec }, modal: Option, jobs, input }` and set a dirty flag; render at most once per tick. ## Streaming markdown rendering - `MessageView` caches rendered `Vec` 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 `JobRecord`s (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 `, `/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).