feat(harness): project rows carry a live-session indicator [SAP-3200] - #841
feat(harness): project rows carry a live-session indicator [SAP-3200]#841gwitwer wants to merge 3 commits into
Conversation
A project row in the rail now shows a dot when one or more of its agents
has a running session, so which projects are active reads at a glance.
Before this the rail could hold a dozen projects with something running in
one of them and say nothing about which.
The mark is the session bar's own dot recipe in its running state
(`.session-dot[data-status="running"]`), reused rather than redrawn: one
dot means one thing across the app, and it introduces no colour and no
token of its own. It stands at the trailing edge of the row, first in the
trailing cluster, so the actions that fade in on hover appear beside it
instead of pushing it along. The count lives in the tooltip and the
accessible name ("1 live session" / "N live sessions") because a bare dot
is mute.
Membership is derived, never stored, and reuses `rootContains`, the app's
one containment answer: a session belongs to a project when it runs inside
the root or is bound to an agent under it, and to a group when it is bound
to a member or sits unbound in a member's own folder. The rail still lists
no sessions; this is a fact about a project, the way the deploy glyph is a
fact about an agent, and agent rows are untouched.
Design of record: design-eng PR #185 (agent-studio-v2, DECISIONS D37),
frames at agent-studio-v2/docs/evidence/rail-live-indicator/. That PR is
still OPEN, so this must not merge before it does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019o9uM8fivJobCpT5zd4ChM
Review — PR #841 (round 1)🔒 Confidentiality: nothing new in the diff. 1. No changeset for a user-facing change to a published package
What breaks: 2.
|
…P-3200] Review round 1, findings 1 to 3 and both nits. The mark's project count was a SECOND answer to a question `session-scope.ts` already answers, and that module's own docblock names the hazard: a session whose cwd is in project A while its binding resolves under project B, which `POST /api/agents/move` produces, would have marked both rows live while B's session tab strip stayed empty, because the strip renders from `liveSessionsForProject` and the mark did not. The rail now calls that same function, so the dot and the tabs cannot disagree by construction, and the near-identical `liveSessionsInProject` is gone rather than left for the next caller to pick by autocomplete. `project-live.ts` keeps only what session-scope has no answer for: a group has no directory, so it cannot be asked the containment question. The group mark now has positive coverage on screen. No fixture had a session BOUND to a group member, so `?mockBoundSession=1` seeds exactly one, bound to `gateway`, behind its own parameter so no spec that counts sessions on the deep fixture moves. The spec asserts the mark on that header, its absence on the neighbours, and the same session counted once by the project row above. That a fresh session marks the project and no group under it is intended, not a gap: a session is unbound until its agent is known, and an unbound session at the root is working on no member yet. Said so in both `liveSessionsOnAgents` and the `liveCount` JSDoc so it is not filed as a bug. Also: a changeset (minor for `@sapiom/harness`, patch for the desktop app), and the standing-mark spec now multiplies opacity up the ancestor chain and checks the mark is not inside a `.workspace-row-action`, since an element's own opacity reads 1 inside a faded parent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019o9uM8fivJobCpT5zd4ChM
|
Round 1 addressed in 4bcfb7e. Findings 1, 2 and 3 taken; one nit taken, one declined with reasoning. 1. Changeset: added
2. Second membership answer: you were right, and it is goneI confirmed the failure rather than taking it on faith. I took your second option, which is also what my own justification argued for: the binding clause is gone and the rail now calls Coverage did not shrink: 3. Group mark dark for the default session flow: documented, and now covered on screenThe behaviour is intended, and you are right that it reads like a bug. It is now stated in two places: I also took the fixture suggestion, since it closed a hole I had flagged myself. NitsOpacity assertion: taken. Correct, the element's own opacity reads 1 inside a faded ancestor. The spec now walks up to Per-render allocation: declining. Taking this would mean adding a counting variant beside Mutation test, re-run after the changes7 mutations, 7 killed, 0 survivors. Each applied to a file while the dev server was already running, so HMR invalidated the transform rather than serving a cached one.
Checks
The merge gate on design-eng#185 still stands: it is still OPEN, and this must not merge first. |
Review — PR #841 (round 2, delta since
|
Review round 2. A project row counts by containment and a group by binding, so a session bound to an agent under a project but rooted outside it lights the group header while the project row stays dark: a child marked live inside a parent that is not. `POST /api/agents/move` produces it. Left standing rather than fixed, and the docblock now says why. A group has nothing on disk behind it, so binding is the only membership it has, and it is the rule `liveSessionsForFocus` already applies to one agent whose own tab strip lists that same session. Intersecting the group rule with the project's containment would make a group mean something different from the agent rows inside it; adding a binding clause to the project rule is the second membership answer round 1 removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019o9uM8fivJobCpT5zd4ChM
|
Round 2 addressed in 4411f4d. Both new items taken; the nit declined again, with the reason sharpened. The project/group asymmetry: documented, not fixedYou are right, and it is the mirror of the round-1 bug: a session bound to an agent under project A but rooted outside A lights the group header and leaves A's row dark. I took the docblock line, and wrote down why it is left standing rather than closed, since "known and deliberate" is only useful if the next reader can see the reasoning:
Every alternative is worse than the odd mark, so the mark stays odd and the rules stay singular. That is now in PR body: refreshedCorrect, it was stale in three places. Updated: Nit, declined againCorrect that Checks after the change
The merge gate on design-eng#185 still stands: it is still OPEN, and this must not merge first. |
The harness half of SAP-3200: a project row in the rail shows an indicator when the project has one or more live sessions, so which projects are active reads at a glance.
Gate: design-eng #185 is still OPEN
The ticket sequences this after sapiom/design-eng#185 merges. As of this push it has not:
git ls-tree origin/mainfinds none of its files, and the frames this cites live only onagent-studio-v2-project-live-indicator. Do not merge this before #185. The implementation matches that mock frame by frame; if #185 changes on the way in, this changes with it.Frames cited:
agent-studio-v2/docs/evidence/rail-live-indicator/rail-live-indicator-light.pngand-dark.png(on that branch, not yet onorigin/main).The mark
+and the...are.workspace-row-action(opacity 0 at rest), so they fade in beside the mark rather than moving it..session-dot[data-status="running"]). No new colour, no new token, no second dot recipe. The one new CSS rule is amargin-right: var(--sp1).Derivation
A project's live sessions are
liveSessionsForProject(session-scope.ts), the same function the session tab strip renders from, so the dot and the tabs cannot disagree about which project a session is in. The first draft of this PR defined its own project rule; review round 1 caught that as a second answer to a question that module already owns, and it is gone. See the round-1 reply below for the failure it would have produced.web/src/lib/project-live.tsnow holds only what session-scope has no answer for: a group is a label over agents with no directory behind it, so it cannot be asked the containment question a project is asked. Membership there is the ruleliveSessionsForFocusalready applies to one agent, over several: bound to a member, or unbound in a member's own folder. Its docblock records the two places the project and group rules read differently, why each is right on its own terms, and why neither is changed to match the other.Before and after, against a real server
A real agent tree (
polsia/sapiom-workflows, 6 agents), two live sessions running on the server in both columns.mainsays nothing about them; this branch shows the dot.origin/main, 2 sessions running)Recording of the mark appearing on a real server, counting down as sessions end, and going when the last one exits: after-live-mark.mp4
Specs
web/src/lib/project-live.test.ts(11 tests) pins the group rule both directions, including the exit walk 2 -> 1 -> none, a starting session counting as live, and the trailing-separator case. The project rule isliveSessionsForProject, already pinned by 8 assertions insession-scope.test.ts.web/e2e/rail-live-indicator.spec.ts(8 tests) drives it on screen: present onacme-appwith the running state and a counted accessible name; standing rather than hover-revealed (opacity multiplied up the ancestor chain, and not inside a.workspace-row-action); absent onrfq-agent(only an exited session) and ononboarding-flow(no sessions); the count walking down and the mark leaving as both sessions end; agent rows carrying no dot; no group header claiming a mark where nothing under it is live; and thegatewaygroup header carrying the mark alone when a session is bound to one of its members.That last one needed a session BOUND to a group member, which no fixture had: the default fixture's live sessions belong to
acme-app, which has one agent and so renders no group sections, whiledeephas the groups and no sessions.?mockBoundSession=1seeds exactly one, behind its own query parameter so nothing already running againstmockFixtures=deepmoves.No existing spec needed narrowing. The two that guard "no sessions in the rail" assert on
workflow-session-dot-*andrail-session-*testids, which this does not add.Mutation test of the new specs
7 mutations, 7 killed, 0 survivors. Each was applied to a file while the dev server was already running, so HMR invalidated the transform rather than serving a cached one.
opacity: 0at rest)Checks
tsc --noEmit(server and web)eslint srcweb/src)vitest:project-live,session-scope,css-brand-surface,apiplaywrightrail-live-indicator(includes bothmockBoundSessionanddeepgroup-axis specs)playwrightsmoke,group-axis,project-axis,rail-grammar,session-scope,session-tabs,agent-map-planning,project-map-groupsThe full suite was not run locally; CI covers it.
did_not_work
DELETE /api/sessions/:id. The mock-mode spec covers the same transition through the UI alone.liveSessionsOnAgentsdocblock.POST /api/agents/moveis the way to produce it.--state-root. Every screenshot above is from a run against a fresh state root.🤖 Generated with Claude Code
https://claude.ai/code/session_019o9uM8fivJobCpT5zd4ChM