Skip to content

fix(workspace): the ask badge said 2 and gave you no way to find them (#2424) - #2425

Merged
dolho merged 1 commit into
devfrom
fix/2424-workspace-ask-discoverability
Aug 28, 2026
Merged

fix(workspace): the ask badge said 2 and gave you no way to find them (#2424)#2425
dolho merged 1 commit into
devfrom
fix/2424-workspace-ask-discoverability

Conversation

@dolho

@dolho dolho commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

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"

// before
:title="`${askCount} ${askCount === 1 ? 'agent is' : 'agents are'} waiting on your answer`"
// stores/clientPortal.js
askCount() { return this.openAsks.length }

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:139 renders a per-agent badge from unreadByAgent — unread replies. Excluding asks from that count is deliberate and documented at line 9:

an ASK is a distinct fact from an unread reply — one is waiting on you to decide, the other on you to read — so it gets its own badge rather than being summed into that one

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.

agentRowTitle had 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-500 changed

Required, 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-urgent is 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 to state-autonomous, an operating mode, which is a different claim.

PortalSidebar.vue is 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:

Neither loses coverage; say so if you disagree, since I edited guards I did not write.

Structure

Everything decidable moved into portalUtils.jsasksByAgent, askBadgeTitle, agentRowTitle, visibleAgentRows, AGENT_COLLAPSE_LIMIT. vitest runs environment: '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

tests/unit/portalAskDiscoverability.spec.js   21 passed   (new)
npm run test:unit                           1518 passed   (67 files)
scan-raw-colors.mjs                          exit 0
npm run build                                clean

Mutation-checked per part — each turns the suite red on its own:

Mutation Result
Revert the noun to "agents" 2 failed
Drop asks from the row title 4 failed (incl. the rewritten #2196 guard)
Restore the plain slice 2 failed (incl. the rewritten #2159 guard)

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.

…#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>
@dolho
dolho requested a review from obasilakis August 28, 2026 08:56

@obasilakis obasilakis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 0
  • scan-raw-colors.mjs — exit 0
  • npm 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:

  1. Of the three rules you moved into portalUtils, the row title is the only one without an SFC-routing assertion. visibleAgentRows(props.roster and askCountFor( are both asserted present in the sidebar source; buildAgentRowTitle is not — so the :title binding 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\(/).

  2. 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.

@dolho
dolho merged commit 4917def into dev Aug 28, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants