feat: Add copilot sidebar mode with drag-to-resize#2785
feat: Add copilot sidebar mode with drag-to-resize#2785hayescode merged 13 commits intoChainlit:mainfrom
Conversation
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
…nts" This reverts commit c90790d.
Disable Radix UI's modal scroll-lock on the display mode DropdownMenu, which was adding overflow:hidden and padding-right to document.body.
|
This PR is stale because it has been open for 14 days with no activity. |
hayescode
left a comment
There was a problem hiding this comment.
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.
| const [isOpen, setIsOpen] = useState(config?.opened || false); | ||
| const [displayMode, setDisplayMode] = useState<DisplayMode>( | ||
| () => | ||
| (localStorage.getItem(LS_DISPLAY_MODE_KEY) as DisplayMode) || |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
@hayescode Thanks for the review! Both items addressed:
Extracted the resolution logic into a standalone |
| > | ||
| <div id="chainlit-copilot-chat" className="flex flex-col h-full w-full"> | ||
| {error ? ( | ||
| <Alert variant="error">{error}</Alert> |
There was a problem hiding this comment.
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.
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.
Changes
displayModepassed viamountChainlitWidget()takes priority over a stale localStorage value; localStorage is only used as a fallback whendisplayModeis omittedresolveDisplayModehelper: Extracted display mode resolution logic into a standalone pure function (libs/copilot/src/resolveDisplayMode.ts)useSidebarResizehook: Encapsulates resize logic, body margin management, and drag lifecycle (includingwindow.blurhandling for edge cases)DisplayModetype: Consolidated intotypes.ts, replacing inline string unionsBackward Compatibility
Fully backward compatible — no breaking changes to any existing API or behavior.
displayModeoption inIWidgetConfigis additive and optional; defaults to'floating'when not specified, preserving existing behaviormountChainlitWidget()calls withoutdisplayModework unchangedunmountChainlitWidget()continues to work as beforechainlit-copilot-displayMode,chainlit-copilot-sidebarWidth) — only read/written when the feature is used, no effect on existing deploymentsConfiguration
To default to the sidebar mode:
Test plan
displayModetakes precedence over localStoragedisplayModefloatingwhen neither config nor localStorage is setScreen.Recording.2026-02-18.at.5.54.15.PM.mov