From bf25f267099d472771ed01906907653b7e972f1e Mon Sep 17 00:00:00 2001 From: David McKay Date: Sat, 12 Sep 2026 15:14:28 -0700 Subject: [PATCH] Wait for a composed character before the desktop question box asks --- CHANGELOG.md | 7 +++++++ desktop/src/App.test.tsx | 35 ++++++++++++++++++++++++++++++++++- desktop/src/Ask.tsx | 11 ++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2f952b2..b124ea88c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. ## Unreleased +### The desktop setup's question box waits for a composed character before it asks + +Enter confirms a character being typed through an input method (Japanese, Chinese, Korean). On the +final setup screen that Enter also sent the question, with its last character still unconfirmed. The +box now waits for the character to be confirmed, the way a chat composer does, and an ordinary Enter +still asks. + ### A malformed page size is refused instead of silently coerced `GET /channels` and `GET /api/admin/people` read `?limit=` with `Number.parseInt`, which diff --git a/desktop/src/App.test.tsx b/desktop/src/App.test.tsx index fc1060d8f..daea7ff6c 100644 --- a/desktop/src/App.test.tsx +++ b/desktop/src/App.test.tsx @@ -1,6 +1,12 @@ import { afterAll, afterEach, beforeAll, expect, mock, test } from "bun:test"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; -import { act, cleanup, render, waitFor } from "@testing-library/react"; +import { + act, + cleanup, + fireEvent, + render, + waitFor, +} from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { StrictMode } from "react"; @@ -1870,3 +1876,30 @@ test("Start credential failures do not expose a restore action", async () => { ), ).toEqual([]); }); + +test("the Enter that finishes a composed character does not ask the Bot", async () => { + useCompatibleEndpointSetup({}); + const previous = invokeHandler; + invokeHandler = async (command, args) => { + if (command === "ask_the_bot") return "42"; + return previous(command, args); + }; + const view = await enterCompatibleEndpoint("https://models.example/v1"); + await userEvent.click(view.getByRole("button", { name: "Continue" })); + await userEvent.click(view.getByRole("button", { name: "Start OpenBot" })); + const question = await view.findByLabelText("Your question"); + // Keys land on the focused field. + await userEvent.click(question); + + // Japanese, Chinese and Korean are typed through an input method, where Enter confirms the + // character being built. Chromium marks that keydown `isComposing`; the macOS WebKit webview + // sends it after compositionend with key code 229. Neither should send the question. + await act(async () => { + fireEvent.keyDown(question, { key: "Enter", isComposing: true }); + fireEvent.keyDown(question, { key: "Enter", keyCode: 229 }); + }); + + expect(invokeCalls.filter((call) => call.command === "ask_the_bot")).toEqual( + [], + ); +}); diff --git a/desktop/src/Ask.tsx b/desktop/src/Ask.tsx index 65a6e0eab..9c6cbec32 100644 --- a/desktop/src/Ask.tsx +++ b/desktop/src/Ask.tsx @@ -70,7 +70,16 @@ export function Ask({ }} disabled={asking} onKeyDown={(event) => { - if (event.key === "Enter" && !asking) { + // Enter also confirms a character being composed through an input method (Japanese, + // Chinese, Korean). Asking on that Enter would send the question with its last character + // still unconfirmed. Chromium marks that keydown `isComposing`; the macOS WebKit webview + // instead sends it after compositionend with key code 229. Wait for either. + if ( + event.key === "Enter" && + !asking && + !event.nativeEvent.isComposing && + event.nativeEvent.keyCode !== 229 + ) { ask(); } }}