agent-tempo · TUI design system

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.

v 0.1
src/tui/utils/theme.ts
src/tui/utils/platform.ts
src/tui/utils/format.ts
01 / BRAND

Brand mark & voice

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.

▕ ▏ ▁▁▕ ▏▁▁ ▁▁▁▕ ▏▁▁▁ ▁▁▁▁▕ ▏▁▁▁▁ ▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
agent-tempo
Multi-session orchestration via Temporal
v0.26

Voice

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".

Primary glyph

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.

Motif restraint

Musical glyphs appear in exactly three places: the splash metronome, the prompt indicator, and the conductor star . Do not introduce new musical symbols.

02 / COLOR

Color tokens

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.

Surface & structure

bg
#0A0E12
Primary canvas. All TUI views sit on this.
bgTitle
#141820
TitleBar / pinned chrome tint. Slightly lifted surface.
inputBg
#1A1F28
Out-bound message row background. Signals the user's own voice.
border
#2A3140
Hairline dividers and panel edges.

Foreground

text
#FAF3EE
Default foreground. Warm cream, not pure white.
textMuted
#9CA3AF
Secondary copy. Player part lines, third-party bodies.
dim
#6B7280
Metadata, timestamps, separators (·), hint text.
muted
#3D4555
Disabled state, version strings, placeholder shade.

Accent & signal

accent
#E07A5F · terracotta
Brand mark, selected palette item, prompt glyph, inbound headers.
success
#81C784
Connected status, active phase, ✓ markers.
warning
#F2CC8F
Disconnected/blocked phase, "No conductor", conductor star ★.
error
#EF5350
Connection failures, hard errors, destructive-action confirms.
NO_COLOR. When the NO_COLOR environment variable is set, every token resolves to undefined and Ink renders monochrome. Every component must remain legible without color — rely on glyphs, bold weight, and labels for meaning, never color alone.
03 / TYPOGRAPHY

Typography

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.

agent-tempo — typography roles
agent-tempo 0.26.0 Ensemble: my-band (3 players) alice active · main · tempo-soloist Building the React dashboard. alice 14:22 PR is up — needs a review when you have a moment. draft the release notes for v0.27 Press / for commands · @player to DM
RoleInk propsTokenUsed for
Brand / titlebold color={accent}accentTitleBar left, Splash wordmark, section headings.
Body defaultcolor={text}textPrimary message bodies, input value.
Body secondarycolor={textMuted}textMutedPlayer part lines, third-party message bodies.
Metadatacolor={dim}dimTimestamps, route labels, separators (·), overlay hints.
Placeholdercolor={muted}mutedDisabled prompt, version strings, empty-state tips.
Selectedbold color={accent}accentHighlighted palette/overlay row (▸ indicator).
Inbound headercolor={accent}accentSender name on ← alice 14:22 rows.
Status okcolor={success}success✓ Connected, active phase dot.
Status warncolor={warning}warning⚠ No conductor, disconnected phase, conductor star.
No Unicode box-drawing for hierarchy. The TUI never draws boxes around things; structure is carried entirely by indentation, blank lines, and color. This keeps the Yoga node count under control (see §09 Performance) and survives narrow terminals.
04 / ICONOGRAPHY

Glyphs & fallbacks

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.

Status icons

NameUnicodeASCIICodeSemantic use
active*U+25CFPlayer is attached and processing.
staleoU+25CBIdle — attached but awaiting input.
pending~U+25D4Booting; not yet attached.
blocked!U+25D0Draining or detached — disconnected.
terminatedxU+2715Gone — workflow terminated.

Role & affordance glyphs

NameUnicodeASCIICodeUsed on
conductor#U+2605Trailing marker on the conductor player.
player-U+2022Generic bullet in ensemble lists.
selected>U+25B8Selected row in palettes, overlays, pickers.
inbound<-U+2190Header prefix on messages received.
arrow->U+2192Conductor routing labels: alice → bob.
check+U+2714Splash "Connected" indicator.
scroll-up^U+2191"N more above" in scrolled lists.
scroll-dnvU+2193"N more below" in scrolled lists.
prompt>U+2669Input prompt glyph and out-bound message marker.
cursor_U+2588Blinking cursor block.
05 / LAYOUT

Shell anatomy

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.

agent-tempo tui — shell anatomy · 120×26
agent-tempo my-band · 3 players ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ← alice 14:22 PR is up — needs a review when you have a moment. ← bob 14:23 I'll take the backend changes. sounds good — push when ready also let's sync on the migration plan /recruit spawn a new player /status show all players /schedule list schedules /re ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── my-band · 3 players (2 active, 1 idle) · Connected

① TitleBar

Pinned top. Brand on left (accent bold), context on right (dim). Background bgTitle. One row.

② Scrollback

Native terminal scrollback. Completed messages flow into it as they scroll off the ConversationStream viewport.

③ ConversationStream

Live feed. Fits as many recent messages as rows allow, works backward from newest. Out-bound rows get an inputBg tint.

④ CommandPalette

Floating dropdown above the prompt when the user types / or @. Max 6 visible rows; scroll indicators above/below.

⑤ PromptArea

Pinned bottom. prompt + current value + cursor. Placeholder hint when empty, completion hint line above.

⑥ StatusBar

