Repository navigation
UI Consistency: 1 issue found
1 finding
apps/web/src/components/chat/ComposerTasksBadge.tsx(lines 151-159, and the same control inComposerTasksDrawerat lines 191-199): the "Dismiss tasks for this turn" icon action is hand-rolled as a raw<button>, while the inline placement in the same component renders the identical action withButton size="icon-micro" variant="ghost-muted". The raw version loses the primitive's focus-visible ring tokens and the coarse-pointer 44px hit target, and usesrounded-md/hover:bg-muted/60instead of the sharedrounded-sm/hover:bg-accenttreatment, so the same control behaves and looks different depending on placement. An inline comment with a suggested fix has been posted.
No other violations were found in the changed web UI scope.
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 changed by the PR, plus directly affected consumers:
components/chat/ComposerTasksBadge.tsx(new),ComposerStashBadge.tsx,ComposerStashMenu.tsx,ComposerCommandMenu.tsx,ComposerBannerStack.tsx,ComposerPendingApprovalActions.tsx,ComposerPendingApprovalPanel.tsx,ComposerPendingUserInputPanel.tsx,ComposerPlanFollowUpBanner.tsx,ThreadSyncStatusPill.tsx,ChatComposer.tsxcomponents/ChatView.tsx,components/ComposerPromptEditor.tsx,components/composerInlineChip.ts,components/ui/button.tsxsrc/index.css(new composer drawer/shoulder-tab glass system)
Items checked and cleared
- New
microbutton size: added as a named primitive size inbuttonVariantsrather than repeated call-site geometry, and it keeps the base focus ring andpointer-coarsehit-target contract. Adopted consistently byComposerPendingApprovalActions,ComposerStashBadge(inline), andComposerTasksBadge(inline). ghost-mutedoverrides inComposerPendingApprovalActions: the[:hover,[data-pressed]]:text-destructive-foreground/text-foregroundcall-site classes are merged after the variant's own[:hover,[data-pressed]]:text-foreground, so the intended tone wins; no icons are routed through--control-icon-colorin these buttons.- Icon color indirection:
ComposerStashMenu's delete button and the inline stash badge explicitly re-point--control-icon-colortocurrentColor, preserving tone after the ghost migration. - CSS ownership in
index.css: the added rules are glass composition, masked pseudo-elements,:has()-driven shell splitting, and runtime theme-token bridges shared by two or more owners — legitimately global under the stated rules. Theme-only declarations use@variant dark; no raw.darkwas introduced.:is(...)groups are non-empty and each branch can match a real class/attribute;.chat-composer-drawer-slot + …::beforesibling rules resolve for the banner-stack → sync-pill → top-drawer stacking order actually produced byChatView. - Removal of
background: transparentfrom.ultrathink-frame: required so the new[data-chat-composer-main-surface]glass background can paint on that element (the unlayered rule would otherwise beat the layered one); the frame remains transparent in the unattached case where the shell::beforestill paints. - Portal-rendered drawers (
ComposerCommandMenuLayer→document.body):--chat-composer-glass-surface/--chat-composer-outlinedo not inherit outside the shell, but every consuming declaration supplies a fallback and thehtml[data-theme-id]bridges are document-scoped, so theming holds. - Moved module
providerSkillPresentation→@t3tools/client-runtime/providerSkills: no stale importers or references to the removedformatProviderSkillInstallSource/groupSlashCommandSectionsremain. - Inline badge duplication: the inline tasks/stash badges are mutually exclusive across the collapsed-mobile row, mobile pending actions, and footer branches; no double render.
- Behavior-changing composition (drag/focus handlers hoisted to the form, command-menu positioning off the main surface, plan step durations) is covered by the added focused tests.
🔍 Agent activity — 2 tool calls
💭 I'm digging deeper into the diff, focusing on web UI files. I'm examining ComposerTasksBadge.tsx, a new component that uses raw button elements instead of the core Button primitive, and checking whether the custom tab geometry and dismiss button styling justify deviating from that convention. I'm also checking ComposerTasksDrawer for the same raw dismiss button pattern, ComposerStashBadge.tsx for similar issues, the new CSS additions in index.css for selector validity and dark variant usage, and ComposerPendingApprovalActions for how it handles icon color indirection through the ghost-muted variant. I'm noticing that the dismiss buttons in ComposerTasksBadge and ComposerTasksDrawer use raw
<button>elements instead of theButtoncomponent already used elsewhere in the same file, which means they're missing the focus-visible ring and the coarse-pointer minimum hit target. This is a clear, concrete accessibility finding worth flagging. The stash badge tab being a raw button is pre-existing and arguably fine as a semantic tab exception, but the newly added tasks tab dismiss button should probably use the Button component like the inline variant does. I'm also noting that when the menu is open, the tab gets pointer-events-none but remains focusable, which could be a keyboard accessibility issue worth flagging. Moving to ComposerCommandMenu, the removedgroupSlashCommandSectionsprop looks fine since its usage was removed too, and the remaining imports appear clean with no unused ones. Now I'm checking the index.css additions for malformed selectors and looking at whether--chat-composer-attachment-overlapis defined redundantly across multiple drawer surface selectors. These appear to be different selectors with distinct purposes, so the duplication is likely intentional rather than a real issue, though I want to verify against the actual CSS region. I'm also checking that the:has()pseudo-class usages without a leading element compile correctly as implicit universal selectors, which they do. One thing that stands out is the removal ofbackground: transparentfrom the globalcoderule in index.css — I need to check what replaced it. I notice the diff dropped a transparent background declaration from a code rule near line 1936, while the pre/code font-family rule nearby stayed intact. I want to check the original file around that region to confirm exactly what was removed. Good, the transparent background removal is scoped to the ultrathink-frame area rather than the code block, so I should double-check that region to confirm it's not a regression. I'm tracing where.ultrathink-frameis used, likely on the composer frame, and thinking through why removing its transparent background matters now that the main surface handles the glass effect instead of the frame itself. I'm checking whether the transparent background on the ultrathink-frame is now redundant, since that same element is the main-surface div that the new CSS rule already targets with a color-mix background when active. Since.ultrathink-framesits outside any@layerblock, it wins over the layered rules regardless of specificity, so removing the transparent background was actually necessary for the glass effect to show through — this looks like a deliberate, correct change rather than an issue. Now I'm moving on to check the form-level changes in ChatComposer, starting with the type update for the drag-leave handler. I want to confirmdata-chat-composer-form="true"is present on the form, and I'm noting thatshowComposerTopDrawerreplacinghasComposerHeadermeans the padding no longer shrinks when there's a header — a minor but intentional visual tweak. Let me look further into the ChatComposer code around that section. The stash badge and tasks tab positioning look fine with their spacing, so I'm checking the inline stash badge and tasks visibility logic next. I'm verifying the drawer inset CSS variable parsing handles rem units correctly, then checking the pending approval panel's focus styling and the stash menu's delete button for its variant and size props. I'm checking that the delete button stays focusable despite pointer-events-none, since focus-visible re-enables pointer events for keyboard users, and confirming the group-focus-within variant works correctly with the stash group naming. Now I'm moving on to look at the ComposerStashMenu's outside-pointer handling. I'm tracing whether the stash menu's outside-click handler conflicts with existing handling in ChatComposer, since the menu renders through a portal layer. Then I'm cataloging concrete findings: raw buttons in ComposerTasksBadge and ComposerTasksDrawer duplicate styling from the shared Button component but lose focus-visible rings, disabled state handling, and proper touch target sizing, and the tab toggle button relies on the default browser focus ring instead of the app's consistent ring token. I'm deciding the tab toggle itself is likely exempt as a semantic tab container, so I'll center the finding on the dismiss icon button, which duplicates an inline icon button elsewhere in the same file. I also check the destructive variant styling in ComposerPendingApprovalActions and confirm it's fine since those buttons have no icons, then start examining how the approval-state footer behavior changed now that pending approval actions moved into the top drawer. Now I want to verify whethergroupSlashCommandSectionsis still referenced anywhere in web after being removed from ComposerCommandMenu, and whetherformatProviderSkillInstallSourcestill has importers pointing at the old providerSkillPresentation path — I should grep for both. I notice the stash-tab and tasks-tab classes are referenced in TSX and tests but have no corresponding CSS rules, so they're just marker classes used as hooks rather than actual dead selectors—harmless. I'm now checking whether the tasks tab's text-muted-foreground styling is applied correctly. Looking closer, the container'sallDone && "text-foreground"gets overridden by the child button's explicittext-muted-foreground hover:text-foreground, making that container-level state color effectively a no-op since nothing else inherits it. It's a minor dead-style inconsistency, but possibly too nitpicky to flag given the count span already handles the success color separately. I want to move on to bigger things — checking the positioning math in the command menu layer, the composer drawer attachment classes in ChatView, and the banner stack's slot styling, which all look fine on inspection. I'm also checking the approval panel's scrollable code block for accessibility, which seems properly labeled, and now I'm looking at whether command menu items override primitive padding with large inline class strings that should probably be flagged as a call-site override pattern. The padding override isn't worth flagging since it's likely harmless. I'm now focusing on real regressions, starting with the raw buttons in ComposerTasksBadge/Drawer's dismiss action, which breaks consistency with the inline branch's Button component and loses the focus ring and proper hit target. Next I'm checking ComposerCommandMenu's skill source rendering — previously a text label, now an icon with sr-only text, which seems fine. But I want to verify whether it still renders the provider-slash-command icon or slash-command BotIcon, since the current logic seems to show nothing for non-skill items; I need to look at the actual leading icon block in the file to confirm. I'm confirming the sr-only span stays out of flow so it won't disrupt the flex gap layout, and noting the theme no longer drives the non-path logic in ComposerCommandMenu. Now I'm checking the scroll area slot styling selectors. Since the command menu renders in a fixed-position portal outside the glass shell, it won't inherit the shell's CSS custom properties for surface and outline colors—but the drawer-surface class has a fallback to--card, so that's covered. I'm now tracing how the attached-surface variable gets set via the theme-id selector to--app-theme-surface-raised. Since the portal renders outside the normal DOM tree, I'm checking whether theme variables still cascade correctly to nested surfaces like the banner stack and thread sync pill, since they rely on inherited custom properties that might not reach them in the portal context. I notice the banner stack and sync status pill actually sit outside the composer glass shell as siblings, relying on a different surface variable that falls back to the card color, while the shell itself uses its own glass surface variable with separate theme overrides—so they're styled independently rather than inheriting from the same source. I'm checking how the context strip interacts with the composer glass shell — when a drawer or tab is attached, the shell's pseudo-element is hidden and the context strip gets its own background instead, which seems correctly handled. But I notice the clip-path rule for the glass host's after-element also ends up applying to the main surface selector once attached, which could cause unintended overlap. I'm also verifying the:has()selector usage for the shoulder tab and context shell — these rely on modern browser support, which should be fine in Electron. There's also an older fallback block for browsers without clip-path shape support that likely wasn't updated for the attached state, though that may not matter much. Given the scale of this PR, I want to focus on identifying concrete, specific violations rather than getting lost in every edge case. I'm checking a few candidate issues: raw dismiss buttons in the tasks badge tab and drawer duplicate a Button variant used elsewhere in the same component, but lose the focus-visible ring, the larger touch target for coarse pointers, the correct hover background token, and the correct border radius. I also looked at whether the stash badge's inline branch drops a shoulder-tab class, but that doesn't seem to apply. Then I'm examining the decline button in pending approval actions, which overrides text color on hover/pressed states using a ghost-muted variant that's meant for icon coloring — there are no icons here, so I'm questioning whether that arbitrary-variant class syntax actually merges correctly with the variant's own hover/pressed styling given how tailwind-merge handles it. I confirm the merge logic works as intended since both the variant and the call-site target the same hover/pressed selector group, so the call-site's destructive color correctly overrides the base foreground color — that's deliberate. I also note the tasks badge's inline button wrapping a fixed-width segment component looks fine, and the drawer's collapse button has a string literal aria-expanded without an aria-label, though its visible text content probably covers that. I flag a minor issue where the tab's outer div has padding that isn't clickable since only the inner button responds, and now I'm looking at a CSS selector chain involving the drawer slot and top-drawer classes. I trace through the sibling structure to confirm the:has()selector correctly targets the wrapper div containing the top drawer, so the border and radius reset on::beforeapplies as intended given the actual DOM nesting. Checking the stacking order of banner stack, sync pill, and top drawer confirms each gets the right rounded or flat top treatment, including the edge case where only a top drawer exists with no preceding slots. Now I'm looking at howexternalComposerDrawerAttachedis computed in ChatView based on banner items and thread sync phase. In the draft-hero state, the banner stack floats detached above the headline in an absolutely positioned container, yetexternalComposerDrawerAttachedstill evaluates true, causing the composer to apply attached-drawer styling (split glass shell, inline badges) even though nothing is actually attached to its top edge. This also makes the floating banner stack get flat-bottom overlap masking and negative margin meant for attached drawers, which seems wrong since it's hovering above the headline rather than touching the composer. That negative margin on the last child shrinks the box height by 1rem, shifting the whole stack down so it overlaps the composer — confirming that's the intended behavior. The hero wrapper also holds the DraftHeroHeadline, which is fine. Now I'm looking at finding 9, about how the first item in ComposerBannerStack gets the "attached" state and its associated class. I'm checking whether swapping in the new drawer-surface classes instead of the alert-glass rounded styling causes the Alert component to lose its variant colors, so I need to look at how ComposerBannerStack actually renders the Alert. I'm checking that the drawer surface CSS strips the Alert's default background and border so thedata-variantattribute can retint it instead—error/info/success/warning have rules, whiledefaultfalls back to the base outline, which works fine. I still need to verify whether Alert's variant styling also controls text color, since that could affect the retint. Tracing the stack cap's border variable, I confirm--chat-composer-attached-outlineresolves through a fallback chain since--chat-composer-outlineisn't defined outside the glass shell, and the theme-scoped selector covers it too. Now I'm checking whether the attached banner loses its rounded top corners—the visible surface comes from the::beforepseudo-element with rounded top corners, while the Alert's own background and border are nullified, so its own rounded classes shouldn't matter visually. I'm also verifying thatoverflow: hiddencombined withisolation: isolateon the drawer surface doesn't clip or misorder the negative z-index pseudo-element, and it looks fine since the stacking context keeps things layered correctly without clipping since the pseudo-element is inset to zero. With one solid finding already identified around the raw dismiss buttons, I'm scanning a couple more spots for thoroughness, including the pending user input panel and option button class changes, which both look like minor, inconsequential tweaks. Now I'm checking the pending approval panel's accessibility setup, which looks fine with its group role and label. I'm also tracing the inline chip label class name change to confirm the removed skill chip label class isn't still referenced elsewhere, and whether the prompt editor's switch to the shared chip label class is consistent across consumers. I'm adjusting the inline chip geometry styling—setting a fixed height with tighter line spacing and centering the label block, used for chips like FileTagChip and SkillInlineText in chat rendering. Since it's inline-flex with truncation, the fixed height shouldn't cause clipping issues. Now I'm checking the ChatComposer keyboard handler logic: when there are pending user inputs and the composer isn't in an approval state, it toggles the stash menu open instead of the previous early-return behavior, even if the stash queue is empty—which just shows an empty state message, so that seems intentional for the stash shortcut. There's a pre-existing effect that closes the stash menu whenever the prompt changes. I'm also noticing that when the stash menu opens via the inline badge while a top drawer is showing, it renders in the command menu layer anchored to the composer, but only under a specific condition checking that the stash menu is open and the composer menu isn't. I'm tracing through the collapsed-mobile case: the anchor div gets hidden via a class when the composer is collapsed, but tapping the inline stash badge should expand the composer first before opening the menu, and since the approval state hides the badge entirely, that edge case is moot. The anchor ref still resolves correctly even when hidden, so positioning falls back to the main surface rect — which actually works fine, so that's not a bug. I think the main finding from this review is still the raw dismiss buttons issue, and I want to double check the tasks badge component for similar pointer-events concerns before moving on. Now I'm checking whether the stash badge's pointer-events-none state still allows keyboard activation via Enter, which would create an inconsistency between mouse and keyboard behavior, though the existing test only checks for visibility rather than interactivity. I also want to verify whether there's a global focus-visible outline rule nearby that might affect the raw tab buttons, so I'm looking at the surrounding CSS context to see what rule that outline property belongs to. The.chat-composer-shoulder-tabclass is shared between the stash tab and tasks tab components, so keeping it as global CSS with the glass/backdrop-filter styling is justified. I should also verify whetherformatDurationis exported from session-logic since it's used in ComposerTasksBadge. I need to check that activeThreadShell's planProgress field matches the expected ComposerTasksProgress shape with step, completedSteps, and totalSteps, so I should look at the shell's type definition. The types line up fine in ChatView, with activePlan.steps matching ComposerTaskStep's optional durationMs and status fields. I'm now weighing whether a single finding is sufficient to fail the check, given it defaults to failure. I do have a solid finding: the dismiss button is hand-recreated instead of using the existing Button primitive already used elsewhere in the same file, missing the focus-visible ring, coarse-pointer touch target, radius, and hover tokens — a genuine consistency and accessibility issue. I'll consider whether the tasks tab toggle button counts as a second finding, but decide to fold it in as context rather than a separate ask, keeping the comment focused on the dismiss buttons. I also check a few other candidates — the collapse button in ComposerTasksDrawer's header (more of a disclosure control than a primitive), a dead conditional class in ComposerTasksBadge, z-index/positioning on ComposerStashBadge, and the gating logic in ChatView — but none of these rise to the level of a real issue worth reporting, so I'll keep the review tight around the one clear finding. Now I'm tracing whether inline task/stash badges could render twice simultaneously across the collapsed mobile row, mobile pending actions, and footer — the footer and pending-actions paths look properly guarded against each other, but I need to check whether the collapsed-mobile compact row andshowCollapsedMobilePromptRowcould both be active at once, so I'm looking up how that flag is defined. The collapsed mobile prompt row avoids double-rendering since it only shows when there are no pending inputs, and inline badges correctly appear there since the collapsed state is part of their condition. I'm now checking whether the top drawer and collapsed row could both render when there are pending inputs alongside the collapsed mobile state. Looking at the micro button sizing, it follows the existing touch-target conventions and uses a named primitive size, which is good practice. I'm also noting that ComposerPendingApprovalActions now uses ghost-muted styling across all four actions, including Approve and Decline which previously had distinct primary/destructive styling — this loses some visual hierarchy but seems like an intentional design choice, and the text-foreground override correctly wins over the variant's muted text color via tailwind-merge ordering. Checking the layout nesting for ComposerPendingApprovalPanel confirms the flex containers are compatible, and the test file update removing the BotIcon SVG check is consistent with that icon's removal. Weighing whether to fail the check over the raw-button duplication finding — it's a legitimate concrete issue involving lost focus rings and touch target sizing, so despite this being an otherwise careful PR, I'll report it as a failure rather than let it pass silently. Looking closer, the same component uses the proper Button component for the identical dismiss action in one placement but hand-rolls a raw button in the tab and drawer placements — that's a clear inconsistency introduced by this PR, confirming failure is the right call. I'll draft the inline comment on ComposerTasksBadge.tsx pointing to both the tab dismiss button and the drawer dismiss button, suggesting they be replaced with the standard Button component to match the existing pattern.
1. modify_pr — One consistency finding on the new composer tasks tab/drawer: the "Dismiss tasks for this turn" control is hand-rolled as a raw <button> in the tab and drawer placements while the inline placement in the same component uses Button size="icon-micro" variant="ghost-muted". See the inline comment.
💭 The inline comment didn't attach even though the review body posted. Since the finding needs to be visible, I'll try posting it as an inline review comment through github_pr_modify, which requires the commit_id, path, and line to properly anchor it.
2. modify_pr — POST repos/{owner}/{repo}/pulls/{pull_number}/comments
💭 The inline comment is posted, so I'll finalize this as a failure with one finding.
Agent Credits: 601 credits