diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 792805dea..4172d18cc 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2214,7 +2214,7 @@ - New `appendModeChange()` method in SessionManager to record mode transitions - New `mode` and `modeData` fields in SessionContext to track active agent mode - Support for `PI_PACKAGE_DIR` environment variable to override package directory (useful for Nix/Guix store paths) -- New keybindings for session management: `toggleSessionNamedFilter` (Ctrl+N), `newSession`, `tree`, `fork`, and `resume` actions +- New keybindings for session management: `newSession`, `tree`, `fork`, and `resume` actions - Support for shell command execution in configuration values (API keys, headers) using `!` prefix, with result caching - New `clearOnShrink` display setting to control whether empty rows are cleared when content shrinks - New `SlashCommandInfo`, `SlashCommandLocation`, and `SlashCommandSource` types for extension slash command discovery diff --git a/packages/coding-agent/src/config/keybindings.ts b/packages/coding-agent/src/config/keybindings.ts index 6bfd3f33b..6fe4ee578 100644 --- a/packages/coding-agent/src/config/keybindings.ts +++ b/packages/coding-agent/src/config/keybindings.ts @@ -25,7 +25,6 @@ export type AppAction = | "togglePlanMode" | "expandTools" | "toggleThinking" - | "toggleSessionNamedFilter" | "externalEditor" | "historySearch" | "followUp" @@ -67,7 +66,6 @@ export const DEFAULT_APP_KEYBINDINGS: Record = { historySearch: "ctrl+r", expandTools: "ctrl+o", toggleThinking: "ctrl+t", - toggleSessionNamedFilter: "ctrl+n", externalEditor: "ctrl+g", followUp: "ctrl+enter", dequeue: "alt+up", @@ -80,7 +78,6 @@ export const DEFAULT_APP_KEYBINDINGS: Record = { resume: [], toggleSTT: "alt+h", }; - /** * All default keybindings (app + editor). */ @@ -103,7 +100,6 @@ const APP_ACTIONS: AppAction[] = [ "historySearch", "expandTools", "toggleThinking", - "toggleSessionNamedFilter", "externalEditor", "followUp", "dequeue", diff --git a/packages/coding-agent/src/modes/components/custom-editor.ts b/packages/coding-agent/src/modes/components/custom-editor.ts index 491c61bf2..be956c20e 100644 --- a/packages/coding-agent/src/modes/components/custom-editor.ts +++ b/packages/coding-agent/src/modes/components/custom-editor.ts @@ -1,34 +1,89 @@ import { Editor, type KeyId, matchesKey, parseKittySequence } from "@oh-my-pi/pi-tui"; +import type { AppAction } from "../../config/keybindings"; + +type ConfigurableEditorAction = Extract< + AppAction, + | "interrupt" + | "clear" + | "exit" + | "suspend" + | "cycleThinkingLevel" + | "cycleModelForward" + | "cycleModelBackward" + | "selectModel" + | "expandTools" + | "toggleThinking" + | "externalEditor" + | "historySearch" + | "dequeue" + | "pasteImage" + | "copyPrompt" +>; + +const DEFAULT_ACTION_KEYS: Record = { + interrupt: ["escape"], + clear: ["ctrl+c"], + exit: ["ctrl+d"], + suspend: ["ctrl+z"], + cycleThinkingLevel: ["shift+tab"], + cycleModelForward: ["ctrl+p"], + cycleModelBackward: ["shift+ctrl+p"], + selectModel: ["ctrl+l"], + expandTools: ["ctrl+o"], + toggleThinking: ["ctrl+t"], + externalEditor: ["ctrl+g"], + historySearch: ["ctrl+r"], + dequeue: ["alt+up"], + pasteImage: ["ctrl+v"], + copyPrompt: ["alt+shift+c"], +}; /** - * Custom editor that handles Escape and Ctrl+C keys for coding-agent + * Custom editor that handles configurable app-level shortcuts for coding-agent. */ export class CustomEditor extends Editor { onEscape?: () => void; shouldBypassAutocompleteOnEscape?: () => boolean; - onCtrlC?: () => void; - onCtrlD?: () => void; - onShiftTab?: () => void; - onCtrlP?: () => void; - onShiftCtrlP?: () => void; - onCtrlL?: () => void; - onCtrlR?: () => void; - onCtrlO?: () => void; - onCtrlT?: () => void; - onCtrlG?: () => void; - onCtrlZ?: () => void; - onQuestionMark?: () => void; - onCapsLock?: () => void; - onAltP?: () => void; - /** Called when Alt+Shift+C is pressed to copy prompt to clipboard. */ + onClear?: () => void; + onExit?: () => void; + onCycleThinkingLevel?: () => void; + onCycleModelForward?: () => void; + onCycleModelBackward?: () => void; + onSelectModel?: () => void; + onExpandTools?: () => void; + onToggleThinking?: () => void; + onExternalEditor?: () => void; + onHistorySearch?: () => void; + onSuspend?: () => void; + onShowHotkeys?: () => void; + onQuickSelectModel?: () => void; + /** Called when the configured copy-prompt shortcut is pressed. */ onCopyPrompt?: () => void; - /** Called when Ctrl+V is pressed. Returns true if handled (image found), false to fall through to text paste. */ - onCtrlV?: () => Promise; - /** Called when Alt+Up is pressed (dequeue keybinding). */ - onAltUp?: () => void; + /** Called when the configured image-paste shortcut is pressed. */ + onPasteImage?: () => Promise; + /** Called when the configured dequeue shortcut is pressed. */ + onDequeue?: () => void; + /** Called when Caps Lock is pressed. */ + onCapsLock?: () => void; - /** Custom key handlers from extensions */ + /** Custom key handlers from extensions and non-built-in app actions. */ #customKeyHandlers = new Map void>(); + #actionKeys = new Map( + Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]), + ); + + setActionKeys(action: ConfigurableEditorAction, keys: KeyId[]): void { + this.#actionKeys.set(action, [...keys]); + } + + #matchesAction(data: string, action: ConfigurableEditorAction): boolean { + const keys = this.#actionKeys.get(action); + if (!keys) return false; + for (const key of keys) { + if (matchesKey(data, key)) return true; + } + return false; + } /** * Register a custom key handler. Extensions use this for shortcuts. @@ -59,111 +114,111 @@ export class CustomEditor extends Editor { return; } - // Intercept Ctrl+V for image paste (async - fires and handles result) - if (matchesKey(data, "ctrl+v") && this.onCtrlV) { - void this.onCtrlV(); + // Intercept configured image paste (async - fires and handles result) + if (this.#matchesAction(data, "pasteImage") && this.onPasteImage) { + void this.onPasteImage(); return; } - // Intercept Ctrl+G for external editor - if (matchesKey(data, "ctrl+g") && this.onCtrlG) { - this.onCtrlG(); + // Intercept configured external editor shortcut + if (this.#matchesAction(data, "externalEditor") && this.onExternalEditor) { + this.onExternalEditor(); return; } // Intercept Alt+P for quick model switching - if (matchesKey(data, "alt+p") && this.onAltP) { - this.onAltP(); + if (matchesKey(data, "alt+p") && this.onQuickSelectModel) { + this.onQuickSelectModel(); return; } - // Intercept Ctrl+Z for suspend - if (matchesKey(data, "ctrl+z") && this.onCtrlZ) { - this.onCtrlZ(); + // Intercept configured suspend shortcut + if (this.#matchesAction(data, "suspend") && this.onSuspend) { + this.onSuspend(); return; } - // Intercept Ctrl+T for thinking block visibility toggle - if (matchesKey(data, "ctrl+t") && this.onCtrlT) { - this.onCtrlT(); + // Intercept configured thinking block visibility toggle + if (this.#matchesAction(data, "toggleThinking") && this.onToggleThinking) { + this.onToggleThinking(); return; } - // Intercept Ctrl+L for model selector - if (matchesKey(data, "ctrl+l") && this.onCtrlL) { - this.onCtrlL(); + // Intercept configured model selector shortcut + if (this.#matchesAction(data, "selectModel") && this.onSelectModel) { + this.onSelectModel(); return; } - // Intercept Ctrl+R for history search - if (matchesKey(data, "ctrl+r") && this.onCtrlR) { - this.onCtrlR(); + // Intercept configured history search shortcut + if (this.#matchesAction(data, "historySearch") && this.onHistorySearch) { + this.onHistorySearch(); return; } - // Intercept Ctrl+O for tool output expansion - if (matchesKey(data, "ctrl+o") && this.onCtrlO) { - this.onCtrlO(); + // Intercept configured tool output expansion shortcut + if (this.#matchesAction(data, "expandTools") && this.onExpandTools) { + this.onExpandTools(); return; } - // Intercept Shift+Ctrl+P for backward model cycling (check before Ctrl+P) - if ((matchesKey(data, "shift+ctrl+p") || matchesKey(data, "ctrl+shift+p")) && this.onShiftCtrlP) { - this.onShiftCtrlP(); + // Intercept configured backward model cycling (check before forward cycling) + if (this.#matchesAction(data, "cycleModelBackward") && this.onCycleModelBackward) { + this.onCycleModelBackward(); return; } - // Intercept Ctrl+P for model cycling - if (matchesKey(data, "ctrl+p") && this.onCtrlP) { - this.onCtrlP(); + // Intercept configured forward model cycling + if (this.#matchesAction(data, "cycleModelForward") && this.onCycleModelForward) { + this.onCycleModelForward(); return; } - // Intercept Shift+Tab for thinking level cycling - if (matchesKey(data, "shift+tab") && this.onShiftTab) { - this.onShiftTab(); + // Intercept configured thinking level cycling + if (this.#matchesAction(data, "cycleThinkingLevel") && this.onCycleThinkingLevel) { + this.onCycleThinkingLevel(); return; } - // Intercept Escape key. - // Default behavior keeps autocomplete dismissal, but parent can prioritize global escape handling. - if ((matchesKey(data, "escape") || matchesKey(data, "esc")) && this.onEscape) { + // Intercept configured interrupt shortcut. + // Default behavior keeps autocomplete dismissal, but parent can prioritize global interrupt handling. + if (this.#matchesAction(data, "interrupt") && this.onEscape) { if (!this.isShowingAutocomplete() || this.shouldBypassAutocompleteOnEscape?.()) { this.onEscape(); return; } } - // Intercept Ctrl+C - if (matchesKey(data, "ctrl+c") && this.onCtrlC) { - this.onCtrlC(); + // Intercept configured clear shortcut + if (this.#matchesAction(data, "clear") && this.onClear) { + this.onClear(); return; } - // Intercept Ctrl+D (only when editor is empty) - if (matchesKey(data, "ctrl+d")) { - if (this.getText().length === 0 && this.onCtrlD) { - this.onCtrlD(); + // Intercept configured exit shortcut (only when editor is empty) + if (this.#matchesAction(data, "exit")) { + if (this.getText().length === 0 && this.onExit) { + this.onExit(); } - // Always consume Ctrl+D (don't pass to parent) + // Always consume exit shortcut (don't pass to parent) return; } - // Intercept Alt+Up for dequeue (restore queued message to editor) - if (matchesKey(data, "alt+up") && this.onAltUp) { - this.onAltUp(); + // Intercept configured dequeue shortcut (restore queued message to editor) + if (this.#matchesAction(data, "dequeue") && this.onDequeue) { + this.onDequeue(); return; } - // Intercept Alt+Shift+C to copy prompt to clipboard - if (matchesKey(data, "alt+shift+c") && this.onCopyPrompt) { + // Intercept configured copy-prompt shortcut + if (this.#matchesAction(data, "copyPrompt") && this.onCopyPrompt) { this.onCopyPrompt(); return; } // Intercept ? when editor is empty to show hotkeys - if (data === "?" && this.getText().length === 0 && this.onQuestionMark) { - this.onQuestionMark(); + if (data === "?" && this.getText().length === 0 && this.onShowHotkeys) { + this.onShowHotkeys(); return; } diff --git a/packages/coding-agent/src/modes/controllers/command-controller.ts b/packages/coding-agent/src/modes/controllers/command-controller.ts index 5ebb6c558..143afe239 100644 --- a/packages/coding-agent/src/modes/controllers/command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/command-controller.ts @@ -505,18 +505,7 @@ export class CommandController { } handleHotkeysCommand(): void { - const expandToolsKey = this.ctx.keybindings.getDisplayString("expandTools") || "Ctrl+O"; - const planModeKey = this.ctx.keybindings.getDisplayString("togglePlanMode") || "Alt+Shift+P"; - const sttKey = this.ctx.keybindings.getDisplayString("toggleSTT") || "Alt+H"; - const copyLineKey = this.ctx.keybindings.getDisplayString("copyLine") || "Alt+Shift+L"; - const copyPromptKey = this.ctx.keybindings.getDisplayString("copyPrompt") || "Alt+Shift+C"; - const hotkeys = buildHotkeysMarkdown({ - expandToolsKey, - planModeKey, - sttKey, - copyLineKey, - copyPromptKey, - }); + const hotkeys = buildHotkeysMarkdown({ keybindings: this.ctx.keybindings }); this.ctx.chatContainer.addChild(new Spacer(1)); this.ctx.chatContainer.addChild(new DynamicBorder()); this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0)); diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index a5d8740d1..592d8090d 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -26,6 +26,7 @@ export class InputController { constructor(private ctx: InteractiveModeContext) {} setupKeyHandlers(): void { + this.ctx.editor.setActionKeys("interrupt", this.ctx.keybindings.getKeys("interrupt")); this.ctx.editor.shouldBypassAutocompleteOnEscape = () => Boolean( this.ctx.loadingAnimation || @@ -64,7 +65,7 @@ export class InputController { } else if (this.ctx.session.isStreaming) { void this.ctx.session.abort(); } else if (!this.ctx.editor.getText().trim()) { - // Double-escape with empty editor triggers /tree, /branch, or nothing based on setting + // Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting const action = settings.get("doubleEscapeAction"); if (action !== "none") { const now = Date.now(); @@ -82,42 +83,44 @@ export class InputController { } }; - this.ctx.editor.onCtrlC = () => this.handleCtrlC(); - this.ctx.editor.onCtrlD = () => this.handleCtrlD(); - this.ctx.editor.onCtrlZ = () => this.handleCtrlZ(); - this.ctx.editor.onShiftTab = () => this.cycleThinkingLevel(); - this.ctx.editor.onCtrlP = () => this.cycleRoleModel(); - this.ctx.editor.onShiftCtrlP = () => this.cycleRoleModel({ temporary: true }); - this.ctx.editor.onAltP = () => this.ctx.showModelSelector({ temporaryOnly: true }); + this.ctx.editor.setActionKeys("clear", this.ctx.keybindings.getKeys("clear")); + this.ctx.editor.onClear = () => this.handleCtrlC(); + this.ctx.editor.setActionKeys("exit", this.ctx.keybindings.getKeys("exit")); + this.ctx.editor.onExit = () => this.handleCtrlD(); + this.ctx.editor.setActionKeys("suspend", this.ctx.keybindings.getKeys("suspend")); + this.ctx.editor.onSuspend = () => this.handleCtrlZ(); + this.ctx.editor.setActionKeys("cycleThinkingLevel", this.ctx.keybindings.getKeys("cycleThinkingLevel")); + this.ctx.editor.onCycleThinkingLevel = () => this.cycleThinkingLevel(); + this.ctx.editor.setActionKeys("cycleModelForward", this.ctx.keybindings.getKeys("cycleModelForward")); + this.ctx.editor.onCycleModelForward = () => this.cycleRoleModel(); + this.ctx.editor.setActionKeys("cycleModelBackward", this.ctx.keybindings.getKeys("cycleModelBackward")); + this.ctx.editor.onCycleModelBackward = () => this.cycleRoleModel({ temporary: true }); + this.ctx.editor.onQuickSelectModel = () => this.ctx.showModelSelector({ temporaryOnly: true }); // Global debug handler on TUI (works regardless of focus) this.ctx.ui.onDebug = () => this.ctx.showDebugSelector(); - this.ctx.editor.onCtrlL = () => this.ctx.showModelSelector(); - this.ctx.editor.onCtrlR = () => this.ctx.showHistorySearch(); - this.ctx.editor.onCtrlT = () => this.ctx.toggleTodoExpansion(); - this.ctx.editor.onCtrlG = () => void this.openExternalEditor(); - this.ctx.editor.onQuestionMark = () => this.ctx.handleHotkeysCommand(); - this.ctx.editor.onCtrlV = () => this.handleImagePaste(); - const copyPromptKeys = this.ctx.keybindings.getKeys("copyPrompt"); - this.ctx.editor.onCopyPrompt = copyPromptKeys.includes("alt+shift+c") ? () => this.handleCopyPrompt() : undefined; + this.ctx.editor.setActionKeys("selectModel", this.ctx.keybindings.getKeys("selectModel")); + this.ctx.editor.onSelectModel = () => this.ctx.showModelSelector(); + this.ctx.editor.setActionKeys("historySearch", this.ctx.keybindings.getKeys("historySearch")); + this.ctx.editor.onHistorySearch = () => this.ctx.showHistorySearch(); + this.ctx.editor.setActionKeys("toggleThinking", this.ctx.keybindings.getKeys("toggleThinking")); + this.ctx.editor.onToggleThinking = () => this.ctx.toggleThinkingBlockVisibility(); + this.ctx.editor.setActionKeys("externalEditor", this.ctx.keybindings.getKeys("externalEditor")); + this.ctx.editor.onExternalEditor = () => void this.openExternalEditor(); + this.ctx.editor.onShowHotkeys = () => this.ctx.handleHotkeysCommand(); + this.ctx.editor.setActionKeys("pasteImage", this.ctx.keybindings.getKeys("pasteImage")); + this.ctx.editor.onPasteImage = () => this.handleImagePaste(); + this.ctx.editor.setActionKeys("copyPrompt", this.ctx.keybindings.getKeys("copyPrompt")); + this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt(); + this.ctx.editor.setActionKeys("expandTools", this.ctx.keybindings.getKeys("expandTools")); + this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion(); + this.ctx.editor.setActionKeys("dequeue", this.ctx.keybindings.getKeys("dequeue")); + this.ctx.editor.onDequeue = () => this.handleDequeue(); + this.ctx.editor.clearCustomKeyHandlers(); // Wire up extension shortcuts this.registerExtensionShortcuts(); - const expandToolsKeys = this.ctx.keybindings.getKeys("expandTools"); - this.ctx.editor.onCtrlO = expandToolsKeys.includes("ctrl+o") ? () => this.toggleToolOutputExpansion() : undefined; - for (const key of expandToolsKeys) { - if (key === "ctrl+o") continue; - this.ctx.editor.setCustomKeyHandler(key, () => this.toggleToolOutputExpansion()); - } - - const dequeueKeys = this.ctx.keybindings.getKeys("dequeue"); - this.ctx.editor.onAltUp = dequeueKeys.includes("alt+up") ? () => this.handleDequeue() : undefined; - for (const key of dequeueKeys) { - if (key === "alt+up") continue; - this.ctx.editor.setCustomKeyHandler(key, () => this.handleDequeue()); - } - const planModeKeys = this.ctx.keybindings.getKeys("togglePlanMode"); for (const key of planModeKeys) { this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handlePlanModeCommand()); @@ -144,10 +147,6 @@ export class InputController { for (const key of this.ctx.keybindings.getKeys("copyLine")) { this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyCurrentLine()); } - for (const key of copyPromptKeys) { - if (key === "alt+shift+c") continue; - this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyPrompt()); - } this.ctx.editor.onChange = (text: string) => { const wasBashMode = this.ctx.isBashMode; diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 49270e97d..359837725 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -555,7 +555,7 @@ export class InteractiveMode implements InteractiveModeContext { }); if (visibleTasks.length < activePhase.tasks.length) { const remaining = activePhase.tasks.length - visibleTasks.length; - lines.push(theme.fg("muted", `${indent} ${hook} +${remaining} more (Ctrl+T to expand)`)); + lines.push(theme.fg("muted", `${indent} ${hook} +${remaining} more`)); } this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0)); return; diff --git a/packages/coding-agent/src/modes/utils/hotkeys-markdown.ts b/packages/coding-agent/src/modes/utils/hotkeys-markdown.ts index a18fc49c2..f8415d443 100644 --- a/packages/coding-agent/src/modes/utils/hotkeys-markdown.ts +++ b/packages/coding-agent/src/modes/utils/hotkeys-markdown.ts @@ -1,13 +1,14 @@ +import type { AppAction, KeybindingsManager } from "../../config/keybindings"; + export interface HotkeysMarkdownBindings { - expandToolsKey: string; - planModeKey: string; - sttKey: string; - copyLineKey: string; - copyPromptKey: string; + keybindings: Pick; +} + +function appKey(bindings: HotkeysMarkdownBindings, action: AppAction): string { + return bindings.keybindings.getDisplayString(action) || "Disabled"; } export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string { - const { expandToolsKey, planModeKey, sttKey, copyLineKey, copyPromptKey } = bindings; return [ "**Navigation**", "| Key | Action |", @@ -25,28 +26,29 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string "| `Ctrl+W` / `Option+Backspace` | Delete word backwards |", "| `Ctrl+U` | Delete to start of line |", "| `Ctrl+K` | Delete to end of line |", - `| \`${copyLineKey}\` | Copy current line |`, - `| \`${copyPromptKey}\` | Copy whole prompt |`, + `| \`${appKey(bindings, "copyLine")}\` | Copy current line |`, + `| \`${appKey(bindings, "copyPrompt")}\` | Copy whole prompt |`, "", "**Other**", "| Key | Action |", "|-----|--------|", "| `Tab` | Path completion / accept autocomplete |", - "| `Escape` | Cancel autocomplete / abort streaming |", - "| `Ctrl+C` | Clear editor (first) / exit (second) |", - "| `Ctrl+D` | Exit (when editor is empty) |", - "| `Ctrl+Z` | Suspend to background |", - "| `Shift+Tab` | Cycle thinking level |", - "| `Ctrl+P` | Cycle role models (slow/default/smol) |", - "| `Shift+Ctrl+P` | Cycle role models (temporary) |", + `| \`${appKey(bindings, "interrupt")}\` | Cancel autocomplete / interrupt active work |`, + `| \`${appKey(bindings, "clear")}\` | Clear editor (first) / exit (second) |`, + `| \`${appKey(bindings, "exit")}\` | Exit (when editor is empty) |`, + `| \`${appKey(bindings, "suspend")}\` | Suspend to background |`, + `| \`${appKey(bindings, "cycleThinkingLevel")}\` | Cycle thinking level |`, + `| \`${appKey(bindings, "cycleModelForward")}\` | Cycle role models (slow/default/smol) |`, + `| \`${appKey(bindings, "cycleModelBackward")}\` | Cycle role models (temporary) |`, "| `Alt+P` | Select model (temporary) |", - "| `Ctrl+L` | Select model (set roles) |", - `| \`${planModeKey}\` | Toggle plan mode |`, - "| `Ctrl+R` | Search prompt history |", - `| \`${expandToolsKey}\` | Toggle tool output expansion |`, - "| `Ctrl+T` | Toggle todo list expansion |", - "| `Ctrl+G` | Edit message in external editor |", - `| \`${sttKey}\` | Toggle speech-to-text recording |`, + `| \`${appKey(bindings, "selectModel")}\` | Select model (set roles) |`, + `| \`${appKey(bindings, "togglePlanMode")}\` | Toggle plan mode |`, + `| \`${appKey(bindings, "historySearch")}\` | Search prompt history |`, + `| \`${appKey(bindings, "expandTools")}\` | Toggle tool output expansion |`, + `| \`${appKey(bindings, "toggleThinking")}\` | Toggle thinking block visibility |`, + `| \`${appKey(bindings, "externalEditor")}\` | Edit message in external editor |`, + `| \`${appKey(bindings, "pasteImage")}\` | Paste image from clipboard |`, + `| \`${appKey(bindings, "toggleSTT")}\` | Toggle speech-to-text recording |`, "| `#` | Open prompt actions |", "| `/` | Slash commands |", "| `!` | Run bash command |", diff --git a/packages/coding-agent/test/input-controller-escape.test.ts b/packages/coding-agent/test/input-controller-escape.test.ts index 22a3de918..a419d434d 100644 --- a/packages/coding-agent/test/input-controller-escape.test.ts +++ b/packages/coding-agent/test/input-controller-escape.test.ts @@ -6,24 +6,29 @@ type FakeEditor = { onEscape?: () => void; onSubmit?: (text: string) => Promise; shouldBypassAutocompleteOnEscape?: () => boolean; - onCtrlC?: () => void; - onCtrlD?: () => void; - onCtrlZ?: () => void; - onShiftTab?: () => void; - onCtrlP?: () => void; - onShiftCtrlP?: () => void; - onAltP?: () => void; - onCtrlL?: () => void; - onCtrlR?: () => void; - onQuestionMark?: () => void; - onCtrlV?: () => void; + onClear?: () => void; + onExit?: () => void; + onSuspend?: () => void; + onCycleThinkingLevel?: () => void; + onCycleModelForward?: () => void; + onCycleModelBackward?: () => void; + onQuickSelectModel?: () => void; + onSelectModel?: () => void; + onHistorySearch?: () => void; + onShowHotkeys?: () => void; + onPasteImage?: () => void; onCopyPrompt?: () => void; - onAltUp?: () => void; + onExpandTools?: () => void; + onToggleThinking?: () => void; + onExternalEditor?: () => void; + onDequeue?: () => void; onChange?: (text: string) => void; setText(text: string): void; getText(): string; addToHistory(text: string): void; + setActionKeys(action: string, keys: string[]): void; setCustomKeyHandler(key: string, handler: () => void): void; + clearCustomKeyHandlers(): void; }; function createSubmission(input: { @@ -83,7 +88,9 @@ function createContext(): { return editorText; }, addToHistory: vi.fn(), + setActionKeys: vi.fn(), setCustomKeyHandler: vi.fn(), + clearCustomKeyHandlers: vi.fn(), }; let ctx!: InteractiveModeContext; diff --git a/packages/coding-agent/test/modes/controllers/command-controller-hotkeys.test.ts b/packages/coding-agent/test/modes/controllers/command-controller-hotkeys.test.ts index d16851a91..0a74b0d22 100644 --- a/packages/coding-agent/test/modes/controllers/command-controller-hotkeys.test.ts +++ b/packages/coding-agent/test/modes/controllers/command-controller-hotkeys.test.ts @@ -3,12 +3,31 @@ import { buildHotkeysMarkdown } from "../../../src/modes/utils/hotkeys-markdown" describe("buildHotkeysMarkdown", () => { it("emits flush-left markdown so headings and tables are parsed instead of treated as indented text", () => { + const displayStrings: Record = { + copyLine: "Alt+Shift+L", + copyPrompt: "Ctrl+Shift+P", + togglePlanMode: "Alt+M", + expandTools: "Ctrl+O", + interrupt: "Esc", + clear: "Ctrl+C", + exit: "Ctrl+D", + suspend: "Ctrl+Z", + cycleThinkingLevel: "Shift+Tab", + cycleModelForward: "Ctrl+P", + cycleModelBackward: "Shift+Ctrl+P", + selectModel: "Ctrl+L", + historySearch: "Ctrl+R", + toggleThinking: "Ctrl+T", + externalEditor: "Ctrl+G", + pasteImage: "Ctrl+V", + toggleSTT: "Alt+H", + }; const markdown = buildHotkeysMarkdown({ - expandToolsKey: "Ctrl+O", - planModeKey: "Alt+M", - sttKey: "Alt+H", - copyLineKey: "Alt+Shift+L", - copyPromptKey: "Ctrl+Shift+P", + keybindings: { + getDisplayString(action) { + return displayStrings[action] ?? "Disabled"; + }, + }, }); const lines = markdown.split("\n");