From 902d5d3d05ae7788fc92a19f8e6b506fcbe52707 Mon Sep 17 00:00:00 2001 From: Thomas Mustier Date: Fri, 9 Jan 2026 19:31:51 +0000 Subject: [PATCH] Add Alt+Up hotkey to restore queued messages (#604) --- packages/coding-agent/src/core/keybindings.ts | 5 +- .../src/modes/interactive/interactive-mode.ts | 47 +++++++++++++++---- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/packages/coding-agent/src/core/keybindings.ts b/packages/coding-agent/src/core/keybindings.ts index 1d00e7ff..9e6ba617 100644 --- a/packages/coding-agent/src/core/keybindings.ts +++ b/packages/coding-agent/src/core/keybindings.ts @@ -26,7 +26,8 @@ export type AppAction = | "expandTools" | "toggleThinking" | "externalEditor" - | "followUp"; + | "followUp" + | "dequeue"; /** * All configurable actions. @@ -56,6 +57,7 @@ export const DEFAULT_APP_KEYBINDINGS: Record = { toggleThinking: "ctrl+t", externalEditor: "ctrl+g", followUp: "alt+enter", + dequeue: "alt+up", }; /** @@ -80,6 +82,7 @@ const APP_ACTIONS: AppAction[] = [ "toggleThinking", "externalEditor", "followUp", + "dequeue", ]; function isAppAction(action: string): action is AppAction { diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 3baf5054..f8cc3fae 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -311,6 +311,7 @@ export class InteractiveMode { const toggleThinking = formatStartupKey(kb.getKeys("toggleThinking")); const externalEditor = formatStartupKey(kb.getKeys("externalEditor")); const followUp = formatStartupKey(kb.getKeys("followUp")); + const dequeue = formatStartupKey(kb.getKeys("dequeue")); const instructions = theme.fg("dim", interrupt) + @@ -361,6 +362,9 @@ export class InteractiveMode { theme.fg("dim", followUp) + theme.fg("muted", " to queue follow-up") + "\n" + + theme.fg("dim", dequeue) + + theme.fg("muted", " to restore queued messages") + + "\n" + theme.fg("dim", "ctrl+v") + theme.fg("muted", " to paste image") + "\n" + @@ -1274,15 +1278,7 @@ export class InteractiveMode { // so they work correctly regardless of which editor is active this.defaultEditor.onEscape = () => { if (this.loadingAnimation) { - // Abort and restore queued messages to editor - const { steering, followUp } = this.session.clearQueue(); - const allQueued = [...steering, ...followUp]; - const queuedText = allQueued.join("\n\n"); - const currentText = this.editor.getText(); - const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n"); - this.editor.setText(combinedText); - this.updatePendingMessagesDisplay(); - this.agent.abort(); + this.restoreQueuedMessagesToEditor({ abort: true }); } else if (this.session.isBashRunning) { this.session.abortBash(); } else if (this.isBashMode) { @@ -1320,6 +1316,7 @@ export class InteractiveMode { this.defaultEditor.onAction("toggleThinking", () => this.toggleThinkingBlockVisibility()); this.defaultEditor.onAction("externalEditor", () => this.openExternalEditor()); this.defaultEditor.onAction("followUp", () => this.handleFollowUp()); + this.defaultEditor.onAction("dequeue", () => this.handleDequeue()); this.defaultEditor.onChange = (text: string) => { const wasBashMode = this.isBashMode; @@ -2080,6 +2077,15 @@ export class InteractiveMode { } } + private handleDequeue(): void { + const restored = this.restoreQueuedMessagesToEditor(); + if (restored === 0) { + this.showStatus("No queued messages to restore"); + } else { + this.showStatus(`Restored ${restored} queued message${restored > 1 ? "s" : ""} to editor`); + } + } + private updateEditorBorderColor(): void { if (this.isBashMode) { this.editor.borderColor = theme.getBashModeBorderColor(); @@ -2253,6 +2259,27 @@ export class InteractiveMode { } } + private restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number { + const { steering, followUp } = this.session.clearQueue(); + const allQueued = [...steering, ...followUp]; + if (allQueued.length === 0) { + this.updatePendingMessagesDisplay(); + if (options?.abort) { + this.agent.abort(); + } + return 0; + } + const queuedText = allQueued.join("\n\n"); + const currentText = options?.currentText ?? this.editor.getText(); + const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n"); + this.editor.setText(combinedText); + this.updatePendingMessagesDisplay(); + if (options?.abort) { + this.agent.abort(); + } + return allQueued.length; + } + private queueCompactionMessage(text: string, mode: "steer" | "followUp"): void { this.compactionQueuedMessages.push({ text, mode }); this.editor.addToHistory?.(text); @@ -3049,6 +3076,7 @@ export class InteractiveMode { const toggleThinking = this.getAppKeyDisplay("toggleThinking"); const externalEditor = this.getAppKeyDisplay("externalEditor"); const followUp = this.getAppKeyDisplay("followUp"); + const dequeue = this.getAppKeyDisplay("dequeue"); let hotkeys = ` **Navigation** @@ -3082,6 +3110,7 @@ export class InteractiveMode { | \`${toggleThinking}\` | Toggle thinking block visibility | | \`${externalEditor}\` | Edit message in external editor | | \`${followUp}\` | Queue follow-up message | +| \`${dequeue}\` | Restore queued messages | | \`Ctrl+V\` | Paste image from clipboard | | \`/\` | Slash commands | | \`!\` | Run bash command |