From 5c01b22771cfc4cb13d7edc649c4cdc5a9920e33 Mon Sep 17 00:00:00 2001 From: sawa-zen Date: Tue, 15 Sep 2026 18:18:32 +0900 Subject: [PATCH] fix(app): ignore IME composition keys in question dock custom input Pressing Enter or Escape to confirm/cancel IME composition (e.g. Japanese kana-kanji conversion) in the question dock's custom answer textarea was treated as commit/dismiss, moving focus out of the input to the option card. Guard key handlers with the same isImeComposing pattern used in prompt-input.tsx. Fixes #49154 --- .../session/composer/session-question-dock.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx index 1fdcb11ccae6..5eec7be2d643 100644 --- a/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -1,4 +1,4 @@ -import { For, Show, createEffect, createMemo, onCleanup, onMount, type Component } from "solid-js" +import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount, type Component } from "solid-js" import { createStore } from "solid-js/store" import { useMutation } from "@tanstack/solid-query" import { Button } from "@opencode-ai/ui/button" @@ -103,6 +103,12 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit const customLabel = () => language.t("ui.messagePart.option.typeOwnAnswer") const customPlaceholder = () => language.t("ui.question.custom.placeholder") + // Track IME composition explicitly: on some platforms (e.g. Safari) `compositionend` + // fires before the confirming Enter keydown, so `event.isComposing` alone is not enough. + // Same pattern as `isImeComposing` in `components/prompt-input.tsx`. + const [composing, setComposing] = createSignal(false) + const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229 + const last = createMemo(() => store.tab >= total() - 1) const collapse = useSpring(() => (store.minimized ? 1 : 0), { visualDuration: 0.3, bounce: 0 }) const hidden = createMemo(() => Math.max(0, Math.min(1, collapse()))) @@ -324,6 +330,8 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit if (event.defaultPrevented) return if (event.key === "Escape") { + // Let IME composition (e.g. Esc to dismiss candidates) finish instead of rejecting. + if (isImeComposing(event)) return event.preventDefault() void reject() return @@ -331,7 +339,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit const mod = (event.metaKey || event.ctrlKey) && !event.altKey if (mod && event.key === "Enter") { - if (event.repeat) return + if (event.repeat || isImeComposing(event)) return event.preventDefault() next() return @@ -614,6 +622,10 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit rows={1} disabled={sending()} onKeyDown={(e) => { + // Ignore Enter/Escape used to confirm or cancel IME composition + // (e.g. Japanese kana-kanji conversion), otherwise focus jumps + // out of the input to the option card. See #49154. + if (isImeComposing(e)) return if (e.key === "Escape") { e.preventDefault() setStore("editing", false) @@ -625,6 +637,8 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit e.preventDefault() commitCustom() }} + onCompositionStart={() => setComposing(true)} + onCompositionEnd={() => setComposing(false)} onInput={(e) => { customUpdate(e.currentTarget.value) resizeInput(e.currentTarget)