Skip to content

fix(workspace): the sidebar date is flush right; an empty availability slot costs nothing (#2641) - #2646

Merged
vybe merged 1 commit into
devfrom
fix/2641-sidebar-date-flush-right
Sep 9, 2026
Merged

vybe merged 1 commit into
devfrom
fix/2641-sidebar-date-flush-right

Conversation

@dolho

@dolho dolho commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

The sidebar row held a 72px availability slot after the date column, on every row, unconditionally. availabilityChip() returns null for every state except stopped and unavailable, so on a fleet where everything is running — the normal case — that strip rendered empty on every row.

Both halves of the reported defect follow from that one fact: the dates stopped 72px short of the row's right edge, and 72px per row was charged to the only element that wanted it, so names truncated (Chief ..., Marke ...) beside a blank strip.

The fix

The reservation is worth keeping — it stops a row reflowing when an agent starts or stops between refreshes (#2196). So it becomes a property of the list, not of a row:

reservesAvailabilitySlot(rows, opts)   // portalUtils.js — pure

Reserve on every row iff any visible row can actually show a chip. Absent entirely otherwise, which puts the date flush against the row's edge.

Three details that are load-bearing rather than incidental:

  • Computed over the rows actually RENDERED, not the whole roster. A stopped agent hidden by search or by the collapse limit would otherwise reserve width on a list that shows no chip — the reported bug with extra steps.
  • The whole <span> goes, not just its width. A zero-width flex child still sits between the date and the row edge, and the row's gap-2.5 would keep paying 10px for it, so the date still would not be flush. The guard is a v-if, never a conditional class — pinned by test.
  • The predicate derives from availabilityChip rather than re-listing the two states, so it cannot drift into a second copy of "which states get a chip". A state neither function has heard of yet reserves nothing.

Acceptance criteria

  • Date flush against the row's right edge on a fleet with no stopped or unavailable agents — the slot is not rendered at all.
  • The empty slot costs no width. Reserved only when the visible roster contains a chip-bearing agent.
  • The name takes the space that frees up — the freed 72px goes to the flex-1 name block at every sidebar width.
  • The no-reflow property survives within a populated list: a second agent stopping, or one restarting while another is still stopped, changes only that row's chip. The roster is still not re-sorted by availability.
  • bug(workspace): activity chart re-renders on every chat-tab switch; agent-list dates not right-aligned; copy/like/dislike misaligned; replies not rateable until reload #2580 is not regressed — the date column stays w-14 text-right tabular-nums, rendered on every row with the v-if INSIDE it. Uniform reservation is precisely what preserves that when a chip does appear; a per-row reservation would give a stopped row a different name width from its neighbours.
  • Ask and waiting badges keep their conditional rendering; nothing else gained a footprint (asserted).
  • Vitest coverage for the chip-present and chip-absent cases.

The one cost, stated rather than discovered later

The 0→1 transition — the first agent in view stopping — reflows the list once, where before it reflowed nothing. That is the honest price of not charging every row for the empty case, and it is the trade the issue explicitly delegates ("implementer's choice, but the empty case must not be charged to the name"). The alternative the issue also names — putting the date last — makes it flush unconditionally but re-introduces the varying truncation point #2580 fixed, on every row that carries a chip. This keeps both properties everywhere except that single transition.

It is written into workspace-sidebar-ia.md beside the reservation's original rationale, not left in a commit message.

Verification

No component-mount harness exists in this project (package.json carries no @vue/test-utils, jsdom or happy-dom; vitest runs environment: 'node'), which is why the decidable half lives in portalUtils.js and is genuinely executed, and the source-regex assertions are scoped to the wiring — the same split portalAvailabilityChip.spec.js documents.

Out of scope

useColumnResize.js's SIDEBAR_MIN = 200 — the issue names it as a side note. Freeing the 72px raises the effective floor for the name at every width without touching it.

Fixes #2641

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bd71qsYbFodvofba8P69eP

…y slot costs nothing (#2641)

The row held a 72px availability slot after the date column, on every row,
unconditionally. `availabilityChip()` returns null for every state except
`stopped` and `unavailable`, so on a fleet where everything is running — the
normal case — that strip rendered EMPTY on every row.

Both halves of the reported defect follow from that one fact: the dates stopped
72px short of the row's right edge, and 72px per row was charged to the only
element that wanted it, so names truncated (`Chief ...`, `Marke ...`) beside a
blank strip.

The reservation itself is worth keeping — it stops a row reflowing when an agent
starts or stops between refreshes (#2196) — so it becomes a property of the
LIST rather than of a row: `reservesAvailabilitySlot(rows)` reserves on every
row iff any VISIBLE row can actually show a chip.

Three details that are load-bearing rather than incidental:

* Computed over the rows actually RENDERED, not the whole roster. A stopped
  agent hidden by search or by the collapse limit would otherwise reserve width
  on a list that shows no chip — the reported bug with extra steps.
* The whole `<span>` goes, not just its width. A zero-width flex child still
  sits between the date and the row edge, and the row's `gap-2.5` would keep
  paying 10px for it, so the date still would not be flush.
* The predicate derives from `availabilityChip` rather than re-listing the two
  states, so it cannot become a second copy of "which states get a chip" — a
  state neither has heard of yet reserves nothing, pinned by test.

#2580 is untouched: the date column stays `w-14 text-right tabular-nums` and
still renders on every row with its `v-if` INSIDE it, so the name's truncation
point is identical down the list. Uniform reservation is what preserves that
when a chip does appear — a per-row reservation would give a stopped row a
different name width from its neighbours.

Accepted cost, stated rather than discovered later: the 0→1 transition (the
first agent in view stops) reflows the list once, where before it reflowed
nothing. Within a populated list nothing moves — a second agent stopping, or
one restarting while another is still stopped, changes only that row's chip.
That is the trade the issue delegates to the implementer.

Frontend suite: 112 files, 2516 tests, all passing; both ratchets green; vite
build clean.

Related to #2641

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd71qsYbFodvofba8P69eP
@dolho
dolho requested a review from vybe September 9, 2026 13:22
@vybe

vybe commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

merge-train 2026-09-09: merging. No criticals from either /validate-pr or /review; both ratchets re-scanned independently against the head blobs and match raw-color-baseline.json byte for byte. The fix is verified non-vacuous: reserveAvailability and chipFor call the same availabilityChip over the same objects, so no row can lose a chip to the gate.

One mechanical fix was applied by the train: the body said Related to #2641, which issue-status-on-merge.yml does not parse, so #2641 would have stranded in status-in-progress. It now reads Fixes #2641. No code was touched.

Four things found that did not block, left for you rather than patched:

1. A background poll can still reflow the roster. PortalSidebar.vue:475 feeds askCounts: asksPerAgent into visibleAgentRows (portalUtils.js:131-140), which appends below-fold agents that have pending asks; asksPerAgent is polled every 20s (Portal.vue:1689,1701). On a 6+ agent fleet with the roster collapsed and all head rows ready, a stopped agent below the fold gaining an ask appends it, flips reserveAvailability, and every row loses 72px. That is the design-system-contract.md:44 no-layout-shift rule, on a surface whose e2e guard (background-refresh-invisible.spec.js) does not cover the portal sidebar. Worth noting the PR's stated cost is the unreachable one — fetchRoster is only called at bootstrap and two "Try again" buttons, never polled — so the reachable trigger is a different one.

2. #2641's AC4 was narrowed, not met. The issue says "an agent going stopped or unavailable between refreshes must not reflow the rows around it", unconditionally; the body rewrites it to "within a populated list" and ticks it. Disclosed rather than hidden, but it wants the issue author's acceptance. The delegation cited is on AC2, not AC4.

3. One test is vacuous — proven, not suspected. portalSidebarDateFlushRight.spec.js:247-253 slices with SIDEBAR.indexOf('reserveAvailability'), which starts at the identifier and so excludes the :class=" prefix the regex then requires. Injecting the exact regression it names still passes. Harmless, because the sibling at :241-245 does catch it — anchoring the slice to <span would make the name honest. I left it alone rather than spend a CI cycle on a redundant guard.

4. A pre-existing guard now over-claims. portalAvailabilityChip.spec.js:164 says "the chip slot reserves its footprint so the row does not reflow" but only asserts the class string is present, so it is blind to the new v-if and passes while the property it names has become conditional. Not touched by this PR; worth retiring or folding into the new spec.

AC7 (rendered geometry) is unasserted at every layer. The "no component-mount harness" note is accurate — environment: 'node', no jsdom/happy-dom/@vue/test-utils — but Playwright e2e exists and could carry it.

@vybe vybe 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.

merge-train 2026-09-09: lane B, validated with /validate-pr + /review. No criticals; both ratchets re-scanned against the head blobs and match baseline exactly. Non-blocking findings recorded in the comment above.

@vybe
vybe merged commit 7be3c1a into dev Sep 9, 2026
28 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