diff --git a/.changeset/align-slash-command-selection.md b/.changeset/align-slash-command-selection.md new file mode 100644 index 00000000000..d8fd2c43c16 --- /dev/null +++ b/.changeset/align-slash-command-selection.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Ensure Enter and Tab execute the slash command highlighted in the VS Code command picker. diff --git a/packages/kilo-vscode/tests/unit/use-slash-command.test.ts b/packages/kilo-vscode/tests/unit/use-slash-command.test.ts index 27934f6607a..7e9eb114831 100644 --- a/packages/kilo-vscode/tests/unit/use-slash-command.test.ts +++ b/packages/kilo-vscode/tests/unit/use-slash-command.test.ts @@ -270,6 +270,78 @@ describe("useSlashCommand sandbox action", () => { }) }) +describe("slash command keyboard selection", () => { + it.each(["Enter", "Tab"] as const)("keeps %s selection aligned with the action-first menu", (key) => { + const state = { text: "/refresh", prevented: 0 } + const ctx = setup(() => {}) + const textarea = { + value: state.text, + selectionStart: state.text.length, + setSelectionRange: () => {}, + focus: () => {}, + } as unknown as HTMLTextAreaElement + const event = { + key, + isComposing: false, + preventDefault: () => state.prevented++, + } as unknown as KeyboardEvent + + ctx.slash.onInput(state.text, state.text.length) + ctx.fire({ + type: "commandsLoaded", + commands: [{ name: "refresh", description: "Run the custom refresh command", hints: [] }], + }) + + expect(ctx.slash.results().map((command) => command.name)).toEqual(["reload", "refresh"]) + const handled = ctx.slash.onKeyDown(event, textarea, (text) => (state.text = text)) + + expect(handled).toBe(true) + expect(state.prevented).toBe(1) + expect(state.text).toBe("") + expect(textarea.value).toBe("") + expect(ctx.sent).toEqual([{ type: "requestCommands" }, { type: "reload" }]) + ctx.dispose() + }) + + it("selects the second displayed result after ArrowDown", () => { + const state = { text: "/refresh", prevented: 0 } + const ctx = setup(() => {}) + const textarea = { + value: state.text, + selectionStart: state.text.length, + setSelectionRange: () => {}, + focus: () => {}, + } as unknown as HTMLTextAreaElement + + ctx.slash.onInput(state.text, state.text.length) + ctx.fire({ + type: "commandsLoaded", + commands: [{ name: "refresh", description: "Run the custom refresh command", hints: [] }], + }) + + const down = { + key: "ArrowDown", + isComposing: false, + preventDefault: () => state.prevented++, + } as unknown as KeyboardEvent + const enter = { + key: "Enter", + isComposing: false, + preventDefault: () => state.prevented++, + } as unknown as KeyboardEvent + + expect(ctx.slash.onKeyDown(down, textarea, (text) => (state.text = text))).toBe(true) + expect(ctx.slash.index()).toBe(1) + expect(ctx.slash.onKeyDown(enter, textarea, (text) => (state.text = text))).toBe(true) + + expect(state.prevented).toBe(2) + expect(state.text).toBe("/refresh ") + expect(textarea.value).toBe("/refresh ") + expect(ctx.sent).toEqual([{ type: "requestCommands" }]) + ctx.dispose() + }) +}) + describe("select", () => { it("preserves trailing text for action commands", () => { let actionCalls = 0 diff --git a/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts index dc377e8c22d..0a32d887eff 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts @@ -243,7 +243,7 @@ export function useSlashCommand( vscode.postMessage({ type: "requestCommands" }) } - const results = () => { + const matched = () => { const q = query() if (q === null) return [] const list = commands() @@ -277,6 +277,12 @@ export function useSlashCommand( return sortByScore(matches, lower) } + const results = () => { + const list = matched() + // PromptInput renders contiguous Actions and Commands groups, so keyboard indexes must use the same order. + return [...list.filter((cmd) => cmd.action), ...list.filter((cmd) => !cmd.action)] + } + const unsubscribe = vscode.onMessage((message) => { if (message.type !== "commandsLoaded") return setServer(message.commands)