Files
darman 420f00494f M0: scaffold Cargo workspace, core types, event bus, permission engine, storage actor
Sets up the Cargo workspace (harness-core, harness-tools, harness-providers, harness-mcp, harness-lsp, harness-app, harness-tui) and the ten architecture docs under docs/. harness-core gets its foundational types (session/message/part/model ids), an in-process event bus, a table-driven permission evaluate function, and a SQLite-backed storage actor with schema and roundtrip coverage. Every crate compiles empty and a bin stub prints its version.
2026-07-10 16:19:27 +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).