fix(components): mark unread sub-session tabs on desktop - #332
Merged
Conversation
A child session tab had no unread indicator. Sub-sessions get no sidebar row of their own, so the tab is the only place their new output can surface — a subagent that finished while the user read the parent thread stayed silent until they happened to click it. The tab's leading icon slot becomes ONE priority-ordered status slot, `waiting > working > unread > agent icon`, the same ladder the desktop sidebar row and the mobile tab sheet already use. Unread reads through the new `sessionHasUnreadMessages`, which shares its comparison with `shouldMarkSessionRead` so announcing unread cannot drift from deciding the read receipt, and is suppressed on the ACTIVE tab — that surface is the one clearing unread, so a dot there would only flash between the click and the receipt landing. That reordering also revives a dead branch: `isWaiting` was tested after `isWorking`, but a permission request is itself live presence, so a tab blocked on approval always rendered the busy spinner and its marker never showed. Waiting now renders the sidebar's `Hand` rather than the old amber dot, because `--primary` and `--status-warning` are both amber in the shipped themes and two dots side by side read as one marker. Adds `tests/session-tab-bar-status-slot.test.tsx` over the whole ladder and a `Sessions/SessionTabBar` story rendering it in one row. Model: claude-opus-5 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue
Problem / pressure
A child session tab carried no unread indicator on desktop. Sub-sessions get no
sidebar row of their own, so the tab is the only place their new output can
surface — a subagent that finished while the user was reading the parent thread
stayed completely silent until they happened to click its tab.
While fixing that, the same slot turned out to have a dead branch:
isWaitingwas tested after
isWorking, but a permission request is itself live presence(
liveStatus != null), so a tab blocked on approval always rendered the busyspinner and its own marker never reached the screen. "This tab needs you" was
indistinguishable from "this tab is busy".
Summary
The tab's leading icon becomes ONE priority-ordered status slot,
waiting > working > unread > agent icon— the same ladder the desktop sidebarrow (
sidebar-row-shared.tsx) and the mobile tab sheet already use.sessionHasUnreadMessages(
lib/session-read-receipt.ts), which shares its comparison withshouldMarkSessionRead, so announcing unread cannot drift from decidingthe read receipt.
unread, so a dot there would only flash between the click and the receipt
landing.
Handinstead of the old amber dot.--primaryand--status-warningare both amber in the shipped themes, so anamber waiting dot beside a primary unread dot read as the same marker (caught
from the screenshots, not from the code).
The invariant is recorded in
components/sessions/AGENTS.md.Before / after
Story
Sessions/SessionTabBar › Unread sub-session tabs, six tabs: activeparent · unread child · never-read child · working child · child awaiting
permission · caught-up child.
bg-primarydot (blue in light, amber in dark).Hand; exactly one spinner remains.DOM counts from the Playwright capture, both themes:
before = 0 unread dots / 2 spinners / 0 hands,after = 2 unread dots / 1 spinner / 1 hand.Test plan
packages/components/tests/session-tab-bar-status-slot.test.tsx(7 cases):unread child, never-read child, caught-up child, unread parent while a child
tab is active, active tab not marked, spinner outranks unread, hand outranks
spinner. Verified they are not vacuous — run against the pre-fix
session-tab-bar.tsx, 4 of the 7 fail.Sessions/SessionTabBar › Unread sub-session tabscovering thewhole ladder in one row; screenshots above were captured from it in both
themes by swapping the component back to
HEADfor the "before" pass.pnpm typecheck(components),oxlint,pnpm lint:i18n: clean.@lody/componentsunit suite: 422 files / 3044 tests passing.SessionTabBar), and the other workspace packages, which this change does nottouch.
Note for anyone reproducing locally: this environment exports
NODE_ENV=production, under which React 19 does not exportactand everypre-existing jsdom test in the package fails with
(0 , act) is not a function. Unrelated to this change — the runs above usedNODE_ENV=development.