From 52b59864fedda38bc2f80774b34557a0be9def71 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 13 Jan 2026 12:37:40 +0100 Subject: [PATCH] Fix number key hotkeys (1, 2, 3) not working in command approval menu --- .changeset/fix-approval-number-hotkeys.md | 5 + .../state/atoms/__tests__/keyboard.test.ts | 151 +++++++++++++++++- cli/src/state/atoms/keyboard.ts | 8 + 3 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-approval-number-hotkeys.md diff --git a/.changeset/fix-approval-number-hotkeys.md b/.changeset/fix-approval-number-hotkeys.md new file mode 100644 index 00000000000..0d40799f081 --- /dev/null +++ b/.changeset/fix-approval-number-hotkeys.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Fix number key hotkeys (1, 2, 3) not working in command approval menu diff --git a/cli/src/state/atoms/__tests__/keyboard.test.ts b/cli/src/state/atoms/__tests__/keyboard.test.ts index 6688ae7111c..d859de95abf 100644 --- a/cli/src/state/atoms/__tests__/keyboard.test.ts +++ b/cli/src/state/atoms/__tests__/keyboard.test.ts @@ -18,7 +18,7 @@ import { submissionCallbackAtom, submitInputAtom, } from "../keyboard.js" -import { pendingApprovalAtom } from "../approval.js" +import { pendingApprovalAtom, approvalOptionsAtom } from "../approval.js" import { historyDataAtom, historyModeAtom, historyIndexAtom as _historyIndexAtom } from "../history.js" import { chatMessagesAtom, extensionModeAtom, customModesAtom } from "../extension.js" import { extensionServiceAtom, isServiceReadyAtom } from "../service.js" @@ -1804,4 +1804,153 @@ describe("keypress atoms", () => { expect(cursor.col).toBe(5) // Moved one character right }) }) + + describe("approval mode number key hotkeys", () => { + it("should select and execute option when pressing number key hotkey (1, 2, 3)", async () => { + // Set up a command approval with hierarchical options + // Command "mkdir test-dir" should generate: + // - Run Command (y) + // - Always Run "mkdir" (1) + // - Always Run "mkdir test-dir" (2) + // - Reject (n) + const mockMessage: ExtensionChatMessage = { + ts: Date.now(), + type: "ask", + ask: "command", + text: "mkdir test-dir", + partial: false, + isAnswered: false, + say: "assistant", + } + store.set(pendingApprovalAtom, mockMessage) + + // Verify we have the expected options with number hotkeys + const options = store.get(approvalOptionsAtom) + expect(options.length).toBeGreaterThanOrEqual(4) + expect(options[0].hotkey).toBe("y") // Run Command + expect(options[1].hotkey).toBe("1") // Always Run "mkdir" + expect(options[2].hotkey).toBe("2") // Always Run "mkdir test-dir" + expect(options[options.length - 1].hotkey).toBe("n") // Reject + + // Press "1" key - should select the "Always Run mkdir" option + const key1: Key = { + name: "1", + sequence: "1", + ctrl: false, + meta: false, + shift: false, + paste: false, + } + await store.set(keyboardHandlerAtom, key1) + + // The option at index 1 should be selected + const selectedIndex = store.get(selectedIndexAtom) + expect(selectedIndex).toBe(1) + }) + + it("should select option 2 when pressing '2' key", async () => { + const mockMessage: ExtensionChatMessage = { + ts: Date.now(), + type: "ask", + ask: "command", + text: "mkdir test-dir", + partial: false, + isAnswered: false, + say: "assistant", + } + store.set(pendingApprovalAtom, mockMessage) + + // Press "2" key + const key2: Key = { + name: "2", + sequence: "2", + ctrl: false, + meta: false, + shift: false, + paste: false, + } + await store.set(keyboardHandlerAtom, key2) + + // The option at index 2 should be selected + const selectedIndex = store.get(selectedIndexAtom) + expect(selectedIndex).toBe(2) + }) + + it("should select option 3 when pressing '3' key for command with 3 hierarchy levels", async () => { + // Command with 3 parts: "mkdir test-dir && touch test-dir/file.ts" + // Should generate: + // - Run Command (y) + // - Always Run "mkdir" (1) + // - Always Run "mkdir test-dir" (2) + // - Always Run "mkdir test-dir && touch test-dir/file.ts" (3) + // - Reject (n) + const mockMessage: ExtensionChatMessage = { + ts: Date.now(), + type: "ask", + ask: "command", + text: "mkdir test-dir && touch test-dir/file.ts", + partial: false, + isAnswered: false, + say: "assistant", + } + store.set(pendingApprovalAtom, mockMessage) + + // Verify we have option with hotkey "3" + const options = store.get(approvalOptionsAtom) + const option3 = options.find((opt) => opt.hotkey === "3") + expect(option3).toBeDefined() + + // Press "3" key + const key3: Key = { + name: "3", + sequence: "3", + ctrl: false, + meta: false, + shift: false, + paste: false, + } + await store.set(keyboardHandlerAtom, key3) + + // The option at index 3 should be selected + const selectedIndex = store.get(selectedIndexAtom) + expect(selectedIndex).toBe(3) + }) + + it("should not select anything when pressing number key that has no matching hotkey", async () => { + // Simple command with only 1 hierarchy level + const mockMessage: ExtensionChatMessage = { + ts: Date.now(), + type: "ask", + ask: "command", + text: "ls", + partial: false, + isAnswered: false, + say: "assistant", + } + store.set(pendingApprovalAtom, mockMessage) + + // Verify we only have options with hotkeys y, 1, n (no 2 or 3) + const options = store.get(approvalOptionsAtom) + expect(options.find((opt) => opt.hotkey === "2")).toBeUndefined() + expect(options.find((opt) => opt.hotkey === "3")).toBeUndefined() + + // Initial selection should be 0 + expect(store.get(selectedIndexAtom)).toBe(0) + + // Press "2" key - should not change selection since there's no option with hotkey "2" + const key2: Key = { + name: "2", + sequence: "2", + ctrl: false, + meta: false, + shift: false, + paste: false, + } + await store.set(keyboardHandlerAtom, key2) + + // Selection should remain unchanged + const selectedIndex = store.get(selectedIndexAtom) + expect(selectedIndex).toBe(0) + }) + }) }) diff --git a/cli/src/state/atoms/keyboard.ts b/cli/src/state/atoms/keyboard.ts index a3677d7be59..33fe454c233 100644 --- a/cli/src/state/atoms/keyboard.ts +++ b/cli/src/state/atoms/keyboard.ts @@ -511,6 +511,14 @@ function handleApprovalKeys(get: Getter, set: Setter, key: Key) { // Guard against empty options array to prevent NaN from modulo 0 if (options.length === 0) return + // Check if the key matches any option's hotkey (for number keys 1, 2, 3, etc.) + const hotkeyIndex = options.findIndex((opt) => opt.hotkey === key.name) + if (hotkeyIndex !== -1) { + set(selectedIndexAtom, hotkeyIndex) + set(executeSelectedAtom) + return + } + switch (key.name) { case "down": set(selectedIndexAtom, (selectedIndex + 1) % options.length)