feat: add undo action and fix hotkeys rendering (#438)

* Fix prompt action undo and hotkeys rendering

* Fix prompt action test typechecks

* Simplify undo and hotkeys PR

* Fix editor formatting
This commit is contained in:
daandden
2026-03-16 14:05:21 +01:00
committed by GitHub
parent 24ef2302ea
commit b7978c9e0d
10 changed files with 270 additions and 64 deletions
@@ -264,10 +264,7 @@ export class KeybindingsManager {
* Get display string for an action.
*/
getDisplayString(action: AppAction): string {
const keys = this.getKeys(action);
if (keys.length === 0) return "";
if (keys.length === 1) return keys[0]!;
return keys.join("/");
return formatKeyHints(this.getKeys(action));
}
/**
@@ -24,6 +24,7 @@ import { DynamicBorder } from "../../modes/components/dynamic-border";
import { PythonExecutionComponent } from "../../modes/components/python-execution";
import { getMarkdownTheme, getSymbolTheme, theme } from "../../modes/theme/theme";
import type { InteractiveModeContext } from "../../modes/types";
import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
import type { AsyncJobSnapshotItem } from "../../session/agent-session";
import type { AuthStorage } from "../../session/auth-storage";
import { outputMeta } from "../../tools/output-meta";
@@ -509,52 +510,13 @@ export class CommandController {
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 = `
**Navigation**
| Key | Action |
|-----|--------|
| \`Arrow keys\` | Move cursor / browse history (Up when empty) |
| \`Option+Left/Right\` | Move by word |
| \`Ctrl+A\` / \`Home\` / \`Cmd+Left\` | Start of line |
| \`Ctrl+E\` / \`End\` / \`Cmd+Right\` | End of line |
**Editing**
| Key | Action |
|-----|--------|
| \`Enter\` | Send message |
| \`Shift+Enter\` / \`Alt+Enter\` | New line |
| \`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 |
**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) |
| \`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 |
| \`#\` | Open prompt actions |
| \`/\` | Slash commands |
| \`!\` | Run bash command |
| \`!!\` | Run bash command (excluded from context) |
| \`$\` | Run Python in shared kernel |
| \`$$\` | Run Python (excluded from context) |
`;
const hotkeys = buildHotkeysMarkdown({
expandToolsKey,
planModeKey,
sttKey,
copyLineKey,
copyPromptKey,
});
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));
@@ -535,6 +535,7 @@ export class InputController {
keybindings: this.ctx.keybindings,
copyCurrentLine: () => this.handleCopyCurrentLine(),
copyPrompt: () => this.handleCopyPrompt(),
undo: prefix => this.ctx.editor.undoPastTransientText(prefix),
moveCursorToMessageEnd: () => this.ctx.editor.moveToMessageEnd(),
moveCursorToMessageStart: () => this.ctx.editor.moveToMessageStart(),
moveCursorToLineStart: () => this.ctx.editor.moveToLineStart(),
@@ -12,12 +12,12 @@ interface PromptActionDefinition {
label: string;
description: string;
keywords: string[];
execute: () => void;
execute: (prefix: string) => void;
}
interface PromptActionAutocompleteItem extends AutocompleteItem {
actionId: string;
execute: () => void;
execute: (prefix: string) => void;
}
interface PromptActionAutocompleteOptions {
@@ -26,6 +26,7 @@ interface PromptActionAutocompleteOptions {
keybindings: KeybindingsManager;
copyCurrentLine: () => void;
copyPrompt: () => void;
undo: (prefix: string) => void;
moveCursorToMessageEnd: () => void;
moveCursorToMessageStart: () => void;
moveCursorToLineStart: () => void;
@@ -141,6 +142,14 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
onApplied?: () => void;
} {
if (prefix.startsWith("#") && isPromptActionItem(item)) {
if (item.actionId === "undo") {
return {
lines,
cursorLine,
cursorCol,
onApplied: () => item.execute(prefix),
};
}
const currentLine = lines[cursorLine] || "";
const beforePrefix = currentLine.slice(0, cursorCol - prefix.length);
const afterCursor = currentLine.slice(cursorCol);
@@ -150,7 +159,7 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
lines: newLines,
cursorLine,
cursorCol: beforePrefix.length,
onApplied: item.execute,
onApplied: () => item.execute(prefix),
};
}
@@ -181,6 +190,13 @@ export function createPromptActionAutocompleteProvider(
keywords: ["copy", "prompt", "clipboard", "message"],
execute: options.copyPrompt,
},
{
id: "undo",
label: "Undo",
description: formatKeyHints(editorKeybindings.getKeys("undo")),
keywords: ["undo", "revert", "edit", "history"],
execute: options.undo,
},
{
id: "cursor-message-end",
label: "Move cursor to end of message",
@@ -0,0 +1,57 @@
export interface HotkeysMarkdownBindings {
expandToolsKey: string;
planModeKey: string;
sttKey: string;
copyLineKey: string;
copyPromptKey: string;
}
export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string {
const { expandToolsKey, planModeKey, sttKey, copyLineKey, copyPromptKey } = bindings;
return [
"**Navigation**",
"| Key | Action |",
"|-----|--------|",
"| `Arrow keys` | Move cursor / browse history (Up when empty) |",
"| `Option+Left/Right` | Move by word |",
"| `Ctrl+A` / `Home` / `Cmd+Left` | Start of line |",
"| `Ctrl+E` / `End` / `Cmd+Right` | End of line |",
"",
"**Editing**",
"| Key | Action |",
"|-----|--------|",
"| `Enter` | Send message |",
"| `Shift+Enter` / `Alt+Enter` | New line |",
"| `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 |`,
"",
"**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) |",
"| `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 |`,
"| `#` | Open prompt actions |",
"| `/` | Slash commands |",
"| `!` | Run bash command |",
"| `!!` | Run bash command (excluded from context) |",
"| `$` | Run Python in shared kernel |",
"| `$$` | Run Python (excluded from context) |",
].join("\n");
}