fix: update /hotkeys and startup hints to use configured keybindings

Both the startup instructions and /hotkeys command now display the
actual configured keybindings from keybindings.json instead of
hardcoded defaults.
This commit is contained in:
Helmut Januschka 2026-01-03 15:01:26 +01:00
parent 2bd7b79a9b
commit f79256ac3b

View file

@ -13,6 +13,7 @@ import {
CombinedAutocompleteProvider, CombinedAutocompleteProvider,
type Component, type Component,
Container, Container,
getEditorKeybindings,
Input, Input,
Loader, Loader,
Markdown, Markdown,
@ -220,43 +221,65 @@ export class InteractiveMode {
async init(): Promise<void> { async init(): Promise<void> {
if (this.isInitialized) return; if (this.isInitialized) return;
// Add header // Add header with keybindings from config
const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`); const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
// Format keybinding for startup display (lowercase, compact)
const formatStartupKey = (keys: string | string[]): string => {
const keyArray = Array.isArray(keys) ? keys : [keys];
return keyArray.join("/");
};
const kb = this.keybindings;
const interrupt = formatStartupKey(kb.getKeys("interrupt"));
const clear = formatStartupKey(kb.getKeys("clear"));
const exit = formatStartupKey(kb.getKeys("exit"));
const suspend = formatStartupKey(kb.getKeys("suspend"));
const deleteToLineEnd = formatStartupKey(getEditorKeybindings().getKeys("deleteToLineEnd"));
const cycleThinkingLevel = formatStartupKey(kb.getKeys("cycleThinkingLevel"));
const cycleModelForward = formatStartupKey(kb.getKeys("cycleModelForward"));
const cycleModelBackward = formatStartupKey(kb.getKeys("cycleModelBackward"));
const selectModel = formatStartupKey(kb.getKeys("selectModel"));
const expandTools = formatStartupKey(kb.getKeys("expandTools"));
const toggleThinking = formatStartupKey(kb.getKeys("toggleThinking"));
const externalEditor = formatStartupKey(kb.getKeys("externalEditor"));
const followUp = formatStartupKey(kb.getKeys("followUp"));
const instructions = const instructions =
theme.fg("dim", "esc") + theme.fg("dim", interrupt) +
theme.fg("muted", " to interrupt") + theme.fg("muted", " to interrupt") +
"\n" + "\n" +
theme.fg("dim", "ctrl+c") + theme.fg("dim", clear) +
theme.fg("muted", " to clear") + theme.fg("muted", " to clear") +
"\n" + "\n" +
theme.fg("dim", "ctrl+c twice") + theme.fg("dim", `${clear} twice`) +
theme.fg("muted", " to exit") + theme.fg("muted", " to exit") +
"\n" + "\n" +
theme.fg("dim", "ctrl+d") + theme.fg("dim", exit) +
theme.fg("muted", " to exit (empty)") + theme.fg("muted", " to exit (empty)") +
"\n" + "\n" +
theme.fg("dim", "ctrl+z") + theme.fg("dim", suspend) +
theme.fg("muted", " to suspend") + theme.fg("muted", " to suspend") +
"\n" + "\n" +
theme.fg("dim", "ctrl+k") + theme.fg("dim", deleteToLineEnd) +
theme.fg("muted", " to delete line") + theme.fg("muted", " to delete line") +
"\n" + "\n" +
theme.fg("dim", "shift+tab") + theme.fg("dim", cycleThinkingLevel) +
theme.fg("muted", " to cycle thinking") + theme.fg("muted", " to cycle thinking") +
"\n" + "\n" +
theme.fg("dim", "ctrl+p/shift+ctrl+p") + theme.fg("dim", `${cycleModelForward}/${cycleModelBackward}`) +
theme.fg("muted", " to cycle models") + theme.fg("muted", " to cycle models") +
"\n" + "\n" +
theme.fg("dim", "ctrl+l") + theme.fg("dim", selectModel) +
theme.fg("muted", " to select model") + theme.fg("muted", " to select model") +
"\n" + "\n" +
theme.fg("dim", "ctrl+o") + theme.fg("dim", expandTools) +
theme.fg("muted", " to expand tools") + theme.fg("muted", " to expand tools") +
"\n" + "\n" +
theme.fg("dim", "ctrl+t") + theme.fg("dim", toggleThinking) +
theme.fg("muted", " to toggle thinking") + theme.fg("muted", " to toggle thinking") +
"\n" + "\n" +
theme.fg("dim", "ctrl+g") + theme.fg("dim", externalEditor) +
theme.fg("muted", " for external editor") + theme.fg("muted", " for external editor") +
"\n" + "\n" +
theme.fg("dim", "/") + theme.fg("dim", "/") +
@ -265,7 +288,7 @@ export class InteractiveMode {
theme.fg("dim", "!") + theme.fg("dim", "!") +
theme.fg("muted", " to run bash") + theme.fg("muted", " to run bash") +
"\n" + "\n" +
theme.fg("dim", "alt+enter") + theme.fg("dim", followUp) +
theme.fg("muted", " to queue follow-up") + theme.fg("muted", " to queue follow-up") +
"\n" + "\n" +
theme.fg("dim", "drop files") + theme.fg("dim", "drop files") +
@ -2262,38 +2285,96 @@ export class InteractiveMode {
this.ui.requestRender(); this.ui.requestRender();
} }
/**
* Format keybindings for display (e.g., "ctrl+c" -> "Ctrl+C").
*/
private formatKeyDisplay(keys: string | string[]): string {
const keyArray = Array.isArray(keys) ? keys : [keys];
return keyArray
.map((key) =>
key
.split("+")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join("+"),
)
.join("/");
}
/**
* Get display string for an app keybinding action.
*/
private getAppKeyDisplay(action: Parameters<KeybindingsManager["getDisplayString"]>[0]): string {
const display = this.keybindings.getDisplayString(action);
return this.formatKeyDisplay(display);
}
/**
* Get display string for an editor keybinding action.
*/
private getEditorKeyDisplay(action: Parameters<ReturnType<typeof getEditorKeybindings>["getKeys"]>[0]): string {
const keys = getEditorKeybindings().getKeys(action);
return this.formatKeyDisplay(keys);
}
private handleHotkeysCommand(): void { private handleHotkeysCommand(): void {
// Navigation keybindings
const cursorWordLeft = this.getEditorKeyDisplay("cursorWordLeft");
const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight");
const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart");
const cursorLineEnd = this.getEditorKeyDisplay("cursorLineEnd");
// Editing keybindings
const submit = this.getEditorKeyDisplay("submit");
const newLine = this.getEditorKeyDisplay("newLine");
const deleteWordBackward = this.getEditorKeyDisplay("deleteWordBackward");
const deleteToLineStart = this.getEditorKeyDisplay("deleteToLineStart");
const deleteToLineEnd = this.getEditorKeyDisplay("deleteToLineEnd");
const tab = this.getEditorKeyDisplay("tab");
// App keybindings
const interrupt = this.getAppKeyDisplay("interrupt");
const clear = this.getAppKeyDisplay("clear");
const exit = this.getAppKeyDisplay("exit");
const suspend = this.getAppKeyDisplay("suspend");
const cycleThinkingLevel = this.getAppKeyDisplay("cycleThinkingLevel");
const cycleModelForward = this.getAppKeyDisplay("cycleModelForward");
const expandTools = this.getAppKeyDisplay("expandTools");
const toggleThinking = this.getAppKeyDisplay("toggleThinking");
const externalEditor = this.getAppKeyDisplay("externalEditor");
const followUp = this.getAppKeyDisplay("followUp");
const hotkeys = ` const hotkeys = `
**Navigation** **Navigation**
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|
| \`Arrow keys\` | Move cursor / browse history (Up when empty) | | \`Arrow keys\` | Move cursor / browse history (Up when empty) |
| \`Option+Left/Right\` | Move by word | | \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word |
| \`Ctrl+A\` / \`Home\` / \`Cmd+Left\` | Start of line | | \`${cursorLineStart}\` | Start of line |
| \`Ctrl+E\` / \`End\` / \`Cmd+Right\` | End of line | | \`${cursorLineEnd}\` | End of line |
**Editing** **Editing**
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|
| \`Enter\` | Send message | | \`${submit}\` | Send message |
| \`Shift+Enter\` / \`Alt+Enter\` | New line | | \`${newLine}\` | New line |
| \`Ctrl+W\` / \`Option+Backspace\` | Delete word backwards | | \`${deleteWordBackward}\` | Delete word backwards |
| \`Ctrl+U\` | Delete to start of line | | \`${deleteToLineStart}\` | Delete to start of line |
| \`Ctrl+K\` | Delete to end of line | | \`${deleteToLineEnd}\` | Delete to end of line |
**Other** **Other**
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|
| \`Tab\` | Path completion / accept autocomplete | | \`${tab}\` | Path completion / accept autocomplete |
| \`Escape\` | Cancel autocomplete / abort streaming | | \`${interrupt}\` | Cancel autocomplete / abort streaming |
| \`Ctrl+C\` | Clear editor (first) / exit (second) | | \`${clear}\` | Clear editor (first) / exit (second) |
| \`Ctrl+D\` | Exit (when editor is empty) | | \`${exit}\` | Exit (when editor is empty) |
| \`Ctrl+Z\` | Suspend to background | | \`${suspend}\` | Suspend to background |
| \`Shift+Tab\` | Cycle thinking level | | \`${cycleThinkingLevel}\` | Cycle thinking level |
| \`Ctrl+P\` | Cycle models | | \`${cycleModelForward}\` | Cycle models |
| \`Ctrl+O\` | Toggle tool output expansion | | \`${expandTools}\` | Toggle tool output expansion |
| \`Ctrl+T\` | Toggle thinking block visibility | | \`${toggleThinking}\` | Toggle thinking block visibility |
| \`Ctrl+G\` | Edit message in external editor | | \`${externalEditor}\` | Edit message in external editor |
| \`${followUp}\` | Queue follow-up message |
| \`/\` | Slash commands | | \`/\` | Slash commands |
| \`!\` | Run bash command | | \`!\` | Run bash command |
`; `;