Skip to content

fix(copilot): keep sidebar mode beside host content instead of overlapping it - #3026

Closed
RoyBA wants to merge 5 commits into
Chainlit:mainfrom
RoyBA:fix/copilot-sidebar-host-wrapper
Closed

fix(copilot): keep sidebar mode beside host content instead of overlapping it#3026
RoyBA wants to merge 5 commits into
Chainlit:mainfrom
RoyBA:fix/copilot-sidebar-host-wrapper

Conversation

@RoyBA

@RoyBA RoyBA commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

In the Copilot widget's sidebar display mode, the fixed side panel could
render on top of and hide host-page UI on some sites. This replaces the old
"push the host via a document.body margin" approach with a wrapper that
constrains host content so the sidebar sits beside it. Floating mode and the
standalone Chainlit app are unaffected.

Root cause

Sidebar mode renders a viewport-fixed panel (fixed top-0 right-0 h-full z-50).
The previous useSidebarResize only set document.body.style.marginRight to make
room. A body margin only shrinks a host whose width derives from the <body>
content box. On viewport-anchored hosts — position: fixed/absolute inset: 0,
width: 100vw, overflow:hidden + inner scroller, or body { margin: 0 !important }
the margin has no effect, so the fixed sidebar overlaps host elements. That's the
"works on some hosts, overlaps on others" report.

Fix

While the sidebar is open, useSidebarResize now:

  • Moves every <body> child except the widget container (#chainlit-copilot)
    into a controlled #chainlit-copilot-host-wrapper (body's first child), and
    restores the original DOM on close / mode-switch / unmount.
  • Constrains the wrapper: width: calc(100vw - {sidebarWidth}px), height: 100vh,
    box-sizing: border-box, overflow-x: hidden; overflow-y: auto, width transition.
  • Applies transform: translateZ(0) on the wrapper — this makes it the containing
    block for position: fixed/absolute descendants, forcing viewport-anchored
    host layouts to reflow within the reduced width instead of sliding under the sidebar.
  • Zeroes body margins while open (captured per-longhand and restored on close) and
    carries the host scroll position onto the wrapper (restored on close). Runs as a
    useLayoutEffect to avoid a flash of unconstrained content. Drag-resize now
    transitions the wrapper rather than body.

Known tradeoffs

Establishing a containing block changes position: fixed semantics for host
elements (they become wrapper-relative). Hosts with hard-coded 100vw/fixed
widths won't reflow and are clipped to the reduced width (horizontal overflow is
hidden by design). A future opt-in config to target a host-provided app root
(instead of wrapping body) is a reasonable follow-up if any host regresses.

Testing

  • pnpm lint, pnpm format-check pass on both files.
  • Frontend pnpm type-check clean; pnpm --filter @chainlit/copilot build succeeds.
  • E2E: the sidebar-mode Cypress assertions were rewritten to check the wrapper
    (exists, width === 'calc(100vw - 400px)', wrapperWidth + sidebarWidth ≈ innerWidth, removed on close/unmount). Runs in CI.
  • Manual: sidebar mode across a normal-flow host, a fixed inset:0 SPA, and a
    100vw host; with host zoom + non-default root font-size; light/dark; plus
    drag-resize and open → close → unmount (host DOM, margins, and scroll fully restore).

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. bug Something isn't working e2e-tests Has E2E tests frontend Pertains to the frontend. labels Aug 26, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="libs/copilot/src/hooks/useSidebarResize.ts">

<violation number="1" location="libs/copilot/src/hooks/useSidebarResize.ts:50">
P2: When a host relies on direct-body selectors or body flex/grid layout, opening the sidebar changes its DOM hierarchy and can break the host layout. Wrap a host-provided app root instead, or preserve and explicitly support those body-level layout assumptions.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread libs/copilot/src/hooks/useSidebarResize.ts
Comment thread libs/copilot/src/hooks/useSidebarResize.ts
Comment thread libs/copilot/src/hooks/useSidebarResize.ts
Comment thread libs/copilot/src/hooks/useSidebarResize.ts
Comment thread cypress/e2e/copilot/spec.cy.ts
RoyBA and others added 2 commits August 26, 2026 15:01
Address review feedback: restore the wrapper's current scroll on close (not the pre-open position), replay the original body child order so hosts whose widget container isn't the last child keep sibling order, and assert body-margin restoration in the close E2E test.

Co-Authored-By: GitHub Copilot <noreply@github.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread libs/copilot/src/hooks/useSidebarResize.ts Outdated
Comment thread libs/copilot/src/hooks/useSidebarResize.ts Outdated
Comment thread libs/copilot/src/hooks/useSidebarResize.ts Outdated
removeHostWrapper already restores host children to their original order when the widget is the last body child (the mounted case), so the snapshot replay was redundant and could re-insert nodes the host detached while the sidebar was open. Reverts that reorder from review feedback; the live-scroll restore stays.

Co-Authored-By: GitHub Copilot <noreply@github.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="libs/copilot/src/hooks/useSidebarResize.ts">

<violation number="1">
P2: Removing the `originalOrder` replay drops host sibling order preservation for the non-default case. `removeHostWrapper` re-inserts the wrapper's children immediately before the wrapper (which sits ahead of `#chainlit-copilot`), so after close every host child ends up before the copilot container. If the copilot container is not the last `<body>` child (the exact case the deleted comment documents, e.g. host content appended after the widget is mounted), that content gets permanently moved in front of the copilot on close. The commit labels this replay "redundant", but it is only redundant when the copilot is always the last child; otherwise it fixes a real reorder. Consider keeping an order snapshot (or restoring children with `insertBefore(firstChildNull, ...)` semantics that honor the copilot's original position) so host sibling order survives open/close.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

