session-activity¶
Each session row's headline IS the worker's own live one-line self-summary (its tmux pane title), overriding the launch-prompt placeholder; the status word and spec-op count drop to a quieter second line.
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 headline from it
would name the folder, not the task. For a non-self-summarizing harness activity stays null and the
headline 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. Once present 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 (two-row face). The shared session face (session-console's SessionRow) is two rows. Row 1
is the headline — the avatar followed by the one best description of what this session is about,
single-line with an ellipsis. The headline prefers the worker's own live self-summary: once the pane title
exists, the agent-generated activity line is the headline — it tracks what the agent is doing now,
sharper than any launch-time label. Before it exists (booting / queued / offline) the headline shows the
first words of the launch prompt (promptPreview) as a placeholder that the smart label overrides the
moment it arrives, so the human's initial wording disappears once the agent has named its own task. A human
rename (name) still wins over both — the session-rename override stays authoritative everywhere.
The avatar (seeded by id) is now the fixed spatial anchor, so the headline is free to renarrate each turn
without the row losing its slot.
Row 2 is the status line — the small state badges that used to crowd the headline, moved off it: the
colour-coded status word and the op tally (how many spec nodes this session is changing, e.g. ~2), in a
smaller, dimmer font spanning the whole row width (the face's flex row wraps and the line takes a
full-width basis, so it drops below the avatar too). It is the parking spot for any further at-a-glance
metadata we add later. When this row is the locked selection a 🔒 sits at the end of Row 1, and the
status word stays on Row 2 (locking no longer hides it). The face is shared, so the top-left window, the
Enter session tabs, and the mobile list all show the identical headline + status line.
The console header reads the same headline. The Enter interface's big-title bar above the live
terminal (session-console's si-th-name) renders the SAME sessionHeadline, not the stable node name —
so the agent's live self-summary that renarrates the rows renarrates the header in lock-step, and the title
over the terminal never disagrees with the row that opened it. The data source and the content are one
shared line across both surfaces; the only difference is room: the header is a wide bar, so it gives the
headline flex:1 of that width and ellipsises far later than the compact rows — less truncation where
there is space for more.
One name, every surface. The sessionHeadline is a session's display name everywhere a human reads
which session this is — rows, window, Enter tabs, console header, and now the search palette and the
lock-hint banner show the identical line, so where a session is searched from and where it is found never
disagree. (Pinning those two to the stable sessionName bought nothing — the avatar is the fixed anchor and
a rename already wins — and cost a palette that named a session differently from its own board.) The stable
sessionName survives ONLY as a fixed-identity reveal, never a title: the avatar/hover tooltips and
the rename prompt (editing the name override). Search still matches the handle even when it no longer
shows it, so finding a session by node/branch/id keeps working.
This node's slice of the shared styles.css is the Row-2 status line (.sess-meta, the full-width dimmer
wrap), the Row-1 headline ellipsis, and the console header big-title's room-to-expand (.si-th-name's
flex:1 + ellipsis — the same headline, more width); classes other surfaces add there — like the yatsu eval
tab's .eval-* verdict/transcript rules from the measure-and-score reframe — are those features' churn, not
session-activity's drift.