diff --git a/webui/src/components/thread/ThreadComposer.tsx b/webui/src/components/thread/ThreadComposer.tsx index 7571e517..973cbdf2 100644 --- a/webui/src/components/thread/ThreadComposer.tsx +++ b/webui/src/components/thread/ThreadComposer.tsx @@ -1304,6 +1304,7 @@ export function ThreadComposer({ function onKeyDown(event: KeyboardEvent): void { if (!isVoiceShortcutDown(event) || event.repeat || voiceShortcutDownRef.current) return; event.preventDefault(); + secondEnterPromptIdRef.current = null; voiceShortcutDownRef.current = true; voiceRecorder.beginShortcutHold(); } diff --git a/webui/src/tests/thread-composer.test.tsx b/webui/src/tests/thread-composer.test.tsx index 239ccdbb..5baf3063 100644 --- a/webui/src/tests/thread-composer.test.tsx +++ b/webui/src/tests/thread-composer.test.tsx @@ -1581,9 +1581,41 @@ describe("ThreadComposer", () => { fireEvent.keyDown(input, { key: "Enter" }); expect(onSend).toHaveBeenCalledWith("send this guidance now"); + expect(onSend).toHaveBeenCalledTimes(1); expect(screen.queryByText("send this guidance now")).not.toBeInTheDocument(); }); + it("disarms the second Enter shortcut when keyboard voice recording starts", async () => { + mockVoiceRecorder(); + const onSend = vi.fn(); + const onTranscribeAudio = vi.fn(async () => "voice guidance"); + render( + , + ); + + const input = screen.getByLabelText("Message input"); + fireEvent.change(input, { target: { value: "keep this queued" } }); + fireEvent.keyDown(input, { key: "Enter" }); + fireEvent.keyDown(window, { code: "KeyD", ctrlKey: true, key: "D", shiftKey: true }); + + expect(await screen.findByLabelText("Recording 0:00")).toBeInTheDocument(); + expect(input).toHaveFocus(); + fireEvent.keyDown(input, { key: "Enter" }); + + expect(onSend).not.toHaveBeenCalled(); + expect(screen.getByText("keep this queued")).toBeInTheDocument(); + + await waitForVoiceCapture(); + fireEvent.keyUp(window, { code: "KeyD", ctrlKey: true, key: "D", shiftKey: true }); + await waitFor(() => expect(onTranscribeAudio).toHaveBeenCalled()); + }); + it("disarms the second Enter shortcut after stopping the active response", () => { const onSend = vi.fn(); const onStop = vi.fn(); @@ -1669,6 +1701,7 @@ describe("ThreadComposer", () => { fireEvent.keyDown(input, { key: "Enter" }); expect(onSend).toHaveBeenCalledWith("guide this one now"); + expect(onSend).toHaveBeenCalledTimes(1); expect(screen.getByText("older guidance")).toBeInTheDocument(); expect(screen.queryByText("guide this one now")).not.toBeInTheDocument(); }); @@ -2021,6 +2054,19 @@ describe("ThreadComposer", () => { expect(screen.queryByText("remember this edited follow-up")).not.toBeInTheDocument(); }); + rerender( + , + ); + expect(await screen.findByText("remember this edited follow-up")).toBeInTheDocument(); + fireEvent.keyDown(screen.getByLabelText("Message input"), { key: "Enter" }); + expect(onSend).not.toHaveBeenCalled(); + unmount(); const remount = render(