Skip to content

🩹 fix: Sync ControlCombobox popover width with trigger after layout changes#12887

Merged
danny-avila merged 3 commits into
danny-avila:devfrom
ethanlaj:agent-builder-select-render-bug/to-dev
Apr 30, 2026
Merged

🩹 fix: Sync ControlCombobox popover width with trigger after layout changes#12887
danny-avila merged 3 commits into
danny-avila:devfrom
ethanlaj:agent-builder-select-render-bug/to-dev

Conversation

@ethanlaj

Copy link
Copy Markdown
Contributor

Summary

Fixes a UI bug where the Agent Builder's agent selector dropdown renders in the wrong place (anchored to the very far left of the viewport, only ~26px wide, with no options fully visible) under the following scenario:

  1. Close the sidebar.
  2. Reload the page (sidebar correctly remains closed).
  3. Click the Agent Builder icon to open the agent builder panel.
  4. Click the agent selector / dropdown.

Root cause

packages/client/src/components/ControlCombobox.tsx measures the trigger's offsetWidth once on mount via useEffect, and applies that as the inline width of the Ariakit SelectPopover. When the Agent Builder side panel mounts after a page reload with the sidebar collapsed, the trigger button is measured during the side panel's expansion transition (~26px) and is never re-measured. The popover is then rendered against that stale width, causing the misplacement.

Fix

Use a ResizeObserver on the trigger button to keep buttonWidth in sync with the trigger's actual rendered width. The observer is disconnected on unmount, and the implementation gracefully no-ops in environments without ResizeObserver.

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

Manual reproduction (before the fix)

  1. Close the sidebar in LibreChat.
  2. Reload the page — sidebar stays closed.
  3. Click the Agent Builder icon.
  4. Click the Agent dropdown — observe that the popover renders at the far left of the viewport, ~26px wide, options not visible.

Manual verification (after the fix)

Repeated the exact same steps:

  • Popover renders directly under the trigger button.
  • Popover width matches the trigger width (275px) and shows all agents and the search input correctly.
  • Verified via document.querySelector('.animate-popover') that style.width === '275px' and getBoundingClientRect().left === trigger.left.

Automated regression tests

Added packages/client/src/components/ControlCombobox.spec.tsx:

  • uses the button width measured on mount when layout is stable — sanity check that the existing on-mount measurement still works.
  • updates the popover width when the trigger resizes after mount (regression: agent select dropdown rendering at narrow width) — directly reproduces the bug: simulates the trigger being measured at 26px on mount, asserts the popover starts at 26px, then fires a ResizeObserver callback at 275px and asserts the popover updates to 275px.
  • disconnects the ResizeObserver on unmount — ensures no observer leak.

Run with:

cd packages/client && npx jest ControlCombobox.spec

All 3 tests pass.

Test Configuration:

  • Node.js: v20+
  • LibreChat docker stack: docker compose up -d --build
  • Browser verification via Playwright at http://localhost:3080

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes
  • Any changes dependent on mine have been merged and published in downstream modules.

…hanges

The popover width was measured once on mount via offsetWidth. When the agent builder side panel opens after a page reload with the sidebar collapsed, the trigger button is initially measured during the layout transition (~26px) and never re-measured, leaving the agent select dropdown rendered at the far left with no options fully visible.

Use a ResizeObserver to keep buttonWidth in sync with the trigger's actual width whenever it resizes, then disconnect on unmount.
Copilot AI review requested due to automatic review settings April 30, 2026 01:50

Copilot AI 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.

Pull request overview

Fixes a ControlCombobox sizing bug in the client UI where the Agent Builder selector popover could render at an incorrect (stale) width after layout transitions, by keeping the popover width synchronized with the trigger’s rendered width.

Changes:

  • Replace one-time offsetWidth measurement with a ResizeObserver that updates buttonWidth as the trigger resizes.
  • Add Jest regression tests covering initial sizing, resize updates, and observer cleanup on unmount.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