@@ -1,11 +1,20 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Removing the originalOrder replay drops host sibling order preservation for the non-default case. removeHostWrapper re-inserts the wrapper's children immediately before the wrapper (which sits ahead of #chainlit-copilot), so after close every host child ends up before the copilot container. If the copilot container is not the last <body> child (the exact case the deleted comment documents, e.g. host content appended after the widget is mounted), that content gets permanently moved in front of the copilot on close. The commit labels this replay "redundant", but it is only redundant when the copilot is always the last child; otherwise it fixes a real reorder. Consider keeping an order snapshot (or restoring children with insertBefore(firstChildNull, ...) semantics that honor the copilot's original position) so host sibling order survives open/close.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libs/copilot/src/hooks/useSidebarResize.ts, line 184:

<comment>Removing the `originalOrder` replay drops host sibling order preservation for the non-default case. `removeHostWrapper` re-inserts the wrapper's children immediately before the wrapper (which sits ahead of `#chainlit-copilot`), so after close every host child ends up before the copilot container. If the copilot container is not the last `<body>` child (the exact case the deleted comment documents, e.g. host content appended after the widget is mounted), that content gets permanently moved in front of the copilot on close. The commit labels this replay "redundant", but it is only redundant when the copilot is always the last child; otherwise it fixes a real reorder. Consider keeping an order snapshot (or restoring children with `insertBefore(firstChildNull, ...)` semantics that honor the copilot's original position) so host sibling order survives open/close.</comment>

<file context>
@@ -138,10 +138,6 @@ export function useSidebarResize({
     const body = document.body;
     const { scrollX, scrollY } = window;
 
-    // Snapshot the original body child order — the widget container isn't always the
-    // last child, so we restore this exact order on close.
-    const originalOrder = Array.from(body.childNodes);
-
     // Preserve the host's inline body margins so we can restore them on close.
     const previousMargin = {
</file context>

@lamylio

lamylio commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Hey, could we get any pic of prior/after the changes ?
Tip: see gh-image for agents-compatible pictures-in-PR. ;)

Hosts that arrange content at the body level (e.g. body { display: flex/grid } centering) had their content jump when the sidebar opened, because moving it into the wrapper dropped those rules. Copy the body's layout-affecting computed properties onto the wrapper so reparented content lays out unchanged.

Co-Authored-By: GitHub Copilot <noreply@github.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="libs/copilot/src/hooks/useSidebarResize.ts">

<violation number="1" location="libs/copilot/src/hooks/useSidebarResize.ts:52">
P2: When the host defines body padding, this copy applies the padding twice and can make the constrained wrapper overlap the sidebar. Transfer the padding by clearing it on the body while the wrapper is active, then restore it during cleanup.</violation>

<violation number="2" location="libs/copilot/src/hooks/useSidebarResize.ts:67">
P2: When a host uses responsive or fractional body grid tracks, the wrapper freezes the full-width computed tracks instead of reflowing them to its reduced width. Copy the source layout declarations or mirror these properties whenever the viewport or host styles change, rather than keeping one computed snapshot.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

wrapper.id = HOST_WRAPPER_ID;

const bodyStyle = getComputedStyle(body);
HOST_LAYOUT_PROPS.forEach((prop) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a host uses responsive or fractional body grid tracks, the wrapper freezes the full-width computed tracks instead of reflowing them to its reduced width. Copy the source layout declarations or mirror these properties whenever the viewport or host styles change, rather than keeping one computed snapshot.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libs/copilot/src/hooks/useSidebarResize.ts, line 67:

<comment>When a host uses responsive or fractional body grid tracks, the wrapper freezes the full-width computed tracks instead of reflowing them to its reduced width. Copy the source layout declarations or mirror these properties whenever the viewport or host styles change, rather than keeping one computed snapshot.</comment>

<file context>
@@ -40,6 +63,11 @@ function createHostWrapper(): HTMLElement {
   wrapper.id = HOST_WRAPPER_ID;
 
+  const bodyStyle = getComputedStyle(body);
+  HOST_LAYOUT_PROPS.forEach((prop) => {
+    wrapper.style.setProperty(prop, bodyStyle.getPropertyValue(prop));
+  });
</file context>

'grid-auto-rows',
'row-gap',
'column-gap',
'padding',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the host defines body padding, this copy applies the padding twice and can make the constrained wrapper overlap the sidebar. Transfer the padding by clearing it on the body while the wrapper is active, then restore it during cleanup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libs/copilot/src/hooks/useSidebarResize.ts, line 52:

<comment>When the host defines body padding, this copy applies the padding twice and can make the constrained wrapper overlap the sidebar. Transfer the padding by clearing it on the body while the wrapper is active, then restore it during cleanup.</comment>

<file context>
@@ -30,6 +30,29 @@ function getHostWrapper(): HTMLElement | null {
+  'grid-auto-rows',
+  'row-gap',
+  'column-gap',
+  'padding',
+  'text-align'
+];
</file context>

@RoyBA RoyBA closed this Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working e2e-tests Has E2E tests frontend Pertains to the frontend. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants