diff --git a/src/components/chat/message-input.test.tsx b/src/components/chat/message-input.test.tsx index c469de545e..0a13bb4971 100644 --- a/src/components/chat/message-input.test.tsx +++ b/src/components/chat/message-input.test.tsx @@ -837,6 +837,72 @@ describe("MessageInput slash menu while the agent connects", () => { ) } + it("keeps `/` as a slash-command trigger", async () => { + await mountAndType({ + commandsLoading: false, + availableCommands: COMMANDS, + }) + const menu = await screen.findByTestId("slash-menu") + expect(within(menu).getByText("/compact")).toBeInTheDocument() + }) + + it("treats `、` as the same slash-command trigger", async () => { + await mountAndType( + { commandsLoading: false, availableCommands: COMMANDS }, + "、" + ) + const menu = await screen.findByTestId("slash-menu") + expect(within(menu).getByText("/compact")).toBeInTheDocument() + }) + + it("preserves `、` in ordinary Chinese text", async () => { + const { editor } = await mountAndType( + { commandsLoading: false, availableCommands: COMMANDS }, + "苹果、香蕉" + ) + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 50)) + }) + expect(serializeDocToText(editor.state.doc)).toBe("苹果、香蕉") + expect(screen.queryByTestId("slash-menu")).toBeNull() + }) + + it("serializes a command selected through `、` with `/`", async () => { + const { editor } = await mountAndType( + { commandsLoading: false, availableCommands: COMMANDS }, + "、" + ) + const menu = await screen.findByTestId("slash-menu") + fireEvent.mouseDown(within(menu).getByRole("button", { name: /\/compact/ })) + await waitFor(() => + expect(serializeDocToText(editor.state.doc).trim()).toBe("/compact") + ) + expect(serializeDocToText(editor.state.doc)).not.toContain("、") + }) + + it("does not select a slash command while the IME is composing", async () => { + const { editor } = await mountAndType( + { commandsLoading: false, availableCommands: COMMANDS }, + "、" + ) + await screen.findByTestId("slash-menu") + + act(() => { + ;(editor.view.dom as HTMLElement).dispatchEvent( + new KeyboardEvent("keydown", { + key: "Enter", + bubbles: true, + cancelable: true, + isComposing: true, + }) + ) + }) + + const text = serializeDocToText(editor.state.doc) + expect(text).toContain("、") + expect(text).not.toContain("/compact") + }) + it("opens the panel on a loading row while commands are still on their way", async () => { await mountAndType({ commandsLoading: true, availableCommands: [] }) const menu = await screen.findByTestId("slash-menu") diff --git a/src/components/chat/message-input.tsx b/src/components/chat/message-input.tsx index 23abc40225..5ab6eb2051 100644 --- a/src/components/chat/message-input.tsx +++ b/src/components/chat/message-input.tsx @@ -794,8 +794,9 @@ export function MessageInput({ // ── Editor-driven `/` (commands) and `$` (Codex skills) trigger detection ── // The `@` mention panel is now owned by RichComposer; this only handles the // runtime-command menus. We inspect the text before the collapsed caret in the - // current block: a `/` (any agent) or `$` (Codex) at the start or right after - // whitespace, and not inside inline code / a code block, opens the menu. + // current block: a `/` or its Chinese-IME alias `、` (any agent), or `$` + // (Codex), at the start or right after whitespace, and not inside inline code + // / a code block, opens the menu. const detectSlashTrigger = useCallback(() => { const editor = editorRef.current?.getEditor() const close = () => { @@ -814,10 +815,11 @@ export function MessageInput({ " " ) const regex = - agentType === "codex" ? /(^|\s)([/$])(\S*)$/ : /(^|\s)(\/)(\S*)$/ + agentType === "codex" ? /(^|\s)([/$、])(\S*)$/ : /(^|\s)([/、])(\S*)$/ const match = before.match(regex) if (!match) return close() - const trigger = match[2] as "/" | "$" + const matchedTrigger = match[2] as "/" | "$" | "、" + const trigger = matchedTrigger === "、" ? "/" : matchedTrigger // Only `/` is gated here. Its source is the agent's own command list, which // exists only once the connection is up — so an empty list means "nothing to // show" unless the connection is still coming, where the panel opens on a @@ -860,7 +862,7 @@ export function MessageInput({ setSlashTriggerChar(null) }, []) - // Replace the live `/`-or-`$` token immediately before the caret with + // Replace the live `/`-, `、`-, or `$` token immediately before the caret with // an inline reference badge (+ a trailing space unless one already follows), // then close the menu. Used by both the command (`/`) and Codex-skill (`$`) // selections — the badge serializes back to its literal `/cmd` / `$skill` @@ -876,7 +878,7 @@ export function MessageInput({ undefined, " " ) - const match = before.match(/(^|\s)([/$])(\S*)$/) + const match = before.match(/(^|\s)([/$、])(\S*)$/) const charAfter = $from.parentOffset < $from.parent.content.size ? $from.parent.textBetween( @@ -889,7 +891,7 @@ export function MessageInput({ const suffix = charAfter && /\s/.test(charAfter) ? "" : " " let chain = editor.chain().focus() if (match) { - // Remove the live `/…` / `$…` token before the caret. + // Remove the live `/…` / `、…` / `$…` token before the caret. const tokenLen = match[2].length + match[3].length chain = chain.deleteRange({ from: $from.pos - tokenLen, to: $from.pos }) }