This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/docs/SIDEBAR.md
T
Miguel Palhas c118020b29 docs(maestro): make sidebar card line 2 unconditional
Line 2 was rendered only past a 30s activity hold, blocked work,
exit, or reported Error, and <command> was gated behind a
metadata.name comparison and TUI-optional. Both worked against
scanability: cards changed height as sessions aged, and fresh
sessions hid their command.

Card is now always two lines. The 30s hold rule survives, demoted
to gating only the <duration> fragment. <command> is unconditional
in both clients.

Closes #116
2026-08-25 12:24:56 +01:00

6.3 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 is always two lines: line 1 and line 2.

Line 1 — name + actions

  • Left: the session name — metadata.name if 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)

Line 2 always renders — never an empty placeholder row either, since its unconditional fragments (see below) always have something to show. Its fragments are adaptive: some show only when their trigger holds. The line itself is not.

Line 2 reads left to right:

<dot> [<duration>] [<detail>] [<blocked work> | exited] <command>

Its minimum content is therefore the dot and the command.

<dot> is a single filled glyph (● on both clients) carrying the activity state's colour, and <duration> follows in the same colour when shown. 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 is an unconditional part of line 2; the bracketed fragments appear only when their trigger holds.

<duration> renders only once the session has been held in its current activity state longer than 30 seconds (activity_since from #102) — under that threshold the duration is absent and the dot renders alone. A payload that predates #102 (no usable activity_since) likewise contributes no duration — the dot still renders. This rule now governs only the duration fragment; it no longer decides whether line 2 exists.

<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 unconditional and muted: the session's command line, shown on every card in both clients — no omission in the TUI, no name-vs-command comparison gating it. A session named by its command line then shows the same string on both lines; that is accepted, the price of a row that never changes height.

Colour per activity state

The state word is never spelled out — the colour carries it.

Activity state Web token ratatui colour
Active --live Color::Green
Idle --muted Color::DarkGray
AwaitingInput --warn Color::Yellow
Error --danger Color::Red

Web tokens resolve per theme automatically (styles.css defines dark, light, and prefers-color-scheme variants for each). The TUI uses the plain ratatui named colours above; do not introduce RGB values or a new palette module for this.

The duration text inherits its state's colour; the blocked-work, exited and command fragments use the muted colour (--faint / default style), 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 Error like 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 — usually Idle for a session that has gone quiet.
  • Only a new report clears an Error before its TTL. The next ActivityState of any kind replaces it (with #106's hook that is typically PreToolUse → Active when 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-reporting Error extends 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.