From 123498dc59b899c85a16dfd7bd9bbdbb4fef4c86 Mon Sep 17 00:00:00 2001 From: dawn <90008@klbr.net> Date: Thu, 17 Sep 2026 11:52:05 +0300 Subject: [PATCH] fix(coding-agent): let alt+k toggle a live spill, since shift+alt+k needs a kitty terminal --- packages/coding-agent/docs/keybindings.md | 4 ++-- packages/coding-agent/src/core/keybindings.ts | 2 +- .../src/modes/interactive/interactive-mode.ts | 15 ++++++++++++++- .../src/modes/interactive/spill-markers.ts | 9 +++++++++ packages/coding-agent/test/spill-markers.test.ts | 6 ++++++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/docs/keybindings.md b/packages/coding-agent/docs/keybindings.md index 5c00c35ce..17d200562 100644 --- a/packages/coding-agent/docs/keybindings.md +++ b/packages/coding-agent/docs/keybindings.md @@ -97,8 +97,8 @@ Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1 | `app.exit` | `ctrl+d` | Exit (when editor empty) | | `app.suspend` | `ctrl+z` (none on Windows) | Suspend to background | | `app.editor.external` | `ctrl+g` | Open in external editor (`$VISUAL` or `$EDITOR`) | -| `app.editor.spill` | `alt+k` | Convert most recent paste to spill | -| `app.editor.spillSecret` | `shift+alt+k` | Convert most recent paste to secret spill | +| `app.editor.spill` | `alt+k` | Spill the most recent paste, or toggle a live spill between blob and secret | +| `app.editor.spillSecret` | `shift+alt+k` | Convert most recent paste to secret spill (needs a terminal that reports shifted keys, e.g. Kitty keyboard protocol) | | `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows) | Paste image from clipboard | | `app.clipboard.copyLoginUrl` | `c`, `alt+c` | Copy the sign-in URL from a login dialog | diff --git a/packages/coding-agent/src/core/keybindings.ts b/packages/coding-agent/src/core/keybindings.ts index c7c1a2573..94fad9d22 100644 --- a/packages/coding-agent/src/core/keybindings.ts +++ b/packages/coding-agent/src/core/keybindings.ts @@ -118,7 +118,7 @@ export const KEYBINDINGS = { }, "app.editor.spill": { defaultKeys: "alt+k", - description: "Convert most recent paste to spill", + description: "Spill the most recent paste, or toggle a live spill between blob and secret", }, "app.editor.spillSecret": { defaultKeys: "shift+alt+k", diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index d372390d2..8f9fd674f 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -331,6 +331,7 @@ import { type SpillEntry, type SpillMode, spillMarkerIds, + toggledSpillMode, } from "./spill-markers.js"; import { getAvailableThemes, @@ -4805,7 +4806,7 @@ export class InteractiveMode { this.showHeartbeatManager(); }); this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor()); - this.defaultEditor.onAction("app.editor.spill", () => this.convertRecentPasteToSpill("blob")); + this.defaultEditor.onAction("app.editor.spill", () => this.toggleRecentPasteSpill()); this.defaultEditor.onAction("app.editor.spillSecret", () => this.convertRecentPasteToSpill("secret")); this.defaultEditor.onAction("app.prompt.stash", () => this.handlePromptStash()); this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp()); @@ -5176,6 +5177,18 @@ export class InteractiveMode { return false; } + private toggleRecentPasteSpill(): void { + const spillId = this.recentPaste?.spillId; + const entry = spillId === undefined ? undefined : this.pastedSpills.get(spillId); + if (entry) { + entry.mode = toggledSpillMode(entry.mode); + this.showStatus(`Spill #${entry.id} set to ${entry.mode} mode`); + this.ui.requestRender(); + return; + } + this.convertRecentPasteToSpill("blob"); + } + private convertRecentPasteToSpill(mode: SpillMode): void { if (this.getCurrentRemoteHost()) { if (!this.warnedRemoteSpillFallback) { diff --git a/packages/coding-agent/src/modes/interactive/spill-markers.ts b/packages/coding-agent/src/modes/interactive/spill-markers.ts index e3ceac868..c98ea541e 100644 --- a/packages/coding-agent/src/modes/interactive/spill-markers.ts +++ b/packages/coding-agent/src/modes/interactive/spill-markers.ts @@ -102,6 +102,15 @@ export function evictSpillsToBudget( } } +/** + * Modes a live spill cycles through. A legacy terminal cannot deliver `shift+alt+k` + * (a shifted letter arrives as its uppercase form, which the key matcher cannot tell + * apart from the unshifted key), so `alt+k` has to be able to reach secret mode on its own. + */ +export function toggledSpillMode(current: SpillMode): SpillMode { + return current === "secret" ? "blob" : "secret"; +} + /** Build the stub replacement text telling the model about the spilled file. */ export function formatSpillStub(info: SpillStubInfo): string { const instruction = diff --git a/packages/coding-agent/test/spill-markers.test.ts b/packages/coding-agent/test/spill-markers.test.ts index 614120e4b..72821245f 100644 --- a/packages/coding-agent/test/spill-markers.test.ts +++ b/packages/coding-agent/test/spill-markers.test.ts @@ -12,6 +12,7 @@ import { remapSpillMarkers, resolveSpillsInText, type SpillEntry, + toggledSpillMode, writeSpillFile, } from "../src/modes/interactive/spill-markers.js"; import { createHarness, getUserTexts } from "./suite/harness.js"; @@ -33,6 +34,11 @@ describe("spill markers pure logic", () => { expect(collectMarkedSpills(pending, "back [spill #1]")).toEqual(["payload"]); }); + test("toggledSpillMode cycles a live spill so alt+k can reach secret mode", () => { + expect(toggledSpillMode("blob")).toBe("secret"); + expect(toggledSpillMode("secret")).toBe("blob"); + }); + test("remapSpillMarkers updates matching marker ids", () => { expect(remapSpillMarkers("[spill #1] [spill #2]", new Map([[1, 10]]))).toBe("[spill #10] [spill #2]"); }); -- 2.51.2