From 4980ba282eb313b3be87a6b7df28f3b42dcb4ce9 Mon Sep 17 00:00:00 2001 From: Tristan Manchester <108270628+tristanmanchester@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:26:57 +0200 Subject: [PATCH] fix(web): queued messages no longer split the composer notice stack (#16400) Co-authored-by: Claude Opus 5.5 (1M context) Co-authored-by: Yash Singh --- apps/web/src/components/chat/ChatComposer.tsx | 2 +- .../components/chat/ComposerBannerStack.tsx | 217 +++++++++--------- 2 files changed, 113 insertions(+), 106 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 0ee5a5fc14021e5544766b51956ef7d7cc0237ca..ee9c313cbec740e8c4ee9bf763bc22185b73d9bf 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -6613,11 +6613,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) : null} - {props.queuedRunsControl} {!activityStackItem && (shownSyncPhase || inlineTasksBadge) ? ( diff --git a/apps/web/src/components/chat/ComposerBannerStack.tsx b/apps/web/src/components/chat/ComposerBannerStack.tsx index 0cf3e8d05ab0d9d509b50c5137f5887e2378d0f4..ff66869a9ec86dfb800b6f6a7355843d8eb093ee 100644 --- a/apps/web/src/components/chat/ComposerBannerStack.tsx +++ b/apps/web/src/components/chat/ComposerBannerStack.tsx @@ -43,9 +43,12 @@ function bannerPriority(item: ComposerBannerStackEntry) { interface ComposerBannerStackProps { readonly className?: string; readonly items: ReadonlyArray; + /** Attachments that sit between the stacked notices and the front item, such as the queue. + The stacked notices peek above the whole column, never from between two attachments. */ + readonly attachedAbove?: ReactNode; } -export function ComposerBannerStack({ className, items }: ComposerBannerStackProps) { +export function ComposerBannerStack({ className, items, attachedAbove }: ComposerBannerStackProps) { const [stackExpanded, setStackExpanded] = useState(false); const noticesRef = useRef(null); const peekRef = useRef(null); @@ -84,15 +87,17 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro } }, [stackExpanded]); - if (items.length === 0) { - return null; - } - // Activity stays attached. Urgency and severity only order the notices behind it. const orderedItems = items.toSorted((a, b) => bannerPriority(a) - bannerPriority(b)); const frontItem = orderedItems[0]; if (!frontItem) { - return null; + return ( + <> + {null} + {attachedAbove} + {null} + + ); } const stackedItems = orderedItems.slice(1); const hasStack = stackedItems.length > 0; @@ -114,12 +119,105 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro }; return ( - -
+ <> + {hasStack ? ( +
{ + if (event.pointerType === "touch") return; + if (document.activeElement === peekRef.current) { + pendingFocusRef.current = "notice"; + } + setStackExpanded(true); + }} + onPointerLeave={(event) => { + if (!event.currentTarget.contains(document.activeElement)) setStackExpanded(false); + }} + onBlurCapture={(event) => { + if ( + !event.currentTarget.contains(event.relatedTarget) && + !event.currentTarget.matches(":hover") + ) { + setStackExpanded(false); + } + }} + onKeyDown={(event) => { + if (event.key !== "Escape" || !stackExpanded) return; + event.preventDefault(); + event.stopPropagation(); + pendingFocusRef.current = "peek"; + setStackExpanded(false); + }} + > + {showCollapsedStackCap && firstStackedItem ? ( + { + event.currentTarget.focus({ preventScroll: true }); + pendingFocusRef.current = "notice"; + setStackExpanded(true); + }} + className={cn(stackExpanded && "pointer-events-none invisible opacity-0")} + /> + ) : null} +
+
+
+ {stackedItems.map((item) => ( +
+ requestDismiss(item)} + /> +
+ ))} +
+
+
+
+ ) : null} + {attachedAbove} +
requestDismiss(frontItem)} />
- {hasStack ? ( -
{ - if (event.pointerType === "touch") return; - if (document.activeElement === peekRef.current) { - pendingFocusRef.current = "notice"; - } - setStackExpanded(true); - }} - onPointerLeave={(event) => { - if (!event.currentTarget.contains(document.activeElement)) setStackExpanded(false); - }} - onBlurCapture={(event) => { - if ( - !event.currentTarget.contains(event.relatedTarget) && - !event.currentTarget.matches(":hover") - ) { - setStackExpanded(false); - } - }} - onKeyDown={(event) => { - if (event.key !== "Escape" || !stackExpanded) return; - event.preventDefault(); - event.stopPropagation(); - pendingFocusRef.current = "peek"; - setStackExpanded(false); - }} - > - {showCollapsedStackCap && firstStackedItem ? ( - { - event.currentTarget.focus({ preventScroll: true }); - pendingFocusRef.current = "notice"; - setStackExpanded(true); - }} - className={cn(stackExpanded && "pointer-events-none invisible opacity-0")} - /> - ) : null} -
-
-
- {stackedItems.map((item) => ( -
- requestDismiss(item)} - /> -
- ))} -
-
-
-
- ) : null} -
-
+
+ ); } -- 2.54.0