feat(web): attach composer state drawers#7150
What changed
- Attaches approval, user-input, plan, sync, slash-command, and stash states directly to the composer surface.
- Preserves real runtime data and the existing approval guards. Pending approvals still disable editing, sending, prompt replacement, and stashing.
- Keeps full approval commands readable and labels session-wide approval explicitly.
Screenshots
Direct parent on the left; this PR on the right. Same viewport and copied application state.
| Before | After |
|---|---|
The composer pass was uncommitted work on the original branch and is independent of tool activity. It belongs in its own UI review.
Review note
- Pending-input stash restore remains hidden because restoration writes the normal composer draft, not the active pending answer. Exposing it there would be a broken affordance.
Validation
- Composer banner, command menu, approval, user-input, stash, and sync-state tests
- Affected-package typechecks
- Changed-file lint and formatting checks
Stack order
- #7153 workspace and navigation
- #7147 Usage
- #7148 Pull Requests
- #7149 terminal
- #7150 composer, this PR
- #7151 tool lifecycle projection
- #7152 tool activity UI
Built with GPT-5.6-sol in the Codex harness.
Note
Medium Risk Large composer layout/CSS refactor and new drawer stacking behavior could cause visual or focus regressions; plan duration derivation changes displayed timing but not server data.
Overview
Reworks the chat composer so runtime state sits in attached βdrawerβ surfaces above the input instead of floating pills and dropdown glass. Task progress from the thread shell appears in a new shoulder tab / expandable list (with per-turn dismiss), alongside stash and top drawers for approvals, pending user input, and plan follow-upβall using shared chat-composer-drawer-* styling and glass-shell attachment when banners or drawers are present.
Slash-command and stash pickers, banner stack, and thread sync status use the same attached drawer treatment; command menu positioning aligns to the main composer surface with drawer inset and overlap. Approval and pending-input UI is compacted (micro buttons, inline command detail, tighter option rows); background-work banner copy is shortened.
Plan activities now get durationMs on completed steps via addPlanStepDurations (handles plan clears, repeated step labels, completion-order fallbacks). Provider skills move presentation helpers to @t3tools/client-runtime/providerSkills with resolveProviderSkillSourceKind and source-specific icons on web and mobile. Adds Button micro size and inline chip geometry tweaks.
Reviewed by Cursor Bugbot for commit 0fbf449b86a430d9b4d99a954a807ade37329e08. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Attach composer state drawers and add task progress badge/drawer
- Converts composer popovers (command menu, stash menu, banner stack) into attached drawer surfaces that anchor to the composer's main surface, with new CSS classes and data attributes for glass backgrounds, outlines, and shadows
- Adds
ComposerTasksBadgeandComposerTasksDrawerto show per-turn task progress inline and in an expandable drawer;ChatComposerwires these via newactiveTasksProgressandactiveTaskStepsprops fromChatView",
Macroscope summarized 0fbf449.
Checks
- SuccessfulCodeRabbitGitHub
Review skipped: automatic reviews are disabled
- Successful
Skipped - Not affected
- SkippedEAS PreviewGitHub
- Skipped
- SuccessfulLabel PR sizeGitHub
- NeutralCursor BugbotGitHub
- SuccessfulLabel PR 7150GitHub
- Successful
- Successful
- SkippedDeploy web previewGitHub
- Successful
- SuccessfulTestGitHub
- SuccessfulRelease SmokeGitHub
- SuccessfulCheckGitHub
- SuccessfulNative fingerprint diffGitHub
- SuccessfulCollect PR targetsGitHub
- SuccessfulPrepare PR size configGitHub
- Neutral
- Skipped[code]smithGitHub
- Successful
UI Consistency
Five findings, all introduced by this PR's composer/drawer refresh. The largest are a responsive geometry override that silently loses at sm and up, and px constants in ComposerCommandMenuLayer that duplicate rem-based drawer tokens.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -4457,19 +4457,21 @@ function ChatViewContent(props: ChatViewProps) { | |
| 4465 | 4465 | size="xs" | |
| 4466 | - | variant="outline" | |
| 4466 | + | variant="ghost-muted" | |
| 4467 | + | className="h-5 px-1.5 text-[11px]" | |
components/ChatView.tsx:4467
Same override as ComposerPendingApprovalActions: size="xs" keeps sm:h-6/sm:text-xs through twMerge, so this banner action is 24px/12px at sm and up rather than the 20px/11px intended here. Suggest switching to the shared micro button size once it exists instead of re-specifying height, padding, and font size per call site.
No longer relevant as of deb67121f12bddcbe8e8a6e48a776981b513fca7
| β¦ | β¦ | @@ -8,7 +8,8 @@ export function ThreadSyncStatusPill({ phase }: { readonly phase: ThreadSyncPhas | |
| 9 | 9 | <div | |
| 10 | 10 | aria-label={label} | |
| 11 | - | className="pointer-events-none mx-auto mb-2 flex w-fit max-w-full items-center gap-2 rounded-full border border-border/60 bg-card/95 px-3 py-1.5 text-foreground text-xs font-medium shadow-sm" | |
| 11 | + | className="chat-composer-drawer-surface chat-composer-drawer-attached pointer-events-none mx-auto mb-[calc(-1rem-1px)] flex w-[calc(100%-2.75rem)] max-w-[calc(48rem-2.75rem)] items-center gap-2 px-3 pt-2 pb-6 text-foreground text-xs font-medium" | |
chat-composer-drawer-attached has no definition anywhere: index.css only styles .chat-composer-drawer-surface / .chat-composer-top-drawer, and no :has(), query selector, or theme rule references it. It is emitted at four call sites and asserted in four tests, so the tests currently lock in a no-op string.
Either drop it (and the assertions), or give it the attachment geometry it appears to be named for β the w-[calc(100%-2.75rem)], mb-[calc(-1rem-1px)], pb-6 trio duplicated here and in ComposerBannerStack, which .chat-composer-top-drawer already owns in CSS. One owner for the seam geometry would keep the drawers from drifting apart.
Posted via Macroscope β UI Consistency
Fixed. The attached drawer class now owns the shared attachment geometry in index.css.
| β¦ | β¦ | @@ -2684,82 +2694,49 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| 2710 | + | <ComposerPendingApprovalActions | |
| 2711 | + | requestId={activePendingApproval.requestId} | |
| 2712 | + | isResponding={respondingRequestIds.includes(activePendingApproval.requestId)} | |
| 2713 | + | showCancel={false} | |
chat/ChatComposer.tsx:2713
With the footer now returning null whenever isComposerApprovalState is true, this showCancel={false} leaves the desktop approval state with no way to abort the turn: the previous footer row had "Cancel turn", and the stop button in ComposerFooterPrimaryActions is no longer rendered either (there is no keyboard interrupt binding). Decline only rejects the single tool call. Suggest keeping the cancel action in this compact row (it already fits the mobile branch below) unless the interrupt path is intentionally moving elsewhere.
No longer relevant as of fc5c94b7ca26c3defda146fbb7bbe61d5db79e00
5b378afOpen thread| β¦ | β¦ | @@ -14,41 +15,48 @@ interface ComposerPendingApprovalActionsProps { | |
| 24 | + | <Button | |
| 25 | + | size="xs" | |
| 26 | + | variant="ghost-muted" | |
| 27 | + | className="h-5 px-1.5 text-[11px] font-normal" | |
chat/ComposerPendingApprovalActions.tsx:27
size="xs" also emits sm:h-6 and sm:text-xs, and twMerge only drops the unprefixed h-7/text-sm from the same groups β the sm:-prefixed ones survive. So at sm and up these buttons render 24px tall with 12px text instead of the intended 20px/11px, which is exactly the viewport where the new desktop approval drawer row is shown.
The same h-5 px-1.5 text-[11px] geometry override is now repeated four times in this file and once in ChatView.tsx, and it also overrides the variant's base and hover text colors (text-foreground, text-destructive-foreground, whose hover override races ghost-muted's own [:hover,[data-pressed]]:text-foreground in emitted-CSS order rather than being deduped).
Consider putting this durable geometry in components/ui/button.tsx as a named size next to icon-micro (e.g. micro: "h-5 gap-1 rounded-md px-1.5 text-[11px] font-normal"), plus a ghost destructive tone for Decline, and keep only layout/context classes at the call sites. That also removes the sm: leak.
No longer relevant as of deb67121f12bddcbe8e8a6e48a776981b513fca7
| β¦ | β¦ | @@ -142,12 +142,20 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: | |
| 150 | + | const drawerInset = 22; | |
| 151 | + | // One extra pixel prevents fractional layout coordinates from exposing | |
| 152 | + | // the canvas between the drawer mask and the composer's foreground edge. | |
| 153 | + | const composerOverlap = 17; | |
chat/ChatComposer.tsx:153
These two constants hardcode in px what index.css expresses in rem: the drawer surfaces are width: calc(100% - 2.75rem) (1.375rem per side) and overlap by --chat-composer-attachment-overlap: calc(1rem + 1px). appearanceFonts.ts sets root.style.fontSize from the interface font-size preference, so at any non-default size the portaled command/stash drawer no longer matches the width or the seam of the attached drawers (the strip rule right below has a comment about deliberately staying in rem for this reason). Suggest deriving both from the root font size:
| 1 | const rootFontSizePx = |
| 2 | Number.parseFloat(window.getComputedStyle(document.documentElement).fontSize) || 16; |
| 3 | // Mirrors the drawer surfaces' `calc(100% - 2.75rem)` inset in index.css. |
| 4 | const drawerInset = 1.375 * rootFontSizePx; |
| 5 | // One extra pixel prevents fractional layout coordinates from exposing |
| 6 | // the canvas between the drawer mask and the composer's foreground edge. |
| 7 | // Mirrors --chat-composer-attachment-overlap: calc(1rem + 1px). |
| 8 | const composerOverlap = rootFontSizePx + 1; |
No longer relevant as of deb67121f12bddcbe8e8a6e48a776981b513fca7
π‘ Medium
File-tree mentions are not inserted when dropped over the top drawer in plan follow-up state, because the drawer is outside the element that receives composerMentionDragHandlers. The drawer still appears to be part of the composer, so attach the drag handlers to a wrapper that contains both the drawer and main surface.
β Resolved in deb67121f12bddcbe8e8a6e48a776981b513fca7
| β¦ | β¦ | @@ -25,29 +24,29 @@ export const ComposerStashBadge = memo(function ComposerStashBadge(props: { | |
| 26 | 25 | data-prompt-stash-badge="true" | |
| 27 | 26 | aria-label={`Stashed prompts: ${props.count}. Open stash.`} | |
| 28 | - | aria-expanded={props.menuOpen} | |
| 27 | + | aria-expanded="false" | |
π‘ Medium chat/ComposerStashBadge.tsx:27
aria-expanded="false" remains false after onToggleMenu opens the stash menu, so assistive technologies report the visible popup as collapsed. Pass the actual isStashMenuOpen state through ComposerStashBadge (replacing the removed menuOpen prop) and use it for aria-expanded.
β Resolved in 5cc14855f9c5202274881bc5315e8e6b82120513
UI consistency review of the composer drawer refactor. Three new findings on this revision (accessibility state on the stash tab, coarse-pointer reachability of the stash delete action, and an inert data-variant hook). Earlier findings on the size="xs" + h-5 px-1.5 text-[11px] overrides, the hardcoded px drawer geometry, and the undefined chat-composer-drawer-attached class are still open in the existing threads and are not repeated here. Thanks for restoring Cancel in the desktop approval row and for keeping the stash tab mounted while its drawer is open.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -25,29 +24,29 @@ export const ComposerStashBadge = memo(function ComposerStashBadge(props: { | |
| 26 | 25 | data-prompt-stash-badge="true" | |
| 27 | 26 | aria-label={`Stashed prompts: ${props.count}. Open stash.`} | |
| 28 | - | aria-expanded={props.menuOpen} | |
| 27 | + | aria-expanded="false" | |
Stash trigger ignores open state
Low Severity
The stash tab hardcodes aria-expanded to false after dropping the menuOpen prop. When the stash menu is open, assistive technology still reports the control as collapsed, so the disclosure state is wrong.
Reviewed by Cursor Bugbot for commit fc5c94b7ca26c3defda146fbb7bbe61d5db79e00. Configure here.
| β¦ | β¦ | @@ -2684,82 +2694,48 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| 2712 | - | ) { | |
| 2713 | - | return; | |
| 2714 | - | } | |
| 2715 | - | if (composerBlurFrameRef.current !== null) { | |
Approval actions overflow expanded composer
Medium Severity
The expanded approval row puts the full command and all four actions in one non-wrapping flex line inside the narrower top drawer. Focusing the composer on a small screen leaves the stacked, wrapping mobile layout and uses this row instead, so Always allow this session and the other actions can overflow or crowd the command out of reach.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f578cd181ba69e9b184700af236590cf18ab4229. Configure here.
Three new findings on the attached-drawer refresh (all in apps/web/src). Findings from my earlier runs on this branch (micro-button geometry overrides, the px drawer inset/overlap constants, the undefined chat-composer-drawer-attached class, aria-expanded="false", the pointer-events-none stash delete button, and the inert data-variant) still apply to the rebased head; I have not re-posted them here.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -2684,82 +2694,48 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| 2747 | - | key={activeProposedPlan.id} | |
| 2748 | - | planTitle={proposedPlanTitle(activeProposedPlan.planMarkdown) ?? null} | |
| 2703 | + | {!isComposerCollapsedMobile && activePendingApproval ? ( | |
| 2704 | + | <div className="flex min-w-0 items-center gap-1 px-3 py-1.5"> | |
chat/ChatComposer.tsx:2704
The approval actions used to sit in their own flex flex-wrap footer row; here they are a shrink-0 sibling on a single line, and .chat-composer-top-drawer has no overflow clip. The four buttons now min out around 310px (Always allow this session alone is ~145px at text-[11px]), while the drawer is only composer width - 2.75rem wide, so once the composer narrows (right panel plus terminal open β the footer's own compact breakpoint is 620px) the buttons spill past the drawer's rounded edge instead of wrapping. Adding flex-wrap lets the button group drop to a second line once the command column is squeezed out.
| 1 | <div className="flex min-w-0 flex-wrap items-center gap-1 px-3 py-1.5"> |
No longer relevant as of deb67121f12bddcbe8e8a6e48a776981b513fca7
| β¦ | β¦ | @@ -234,22 +151,11 @@ const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: { | |
| 241 | - | <span className="inline-flex size-4 shrink-0 items-center justify-center text-icon-muted"> | |
| 242 | - | <SkillGlyph className="size-3.5" /> | |
| 154 | + | <span className="flex min-w-0 flex-1 items-baseline gap-3"> | |
| 155 | + | <span className="shrink-0 font-sans text-xs font-medium text-foreground"> | |
chat/ComposerCommandMenu.tsx:155
Hardcoding text-foreground on the label breaks the highlighted row's color pairing: the row sets bg-accent! text-accent-foreground! (line 135), but a child's own color declaration wins over the inherited one, so the label keeps --foreground on an --accent background. --accent-foreground is contrast-solved against --accent per theme (themePalette.ts readableThemeForeground / updateThemeColorFamily("accentSurface") can return pure white/black while text stays the other polarity), so a custom palette can render this label near-unreadable when highlighted. The description span next to it already carries its own muted tone, so the label can simply inherit.
| 1 | <span className="shrink-0 font-sans text-xs font-medium"> |
No longer relevant as of deb67121f12bddcbe8e8a6e48a776981b513fca7
Three new findings on the attached-drawer refresh (all in apps/web/src). Findings from my earlier runs on this branch (micro-button geometry overrides, the px drawer inset/overlap constants, the undefined chat-composer-drawer-attached class, aria-expanded="false", the pointer-events-none stash delete button, and the inert data-variant) still apply to the rebased head; I have not re-posted them here.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -150,20 +134,33 @@ export const ComposerStashMenu = memo(function ComposerStashMenu(props: { | |
| 157 | 154 | variant="ghost" | |
| 158 | 155 | size="icon-xs" | |
| 159 | - | className="shrink-0 opacity-0 transition-opacity group-hover/stash:opacity-100" | |
| 156 | + | className="pointer-events-none absolute top-1/2 right-2 z-10 -translate-y-1/2 bg-popover! text-foreground! opacity-0 shadow-none! transition-opacity hover:bg-popover! hover:text-foreground! group-hover/stash:pointer-events-auto group-hover/stash:opacity-100 group-focus-within/stash:pointer-events-auto group-focus-within/stash:opacity-100 focus-visible:pointer-events-auto focus-visible:bg-popover! focus-visible:text-foreground! focus-visible:opacity-100" | |
Stash delete blocked on touch
Medium Severity
The stash delete control stays pointer-events-none until hover or focus-within. Row onMouseDown calls preventDefault, so a tap never focuses the row or produces hover. On touch, the hit target stays the restore row, so users cannot delete a stashed prompt from the drawer.
Reviewed by Cursor Bugbot for commit 015199d800d3d2c7e7f7371a3c60a4048b1a231d. Configure here.
Reviewed the composer drawer refactor for shared-primitive and Tailwind/CSS ownership consistency. Three findings, all on changed lines. Previously flagged items (inert data-variant, missing micro size, aria-expanded, coarse-pointer delete action, drawer stacking with the sync pill, hardcoded px in the portal layer) are resolved in this head.
Posted via Macroscope β UI Consistency
Reviewed the composer drawer refactor for shared-primitive and Tailwind/CSS ownership consistency. Three findings, all on changed lines. Previously flagged items (inert data-variant, missing micro size, aria-expanded, coarse-pointer delete action, drawer stacking with the sync pill, hardcoded px in the portal layer) are resolved in this head.
Posted via Macroscope β UI Consistency
π‘ Medium src/index.css:1446
Warning chat-composer-top-drawer[data-variant="warning"] elements always receive --chat-composer-attached-outline from this theme rule, so their warning-colored border is replaced with the ordinary themed outline whenever data-theme-id is present. Exclude the warning variant from this selector (or explicitly override it) so warning drawers retain their warning border.
No longer relevant as of c2a57120390624a31b144abf77928ed0f450df1b
One new finding on the attached-drawer geometry; the previously reported chat-composer-drawer-attached padding no-op, the lost banner severity tone, and the hover:text-destructive-foreground conflict still stand.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -680,7 +680,128 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | |
| 744 | + | width: calc(100% - 2.75rem); | |
| 745 | + | margin-inline: auto; | |
| 746 | + | margin-bottom: calc(-1 * var(--chat-composer-attachment-overlap)); | |
| 747 | + | } | |
src/index.css:747
--chat-composer-attachment-overlap is only declared on .chat-composer-drawer-surface / .chat-composer-top-drawer (line 712) and on .chat-composer-glass-shell (line 804). ComposerBannerStack puts chat-composer-drawer-slot on its wrapper (ComposerBannerStack.tsx:109) while the surface class lives on the child Alert, and ChatView renders that wrapper as a sibling above .chat-composer-glass-shell (ChatView.tsx:6402-6413, shell at 6428) β so the variable is out of scope there. Custom properties inherit downward only, so calc(-1 * var(--chat-composer-attachment-overlap)) is invalid at computed-value time and margin-bottom resets to 0.
Result: the attached banner never pulls onto the composer, so its masked bottom 1rem + 1px band stays open over the canvas instead of being covered by the composer surface β an attached banner shows a transparent seam gap rather than joining the composer (ThreadSyncStatusPill is unaffected because it carries the surface class on the same element).
Suggest declaring the overlap on the slot itself so the value is defined wherever the geometry is used:
| 1 | .chat-composer-drawer-slot, |
| 2 | .chat-composer-top-drawer { |
| 3 | --chat-composer-attachment-overlap: calc(1rem + 1px); |
| 4 | width: calc(100% - 2.75rem); |
| 5 | margin-inline: auto; |
| 6 | margin-bottom: calc(-1 * var(--chat-composer-attachment-overlap)); |
| 7 | } |
No longer relevant as of c2a57120390624a31b144abf77928ed0f450df1b
Reviewed the composer drawer refactor for shared-primitive and CSS-ownership consistency. The earlier findings (micro button size, menuOpen/aria-expanded, coarse-pointer delete target, rem-based drawer geometry, chat-composer-drawer-attached now having a definition, single attached drawer above the composer) all look resolved. Three issues remain, all tied to the new attachment geometry.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -4457,19 +4457,20 @@ function ChatViewContent(props: ChatViewProps) { | |
| 4470 | 4470 | {isStoppingBackgroundWork ? "Stopping..." : "Stop"} | |
| 4471 | 4471 | </Button> | |
| 4472 | 4472 | ), | |
| 4473 | + | className: "px-3 pt-1.5 pb-[1.375rem] text-xs", | |
components/ChatView.tsx:4473
pb-[1.375rem] compensates for the attached drawer's masked seam, but this item class is applied in both attachment modes. ComposerBannerStack is now rendered with attached={false} whenever the thread-sync pill is showing (lines 6403/6410), and in that branch the same class lands on the alert-glass rounded-[22px] floating pill, leaving it with ~6px top and ~22px bottom padding.
Suggest keeping the seam compensation in ComposerBannerStack's attached branch (next to px-3 pt-2) and passing only symmetric padding from the call site.
No longer relevant as of d070c59252c99f7ed54e42f5ee4ea7c81fa27bcb
| β¦ | β¦ | @@ -2828,269 +2809,432 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| 2812 | + | </div> | |
| 2813 | + | ) : null} | |
| 2814 | + | <div className="relative"> | |
| 2815 | + | {!showComposerTopDrawer && !isComposerCollapsedMobile ? ( | |
The stash tab is suppressed for the composer's own top drawer, but not for the drawers that now attach from outside it, and it is 28px tall above the composer instead of the old 12px pill overhang:
ComposerBannerStack/ThreadSyncStatusPillusechat-composer-drawer-slot, whose negative margin ends their opaque area flush with the composer's top edge, so the tab's full-top-7 h-8overhang paints over the banner row (right side, where its action button sits) rather than over the old 8px gap.- Opening the stash drawer covers its own toggle:
ComposerCommandMenuLayerplaces the portal atbottom = rect.top + overlapand inset1.375remper side, so thepointer-events-autolayer spans the entire tab except a ~6px sliver past the drawer's right edge. Clicking the tab to collapse it no longer works even though it still reportsaria-expanded="true".
Suggest plumbing the same "a drawer is already attached above" signal ChatView computes for attached into this component and suppressing the tab in that case, and lifting the tab above the drawer layer (or shortening its overhang) so it stays clickable while the stash drawer is open.
Posted via Macroscope β UI Consistency
Fixed. ChatComposer now receives the real external attachment state and hides the stash tab for both banner and sync drawers.
| β¦ | β¦ | @@ -680,7 +680,128 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | |
| 795 | + | z-index: 0; | |
| 796 | + | } | |
| 797 | + | ||
| 798 | + | .chat-composer-top-drawer[data-variant="warning"] { | |
src/index.css:798
The warning outline is dropped under every custom theme. This rule is inside @layer components, while the theme bridge at the bottom of the file (html[data-theme-id] :is(.chat-composer-drawer-surface, .chat-composer-top-drawer), ~line 1446) is unlayered, so it wins over any layered rule regardless of specificity and resets --chat-composer-attached-outline to --app-theme-toolbar-border. With a theme active the pending-approval drawer keeps its 8% tint (the theme rule does not set --chat-composer-attached-tint) but renders the same neutral outline as the info drawer.
Suggest either exempting the variant in the theme rule (html[data-theme-id] :is(...):not([data-variant="warning"])) or routing the warning edge through a separate variable the theme bridge does not assign, so the state override survives the theme cascade.
No longer relevant as of c2a57120390624a31b144abf77928ed0f450df1b
Reviewed the composer drawer refactor for shared-primitive and CSS-ownership consistency. The earlier findings (micro button size, menuOpen/aria-expanded, coarse-pointer delete target, rem-based drawer geometry, chat-composer-drawer-attached now having a definition, single attached drawer above the composer) all look resolved. Three issues remain, all tied to the new attachment geometry.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -680,7 +680,128 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | |
| 795 | + | z-index: 0; | |
| 796 | + | } | |
| 797 | + | ||
| 798 | + | .chat-composer-top-drawer[data-variant="warning"] { | |
src/index.css:798
The warning outline is dropped under every custom theme. This rule is inside @layer components, while the theme bridge at the bottom of the file (html[data-theme-id] :is(.chat-composer-drawer-surface, .chat-composer-top-drawer), ~line 1446) is unlayered, so it wins over any layered rule regardless of specificity and resets --chat-composer-attached-outline to --app-theme-toolbar-border. With a theme active the pending-approval drawer keeps its 8% tint (the theme rule does not set --chat-composer-attached-tint) but renders the same neutral outline as the info drawer.
Suggest either exempting the variant in the theme rule (html[data-theme-id] :is(...):not([data-variant="warning"])) or routing the warning edge through a separate variable the theme bridge does not assign, so the state override survives the theme cascade.
No longer relevant as of c2a57120390624a31b144abf77928ed0f450df1b
| β¦ | β¦ | @@ -2828,269 +2809,432 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| 2812 | + | </div> | |
| 2813 | + | ) : null} | |
| 2814 | + | <div className="relative"> | |
| 2815 | + | {!showComposerTopDrawer && !isComposerCollapsedMobile ? ( | |
The stash tab is suppressed for the composer's own top drawer, but not for the drawers that now attach from outside it, and it is 28px tall above the composer instead of the old 12px pill overhang:
ComposerBannerStack/ThreadSyncStatusPillusechat-composer-drawer-slot, whose negative margin ends their opaque area flush with the composer's top edge, so the tab's full-top-7 h-8overhang paints over the banner row (right side, where its action button sits) rather than over the old 8px gap.- Opening the stash drawer covers its own toggle:
ComposerCommandMenuLayerplaces the portal atbottom = rect.top + overlapand inset1.375remper side, so thepointer-events-autolayer spans the entire tab except a ~6px sliver past the drawer's right edge. Clicking the tab to collapse it no longer works even though it still reportsaria-expanded="true".
Suggest plumbing the same "a drawer is already attached above" signal ChatView computes for attached into this component and suppressing the tab in that case, and lifting the tab above the drawer layer (or shortening its overhang) so it stays clickable while the stash drawer is open.
Posted via Macroscope β UI Consistency
Fixed with the explicit external drawer attachment state.
| β¦ | β¦ | @@ -4457,19 +4457,20 @@ function ChatViewContent(props: ChatViewProps) { | |
| 4470 | 4470 | {isStoppingBackgroundWork ? "Stopping..." : "Stop"} | |
| 4471 | 4471 | </Button> | |
| 4472 | 4472 | ), | |
| 4473 | + | className: "px-3 pt-1.5 pb-[1.375rem] text-xs", | |
components/ChatView.tsx:4473
pb-[1.375rem] compensates for the attached drawer's masked seam, but this item class is applied in both attachment modes. ComposerBannerStack is now rendered with attached={false} whenever the thread-sync pill is showing (lines 6403/6410), and in that branch the same class lands on the alert-glass rounded-[22px] floating pill, leaving it with ~6px top and ~22px bottom padding.
Suggest keeping the seam compensation in ComposerBannerStack's attached branch (next to px-3 pt-2) and passing only symmetric padding from the call site.
No longer relevant as of c2a57120390624a31b144abf77928ed0f450df1b
One new finding: the split-glass rule relies on a layered background on an element that can carry an unlayered background: transparent, so the composer surface goes see-through in the ultrathink state. Previously reported items (theme warning outline, banner seam padding, approval button sizing/hover, drawer width/overlap constants, aria-expanded, coarse-pointer delete target, dual attached drawers) look addressed in this revision.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -775,7 +912,63 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | |
| 934 | + | ); | |
| 935 | + | -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); | |
| 936 | + | backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); | |
| 937 | + | box-shadow: 0 12px 28px -18px rgb(0 0 0 / 40%); | |
src/index.css:937
This is now the only thing painting the composer fill when a top drawer or stash tab is present (the shell ::before and host ::after are display: none just above), but it is inside @layer components while .ultrathink-frame { background: transparent } (~line 2149) is unlayered β unlayered author rules beat every layer regardless of specificity.
composerFrameClassName puts ultrathink-frame on this exact [data-chat-composer-main-surface="true"] element (composerProviderState.tsx:75), so with an Ultrathink: prompt plus a pending-approval/plan drawer or a stash tab the composer loses its card fill entirely and only keeps the backdrop-filter, leaving the timeline blurrily visible through the composer body in both light and dark.
Smallest fixes: paint the split fill on the inner [data-chat-composer-surface="true"] element (which never receives composerFrameClassName), or drop the redundant background: transparent from .ultrathink-frame so the layered fill can apply. Note ::before on this element is already owned by .ultrathink-frame::before, so the fill cannot move to that pseudo-element either.
No longer relevant as of 5cc14855f9c5202274881bc5315e8e6b82120513
| β¦ | β¦ | @@ -1,48 +1,44 @@ | |
| 34 | + | data-approval-detail="complete" | |
| 35 | + | > | |
| 36 | + | {approval.detail ?? fallbackLabel} | |
| 37 | + | </code> | |
Approval detail grows without cap
Low Severity
The compact approval row replaced the old max-h-40 scrollable detail block with whitespace-pre and only horizontal overflow. Multiline command details can expand the top drawer without a vertical bound and push the composer down the viewport.
Reviewed by Cursor Bugbot for commit 5cc14855f9c5202274881bc5315e8e6b82120513. Configure here.
| β¦ | β¦ | @@ -2828,269 +2810,432 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| 2821 | + | pulsing={stashPulse.active} | |
| 2822 | + | onToggleMenu={toggleStashMenu} | |
| 2823 | + | /> | |
| 2824 | + | ) : null} | |
Stash badge hidden during plan
Medium Severity
ComposerStashBadge is gated on !showComposerTopDrawer, and plan follow-up now opens that drawer. Unlike approval or pending user input, plan state still allows editing and βS stashing, but the badge (the only normal way to open and restore the stash) disappears for the whole plan-ready period.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 5cc14855f9c5202274881bc5315e8e6b82120513. Configure here.
| β¦ | β¦ | @@ -150,20 +134,33 @@ export const ComposerStashMenu = memo(function ComposerStashMenu(props: { | |
| 164 | 161 | }} | |
| 165 | 162 | > | |
| 166 | - | <XIcon /> | |
| 163 | + | <XIcon className="size-3.5 stroke-2" /> | |
Delete covers stash thumbnails
Low Severity
The delete control is absolutely positioned at the trailing edge and always visible on small screens. On entries with attachments, that overlay sits on top of the image thumbnails, so taps near the right edge hit delete instead of restore and the previews are hard to see.
Reviewed by Cursor Bugbot for commit 5cc14855f9c5202274881bc5315e8e6b82120513. Configure here.
One finding: the single-attachment invariant that was fixed for ThreadSyncStatusPill is still unenforced against the composer's own top drawer, so an attached banner can seam onto the drawer instead of the composer. Everything else in this revision looks consistent β the micro size now overrides the base sm:text-sm, the data-variant tint/outline mapping and the themed-outline scoping (:is(:not([data-variant]), [data-variant="default"])) line up, the drawer seam padding is expressed as a utility so it survives Tailwind's layer order, and dropping background: transparent from .ultrathink-frame lets the layered [data-chat-composer-main-surface="true"] fill paint.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -6398,10 +6405,18 @@ function ChatViewContent(props: ChatViewProps) { | |
| 6416 | + | attached={composerBannerAttached} | |
| 6417 | + | className="relative z-0" | |
| 6418 | + | items={composerBannerItems} | |
| 6419 | + | /> | |
Sync drawer stacks on top drawer
Medium Severity
composerBannerAttached already treats sync and composer-owned top state as competing for the same attachment seam, but ThreadSyncStatusPill still renders whenever sync is active. During syncing with a pending approval, user-input, or plan drawer, both the sync pill and chat-composer-top-drawer apply chat-composer-drawer-slot overlap, so two attached drawers stack on the composer and fight for the same inset seam.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit d070c59252c99f7ed54e42f5ee4ea7c81fa27bcb. Configure here.
Reviewed the composer drawer refactor against the shared primitive/CSS ownership rules. Earlier findings (micro button sm: leak, undefined chat-composer-drawer-attached, theme bridge overriding variant outlines, ultrathink-frame beating the split glass fill, drawer geometry hardcoded in px, stash delete unreachable on coarse pointers, missing cancel in the approval row) all look addressed. Two remaining gaps in the new attachment model are flagged inline.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -2318,6 +2318,12 @@ function ChatViewContent(props: ChatViewProps) { | |
| 2323 | + | pendingUserInputs.length > 0 || | |
| 2324 | + | (showPlanFollowUpPrompt && activeProposedPlan !== null); | |
| 2325 | + | const composerBannerAttached = | |
| 2326 | + | (!threadSyncPhase || activeEnvironmentUnavailable) && !composerOwnsTopAttachment; | |
Plan attachment ownership mismatch
Medium Severity
composerOwnsTopAttachment treats a ready plan as owning the composer seam, but showComposerTopDrawer only mounts the plan drawer when the mobile composer is expanded. On a collapsed mobile plan-ready thread, banners and the sync pill detach even though no top drawer is present, so status chrome floats with the wrong attached layout.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 6202e181afdd31d428d033539690798ec19df044. Configure here.
Reviewed the composer drawer refactor against the shared primitives, Tailwind/CSS ownership rules, and theme handling. Most of the earlier findings are resolved in this revision (drawer variant tokens now drive --chat-composer-attached-*, the seam padding is a Tailwind utility on the owner, the stash badge is reachable while a pending-input/plan drawer is up, aria-expanded is wired again, the portaled drawer geometry is derived from the root font size, and the approval row wraps and keeps a cancel action).
Two issues remain, both about a declaration that does not reach the element it targets:
apps/web/src/index.cssβ the new context-strip backdrop rule outranks the strip's dark treatment, so dark mode loses the seam shading whenever a stash tab or top drawer is present.apps/web/src/components/chat/ComposerStashMenu.tsxβ thetext-foreground!overrides on the delete button do not affect the icon, which is what is actually visible.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -1143,13 +1159,17 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| 1152 | 1168 | ||
| 1169 | + | useEffect(() => { | |
| 1170 | + | onTopDrawerVisibilityChange?.(showComposerTopDrawer); | |
| 1171 | + | }, [onTopDrawerVisibilityChange, showComposerTopDrawer]); | |
Plan drawer attachment state lags
Medium Severity
Plan follow-up ownership of the composer attachment seam depends on composerTopDrawerVisible, which is updated from ChatComposer in a passive useEffect. Parent ComposerBannerStack / ThreadSyncStatusPill attached flags therefore lag the actual plan drawer by at least one paint, so banners and sync can briefly fight the plan drawer for the seam (or stay detached after the drawer collapses).
Additional Locations (2)
Reviewed by Cursor Bugbot for commit f713405400c94e8e5e4258e0a60e6b8ba6bc2c3b. Configure here.
Reviewed the composer drawer/attachment refactor for shared-primitive and Tailwind/CSS ownership consistency. The earlier cascade issues I raised (theme bridge vs. data-variant outlines, the context strip's dark seam under the split-glass path, the chat-composer-drawer-attached seam padding, ghost-muted hover conflict, the unlayered .ultrathink-frame background, the stash icon color indirection, and the single-attached-surface gating via resolveComposerOwnsTopAttachment) all look resolved in this head.
Two remaining findings, both introduced by changed lines:
apps/web/src/components/chat/ChatComposer.tsxβ the stash tab is only suppressed for the composer's own top drawer, so it still paints over the drawers that attach fromChatView, covering the banner's action/dismiss buttons.apps/web/src/components/chat/ComposerStashMenu.tsxβ the newly absolute delete chip paints an opaque--popoversurface over a row that is--accentwhile hovered, and permanently hides the timestamp underneath.
Posted via Macroscope β UI Consistency
Two new dark-mode rules put @variant dark inside a ::before rule, which emits an invalid selector (pseudo-element not last) and silently drops the dark treatment. Everywhere else in this file the convention is the reverse β @variant dark on the element with &::before nested inside.
Posted via Macroscope β UI Consistency
One finding on the detached branch of ThreadSyncStatusPill. Everything else in the composer/drawer refresh (attachment ownership gating, the split-glass :has() path, @variant dark nesting around ::before, theme-bridge scoping for data-variant, and the micro button geometry) looks consistent with the shared primitives and with the run-context strip it mirrors.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -1,14 +1,27 @@ | |
| 19 | + | "pointer-events-none flex max-w-[calc(48rem-2.75rem)] items-center gap-2 px-3 text-foreground text-xs font-medium", | |
| 20 | + | attached | |
| 21 | + | ? "chat-composer-drawer-surface chat-composer-drawer-attached chat-composer-drawer-slot pt-2" | |
| 22 | + | : "alert-glass mx-auto mb-2 w-[calc(100%-2.75rem)] rounded-[22px] py-2", | |
chat/ThreadSyncStatusPill.tsx:22
The detached pill loses its edge: alert-glass only supplies a translucent --background fill plus a backdrop blur, and this branch drops the previous border border-border/60 and shadow-sm with no replacement. Over the chat canvas (also --background) the bar ends up with no visible boundary, and it is inconsistent with the sibling detached surface β ComposerBannerStack's detached branch is an Alert, whose base border (colored by the * { border-border } base rule) keeps its outline under the same alert-glass rounded-[22px] treatment.
Suggest restoring the border/shadow on the detached branch only; the attached branch draws its own outline through chat-composer-drawer-surface::before.
| 1 | : "alert-glass mx-auto mb-2 w-[calc(100%-2.75rem)] rounded-[22px] border border-border/60 py-2 shadow-sm", |
No longer relevant as of e47d5a4d35b18c2e62e23e26c2f4806da4030b39
One finding on the new stash tab's outline ownership. Everything else from the earlier rounds (drawer seam variables, dark @variant nesting, theme bridge vs. severity tints, detached pill border, stash delete affordance, aria-expanded, micro sizing) reads as resolved at this head.
Posted via Macroscope β UI Consistency
One geometry inconsistency in the new attached-drawer set. Everything else in the composer drawer/tab rework looks consistent with the shared primitives and the cascade layering in index.css.
Posted via Macroscope β UI Consistency
UI Consistency
Two findings on the composer drawer migration. Previously raised items (drawer seam padding, severity tints, @variant dark nesting, theme-bridge outline, micro size, rem-based drawer geometry, approval-row wrapping, stash reachability during pending input, aria-expanded) all look addressed at this head.
ChatComposer.tsxβ moving the top drawer out ofcomposerSurfaceRefleaves the mobile collapse check attached to a subtree that no longer contains the drawer's controls, soisComposerFocusedcan no longer be cleared; thedata-chat-composer-collapsed-controlsguard is also now unreachable.ComposerStashMenu.tsxβ the delete icon is pinned to--foregroundon a row that becomesbg-accent/text-accent-foregroundexactly when the button is revealed.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -1255,6 +1479,26 @@ html[data-theme-id] .chat-composer-glass-shell { | |
| 1490 | + | ) { | |
| 1491 | + | --chat-composer-attached-outline: var(--app-theme-toolbar-border); | |
| 1492 | + | ||
| 1493 | + | @variant dark { | |
π‘ Medium src/index.css:1493
In dark t3-chat, attached default drawers render a generic color-mix(...) outline while the main composer edge renders #241e28, so the seam has mismatched outlines. The dark @variant here overrides the inherited --chat-composer-outline; extend the T3-specific override to attached drawers or derive this value from --chat-composer-outline.
No longer relevant as of 2843d45cb0b2f48f8466f4e520db49e927f17838
Reviewed the composer drawer refactor for shared-primitive use, Tailwind/CSS ownership, emitted-selector validity, and accessibility preservation. The previously reported items (form-level focus/blur capture scope, --control-icon-color on the stash delete action, @variant dark nesting inside ::before, theme-bridge vs data-variant outlines, seam padding, stash-tab suppression while an external drawer is attached, micro size sm: leakage) all look resolved in the current head. One accessibility regression remains in the pending-approval drawer.
Posted via Macroscope β UI Consistency
898187fOpen thread| β¦ | β¦ | @@ -1,48 +1,44 @@ | |
| 31 | + | <code | |
| 32 | + | aria-label={detailAriaLabel} | |
| 33 | + | className="block max-h-20 min-w-0 flex-1 overflow-auto whitespace-pre font-mono text-[11px] text-foreground/85" | |
| 34 | + | data-approval-detail="complete" | |
chat/ComposerPendingApprovalPanel.tsx:34
aria-label on <code> is not exposed: <code> maps to ARIA role code, which prohibits an author-provided name, so assistive tech drops it. That was harmless before because the panel also rendered visible context (PENDING APPROVAL, Command approval requested, and the Command / File to read detail label); with all of those removed, a screen reader now reaches this drawer as a bare command string followed by Cancel / Decline / Always allow this session / Approve, with nothing stating that an approval is pending or which kind it is. The data-variant="warning" tint on the drawer is the only remaining cue, i.e. colour alone. fallbackLabel does not cover this β it only renders when approval.detail is null.
Suggest moving the name onto a container that accepts one (or adding an sr-only span with detailAriaLabel), keeping the compact row visually unchanged:
| 1 | <div |
| 2 | aria-label={detailAriaLabel} |
| 3 | className={cn("flex min-w-0 flex-1 items-center gap-2", className)} |
| 4 | role="group" |
| 5 | > |
| 6 | <code |
| 7 | className="block max-h-20 min-w-0 flex-1 overflow-auto whitespace-pre font-mono text-[11px] text-foreground/85" |
| 8 | data-approval-detail="complete" |
No longer relevant as of 2843d45cb0b2f48f8466f4e520db49e927f17838
Two findings on the new composer attachment seam. Everything flagged in earlier runs (drawer variant tones, @variant dark inside ::before, the ultrathink-frame override, the seam padding, micro's sm: leftovers, the stash delete affordance, aria-expanded) now looks addressed.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -2828,269 +2850,414 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| 2853 | + | </div> | |
| 2854 | + | ) : null} | |
| 2855 | + | <div className="relative"> | |
| 2856 | + | {!showComposerTopDrawer && !topAttachmentOccupied && !isComposerCollapsedMobile ? ( | |
chat/ChatComposer.tsx:2856
Adding !topAttachmentOccupied removes the only visible stash affordance in a fairly common state. embeddedStashBadge is only rendered inside the composer's own top drawer (pending approval / pending input / plan-ready), so when the seam is taken by an external attachment β composerBannerAttached && composerBannerItems.length > 0 or the thread-sync pill β nothing renders the badge at all, and both the click target and the stash count disappear.
That is not a rare state: backgroundLivenessBannerItem (ChatView.tsx:4454) keeps a Monitoring / N agents working banner up for the whole time a thread has background liveness, and woke-thread / branch-mismatch / system banners behave the same way. The only remaining route is βS with an empty composer (stashCurrentPrompt falls through to setIsStashMenuOpen), which is undiscoverable.
Suggest keeping a badge in this case rather than dropping it β e.g. render embeddedStashBadge inside the composer surface (or hand it to the occupying banner as trailing content) when topAttachmentOccupied is true, so the tab is only traded for an inline badge, never for nothing.
No longer relevant as of 2843d45cb0b2f48f8466f4e520db49e927f17838
| β¦ | β¦ | @@ -1255,6 +1479,26 @@ html[data-theme-id] .chat-composer-glass-shell { | |
| 1496 | + | var(--app-theme-input) 30%, | |
| 1497 | + | var(--background) | |
| 1498 | + | ); | |
| 1499 | + | } | |
src/index.css:1499
This bridge hardcodes the outline instead of preferring the composer's own edge token, so per-theme overrides of that edge no longer reach the attached drawers. The layered default is --chat-composer-attached-outline: var(--chat-composer-outline, β¦), which in-shell drawers (.chat-composer-top-drawer) inherit from .chat-composer-glass-shell; this unlayered rule replaces that indirection with a fixed value.
Concretely, under html[data-theme-id="t3-chat"] in dark mode the composer edge is #241e28 (line 1506, explicitly chosen over the derived value), while the drawer attached directly to it resolves to color-mix(in srgb, var(--app-theme-input) 30%, var(--background)) β two different edge colors meeting at the seam that is supposed to read as one surface.
Keeping the indirection with the theme value as the fallback preserves the out-of-shell/portaled drawers (banner stack, sync pill, command and stash menus) that genuinely need it:
| 1 | --chat-composer-attached-outline: var( |
| 2 | --chat-composer-outline, |
| 3 | var(--app-theme-toolbar-border) |
| 4 | ); |
| 5 | |
| 6 | @variant dark { |
| 7 | --chat-composer-attached-outline: var( |
| 8 | --chat-composer-outline, |
| 9 | color-mix(in srgb, var(--app-theme-input) 30%, var(--background)) |
| 10 | ); |
| 11 | } |
No longer relevant as of 2843d45cb0b2f48f8466f4e520db49e927f17838
One finding: the stash affordance disappears entirely in the pending-user-input state. Everything else I re-checked in this revision (attached-drawer dark ::before nesting, the theme bridge's per-variant outline scoping, .ultrathink-frame's unlayered background, the ghost-muted hover pairing in the approval actions, aria-expanded on the stash tab, the coarse-pointer escape hatch on the stash delete button, and the --chat-composer-attachment-overlap scoping on the banner/pill slots) now looks correct.
Posted via Macroscope β UI Consistency
One new finding: the composer surface loses its bottom padding while an approval is pending, now that the footer branch that used to supply it is gone. Details inline.
Previously reported items still open in this head (not re-posted): the stash tab being suppressed whenever the composer owns a top drawer, and multiple surfaces (ComposerBannerStack front item, ThreadSyncStatusPill, the composer's own .chat-composer-top-drawer) all claiming the attached seam at the same time.
Posted via Macroscope β UI Consistency
UI consistency review: one finding β the attached composer-drawer treatment is applied to two surfaces that can render at the same time, so the shared seam contract breaks. Details inline.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -8,7 +8,8 @@ export function ThreadSyncStatusPill({ phase }: { readonly phase: ThreadSyncPhas | |
| 9 | 9 | <div | |
| 10 | 10 | aria-label={label} | |
| 11 | - | className="pointer-events-none mx-auto mb-2 flex w-fit max-w-full items-center gap-2 rounded-full border border-border/60 bg-card/95 px-3 py-1.5 text-foreground text-xs font-medium shadow-sm" | |
| 11 | + | className="chat-composer-drawer-surface chat-composer-drawer-attached chat-composer-drawer-slot pointer-events-none flex max-w-[calc(48rem-2.75rem)] items-center gap-2 px-3 pt-2 pb-[calc(var(--chat-composer-attachment-overlap)_+_0.375rem)] text-foreground text-xs font-medium" | |
This pill and ComposerBannerStack's front item both hardcode the attached-drawer treatment (chat-composer-drawer-surface + chat-composer-drawer-attached + chat-composer-drawer-slot), but that contract assumes exactly one surface joins the composer's top edge. ChatView renders them as ungated consecutive siblings (ChatView.tsx 6401/6404 and 6407), and chat-composer-drawer-slot gives each one margin-bottom: calc(-1 * var(--chat-composer-attachment-overlap)).
So when a banner and a sync phase are live together β e.g. opening a settled or snoozed thread (parkedThreadBannerItem) while cached messages reconcile β the banner pulls this pill into its own masked overlap: the banner's ::before is transparent across those 17px (it expects the composer surface and outline to paint over them) while the pill paints its 16px top corners and 1px top border inside that strip, leaving a notch at the joint and a double edge instead of one continuous drawer. ChatComposer's .chat-composer-top-drawer is a third surface with the same geometry and the same collision.
Suggest deriving the treatment rather than hardcoding it β take an attached prop here (as ComposerBannerStack already does for its stacked items) and let only the surface directly above the composer attach, with the others keeping the detached alert-glass styling.
Posted via Macroscope β UI Consistency
Fixed. Banner, sync, and top-drawer surfaces now use the shared stacked attachment rules without double seams.
ComposerBannerStack now correctly derives attachment via its attached prop, but ThreadSyncStatusPill still hardcodes chat-composer-drawer-surface chat-composer-drawer-attached chat-composer-drawer-slot on line 11 and doesn't accept an attached prop. Since both can render simultaneously (ChatView 6437β6444), the double-seam issue persists when a banner and sync phase overlap.
Two consistency findings on new controls introduced by this PR. Everything else in the shared-primitive migration (Button micro/icon-micro, Toggle/ToggleGroup segmented, WorkspacePageHeader/WorkspacePageContainer, the attached-drawer CSS and its theme bridge) looks coherent from here.
Posted via Macroscope β UI Consistency
| β¦ | β¦ | @@ -2828,269 +2828,414 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| 2918 | + | onDelete={deleteStashEntry} | |
| 2919 | + | onClose={() => setIsStashMenuOpen(false)} | |
| 2920 | + | /> | |
| 2921 | + | </ComposerCommandMenuLayer> | |
Stash menu survives pending input
Medium Severity
Hiding the stash badge whenever showComposerTopDrawer is true (including pending user input) does not close or gate the stash menu. An already-open menu stays mounted because render only checks !isComposerApprovalState, and with the badge gone there is no click target to dismiss it. Choosing a row still runs restoreStashEntry, which writes the normal composer draft rather than the active pending answerβthe broken affordance this PR aimed to hide.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 1fb883e96bf0c037c34316e0fd240ae68c34f939. Configure here.
One finding: the background-liveness banner item carries attached-drawer styling that is also applied when the same item renders detached in the stack. Everything else in this revision checks out β the data-variant tint mapping, the slot-adjacency rules, the themed-outline bridge, the drawer padding compensation, the [:hover,[data-pressed]] destructive tint, and the form-level focus/blur capture all land where they should.
Posted via Macroscope β UI Consistency
One finding: the background-liveness banner item carries attached-drawer styling that is also applied when the same item renders detached in the stack. Everything else in this revision checks out β the data-variant tint mapping, the slot-adjacency rules, the themed-outline bridge, the drawer padding compensation, the [:hover,[data-pressed]] destructive tint, and the form-level focus/blur capture all land where they should.
Posted via Macroscope β UI Consistency
Two findings in the shared-control layer this PR introduces. Prior findings on PanelLayoutControls (terminal toggle still passes a disabled Toggle as render) and the hand-rolled terminal-tab close button are unchanged at this head and were already commented on, so they are not repeated here.
Posted via Macroscope β UI Consistency
b6a9087Open thread| β¦ | β¦ | @@ -11,6 +11,8 @@ interface ComposerPendingApprovalActionsProps { | |
| 11 | 11 | ) => Promise<unknown>; | |
| 12 | 12 | } | |
| 13 | 13 | ||
| 14 | + | const APPROVAL_ACTION_CLASS_NAME = "font-normal text-[11px] sm:text-[11px]"; | |
This shared constant exists to defeat the micro size's own type scale: micro declares text-xs, but buttonVariants' base string keeps sm:text-sm, and tailwind-merge treats the sm: modifier as a separate group, so from 640px up the button renders 14px text in a 20px box. xs right below it pairs text-sm sm:text-xs for exactly this reason.
The result is the same override duplicated at two call sites in this PR - here and ChatView.tsx:4473 (className="text-[11px] sm:text-[11px]" on the background-work action) - which is the treatment the shared size should own.
Suggest fixing it in the primitive (apps/web/src/components/ui/button.tsx, size.micro) by pairing the small-screen and sm: type scale, then dropping both call-site overrides so the variant keeps ownership of its typography:
| 1 | - micro: |
| 2 | - "h-5 gap-1 rounded-sm px-[calc(--spacing(1.5)-1px)] text-xs before:rounded-[calc(var(--radius-sm)-1px)] [&_svg:not([class*='size-'])]:size-3", |
| 3 | + micro: |
| 4 | + "h-5 gap-1 rounded-sm px-[calc(--spacing(1.5)-1px)] text-[11px] before:rounded-[calc(var(--radius-sm)-1px)] sm:text-[11px] [&_svg:not([class*='size-'])]:size-3", |
Posted via Macroscope β UI Consistency
One new finding in this revision: the stash-menu delete action's reveal is gated on a width breakpoint rather than pointer capability, which removes the only touch affordance for deleting a stashed prompt on coarse-pointer devices wider than 640px. Details inline.
Previously reported items that are still open (no new comments posted): the stash tab's 28px overhang colliding with externally attached drawers (ComposerBannerStack / ThreadSyncStatusPill are still ungated at ChatView.tsx:6407-6413 while the tab is suppressed only for the composer's own drawer), and backgroundLivenessBannerItem's className: "px-3 pt-1.5 text-xs" being applied in both the attached and the detached (alert-glass) banner branches.
Posted via Macroscope β UI Consistency
Two findings in the composer drawer refresh: the stash tab is now fully occluded by the drawer it opens, and the stash-menu delete action becomes unreachable on hover-less pointers at sm and above.
Posted via Macroscope β UI Consistency
The tab now sits entirely inside the area the stash drawer paints over, so opening the stash hides its own toggle.
-top-7 h-8 puts the tab between the composer's top edge β28px and +4px, at z-0 behind the main surface. ComposerCommandMenuLayer portals the drawer to document.body as fixed z-[70] with bottom = innerHeight - surfaceTop - composerOverlap and left/width inset by only 1.375rem per side (ChatComposer.tsx 145-163), so its opaque region ends exactly at the composer's top edge and covers the tab's full 28px overhang. Escape (or the composer.stash shortcut) becomes the only way to collapse the drawer, and menuOpen no longer has any rendered effect β it only feeds aria-expanded, which is now set on an invisible control. The pre-PR pill signalled the open state (border-border text-foreground opacity-100) and was only clipped by ~4px.
Suggest keeping the tab out of the drawer's footprint (e.g. inset the drawer's right edge past the tab, or render the tab inside the drawer layer while it is open) and restoring an open-state treatment so the toggle still reads as expanded:
| 1 | "chat-composer-stash-tab absolute -top-7 right-4 z-0 inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-t-xl border border-b-0 px-3 pb-1 text-xs leading-none", |
| 2 | "transition-[color,border-color] duration-200", |
| 3 | props.menuOpen || props.pulsing ? "text-foreground" : "text-muted-foreground hover:text-foreground", |
Posted via Macroscope β UI Consistency
Fixed. The open stash drawer now has a compact visible Close stash button.
| β¦ | β¦ | @@ -150,20 +134,33 @@ export const ComposerStashMenu = memo(function ComposerStashMenu(props: { | |
| 157 | 154 | variant="ghost" | |
| 158 | 155 | size="icon-xs" | |
| 159 | - | className="shrink-0 opacity-0 transition-opacity group-hover/stash:opacity-100" | |
| 156 | + | className="pointer-events-none shrink-0 [--control-icon-color:currentColor] opacity-0 shadow-none! transition-opacity max-sm:pointer-events-auto max-sm:opacity-100 group-hover/stash:pointer-events-auto group-hover/stash:opacity-100 group-focus-within/stash:pointer-events-auto group-focus-within/stash:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100" | |
chat/ComposerStashMenu.tsx:156
Adding pointer-events-none makes the hover reveal gate interaction, not just opacity, while the touch escape hatch is a viewport query (max-sm:) rather than a pointer query. On a coarse pointer at β₯640px there is no hover, so the button stays pointer-events-none opacity-0; tapping the row runs onRestore and closes the menu, so a stashed entry can no longer be deleted at all. Before this change the button was opacity-0 but still tappable.
This repo gates touch reveals on the pointer rather than width (e.g. ThreadTerminalDrawer.tsx:1759 uses pointer-coarse:hidden / pointer-coarse:block). Suggest swapping the width query for a pointer query:
| 1 | className="pointer-events-none shrink-0 [--control-icon-color:currentColor] opacity-0 shadow-none! transition-opacity pointer-coarse:pointer-events-auto pointer-coarse:opacity-100 group-hover/stash:pointer-events-auto group-hover/stash:opacity-100 group-focus-within/stash:pointer-events-auto group-focus-within/stash:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100" |
No longer relevant as of b034b4120e367b79cc71b1600942b78df4584bca
One follow-up on the composer-drawer refactor. The seam padding moved into ComposerBannerStack's attached branch as suggested, but the banner item still carries call-site padding/type overrides that leak into the detached (stacked) rendering.
Previously reported findings that are still live in this head β not re-commented to avoid duplicate threads:
ChatComposer.tsx(~2834): the stash tab is suppressed only for the composer's ownshowComposerTopDrawer, so it still overlapsComposerBannerStack/ThreadSyncStatusPillwhen those attach from outside the form (-top-7 h-8vs the drawer slot's flush bottom edge), and there is still no way to open the stash while a pending user input is on screen.ComposerStashMenu.tsx(~156): the delete button's reveal is keyed tomax-sm:rather than pointer capability, so hover-less pointers wider than 640px getopacity-0 pointer-events-nonewith no reachable alternative.ComposerPendingApprovalActions.tsx(~14) /ChatView.tsx(~4473):text-[11px] sm:text-[11px]still exists only to defeat themicrosize inheriting the basesm:text-sm; the durable fix belongs inbuttonVariants(microshould pairtext-xs sm:text-xs, asxsdoes).
Posted via Macroscope β UI Consistency
One new finding in the compact pending-approval row; the rest of the drawer/glass work (dark @variant restructuring, theme-bridge outline, .ultrathink-frame fill, seam padding, form-level focus capture) checks out.
Posted via Macroscope β UI Consistency
1d1ff47Open thread| β¦ | β¦ | @@ -1,48 +1,48 @@ | |
| 33 | + | role="group" | |
| 34 | + | > | |
| 35 | + | <code | |
| 36 | + | className="block max-h-20 min-w-0 flex-1 overflow-auto whitespace-pre font-mono text-[11px] text-foreground/85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70" | |
chat/ComposerPendingApprovalPanel.tsx:36
whitespace-pre + overflow-auto makes this a horizontal scroller inside a row that is one 11px line tall, and the app's global scrollbar rule only sets a width (index.css: ::-webkit-scrollbar { width: 6px }), so a horizontal lane falls back to Chromium's ~15px default β taller than the command text above it, and always visible (styling ::-webkit-scrollbar opts every scroller out of overlay scrollbars). The previous panel wrapped instead (whitespace-pre-wrap [overflow-wrap:anywhere] in a max-h-40 box), so the lane is new here; .chat-markdown pre::-webkit-scrollbar { height: 7px } exists for exactly this gap.
Suggest sizing the lane at the owner so the row keeps its compact height:
| 1 | className="block max-h-20 min-w-0 flex-1 overflow-auto whitespace-pre font-mono text-[11px] text-foreground/85 [scrollbar-width:thin] [&::-webkit-scrollbar]:h-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70" |
No longer relevant as of b034b4120e367b79cc71b1600942b78df4584bca
Two new findings on the attached-drawer geometry and token scoping. Earlier findings on this PR that still stand are noted in the check summary rather than re-posted.
Posted via Macroscope β UI Consistency
Reviewed the composer drawer refactor for shared-primitive and Tailwind-ownership consistency. The drawer/glass split, the @variant dark nesting, the attached-seam stacking rule, and the removal of .ultrathink-frame { background: transparent } all look sound. Two rendering regressions on changed lines are noted inline.
Posted via Macroscope β UI Consistency
One finding: the new rule that is supposed to suppress the stash tab when an external drawer already occupies the composer seam can never match the rendered DOM, so the collision it targets still happens. Details inline.
Posted via Macroscope β UI Consistency
Reviewed the composer drawer refactor for shared-primitive use, Tailwind/CSS ownership, and emitted-selector behavior. Most of the drawer plumbing looks coherent (masked seam mirrors the context strip, @variant dark now wraps the pseudo-elements correctly, the attached surfaces resolve theme tokens through --chat-composer-outline with fallbacks that reach portaled drawers). Two findings below, both about the stash tab's new attached-tab behavior.
Posted via Macroscope β UI Consistency
One finding on the new shared segmented toggle size. Everything else in the composer-drawer, terminal-sidebar, pull-request and usage work looked consistent with the primitives.
Posted via Macroscope β UI Consistency
One finding on the new terminal toolbar primitive; the rest of the composer/terminal/pull-request migrations look consistent with the shared component system.
Posted via Macroscope β UI Consistency
Reviewed the composer drawer refactor (ChatComposer, ComposerBannerStack, ComposerCommandMenu, ComposerStashBadge/Menu, ThreadSyncStatusPill, approval/pending-input panels, and the new chat-composer-drawer-* rules in index.css). The previously reported problems in this area β the dark @variant/::before nesting order, the missing hero-branch marker for the stash-tab suppression rule, the <code aria-label> naming, the coarse-pointer delete affordance, the horizontal scrollbar lane, and the attached pill's bottom padding β all look addressed at this head. One small inconsistency left.
Posted via Macroscope β UI Consistency
chat/ChatComposer.tsx:2731
This row is the only body in the top drawer that stops at px-3; every sibling state uses the composer's px-3 sm:px-4 column β the plan banner (ComposerPlanFollowUpBanner, px-3 py-2 sm:px-4), the collapsed approval branch just below (line 2763, px-3 pt-2 sm:px-4), and the pending-input panel (px-1 sm:px-2 header + px-3 sm:px-4 panel). So from 640px up the approval command sits 4px closer to the drawer edge than whatever replaces it, and the content shifts sideways when the drawer switches state.
Suggest matching the shared column:
| 1 | <div className="flex min-w-0 flex-wrap items-center gap-1 px-3 py-1.5 sm:px-4"> |
No longer relevant as of f5bd5efd9a28cf7025073c6c9e76bf24aeec678d
Fixed by matching the drawer content column with sm:px-4.
UI consistency review of the composer drawer refactor. One finding on how the attached-drawer geometry contract is owned; details inline.
Posted via Macroscope β UI Consistency
UI consistency β 1 finding
apps/web/src/index.css: the new attached-drawer glass adds a dark-mode ::before background whose specificity outranks the @supports not (backdrop-filter) opaque fallback, so the fallback is dead in dark mode. Details inline.
Re-checked and now resolved from earlier revisions of this branch: the stash-tab suppression selector matches the draft-hero tree (chat-composer-drawer-host / chat-composer-shell-slot), the tab no longer goes invisible while focused, the approval detail's horizontal lane is styled ([&::-webkit-scrollbar]:h-1.5, [scrollbar-width:thin]), the chip label keeps leading-tight, the stash delete button is gated on pointer-coarse with currentColor icon tone, and the composer focus/blur capture handlers now sit on the <form> so drawer controls stay inside the tracked subtree.
Posted via Macroscope β UI Consistency
UI consistency β 1 finding in the composer top drawer's type scale. Details inline.
Posted via Macroscope β UI Consistency
ef3ffabOpen thread| β¦ | β¦ | @@ -1,23 +1,16 @@ | |
| 16 | - | > | |
| 17 | - | Plan Ready | |
| 18 | - | </Badge> | |
| 11 | + | <span className="text-sm font-medium text-foreground/90">Plan ready</span> | |
chat/ComposerPlanFollowUpBanner.tsx:11
The three states of the new top drawer now disagree on their leading type, and this one inverts its own hierarchy: the constant label is text-sm/text-foreground/90 while the plan title beside it dropped from text-sm font-medium to text-xs text-foreground/85, so the fixed word is now larger and higher-contrast than the content it describes.
Across the drawer the same slot leads with text-xs font-medium text-muted-foreground for pending input (ComposerPendingUserInputPanel.tsx:190) and text-[11px] for a pending approval (ComposerPendingApprovalPanel.tsx:36). Since ChatComposer swaps these in place inside one .chat-composer-top-drawer, the leading text jumps 14px β 12px β 11px as the state changes, and this is the only 14px text left in the drawer after the compaction the rest of the PR applies.
Suggest matching the pending-input header so the plan title stays the emphasized part of the row:
| 1 | <span className="text-xs font-medium text-muted-foreground">Plan ready</span> |
No longer relevant as of f5bd5efd9a28cf7025073c6c9e76bf24aeec678d
UI consistency review of the composer drawer refactor. One new finding: the split-glass fill is applied to the same element that carries the provider frame class, whose unlayered background: transparent wins the cascade.
Posted via Macroscope β UI Consistency
UI consistency review β 1 finding on the new attached-drawer treatment. Details inline.
Posted via Macroscope β UI Consistency
UI consistency review of the composer drawer refactor. Two findings: one CSS-ownership gap where the new split-glass path never activates for the drawers that attach from outside the shell, and one focus-semantics regression on the new stash close button.
Posted via Macroscope β UI Consistency
Reviewed the composer drawer refactor against the shared control/Tailwind ownership rules. Most of the earlier structural concerns are resolved in this revision (the micro size now owns its own type scale, the split-glass path is driven by an explicit chat-composer-glass-shell-attached class instead of :has() reaching outside the shell, the banner cap shares the drawer's surface tokens, the drawer inset is tokenized and read by the portal positioner, and the stash drawer gained an outside-pointer/close path that preserves composer focus).
Two findings remain, both on changed lines.
Posted via Macroscope β UI Consistency
Suppressing the tab on externalDrawerAttached also removes the only acknowledgement that βS worked.
ComposerStashBadge is the sole pointer entry point to the stash (toggleStashMenu has no other call site) and the sole success signal for a stash β stashCurrentPrompt only toasts on failure (written/durable), so on the happy path the badge's count and pulse are the entire feedback (see its own doc comment: "On save the badge gives one quiet acknowledgement").
externalDrawerAttached is true for any composer banner (background liveness, woke, update) or the sync pill (ChatView.tsx:6251), and none of those states block stashing: βS still clears the prompt and images, but with the tab gone there is no toast, no count, and no pointer way back into the stash. Touch has no βS at all, and the stashCurrentPrompt empty-composer fallback only opens the menu when the composer is already empty.
Hiding the tab is right β it would collide with the drawer above it β so the smallest fix is to keep some affordance alive in that state rather than restoring the tab: e.g. surface the stash count/toggle in the footer row while the tab is suppressed, or toast on a successful stash when it isn't rendered.
Posted via Macroscope β UI Consistency
Fixed in 690bf89ec. The tab stays hidden beside attached drawers, but the same count, pulse, and toggle move into existing controls. Expanded, collapsed, and pending mobile layouts all keep a pointer entry, and the inline control remains clickable while the menu is open.
| β¦ | β¦ | @@ -190,7 +197,12 @@ function ComposerBannerStackAlert({ | |
| 193 | - | className={cn("alert-glass rounded-[22px]", item.className)} | |
| 200 | + | className={cn( | |
| 201 | + | attached | |
| 202 | + | ? "chat-composer-drawer-surface chat-composer-drawer-attached px-3 pt-2 pb-[calc(var(--chat-composer-attachment-overlap)_+_0.375rem)] text-xs" | |
The surfaces that stack in the attached-drawer column disagree on their content inset from sm up.
This banner is px-3 and ThreadSyncStatusPill is px-3 (ThreadSyncStatusPill.tsx:11), while every body inside chat-composer-top-drawer uses the composer's px-3 sm:px-4: the approval row (ChatComposer.tsx:2737), the collapsed approval panel (2769), ComposerPlanFollowUpBanner.tsx:9, and ComposerPendingUserInputPanel.tsx:220. All of them share the same --chat-composer-drawer-inset column and are rendered directly on top of one another, so at β₯640px the banner/pill text starts 4px inside the top drawer's text, and content shifts sideways as the stack changes.
Suggest adopting the same px-3 sm:px-4 here and in ThreadSyncStatusPill.
| 1 | ? "chat-composer-drawer-surface chat-composer-drawer-attached px-3 pt-2 pb-[calc(var(--chat-composer-attachment-overlap)_+_0.375rem)] text-xs sm:px-4" |
Posted via Macroscope β UI Consistency
UI consistency review of the composer drawer refresh. One finding on the new inline stash placement.
Posted via Macroscope β UI Consistency
Added 14 commits
- feat(web): add compact composer button size2376e4c
- feat(web): attach composer state drawers28259e5
- fix(web): preserve composer drawer fallback65f67a6
- fix(web): match composer stack surfaces7f028ff
- fix(web): preserve composer drawer access2f27ece
- fix(web): use shared stash button styles7b59ba0
- fix(web): keep attached stash controls reachable8969a26
- feat(composer): show skill source iconsa08fb96
- fix(web): keep stash access beside drawersbd2d363
- fix(web): join external composer drawersc7dae85
- feat(web): show live composer tasksee1a59e
- fix(web): polish composer task controls8e8f32d
- fix(web): keep task progress currentd5bdcc7
- fix(web): hide unavailable task toggle0fbf449