From 79f270f5496267ca9713d40af277e8453e528d8f Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 22 Jun 2026 13:37:31 -0500 Subject: [PATCH] fix(desktop): portal floating composer to body so it can't be clipped off-screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The popped-out composer is position:fixed, but the chat content wrapper sets `contain: layout paint`, which makes it a containing block for — and clips — fixed descendants. Inline, the floating composer was positioned/clipped relative to the chat column (which shifts with the sidebars), not the viewport, so the viewport-based bounds clamp from #50466 couldn't keep it reachable: users still lost it off-screen. Portal it to when popped out so fixed positioning and the clamp finally share the viewport as their reference. Docked stays inline (it's absolute within the chat column by design). --- apps/desktop/src/app/chat/composer/index.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/app/chat/composer/index.tsx b/apps/desktop/src/app/chat/composer/index.tsx index 44ad0fa2a39..f6a5c5ff48d 100644 --- a/apps/desktop/src/app/chat/composer/index.tsx +++ b/apps/desktop/src/app/chat/composer/index.tsx @@ -12,6 +12,7 @@ import { useRef, useState } from 'react' +import { createPortal } from 'react-dom' import { hermesDirectiveFormatter, type SlashChipKind } from '@/components/assistant-ui/directive-text' import { composerFill, composerSurfaceGlass } from '@/components/chat/composer-dock' @@ -1923,7 +1924,7 @@ export function ChatBar({ ) - return ( + const composerOverlay = ( <> {dragging && poppedOut && (
+ + ) + + return ( + <> + {/* Floating: portal to so position:fixed resolves against the + viewport. The chat content wrapper sets `contain: layout paint`, which + makes it a containing block for (and clips) fixed descendants — left + inline, the popped-out composer is positioned/clipped relative to the + chat column (which shifts with the sidebars), not the viewport, so the + viewport-based clamp can't keep it on-screen. Docked stays inline: it's + `absolute` within that column by design. */} + {poppedOut ? createPortal(composerOverlay, document.body) : composerOverlay}