From 2714bd74188d2d81c670fe9eaa0d99b43fcf17c0 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 18 Aug 2026 15:22:45 +0200 Subject: [PATCH 1/2] fix(coding-agent): let a raw \n insert a newline instead of toggling edit diffs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.7.3 added app.edits.expand with the default ctrl+j (#1388). The custom editor dispatches app actions before the base editor handling, and a raw "\n" byte decodes as ctrl+j — but that byte is exactly what Shift+Enter sends in terminals that map it to a literal newline (the mapping our own keys.ts comments recommend for Ghostty), and ctrl+j is itself a traditional newline key. Since 0.7.3, that input toggled edit diffs and Shift+Enter stopped producing newlines outside kitty-protocol terminals. Skip app-action dispatch for the raw "\n" byte in the editor and in the subagent-line key handler so it reaches the newline handling; ctrl+j still triggers the toggle via the kitty CSI-u encoding, which is unambiguous. --- packages/coding-agent/CHANGELOG.md | 1 + .../interactive/components/custom-editor.ts | 8 ++++++- .../src/modes/interactive/interactive-mode.ts | 4 +++- .../coding-agent/test/custom-editor.test.ts | 24 +++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 379b4ced95..73fdf74faa 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] +- Fixed Shift+Enter no longer inserting a newline in terminals that send a literal `\n` (for example a Ghostty `shift+enter=text:\n` mapping): the byte decoded as `ctrl+j` and triggered the new edit-diff toggle instead of the editor newline. - Removed a system prompt paragraph referring to an async `bash()` kernel helper and managed jobs that do not exist in the runtime. - Changed RLM guidance to orchestrate independent workers in parallel, use available async shell helpers safely, end the turn instead of sleeping, polling, or blocking on long awaits, provide proactive outcome-focused progress updates from root agents, and use simplified technical English for user-facing prose. - Fixed new top-level daemon sessions inheriting an RLM child depth from the supervisor process. diff --git a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts index 9e36e95ca8..f4d48b59b7 100644 --- a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts @@ -198,9 +198,15 @@ export class CustomEditor extends Editor { // Fall through to editor handling for delete-char-forward when not empty } - // Check all other app actions + // Check all other app actions. + // A raw "\n" byte is ambiguous: it decodes as ctrl+j, but it is also what + // Shift+Enter sends in terminals that map it to a literal newline (and a + // traditional newline key itself). Let the editor's newline handling win; + // ctrl+j still reaches actions from kitty-protocol terminals as CSI-u. + const isLegacyNewlineByte = data === "\n"; for (const [action, handler] of this.actionHandlers) { if ( + !isLegacyNewlineByte && action !== "app.input.clear" && action !== "app.exit" && (action !== "app.shortcuts" || this.getText().length === 0) && diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 02c64a8d4d..4359af5f99 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -5900,7 +5900,9 @@ export class InteractiveMode { this.toggleAgentMessageExpansion(); return; } - if (this.keybindings.matches(data, "app.edits.expand")) { + // A raw "\n" decodes as ctrl+j but is Shift+Enter's newline in terminals + // that map it literally; hand it to the editor instead of toggling diffs. + if (data !== "\n" && this.keybindings.matches(data, "app.edits.expand")) { this.toggleEditDiffExpansion(); return; } diff --git a/packages/coding-agent/test/custom-editor.test.ts b/packages/coding-agent/test/custom-editor.test.ts index bef6fc798a..ef3336e3ce 100644 --- a/packages/coding-agent/test/custom-editor.test.ts +++ b/packages/coding-agent/test/custom-editor.test.ts @@ -78,6 +78,30 @@ describe("CustomEditor", () => { expect(editor.getText()).toBe("/"); }); + it("inserts a newline for a raw \\n byte instead of firing the ctrl+j edit-diff action", () => { + const editor = new CustomEditor(fakeTui, editorTheme, new KeybindingsManager()); + const toggleEditDiffs = vi.fn(); + editor.onAction("app.edits.expand", toggleEditDiffs); + + editor.handleInput("a"); + editor.handleInput("\n"); + editor.handleInput("b"); + + expect(toggleEditDiffs).not.toHaveBeenCalled(); + expect(editor.getText()).toBe("a\nb"); + }); + + it("still fires the edit-diff action for kitty CSI-u ctrl+j", () => { + const editor = new CustomEditor(fakeTui, editorTheme, new KeybindingsManager()); + const toggleEditDiffs = vi.fn(); + editor.onAction("app.edits.expand", toggleEditDiffs); + + editor.handleInput("\x1b[106;5u"); + + expect(toggleEditDiffs).toHaveBeenCalledOnce(); + expect(editor.getText()).toBe(""); + }); + it("routes Escape through its handler while dismissing autocomplete", async () => { const editor = new CustomEditor(fakeTui, editorTheme, new KeybindingsManager()); const handler = vi.fn(); From 6f6638a11e97f2f253e353d7d20fb203bb8e39ed Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 18 Aug 2026 15:35:12 +0200 Subject: [PATCH 2/2] Trim the collision comments to one line each --- .../src/modes/interactive/components/custom-editor.ts | 10 +++------- .../src/modes/interactive/interactive-mode.ts | 3 +-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts index f4d48b59b7..058e4076be 100644 --- a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts @@ -198,15 +198,11 @@ export class CustomEditor extends Editor { // Fall through to editor handling for delete-char-forward when not empty } - // Check all other app actions. - // A raw "\n" byte is ambiguous: it decodes as ctrl+j, but it is also what - // Shift+Enter sends in terminals that map it to a literal newline (and a - // traditional newline key itself). Let the editor's newline handling win; - // ctrl+j still reaches actions from kitty-protocol terminals as CSI-u. - const isLegacyNewlineByte = data === "\n"; + // Check all other app actions. A raw "\n" is Shift+Enter's newline in some + // terminals, so it goes to the editor even though it decodes as ctrl+j. for (const [action, handler] of this.actionHandlers) { if ( - !isLegacyNewlineByte && + data !== "\n" && action !== "app.input.clear" && action !== "app.exit" && (action !== "app.shortcuts" || this.getText().length === 0) && diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 4359af5f99..15f4f79e09 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -5900,8 +5900,7 @@ export class InteractiveMode { this.toggleAgentMessageExpansion(); return; } - // A raw "\n" decodes as ctrl+j but is Shift+Enter's newline in terminals - // that map it literally; hand it to the editor instead of toggling diffs. + // A raw "\n" is a newline for the editor, not ctrl+j. if (data !== "\n" && this.keybindings.matches(data, "app.edits.expand")) { this.toggleEditDiffExpansion(); return;