From 25c813de3bd8e5cb28f0d7e67b2ae3eed8599150 Mon Sep 17 00:00:00 2001 From: "dongyang.lin" Date: Wed, 6 May 2026 06:35:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20TUI=20=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E6=96=9C=E6=9D=A0=E5=91=BD=E4=BB=A4=E6=8B=A6=E6=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cli/cmd/tui/component/dialog-command.tsx | 18 +++++++++++++ .../cli/cmd/tui/component/prompt/index.tsx | 12 +++++++++ .../src/cli/cmd/tui/component/prompt/slash.ts | 10 +++++++ .../test/cli/cmd/tui/prompt-slash.test.ts | 26 +++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 packages/opencode/src/cli/cmd/tui/component/prompt/slash.ts create mode 100644 packages/opencode/test/cli/cmd/tui/prompt-slash.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx index 49bf42c63e85..d6e3ccc4a3d6 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx @@ -30,6 +30,12 @@ export type CommandOption = DialogSelectOption & { enabled?: boolean } +function matchesSlash(option: CommandOption, name: string) { + const slash = option.slash + if (!slash) return false + return slash.name === name || !!slash.aliases?.includes(name) +} + function init() { const root = getOwner() const [registrations, setRegistrations] = createSignal[]>([]) @@ -84,6 +90,18 @@ function init() { } } }, + hasSlash(name: string) { + return entries().some((option) => isVisible(option) && matchesSlash(option, name)) + }, + executeSlash(name: string) { + for (const option of entries()) { + if (!isVisible(option)) continue + if (!matchesSlash(option, name)) continue + option.onSelect?.(dialog) + return true + } + return false + }, slashes() { return visibleOptions().flatMap((option) => { const slash = option.slash diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 74332c77be77..f16ed45d00c9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -20,6 +20,7 @@ import { useKeybind } from "@tui/context/keybind" import { usePromptHistory, type PromptInfo } from "./history" import { computePromptTraits } from "./traits" import { assign } from "./part" +import { getLocalSlashCommand } from "./slash" import { usePromptStash } from "./stash" import { DialogStash } from "../dialog-stash" import { type AutocompleteRef, Autocomplete } from "./autocomplete" @@ -827,6 +828,17 @@ export function Prompt(props: PromptProps) { void exit() return true } + const localSlash = getLocalSlashCommand(store.prompt.input, command.hasSlash) + if (localSlash && command.executeSlash(localSlash)) { + input.extmarks.clear() + input.clear() + setStore("prompt", { + input: "", + parts: [], + }) + setStore("extmarkToPartIndex", new Map()) + return true + } const selectedModel = local.model.current() if (!selectedModel) { void promptModelWarning() diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/slash.ts b/packages/opencode/src/cli/cmd/tui/component/prompt/slash.ts new file mode 100644 index 000000000000..804bef3e9487 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/slash.ts @@ -0,0 +1,10 @@ +export function getLocalSlashCommand(input: string, hasSlash: (name: string) => boolean) { + const trimmed = input.trim() + if (!trimmed.startsWith("/")) return + if (trimmed.includes(" ") || trimmed.includes("\n")) return + + const name = trimmed.slice(1) + if (!name) return + + return hasSlash(name) ? name : undefined +} \ No newline at end of file diff --git a/packages/opencode/test/cli/cmd/tui/prompt-slash.test.ts b/packages/opencode/test/cli/cmd/tui/prompt-slash.test.ts new file mode 100644 index 000000000000..fc5dfc1e69b9 --- /dev/null +++ b/packages/opencode/test/cli/cmd/tui/prompt-slash.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "bun:test" +import { getLocalSlashCommand } from "../../../../src/cli/cmd/tui/component/prompt/slash" + +describe("getLocalSlashCommand", () => { + test("returns a standalone local slash command name", () => { + expect(getLocalSlashCommand(" /spec ", (name) => name === "spec")).toBe("spec") + }) + + test("does not intercept unknown standalone slash commands", () => { + expect(getLocalSlashCommand("/review", () => false)).toBeUndefined() + }) + + test("does not intercept slash commands with arguments or multiline content", () => { + const hasSlash = () => true + + expect(getLocalSlashCommand("/spec draft", hasSlash)).toBeUndefined() + expect(getLocalSlashCommand("/spec\ndraft", hasSlash)).toBeUndefined() + }) + + test("does not intercept bare slashes or regular text", () => { + const hasSlash = () => true + + expect(getLocalSlashCommand("/", hasSlash)).toBeUndefined() + expect(getLocalSlashCommand("spec", hasSlash)).toBeUndefined() + }) +}) \ No newline at end of file