From de7ad8b78eaeab96324b9800e28f12d8b92e83a7 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 22 Jun 2026 13:59:26 -0500 Subject: [PATCH] fix(desktop): guarantee out-of-bounds composer is reclamped on load Re-clamp once more on the next frame after pop-out so layout (sidebar widths, fonts) has settled, and treat a degenerate pre-layout bounds rect as "unknown" (fall back to the window) so we never clamp the box into a collapsed area. Net: anyone who loads in with a stranded position is pulled back on-screen and the fix is persisted, even if the first measure was premature. --- apps/desktop/src/app/chat/composer/index.tsx | 15 +++++++++++---- apps/desktop/src/store/composer-popout.ts | 6 ++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/chat/composer/index.tsx b/apps/desktop/src/app/chat/composer/index.tsx index ae175c902eb..1ecc76de8bc 100644 --- a/apps/desktop/src/app/chat/composer/index.tsx +++ b/apps/desktop/src/app/chat/composer/index.tsx @@ -543,9 +543,12 @@ export function ChatBar({ syncComposerMetrics() }, [poppedOut, syncComposerMetrics]) - // Keep the floating box on-screen: re-clamp (with the real measured size) when - // it pops out and whenever the window resizes — so a position persisted on a - // bigger/other monitor, or a shrunk window, can never strand it out of reach. + // Keep the floating box on-screen: re-clamp (with the real measured size + + // thread bounds) when it pops out and on every window resize — so a position + // persisted on a bigger/other monitor, a shrunk window, or now-wider sidebar + // can never strand it. The rAF pass re-clamps after layout settles (sidebar + // widths, fonts), so anyone loading in out of bounds is pulled back + saved + // even if the first measure was premature. useEffect(() => { if (!poppedOut) { return undefined @@ -558,10 +561,14 @@ export function ChatBar({ } reclamp(true) + const raf = requestAnimationFrame(() => reclamp(true)) const onResize = () => reclamp(false) window.addEventListener('resize', onResize) - return () => window.removeEventListener('resize', onResize) + return () => { + cancelAnimationFrame(raf) + window.removeEventListener('resize', onResize) + } }, [poppedOut]) useEffect(() => { diff --git a/apps/desktop/src/store/composer-popout.ts b/apps/desktop/src/store/composer-popout.ts index 1cc2d5f2f96..a739f2f3cb8 100644 --- a/apps/desktop/src/store/composer-popout.ts +++ b/apps/desktop/src/store/composer-popout.ts @@ -88,9 +88,11 @@ export function readPopoutBounds(composer: Element | null): PopoutBounds | undef return undefined } - const { bottom, left, right, top } = el.getBoundingClientRect() + const { bottom, height, left, right, top, width } = el.getBoundingClientRect() - return { bottom, left, right, top } + // Pre-layout (mount before first layout) the rect is empty — fall back to the + // window rather than clamping the box into a collapsed area. + return width > 0 && height > 0 ? { bottom, left, right, top } : undefined } // Bound the bottom/right inset so the WHOLE box stays inside `area` (the thread