Skip to content

feat: Add copilot sidebar mode with drag-to-resize#2785

Merged
hayescode merged 13 commits intoChainlit:mainfrom
EyalAmitay:feat/copilot-sidebar-with-resize
Apr 3, 2026
Merged

feat: Add copilot sidebar mode with drag-to-resize#2785
hayescode merged 13 commits intoChainlit:mainfrom
EyalAmitay:feat/copilot-sidebar-with-resize

Conversation

@EyalAmitay
Copy link
Copy Markdown
Contributor

@EyalAmitay EyalAmitay commented Feb 18, 2026

Summary

Adds a new sidebar display mode for the copilot widget. Instead of the default floating popover, the copilot can now render as a full-height side panel anchored to the right edge of the viewport, pushing the host page content to make room. Users can drag the left edge to resize and switch between sidebar and floating modes via a dropdown in the header.

image

Changes

  • Sidebar display mode: Right-side sidebar panel as an alternative to the floating popover
  • Drag-to-resize: Left-edge drag handle (300px min, 50% viewport max) with smooth transitions
  • Display mode toggle: Header dropdown to switch between "Floating" and "Sidebar"; close button in sidebar mode
  • Persistence: Sidebar width and display mode saved to localStorage across sessions
  • Config-over-localStorage precedence: Explicit displayMode passed via mountChainlitWidget() takes priority over a stale localStorage value; localStorage is only used as a fallback when displayMode is omitted
  • resolveDisplayMode helper: Extracted display mode resolution logic into a standalone pure function (libs/copilot/src/resolveDisplayMode.ts)
  • useSidebarResize hook: Encapsulates resize logic, body margin management, and drag lifecycle (including window.blur handling for edge cases)
  • DisplayMode type: Consolidated into types.ts, replacing inline string unions

Backward Compatibility

Fully backward compatible — no breaking changes to any existing API or behavior.

  • The displayMode option in IWidgetConfig is additive and optional; defaults to 'floating' when not specified, preserving existing behavior
  • Existing mountChainlitWidget() calls without displayMode work unchanged
  • unmountChainlitWidget() continues to work as before
  • Two new localStorage keys are introduced (chainlit-copilot-displayMode, chainlit-copilot-sidebarWidth) — only read/written when the feature is used, no effect on existing deployments

Configuration

To default to the sidebar mode:

window.mountChainlitWidget({
  chainlitServer: 'http://localhost:8000',
  displayMode: 'sidebar',
});

Test plan

  • E2E: Sidebar opens and pushes body content
  • E2E: Sidebar closes and restores body margin
  • E2E: Drag handle resizes sidebar and updates body margin
  • E2E: Switch between sidebar and floating mode
  • E2E: Unmount cleanup restores body margin
  • E2E: localStorage persistence survives widget remount
  • Unit: config displayMode takes precedence over localStorage
  • Unit: localStorage fallback when config omits displayMode
  • Unit: defaults to floating when neither config nor localStorage is set
  • Build and lint pass
  • Manual: Open, drag left edge to resize
  • Manual: State persists in localstorage
Screen.Recording.2026-02-18.at.5.54.15.PM.mov
image

EyalAmitay and others added 9 commits February 18, 2026 13:37
Add sidebar display mode as an alternative to the floating popover.
The sidebar opens as a fixed panel on the right edge with a drag handle
on the left edge for Notion-style resize (300px min, 50% viewport max).
Width persists to localStorage. Includes display mode toggle in header
and E2E tests for sidebar open/close/resize/mode-switch.
- Fix stale originalMargin capture by splitting into lifecycle + sync effects
  so the true original margin is captured once on open via ref, not re-captured
  on every width change
- Fix userSelect not restored on unmount when drag is in progress
- Fix CSS transition lag during drag by disabling transition on mousedown
  and re-enabling on mouseup
- Add window.blur handler to cancel stuck drags when mouse released outside browser
- Extract resize logic into useSidebarResize hook, consolidate DisplayMode type
- Deduplicate chatContent and renderButtonIcon in widget.tsx
- Add E2E tests for unmount cleanup and localStorage persistence across remounts
Replace drag-based resize test with localStorage pre-set approach.
The Cypress trigger-based drag simulation was unreliable in CI headless
mode, causing the test to fail with 'expected 400 to be above 400'.
- Add copilot.displayMode translation keys to all 20 language files
- Use useTranslation in Header for Floating/Sidebar dropdown labels
- Remove unnecessary code comments across modified files
- Remove backward-compat regression: stop clearing body margin/transition
  in unmountChainlitWidget (React cleanup handles it)
