Repository navigation
UI Consistency: 2 issues found
Two concrete regressions were found in the new shared composer banner primitives.
apps/web/src/components/chat/ComposerBanner.tsx—ToggleIconapplies the fullbuttonVariantsbase to a decorativearia-hiddenspan, pulling in the coarse-pointerafter:44×44 hit-target overlay withoutpointer-events-none. On touch devices the overlay overflows the row and intercepts taps meant for the content below/right (first option button, first task row, first stash entry), activating the enclosing disclosure instead.apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx— the horizontal padding that used to live on the clippingCollapsiblePanel(with a comment explaining why) moved toComposerBanner.Body, which only suppliespadding-inline-start. The full-width option buttons now sit flush against the panel's inline-end and block-end edges, so theirfocus-visible:ring-1is shaved byoverflow-hidden.
Both findings have inline suggestions on the PR.
Details
Note
Your check run agent prompt is: .macroscope/check-run-agents/ui-consistency.md
More information about how Check Run Agents work can be found in our Help Center.
Scope reviewed
apps/web/src/**/*.ts, **/*.tsx, **/*.css in the diff, plus directly affected call sites.
Checks performed and found clean
- Deleted
index.cssblocks (.chat-composer-shoulder-tab,.chat-composer-drawer-surface/-attached/-slot,.chat-composer-top-drawer,.chat-composer-glass-shell/-host/-with-context/-attached,.chat-composer-context-strip,.chat-composer-banner-stack-cap, and theirhtml[data-theme-id]/t3-chatoverrides): grepped the whole repo at the PR head — no remaining literal, dynamic, or test consumers. The runtime token bridges were re-expressed on the owning modules (ComposerSurface.tsx,ComposerBanner.tsx) including the[html[data-theme-id]…]andt3-chatdark scopes, and the@supports not (backdrop-filter…)/@supports not (clip-path: shape(…))fallbacks were preserved. --chat-composer-drawer-insetis no longer declared, but the only reader (ComposerCommandMenuLayer) already falls back to1.375rem, so positioning is unchanged.- Removed exports (
shoulderTabReserve,MAX_VISIBLE_WORK_LOG_ENTRIES,ThreadSyncStatusPill,ServerUpdateProgressfrom ChatView,workingStepLabel,activeTurnStartedAt) have no dangling consumers;ServerUpdateProgressis still used by Settings. ComposerCommandMenu/ComposerStashMenurender through acreatePortallayer ondocument.body, so theirdata-composer-banner-surface="attached"does not leak intoComposerSurface.Shell'shas-data-…glass-ownership switch.- Composer clearance measurement still updates via the retained
ResizeObserver; dropping theMutationObserverdoes not lose height updates. ComposerPendingApprovalPaneldiv→span change keeps valid nesting insideComposerBanner.Content(a span) and preservesrole="group"/aria-label.data-chat-composer-collapsed-controlson the approval row and banner stack only affects the mobile-collapsed focus path, which is already gated byisComposerCollapsedMobile.
Not verified
Emitted production CSS for the new compound Tailwind variants (group-not-has-[…]/banner-row, group-has-data-[composer-banner-surface=attached]/composer-surface, before:mask-[…], duration-220) could not be inspected with the available tools; the source reads as valid v4 syntax and mirrors patterns already used in the file, so no finding is raised on it.
🔍 Agent activity — 32 tool calls
💭 I'm scanning the diff for concrete UI consistency issues, starting with ChatComposer.tsx where there's logic checking whether to close the stash menu when the last entry is taken—verifying whether the condition correctly detects if no other entries remain in the queue. I'm also checking whether
isMobileViewportis actually defined where it replacedisComposerCollapsedMobilein the drawer-blocking condition, and looking at related components like ComposerActivityStatus, ComposerTasksBadge, and ComposerStashBadge for accessibility attributes and whether any tests referencing removed classes still exist. Now I'm looking at ComposerStashMenu.tsx to see if the Root component correctly forwards its ref since it's implemented as a plain function component. I'm confirming that React 19 treats ref as a regular prop for function components, so since Root's ComponentProps<"div"> type spreads unused props (including ref) down through Surface into the div, the ref forwarding works without explicit forwardRef. Now I'm looking at how the removed useEffect that reset highlightedId on entries change affects the highlightedEntry lookup logic, and checking the accessibility setup of the ComposerBanner.Row in the stash menu. The overlay button uses an absolute-positioned pseudo-element to cover the whole row for the restore click target, while the dismiss button gets z-10 to remain clickable above it, and the row itself is positioned relatively so that overlay stays scoped correctly. I'm also noting how Row merges its render prop with the button styles, and now I'm looking at how CollapsibleTrigger composes with ComposerBanner.Row and a nested button in the pending user input panel. I'm double-checking whether Tooltip accepts a triggerId prop for the server update status component, and reviewing how ChatComposer swapped out its progress-related props for isWorking and activeWorkStartedAt, deriving activeTasksProgress conditionally based on threadSyncPhase. I also note the dismiss handler for tasks appears to have been removed entirely. The removal of task dismissal is likely intentional given the PR's goal of restoring composer activity banners, though it does eliminate an accessibility affordance—more of a product decision than a bug. I'm also checking a minor icon change in ChatView's server update banner, and examining the dismiss logic condition that combines update-in-progress, update-failed, and version-mismatch-dismissed states. Comparing to the previous version, failed states now also get a dismiss option, which seems intentional. I'm also noting some redundancy between the WeakSet-based dismissal check and a direct object identity comparison in ChatView—both seem to accomplish the same thing. Now I'm looking at a chunk of CSS removed from index.css, including classes like.chat-composer-shoulder-tab,.chat-composer-drawer-surface, and several others tied to the composer's drawer and banner UI. I need to verify none of these selectors still have consumers—checking for literal, dynamic, generated, imperative, test, or shadow-root references before treating them as dead code, so I should grep the repo at the PR head for these class names. I'm also examining a Tailwind class in ChatView.tsx using the arbitrary variantgroup-has-data-[composer-shoulder-tab], questioning whether this is valid Tailwind v4 syntax given the framework'sdata-*andhas-[...]variant support. I won't over-guess the exact modifier placement rules here. The real thing to verify is that the Shell'shas-data-[composer-banner-surface=attached]check and Main'sgroup-has-data-[...]/composer-surfacecheck are both consistently reading thedata-composer-banner-surfaceattribute value set on the Surface element to determine attached vs floating state, which matters for how ComposerBannerStack renders. Confirming that ComposerBannerStack is rendered inside ChatComposer's form, which sits inside ComposerSurface.Host inside Shell -- this was previously rendered outside by ChatView but has since moved inside ChatComposer. I'm now checking the styling on ComposerBanner.Attachment, which centers itself with negative bottom margin and calculated width/max-width, comparing it against the previous CSS class that handled this positioning. I'm verifying the ComposerSurface.ContextStrip max-width value matches the earlier calc-based approach, and now I want to actually dig into the codebase to check for leftover usages of removed CSS classes, confirm whether isMobileViewport still exists, and check the Tooltip triggerId prop, ScrollArea scrollFade prop, and the icon-xs button size variant.
1. git_tools — chat-composer-(shoulder-tab|drawer-surface|drawer-attached|drawer-slot|top-drawer|glass-shell|glass-host|context-strip|banner-stack-cap|tasks-tab|stash-tab)
2. git_tools — isMobileViewport
💭 No leftover consumers of removed CSS classes, good. Now I'm checking the ScrollArea scrollFade prop, Tooltip triggerId, and Button's icon-xs/micro sizes.
3. git_tools — scrollFade
4. git_tools — triggerId
💭 I notice the tooltip doesn't have a
triggerIdproperty, so I should check the tooltip component itself.
5. browse_code — apps/web/src/components/ui/tooltip.tsx
💭 I'm wondering whether Base UI's Tooltip Root exposes a
triggerIdprop for handling multiple triggers, since Tooltip is just an alias for TooltipPrimitive.Root. Let me check how it's used elsewhere in the codebase to confirm. Also need to check if TooltipTrigger supports acloseOnClickprop in Base UI — let me grep for it.
6. git_tools — triggerId|closeOnClick
💭 I'm second-guessing whether
closeOnClickactually exists on Base UI's Tooltip.Trigger, since I recall that prop being associated with menu items rather than tooltips. I want to verify against the installed base-ui package version in the repo, but without access to node_modules I'll need to check package.json for the version and reason from there about whether this API was added in a recent release.
7. git_tools — base-ui
8. git_tools — data-composer-shoulder-tab|composer-stack
💭 I'm assuming
triggerIdis valid for this Base UI version and won't flag it. Now I'm tracing where the shoulder tabs render relative to thegroup/composer-stackwrapper, checking whether the form containingComposerBanner.Dockfalls inside that group so thegroup-has-dataselector applies correctly. I want to verify thehasShoulderTabgating logic, so I need to look more closely at the relevant sections of ChatComposer's code.
9. browse_code — apps/web/src/components/chat/ChatComposer.tsx
💭 Now I'm examining the render section spanning lines 3400-3620.
10. browse_code — apps/web/src/components/chat/ChatComposer.tsx
💭 I'm tracing through the conditional logic to figure out when the stash badge appears inline versus as a shoulder tab—it seems like the inline badge only shows under shoulder-tab mode when on mobile viewport and the composer isn't collapsed. That means on desktop,
!showInlineStashBadgeis always true, so ComposerStashBadge would render unconditionally even with an empty stash queue. I want to check whether the previous version had the same issue, and whether ComposerStashBadge itself returns null when count is zero, so I need to look at its implementation.
11. browse_code — apps/web/src/components/chat/ComposerStashBadge.tsx
💭 Good, that returns null at count 0. Now I need to check where ComposerStashMenu is rendered and whether showComposerTopDrawer includes the stash menu case, so I'll grep for isStashMenuOpen usage.
12. git_tools — ComposerStashMenu|showComposerTopDrawer =
💭 I only found the import for ComposerStashMenu, not where it's actually rendered in ChatComposer — the grep seems to have stopped early, possibly due to a null byte in the file. I'll switch to viewing the file directly to locate its usage.
13. browse_code — In apps/web/src/components/chat/ChatComposer.tsx at commit 39283f4, where is <ComposerStashMenu ...> rendered in the JSX? Give the exact line numbers and surrounding conditional rendering code (about 20 lines).
💭 I'm noting that the stash menu now lives inside the floating ComposerCommandMenuLayer and renders as a ComposerBanner with default "attached" placement, which brings in the attachment overlap variable, bottom mask, and rounded top corners that previously came from the drawer-attached styling. Critically, this also sets
data-composer-banner-surface="attached"on the Surface, and since Shell and Main both key their glass-ownership styles off that attribute, opening the command menu now flips those styles for the whole surface — since the command menu layer sits inside Main, which sits inside Host, which sits inside Shell. I'm checking whether this is an actual regression: previously the split-glass trigger required either the explicit-attachedclass or a descendant top-drawer/shoulder-tab element, and a bare.chat-composer-drawer-surface(used by the command and stash menus) never triggered it — only theexternalComposerDrawerAttachedflag did. Now anyComposerBanner.Surfaceusing placement="attached", which includes the command and stash menus, does trigger it. That would mean opening the "/" command menu now hides the shell's glass backdrop and shifts it to Main, so I need to check whether the command menu is actually meant to render as a visually attached drawer by looking atComposerCommandMenuLayer.
14. git_tools — ComposerCommandMenuLayer
💭 I want to check lines 181-260 to see if it renders in a portal or in place.
15. browse_code — apps/web/src/components/chat/ChatComposer.tsx
💭 Since the command menu and stash menu render through a portal to document.body, they're not descendants of the Shell element, so the
has-dataselector targeting the composer banner attribute won't match them. I'm also tracing how the--chat-composer-drawer-insetCSS variable gets read from the form's computed style, and recalling that the removed CSS previously set this variable on the composer form and drawer slot elements. That deletion means nothing now defines--chat-composer-drawer-inset, but sincegetPropertyValuewould return an empty string andparseFloatwould yield NaN, the code's fallback value of 1.375 kicks in automatically — so there's no visible regression, just now-dead code reading a variable that no longer exists. I want to double check there's no other place depending on that variable, and I'm also looking at whether--chat-composer-attachment-overlap, used for padding in the ComposerCommandMenu, is actually defined in that portal-rendered context. I confirm the overlap variable is set directly on the same element via a class ("attached" placement), so custom-property scoping works fine there. I'm now tracing whether the padding calculation in ComposerBanner.Root behaves correctly for the "floating" placement, where the overlap value should collapse to zero. Turning to accessibility, I'm checking whether working-status rows expose arole="status"like sync status pills do -- noting that the working state currently lacks that announcement, which could be a minor gap worth flagging. Also looking atComposerServerUpdateStatuswhere a nested interactive button sits inside the banner row -- that seems fine since the row itself isn't clickable, but I notice the failed-status span has its ownrole="alert"nested inside the banner root'srole="alert", which could cause duplicate screen reader announcements. Now I'm tracinginlineTasksBadgeusage — the diff removed it from three inline composer control positions and the mobile compact view, leaving it only referenced once inside theComposerBanner.Rootat line 3426, so I need to confirm nothing else still depends on those removed spots. I'm checkingshowInlineTasksBadge's condition against the surrounding JSX around line 3422, tracing whetherhasBannerItems,activityStackItem, andactiveTasksProgressinteract consistently so the badge and activity stack content never end up in a contradictory null state — and it seems to hold together correctly. Now I'm looking at the grid column setup for the composer banner row and confirming it's fine. Then I noticeComposerBanner.Dismisschanged behavior: the delete button used to be hover-revealed withvariant="ghost"and[--control-icon-color:currentColor]for color inheritance, but now it's always visible viaclassName="z-10"and has lost that color indirection — worth flagging since the review criteria specifically calls out shared primitives that route icon color through inherited variables. The old stash delete button usedcurrentColorso its X icon matched the row's text tint even when highlighted withtext-accent-foreground. But the newButton variant="ghost"likely sets its own muted--control-icon-color, which could clash or lose contrast on a highlighted accent background. I want to check how the Button component actually resolves that icon color variable. Turning to the keyboard navigation changes: the old menu usedCommand/CommandItemwith arrow-key handling and ahighlightedIdstate that reset via effect when entries changed. Now that wrapper is gone, rows are plain<li>elements with nested restore buttons, and arrow keys only move DOM focus if the drawer already contains focus — but since focus stays in the editor when the menu opens, arrow keys just updatehighlightedIdwithout moving focus, which actually matches the prior behavior. I notice the removed effect that resethighlightedIdto the first entry if the current highlighted id no longer matched any entry is gone now. I should check whetherhighlightedEntrystill has a fallback, so I'm going to look at the current ComposerStashMenu implementation directly.
16. browse_code — apps/web/src/components/chat/ComposerStashMenu.tsx
💭 The onFocus on the li correctly catches focus bubbling from the buttons inside. But I notice the restore button's absolute overlay spans the entire row, which means it's covering the Dismiss button and intercepting clicks meant for it. Since Dismiss sits inside a flex container (Actions), its z-10 actually works even without explicit positioning—flex/grid children respect z-index regardless of position. The restore button's overlay lives inside Content, a sibling grid item with default z-index (auto), while Actions has z-10, so Dismiss should sit above it in stacking order. Since z-10 is positive, it gets painted in the positive z-index step, above the overlay layer, confirming the Dismiss button stacks correctly above it. Looking at the header row change, turning the whole header into a "Close stash" button with a ToggleIcon instead of an icon-only close button seems like a plausible behavior shift, and there's no nested interactive element conflict there. The removed useEffect for resetting the highlight is now handled inline by falling back to the first entry when no match is found, and the aria-expanded string value on the button looks fine too. Now I'm checking the Children render of a
ulwith rows aslielements and an aria-label to see if the structure holds up. I'm now worried that rendering theulas a grid container and thelirows as grid items could strip list semantics in WebKit browsers, since Safari/VoiceOver is known to droplist/listitemroles whendisplay: gridorflexis applied directly toul/li— this is a real accessibility pitfall worth flagging. The bigger concern is that the previous implementation usedCommand/CommandList/CommandItemprimitives that provided properrole="listbox"/role="option"semantics andaria-selectedon the highlighted item, but the new version just uses plainlielements with adata-highlightedattribute and a nested button, meaning the listbox/option roles and selected-state exposure to assistive tech may have been lost in the migration. Actually, that aria-selected behavior was probably tied to the Command primitive's own highlight logic, which was disabled anyway, so it may never have worked correctly — this is getting too deep into a weak thread. Let me pivot to more concrete, defensible findings instead, like the removed dismiss/onDismiss handling or the shoulderTabReserve removal in ChatView, since these feel like clearer consistency issues to pursue.
17. browse_code — apps/web/src/components/ChatView.tsx
💭 The ResizeObserver on the overlay handles height changes fine. Now I'm double-checking ComposerBanner.tsx for Tailwind concerns, specifically whether the arbitrary mask gradient syntax correctly maps to mask-image in Tailwind v4. 1, and whether stacking a color-mix background alongside a linear-gradient background on the same pseudo-element resolves correctly into separate background-color and background-image properties rather than conflicting. I'm also verifying that tailwind-merge treats these two arbitrary bg values as distinct groups instead of deduping them, since one matches its image regex and the other its color regex — and confirming that negative z-index like
-z-1is valid in v4. Now I'm checking the@max-[400px]container query variant on Row, which only works when there's an ancestor with@container— that's only true when Root useswidth="fill", notwidth="content". I'm tracing through whether any content-width Root actually pairs with a wrap-actions Row, since that combination would break, and so far the content-width usages I'm checking use the default inline layout instead. I'm also verifyingnot-has-[...]andgroup-not-has-[...]variants with comma-separated selectors inside brackets are valid in Tailwind v4's arbitrary variant syntax. Good, Row has that named group. Now I'm checking whether Tailwind's compoundgroup-not-has-[...]/banner-rowsyntax actually parses correctly, sincegroup-*variants expect either an arbitrary selector or another registered variant name, plus the optional/namesuffix. Since I can't run a build to verify how these unusual variants get emitted, I shouldn't guess at behavior I'm not sure of, so I'll focus on higher-confidence findings instead. I'm now looking at the adjacency selectors used forComposerBanner.Attachment, examining the[&+[data-slot=...]]sibling selectors targeting attached surfaces. That checks out as equivalent to the old CSS structure. Now I'm turning to something more suspicious:ComposerBanner.Peekrendered inside thenoticesRefdiv within a reversed flex-column container. Peek now has an accessible label and is a real button, which is an improvement. I noticeComposerBannerStacksorts items usingtoSorted, which requires fairly recent browser support (Chrome 110, Safari 16.4) — I should check whether this method is used elsewhere in the codebase to see if that's an established pattern.
18. git_tools — toSorted(
19. browse_code — apps/web/src/components/chat/ComposerBannerStack.tsx
💭 I'm confirming that Tailwind v4 accepts arbitrary bare numeric values for duration, so something like duration-220 works fine, and thinking through combining a transition property list with translate utilities. Then I check whether the ComposerBanner.Peek component using ref as a prop is compatible, which depends on React 19's ref-as-prop support. I'm also verifying that pointer events still bubble correctly from the absolutely positioned Peek child up to the parent notices container, so the hover-expand behavior should still trigger properly even though the collapsed container has zero height. Now I'm noting the intentional behavior change — hovering used to expand the whole banner stack including the front banner, but that's been replaced with click-to-expand for the peek, which seems like the correct new design rather than a regression. I'm also confirming the role="alert" semantics carry over cleanly to the new Root component, and that the wrap-actions layout's container queries will work correctly since Root sets @container for fill-width banners, which is the default case for the floating stack. Next I want to check ComposerBanner.Dismiss and the Button component for how they handle the --control-icon-color styling.
20. git_tools — control-icon-color
💭 I'm realizing that ComposerStashMenu's delete button used to explicitly override the icon color to currentColor so the X matched the row text, but now ComposerBanner.Dismiss uses the ghost variant without that override, so the X ends up using the muted foreground color instead of matching the highlighted row's text color. Though I question whether this is truly a contrast issue since accent backgrounds tend to be subtle, this still looks like a genuine, if minor, regression worth flagging with moderate confidence. I also note the hover-reveal behavior was intentionally removed—the button used to fade in on hover but is now always visible, which seems like a deliberate simplification rather than a bug. I want to keep digging for a clearer violation before settling on this one. I'm now comparing the dismiss button configurations across ComposerBanner and ComposerBannerStack, confirming they use the same icon-xs ghost variant. Then I'm tracing how className and props merge through the Row component's button render, checking that user-provided classes and event handlers are appended correctly rather than overwritten. Finally I'm examining how flex-1 sizing applies to the tasks badge versus the stash badge within the dock layout, since their Root elements may size differently depending on the fill-width data attribute. I'm now checking the conditions under which the Dock renders shoulder tabs — verifying that
hasShoulderTabcorrectly accounts for cases where only a stash badge is present, without requiring a nonzero task count, and tracing what happens when the stash queue is populated but inline stash badge display is disabled. Stepping back, I want to identify the highest-confidence UI inconsistency, and I'm zeroing in on a suspicious change where adivwas swapped for aspanin the pending approval panel. That makes sense since it's now nested insideComposerBanner.Content, which is itself a span, and divs can't legally nest inside spans — so I want to check whether the panel's internal children still use divs that would break this. I'm also tracing howComposerBanner.Contentwraps other banner titles, like the server update status, to confirm the button-inside-span nesting there is valid.
21. browse_code — apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx
💭 I'm noticing the two separate branches for desktop and mobile-collapsed approval states have been merged into a single unified branch, so the mobile-specific layout differences are now folded into the shared markup. But that unification means
data-chat-composer-collapsed-controls="true"is now applied on desktop too, and this attribute gets checked in the form's focus handler to skip setting composer-focused state. So focusing approval buttons on desktop will now also bypass that focus flag, which previously only happened on mobile — same thing now happens with ComposerBannerStack's Attachment wrapper, so dismiss/peek button focus won't mark the composer as focused either. I'm trying to figure out whether that desktop side-effect is intentional or an unintended consequence of the merge. I want to check whether this even affects anything on desktop sinceisComposerFocusedprimarily drives the mobile collapsed state, so its desktop impact might be negligible — but I should check other consumers before assuming it's harmless. I'm also looking at how banner priority ordering changed: the old explicit ordering blocks for urgent/background/calm/woke/parked items got replaced by a single spread with sorting handled bybannerPriority, and sincetoSortedis stable, same-priority items should keep their relative order, though I need to verify the previously documented ordering guarantees still hold. There was an important invariant that the background liveness banner needed to appear before calm system items so it wouldn't get covered, since its Stop button is the only way to stop settled turns. I need to check the current ChatView code around where banners are assembled now to see if that ordering is preserved with the single spread.
22. browse_code — apps/web/src/components/ChatView.tsx
💭 I'm checking the DOM order and priority sorting between backgroundLivenessItems and systemComposerBannerItems, tracing where the "activity" priority got added in the diff to figure out how it compares against calmer system items.
23. browse_code — apps/web/src/components/ChatView.tsx
💭 Both backgroundLiveness and the composer activity item share priority "activity", giving them the same bannerPriority of 0. Since the sort is stable and bannerItems are processed before the appended activityStackItem, background liveness ends up as the front attached item when present, pushing the composer's own activity row into the hidden stack. This seems like it could be a regression: previously ThreadSyncStatusPill and task badges were always separately visible, not stacked, but now they may get hidden behind the collapsed peek whenever background liveness is active. The stated invariant "activity stays attached" only holds for one item at a time, and having two competing priority-0 items breaks that assumption. Then again, maybe this ordering is deliberate — the old comment about the Stop button suggests background liveness needs to stay visible, though that would hide the composer's own working/syncing indicator, which seems like a regression too. I need to check whether
activeBackgroundLivenessandisWorkingcan actually be true at the same time, so I should look for whereactiveBackgroundLivenessis defined.
24. git_tools — const activeBackgroundLiveness
💭 I'm noticing that background liveness requires not-working, so working state and background liveness are mutually exclusive, but sync phase can coexist with both non-working and background liveness—meaning the sync status and the background liveness item end up sharing the same priority level, which could hide the composer's sync indicator behind the peek when previously the sync status pill was always rendered visibly in its own drawer. Weighing whether this is a real risk versus a marginal edge case, I think it's worth checking something more clear-cut in the MessagesTimeline changes instead, since those touch a broad scope of the web app. The key change there is removing the "working" timeline row and its visible-entry cap, replacing it with a single collapsed activity toggle that groups all non-error, non-spawn entries together. I notice the toggle row now always carries a non-null summary, but the chevron that previously signaled expandability has been swapped for a tool-group icon, so there's no visual cue that the row can be expanded or collapsed — though this actually matches the existing tool-group summary style elsewhere, so it's consistent rather than a regression. The removal of the "Working for X" timer row from the timeline (now moved to the composer) is clearly the intended point of the PR, and I'm also noting the removal of ThinkingActivityRow and the showThinking flag as related cleanup. I should check whether removing WorkingTimer leaves unused imports in MessagesTimeline.tsx that would trip lint, whether workingStepLabel is still referenced elsewhere, and whether MAX_VISIBLE_WORK_LOG_ENTRIES or ThinkingActivityRow have any other consumers I need to update.
25. git_tools — MAX_VISIBLE_WORK_LOG_ENTRIES|ThinkingActivityRow|workingStepLabel|activeTurnStartedAt|shoulderTabReserve|ThreadSyncStatusPill|ServerUpdateProgress
💭 No dangling references there. I want to double-check that serverUpdateStageLabel is actually exported from ServerUpdateAction.
26. git_tools — serverUpdateStageLabel
27. browse_code — apps/web/src/components/chat/ComposerBanner.tsx
💭 I'm noticing that selector on line 111 now looks dead since ComposerBannerStack moved inside the form, making that sibling-based selector unreachable. It's not really a violation since it's just an unused defensive rule left in place rather than a deletion risk. Now I'm checking whether Tailwind v4.1's
mask-[...]utility emits bothmask-imageand the-webkit-mask-imageprefix, since that affects cross-browser support for this masking effect. Confirming line 108's max-width value matches the old calc-based CSS equivalent (45.25rem), so it's unchanged. Next I want to move through the rest of ComposerBanner.tsx, from Root down to Dismiss, to check the remaining pieces.
28. browse_code — apps/web/src/components/chat/ComposerBanner.tsx
💭 I'm noticing that ToggleIcon applies the full interactive button styling — cursor, focus rings, touch-target pseudo-elements, disabled states — onto what's actually a non-interactive, aria-hidden span, which seems like a mismatch. Specifically, that touch-target enlargement (
::afterat min 44×44) is meant to expand a real button's own hit area, but here it's on a decorative span nested inside a row button. Since the span has nopointer-events:none, this::afteroverlay could end up sitting adjacent to other controls in the row and interfere with them on coarse-pointer devices. Since the enclosing Row itself is the button in both ComposerStashMenu and ComposerTasksBadge, clicks still land on the correct target regardless — so it's contained, though borrowingbuttonVariantson a non-button span is an unusual pattern, likely a deliberate compromise since buttons can't nest. Now I'm shifting to look at the Row's icon-column min-height sizing across breakpoints. More concerning is that the coarse-pointer hit target expansion previously present on the Button-based badges (pointer-coarse:after:min-h-11) seems to have been dropped for the bare Row-button used in the inline tasks badge, which the migration should have preserved per the accessibility guideline on touch targets — I want to confirm whetherComposerTasksBadge's inline variant still usesComposerBanner.Rowin a way that retains this expansion or not. I'm weighing whether the stash menu's icon color loss is worth flagging — probably too weak since the muted foreground likely still reads fine against the highlighted background. A stronger candidate is the ToggleIcon applying button styling to a decorative aria-hidden span, which seems like a more defensible semantic issue worth reporting. Since there's no explicit top/left offset, the overlay's static positioning extends 44px down and right from the span's top-left corner — with a ~28px row height, that's roughly 16px overflow below and overflow past the banner's right edge. This could swallow taps meant for the row underneath, like the first task item or the panel's option buttons in ComposerPendingUserInputPanel's collapsible content, which is a real coarse-pointer regression worth flagging. I'm now confident this is a legitimate finding: applyingbuttonVariantsto a decorative span reintroduces the coarse-pointer hit-target pseudo-element withoutpointer-events-none, so the fix is to addpointer-events-noneto that span or drop the coarse hit-target entirely. I'm double-checking the base Button styles to confirm there's no existingafter:pointer-events-noneand thatrelativepositioning is present, which would confirm the overlay behaves as I expect. I'm also noting a possible ordering issue inComposerBannerStackwhere an appended activity item could conflict withbackgroundLivenessBannerItemsince both share the samepriority: "activity"slot -- worth flagging as a risk. Actually I decide it's too speculative since only one item likely coexists in practice, so I'll skip that concern. Now I'm looking at Finding 5, whereComposerBannerStackAlertsetsrole="alert"on the Root while another component nested inside also declares an alert role, creating a possible duplicate. But checking further, when items render via the content branch they skip the alert role, so nesting only happens forComposerServerUpdateStatusinside a full item's title — a real, moderate issue worth flagging. TheComposerServerUpdateStatustrigger button uses cursor-help styling with a click handler for the tooltip, which is a bit unconventional as a disclosure pattern, but it does carry a full aria-label so it's acceptable. The working timer logic wasn't duplicated either — it was relocated from MessagesTimeline into ComposerActivityStatus, which is fine. CheckingComposerBanner.Root's padding calc using--spacing(1)inside a calc expression — this is valid in Tailwind v4.1, which supports the spacing function in arbitrary values. Now I'm looking atComposerStashBadge, which overrides the primitive's font size at the call site withtext-xs sm:text-xs, duplicating the responsive breakpoint redundantly since the baseButtoncomponent already setstext-base sm:text-sm— so this override seems necessary to shrink it down but is written a bit oddly. I'm reasoning through whether the tailwind-merge behavior and the audit rules flag this: font size isn't in the listed set of critical properties to check for overrides, and since only one call site does this, it doesn't warrant a shared variant, so I'll skip flagging it. Moving toComposerTasksBadge, I'm checking whetherTaskSegmentswithclassName="hidden w-20 sm:flex"returns null under certain step thresholds. Now I'm looking atComposerPendingUserInputPanel'sCollapsibleTrigger, which now nests arenderprop composingComposerBanner.Rowand a button. Since Base UI'sCollapsibleTriggerhandlesaria-expanded/aria-controlsand passes through thetitleprop, the composition should work, but I'm checking whether the previous hover and focus-visible ring styles on the trigger are preserved or replaced by whateverRownow supplies. Also theCollapsiblePanel's horizontal padding, which was intentionally kept there with a comment explaining that the panel must own it since the panel clips content while animating height — that comment is now gone, and the padding has moved intoComposerBanner.Bodyinside the panel, seemingly contradicting what the original comment warned against. I should verify this against the guideline about preserving intentional exceptions tied to browser or component constraints, but first I need to confirm whether CollapsiblePanel actually applies overflow hidden during animation—checking Base UI's collapsible implementation and the current ComposerPendingUserInputPanel structure would clarify this.
29. browse_code — apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx
30. browse_code — apps/web/src/components/ui/collapsible.tsx
💭 I'm realizing the panel lost its horizontal padding, so the focus ring on the option buttons now has no room on the trailing edge since the banner body only pads the leading side for the icon column, causing the ring to get clipped by overflow-hidden. The vertical padding is also gone since the old inner div's bottom padding was dropped, meaning the last option's focus ring gets clipped at the bottom too. This confirms it's a real regression, so the fix is to restore trailing/bottom padding either on the ComposerBanner.Body or back on the CollapsiblePanel itself, matching the original constraint that padding needs to live on the panel so it clips correctly during the height animation. Given the ToggleIcon hit-target overlap with the focus-ring clipping issue, I think this combination is enough to fail the check by default. I'm settling on two findings: the ring clipping on the end/bottom sides, and the decorative ToggleIcon's
aria-hiddenspan carrying the full button variant styles including the 44px touch hit-target overlay, which could swallow taps on adjacent content — fixable by addingpointer-events-none. I'm working out the exact line number to attach the inline suggestion. Confirming line 204 is within the diff. Next, I'm flagging that ComposerServerUpdateStatus nests a role="alert"/role="status" span inside the banner's own role="alert" container, since ComposerBannerStackAlert applies role="alert" to all stack items — this causes double-announcement, so I'd suggest dropping the inner role. Weighing whether this nested-alert issue is worth flagging: it's a legitimate but low-severity accessibility regression, since the title used to be a plain string and the nesting is newly introduced by this change, so I'll keep it as a third, softer comment alongside the two stronger ones. I want to check something else though — the newdata-chat-composer-collapsed-controls="true"attribute on desktop approval rows, since the focus-capture handler skips setting composer focus for elements carrying it. I need to grep for other usages of that attribute and check the code around lines 3399-3404 in ChatComposer, plus whetherscheduleComposerCollapseCheckdepends on it, before deciding if this is worth flagging.
31. browse_code — apps/web/src/components/chat/ChatComposer.tsx
💭 That mobile-only gating means desktop is unaffected, so I can drop that concern. Now checking finding #1's claim about the pseudo-element positioning on Button — with
relativeon the base andafter:size-fullbut no explicit inset offsets, the absolutely positioned pseudo-element would just match its parent's static position without any shift. Working through the geometry: a 44×44px::afterbox anchored at the icon span's top-left corner would overflow to the right and below by roughly 20px given the icon itself is much smaller. Since pseudo-elements are hit-testable by default and Button doesn't disable pointer events forafter, this creates a real tap-blocking overlay on coarse pointers — and since it overflows past the icon's own bounds, it could intercept touches in neighboring content, like the option buttons below a collapsed panel, the task list under an expanded badge, or the first item in a stash menu header. That confirms a genuine touch regression where taps meant for content below the header hit the header button instead, triggering the wrong action (like collapsing/closing) rather than the intended one. I'm also verifying the second finding: checking whetherComposerBanner.Bodyis really the only relevant wrapper, tracing its padding classes, and confirming that its focus ring gets clipped by the panel's overflow-hidden since Tailwind's ring utility renders as an outside box-shadow. Then I'm examining how the padding onComposerBanner.Rootinteracts with the panel's own padding — since the Root's padding surrounds the panel but doesn't affect where the panel's own clipping boundary sits internally. Now I'm confirming that the ring clipping still happens regardless of the Root's padding, since the option buttons span the full width of the panel and get cut off at the panel's own edge, not the Root's. I'm deciding to skip flagging the list semantics loss inComposerStashMenuto keep the review focused, but I want to double check whetherComposerBanner.Attachment's dead second selector branch ever actually gets triggered by checking if any attachment renders outside the expected form context. I've verified the dead selector is harmless since it's just a defensive forward-looking check, and both the composer stack and tasks drawer render attachments inside the form anyway, so I'll leave it out of the inline comments to avoid clutter. I'm finalizing the review with three inline comments: one on the ToggleIcon in ComposerBanner.tsx, one on the pending user input panel, and one on the server update status component. I'm now pinning down the exact line numbers in ComposerServerUpdateStatus.tsx by walking through the new file's imports, matching each line against where the role attribute (alert vs status) appears so I can reference the correct location in comment 3. My line-count check confirms the detail logic and role attribute match up as expected. Now I'm reconsidering whether the third comment about a duplicate live region is even worth raising, since the outer banner root already has its own alert role. Actually, there's a plausible reason the inner role exists: the outer alert fires only on insertion, while the inner one could re-announce stage transitions as content changes, though nesting alerts risks a double announcement on initial insertion. Given that ambiguity, I'll drop this third finding and stick with the two higher-confidence issues, briefly considering other spots like the stash menu button before deciding they're not worth flagging either. I check the width handling across the Dock components—confirming thatflex-1andw-fullstyling flows correctly whether a Row is inside a fill-width or content-width Root, and it all checks out as consistent. TheshowInlineStashBadgeswitch fromisComposerCollapsedMobiletoisMobileViewportlooks intentional—it just changes where the stash badge renders on mobile when the composer is focused. I'll flag two inline comments with clean suggestions rather than a broader restructuring note. For the first, I'm proposing addingpointer-events-nonealongside the existing button variant classes so the icon button doesn't intercept clicks. For the second, I'm suggesting theComposerBanner.Bodyelement pick up padding classes (pb-1 pe-1) to fix spacing—just double-checking the exact indentation from the file view before finalizing the suggestion block. Since I have actual findings, I'll set the check status to failure rather than "All clear" and post the review with these two suggestions.
32. modify_pr — UI consistency review found 2 issues in the new composer banner primitives. Details inline.
Agent Credits: 447 credits
Token usage: 64 uncached input · 3,679,478 cache-read input · 215,766 cache-write input · 42,381 output
Agent Credits may also include non-token charges from external tools such as web research.