fix(coding-agent): reload keybindings on /reload closes #2309

This commit is contained in:
Mario Zechner
2026-03-17 23:59:45 +01:00
parent 3dd63848a0
commit 7eb55b08cf
5 changed files with 45 additions and 22 deletions

View File

@@ -154,7 +154,7 @@ Type `/` in the editor to trigger commands. [Extensions](#extensions) can regist
| `/copy` | Copy last assistant message to clipboard | | `/copy` | Copy last assistant message to clipboard |
| `/export [file]` | Export session to HTML file | | `/export [file]` | Export session to HTML file |
| `/share` | Upload as private GitHub gist with shareable HTML link | | `/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 | | `/hotkeys` | Show all keyboard shortcuts |
| `/changelog` | Display version history | | `/changelog` | Display version history |
| `/quit`, `/exit` | Quit pi | | `/quit`, `/exit` | Quit pi |

View File

@@ -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. 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 ## Key Format
`modifier+key` where modifiers are `ctrl`, `shift`, `alt` (combinable) and keys are: `modifier+key` where modifiers are `ctrl`, `shift`, `alt` (combinable) and keys are:

View File

@@ -112,10 +112,12 @@ function isAppAction(action: string): action is AppAction {
*/ */
export class KeybindingsManager { export class KeybindingsManager {
private config: KeybindingsConfig; private config: KeybindingsConfig;
private configPath: string | undefined;
private appActionToKeys: Map<AppAction, KeyId[]>; private appActionToKeys: Map<AppAction, KeyId[]>;
private constructor(config: KeybindingsConfig) { private constructor(config: KeybindingsConfig, configPath?: string) {
this.config = config; this.config = config;
this.configPath = configPath;
this.appActionToKeys = new Map(); this.appActionToKeys = new Map();
this.buildMaps(); this.buildMaps();
} }
@@ -126,18 +128,8 @@ export class KeybindingsManager {
static create(agentDir: string = getAgentDir()): KeybindingsManager { static create(agentDir: string = getAgentDir()): KeybindingsManager {
const configPath = join(agentDir, "keybindings.json"); const configPath = join(agentDir, "keybindings.json");
const config = KeybindingsManager.loadFromFile(configPath); const config = KeybindingsManager.loadFromFile(configPath);
const manager = new KeybindingsManager(config); const manager = new KeybindingsManager(config, configPath);
manager.applyEditorKeybindings();
// 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));
return manager; return manager;
} }
@@ -148,6 +140,13 @@ export class KeybindingsManager {
return new KeybindingsManager(config); 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 { private static loadFromFile(path: string): KeybindingsConfig {
if (!existsSync(path)) return {}; if (!existsSync(path)) return {};
try { 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. * Check if input matches an app action.
*/ */

View File

@@ -33,6 +33,6 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
{ name: "new", description: "Start a new session" }, { name: "new", description: "Start a new session" },
{ name: "compact", description: "Manually compact the session context" }, { name: "compact", description: "Manually compact the session context" },
{ name: "resume", description: "Resume a different session" }, { 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" }, { name: "quit", description: "Quit pi" },
]; ];

View File

@@ -3794,9 +3794,14 @@ export class InteractiveMode {
this.resetExtensionUI(); this.resetExtensionUI();
const loader = new BorderedLoader(this.ui, theme, "Reloading extensions, skills, prompts, themes...", { const loader = new BorderedLoader(
cancellable: false, this.ui,
}); theme,
"Reloading keybindings, extensions, skills, prompts, themes...",
{
cancellable: false,
},
);
const previousEditor = this.editor; const previousEditor = this.editor;
this.editorContainer.clear(); this.editorContainer.clear();
this.editorContainer.addChild(loader); this.editorContainer.addChild(loader);
@@ -3813,6 +3818,7 @@ export class InteractiveMode {
try { try {
await this.session.reload(); await this.session.reload();
this.keybindings.reload();
setRegisteredThemes(this.session.resourceLoader.getThemes().themes); setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
const themeName = this.settingsManager.getTheme(); const themeName = this.settingsManager.getTheme();
@@ -3846,7 +3852,7 @@ export class InteractiveMode {
if (modelsJsonError) { if (modelsJsonError) {
this.showError(`models.json error: ${modelsJsonError}`); this.showError(`models.json error: ${modelsJsonError}`);
} }
this.showStatus("Reloaded extensions, skills, prompts, themes"); this.showStatus("Reloaded keybindings, extensions, skills, prompts, themes");
} catch (error) { } catch (error) {
dismissLoader(previousEditor as Component); dismissLoader(previousEditor as Component);
this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`); this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -4084,6 +4090,10 @@ export class InteractiveMode {
private handleHotkeysCommand(): void { private handleHotkeysCommand(): void {
// Navigation keybindings // 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 cursorWordLeft = this.getEditorKeyDisplay("cursorWordLeft");
const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight"); const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight");
const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart"); const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart");
@@ -4116,14 +4126,16 @@ export class InteractiveMode {
const expandTools = this.getAppKeyDisplay("expandTools"); const expandTools = this.getAppKeyDisplay("expandTools");
const toggleThinking = this.getAppKeyDisplay("toggleThinking"); const toggleThinking = this.getAppKeyDisplay("toggleThinking");
const externalEditor = this.getAppKeyDisplay("externalEditor"); const externalEditor = this.getAppKeyDisplay("externalEditor");
const cycleModelBackward = this.getAppKeyDisplay("cycleModelBackward");
const followUp = this.getAppKeyDisplay("followUp"); const followUp = this.getAppKeyDisplay("followUp");
const dequeue = this.getAppKeyDisplay("dequeue"); const dequeue = this.getAppKeyDisplay("dequeue");
const pasteImage = this.getAppKeyDisplay("pasteImage");
let hotkeys = ` let hotkeys = `
**Navigation** **Navigation**
| Key | Action | | 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 | | \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word |
| \`${cursorLineStart}\` | Start of line | | \`${cursorLineStart}\` | Start of line |
| \`${cursorLineEnd}\` | End of line | | \`${cursorLineEnd}\` | End of line |
@@ -4153,14 +4165,14 @@ export class InteractiveMode {
| \`${exit}\` | Exit (when editor is empty) | | \`${exit}\` | Exit (when editor is empty) |
| \`${suspend}\` | Suspend to background | | \`${suspend}\` | Suspend to background |
| \`${cycleThinkingLevel}\` | Cycle thinking level | | \`${cycleThinkingLevel}\` | Cycle thinking level |
| \`${cycleModelForward}\` | Cycle models | | \`${cycleModelForward}\` / \`${cycleModelBackward}\` | Cycle models |
| \`${selectModel}\` | Open model selector | | \`${selectModel}\` | Open model selector |
| \`${expandTools}\` | Toggle tool output expansion | | \`${expandTools}\` | Toggle tool output expansion |
| \`${toggleThinking}\` | Toggle thinking block visibility | | \`${toggleThinking}\` | Toggle thinking block visibility |
| \`${externalEditor}\` | Edit message in external editor | | \`${externalEditor}\` | Edit message in external editor |
| \`${followUp}\` | Queue follow-up message | | \`${followUp}\` | Queue follow-up message |
| \`${dequeue}\` | Restore queued messages | | \`${dequeue}\` | Restore queued messages |
| \`Ctrl+V\` | Paste image from clipboard | | \`${pasteImage}\` | Paste image from clipboard |
| \`/\` | Slash commands | | \`/\` | Slash commands |
| \`!\` | Run bash command | | \`!\` | Run bash command |
| \`!!\` | Run bash command (excluded from context) | | \`!!\` | Run bash command (excluded from context) |