# 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.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) Rendered only when at least one of these holds: 1. The session has been held in its current activity state longer than **30 seconds** (`activity_since` from #102). A freshly-active session stays one line. 2. Its worktree holds blocked work: `blocked_work` is `Some` and not clear (`InTheWay::is_clear()`), rendered in `formatInTheWay`'s phrasing on web and as `⚠ ` on the TUI. 3. It has exited: `status == Dead`. 4. An agent-reported state (#105) of `Error` is in force (see Precedence). Otherwise line 2 is absent — never an empty placeholder row. When present, line 2 reads left to right: ``` [] [ | exited] [] ``` `` is a single filled glyph (`●` on both clients) carrying the activity state's colour, and `` 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. `` 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 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 | 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`. 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.