From 082b90c5a9a5b05208379927f8205cb26989a3dd Mon Sep 17 00:00:00 2001 From: badcuban <108198679+badcuban@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:35:07 -0400 Subject: [PATCH] fix(web): preserve composer model labels before overflow --- apps/web/src/components/ChatView.browser.tsx | 78 ++++++++++++------- apps/web/src/components/chat/ChatComposer.tsx | 4 +- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index 65231ec2..845a53d4 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -1746,6 +1746,37 @@ function findComposerProviderModelPicker(): HTMLButtonElement | null { return document.querySelector('[data-chat-provider-model-picker="true"]'); } +function findVisibleComposerFooter(): HTMLElement | null { + return ( + Array.from(document.querySelectorAll('[data-chat-composer-footer="true"]')).find( + (candidate) => candidate.getBoundingClientRect().width > 0, + ) ?? null + ); +} + +function findVisibleComposerForm(): HTMLFormElement | null { + return ( + Array.from(document.querySelectorAll('[data-chat-composer-form="true"]')).find( + (candidate) => candidate.getBoundingClientRect().width > 0, + ) ?? null + ); +} + +async function setVisibleComposerFormWidth(targetWidth: number): Promise { + // Resize the element observed by ChatComposer. Shell chrome has a larger + // min-content width in some renderers and is not part of this footer contract. + const composerForm = await waitForElement( + findVisibleComposerForm, + "Unable to find visible composer form.", + ); + composerForm.style.width = `${targetWidth}px`; + composerForm.style.maxWidth = `${targetWidth}px`; + await waitForLayout(); + await vi.waitFor(() => { + expect(composerForm.clientWidth).toBe(targetWidth); + }); +} + function findButtonByText(text: string): HTMLButtonElement | null { return (Array.from(document.querySelectorAll("button")).find( (button) => button.textContent?.trim() === text, @@ -9146,20 +9177,20 @@ describe("ChatView timeline estimator parity (full app)", () => { }); const footer = await waitForElement( - () => document.querySelector('[data-chat-composer-footer="true"]'), + findVisibleComposerFooter, "Unable to find composer footer.", ); const leftActions = await waitForElement( - () => document.querySelector('[data-chat-composer-actions="left"]'), + () => footer.querySelector('[data-chat-composer-actions="left"]'), "Unable to find left composer actions.", ); const rightActions = await waitForElement( - () => document.querySelector('[data-chat-composer-actions="right"]'), + () => footer.querySelector('[data-chat-composer-actions="right"]'), "Unable to find right composer actions.", ); const moreControlsButton = await waitForElement( () => - document.querySelector('button[aria-label="More composer controls"]'), + footer.querySelector('button[aria-label="More composer controls"]'), "Unable to find compact composer controls.", ); @@ -9194,7 +9225,7 @@ describe("ChatView timeline estimator parity (full app)", () => { } }); - it("protects model and reasoning labels before shrinking interaction and access", async () => { + it("keeps model and reasoning labels while secondary controls are icon-only", async () => { const mounted = await mountChatView({ viewport: WIDE_FOOTER_VIEWPORT, snapshot: fixture.snapshot, @@ -9235,28 +9266,26 @@ describe("ChatView timeline estimator parity (full app)", () => { try { await waitForServerConfigToApply(); - await mounted.setContainerSize({ - width: 900, - height: WIDE_FOOTER_VIEWPORT.height, - }); + await setVisibleComposerFormWidth(560); const footer = await waitForElement( - () => document.querySelector('[data-chat-composer-footer="true"]'), + findVisibleComposerFooter, "Unable to find composer footer.", ); + await vi.waitFor(() => { + expect(footer.dataset.chatComposerFooterIconOnly).toBe("true"); + expect(footer.dataset.chatComposerFooterCompact).toBe("false"); + }); const modelButton = await waitForElement( - findComposerProviderModelPicker, + () => footer.querySelector('[data-chat-provider-model-picker="true"]'), "Unable to find provider model picker.", ); const reasoningButton = await waitForButtonByTextWithin(footer, "Medium"); - const interactionButton = await waitForButtonByTextWithin(footer, "Build"); - const accessButton = await waitForButtonByTextWithin(footer, "Full access"); - - expect(footer.dataset.chatComposerFooterIconOnly).toBe("false"); - expect(getComputedStyle(modelButton).flexShrink).toBe("0"); - expect(getComputedStyle(reasoningButton).flexShrink).toBe("0"); - expect(getComputedStyle(interactionButton).flexShrink).toBe("1"); - expect(getComputedStyle(accessButton).flexShrink).toBe("1"); + expect(modelButton.textContent).toContain("GPT-5"); + expect(reasoningButton.textContent).toBe("Medium"); + expect(modelButton.getBoundingClientRect().width).toBeGreaterThan(32); + expect(reasoningButton.getBoundingClientRect().width).toBeGreaterThan(32); + expect(footer.scrollWidth).toBeLessThanOrEqual(footer.clientWidth + 1); } finally { await mounted.cleanup(); } @@ -9269,13 +9298,10 @@ describe("ChatView timeline estimator parity (full app)", () => { }); try { - await mounted.setContainerSize({ - width: 760, - height: WIDE_FOOTER_VIEWPORT.height, - }); + await setVisibleComposerFormWidth(560); const footer = await waitForElement( - () => document.querySelector('[data-chat-composer-footer="true"]'), + findVisibleComposerFooter, "Unable to find composer footer.", ); await vi.waitFor(() => { @@ -9283,11 +9309,11 @@ describe("ChatView timeline estimator parity (full app)", () => { expect(footer.dataset.chatComposerFooterCompact).toBe("false"); }); const interactionButton = await waitForElement( - () => document.querySelector('button[aria-label^="Interaction mode:"]'), + () => footer.querySelector('button[aria-label^="Interaction mode:"]'), "Unable to find icon-only interaction mode.", ); const accessButton = await waitForElement( - () => document.querySelector('button[aria-label^="Access level:"]'), + () => footer.querySelector('button[aria-label^="Access level:"]'), "Unable to find icon-only access level.", ); const iconButtons = [interactionButton, accessButton]; diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 34eac766..6dba5398 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -1498,7 +1498,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) model: selectedModel, models: selectedProviderModels, modelOptions: composerModelOptions?.[selectedInstanceId], - iconOnly: isComposerFooterIconOnly, + iconOnly: isComposerFooterCompact, }); const pendingPrimaryAction = useMemo( () => @@ -3622,7 +3622,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) )} >