SPEC-0001: The TUI
Overview
harness (no args) opens a keyboard-driven dashboard onto the daemon: see
every harness and its state, hop into any one as a live terminal, switch
between profiles ("configurations"), and start/stop/edit — without ever
touching systemctl, tmux, or $EDITOR unless you want to. Built with
Bubble Tea / Bubbles / Lip Gloss / Huh (📝 ADR-0001) as a thin client of the
daemon protocol (SPEC-0002). Layouts, visual direction, and the design
exploration live in design.md and docs/design/.
Requirements
Requirement: Mode Machine
The app SHALL be a small mode machine with two primary modes — Dashboard (the list/overview, default landing screen) and Attached (full-attention live terminal for one harness) — plus overlays that can appear over either mode: command palette, profile switcher, harness form, confirm dialogs, and help.
Scenario: Detach returns home
- WHEN the user detaches from an attached harness (
Escor detach chord) - THEN the TUI returns to the Dashboard and the harness keeps running
Requirement: Dashboard
The Dashboard SHALL show a list of harnesses filtered to the active profile
(with a toggle to show all), each row carrying status glyph, name, state,
restart count (↻), and uptime/next-action, over a metadata sub-line carrying
that harness's config summary (cmd, prompt, or the prompt_file path,
model, backend, last exit,
pid). It SHALL show a detail/peek pane for the selected harness — a live
read-only tail of its output (streamed via SPEC-0002 snapshot+tail) and
nothing else. The list pane SHALL be sized to its content rather than to a
fixed fraction of the window, bounded by a floor and a ceiling, so unused
columns go to the peek. The header SHALL show app name, active profile, and
daemon identity (local or user@host); the footer SHALL be a key bar (?
expands to full help).
Scenario: Metadata reads beside the harness, not inside the preview
- WHEN the user looks at a harness's cmd, backend, exit code, or pid
- THEN they are on the sub-line under that harness's own row, and the peek pane carries only the head line and the guest's screen
The peek pane SHALL hold a read-only attach session (📝 ADR-0008) on the selected harness, sized to the pane, so the guest's PTY is sized to its viewer under the same smallest-attached-wins policy as attached mode (📝 ADR-0003) — the Dashboard SHALL NOT define a second sizing mechanism. It SHALL report no viewport at all when the window size is unknown, and SHALL hold no session while attached.
Scenario: Glance before you hop
- WHEN the user moves the selection to a different harness
- THEN the peek pane switches to a live read-only view of that harness without entering attached mode, and releases the session it held on the previous one
Scenario: The preview sizes the guest
- WHEN a harness nobody has attached to is selected on the Dashboard
- THEN its PTY is resized to the peek pane and the guest is signalled, so a full-screen agent lays out at the size it is actually being displayed at rather than at the size it was spawned with
Scenario: Hopping in takes the whole window back
- WHEN the user attaches (
↵) to the harness the preview is showing - THEN the preview's session is released before the attach's is opened, so smallest-attached-wins cannot clamp the full-window attach to the pane; on detach the preview takes the harness back
Scenario: Peek without a session
- WHEN the peek has no session yet (the selection has not settled) or the daemon cannot serve one, and the guest's PTY is larger than the pane
- THEN the peek reconstructs the guest's screen at the guest's viewport and crops it to the pane, naming that viewport, rather than reflowing the tail into the pane's geometry
Requirement: Attached Mode
Attached mode SHALL render the harness's actual terminal full-width/height
from the daemon's x/vt screen — colors, cursor, and TUI apps inside it all
work; keystrokes forward straight to the PTY. A thin status ribbon SHALL
always show which harness is being driven, its state, and the detach hint. A
rebindable detach chord (default Ctrl-b d, tmux-muscle-memory-friendly) or
Esc-Esc SHALL return to the Dashboard. Read-only attaches (📝 ADR-0008) SHALL
show a visible read-only badge and ignore input.
Scenario: Driving a live agent
- WHEN the user types while attached in interactive substate
- THEN the keystrokes go straight to the harness PTY
Scenario: Read-only badge
- WHEN a harness is attached in read-only mode
- THEN a
👁 read-onlybadge is visible and input is ignored
Requirement: Scrollback Substate
From attached mode, Ctrl-b [ or PgUp SHALL enter a scrollback substate
that freezes the view and enables ↑/↓/PgUp/PgDn/g/G navigation and /
search over the daemon-owned scrollback (📝 ADR-0007). q/Esc SHALL exit
scrollback back to live. History lines are sanitized before display (#280):
contentless lines are dropped and consecutive status-chatter lines (identical
except for a ticking counter or spinner glyph) collapse to one, so the frozen
chat shows what the agent did rather than a line per second a tool ran.
Scenario: Chatter collapses in frozen history
- WHEN the log tail carries repeated per-tick status lines for a running
tool (
✻ Working (3s)…(4s)…) - THEN the frozen scrollback shows one status line for that run, and the tool invocation line itself remains visible
Scenario: Searching history
- WHEN the user presses
/in scrollback and enters a term - THEN matches in the scrollback ring are navigable without disturbing the live harness
Requirement: Harness Hop
[ / ] SHALL hop to the previous/next harness directly from attached mode —
one keystroke, without returning to the Dashboard. The hop is the product's
signature interaction and SHOULD feel instant and physical (subtle
slide/status-ribbon flash; springs via harmonica rather than linear easing).
Scenario: One-keystroke hop
- WHEN the user presses
]while attached to harness A - THEN the view switches to the next harness in the list, attached, with the ribbon reflecting the new harness
Requirement: Command Palette
Ctrl-k / : SHALL open a command palette that fuzzy-matches over verbs
and harness names (attach crush-worker, restart reduit-agent,
profile signal-ops, new). The palette SHALL mirror the scriptable CLI
verbs 1:1 so the palette and CLI never drift.
Scenario: Verb plus target
- WHEN the user types "rest redu" in the palette
- THEN
restart reduit-agentis offered and executes on Enter
Requirement: Profile Switcher
p SHALL open a profile switcher listing [profile.*] entries with
description and member count. Selecting a profile SHALL filter the dashboard
and offer to start that profile's stopped harnesses — non-destructively:
harnesses outside the profile keep running (📝 ADR-0006).
Scenario: Non-destructive switch
- WHEN the user switches from profile A to profile B and accepts "start stopped"
- THEN B's stopped members start, and A's running members keep running
Requirement: Harness Form
n (new) and e (edit) SHALL open a Huh form over the harness schema
(harness/args/prompt/prompt_file/model/auto_accept/max_turns/quiet/
schedule/workdir/env_file/restart_delay/restart/backend/
tmux_socket/description/enabled/harvest_trajectory/mcp_allow/
profile membership) that writes back to harness.toml (📝 ADR-0006 — file is
truth). e SHALL pre-fill from the existing harness. An "edit raw TOML"
escape hatch SHALL open $EDITOR.
Scenario: Create without leaving the TUI
- WHEN the user completes the
nform - THEN the new harness lands in
harness.toml, the daemon reloads, and the harness appears on the dashboard
Requirement: Lossless Edit Round-Trip
The e save path deletes the whole [harness.<name>] table and re-renders it
from the form, so the form SHALL carry EVERY config key core.Harness defines,
not the subset the daemon's HarnessInfo projection exposes. Each key SHALL be
pre-filled from the config file (📝 ADR-0006 — file is truth) and re-emitted on
save, so an edit that changes one field leaves every other key byte-identical
after a reparse.
Adding a key to the harness schema SHALL include adding it to the form; a key
the form does not carry is silently deleted from harness.toml on the next
unrelated edit, with no error surfaced to the operator.
Scenario: Editing an unrelated field
- WHEN an operator presses
eon a harness configured withtmux_socket,harvest_trajectory, ormcp_allowand changes only the description - THEN the rewritten table still carries those keys and re-parses to a harness equal to the original but for the description
Scenario: A deliberately empty capability scope
- WHEN an operator presses
eon a harness configuredmcp_allow = []— locked out of the MCP facade entirely — and changes an unrelated field - THEN the rewritten table still carries the empty scope, rather than
omitting the key and letting the parser's
["read"]default silently grant the harness back read-class facade access
Scenario: A new schema key
- WHEN a field is added to
core.Harnesswithout a matching form field - THEN the form's round-trip guard fails, rather than the omission surfacing later as silent config loss
Requirement: Confirmation Guards
Stop, restart, and delete SHALL present a small confirm dialog
(destructive-action guard), skippable via a --yes-style setting.
Scenario: Accidental stop
- WHEN the user presses
xon a running harness - THEN a confirm dialog intercepts before anything is signaled
Requirement: State Presentation
The TUI SHALL render lifecycle states (SPEC-0003) with paired glyph + color:
● running (green), ◐ degraded/flapping (amber), ◌ starting/restarting
(cyan), ○ stopped (dim), ✖ failed (red), 👁 read-only (blue accent).
Colors SHALL use Lip Gloss adaptive palettes for light/dark terminals, degrade
via colorprofile on 256/16-color or mono terminals, and never carry meaning
alone — the glyph always accompanies the color (colorblind-safe).
Scenario: Mono terminal
- WHEN the TUI runs in a monochrome terminal or degraded SSH client
- THEN state remains fully legible from glyphs and text
Requirement: Keybinding Registry
All bindings SHALL be declared through the Bubbles key.Binding registry so
help renders them and a future config can remap them. Defaults: single-key
verbs on the dashboard (s/x/r/e/n/p), vim-ish scroll (j/k/g/G), /
search everywhere, ? help everywhere, Ctrl-k palette everywhere.
tmux-adjacent chords (Ctrl-b d, Ctrl-b [) are provided but never
required — Esc always works.
Scenario: Discoverable keymap
- WHEN the user presses
?in any mode - THEN the full current keymap renders from the binding registry
Requirement: Zero And Error States
The TUI SHALL design, not skip, its edge states: no daemon (offer to start
it inline, don't just error); no harnesses / empty profile (friendly
zero-state with "press n to create your first harness"); config parse
error on reload (non-fatal banner "using last-good config; line 12: …" —
never a crash, 📝 ADR-0006); daemon disconnect (reconnecting overlay —
harnesses are fine, only the view dropped, 📝 ADR-0002); flapping harness
(the ◐ row expands to show last exit code + backoff countdown, one keystroke
to logs).
Scenario: Daemon not running
- WHEN
harnessstarts and no daemon socket is found - THEN the TUI offers to start the daemon inline instead of printing an error and exiting
Scenario: Bad config reload
- WHEN a TOML reload fails to parse
- THEN the daemon keeps the last-good config and the TUI shows a non-fatal banner with the parse location
Out of scope for v1
- Tiled multi-pane (several harnesses on screen at once) — 📝 ADR-0003 keeps it possible; v1 is single-attach + fast hop.
- Full tmux copy-mode selection semantics — v1 is scrollback + search.
- Mouse-first interaction — keyboard-first; mouse is a bonus.
Related Artifacts
Direct relationships declared in YAML frontmatter (per ADR-0023 / SPEC-0018). Run /sdd:graph chain SPEC-0001 for the transitive view.