packages/client/src/components/ControlCombobox.tsx Observes trigger width changes and updates popover inline width accordingly.
packages/client/src/components/ControlCombobox.spec.tsx Adds regression/unit tests to validate popover width sync behavior and observer disconnection.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@ethanlaj

Copy link
Copy Markdown
Contributor Author

@danny-avila ready for review!

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…width branches

Address review feedback:
- Use button.offsetWidth as the ResizeObserver fallback instead of
  entry.contentRect.width to avoid a content-box vs border-box mismatch in
  pre-2022 browsers that ship ResizeObserver without borderBoxSize.
- Add tests for the three previously-untested branches: isCollapsed=true
  (no observation of the trigger), ResizeObserver unavailable (sync-only
  measurement), and zero-width entries (state unchanged).
Add a test that drives the ResizeObserver callback with borderBoxSize
absent and divergent contentRect.width vs offsetWidth (251 vs 275).
The fix would silently revert to entry.contentRect.width without this
test failing, so this pins the chosen fallback semantics.
@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila danny-avila merged commit 781bfb8 into danny-avila:dev Apr 30, 2026
10 checks passed
fuuuzzy pushed a commit to fuuuzzy/LibreChat that referenced this pull request May 3, 2026
…hanges (danny-avila#12887)

* 🩹 fix: Sync ControlCombobox popover width with trigger after layout changes

The popover width was measured once on mount via offsetWidth. When the agent builder side panel opens after a page reload with the sidebar collapsed, the trigger button is initially measured during the layout transition (~26px) and never re-measured, leaving the agent select dropdown rendered at the far left with no options fully visible.

Use a ResizeObserver to keep buttonWidth in sync with the trigger's actual width whenever it resizes, then disconnect on unmount.

* test: cover ControlCombobox isCollapsed, no-ResizeObserver, and zero-width branches

Address review feedback:
- Use button.offsetWidth as the ResizeObserver fallback instead of
  entry.contentRect.width to avoid a content-box vs border-box mismatch in
  pre-2022 browsers that ship ResizeObserver without borderBoxSize.
- Add tests for the three previously-untested branches: isCollapsed=true
  (no observation of the trigger), ResizeObserver unavailable (sync-only
  measurement), and zero-width entries (state unchanged).

* test: lock the button.offsetWidth fallback against revert

Add a test that drives the ResizeObserver callback with borderBoxSize
absent and divergent contentRect.width vs offsetWidth (251 vs 275).
The fix would silently revert to entry.contentRect.width without this
test failing, so this pins the chosen fallback semantics.

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
jcbartle pushed a commit to jcbartle/LibreChat that referenced this pull request May 11, 2026
…hanges (danny-avila#12887)

* 🩹 fix: Sync ControlCombobox popover width with trigger after layout changes

The popover width was measured once on mount via offsetWidth. When the agent builder side panel opens after a page reload with the sidebar collapsed, the trigger button is initially measured during the layout transition (~26px) and never re-measured, leaving the agent select dropdown rendered at the far left with no options fully visible.

Use a ResizeObserver to keep buttonWidth in sync with the trigger's actual width whenever it resizes, then disconnect on unmount.

* test: cover ControlCombobox isCollapsed, no-ResizeObserver, and zero-width branches

Address review feedback:
- Use button.offsetWidth as the ResizeObserver fallback instead of
  entry.contentRect.width to avoid a content-box vs border-box mismatch in
  pre-2022 browsers that ship ResizeObserver without borderBoxSize.
- Add tests for the three previously-untested branches: isCollapsed=true
  (no observation of the trigger), ResizeObserver unavailable (sync-only
  measurement), and zero-width entries (state unchanged).

* test: lock the button.offsetWidth fallback against revert

Add a test that drives the ResizeObserver callback with borderBoxSize
absent and divergent contentRect.width vs offsetWidth (251 vs 275).
The fix would silently revert to entry.contentRect.width without this
test failing, so this pins the chosen fallback semantics.

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants