# 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 - Left: the session name — `metadata.name` if present, else the command line (same fallback both clients already use). - In the web client, **spawn similar** (`⧉`) and **kill** (`✕`) remain buttons anchored on the right. The TUI omits those glyphs because its actions are keyboard-driven and the icons do not identify their keys. - The name truncates with `…` at the available width. In the web client it must stop before the action buttons; in the TUI it may use the full row. Line 1 carries no badges. The `⚠ blocked work` badge (#83) and the `exited` badge move off this row into line 2. The name is editable in place (#125). In the web client, right-clicking a card opens a context menu whose **rename** entry swaps the name for a text field — Enter commits, Escape cancels. The TUI opens the same edit as a prompt, bound to `r` in the sidebar by default. Both send `PATCH /api/sessions/:id`; both start from the session's own name, never from the command it falls back to, and an empty field clears the name so the fallback resumes. ## 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: ``` [] [] [ | exited] ``` Its minimum content is therefore the dot and the command. `` is a single filled glyph (`●` on both clients) carrying the activity state's colour, and `` 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. `` 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. `` 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 ``, `exited`, or `` off the line, and neither client ever lets it wrap. `` is the blocked-work breakdown (#83) in diff notation, replacing the spelled-out `⚠ 3 uncommitted files, 2 unpushed commits`. It renders only while something would block an unforced kill, and reads: ``` + - ? ↑ ``` Each fragment is omitted when its count is zero. The line counts cover **tracked** changes only — staged and unstaged together, measured against `HEAD` — so a `git add` never makes the counter drop. Untracked files are counted as files, not lines: counting their lines means reading every one off disk, and `?3` already says what is there. A tree whose only tracked change carries no line delta — a mode change, a rename — shows `~` instead of the additions and deletions, since `+0 -0` would describe it wrongly. Only `+` and `-` take colour (`--live` / `--danger` in the web client, the surface's `active` / `error` roles in the TUI); `~`, `?` and `↑` stay muted, so the fragment reads as a diff rather than as an alarm. The spelled-out phrasing of `maestro_protocol::InTheWay`'s `Display` does not disappear: it is the web card's tooltip and the wording of the kill refusal, so no two clients describe the same tree differently. `` 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. In the **web client** the command's leading program is replaced by that tool's icon when it is one we have a mark for — Claude Code, Codex, opencode, Gemini CLI, Aider, Cursor, pi, and any shell — and only the remaining words of the command line follow as text. Recognition looks through a path, leading `FOO=bar` assignments and wrappers (`sudo`, `env`, `npx`, `pnpm dlx`, …); an unrecognised program keeps its full text, so the card never shows a mark standing for the wrong tool. The full command line stays in the card's tooltip either way. The **TUI** keeps the command as text: a terminal cannot be assumed to have a font carrying tool glyphs. ## 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 exited and command fragments use the surface's `muted` role, as do the diff stat's `~`, `?` and `↑` fragments. Only `+` and `-` step outside the surface's muted role, and they take the `active` and `error` roles of the same surface. ## 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.