From 91bfdeb47cdc00a0666675746e53caf3d6120fd3 Mon Sep 17 00:00:00 2001 From: 0xPratik Date: Mon, 31 Aug 2026 18:18:21 +0545 Subject: [PATCH 1/4] Add a regression test for composer action alignment --- packages/chat-ui/test/composer.test.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/chat-ui/test/composer.test.tsx b/packages/chat-ui/test/composer.test.tsx index 0cbbdc192..2d60daac0 100644 --- a/packages/chat-ui/test/composer.test.tsx +++ b/packages/chat-ui/test/composer.test.tsx @@ -277,7 +277,7 @@ describe("Composer keyboard hint", () => { expect(keyboardHint()?.getAttribute("aria-hidden")).toBe("false"); expect(keyboardHint()?.textContent).toBe("Enter to send"); expect( - container?.querySelectorAll(".chat-composer-actions > button").length, + container?.querySelectorAll(".chat-composer-actions button").length, ).toBe(2); expect(container?.querySelector(".chat-composer-row > textarea")).toBe( textarea(), @@ -738,6 +738,14 @@ function mountStoppable( } describe("Composer stop affordance (CL-7201)", () => { + test("keeps Stop and Send together in the right-aligned action group", () => { + mountStoppable(true, () => undefined); + + const actions = container?.querySelector(".chat-composer-actions"); + expect(actions?.children).toHaveLength(3); + expect(stopButton().parentElement).toBe(sendButton().parentElement); + }); + test("renders no Stop button when no turn is running", () => { mountStoppable(false, () => undefined); expect(container?.querySelector('[aria-label="Stop"]')).toBeNull(); From 2d9c2ccee0cbcb2dcf1654e428e767d6c81d0c80 Mon Sep 17 00:00:00 2001 From: 0xPratik Date: Mon, 31 Aug 2026 18:18:24 +0545 Subject: [PATCH 2/4] Keep composer submit controls aligned while stopping --- packages/chat-ui/src/composer.tsx | 70 +++++++++++++++++-------------- packages/chat-ui/src/styles.css | 6 +++ 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/packages/chat-ui/src/composer.tsx b/packages/chat-ui/src/composer.tsx index 06cefa1f9..653e52902 100644 --- a/packages/chat-ui/src/composer.tsx +++ b/packages/chat-ui/src/composer.tsx @@ -960,44 +960,50 @@ export const Composer = forwardRef< > {CHAT_STRINGS.composerKeyboardHint} - {canStopComposer({ running }) ? ( +
+ {canStopComposer({ running }) ? ( + + ) : null} - ) : null} - +
Date: Wed, 2 Sep 2026 16:44:23 +0545 Subject: [PATCH 3/4] Cover queued composer sends while stopping --- packages/chat-ui/test/composer.test.tsx | 37 +++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/chat-ui/test/composer.test.tsx b/packages/chat-ui/test/composer.test.tsx index 2d60daac0..70165613e 100644 --- a/packages/chat-ui/test/composer.test.tsx +++ b/packages/chat-ui/test/composer.test.tsx @@ -700,6 +700,8 @@ function stopButton(): HTMLButtonElement { function mountStoppable( running: boolean, onStop: () => void | Promise, + onSend: (payload: ComposerSendPayload) => Promise = () => + Promise.resolve(true), ) { container = document.createElement("div"); document.body.appendChild(container); @@ -708,7 +710,7 @@ function mountStoppable( root?.render( createElement(Composer, { agents: [], - onSend: () => Promise.resolve(true), + onSend, onInviteAgent: () => undefined, onOpenAgentsSettings: () => undefined, onCreateRoutineInSpace: () => undefined, @@ -724,7 +726,7 @@ function mountStoppable( root?.render( createElement(Composer, { agents: [], - onSend: () => Promise.resolve(true), + onSend, onInviteAgent: () => undefined, onOpenAgentsSettings: () => undefined, onCreateRoutineInSpace: () => undefined, @@ -742,8 +744,37 @@ describe("Composer stop affordance (CL-7201)", () => { mountStoppable(true, () => undefined); const actions = container?.querySelector(".chat-composer-actions"); + const submitActions = stopButton().parentElement; expect(actions?.children).toHaveLength(3); - expect(stopButton().parentElement).toBe(sendButton().parentElement); + expect( + submitActions?.classList.contains("chat-composer-submit-actions"), + ).toBe(true); + expect(submitActions?.children).toHaveLength(2); + expect(submitActions?.lastElementChild).toBe(sendButton()); + expect(actions?.lastElementChild).toBe(submitActions); + }); + + test("keeps queued sends available while Stop is visible", async () => { + const sent: ComposerSendPayload[] = []; + mountStoppable( + true, + () => undefined, + (payload) => { + sent.push(payload); + return Promise.resolve(true); + }, + ); + + typeInto(textarea(), "follow-up"); + await settle(); + + expect(sendButton().hasAttribute("disabled")).toBe(false); + act(() => { + sendButton().click(); + }); + await settle(); + + expect(sent).toEqual([{ text: "follow-up", attachments: [] }]); }); test("renders no Stop button when no turn is running", () => { From c4c8cf023d4d2d63080d22e1361ee80e07fec182 Mon Sep 17 00:00:00 2001 From: 0xPratik Date: Thu, 3 Sep 2026 14:14:31 +0545 Subject: [PATCH 4/4] Add composer action accessibility coverage --- packages/chat-ui/test/composer.test.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/chat-ui/test/composer.test.tsx b/packages/chat-ui/test/composer.test.tsx index 70165613e..3e8a7acc7 100644 --- a/packages/chat-ui/test/composer.test.tsx +++ b/packages/chat-ui/test/composer.test.tsx @@ -754,6 +754,13 @@ describe("Composer stop affordance (CL-7201)", () => { expect(actions?.lastElementChild).toBe(submitActions); }); + test("gives Stop and Send distinct accessible names while both are visible", () => { + mountStoppable(true, () => undefined); + + expect(stopButton().getAttribute("aria-label")).toBe("Stop"); + expect(sendButton().getAttribute("aria-label")).toBe("Send"); + }); + test("keeps queued sends available while Stop is visible", async () => { const sent: ComposerSendPayload[] = []; mountStoppable(