fix: stop the transcript mask painting over composer status bars - #6912
Conversation
UX Review (Fable 5) — ✅ PASSUX-level review of The screenshots confirm the claims: the before image shows the status bar's top border and corners shaved flat by the mask; the after image shows the intact rounded card with the composer unmoved. The remaining file in the diff is a source-text guard test with no user-facing surface. No strings, flows, states, or controls change — this is a pure paint-order repair of an existing visual defect, and the fix is exactly what the pixels show. UX-Verdict: PASS Pure paint-order repair: the status bars regain their top border and corners, nothing else about the experience changes, and the screenshots prove it. [UX-REVIEWED] 087ba92 |
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Root-cause paint-order fix at minimal blast radius; the tempting wrapper shortcut is rejected for a verified stacking-context reason and test-pinned against reintroduction. I verified the premises in the tree: the mask is [DESIGN-REVIEWED] 087ba92 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All verification is done. The mask geometry claims match First-Principles-Verdict: PASS A reported paint defect fixed at its mechanism — the mask outranking the bars — with every rider declared and the rejected general shortcut given a derived reason. What this change shipsIntent: stop the transcript's bottom mask shaving the top edge off composer status bars — a FIX.
Per-child lift vs. one wrapper z-index: the wrapper alternative was considered and rejected on a verifiable constraint ( [FIRST-PRINCIPLES-REVIEWED] 087ba92 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
CI status: both failures are pre-existing on
|
| Gate | pristine main |
this branch rebased on main |
|---|---|---|
npx tsc -b |
exit 1, same two TS2300 lines |
exit 1, same two TS2300 lines |
npx vitest run src/i18n/productName.test.ts |
exit 1, same assertion | exit 1, same assertion |
Identical on both sides, so neither failure is attributable to this change.
For completeness, on the base this branch was reviewed and pushed from, the full frontend suite was green with this diff applied: tsc -b 0, eslint 0 errors, 1658 files / 26171 tests passing.
Disposition
accepted-and-deferred — not fixed here. Both are repo-wide base breaks; fixing them inside a chat-composer paint-order PR would mix an unrelated import removal and a 12-locale i18n change into this diff. I found no open PR for either, so they need their own. This PR will go green on a rebase once they land.
The composer status bars rendered with their top border, both top corners and the first line's ascenders shaved off, which reads as the bar being clipped by the UI. Nothing clipped them. The transcript's bottom mask is `relative z-[1]` and deliberately overshoots COMPOSER_MASK_OVERSHOOT_PX (10px) BELOW the scrollport edge so it ends flush against the composer box with no hairline between them. That overshoot is sized for the distance ChatInput owns -- `pt-1` plus its `h-[6px]` top spacer -- a distance that only holds while the composer status stack is EMPTY. With a bar present the bar occupies that strip, and the mask's tail (opaque: `from-bg from-[62%]` makes the bottom 62% solid, and the overshoot sits entirely inside it) painted over the bar's top 10px. Measured in an isolated repro of the same column: mask.bottom - bar.top == 10, exactly the overshoot, and lifting the bar changed pixels while leaving every rect identical -- paint order, not layout. SubagentProgressBar was immune only by accident: its wave chip already sits at `z-[46]` to clear theme-experience overlays, which is why the defect only surfaced on the bars that had no z-index at all -- TaskProgressBar, WorkflowProgressBar, SubagentDeliveryProgress, and QueueStack at `zIndex: 0`. Each of those is lifted to `z-[2]`. QueueStack stays far below the composer's own `z-10`, so its collapsed front card's -OVERLAP fuse keeps sliding UNDER the input box rather than over it. The fix is per-child rather than one z-index on the stack wrapper on purpose: a z-index there would make the wrapper a stacking context and CONFINE the wave chip's 46, re-exposing it to the overlays it was lifted to clear. ChatPage.statusStackAboveMask.test.tsx therefore pins three things -- the ordering, the wrapper staying context-free, and the child LIST, so a sixth bar cannot quietly inherit the bug.
a1e704c to
087ba92
Compare
Both base breaks have landed; rebased onto themFollow-up to the CI note above — the two
Rebased onto Also addressed one review finding in the same pushA reviewer noted that this PR's new Dropped. Evidence image URLs re-pinned to the new head and verified 200. |
Problem / Motivation
The composer status bars render with their top border, both top corners, and the first line's ascenders shaved off. It reads as the bar being clipped by the UI — the reporter's words were "the yellow part is covered by the UI".
Reported against
SubagentDeliveryProgress(the "N sub-agent results ready — processing after the current turn" line), butTaskProgressBar,WorkflowProgressBarandQueueStack's front card share the same defect.Why it matters
The status stack is where the chat tells you about work you cannot otherwise see — a sub-agent result being held behind the current turn, a task's progress, a queued message. A bar whose top edge is sliced off reads as a rendering fault, and the reader's first instinct is to distrust the surface rather than the message.
QueueStack's front card is the worst case: its top border is what separates a queued message from the input box it is fused to.What changed (motivation → approach → change)
Nothing was clipping them. The transcript's bottom mask is
relative z-[1]and deliberately overshootsCOMPOSER_MASK_OVERSHOOT_PX(10px) below the scrollport edge so it lands flush against the composer box, with no hairline strip between the two. That overshoot is sized to a distanceChatInputowns — itspt-1(4px) plus itsh-[6px]top spacer — and that distance only holds while the composer status stack is empty.With a bar present, the bar occupies that strip. The mask's tail is fully opaque there (
from-bg from-[62%]makes the bottom 62% solid, and the 10px overshoot sits entirely inside it), so it painted over the bar's top 10px.SubagentProgressBarwas immune by accident: its wave chip already sits atz-[46]to clear theme-experience overlays. That is why the defect only ever surfaced on the bars carrying no z-index at all.The change lifts each of them above the mask:
TaskProgressBar,WorkflowProgressBarandSubagentDeliveryProgresstorelative z-[2], andQueueStackfromzIndex: 0to2. All stay far below the composer's ownz-10, soQueueStack's collapsed front card keeps sliding its-OVERLAPfuse under the input box rather than over it. The mask's tail now paints harmlessly behind the topmost bar, whose own box is what sits flush under the transcript.Why per-child and not one z-index on the stack wrapper. One z-index there would fix all five at once and is the tempting shortcut, but it would make the wrapper a stacking context and confine the wave chip's
z-[46], re-exposing it to the very overlays it was lifted to clear.ChatPage.tsx'sCOMPOSER_MASK_OVERSHOOT_PXdoc comment now records the corrected premise, since its old wording asserted a distance that is only true for an empty stack.Tests
website/src/test/ChatPage.statusStackAboveMask.test.tsx— a source-text guard, matching how the two neighbouring mask invariants (ChatPage.fadeClearance,ChatPage.composerChromeOcclusion) are already pinned: the values live in five files, several as Tailwind classes jsdom cannot resolve into a paint order, and the invariant is the comparison between them.undefined > undefinedvacuityQueueStackstays below the composerpositionAll 6 mutations verified: each of the four reverted lifts, the wrapper shortcut, and a sixth unlifted bar. Every one turned exactly its own case red, and the sources were byte-identical to their pre-mutation copies afterwards.
A seventh case and mutation (the mask staying opaque across the overshoot) was dropped after review:
ChatPage.fadeClearance.test.tsx:69-77already pins the same arithmetic strictly more strongly —toBeGreaterThanplus an upper bound, against this file'stoBeGreaterThanOrEqual— so it added no coverage while making a second regex track the same class string.Full frontend suite green on this base:
tsc -b0, eslint 0 errors, 1667 files / 26350 tests passing.Manual verification
Measured in an isolated repro of the same column geometry (scroller → mask with the same height/negative margins → status stack → composer), driven by Playwright:
mask.bottom − bar.top === 10— exactlyCOMPOSER_MASK_OVERSHOOT_PX, confirming the mask's box is what covers the bar's top.bar.top323.02,box.top367.81 before and after) while the rendered pixels differed — proving this is paint order, not layout, so the composer does not move.Not reproduced in the live app: the reported state needs a real sub-agent delivery held behind a busy turn, which cannot be staged from a test harness. The repro reproduces the reported symptom exactly and the measured overlap equals the production constant, but a reviewer wanting the live surface should treat that as the remaining gap.
Screenshots / video
Isolated repro of the same column at the same geometry — not the live app (see Manual verification for why). Left: today, the mask's tail shaves the bar's top border, both corners and the glyph tops. Right: with the lift, the card is intact.
Related Issues
No linked issue — reported directly by a user against the running dashboard.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)COMPOSER_MASK_OVERSHOOT_PXdoc comment, whose stated premise this fix corrects