fix: route all app-level shortcuts through KeybindingsManager
fixes #468
This commit is contained in:
@@ -2214,7 +2214,7 @@
|
||||
- New `appendModeChange()` method in SessionManager to record mode transitions
|
||||
- New `mode` and `modeData` fields in SessionContext to track active agent mode
|
||||
- Support for `PI_PACKAGE_DIR` environment variable to override package directory (useful for Nix/Guix store paths)
|
||||
- New keybindings for session management: `toggleSessionNamedFilter` (Ctrl+N), `newSession`, `tree`, `fork`, and `resume` actions
|
||||
- New keybindings for session management: `newSession`, `tree`, `fork`, and `resume` actions
|
||||
- Support for shell command execution in configuration values (API keys, headers) using `!` prefix, with result caching
|
||||
- New `clearOnShrink` display setting to control whether empty rows are cleared when content shrinks
|
||||
- New `SlashCommandInfo`, `SlashCommandLocation`, and `SlashCommandSource` types for extension slash command discovery
|
||||
|
||||
@@ -25,7 +25,6 @@ export type AppAction =
|
||||
| "togglePlanMode"
|
||||
| "expandTools"
|
||||
| "toggleThinking"
|
||||
| "toggleSessionNamedFilter"
|
||||
| "externalEditor"
|
||||
| "historySearch"
|
||||
| "followUp"
|
||||
@@ -67,7 +66,6 @@ export const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {
|
||||
historySearch: "ctrl+r",
|
||||
expandTools: "ctrl+o",
|
||||
toggleThinking: "ctrl+t",
|
||||
toggleSessionNamedFilter: "ctrl+n",
|
||||
externalEditor: "ctrl+g",
|
||||
followUp: "ctrl+enter",
|
||||
dequeue: "alt+up",
|
||||
@@ -80,7 +78,6 @@ export const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {
|
||||
resume: [],
|
||||
toggleSTT: "alt+h",
|
||||
};
|
||||
|
||||
/**
|
||||
* All default keybindings (app + editor).
|
||||
*/
|
||||
@@ -103,7 +100,6 @@ const APP_ACTIONS: AppAction[] = [
|
||||
"historySearch",
|
||||
"expandTools",
|
||||
"toggleThinking",
|
||||
"toggleSessionNamedFilter",
|
||||
"externalEditor",
|
||||
"followUp",
|
||||
"dequeue",
|
||||
|
||||
@@ -1,34 +1,89 @@
|
||||
import { Editor, type KeyId, matchesKey, parseKittySequence } from "@oh-my-pi/pi-tui";
|
||||
import type { AppAction } from "../../config/keybindings";
|
||||
|
||||
type ConfigurableEditorAction = Extract<
|
||||
AppAction,
|
||||
| "interrupt"
|
||||
| "clear"
|
||||
| "exit"
|
||||
| "suspend"
|
||||
| "cycleThinkingLevel"
|
||||
| "cycleModelForward"
|
||||
| "cycleModelBackward"
|
||||
| "selectModel"
|
||||
| "expandTools"
|
||||
| "toggleThinking"
|
||||
| "externalEditor"
|
||||
| "historySearch"
|
||||
| "dequeue"
|
||||
| "pasteImage"
|
||||
| "copyPrompt"
|
||||
>;
|
||||
|
||||
const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
|
||||
interrupt: ["escape"],
|
||||
clear: ["ctrl+c"],
|
||||
exit: ["ctrl+d"],
|
||||
suspend: ["ctrl+z"],
|
||||
cycleThinkingLevel: ["shift+tab"],
|
||||
cycleModelForward: ["ctrl+p"],
|
||||
cycleModelBackward: ["shift+ctrl+p"],
|
||||
selectModel: ["ctrl+l"],
|
||||
expandTools: ["ctrl+o"],
|
||||
toggleThinking: ["ctrl+t"],
|
||||
externalEditor: ["ctrl+g"],
|
||||
historySearch: ["ctrl+r"],
|
||||
dequeue: ["alt+up"],
|
||||
pasteImage: ["ctrl+v"],
|
||||
copyPrompt: ["alt+shift+c"],
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom editor that handles Escape and Ctrl+C keys for coding-agent
|
||||
* Custom editor that handles configurable app-level shortcuts for coding-agent.
|
||||
*/
|
||||
export class CustomEditor extends Editor {
|
||||
onEscape?: () => void;
|
||||
shouldBypassAutocompleteOnEscape?: () => boolean;
|
||||
onCtrlC?: () => void;
|
||||
onCtrlD?: () => void;
|
||||
onShiftTab?: () => void;
|
||||
onCtrlP?: () => void;
|
||||
onShiftCtrlP?: () => void;
|
||||
onCtrlL?: () => void;
|
||||
onCtrlR?: () => void;
|
||||
onCtrlO?: () => void;
|
||||
onCtrlT?: () => void;
|
||||
onCtrlG?: () => void;
|
||||
onCtrlZ?: () => void;
|
||||
onQuestionMark?: () => void;
|
||||
onCapsLock?: () => void;
|
||||
onAltP?: () => void;
|
||||
/** Called when Alt+Shift+C is pressed to copy prompt to clipboard. */
|
||||
onClear?: () => void;
|
||||
onExit?: () => void;
|
||||
onCycleThinkingLevel?: () => void;
|
||||
onCycleModelForward?: () => void;
|
||||
onCycleModelBackward?: () => void;
|
||||
onSelectModel?: () => void;
|
||||
onExpandTools?: () => void;
|
||||
onToggleThinking?: () => void;
|
||||
onExternalEditor?: () => void;
|
||||
onHistorySearch?: () => void;
|
||||
onSuspend?: () => void;
|
||||
onShowHotkeys?: () => void;
|
||||
onQuickSelectModel?: () => void;
|
||||
/** Called when the configured copy-prompt shortcut is pressed. */
|
||||
onCopyPrompt?: () => void;
|
||||
/** Called when Ctrl+V is pressed. Returns true if handled (image found), false to fall through to text paste. */
|
||||
onCtrlV?: () => Promise<boolean>;
|
||||
/** Called when Alt+Up is pressed (dequeue keybinding). */
|
||||
onAltUp?: () => void;
|
||||
/** Called when the configured image-paste shortcut is pressed. */
|
||||
onPasteImage?: () => Promise<boolean>;
|
||||
/** Called when the configured dequeue shortcut is pressed. */
|
||||
onDequeue?: () => void;
|
||||
/** Called when Caps Lock is pressed. */
|
||||
onCapsLock?: () => void;
|
||||
|
||||
/** Custom key handlers from extensions */
|
||||
/** Custom key handlers from extensions and non-built-in app actions. */
|
||||
#customKeyHandlers = new Map<KeyId, () => void>();
|
||||
#actionKeys = new Map<ConfigurableEditorAction, KeyId[]>(
|
||||
Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]),
|
||||
);
|
||||
|
||||
setActionKeys(action: ConfigurableEditorAction, keys: KeyId[]): void {
|
||||
this.#actionKeys.set(action, [...keys]);
|
||||
}
|
||||
|
||||
#matchesAction(data: string, action: ConfigurableEditorAction): boolean {
|
||||
const keys = this.#actionKeys.get(action);
|
||||
if (!keys) return false;
|
||||
for (const key of keys) {
|
||||
if (matchesKey(data, key)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom key handler. Extensions use this for shortcuts.
|
||||
@@ -59,111 +114,111 @@ export class CustomEditor extends Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Ctrl+V for image paste (async - fires and handles result)
|
||||
if (matchesKey(data, "ctrl+v") && this.onCtrlV) {
|
||||
void this.onCtrlV();
|
||||
// Intercept configured image paste (async - fires and handles result)
|
||||
if (this.#matchesAction(data, "pasteImage") && this.onPasteImage) {
|
||||
void this.onPasteImage();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Ctrl+G for external editor
|
||||
if (matchesKey(data, "ctrl+g") && this.onCtrlG) {
|
||||
this.onCtrlG();
|
||||
// Intercept configured external editor shortcut
|
||||
if (this.#matchesAction(data, "externalEditor") && this.onExternalEditor) {
|
||||
this.onExternalEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Alt+P for quick model switching
|
||||
if (matchesKey(data, "alt+p") && this.onAltP) {
|
||||
this.onAltP();
|
||||
if (matchesKey(data, "alt+p") && this.onQuickSelectModel) {
|
||||
this.onQuickSelectModel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Ctrl+Z for suspend
|
||||
if (matchesKey(data, "ctrl+z") && this.onCtrlZ) {
|
||||
this.onCtrlZ();
|
||||
// Intercept configured suspend shortcut
|
||||
if (this.#matchesAction(data, "suspend") && this.onSuspend) {
|
||||
this.onSuspend();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Ctrl+T for thinking block visibility toggle
|
||||
if (matchesKey(data, "ctrl+t") && this.onCtrlT) {
|
||||
this.onCtrlT();
|
||||
// Intercept configured thinking block visibility toggle
|
||||
if (this.#matchesAction(data, "toggleThinking") && this.onToggleThinking) {
|
||||
this.onToggleThinking();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Ctrl+L for model selector
|
||||
if (matchesKey(data, "ctrl+l") && this.onCtrlL) {
|
||||
this.onCtrlL();
|
||||
// Intercept configured model selector shortcut
|
||||
if (this.#matchesAction(data, "selectModel") && this.onSelectModel) {
|
||||
this.onSelectModel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Ctrl+R for history search
|
||||
if (matchesKey(data, "ctrl+r") && this.onCtrlR) {
|
||||
this.onCtrlR();
|
||||
// Intercept configured history search shortcut
|
||||
if (this.#matchesAction(data, "historySearch") && this.onHistorySearch) {
|
||||
this.onHistorySearch();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Ctrl+O for tool output expansion
|
||||
if (matchesKey(data, "ctrl+o") && this.onCtrlO) {
|
||||
this.onCtrlO();
|
||||
// Intercept configured tool output expansion shortcut
|
||||
if (this.#matchesAction(data, "expandTools") && this.onExpandTools) {
|
||||
this.onExpandTools();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Shift+Ctrl+P for backward model cycling (check before Ctrl+P)
|
||||
if ((matchesKey(data, "shift+ctrl+p") || matchesKey(data, "ctrl+shift+p")) && this.onShiftCtrlP) {
|
||||
this.onShiftCtrlP();
|
||||
// Intercept configured backward model cycling (check before forward cycling)
|
||||
if (this.#matchesAction(data, "cycleModelBackward") && this.onCycleModelBackward) {
|
||||
this.onCycleModelBackward();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Ctrl+P for model cycling
|
||||
if (matchesKey(data, "ctrl+p") && this.onCtrlP) {
|
||||
this.onCtrlP();
|
||||
// Intercept configured forward model cycling
|
||||
if (this.#matchesAction(data, "cycleModelForward") && this.onCycleModelForward) {
|
||||
this.onCycleModelForward();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Shift+Tab for thinking level cycling
|
||||
if (matchesKey(data, "shift+tab") && this.onShiftTab) {
|
||||
this.onShiftTab();
|
||||
// Intercept configured thinking level cycling
|
||||
if (this.#matchesAction(data, "cycleThinkingLevel") && this.onCycleThinkingLevel) {
|
||||
this.onCycleThinkingLevel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Escape key.
|
||||
// Default behavior keeps autocomplete dismissal, but parent can prioritize global escape handling.
|
||||
if ((matchesKey(data, "escape") || matchesKey(data, "esc")) && this.onEscape) {
|
||||
// Intercept configured interrupt shortcut.
|
||||
// Default behavior keeps autocomplete dismissal, but parent can prioritize global interrupt handling.
|
||||
if (this.#matchesAction(data, "interrupt") && this.onEscape) {
|
||||
if (!this.isShowingAutocomplete() || this.shouldBypassAutocompleteOnEscape?.()) {
|
||||
this.onEscape();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Intercept Ctrl+C
|
||||
if (matchesKey(data, "ctrl+c") && this.onCtrlC) {
|
||||
this.onCtrlC();
|
||||
// Intercept configured clear shortcut
|
||||
if (this.#matchesAction(data, "clear") && this.onClear) {
|
||||
this.onClear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Ctrl+D (only when editor is empty)
|
||||
if (matchesKey(data, "ctrl+d")) {
|
||||
if (this.getText().length === 0 && this.onCtrlD) {
|
||||
this.onCtrlD();
|
||||
// Intercept configured exit shortcut (only when editor is empty)
|
||||
if (this.#matchesAction(data, "exit")) {
|
||||
if (this.getText().length === 0 && this.onExit) {
|
||||
this.onExit();
|
||||
}
|
||||
// Always consume Ctrl+D (don't pass to parent)
|
||||
// Always consume exit shortcut (don't pass to parent)
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Alt+Up for dequeue (restore queued message to editor)
|
||||
if (matchesKey(data, "alt+up") && this.onAltUp) {
|
||||
this.onAltUp();
|
||||
// Intercept configured dequeue shortcut (restore queued message to editor)
|
||||
if (this.#matchesAction(data, "dequeue") && this.onDequeue) {
|
||||
this.onDequeue();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Alt+Shift+C to copy prompt to clipboard
|
||||
if (matchesKey(data, "alt+shift+c") && this.onCopyPrompt) {
|
||||
// Intercept configured copy-prompt shortcut
|
||||
if (this.#matchesAction(data, "copyPrompt") && this.onCopyPrompt) {
|
||||
this.onCopyPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept ? when editor is empty to show hotkeys
|
||||
if (data === "?" && this.getText().length === 0 && this.onQuestionMark) {
|
||||
this.onQuestionMark();
|
||||
if (data === "?" && this.getText().length === 0 && this.onShowHotkeys) {
|
||||
this.onShowHotkeys();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -505,18 +505,7 @@ export class CommandController {
|
||||
}
|
||||
|
||||
handleHotkeysCommand(): void {
|
||||
const expandToolsKey = this.ctx.keybindings.getDisplayString("expandTools") || "Ctrl+O";
|
||||
const planModeKey = this.ctx.keybindings.getDisplayString("togglePlanMode") || "Alt+Shift+P";
|
||||
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 = buildHotkeysMarkdown({
|
||||
expandToolsKey,
|
||||
planModeKey,
|
||||
sttKey,
|
||||
copyLineKey,
|
||||
copyPromptKey,
|
||||
});
|
||||
const hotkeys = buildHotkeysMarkdown({ keybindings: this.ctx.keybindings });
|
||||
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));
|
||||
|
||||
@@ -26,6 +26,7 @@ export class InputController {
|
||||
constructor(private ctx: InteractiveModeContext) {}
|
||||
|
||||
setupKeyHandlers(): void {
|
||||
this.ctx.editor.setActionKeys("interrupt", this.ctx.keybindings.getKeys("interrupt"));
|
||||
this.ctx.editor.shouldBypassAutocompleteOnEscape = () =>
|
||||
Boolean(
|
||||
this.ctx.loadingAnimation ||
|
||||
@@ -64,7 +65,7 @@ export class InputController {
|
||||
} else if (this.ctx.session.isStreaming) {
|
||||
void this.ctx.session.abort();
|
||||
} else if (!this.ctx.editor.getText().trim()) {
|
||||
// Double-escape with empty editor triggers /tree, /branch, or nothing based on setting
|
||||
// Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
|
||||
const action = settings.get("doubleEscapeAction");
|
||||
if (action !== "none") {
|
||||
const now = Date.now();
|
||||
@@ -82,42 +83,44 @@ export class InputController {
|
||||
}
|
||||
};
|
||||
|
||||
this.ctx.editor.onCtrlC = () => this.handleCtrlC();
|
||||
this.ctx.editor.onCtrlD = () => this.handleCtrlD();
|
||||
this.ctx.editor.onCtrlZ = () => this.handleCtrlZ();
|
||||
this.ctx.editor.onShiftTab = () => this.cycleThinkingLevel();
|
||||
this.ctx.editor.onCtrlP = () => this.cycleRoleModel();
|
||||
this.ctx.editor.onShiftCtrlP = () => this.cycleRoleModel({ temporary: true });
|
||||
this.ctx.editor.onAltP = () => this.ctx.showModelSelector({ temporaryOnly: true });
|
||||
this.ctx.editor.setActionKeys("clear", this.ctx.keybindings.getKeys("clear"));
|
||||
this.ctx.editor.onClear = () => this.handleCtrlC();
|
||||
this.ctx.editor.setActionKeys("exit", this.ctx.keybindings.getKeys("exit"));
|
||||
this.ctx.editor.onExit = () => this.handleCtrlD();
|
||||
this.ctx.editor.setActionKeys("suspend", this.ctx.keybindings.getKeys("suspend"));
|
||||
this.ctx.editor.onSuspend = () => this.handleCtrlZ();
|
||||
this.ctx.editor.setActionKeys("cycleThinkingLevel", this.ctx.keybindings.getKeys("cycleThinkingLevel"));
|
||||
this.ctx.editor.onCycleThinkingLevel = () => this.cycleThinkingLevel();
|
||||
this.ctx.editor.setActionKeys("cycleModelForward", this.ctx.keybindings.getKeys("cycleModelForward"));
|
||||
this.ctx.editor.onCycleModelForward = () => this.cycleRoleModel();
|
||||
this.ctx.editor.setActionKeys("cycleModelBackward", this.ctx.keybindings.getKeys("cycleModelBackward"));
|
||||
this.ctx.editor.onCycleModelBackward = () => this.cycleRoleModel({ temporary: true });
|
||||
this.ctx.editor.onQuickSelectModel = () => this.ctx.showModelSelector({ temporaryOnly: true });
|
||||
|
||||
// Global debug handler on TUI (works regardless of focus)
|
||||
this.ctx.ui.onDebug = () => this.ctx.showDebugSelector();
|
||||
this.ctx.editor.onCtrlL = () => this.ctx.showModelSelector();
|
||||
this.ctx.editor.onCtrlR = () => this.ctx.showHistorySearch();
|
||||
this.ctx.editor.onCtrlT = () => this.ctx.toggleTodoExpansion();
|
||||
this.ctx.editor.onCtrlG = () => void this.openExternalEditor();
|
||||
this.ctx.editor.onQuestionMark = () => this.ctx.handleHotkeysCommand();
|
||||
this.ctx.editor.onCtrlV = () => this.handleImagePaste();
|
||||
const copyPromptKeys = this.ctx.keybindings.getKeys("copyPrompt");
|
||||
this.ctx.editor.onCopyPrompt = copyPromptKeys.includes("alt+shift+c") ? () => this.handleCopyPrompt() : undefined;
|
||||
this.ctx.editor.setActionKeys("selectModel", this.ctx.keybindings.getKeys("selectModel"));
|
||||
this.ctx.editor.onSelectModel = () => this.ctx.showModelSelector();
|
||||
this.ctx.editor.setActionKeys("historySearch", this.ctx.keybindings.getKeys("historySearch"));
|
||||
this.ctx.editor.onHistorySearch = () => this.ctx.showHistorySearch();
|
||||
this.ctx.editor.setActionKeys("toggleThinking", this.ctx.keybindings.getKeys("toggleThinking"));
|
||||
this.ctx.editor.onToggleThinking = () => this.ctx.toggleThinkingBlockVisibility();
|
||||
this.ctx.editor.setActionKeys("externalEditor", this.ctx.keybindings.getKeys("externalEditor"));
|
||||
this.ctx.editor.onExternalEditor = () => void this.openExternalEditor();
|
||||
this.ctx.editor.onShowHotkeys = () => this.ctx.handleHotkeysCommand();
|
||||
this.ctx.editor.setActionKeys("pasteImage", this.ctx.keybindings.getKeys("pasteImage"));
|
||||
this.ctx.editor.onPasteImage = () => this.handleImagePaste();
|
||||
this.ctx.editor.setActionKeys("copyPrompt", this.ctx.keybindings.getKeys("copyPrompt"));
|
||||
this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt();
|
||||
this.ctx.editor.setActionKeys("expandTools", this.ctx.keybindings.getKeys("expandTools"));
|
||||
this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
|
||||
this.ctx.editor.setActionKeys("dequeue", this.ctx.keybindings.getKeys("dequeue"));
|
||||
this.ctx.editor.onDequeue = () => this.handleDequeue();
|
||||
|
||||
this.ctx.editor.clearCustomKeyHandlers();
|
||||
// Wire up extension shortcuts
|
||||
this.registerExtensionShortcuts();
|
||||
|
||||
const expandToolsKeys = this.ctx.keybindings.getKeys("expandTools");
|
||||
this.ctx.editor.onCtrlO = expandToolsKeys.includes("ctrl+o") ? () => this.toggleToolOutputExpansion() : undefined;
|
||||
for (const key of expandToolsKeys) {
|
||||
if (key === "ctrl+o") continue;
|
||||
this.ctx.editor.setCustomKeyHandler(key, () => this.toggleToolOutputExpansion());
|
||||
}
|
||||
|
||||
const dequeueKeys = this.ctx.keybindings.getKeys("dequeue");
|
||||
this.ctx.editor.onAltUp = dequeueKeys.includes("alt+up") ? () => this.handleDequeue() : undefined;
|
||||
for (const key of dequeueKeys) {
|
||||
if (key === "alt+up") continue;
|
||||
this.ctx.editor.setCustomKeyHandler(key, () => this.handleDequeue());
|
||||
}
|
||||
|
||||
const planModeKeys = this.ctx.keybindings.getKeys("togglePlanMode");
|
||||
for (const key of planModeKeys) {
|
||||
this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handlePlanModeCommand());
|
||||
@@ -144,10 +147,6 @@ export class InputController {
|
||||
for (const key of this.ctx.keybindings.getKeys("copyLine")) {
|
||||
this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyCurrentLine());
|
||||
}
|
||||
for (const key of copyPromptKeys) {
|
||||
if (key === "alt+shift+c") continue;
|
||||
this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyPrompt());
|
||||
}
|
||||
|
||||
this.ctx.editor.onChange = (text: string) => {
|
||||
const wasBashMode = this.ctx.isBashMode;
|
||||
|
||||
@@ -555,7 +555,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
});
|
||||
if (visibleTasks.length < activePhase.tasks.length) {
|
||||
const remaining = activePhase.tasks.length - visibleTasks.length;
|
||||
lines.push(theme.fg("muted", `${indent} ${hook} +${remaining} more (Ctrl+T to expand)`));
|
||||
lines.push(theme.fg("muted", `${indent} ${hook} +${remaining} more`));
|
||||
}
|
||||
this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
|
||||
return;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { AppAction, KeybindingsManager } from "../../config/keybindings";
|
||||
|
||||
export interface HotkeysMarkdownBindings {
|
||||
expandToolsKey: string;
|
||||
planModeKey: string;
|
||||
sttKey: string;
|
||||
copyLineKey: string;
|
||||
copyPromptKey: string;
|
||||
keybindings: Pick<KeybindingsManager, "getDisplayString">;
|
||||
}
|
||||
|
||||
function appKey(bindings: HotkeysMarkdownBindings, action: AppAction): string {
|
||||
return bindings.keybindings.getDisplayString(action) || "Disabled";
|
||||
}
|
||||
|
||||
export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string {
|
||||
const { expandToolsKey, planModeKey, sttKey, copyLineKey, copyPromptKey } = bindings;
|
||||
return [
|
||||
"**Navigation**",
|
||||
"| Key | Action |",
|
||||
@@ -25,28 +26,29 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
|
||||
"| `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 |`,
|
||||
`| \`${appKey(bindings, "copyLine")}\` | Copy current line |`,
|
||||
`| \`${appKey(bindings, "copyPrompt")}\` | 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) |",
|
||||
`| \`${appKey(bindings, "interrupt")}\` | Cancel autocomplete / interrupt active work |`,
|
||||
`| \`${appKey(bindings, "clear")}\` | Clear editor (first) / exit (second) |`,
|
||||
`| \`${appKey(bindings, "exit")}\` | Exit (when editor is empty) |`,
|
||||
`| \`${appKey(bindings, "suspend")}\` | Suspend to background |`,
|
||||
`| \`${appKey(bindings, "cycleThinkingLevel")}\` | Cycle thinking level |`,
|
||||
`| \`${appKey(bindings, "cycleModelForward")}\` | Cycle role models (slow/default/smol) |`,
|
||||
`| \`${appKey(bindings, "cycleModelBackward")}\` | 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 |`,
|
||||
`| \`${appKey(bindings, "selectModel")}\` | Select model (set roles) |`,
|
||||
`| \`${appKey(bindings, "togglePlanMode")}\` | Toggle plan mode |`,
|
||||
`| \`${appKey(bindings, "historySearch")}\` | Search prompt history |`,
|
||||
`| \`${appKey(bindings, "expandTools")}\` | Toggle tool output expansion |`,
|
||||
`| \`${appKey(bindings, "toggleThinking")}\` | Toggle thinking block visibility |`,
|
||||
`| \`${appKey(bindings, "externalEditor")}\` | Edit message in external editor |`,
|
||||
`| \`${appKey(bindings, "pasteImage")}\` | Paste image from clipboard |`,
|
||||
`| \`${appKey(bindings, "toggleSTT")}\` | Toggle speech-to-text recording |`,
|
||||
"| `#` | Open prompt actions |",
|
||||
"| `/` | Slash commands |",
|
||||
"| `!` | Run bash command |",
|
||||
|
||||
@@ -6,24 +6,29 @@ type FakeEditor = {
|
||||
onEscape?: () => void;
|
||||
onSubmit?: (text: string) => Promise<void>;
|
||||
shouldBypassAutocompleteOnEscape?: () => boolean;
|
||||
onCtrlC?: () => void;
|
||||
onCtrlD?: () => void;
|
||||
onCtrlZ?: () => void;
|
||||
onShiftTab?: () => void;
|
||||
onCtrlP?: () => void;
|
||||
onShiftCtrlP?: () => void;
|
||||
onAltP?: () => void;
|
||||
onCtrlL?: () => void;
|
||||
onCtrlR?: () => void;
|
||||
onQuestionMark?: () => void;
|
||||
onCtrlV?: () => void;
|
||||
onClear?: () => void;
|
||||
onExit?: () => void;
|
||||
onSuspend?: () => void;
|
||||
onCycleThinkingLevel?: () => void;
|
||||
onCycleModelForward?: () => void;
|
||||
onCycleModelBackward?: () => void;
|
||||
onQuickSelectModel?: () => void;
|
||||
onSelectModel?: () => void;
|
||||
onHistorySearch?: () => void;
|
||||
onShowHotkeys?: () => void;
|
||||
onPasteImage?: () => void;
|
||||
onCopyPrompt?: () => void;
|
||||
onAltUp?: () => void;
|
||||
onExpandTools?: () => void;
|
||||
onToggleThinking?: () => void;
|
||||
onExternalEditor?: () => void;
|
||||
onDequeue?: () => void;
|
||||
onChange?: (text: string) => void;
|
||||
setText(text: string): void;
|
||||
getText(): string;
|
||||
addToHistory(text: string): void;
|
||||
setActionKeys(action: string, keys: string[]): void;
|
||||
setCustomKeyHandler(key: string, handler: () => void): void;
|
||||
clearCustomKeyHandlers(): void;
|
||||
};
|
||||
|
||||
function createSubmission(input: {
|
||||
@@ -83,7 +88,9 @@ function createContext(): {
|
||||
return editorText;
|
||||
},
|
||||
addToHistory: vi.fn(),
|
||||
setActionKeys: vi.fn(),
|
||||
setCustomKeyHandler: vi.fn(),
|
||||
clearCustomKeyHandlers: vi.fn(),
|
||||
};
|
||||
|
||||
let ctx!: InteractiveModeContext;
|
||||
|
||||
@@ -3,12 +3,31 @@ import { buildHotkeysMarkdown } from "../../../src/modes/utils/hotkeys-markdown"
|
||||
|
||||
describe("buildHotkeysMarkdown", () => {
|
||||
it("emits flush-left markdown so headings and tables are parsed instead of treated as indented text", () => {
|
||||
const displayStrings: Record<string, string> = {
|
||||
copyLine: "Alt+Shift+L",
|
||||
copyPrompt: "Ctrl+Shift+P",
|
||||
togglePlanMode: "Alt+M",
|
||||
expandTools: "Ctrl+O",
|
||||
interrupt: "Esc",
|
||||
clear: "Ctrl+C",
|
||||
exit: "Ctrl+D",
|
||||
suspend: "Ctrl+Z",
|
||||
cycleThinkingLevel: "Shift+Tab",
|
||||
cycleModelForward: "Ctrl+P",
|
||||
cycleModelBackward: "Shift+Ctrl+P",
|
||||
selectModel: "Ctrl+L",
|
||||
historySearch: "Ctrl+R",
|
||||
toggleThinking: "Ctrl+T",
|
||||
externalEditor: "Ctrl+G",
|
||||
pasteImage: "Ctrl+V",
|
||||
toggleSTT: "Alt+H",
|
||||
};
|
||||
const markdown = buildHotkeysMarkdown({
|
||||
expandToolsKey: "Ctrl+O",
|
||||
planModeKey: "Alt+M",
|
||||
sttKey: "Alt+H",
|
||||
copyLineKey: "Alt+Shift+L",
|
||||
copyPromptKey: "Ctrl+Shift+P",
|
||||
keybindings: {
|
||||
getDisplayString(action) {
|
||||
return displayStrings[action] ?? "Disabled";
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const lines = markdown.split("\n");
|
||||
|
||||
Reference in New Issue
Block a user