Visual language, tokens, and component anatomy for the agent-tempo terminal UI. Warm-neutral dark palette, monospace-only typography, and a metronome motif shared with the conductor/player coordination model.
The product coordinates multi-session Claude Code workflows via Temporal. Musical vocabulary — conductor, player, ensemble, cue, tempo — is the source of the visual motif. A pendulum-and-triangle metronome is the primary mark; the quarter note ♩ is the in-product glyph for user input.
Direct, technical, musician-adjacent. Avoid jargon for its own sake but use the ensemble vocabulary consistently — a session is a "player", a terminated session is "gone", a held player is "released".
The quarter note ♩ (U+2669) fronts the user's input prompt and every out-bound message. It is the only decorative glyph allowed on bare text lines.
Musical glyphs appear in exactly three places: the splash metronome, the prompt indicator, and the conductor star ★. Do not introduce new musical symbols.
Thirteen named colors cover the entire TUI. Warm cream foreground (#FAF3EE) on a near-black warm-cool neutral (#0A0E12), with a terracotta accent (#E07A5F). Status colors are pulled from the pastel-terminal family so they sit comfortably with the accent instead of fighting it.
One family: the terminal's monospace. Hierarchy is expressed through bold, color tokens, and a small spacing vocabulary — there are no type-size options in a TUI. Spec the exact Ink <Text> props for each role.
| Role | Ink props | Token | Used for |
|---|---|---|---|
| Brand / title | bold color={accent} | accent | TitleBar left, Splash wordmark, section headings. |
| Body default | color={text} | text | Primary message bodies, input value. |
| Body secondary | color={textMuted} | textMuted | Player part lines, third-party message bodies. |
| Metadata | color={dim} | dim | Timestamps, route labels, separators (·), overlay hints. |
| Placeholder | color={muted} | muted | Disabled prompt, version strings, empty-state tips. |
| Selected | bold color={accent} | accent | Highlighted palette/overlay row (▸ indicator). |
| Inbound header | color={accent} | accent | Sender name on ← alice 14:22 rows. |
| Status ok | color={success} | success | ✓ Connected, active phase dot. |
| Status warn | color={warning} | warning | ⚠ No conductor, disconnected phase, conductor star. |
Every glyph has an ASCII fallback for SSH sessions and non-UTF-8 locales. Unicode is detected via WT_SESSION, LANG, and platform; when unavailable, statusIcons(false) substitutes safe ASCII. Never ship a glyph without a fallback.
| Name | Unicode | ASCII | Code | Semantic use |
|---|---|---|---|---|
| active | ● | * | U+25CF | Player is attached and processing. |
| stale | ○ | o | U+25CB | Idle — attached but awaiting input. |
| pending | ◔ | ~ | U+25D4 | Booting; not yet attached. |
| blocked | ◐ | ! | U+25D0 | Draining or detached — disconnected. |
| terminated | ✕ | x | U+2715 | Gone — workflow terminated. |
| Name | Unicode | ASCII | Code | Used on |
|---|---|---|---|---|
| conductor | ★ | # | U+2605 | Trailing marker on the conductor player. |
| player | • | - | U+2022 | Generic bullet in ensemble lists. |
| selected | ▸ | > | U+25B8 | Selected row in palettes, overlays, pickers. |
| inbound | ← | <- | U+2190 | Header prefix on messages received. |
| arrow | → | -> | U+2192 | Conductor routing labels: alice → bob. |
| check | ✓ | + | U+2714 | Splash "Connected" indicator. |
| scroll-up | ↑ | ^ | U+2191 | "N more above" in scrolled lists. |
| scroll-dn | ↓ | v | U+2193 | "N more below" in scrolled lists. |
| prompt | ♩ | > | U+2669 | Input prompt glyph and out-bound message marker. |
| cursor | █ | _ | U+2588 | Blinking cursor block. |
The TUI has five pinned regions and one scrolling canvas. Minimum terminal size is 80×24; below 60×15 during resize a soft warning appears. Every component renders as a single <Text> root to keep Yoga under 10 nodes.
Pinned top. Brand on left (accent bold), context on right (dim). Background bgTitle. One row.
Native terminal scrollback. Completed messages flow into it as they scroll off the ConversationStream viewport.
Live feed. Fits as many recent messages as rows allow, works backward from newest. Out-bound rows get an inputBg tint.
Floating dropdown above the prompt when the user types / or @. Max 6 visible rows; scroll indicators above/below.
Pinned bottom. ♩ prompt + current value + █ cursor. Placeholder hint when empty, completion hint line above.
Pinned bottom-most. Single line: ensemble · player breakdown · schedule count · conductor warning · connection dot.
Each component is documented as a rendered mock plus the exact tokens it consumes. Components live in src/tui/components/ and must render as a single <Text> root (see §09).
Single-row banner. Brand stays hard-left at 2-space inset; right-hand context is padded to the right edge via calculated spaces — never a flex <Box>.
Tokens: bgTitle, accent (brand), dim (context).
Comma-separated phase breakdown + schedule count + conductor warning + connection dot. Separators are always · (U+00B7) in dim. gone players never appear here.
Phase buckets: active, idle, disconnected, pending. See §07 for the full mapping.
Player cards: icon + name + conductor star, then dim detail line (phase · branch · type), then optional part in textMuted. Cards separated by a blank line. Hint row always in dim at the bottom.
Anatomy: glyph (phase color) → bold name → warning-colored ★ → detail row → muted part. Long lines wrap at a 4-space hanging indent.
Max 6 visible rows. The selected row uses accent color + bold + a leading ▸. Non-selected rows use success green for the command token (a terminal convention for runnable things) and muted for description.
Tokens: accent, success, muted, dim.
Three message roles are rendered distinctly:
Three states: typing (value + cursor, with optional tab-completion hint line above in muted), empty (cursor + placeholder hint in dim), disabled (... in muted) during wizards or splash.
Tokens: accent (♩, cursor, completion chosen), text, dim, muted.
Vertically centered. The metronome is a 3-frame braille animation at 30×12 cells (swings left → center → right every 300ms). Ensemble list shows up to 5 with up/down scroll indicators. Selected row uses ▸ in accent + bold.
States: connecting (spinner in warning), connected & loading (⟳ Discovering ensembles… in dim), connected & ready (list + "+ Create"), connected & empty (No ensembles running.).
Wizards are a stack of labeled rows: current step label in accent bold, filled rows in text, not-yet-set rows in muted. Picker overlay hangs below the active row with the same ▸ selector.
Applies to: RecruitWizard, ScheduleWizard, CreateEnsembleWizard.
Seven internal attachment phases collapse to five user-facing buckets. This table is the single source of truth for how a player looks at any moment — icon, color, and label. Maps directly onto src/tui/utils/format.ts.
| Internal phase | Bucket | Icon | Icon bucket | Color | Shown in StatusBar? |
|---|---|---|---|---|---|
| attached | active | ● | active | success | yes |
| processing | active | ● | active | success | yes |
| awaiting | idle | ○ | stale | text | yes |
| draining | disconnected | ◐ | blocked | warning | yes |
| detached | disconnected | ◐ | blocked | warning | yes |
| booting | pending | ◔ | pending | dim | yes |
| gone | gone | ✕ | terminated | dim | no (terminal) |
| (unknown) | unknown | ◐ | blocked | text | no |
The TUI must work on unstyled terminals, in SSH sessions without Unicode, and for users who disable color. None of these is a degraded mode — all three are supported paths.
When process.env.NO_COLOR is set, THEME resolves every key to undefined. Every meaning that lives in a color must also live in a glyph, label, or bold weight.
supportsUnicode() checks WT_SESSION, SSH env, LANG, and platform. ASCII fallbacks are declared next to every glyph in statusIcons() and the metronome drops to a single-character animation.
80 × 24 at launch or the process exits. A soft warning appears below 60 × 15 during live resize. All component layouts assume 80 columns of usable width.
Nothing in the TUI requires a pointing device. Every surface documents its keys in a footer line in dim color (e.g. ↑↓ scroll, Esc to dismiss).
Any windowed list (palette, status, ensembles) shows ↑ N more above / ↓ N more below in dim when content is clipped, never silently truncates.
Out-bound messages render optimistically from local echo with the ♩ glyph, then deduplicate against the server feed within a 30s window. Network blips never look like lost keystrokes.
Ink is React + Yoga layout over the terminal. Every Yoga node costs a full-screen recompute on change, so the design system's structural rules are inseparable from its visual ones.
Every component in src/tui/components/ renders one <Text> element at the top. Children must be nested <Text> or plain strings — these become ink-virtual-text (0 Yoga nodes). Avoid <Box> for layout in leaf components.
Right-alignment (TitleBar, StatusBar) uses calculated space strings based on process.stdout.columns, not flex. Vertical centering (Splash) pads with \n repetitions.
Phase → label/color/icon maps are Object.freezed at module scope. No per-frame object allocation in hot paths.
PromptArea keeps its own local state. Parent never re-renders per keystroke; it receives the final value via ref or on submit.
ConversationStream walks backward from the newest message and stops when the line budget is filled. Older messages are committed to native scrollback via <Static>.
Every useInput callback reads a ref.current bag instead of closing over state. The callback identity never changes, which keeps Ink's key-binding registration stable.
Short rules that keep additions consistent with the existing surface.