authorgravatar for 114284668+ifBars@users.noreply.github.comifBars <114284668+ifBars@users.noreply.github.com> 2026-03-25 09:57:09-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-25 16:57:09+00:00
log3289a25e57fe398176bfc85bd6849711ffa40352
treedcf7406a390c44cd45bdebc150ed97c2d0f926cf
parentdf5bde7e112027d5e630ae3ab0705b2a883dc86d
signature Signed by PGP key B5690EEEBB952194

fix(web): prevent provider model submenu overlap (#1403)


2 files changed, 46 insertions(+), 1 deletions(-)

apps/web/src/components/chat/ProviderModelPicker.browser.tsx+45
......@@ -71,6 +71,51 @@ describe("ProviderModelPicker", () => {
7171 }
7272 });
7373
74 it("opens provider submenus with a visible gap from the parent menu", async () => {
75 const mounted = await mountPicker({
76 provider: "claudeAgent",
77 model: "claude-opus-4-6",
78 lockedProvider: null,
79 });
80
81 try {
82 await page.getByRole("button").click();
83 const providerTrigger = page.getByRole("menuitem", { name: "Codex" });
84 await providerTrigger.hover();
85
86 await vi.waitFor(() => {
87 expect(document.body.textContent ?? "").toContain("GPT-5 Codex");
88 });
89
90 const providerTriggerElement = Array.from(
91 document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
92 ).find((element) => element.textContent?.includes("Codex"));
93 if (!providerTriggerElement) {
94 throw new Error("Expected the Codex provider trigger to be mounted.");
95 }
96
97 const providerTriggerRect = providerTriggerElement.getBoundingClientRect();
98 const modelElement = Array.from(
99 document.querySelectorAll<HTMLElement>('[role="menuitemradio"]'),
100 ).find((element) => element.textContent?.includes("GPT-5 Codex"));
101 if (!modelElement) {
102 throw new Error("Expected the submenu model option to be mounted.");
103 }
104
105 const submenuPopup = modelElement.closest('[data-slot="menu-sub-content"]');
106 if (!(submenuPopup instanceof HTMLElement)) {
107 throw new Error("Expected submenu popup to be mounted.");
108 }
109
110 const submenuRect = submenuPopup.getBoundingClientRect();
111
112 expect(submenuRect.left).toBeGreaterThanOrEqual(providerTriggerRect.right);
113 expect(submenuRect.left - providerTriggerRect.right).toBeGreaterThanOrEqual(2);
114 } finally {
115 await mounted.cleanup();
116 }
117 });
118
74119 it("shows models directly when the provider is locked mid-thread", async () => {
75120 const mounted = await mountPicker({
76121 provider: "claudeAgent",
apps/web/src/components/chat/ProviderModelPicker.tsx+1-1
......@@ -153,7 +153,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
153153 />
154154 {option.label}
155155 </MenuSubTrigger>
156 <MenuSubPopup className="[--available-height:min(24rem,70vh)]">
156 <MenuSubPopup className="[--available-height:min(24rem,70vh)]" sideOffset={4}>
157157 <MenuGroup>
158158 <MenuRadioGroup
159159 value={props.provider === option.value ? props.model : ""}