diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 099fa9d83eb7..dda33d26e9f4 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -551,6 +551,7 @@ export function Autocomplete(props: { } function select() { + if (store.visible === "/" && !search()) return const selected = options()[store.selected] if (!selected) return hide() diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index aa002080b1dd..469a73bb3955 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -56,6 +56,7 @@ import { useTuiConfig } from "../../config" import { usePromptWorkspace } from "./workspace" import { usePromptMove } from "./move" import { readLocalAttachment } from "./local-attachment" +import { resolvePromptSlashCommand } from "./slash-command" export type PromptProps = { sessionID?: string @@ -1050,6 +1051,8 @@ export function Prompt(props: PromptProps) { ] : [] + const command = resolvePromptSlashCommand(inputText, sync.data.command) + if (store.mode === "shell") { move.startSubmit() void sdk.client.session.shell({ @@ -1062,22 +1065,12 @@ export function Prompt(props: PromptProps) { command: inputText, }) setStore("mode", "normal") - } else if ( - inputText.startsWith("/") && - sync.data.command.some((x) => x.name === inputText.split("\n")[0].split(" ")[0].slice(1)) - ) { + } else if (command) { move.startSubmit() - // Parse command from first line, preserve multi-line content in arguments - const firstLineEnd = inputText.indexOf("\n") - const firstLine = firstLineEnd === -1 ? inputText : inputText.slice(0, firstLineEnd) - const [command, ...firstLineArgs] = firstLine.split(" ") - const restOfInput = firstLineEnd === -1 ? "" : inputText.slice(firstLineEnd + 1) - const args = firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "") - void sdk.client.session.command({ sessionID, - command: command.slice(1), - arguments: args, + command: command.name, + arguments: command.arguments, agent: agent.name, model: `${selectedModel.providerID}/${selectedModel.modelID}`, variant, diff --git a/packages/tui/src/component/prompt/slash-command.ts b/packages/tui/src/component/prompt/slash-command.ts new file mode 100644 index 000000000000..7f2368192582 --- /dev/null +++ b/packages/tui/src/component/prompt/slash-command.ts @@ -0,0 +1,21 @@ +export type SlashCommand = { + name: string +} + +export function resolvePromptSlashCommand(inputText: string, commands: readonly SlashCommand[]) { + if (!inputText.startsWith("/")) return + + const firstLineEnd = inputText.indexOf("\n") + const firstLine = firstLineEnd === -1 ? inputText : inputText.slice(0, firstLineEnd) + const [rawCommand, ...firstLineArgs] = firstLine.split(" ") + const name = rawCommand.slice(1) + if (!name) return + if (!commands.some((command) => command.name === name)) return + + const restOfInput = firstLineEnd === -1 ? "" : inputText.slice(firstLineEnd + 1) + const args = firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "") + return { + name, + arguments: args, + } +} diff --git a/packages/tui/test/prompt/slash-command.test.ts b/packages/tui/test/prompt/slash-command.test.ts new file mode 100644 index 000000000000..7b49d9dac5e4 --- /dev/null +++ b/packages/tui/test/prompt/slash-command.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "bun:test" +import { resolvePromptSlashCommand } from "../../src/component/prompt/slash-command" + +describe("prompt slash command parsing", () => { + const commands = [{ name: "review" }, { name: "skills" }] + + test("ignores a bare slash", () => { + expect(resolvePromptSlashCommand("/", commands)).toBeUndefined() + }) + + test("ignores empty command names even if a command snapshot is malformed", () => { + expect(resolvePromptSlashCommand("/", [{ name: "" }])).toBeUndefined() + }) + + test("parses known slash command arguments", () => { + expect(resolvePromptSlashCommand("/review branch", commands)).toEqual({ + name: "review", + arguments: "branch", + }) + }) + + test("preserves multiline command arguments", () => { + expect(resolvePromptSlashCommand("/review branch\ninclude logs", commands)).toEqual({ + name: "review", + arguments: "branch\ninclude logs", + }) + }) + + test("ignores unknown slash commands", () => { + expect(resolvePromptSlashCommand("/missing", commands)).toBeUndefined() + }) +})