fix(coding-agent): reload keybindings on /reload closes #2309
This commit is contained in:
@@ -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 |
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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" },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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) |
|
||||||
|
|||||||
Reference in New Issue
Block a user