fix(workspace): the ask badge said 2 and gave you no way to find them (#2424) - #2425
Conversation
…#2424) The sidebar advertised "2 asks are waiting on your answer" and then stranded you: the agent that raised them carried no badge, its tooltip did not mention them, and it could be collapsed out of the roster entirely. The only way to locate a blocked agent was to open agents one at a time. Observed on a 12-agent roster with two asks on ws-sage (11th of 12), so on a fresh load the one row that mattered was behind the "show more" toggle. Three failures, fixed together because separately each is a half-measure — a badge with no destination, or a destination nobody can see. 1. The unit. `askCount` is `openAsks.length`, and the tooltip said "agents": two asks on ONE agent rendered as "2 agents are waiting on your answer". The number was right, the noun was wrong, and they only diverge when a single agent raises more than one ask — which is why it went unnoticed. Resolved toward ASKS rather than agents, because the row badges added here now answer "which agent", leaving the header to answer "how many decisions". 2. The row. `PortalSidebar.vue:139` renders a per-agent badge from `unreadByAgent` — unread REPLIES. Keeping asks out of that count is deliberate and documented at line 9 ("one is waiting on you to decide, the other on you to read"), and is preserved: the ask gets the *own badge* that comment promised, in `status-urgent` — the token the operator NavBar's pending-operator-queue badge already uses, so the two surfaces agree — and visually distinct from the indigo unread pill beside it. `agentRowTitle` had the same hole, so this is an accessibility fix too: a blocked agent's accessible name was the bare "Open ws-sage". 3. The collapse. #2159 capped the roster at five for a good reason (a long fleet pushed chats below the fold), but the slice is plain roster order with no ask weighting. Ask-bearing agents are now never hidden — appended, NOT floated to the top, because re-sorting on a transient count moves rows under the cursor between refreshes, the same reason the roster is not re-sorted by availability. Not a regression: every piece shipped in its intended form; the gap was between them. Everything decidable moved into `portalUtils` (`asksByAgent`, `askBadgeTitle`, `agentRowTitle`, `visibleAgentRows`, `AGENT_COLLAPSE_LIMIT`) because vitest runs `environment: 'node'` with no mount harness — a rule inside the SFC is one no test can reach, which is how all three of these shipped. Mutation-checked: reverting the noun, dropping asks from the title, and restoring the plain slice each turn the suite red. `bg-amber-500` -> `bg-status-urgent-500` is required, not drive-by: new code must be at zero raw palette classes, so the new badge needed a token, and the header had to match it or the two ask indicators would differ. Amber maps to `state-autonomous` (an operating mode), which is the wrong claim. PortalSidebar is now at zero non-gray raw classes. Two pre-existing guards asserted the moved expressions as source strings and are rewritten to assert the properties behaviourally — strictly stronger, since they now fail on a broken bound or a dropped chip title, not only on a reworded one: - portalRosterRow #2159 "shows a fixed number by default" - portalAvailabilityChip #2196 "row title carries the state" Verification: 1518/1518 frontend unit tests, raw-color ratchet exit 0, production build clean. Closes #2424 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
obasilakis
left a comment
There was a problem hiding this comment.
Approving. I re-ran every number in the description against a clean worktree off the PR head and they all hold:
npx vitest run— 67 files, 1518 passed (matches the claim exactly)- the three affected spec files — 50 passed
npm run check:tokens— exit 0scan-raw-colors.mjs— exit 0npm run build— clean
Two things worth recording because they make the change stronger than the description claims:
The raw-color fix repairs a pre-existing drift, not just avoids adding one. Measured PortalSidebar.vue on both sides: dev reports raw_nongray: 1, this branch reports 0, and raw-color-baseline.json already said 0. So dev was over its own baseline — ent#364's bg-amber-500 landed after the baseline was seeded. This brings the file back into agreement with the checked-in ratchet.
The token claim checks out at the source. tailwind.config.js:30 aliases status-urgent to colors.orange, and NavBar.vue:56 is the operator-side pending-queue badge using the same token — so "the two surfaces agree on what 'waiting on you' looks like" is literally true, not just visually approximate.
On the two guards you rewrote: I agree with the rewrites and don't think either loses coverage. Both moved from asserting a source string to asserting the property, which catches a dropped chip title or a broken bound rather than only a rewording. That is strictly more than they had.
Two non-blocking suggestions, neither worth holding the merge:
-
Of the three rules you moved into
portalUtils, the row title is the only one without an SFC-routing assertion.visibleAgentRows(props.rosterandaskCountFor(are both asserted present in the sidebar source;buildAgentRowTitleis not — so the:titlebinding could be unwired and the suite would stay green. That is the one asymmetry with your own stated reason for the move. One line closes it:expect(sidebarSource()).toMatch(/buildAgentRowTitle\(/). -
The new ask badge has no reserved footprint, while the availability chip one element earlier reserves
min-w-[4.5rem]with a comment arguing that reservation is the layout-stability rule for this row. An ask arriving or resolving between refreshes will reflow the name column. It matches the existing unread pill's precedent so it is consistent — just noting that the file argues the opposite a few lines up.
Both are follow-up material, not merge blockers.
CI note: the six pytest seed jobs were still pending when I approved. The diff is five frontend files, so they are not exercising anything this PR touches, but they should be green before merge.
Journey Impact: none: a discoverability fix inside an existing surface — the ask flow itself (raise → answer → write-back) is unchanged, so no user-facing promise is added or extended.
Closes #2424
The problem
The Workspace sidebar advertised "2 asks are waiting on your answer" and then stranded you. The agent that raised them carried no badge, its tooltip did not mention them, and it could be collapsed out of the roster entirely. The only way to find a blocked agent was to open agents one at a time.
Observed on a 12-agent roster with two asks on
ws-sage— 11th of 12 — so on a fresh load the one row that mattered was behind the "show more" toggle.Three separate failures, and each alone is a half-measure: a badge with no destination, or a destination nobody can see. Hence one PR.
1. The badge counted asks and said "agents"
Two asks on one agent rendered as "2 agents are waiting on your answer". The count was right; the noun was wrong. They only diverge when a single agent raises more than one ask, which is why it survived.
Resolved toward asks, not agents — a decision the issue deliberately left open, because it depends on part 2. Now that the rows answer which agent, the header's job is how many decisions, and that is a count of asks.
2. The row said nothing
PortalSidebar.vue:139renders a per-agent badge fromunreadByAgent— unread replies. Excluding asks from that count is deliberate and documented at line 9:That rule is preserved. The gap was that the ask never got the own badge the comment promises — it got an aggregate in the header and nothing per row. It has one now, in
status-urgent, visually distinct from the indigo unread pill beside it.agentRowTitlehad the identical hole — it composed unread replies and the availability chip and never mentioned asks — so this is an accessibility fix too: a blocked agent's accessible name was the bare"Open ws-sage", leaving the state unreachable for a screen-reader user as well as invisible.3. A blocked agent could be hidden
#2159 capped the roster at five for a good reason — a long fleet pushed chats below the fold — but the slice is plain roster order with no ask weighting.
Ask-bearing agents are now never collapsed out. Appended, not floated to the top: re-sorting on a transient count moves rows under the cursor between refreshes, which is the same reason the roster is not re-sorted by availability. The first five stay exactly where they were; the visible list simply grows.
Why
bg-amber-500changedRequired, not drive-by. The design contract ratchets raw palette classes and says "new code must be at zero", so the new badge needed a token — and the header had to match it, or the two ask indicators would be different colours.
status-urgentis the token the operator NavBar's pending-operator-queue badge already uses (NavBar.vue:56), so the two surfaces now agree on what "waiting on you" looks like. Amber maps tostate-autonomous, an operating mode, which is a different claim.PortalSidebar.vueis now at zero non-gray raw classes — that was its only one.Two pre-existing guards rewritten
Both asserted the moved expressions as source strings, so they failed on the refactor while their properties still held. Rewritten to assert the properties behaviourally — strictly stronger, since they now catch a broken bound or a dropped chip title, not only a reworded one:
portalRosterRow.spec.js— fix(workspace): sidebar agent roster — no loading state, unbounded list, slug-as-title naming #2159 "shows a fixed number by default": now drivesvisibleAgentRowswith a 12-agent roster and asserts the cap, the expansion, and that the SFC still routes through it.portalAvailabilityChip.spec.js— bug: Workspace roster lists agents with no container (DB-sourced roster vs Docker-as-truth) #2196 "row title carries the state": now drivesagentRowTitleand asserts the chip title survives composition.Neither loses coverage; say so if you disagree, since I edited guards I did not write.
Structure
Everything decidable moved into
portalUtils.js—asksByAgent,askBadgeTitle,agentRowTitle,visibleAgentRows,AGENT_COLLAPSE_LIMIT. vitest runsenvironment: 'node'with no mount harness, so a rule living inside the SFC is one no test can reach — which is how all three of these shipped in the first place.Verification
Mutation-checked per part — each turns the suite red on its own:
Not a regression
Every piece shipped in its intended form. The gap was between them: ent#364 added the aggregate badge, ent#359 built the sidebar surfaces, #2159 added the collapse — and nothing connected the count to the row.
How to see it
With two asks addressed to the signed-in user on one agent that sorts outside the first five: the header badge reads "2 asks are waiting on your answer", the agent's row carries an orange
2, its tooltip names the pending decision, and the row is on screen without expanding.