diff --git a/CLAUDE.md b/CLAUDE.md index 50ec101..3d58eaf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,6 +66,10 @@ See `docs/INSTALL.md` for end-user install instructions. collisions, what blocks a kill, what `--force` retains, and what reclaim touches. Read it before changing anything that creates or removes one. +`docs/SIDEBAR.md` is the contract for session cards in the web and TUI +sidebars — line layout, activity colours, duration format, state precedence. +Read it before changing sidebar rendering. + `nix run .#dev` (or `just dev`) brings up the whole local stack under process-compose: a debug daemon on 127.0.0.1:7071, then Vite on :5173 once the daemon answers `/api/info`. The SPA proxies `/api/*` back to the daemon, so only diff --git a/docs/SIDEBAR.md b/docs/SIDEBAR.md new file mode 100644 index 0000000..c3e5eb4 --- /dev/null +++ b/docs/SIDEBAR.md @@ -0,0 +1,92 @@ +# 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] +``` + +## 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 and exited +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 +the TTL the report carries; when the TTL expires, inference resumes. 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.