Overview: an Unread section, with durable read tracking

Running → Unread → Remaining, reusing the existing cards and tiles. New human-facing assistant output makes a session unread; tool traffic and status changes do not. A reply counts as read only once it has actually been put in front of the operator.

The three sections

Card view. One running agent, two with replies the operator has not seen, two already read. Five sessions, five cards — a session is in exactly one section.
Tile view. The same three blocks, same order, same counts.
list  : [running now] 1 · deploy      tiles : [running now] 1 · deploy
        [unread]      2 · billing, triage      [unread]      2 · billing, triage
        [by the last reply] 2 · infra, docs     [by the last reply] 2 · infra, docs
        total cards on screen: 5                total cards on screen: 5

Mark all read

A text button on the heading, in the heading's own voice — no toolbar, no menu, no icon-only control. Its title names the scope.
After activation, with a reply that arrived afterwards. The empty section is omitted entirely, then returns with a count of 1.

Driven by keyboard, since that is the harder path:

accessible name: "Mark the 2 unread replies in this section as read.
                  Running agents and anything the current filters hide are left alone."

before      : running now=1  unread=2  by the last reply=2
after Enter : running now=1            by the last reply=4     ← empty section omitted
server state: deploy:read triage:read billing:read docs:read infra:read
after a new reply arrives:
              running now=1  unread=1  by the last reply=3     ← and it is unread again

What makes a session unread

A comparison, not a flag: the newest reply the transcript has produced against the newest the operator has been shown. Both are {src, seq, hash} — the transcript generation, how many distinct replies it has produced, and a hash of the full text of the newest one.

CaseNew unread?Why
A new assistant replyyesthe sequence advances
A streaming answer that grewyessame position, different hash — including past the 280-char card clip, where the visible summary is unchanged
Two replies with identical textyesa prompt between them starts a new reply whatever it says
Two replies at the same timestampyesthe sequence separates them; no clock is consulted
The same record mirrorednocodex writes agent_message and task_complete with the same words
Tool calls, token counts, statusnothe cursor moves on assistant text only — deliberately not the file's revision, which moves for all of these
The operator sends a promptnoand, critically, it does not clear an existing unread state — see below
The transcript is replaced (new run)yesthe generation changes, so the old mark cannot cover the new file's replies

The bug this had to avoid: the existing digest clears its assistant fields on every new prompt. Had the cursor lived in those fields, typing at an agent would have silently marked its unseen answer as read. The cursor deliberately survives digestPrompt, and there is a test that fails if it stops doing so.

When a reply counts as read

The bar is higher than "the component rendered". Mounting a conversation, selecting a route, a fetch resolving and the reader's At latest flag all describe the program, not what the operator can see. The hook asks the browser instead: is the element carrying the latest reply intersecting the viewport, in a foreground tab, on the active surface?

SituationAcknowledged?
Latest reply visible in an expanded Overview conversation, foregroundyes
Latest reply visible in the Reader, in the active pane, foregroundyes
Rendered but below the foldno
On screen, but the tab is in the backgroundno
A card or tile previewno
A pane sitting behind another in a deckno
Paged back to an earlier turnno
A reply too long for the card's copy of itno — finished in the Reader, which serves the whole thing
A search showing older matchesno

Proven against a real IntersectionObserver in Chromium rather than by calling the acknowledgement path directly — the test also asserts the observer would have fired in the hidden-tab case, so it is the guard that stops it and not an accident of layout.

Races

An acknowledgement names the exact version that was displayed, never "whatever is newest". The server accepts it only if that is still the newest, so a reply arriving between rendering and acknowledging stays unread instead of being swept up by a mark it was never part of.

RequestAnswer
Exactly what is on screenok
The same one againok — replays are harmless
An older reply, arriving lateok, but recorded progress does not move back
A different transcript generationstale
Further than any reply that existsfuture
Right position, wrong content (a streaming answer moved on)mismatch
A session that does not existunknown

A rejection is an ordinary answer, not a failure to retry: it means the reply it described is no longer the newest, and the card correctly stays unread. Nothing is claimed read unless the server said so — a failed bulk action leaves the section exactly as it was, with one compact retry that reuses the original capture.

Rollout

Existing replies start read, once ever, from the versions actually observed in the first pass that sees any output — not a wall-clock cutoff. A reply landing while the baseline is being written is newer than anything captured, so it stays eligible for Unread.

A session with nothing readable yet is skipped rather than marked read at position zero, so its first reply is unread rather than presumed seen. Persistence and the once-only rule are tested across a restart.

Deliberately not in this PR

Manual (grouped) mode is untouched — capsules, no sections, no Mark all read.

The three sections live in the ranked feed, which is the layout that already renders every card under one parent so a card changing section is a reorder rather than a remount. Manual mode draws each group as its own capsule; hoisting a session out of its capsule into an Unread block would change its parent, and React remounts on a changed parent however stable the key — destroying the reply draft, unfolded work and scroll position of a card that became unread from a background poll. That is exactly the failure the ranked feed's stable-parent comment exists to prevent.

Giving manual mode the section therefore requires the capsule layout to adopt a single parent first, which is a separate change. Also left out, as the issue directs: toolbar relocation, full-history search, general polling changes and the mode-switch position fix.