Every colour the TUI painted on its own chrome was a named ratatui colour spelled out at the use site, so nothing could be themed and the selected card had broken contrast: the highlighted card's black-on-cyan base was patched with an activity colour chosen for the dark card background, leaving AwaitingInput yellow on light blue. Roles now live in `palette.rs`, named for meaning. A palette holds one role set per surface — card and selection — plus the panel border in its focused and unfocused forms, a focus accent, a group-row style for the sidebar grouping, the status-bar mode indicators and error text. Each surface carries its own activity colours, so the selected card's dot keeps carrying state without wearing a colour picked for another background. Values are `Color::Rgb`, so contrast is asserted rather than left to the terminal theme: every role clears WCAG AA on its own surface, and a sidebar test fails if a card-surface state colour is painted onto the selection background. PTY cell colours stay a passthrough of what the program in the pane emits — they are not chrome and not a palette role.
6.8 KiB
SIDEBAR.md — the session card contract
One rule for how a session is rendered as a card in the sidebar, so the web sidebar (#107) and the TUI sidebar (#108) describe a session the same way. Read this before changing either. Both clients implement exactly what is written here; if it needs to change, change it here first.
A card has line 1 always and line 2 only when line 2 has something to say.
Line 1 — name + actions
- Left: the session name —
metadata.nameif present, else the command line (same fallback both clients already use). - Right: two actions anchored to the top-right corner: spawn similar
(
⧉) and kill (✕). In the TUI these are keyboard affordances shown on the highlighted row; in the web they are buttons. The glyphs are shared so muscle memory transfers. - Actions reserve their fixed width; the name truncates with
…before it can ever reach them. Overlap between the name and the actions is a defect — that overlap is what started this milestone.
Line 1 carries no badges. The ⚠ blocked work badge (#83) and the exited
badge move off this row into line 2.
Line 2 — meta line (adaptive)
Rendered only when at least one of these holds:
- The session has been held in its current activity state longer than
30 seconds (
activity_sincefrom #102). A freshly-active session stays one line. - Its worktree holds blocked work:
blocked_workisSomeand not clear (InTheWay::is_clear()), rendered informatInTheWay's phrasing on web and as⚠ <summary>on the TUI. - It has exited:
status == Dead. - An agent-reported state (#105) of
Erroris in force (see Precedence).
Otherwise line 2 is absent — never an empty placeholder row.
When present, line 2 reads left to right:
<dot> <duration> [<detail>] [<blocked work> | exited] [<command>]
<dot> is a single filled glyph (● on both clients) carrying the activity
state's colour, and <duration> follows in the same colour. There is no state
word and no second badge: the dot exists so the state is still legible when a
row is dimmed or the terminal renders the duration in the default fg. The dot
and duration are unconditional parts of line 2; the bracketed fragments appear
only when their trigger holds. A payload that predates #102 (no usable
activity_since) contributes no duration — the dot still renders.
<detail> is an agent-reported string (#105) — e.g. why a session is in
Error. It sits right after the duration, in the muted style, and shows only
while an agent-reported state is in force (see Precedence): when the report
lapses at its TTL or is replaced, the detail goes with it. The detail
truncates with … ahead of the trailing fragments — a long detail never
pushes <blocked work>, exited, or <command> off the line, and neither
client ever lets it wrap.
<command> is optional and muted: the session's command line, shown only when
the rendered name came from metadata.name and differs from the command (the
web has always surfaced this; the TUI may omit it).
Colour per activity state
The state word is never spelled out — the colour carries it.
| Activity state | Web token | TUI palette role |
|---|---|---|
Active |
--live |
active |
Idle |
--muted |
idle |
AwaitingInput |
--warn |
awaiting |
Error |
--danger |
error |
Web tokens resolve per theme automatically (styles.css defines dark, light,
and prefers-color-scheme variants for each). The TUI names roles, not
colours: a role's value comes from the active palette
(maestro-tui/src/palette.rs), never from a colour spelled out at the use
site.
A palette holds one role set per surface — the card surface every card sits on, and the selection surface the highlighted card swaps in — so each state role resolves to the value picked for the surface it is painted on. The highlighted card therefore takes its state colours from the selection surface; a colour chosen for the card background is never painted onto the selection background. Contrast is a property of the palette (values are RGB) and is asserted in the palette's tests, not left to the terminal theme.
The duration text inherits its state's colour; the blocked-work, exited and
command fragments use the surface's muted role, since the ⚠ glyph already
signals severity.
Duration format
Time spent in the current activity state, sourced from activity_since
(#102) — not time since spawn, and not time since last PTY output
(last_activity_at). Both clients compute it identically:
< 60s→ whole seconds, floored:30s< 60m→ whole minutes, floored:4m≥ 60m→ whole hours, floored:2h
No days unit, no seconds inside minutes, no rounding up. Floor everywhere so the counter never shows a duration the state hasn't actually reached yet. Clock granularity: re-render at least once every 30s while visible so the cutover points land within one tick.
Precedence
An agent-reported ActivityState (#105) overrides the daemon's inference for
five minutes (the TTL is fixed by the daemon; re-reporting extends it).
When the TTL expires, inference resumes and the session's reported detail —
if any — clears with the report. Clients never blend the two: whichever source
is in force supplies both the colour and the clock for activity_since.
Two rulings specific to a reported Error (#114):
- The five-minute TTL applies to
Errorlike to any other state. A session that failed is still failed after five minutes, but a card pinned red forever lies once the session moves on; when the report lapses the card returns to the daemon's own inference — usuallyIdlefor a session that has gone quiet. - Only a new report clears an
Errorbefore its TTL. The nextActivityStateof any kind replaces it (with #106's hook that is typicallyPreToolUse→Activewhen the user prompts again). PTY output and process changes do not clear it — only the agent itself may withdraw what the agent itself reported. Re-reportingErrorextends the TTL rather than stacking reports.
If a client receives an ActivityState variant it does not know, it must not
fail rendering: treat it as Idle (neutral/dim, line-2 rules apply as usual)
until the daemon sends a variant it does know. Rust clients deserialize
activity leniently for the same reason.
Leniency runs one way only. It covers a client reading a newer daemon, where
dropping the whole session would be worse than dimming one card. A report
arriving at POST /api/sessions/:id/activity is parsed strictly and an unknown
state is a 4xx: an agent posting a state the daemon does not know is a bug in
the agent, and answering 204 would hide it.