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:
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user