From 7eb55b08cfb7f6c58189da847ddd615446690bae Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 17 Mar 2026 23:59:45 +0100 Subject: [PATCH] fix(coding-agent): reload keybindings on /reload closes #2309 --- packages/coding-agent/README.md | 2 +- packages/coding-agent/docs/keybindings.md | 2 ++ packages/coding-agent/src/core/keybindings.ts | 35 ++++++++++++------- .../coding-agent/src/core/slash-commands.ts | 2 +- .../src/modes/interactive/interactive-mode.ts | 26 ++++++++++---- 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index f4804e99..084bf826 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -154,7 +154,7 @@ Type `/` in the editor to trigger commands. [Extensions](#extensions) can regist | `/copy` | Copy last assistant message to clipboard | | `/export [file]` | Export session to HTML file | | `/share` | Upload as private GitHub gist with shareable HTML link | -| `/reload` | Reload extensions, skills, prompts, context files (themes hot-reload automatically) | +| `/reload` | Reload keybindings, extensions, skills, prompts, and context files (themes hot-reload automatically) | | `/hotkeys` | Show all keyboard shortcuts | | `/changelog` | Display version history | | `/quit`, `/exit` | Quit pi | diff --git a/packages/coding-agent/docs/keybindings.md b/packages/coding-agent/docs/keybindings.md index 52a32c3c..b09e62b5 100644 --- a/packages/coding-agent/docs/keybindings.md +++ b/packages/coding-agent/docs/keybindings.md @@ -2,6 +2,8 @@ All keyboard shortcuts can be customized via `~/.pi/agent/keybindings.json`. Each action can be bound to one or more keys. +After editing `keybindings.json`, run `/reload` in pi to apply the changes without restarting the session. + ## Key Format `modifier+key` where modifiers are `ctrl`, `shift`, `alt` (combinable) and keys are: diff --git a/packages/coding-agent/src/core/keybindings.ts b/packages/coding-agent/src/core/keybindings.ts index c15c5cd2..091519b6 100644 --- a/packages/coding-agent/src/core/keybindings.ts +++ b/packages/coding-agent/src/core/keybindings.ts @@ -112,10 +112,12 @@ function isAppAction(action: string): action is AppAction { */ export class KeybindingsManager { private config: KeybindingsConfig; + private configPath: string | undefined; private appActionToKeys: Map; - private constructor(config: KeybindingsConfig) { + private constructor(config: KeybindingsConfig, configPath?: string) { this.config = config; + this.configPath = configPath; this.appActionToKeys = new Map(); this.buildMaps(); } @@ -126,18 +128,8 @@ export class KeybindingsManager { static create(agentDir: string = getAgentDir()): KeybindingsManager { const configPath = join(agentDir, "keybindings.json"); const config = KeybindingsManager.loadFromFile(configPath); - const manager = new KeybindingsManager(config); - - // Set up editor keybindings globally - // Include both editor actions and expandTools (shared between app and editor) - const editorConfig: EditorKeybindingsConfig = {}; - for (const [action, keys] of Object.entries(config)) { - if (!isAppAction(action) || action === "expandTools") { - editorConfig[action as EditorAction] = keys; - } - } - setEditorKeybindings(new EditorKeybindingsManager(editorConfig)); - + const manager = new KeybindingsManager(config, configPath); + manager.applyEditorKeybindings(); return manager; } @@ -148,6 +140,13 @@ export class KeybindingsManager { return new KeybindingsManager(config); } + reload(): void { + if (!this.configPath) return; + this.config = KeybindingsManager.loadFromFile(this.configPath); + this.buildMaps(); + this.applyEditorKeybindings(); + } + private static loadFromFile(path: string): KeybindingsConfig { if (!existsSync(path)) return {}; try { @@ -174,6 +173,16 @@ export class KeybindingsManager { } } + private applyEditorKeybindings(): void { + const editorConfig: EditorKeybindingsConfig = {}; + for (const [action, keys] of Object.entries(this.config)) { + if (!isAppAction(action) || action === "expandTools") { + editorConfig[action as EditorAction] = keys; + } + } + setEditorKeybindings(new EditorKeybindingsManager(editorConfig)); + } + /** * Check if input matches an app action. */ diff --git a/packages/coding-agent/src/core/slash-commands.ts b/packages/coding-agent/src/core/slash-commands.ts index 0f814a6b..01950bac 100644 --- a/packages/coding-agent/src/core/slash-commands.ts +++ b/packages/coding-agent/src/core/slash-commands.ts @@ -33,6 +33,6 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray = [ { name: "new", description: "Start a new session" }, { name: "compact", description: "Manually compact the session context" }, { name: "resume", description: "Resume a different session" }, - { name: "reload", description: "Reload extensions, skills, prompts, and themes" }, + { name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes" }, { name: "quit", description: "Quit pi" }, ]; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index a64f16b3..fcf73c9f 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3794,9 +3794,14 @@ export class InteractiveMode { this.resetExtensionUI(); - const loader = new BorderedLoader(this.ui, theme, "Reloading extensions, skills, prompts, themes...", { - cancellable: false, - }); + const loader = new BorderedLoader( + this.ui, + theme, + "Reloading keybindings, extensions, skills, prompts, themes...", + { + cancellable: false, + }, + ); const previousEditor = this.editor; this.editorContainer.clear(); this.editorContainer.addChild(loader); @@ -3813,6 +3818,7 @@ export class InteractiveMode { try { await this.session.reload(); + this.keybindings.reload(); setRegisteredThemes(this.session.resourceLoader.getThemes().themes); this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); const themeName = this.settingsManager.getTheme(); @@ -3846,7 +3852,7 @@ export class InteractiveMode { if (modelsJsonError) { this.showError(`models.json error: ${modelsJsonError}`); } - this.showStatus("Reloaded extensions, skills, prompts, themes"); + this.showStatus("Reloaded keybindings, extensions, skills, prompts, themes"); } catch (error) { dismissLoader(previousEditor as Component); this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`); @@ -4084,6 +4090,10 @@ export class InteractiveMode { private handleHotkeysCommand(): void { // Navigation keybindings + const cursorUp = this.getEditorKeyDisplay("cursorUp"); + const cursorDown = this.getEditorKeyDisplay("cursorDown"); + const cursorLeft = this.getEditorKeyDisplay("cursorLeft"); + const cursorRight = this.getEditorKeyDisplay("cursorRight"); const cursorWordLeft = this.getEditorKeyDisplay("cursorWordLeft"); const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight"); const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart"); @@ -4116,14 +4126,16 @@ export class InteractiveMode { const expandTools = this.getAppKeyDisplay("expandTools"); const toggleThinking = this.getAppKeyDisplay("toggleThinking"); const externalEditor = this.getAppKeyDisplay("externalEditor"); + const cycleModelBackward = this.getAppKeyDisplay("cycleModelBackward"); const followUp = this.getAppKeyDisplay("followUp"); const dequeue = this.getAppKeyDisplay("dequeue"); + const pasteImage = this.getAppKeyDisplay("pasteImage"); let hotkeys = ` **Navigation** | Key | Action | |-----|--------| -| \`Arrow keys\` | Move cursor / browse history (Up when empty) | +| \`${cursorUp}\` / \`${cursorDown}\` / \`${cursorLeft}\` / \`${cursorRight}\` | Move cursor / browse history (Up when empty) | | \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word | | \`${cursorLineStart}\` | Start of line | | \`${cursorLineEnd}\` | End of line | @@ -4153,14 +4165,14 @@ export class InteractiveMode { | \`${exit}\` | Exit (when editor is empty) | | \`${suspend}\` | Suspend to background | | \`${cycleThinkingLevel}\` | Cycle thinking level | -| \`${cycleModelForward}\` | Cycle models | +| \`${cycleModelForward}\` / \`${cycleModelBackward}\` | Cycle models | | \`${selectModel}\` | Open model selector | | \`${expandTools}\` | Toggle tool output expansion | | \`${toggleThinking}\` | Toggle thinking block visibility | | \`${externalEditor}\` | Edit message in external editor | | \`${followUp}\` | Queue follow-up message | | \`${dequeue}\` | Restore queued messages | -| \`Ctrl+V\` | Paste image from clipboard | +| \`${pasteImage}\` | Paste image from clipboard | | \`/\` | Slash commands | | \`!\` | Run bash command | | \`!!\` | Run bash command (excluded from context) |