diff --git a/ui/src/ui/e2e/chat-flow.e2e.test.ts b/ui/src/ui/e2e/chat-flow.e2e.test.ts index 9764ce00273..00549a87bab 100644 --- a/ui/src/ui/e2e/chat-flow.e2e.test.ts +++ b/ui/src/ui/e2e/chat-flow.e2e.test.ts @@ -297,6 +297,11 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { await page.getByRole("button", { name: "Send message" }).click(); const sendRequest = await gateway.waitForRequest("chat.send"); + await expect + .poll(() => page.locator(".agent-chat__composer-combobox textarea").inputValue(), { + timeout: 10_000, + }) + .toBe(""); const params = requireRecord(sendRequest.params); expect(params.message).toBe(prompt); expect(params.sessionKey).toBe("global"); @@ -377,6 +382,11 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { await page.getByRole("button", { name: "Send message" }).click(); const sendRequest = await gateway.waitForRequest("chat.send"); + await expect + .poll(() => page.locator(".agent-chat__composer-combobox textarea").inputValue(), { + timeout: 10_000, + }) + .toBe(""); const params = requireRecord(sendRequest.params); const runId = requireString(params.idempotencyKey, "chat send idempotency key"); diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 47c7ad88eea..d0cfab509a7 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -1144,6 +1144,31 @@ describe("chat slash menu accessibility", () => { expect(onSend).toHaveBeenCalledTimes(1); }); + it("clears the visible local draft immediately when send clears the host draft", () => { + let draft = ""; + const container = document.createElement("div"); + const onDraftChange = vi.fn((next: string) => { + draft = next; + }); + const onSend = vi.fn(() => { + draft = ""; + }); + const renderWithDraft = () => { + render( + renderChat(createChatProps({ draft, getDraft: () => draft, onDraftChange, onSend })), + container, + ); + }; + + renderWithDraft(); + inputDraft(container, "submitted message"); + container.querySelector(".chat-send-btn")!.click(); + + expect(onDraftChange).toHaveBeenCalledWith("submitted message"); + expect(onSend).toHaveBeenCalledTimes(1); + expect(container.querySelector("textarea")?.value).toBe(""); + }); + it("commits local draft input before Enter sends", () => { const onDraftChange = vi.fn(); const onSend = vi.fn(); diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts index a56b7d27cc7..c6f15007e3d 100644 --- a/ui/src/ui/views/chat.ts +++ b/ui/src/ui/views/chat.ts @@ -1388,6 +1388,7 @@ export function renderChat(props: ChatProps) { }; const draftMirror = getComposerDraftMirror(props); const visibleDraft = draftMirror.value; + let composerTextarea: HTMLTextAreaElement | null = null; const pinned = getPinnedMessages(props.sessionKey); const deleted = getDeletedMessages(props.sessionKey); const hasAttachments = (props.attachments?.length ?? 0) > 0; @@ -1624,6 +1625,21 @@ export function renderChat(props: ChatProps) { `; + const syncComposerDraftAfterSend = (target: HTMLTextAreaElement | null) => { + const hostDraft = props.getDraft?.(); + if (typeof hostDraft !== "string") { + return; + } + // Sends can clear the host draft synchronously before Lit rerenders; keep + // the local mirror aligned so the submitted text does not stay editable. + draftMirror.hostDraft = hostDraft; + draftMirror.value = hostDraft; + if (target && target.value !== hostDraft) { + target.value = hostDraft; + adjustTextareaHeight(target); + } + }; + const handleKeyDown = (e: KeyboardEvent) => { // Slash menu navigation — arg mode if (vs.slashMenuOpen && vs.slashMenuMode === "args" && vs.slashMenuArgItems.length > 0) { @@ -1743,6 +1759,7 @@ export function renderChat(props: ChatProps) { const target = e.target as HTMLTextAreaElement; commitComposerDraft(props, target.value); props.onSend(); + syncComposerDraftAfterSend(target); } } }; @@ -1764,6 +1781,7 @@ export function renderChat(props: ChatProps) { const handleSend = () => { commitComposerDraft(props, draftMirror.value); props.onSend(); + syncComposerDraftAfterSend(composerTextarea); }; const slashMenuVisible = isSlashMenuVisible(); const activeSlashMenuOptionId = getActiveSlashMenuOptionId(); @@ -1899,7 +1917,12 @@ export function renderChat(props: ChatProps) {