Skip to content

session-activity

Provenance

  • Source: .spec/spexcode/spec-dashboard/dashboard-ui/session-console/session-activity/spec.md
  • Source SHA-256: 02dd270c2cf50a9856e6f3b59c7bbb390acee5b10d7acf3c5a86bade9631c5a9

session-activity

raw source

Each worker already narrates itself: Claude Code keeps its terminal title set to a short summary of the task in front of it, updated every turn. That signal sits unused in tmux. Surface it on every session row so a glance answers what is each agent doing right now, not merely which session this is — the way a terminal tab renames itself to fit the work.

expanded spec

Capture (free, one call). A self-narrating agent sets its terminal title via an OSC escape; tmux records it as the pane title (never the window name — OSC titles don't touch it). Our worker runs one pane per session named with the session id, so listSessions reads every pane title in a single list-panes snapshot (paneTitles) — same shape and cost as the liveness snapshot — and, only when this session's harness declares its pane title to BE a self-summary, hangs a cleaned summary (paneActivity) on Session.activity. Whether a pane title is a self-summary is a harness capability (paneTitleIsSelfSummary, on the harness adapter — the one branch, data not a scattered if): Claude Code continuously writes its task summary into the title, so it qualifies; Codex does not — it sets the pane title to a spinner glyph + the cwd basename (the worktree FOLDER name), so deriving a session title from it would name the folder, not the task. For a non-self-summarizing harness activity stays null and the title falls through to the launch-prompt preview (below) — its task, never its folder. A genuine Claude Code self-summary always leads with a status glyph ( idle, a braille spinner frame while working); that glyph is the proof the title is the agent's own OSC summary and not tmux's default — which, from pane birth until the agent first speaks, is the host name (e.g. ser581555022561), and the app may flash a bare splash before its first task. So the glyph is required: a pane title without one is "not spoken yet" → activity is null, and the row keeps its launch-prompt placeholder rather than flickering through the host name and splash. The glyph gate alone is not quite enough: Claude Code also emits a glyph-LED splash of its own app name (✳ Claude Code) in the gap between pane birth and its first real task summary — it clears the glyph gate yet is the app naming itself, not the task, so it too is refused (a stripped summary equal to the bare app name → null) and the row holds its launch-prompt placeholder rather than flashing "Claude Code" for a tick. Once present and past those guards the glyph is stripped (the dashboard draws its own status; a frozen spinner frame is noise) and only the summary text is kept. Activity is live and never persisted — also null for any session that isn't up (offline / starting / queued), so a dead or booting row never shows a stale line. A tmux hiccup drops the line for one tick, never the session.

Render (one shared face, two variants). The shared session face ([[session-console]]'s SessionRow) centres on the title — the one best description of what this session is about, single-line with an ellipsis. The title prefers the worker's own live self-summary: once the pane title exists, the agent-generated activity line tracks what the agent is doing now. Before it exists (booting / queued / offline), a meaningful line from the declaration note or launch prompt (promptPreview) fills the title. A bare URL prompt is skipped when a later prose line exists. A human rename (name) still wins over all of these — the [[session-rename]] override stays authoritative everywhere.

The self-summary holds the line only while the agent is still producing it. A pane title is a byproduct, so the liveness gate removes it once the worker is no longer up; then the launch prompt becomes the fallback. A declaration's complete note stays readable in Timeline and review; its first meaningful line is only the fallback title when no rename or live activity exists ([[session-label]] owns the derivation). activity remains captured and rides the wire for every live working session.

There is ONE row face, and one thing flexes by surface: showAvatar. Every list surface — the two desktop lists and the phone's ([[mobile-ui]]) — renders the compact one-line face: the title followed by a single colour-coded status glyph (STATUS_GLYPH, painted by STATUS_COLOR) rather than the word — the exact word kept on the hover title for a11y — grouped into the three triage zones ([[session-console]]). The map-side board window (SessionWindow) keeps the avatar, the fixed spatial anchor that lets a session be cross-referenced against the avatars on the very nodes it edits; the console sidebar and the phone drop it (showAvatar={false}, redundant beside the title). Where the avatar is gone the fixed anchor is simply the row's slot in the ordered list, so the title still renarrates each turn without the row losing its place. (An older two-row variant — status word + op tally on a second line — lived on as the mobile list's face long after both desktop lists folded it into the glyph; it is retired, deleted with its compact prop rather than kept as a dead second implementation.)

The compact single line is a resting state, not a reason to widen the whole sidebar. The console list's default width is 204px (15% narrower than its former 240px default) and its row type steps down to the 10px caption token: the list is a dense index, and the saved user-resized width still wins. Resting rows share one explicit 18px line box with 5px vertical padding, yielding a consistent 28px content height whether or not a row has a nesting rail. A selected row reveals more context in place, but the expansion is capped at three lines. Three lines make the current task recognizable without letting one generated title turn the remaining sessions into a moving target; the row's tooltip and accessible name retain the complete text. Reveal is tied to selection, never hover. The small markers (status glyph, op tally) stay pinned to the selected title's first-line top-right while later lines use the available width. That wrapped-reveal float is the only remaining job of .sess-meta's full-width base rule; the meta line stays the parking spot for any further at-a-glance metadata added later. When the selected row belongs to the nesting forest, its leading connector/fold pod keeps the same natural gap before the title after the row changes from flex to block flow; revealing a title never glues the subtree count to its first word.

One name, every surface. The sessionTitle/sessionHeadline accessor is a session's display name everywhere a human reads which session this is — rows, window, the console sidebar, the search palette, the lock-hint banner, and the [[node-menu]] overlay list (right-clicking a node lists its live sessions — the same live line the board rows show, never the stable label beside it) all show the identical line, so wherever a session is named it reads the same. (Pinning any of them to the stable handle bought nothing — the ordered slot, and the avatar where it's shown, are the fixed anchor and a rename already wins — and cost a surface that named a session differently from its own board.) The stable sessionHandle survives ONLY as a fixed-identity reveal, never a one-line title: the avatar/hover tooltips and mobile's handle-line. Even the [[session-rename]] prompt — where you edit the name override — titles itself with the title the row shows, not this handle, so it never names the session differently from the row it was opened from; its input still prefills with the raw name override. Search matches the handle even when it no longer shows it — and on a current backend the handle IS the server-derived label (a rename name or the prompt truncation), which is the whole search promise: a session is found by the stable name a human knows it by, while raw id/node/branch fragments are deliberately not promised searchable. That the door is named sessionHandle, not sessionName, is what keeps this guarantee from eroding again — a new pick-list can't grab the stable label by reflex ([[session-label]]).

The selected console sidebar row is also the terminal pane's sole visible identity/state surface. The toolbar deliberately carries neither sessionHeadline nor lifecycle/liveness text: repeating the selected row spent vertical space, competed with Eval/actions, and leaked arbitrarily long prompt or HTML-derived text into tooltips and accessible names. Removing that duplicate is not a second naming rule; it leaves the shared row face as the one place the console identifies the selected session.

Keyboard selection always remains visible: after a session change the sidebar scrolls the selected row to the nearest edge, matching the app's other keyboard-driven lists. The resize divider keeps its 180–480px clamp and terminal-lane guard; double-clicking it clears the saved override and restores the 204px product default.

This node's slice of the shared styles.css is the status line (.sess-meta, the full-width dimmer wrap) and its compact-variant collapse (the .si-item one-line overrides that fold .sess-meta inline and drop the status word for the .sess-glyph mark), and the Row-1 title ellipsis; classes other surfaces add there — like the eval tab's .eval-* verdict/transcript rules from the measure-and-score reframe, or the console list's own compact-face overrides ([[session-console]]) — are those features' churn, not session-activity's drift.