From 816628af27c29d9f69b7be8ef04725a94d479b87 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Thu, 27 Aug 2026 01:51:37 +0300 Subject: [PATCH 1/9] fix(copilot): keep sidebar mode beside host content instead of overlapping it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In sidebar display mode, reserve space for the panel with body margin-right and suspend the containing-block properties the host may set on (transform, perspective, will-change) while the sidebar is open — all restored on close. Those properties make the containing block for the fixed sidebar, re-anchoring it to the margin-shrunk body: the sidebar gets pushed (white gap) and, on tall pages, stretches to the document height and scrolls with the page. Suspending them keeps the sidebar a true viewport-fixed panel and leaves the host's own layout untouched. Co-Authored-By: GitHub Copilot --- cypress/e2e/copilot/spec.cy.ts | 24 ++++++++++++++++++++++ libs/copilot/src/hooks/useSidebarResize.ts | 24 +++++++++++++++++----- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/copilot/spec.cy.ts b/cypress/e2e/copilot/spec.cy.ts index 2deb0a5214..0edfc56fe5 100644 --- a/cypress/e2e/copilot/spec.cy.ts +++ b/cypress/e2e/copilot/spec.cy.ts @@ -189,6 +189,30 @@ describe('Copilot', { includeShadowDom: true }, () => { }); }); + it('should neutralize a host body transform while open and restore it', () => { + cy.step('Give the host a transform on '); + cy.document().then((doc) => { + doc.body.style.transform = 'translateZ(0)'; + }); + + mountCopilotWidget({ displayMode: 'sidebar', opened: true }); + + cy.get('#chainlit-copilot-chat').should('exist'); + cy.step( + 'Body transform is neutralized so the sidebar stays viewport-fixed' + ); + cy.document().should((doc) => { + expect(doc.body.style.transform).to.equal('none'); + }); + + cy.step('Close and verify the host transform is restored'); + cy.get('#close-sidebar-button').click(); + cy.get('#chainlit-copilot-chat').should('not.exist'); + cy.document().should((doc) => { + expect(doc.body.style.transform).to.equal('translateZ(0)'); + }); + }); + it('should resize sidebar via drag handle', () => { mountCopilotWidget({ displayMode: 'sidebar', opened: true }); diff --git a/libs/copilot/src/hooks/useSidebarResize.ts b/libs/copilot/src/hooks/useSidebarResize.ts index c5e45c2fd0..c2495879bb 100644 --- a/libs/copilot/src/hooks/useSidebarResize.ts +++ b/libs/copilot/src/hooks/useSidebarResize.ts @@ -26,7 +26,6 @@ export function useSidebarResize({ return stored ? Number(stored) : SIDEBAR_DEFAULT_WIDTH; }); const isDragging = useRef(false); - const originalMarginRef = useRef(''); useEffect(() => { if (displayMode === 'sidebar') { @@ -74,13 +73,28 @@ export function useSidebarResize({ }; }, [stopDragging, displayMode, isOpen]); + // A host containing block on (transform / perspective / will-change) re-anchors + // the fixed sidebar to the body and breaks its positioning, so suspend those while the + // sidebar is open and reserve space with a right margin. All restored on close. useEffect(() => { if (displayMode === 'sidebar' && isOpen) { - originalMarginRef.current = document.body.style.marginRight; - document.body.style.transition = 'margin-right 0.3s ease-in-out'; + const body = document.body; + const previous = { + marginRight: body.style.marginRight, + transform: body.style.transform, + perspective: body.style.perspective, + willChange: body.style.willChange + }; + body.style.transform = 'none'; + body.style.perspective = 'none'; + body.style.willChange = 'auto'; + body.style.transition = 'margin-right 0.3s ease-in-out'; return () => { - document.body.style.marginRight = originalMarginRef.current; - document.body.style.transition = ''; + body.style.marginRight = previous.marginRight; + body.style.transform = previous.transform; + body.style.perspective = previous.perspective; + body.style.willChange = previous.willChange; + body.style.transition = ''; }; } }, [displayMode, isOpen]); From 23b3065751d34ca81d0efd2387c14d093649b9e4 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Thu, 27 Aug 2026 09:29:40 +0300 Subject: [PATCH 2/9] feat(copilot): constrain a configured hostRoot for viewport-filling apps in sidebar mode Viewport-filling hosts (100vw / position: absolute inset shells like maps and dashboards) ignore the body margin, so the sidebar overlapped them. Add an optional hostRoot widget config: when set, sidebar mode constrains that element's width to calc(100vw - sidebarWidth) instead of nudging the body margin, restored on close. No behavior change when it is unset. Co-Authored-By: GitHub Copilot --- cypress/e2e/copilot/spec.cy.ts | 26 +++++++ libs/copilot/src/hooks/useSidebarResize.ts | 85 +++++++++++++++------- libs/copilot/src/types.ts | 4 + libs/copilot/src/widget.tsx | 3 +- 4 files changed, 92 insertions(+), 26 deletions(-) diff --git a/cypress/e2e/copilot/spec.cy.ts b/cypress/e2e/copilot/spec.cy.ts index 0edfc56fe5..68cf547352 100644 --- a/cypress/e2e/copilot/spec.cy.ts +++ b/cypress/e2e/copilot/spec.cy.ts @@ -213,6 +213,32 @@ describe('Copilot', { includeShadowDom: true }, () => { }); }); + it('should constrain a configured hostRoot instead of the body margin', () => { + cy.step('Add a host root element the widget should constrain'); + cy.document().then((doc) => { + const el = doc.createElement('div'); + el.id = 'test-host-root'; + doc.body.appendChild(el); + }); + + mountCopilotWidget({ + displayMode: 'sidebar', + opened: true, + hostRoot: '#test-host-root' + }); + + cy.get('#chainlit-copilot-chat').should('exist'); + cy.step( + 'hostRoot width is constrained and the body margin is left alone' + ); + cy.get('#test-host-root').should(($el) => { + expect($el[0].style.width).to.equal('calc(100vw - 400px)'); + }); + cy.document().should((doc) => { + expect(doc.body.style.marginRight).to.not.equal('400px'); + }); + }); + it('should resize sidebar via drag handle', () => { mountCopilotWidget({ displayMode: 'sidebar', opened: true }); diff --git a/libs/copilot/src/hooks/useSidebarResize.ts b/libs/copilot/src/hooks/useSidebarResize.ts index c2495879bb..d83406267f 100644 --- a/libs/copilot/src/hooks/useSidebarResize.ts +++ b/libs/copilot/src/hooks/useSidebarResize.ts @@ -10,6 +10,7 @@ const LS_WIDTH_KEY = 'chainlit-copilot-sidebarWidth'; interface UseSidebarResizeOptions { displayMode: DisplayMode; isOpen: boolean; + hostRoot?: string; } interface UseSidebarResizeReturn { @@ -19,7 +20,8 @@ interface UseSidebarResizeReturn { export function useSidebarResize({ displayMode, - isOpen + isOpen, + hostRoot }: UseSidebarResizeOptions): UseSidebarResizeReturn { const [sidebarWidth, setSidebarWidth] = useState(() => { const stored = localStorage.getItem(LS_WIDTH_KEY); @@ -73,37 +75,70 @@ export function useSidebarResize({ }; }, [stopDragging, displayMode, isOpen]); - // A host containing block on (transform / perspective / will-change) re-anchors - // the fixed sidebar to the body and breaks its positioning, so suspend those while the - // sidebar is open and reserve space with a right margin. All restored on close. + // Suspend any containing block the host set on (transform / perspective / + // will-change) so the fixed sidebar stays anchored to the viewport, then reserve space + // for it. A viewport-filling host (100vw / absolute inset) can't be shrunk by a body + // margin, so when `hostRoot` is given we constrain that element's width instead. + // Everything is restored on close. useEffect(() => { - if (displayMode === 'sidebar' && isOpen) { - const body = document.body; - const previous = { - marginRight: body.style.marginRight, - transform: body.style.transform, - perspective: body.style.perspective, - willChange: body.style.willChange - }; - body.style.transform = 'none'; - body.style.perspective = 'none'; - body.style.willChange = 'auto'; + if (displayMode !== 'sidebar' || !isOpen) return; + + const body = document.body; + const host = hostRoot + ? document.querySelector(hostRoot) + : null; + + const prevBody = { + transform: body.style.transform, + perspective: body.style.perspective, + willChange: body.style.willChange, + marginRight: body.style.marginRight, + transition: body.style.transition + }; + body.style.transform = 'none'; + body.style.perspective = 'none'; + body.style.willChange = 'auto'; + + const prevHost = host && { + width: host.style.width, + overflowX: host.style.overflowX, + transition: host.style.transition + }; + if (host) { + host.style.width = `calc(100vw - ${sidebarWidth}px)`; + host.style.overflowX = 'hidden'; + host.style.transition = 'width 0.3s ease-in-out'; + } else { + body.style.marginRight = `${sidebarWidth}px`; body.style.transition = 'margin-right 0.3s ease-in-out'; - return () => { - body.style.marginRight = previous.marginRight; - body.style.transform = previous.transform; - body.style.perspective = previous.perspective; - body.style.willChange = previous.willChange; - body.style.transition = ''; - }; } - }, [displayMode, isOpen]); + + return () => { + body.style.transform = prevBody.transform; + body.style.perspective = prevBody.perspective; + body.style.willChange = prevBody.willChange; + if (host && prevHost) { + host.style.width = prevHost.width; + host.style.overflowX = prevHost.overflowX; + host.style.transition = prevHost.transition; + } else { + body.style.marginRight = prevBody.marginRight; + body.style.transition = prevBody.transition; + } + }; + }, [displayMode, isOpen, hostRoot]); useEffect(() => { - if (displayMode === 'sidebar' && isOpen) { + if (displayMode !== 'sidebar' || !isOpen) return; + const host = hostRoot + ? document.querySelector(hostRoot) + : null; + if (host) { + host.style.width = `calc(100vw - ${sidebarWidth}px)`; + } else { document.body.style.marginRight = `${sidebarWidth}px`; } - }, [sidebarWidth, displayMode, isOpen]); + }, [sidebarWidth, displayMode, isOpen, hostRoot]); return { sidebarWidth, handleMouseDown }; } diff --git a/libs/copilot/src/types.ts b/libs/copilot/src/types.ts index b71f9a44a0..9b9214983e 100644 --- a/libs/copilot/src/types.ts +++ b/libs/copilot/src/types.ts @@ -16,4 +16,8 @@ export interface IWidgetConfig { language?: string; opened?: boolean; displayMode?: DisplayMode; + // CSS selector for a viewport-filling host root (e.g. a full-screen map/dashboard + // shell). In sidebar mode its width is constrained instead of nudging the body margin, + // which a `100vw` / `position: absolute inset` layout would ignore. + hostRoot?: string; } diff --git a/libs/copilot/src/widget.tsx b/libs/copilot/src/widget.tsx index e2485f316a..9a950648f3 100644 --- a/libs/copilot/src/widget.tsx +++ b/libs/copilot/src/widget.tsx @@ -36,7 +36,8 @@ const Widget = ({ config, error }: Props) => { const projectConfig = useConfig(); const { sidebarWidth, handleMouseDown } = useSidebarResize({ displayMode, - isOpen + isOpen, + hostRoot: config?.hostRoot }); useEffect(() => { From 9d687b944e9bccb2f3f98bd530821f59e858ce3d Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Thu, 27 Aug 2026 10:00:16 +0300 Subject: [PATCH 3/9] refactor(copilot): address sidebar hostRoot review feedback Share a single getHostRoot helper and re-query the host on cleanup instead of reusing a cached node; use overflow-x: clip (not hidden) so the host does not become a scroll container; toggle the drag transition on whichever element is reserved so host-width drags animate too. Co-Authored-By: GitHub Copilot --- libs/copilot/src/hooks/useSidebarResize.ts | 87 ++++++++++++++-------- 1 file changed, 57 insertions(+), 30 deletions(-) diff --git a/libs/copilot/src/hooks/useSidebarResize.ts b/libs/copilot/src/hooks/useSidebarResize.ts index d83406267f..504202ccc5 100644 --- a/libs/copilot/src/hooks/useSidebarResize.ts +++ b/libs/copilot/src/hooks/useSidebarResize.ts @@ -29,6 +29,43 @@ export function useSidebarResize({ }); const isDragging = useRef(false); + // Resolve the host root fresh on each use so a host that swaps the node (SPA re-render) + // is always handled — never a cached, detached node. + const getHostRoot = useCallback( + () => (hostRoot ? document.querySelector(hostRoot) : null), + [hostRoot] + ); + + // Reserve space beside the sidebar: constrain the host root's width, or (default) push + // the body with a right margin. + const reserveSpace = useCallback( + (width: number) => { + const host = getHostRoot(); + if (host) { + host.style.width = `calc(100vw - ${width}px)`; + } else { + document.body.style.marginRight = `${width}px`; + } + }, + [getHostRoot] + ); + + // Toggle the reservation transition on whichever element we actually resize, so drags + // follow the pointer instantly instead of animating each step. + const setReserveTransition = useCallback( + (enabled: boolean) => { + const host = getHostRoot(); + if (host) { + host.style.transition = enabled ? 'width 0.3s ease-in-out' : ''; + } else { + document.body.style.transition = enabled + ? 'margin-right 0.3s ease-in-out' + : ''; + } + }, + [getHostRoot] + ); + useEffect(() => { if (displayMode === 'sidebar') { localStorage.setItem(LS_WIDTH_KEY, String(sidebarWidth)); @@ -39,14 +76,14 @@ export function useSidebarResize({ if (!isDragging.current) return; isDragging.current = false; document.body.style.userSelect = ''; - document.body.style.transition = 'margin-right 0.3s ease-in-out'; - }, []); + setReserveTransition(true); + }, [setReserveTransition]); const handleMouseDown = useCallback(() => { isDragging.current = true; document.body.style.userSelect = 'none'; - document.body.style.transition = ''; - }, []); + setReserveTransition(false); + }, [setReserveTransition]); useEffect(() => { if (displayMode !== 'sidebar' || !isOpen) return; @@ -84,9 +121,7 @@ export function useSidebarResize({ if (displayMode !== 'sidebar' || !isOpen) return; const body = document.body; - const host = hostRoot - ? document.querySelector(hostRoot) - : null; + const host = getHostRoot(); const prevBody = { transform: body.style.transform, @@ -104,41 +139,33 @@ export function useSidebarResize({ overflowX: host.style.overflowX, transition: host.style.transition }; - if (host) { - host.style.width = `calc(100vw - ${sidebarWidth}px)`; - host.style.overflowX = 'hidden'; - host.style.transition = 'width 0.3s ease-in-out'; - } else { - body.style.marginRight = `${sidebarWidth}px`; - body.style.transition = 'margin-right 0.3s ease-in-out'; - } + // `clip` shrinks non-reflowing content without turning the host into a scroll container. + if (host) host.style.overflowX = 'clip'; + // Reserve first (instant), then enable the transition so only later drags animate. + reserveSpace(sidebarWidth); + setReserveTransition(true); return () => { body.style.transform = prevBody.transform; body.style.perspective = prevBody.perspective; body.style.willChange = prevBody.willChange; - if (host && prevHost) { - host.style.width = prevHost.width; - host.style.overflowX = prevHost.overflowX; - host.style.transition = prevHost.transition; - } else { + // Re-query so we reset the node mounted now, not a stale one. + const current = getHostRoot(); + if (prevHost && current) { + current.style.width = prevHost.width; + current.style.overflowX = prevHost.overflowX; + current.style.transition = prevHost.transition; + } else if (!prevHost) { body.style.marginRight = prevBody.marginRight; body.style.transition = prevBody.transition; } }; - }, [displayMode, isOpen, hostRoot]); + }, [displayMode, isOpen, getHostRoot, reserveSpace, setReserveTransition]); useEffect(() => { if (displayMode !== 'sidebar' || !isOpen) return; - const host = hostRoot - ? document.querySelector(hostRoot) - : null; - if (host) { - host.style.width = `calc(100vw - ${sidebarWidth}px)`; - } else { - document.body.style.marginRight = `${sidebarWidth}px`; - } - }, [sidebarWidth, displayMode, isOpen, hostRoot]); + reserveSpace(sidebarWidth); + }, [sidebarWidth, displayMode, isOpen, reserveSpace]); return { sidebarWidth, handleMouseDown }; } From 2cc1c83847327c4b67ffca89aedfad6e56ba252b Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Thu, 27 Aug 2026 13:24:17 +0300 Subject: [PATCH 4/9] fix(copilot): restore the styled host node on close and commit host width before transition Cleanup now restores the exact node it styled using that node's captured styles, and undoes the body-margin reservation whenever the body fallback was used, so a hostRoot that is swapped or removed while open no longer leaves the wrong node or the body in a stale state. Also flush the host width before enabling its transition: Chromium cannot interpolate width from auto to calc(), so it stuck the host at its pre-open width and never shrank. The body margin animates from 0 and is unaffected. Co-Authored-By: GitHub Copilot --- libs/copilot/src/hooks/useSidebarResize.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/libs/copilot/src/hooks/useSidebarResize.ts b/libs/copilot/src/hooks/useSidebarResize.ts index 504202ccc5..3202253900 100644 --- a/libs/copilot/src/hooks/useSidebarResize.ts +++ b/libs/copilot/src/hooks/useSidebarResize.ts @@ -143,19 +143,23 @@ export function useSidebarResize({ if (host) host.style.overflowX = 'clip'; // Reserve first (instant), then enable the transition so only later drags animate. reserveSpace(sidebarWidth); + // Commit the host width before enabling its transition, else Chromium tries to animate + // width from `auto`, sticks at the pre-open value, and the host never shrinks. A body + // margin animates from 0 fine, so it needs no flush. + if (host) void host.offsetWidth; setReserveTransition(true); return () => { body.style.transform = prevBody.transform; body.style.perspective = prevBody.perspective; body.style.willChange = prevBody.willChange; - // Re-query so we reset the node mounted now, not a stale one. - const current = getHostRoot(); - if (prevHost && current) { - current.style.width = prevHost.width; - current.style.overflowX = prevHost.overflowX; - current.style.transition = prevHost.transition; - } else if (!prevHost) { + if (host && prevHost) { + // Restore the exact node we styled (harmless if it was since detached/swapped). + host.style.width = prevHost.width; + host.style.overflowX = prevHost.overflowX; + host.style.transition = prevHost.transition; + } else { + // We took the body-margin fallback; undo it. body.style.marginRight = prevBody.marginRight; body.style.transition = prevBody.transition; } From 6b17d7c9262f35f0da2490e8b965cef3e4d635dc Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Thu, 27 Aug 2026 14:53:54 +0300 Subject: [PATCH 5/9] test(copilot): make sidebar e2e assertions robust to inline-style normalization The browser rewrites inline style values: translateZ(0) becomes translateZ(0px) and calc(100vw - 400px) becomes calc(-400px + 100vw). Assert the restored transform against the normalized value and check the hostRoot width via its bounding rect instead of the raw calc string. Co-Authored-By: GitHub Copilot --- cypress/e2e/copilot/spec.cy.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/copilot/spec.cy.ts b/cypress/e2e/copilot/spec.cy.ts index 68cf547352..60d099bd36 100644 --- a/cypress/e2e/copilot/spec.cy.ts +++ b/cypress/e2e/copilot/spec.cy.ts @@ -192,7 +192,7 @@ describe('Copilot', { includeShadowDom: true }, () => { it('should neutralize a host body transform while open and restore it', () => { cy.step('Give the host a transform on '); cy.document().then((doc) => { - doc.body.style.transform = 'translateZ(0)'; + doc.body.style.transform = 'translateZ(0px)'; }); mountCopilotWidget({ displayMode: 'sidebar', opened: true }); @@ -209,7 +209,7 @@ describe('Copilot', { includeShadowDom: true }, () => { cy.get('#close-sidebar-button').click(); cy.get('#chainlit-copilot-chat').should('not.exist'); cy.document().should((doc) => { - expect(doc.body.style.transform).to.equal('translateZ(0)'); + expect(doc.body.style.transform).to.equal('translateZ(0px)'); }); }); @@ -232,7 +232,13 @@ describe('Copilot', { includeShadowDom: true }, () => { 'hostRoot width is constrained and the body margin is left alone' ); cy.get('#test-host-root').should(($el) => { - expect($el[0].style.width).to.equal('calc(100vw - 400px)'); + const el = $el[0]; + const win = el.ownerDocument.defaultView; + if (!win) throw new Error('missing host window'); + expect(el.getBoundingClientRect().width).to.be.closeTo( + win.innerWidth - 400, + 2 + ); }); cy.document().should((doc) => { expect(doc.body.style.marginRight).to.not.equal('400px'); From dc716835f7d9ae36d85e9330777ee5cdf6ee1c47 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Thu, 27 Aug 2026 15:06:28 +0300 Subject: [PATCH 6/9] fix(copilot): restore every host node constrained during a sidebar session reserveSpace resolves the host fresh each call to handle SPA node swaps, but cleanup only restored the node captured at open, leaving a swapped-in host stuck at width: calc(100vw - Xpx). Track each styled host node in a ref map and restore them all on close. Co-Authored-By: GitHub Copilot --- libs/copilot/src/hooks/useSidebarResize.ts | 68 +++++++++++++++------- 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/libs/copilot/src/hooks/useSidebarResize.ts b/libs/copilot/src/hooks/useSidebarResize.ts index 3202253900..bcb67796a3 100644 --- a/libs/copilot/src/hooks/useSidebarResize.ts +++ b/libs/copilot/src/hooks/useSidebarResize.ts @@ -36,18 +36,40 @@ export function useSidebarResize({ [hostRoot] ); + // Original inline styles of every host node we've constrained this session. The SPA can + // swap the host mid-session, so we key by node and restore them all on close. + const styledHosts = useRef( + new Map< + HTMLElement, + { width: string; overflowX: string; transition: string } + >() + ); + + // Snapshot a host's original inline styles the first time we touch it, so close can + // restore it even after a swap. + const rememberHost = useCallback((host: HTMLElement) => { + if (!styledHosts.current.has(host)) { + styledHosts.current.set(host, { + width: host.style.width, + overflowX: host.style.overflowX, + transition: host.style.transition + }); + } + }, []); + // Reserve space beside the sidebar: constrain the host root's width, or (default) push // the body with a right margin. const reserveSpace = useCallback( (width: number) => { const host = getHostRoot(); if (host) { + rememberHost(host); host.style.width = `calc(100vw - ${width}px)`; } else { document.body.style.marginRight = `${width}px`; } }, - [getHostRoot] + [getHostRoot, rememberHost] ); // Toggle the reservation transition on whichever element we actually resize, so drags @@ -56,6 +78,7 @@ export function useSidebarResize({ (enabled: boolean) => { const host = getHostRoot(); if (host) { + rememberHost(host); host.style.transition = enabled ? 'width 0.3s ease-in-out' : ''; } else { document.body.style.transition = enabled @@ -63,7 +86,7 @@ export function useSidebarResize({ : ''; } }, - [getHostRoot] + [getHostRoot, rememberHost] ); useEffect(() => { @@ -134,13 +157,11 @@ export function useSidebarResize({ body.style.perspective = 'none'; body.style.willChange = 'auto'; - const prevHost = host && { - width: host.style.width, - overflowX: host.style.overflowX, - transition: host.style.transition - }; - // `clip` shrinks non-reflowing content without turning the host into a scroll container. - if (host) host.style.overflowX = 'clip'; + if (host) { + rememberHost(host); + // `clip` shrinks non-reflowing content without turning the host into a scroll container. + host.style.overflowX = 'clip'; + } // Reserve first (instant), then enable the transition so only later drags animate. reserveSpace(sidebarWidth); // Commit the host width before enabling its transition, else Chromium tries to animate @@ -149,22 +170,29 @@ export function useSidebarResize({ if (host) void host.offsetWidth; setReserveTransition(true); + const hosts = styledHosts.current; return () => { body.style.transform = prevBody.transform; body.style.perspective = prevBody.perspective; body.style.willChange = prevBody.willChange; - if (host && prevHost) { - // Restore the exact node we styled (harmless if it was since detached/swapped). - host.style.width = prevHost.width; - host.style.overflowX = prevHost.overflowX; - host.style.transition = prevHost.transition; - } else { - // We took the body-margin fallback; undo it. - body.style.marginRight = prevBody.marginRight; - body.style.transition = prevBody.transition; - } + body.style.marginRight = prevBody.marginRight; + body.style.transition = prevBody.transition; + // Restore every host node we constrained — the SPA may have swapped it mid-session. + hosts.forEach((prev, node) => { + node.style.width = prev.width; + node.style.overflowX = prev.overflowX; + node.style.transition = prev.transition; + }); + hosts.clear(); }; - }, [displayMode, isOpen, getHostRoot, reserveSpace, setReserveTransition]); + }, [ + displayMode, + isOpen, + getHostRoot, + reserveSpace, + setReserveTransition, + rememberHost + ]); useEffect(() => { if (displayMode !== 'sidebar' || !isOpen) return; From f2c4c9d47e1f532d388ebd9960f78590dc21cee2 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Thu, 27 Aug 2026 15:11:22 +0300 Subject: [PATCH 7/9] fix(copilot): only restore body margin when the sidebar used the body fallback The host path never touches body margin-right or transition, so restoring them unconditionally on close clobbered any host-app updates made while the sidebar was open. Restore them only when no host node was constrained. Co-Authored-By: GitHub Copilot --- libs/copilot/src/hooks/useSidebarResize.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/libs/copilot/src/hooks/useSidebarResize.ts b/libs/copilot/src/hooks/useSidebarResize.ts index bcb67796a3..e1a369714a 100644 --- a/libs/copilot/src/hooks/useSidebarResize.ts +++ b/libs/copilot/src/hooks/useSidebarResize.ts @@ -175,15 +175,19 @@ export function useSidebarResize({ body.style.transform = prevBody.transform; body.style.perspective = prevBody.perspective; body.style.willChange = prevBody.willChange; - body.style.marginRight = prevBody.marginRight; - body.style.transition = prevBody.transition; - // Restore every host node we constrained — the SPA may have swapped it mid-session. - hosts.forEach((prev, node) => { - node.style.width = prev.width; - node.style.overflowX = prev.overflowX; - node.style.transition = prev.transition; - }); - hosts.clear(); + if (hosts.size) { + // Restore every host node we constrained — the SPA may have swapped it mid-session. + hosts.forEach((prev, node) => { + node.style.width = prev.width; + node.style.overflowX = prev.overflowX; + node.style.transition = prev.transition; + }); + hosts.clear(); + } else { + // We took the body-margin fallback; undo only what we touched. + body.style.marginRight = prevBody.marginRight; + body.style.transition = prevBody.transition; + } }; }, [ displayMode, From 304415d02d5dc6830df5d002984292a7dc2654a1 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Thu, 27 Aug 2026 15:25:35 +0300 Subject: [PATCH 8/9] fix(copilot): track body-fallback usage apart from styled host nodes hosts.size doubled as the body-fallback signal, so a hostRoot that disappeared mid-session (map non-empty, body margin written) skipped the margin restore and left the page shifted. Add a usedBodyFallback ref set on each body-branch write and reset per session; cleanup restores all host nodes always and body margin/transition only when the fallback was used. Co-Authored-By: GitHub Copilot --- libs/copilot/src/hooks/useSidebarResize.ts | 27 ++++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/libs/copilot/src/hooks/useSidebarResize.ts b/libs/copilot/src/hooks/useSidebarResize.ts index e1a369714a..39fa60976b 100644 --- a/libs/copilot/src/hooks/useSidebarResize.ts +++ b/libs/copilot/src/hooks/useSidebarResize.ts @@ -45,6 +45,10 @@ export function useSidebarResize({ >() ); + // Whether we wrote the body-margin fallback this session (host absent). Tracked apart + // from styledHosts because a session can use both if a configured host disappears. + const usedBodyFallback = useRef(false); + // Snapshot a host's original inline styles the first time we touch it, so close can // restore it even after a swap. const rememberHost = useCallback((host: HTMLElement) => { @@ -66,6 +70,7 @@ export function useSidebarResize({ rememberHost(host); host.style.width = `calc(100vw - ${width}px)`; } else { + usedBodyFallback.current = true; document.body.style.marginRight = `${width}px`; } }, @@ -81,6 +86,7 @@ export function useSidebarResize({ rememberHost(host); host.style.transition = enabled ? 'width 0.3s ease-in-out' : ''; } else { + usedBodyFallback.current = true; document.body.style.transition = enabled ? 'margin-right 0.3s ease-in-out' : ''; @@ -145,6 +151,7 @@ export function useSidebarResize({ const body = document.body; const host = getHostRoot(); + usedBodyFallback.current = false; const prevBody = { transform: body.style.transform, @@ -175,16 +182,16 @@ export function useSidebarResize({ body.style.transform = prevBody.transform; body.style.perspective = prevBody.perspective; body.style.willChange = prevBody.willChange; - if (hosts.size) { - // Restore every host node we constrained — the SPA may have swapped it mid-session. - hosts.forEach((prev, node) => { - node.style.width = prev.width; - node.style.overflowX = prev.overflowX; - node.style.transition = prev.transition; - }); - hosts.clear(); - } else { - // We took the body-margin fallback; undo only what we touched. + // Restore every host node we constrained — the SPA may have swapped it mid-session. + hosts.forEach((prev, node) => { + node.style.width = prev.width; + node.style.overflowX = prev.overflowX; + node.style.transition = prev.transition; + }); + hosts.clear(); + // Undo the body-margin fallback only if we actually used it (a host may have vanished + // mid-session), so we never clobber host-app updates in the pure-host path. + if (usedBodyFallback.current) { body.style.marginRight = prevBody.marginRight; body.style.transition = prevBody.transition; } From 4537875fdb4d30b34711308a05746b0453eaa935 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Thu, 27 Aug 2026 16:01:04 +0300 Subject: [PATCH 9/9] chore: re-trigger CI