- Guard localStorage write to only persist sidebar width in sidebar mode
- Gate mousemove/mouseup/blur listeners on sidebar mode and open state
- Remove hidden test-only button from sidebar production code
- Remove renderButtonIcon and inline icon in sidebar closed-state button
- Revert unrelated formatting changes in test file
- Remove unnecessary non-null assertions in tests
Disable Radix UI's modal scroll-lock on the display mode DropdownMenu,
which was adding overflow:hidden and padding-right to document.body.
@EyalAmitay EyalAmitay marked this pull request as ready for review February 18, 2026 15:33
@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. frontend Pertains to the frontend. labels Feb 18, 2026
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

No issues found across 6 files

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 5, 2026

This PR is stale because it has been open for 14 days with no activity.

@github-actions github-actions bot added the stale Issue has not had recent activity or appears to be solved. Stale issues will be automatically closed label Mar 5, 2026
@EyalAmitay
Copy link
Copy Markdown
Contributor Author

@hayescode

@github-actions github-actions bot removed the stale Issue has not had recent activity or appears to be solved. Stale issues will be automatically closed label Mar 6, 2026
@EyalAmitay
Copy link
Copy Markdown
Contributor Author

@asvishnyakov

Copy link
Copy Markdown
Contributor

@hayescode hayescode left a comment

Choose a reason for hiding this comment

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

Thanks @EyalAmitay. I like the feature, but I can't approve it yet because the persisted display mode currently overrides an explicit displayMode passed by the host page. Please flip that precedence and add a focused regression test for config-vs-localStorage precedence, then this should be in good shape.

Comment thread libs/copilot/src/widget.tsx Outdated
const [isOpen, setIsOpen] = useState(config?.opened || false);
const [displayMode, setDisplayMode] = useState<DisplayMode>(
() =>
(localStorage.getItem(LS_DISPLAY_MODE_KEY) as DisplayMode) ||
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.

This initialization order lets a previously saved localStorage value override an explicit mountChainlitWidget({ displayMode }) setting. That means an embed can ask for sidebar and still come up floating because of an older user preference. Can we flip this so the explicit config wins, and only fall back to localStorage when displayMode is omitted?

Copy link
Copy Markdown
Contributor Author

@EyalAmitay EyalAmitay Apr 3, 2026

Choose a reason for hiding this comment

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

Fixed - flipped the precedence so config.displayMode wins over localStorage. Also extracted the resolution logic into resolveDisplayMode() and added a focused regression test covering all three cases (config wins over localStorage, localStorage fallback when config omits displayMode, default to floating).

Flip initialization order so an explicit displayMode passed via
mountChainlitWidget() wins over a stale localStorage value.
Add focused regression test for config-vs-localStorage precedence.
@EyalAmitay
Copy link
Copy Markdown
Contributor Author

EyalAmitay commented Apr 3, 2026

@hayescode Thanks for the review! Both items addressed:

  1. Precedence flipped -config.displayMode now takes priority over localStorage. localStorage is only used as a fallback when no explicit config is passed.
  2. Regression test added - frontend/tests/displayModePrecedence.spec.ts covers config-wins-over-localStorage, localStorage-fallback, and default-to-floating cases.

Extracted the resolution logic into a standalone resolveDisplayMode() function (libs/copilot/src/resolveDisplayMode.ts) to keep the test focused and free of component dependencies.

@EyalAmitay EyalAmitay requested a review from hayescode April 3, 2026 11:47
@hayescode hayescode added this pull request to the merge queue Apr 3, 2026
Merged via the queue into Chainlit:main with commit 08dc94e Apr 3, 2026
10 checks passed
>
<div id="chainlit-copilot-chat" className="flex flex-col h-full w-full">
{error ? (
<Alert variant="error">{error}</Alert>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If the widget encountered an error (like a network failure), the original PR logic used error ? - :

..., meaning it entirely skipped rendering the component when an error was present.

In the old "floating" popover mode, this is somewhat okay because the user can click outside the popover to close it. However, this new "sidebar" mode renders as a fixed, full-height element with no click-outside handler. Without the

component (which contains the close button), a user who encountered an error while in sidebar mode would be left permanently stuck with an uncloseable error overlay covering the side of their screen.

So, it may be a good idea to remove this ---error ? (alert variant=""error" ... --- line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

3 participants