Pinned bottom-most. Single line: ensemble · player breakdown · schedule count · conductor warning · connection dot.

Spacing. All block separation is by blank lines — one blank between peers, two blanks between groups (player cards, message clusters). Indentation is two or three spaces; never tabs. A 3-space indent (INDENT) aligns continuation lines with message-header glyphs.
06 / COMPONENTS

Component inventory

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).

TitleBar · pinned top

TitleBar
agent-tempo my-band · 3 players

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).

StatusBar · pinned bottom

StatusBar
my-band · 3 players (2 active, 1 idle) · 1 schedule · ⚠ No conductor · Connected

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.

StatusOverlay · dismissible

/status
Ensemble: my-band (3 players) alice active · main · tempo-conductor Coordinating frontend / backend work. bob idle · feat/auth · tempo-soloist Waiting on design review. carol disconnected · main · tempo-critic ↑↓ scroll, Esc to dismiss

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.

CommandPalette · floating dropdown

palette (typed "/re")
↑ 2 more above /recruit spawn a new player /recruit-conductor recruit a conductor for this ensemble /recall read a player's message history /release release held players /restart restart a player /resume resume a paused ensemble ↓ 4 more below

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.

ConversationStream · message feed

Ensemble chat
← alice 14:22 PR is up — needs a review when you have a moment. alice → bob 14:23 ship the migration once tests pass sounds good — push when ready also let's sync on the migration plan

Three message roles are rendered distinctly:

  • Inbound direct: ← sender HH:MM header, text body.
  • Third-party (conductor routing): dim header with from → to HH:MM, textMuted body, capped at 4 lines with "… (N more lines)".
  • Out-bound: prefix, text body, full-row inputBg tint.

PromptArea · input

Prompt — idle & typing
recruit recruit-conductor recall /re Type a message, /command, or @player… ...

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.

Splash · landing screen

agent-tempo tui
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⢀⣀⠀⠀⠀ ⠀⠀⢠⡏⢳⡀⠀⠀ ⢠⠟⠀⠀⢻⡄ ⠟⠒⠒⠒⠒⠻ agent-tempo Multi-session orchestration via Temporal v0.26.0 ✓ Connected ★ my-band (3 players) • backend-team (2 players) • experiments (0 players) + Create new ensemble ↑↓ to select, Enter to continue

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.).

Wizard pattern · Recruit / Schedule / Create ensemble

/recruit — step 2 of 4
Recruit a new player · step 2 / 4 Name frontend-eng Type tempo-soloist Work dir /repos/my-app Task tempo-soloist single-focus engineer tempo-composer architect / planner tempo-critic reviewer tempo-tuner performance / quality ↑↓ select · Enter to confirm · Esc to cancel

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.

07 / STATE

Attachment-phase matrix

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?
attachedactiveactivesuccessyes
processingactiveactivesuccessyes
awaitingidlestaletextyes
drainingdisconnectedblockedwarningyes
detacheddisconnectedblockedwarningyes
bootingpendingpendingdimyes
gonegoneterminateddimno (terminal)
(unknown)unknownblockedtextno
Invariant. Never hand-pick a color or icon for a player — call phaseToColor(phase) and phaseToIconName(phase) then index statusIcons(). These lookup maps are frozen at module scope so repeated calls are allocation-free.
08 / ACCESSIBILITY

Accessibility & resilience

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.

NO_COLOR

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.

Unicode fallback

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.

Minimum size

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.

Keyboard-only

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).

Scroll indicators

Any windowed list (palette, status, ensembles) shows ↑ N more above / ↓ N more below in dim when content is clipped, never silently truncates.

Echo & latency

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.

09 / PERFORMANCE

Rendering rules

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.

One Text root per component

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.

Precomputed padding

Right-alignment (TitleBar, StatusBar) uses calculated space strings based on process.stdout.columns, not flex. Vertical centering (Splash) pads with \n repetitions.

Frozen lookup maps

Phase → label/color/icon maps are Object.freezed at module scope. No per-frame object allocation in hot paths.

Uncontrolled inputs

PromptArea keeps its own local state. Parent never re-renders per keystroke; it receives the final value via ref or on submit.

Viewport-only rendering

ConversationStream walks backward from the newest message and stops when the line budget is filled. Older messages are committed to native scrollback via <Static>.

Stable callbacks

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.

10 / USAGE

Do / don't

Short rules that keep additions consistent with the existing surface.

Do

  • Route all color access through THEME.
  • Use phase helpers (phaseToLabel, phaseToColor, phaseToIconName) for anything player-related.
  • Ship ASCII fallbacks alongside any new glyph.
  • Keep copy direct: "No conductor", "3 players (2 active, 1 idle)".
  • Indent continuation lines to 3 spaces to align with the prompt.
  • End scrollable surfaces with a dim key-hint line.

Don't

  • Introduce new colors — if a role feels missing, reuse a token or open a design review.
  • Draw Unicode borders around things; indentation + color is the whole layout system.
  • Use emoji; the metronome vocabulary (♩ ★ • ▸) is the full kit.
  • Rely on color alone to convey a state — always pair with a glyph or label.
  • Add <Box> layout inside leaf components; keep it single-Text.
  • Hardcode a phase check — phases change; the lookup maps don't.