From ccfba49879df1dfe50f53651bd963f2b84bcce5f Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Thu, 6 Aug 2026 17:47:00 +0900 Subject: [PATCH] fix(ui): recognize ctrl+enter as newline in prompt input (#169) Terminals with modifyOtherKeys enabled report Ctrl+Enter as ESC[13;5u / ESC[27;5;13~. Parse these as return+ctrl and let getPromptReturnKeyAction insert a newline instead of submitting. (cherry picked from commit f1806d508241a9e8519ed8f493ffc4be8b78c6a4) --- packages/cli/src/tests/prompt-input-keys.test.ts | 11 +++++++++++ packages/cli/src/ui/hooks/useTerminalInput.ts | 10 ++++++++-- packages/cli/src/ui/views/PromptInput.tsx | 8 +++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/tests/prompt-input-keys.test.ts b/packages/cli/src/tests/prompt-input-keys.test.ts index 2b3e3791..77a8f15e 100644 --- a/packages/cli/src/tests/prompt-input-keys.test.ts +++ b/packages/cli/src/tests/prompt-input-keys.test.ts @@ -186,6 +186,17 @@ test("parseTerminalInput recognizes alternate shifted return sequences", () => { } }); +test("parseTerminalInput recognizes ctrl+enter modifyOtherKeys sequences as newline", () => { + for (const sequence of ["\u001B[13;5u", "\u001B[27;5;13~"]) { + const { input, key } = parseTerminalInput(sequence); + assert.equal(key.return, true); + assert.equal(key.ctrl, true); + assert.equal(key.shift, false); + assert.equal(getPromptReturnKeyAction(key), "newline"); + assert.equal(input.startsWith("["), true); + } +}); + test("terminal extended key helpers request and restore modifyOtherKeys mode", () => { assert.equal(enableTerminalExtendedKeys(), "\u001B[>4;1m"); assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m"); diff --git a/packages/cli/src/ui/hooks/useTerminalInput.ts b/packages/cli/src/ui/hooks/useTerminalInput.ts index 9e985fa9..a67ff442 100644 --- a/packages/cli/src/ui/hooks/useTerminalInput.ts +++ b/packages/cli/src/ui/hooks/useTerminalInput.ts @@ -30,6 +30,8 @@ const HOME_SEQUENCES = new Set(["\u001B[H", "\u001B[1~", "\u001B[7~", "\u001BOH" const END_SEQUENCES = new Set(["\u001B[F", "\u001B[4~", "\u001B[8~", "\u001BOF"]); const SHIFT_RETURN_SEQUENCES = new Set(["\u001B\r", "\u001B[13;2u", "\u001B[13;2~", "\u001B[27;2;13~"]); const META_RETURN_SEQUENCES = new Set(["\u001B[13;3u", "\u001B[13;4u"]); +// Ctrl+Enter in modifyOtherKeys mode: keycode 13 (Enter), modifier 5 (Ctrl). +const CTRL_RETURN_SEQUENCES = new Set(["\u001B[13;5u", "\u001B[27;5;13~"]); const CTRL_LEFT_SEQUENCES = new Set(["\u001B[1;5D", "\u001B[5D"]); const CTRL_RIGHT_SEQUENCES = new Set(["\u001B[1;5C", "\u001B[5C"]); const META_LEFT_SEQUENCES = new Set(["\u001B[1;3D", "\u001B[3D", "\u001Bb"]); @@ -124,9 +126,13 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key: end: END_SEQUENCES.has(raw), pageDown: raw === "\u001B[6~", pageUp: raw === "\u001B[5~", - return: raw === "\r" || SHIFT_RETURN_SEQUENCES.has(raw) || META_RETURN_SEQUENCES.has(raw), + return: + raw === "\r" || + SHIFT_RETURN_SEQUENCES.has(raw) || + META_RETURN_SEQUENCES.has(raw) || + CTRL_RETURN_SEQUENCES.has(raw), escape: raw === "\u001B", - ctrl: CTRL_LEFT_SEQUENCES.has(raw) || CTRL_RIGHT_SEQUENCES.has(raw), + ctrl: CTRL_LEFT_SEQUENCES.has(raw) || CTRL_RIGHT_SEQUENCES.has(raw) || CTRL_RETURN_SEQUENCES.has(raw), shift: SHIFT_RETURN_SEQUENCES.has(raw), tab: raw === "\t" || raw === "\u001B[Z", backspace: BACKSPACE_BYTES.has(raw), diff --git a/packages/cli/src/ui/views/PromptInput.tsx b/packages/cli/src/ui/views/PromptInput.tsx index 9e6240a4..230310a6 100644 --- a/packages/cli/src/ui/views/PromptInput.tsx +++ b/packages/cli/src/ui/views/PromptInput.tsx @@ -212,7 +212,7 @@ export const PromptInput = React.memo(function PromptInput({ ? statusMessage : busy ? busyStatusText - : `enter send · shift+enter newline · @ files · ctrl+v image · / commands · ctrl+d exit${processOrPasteHint}`; + : `enter send · shift+enter/ctrl+enter newline · @ files · ctrl+v image · / commands · ctrl+d exit${processOrPasteHint}`; const showFooterText = useMemo( () => showMenu || showSkillsDropdown || openRawModelDropdown || showModelDropdown || showFileMentionMenu, [showMenu, showSkillsDropdown, showModelDropdown, openRawModelDropdown, showFileMentionMenu] @@ -968,11 +968,13 @@ export function isRawModeShortcut(input: string, key: Pick): b export type PromptReturnKeyAction = "submit" | "newline" | null; -export function getPromptReturnKeyAction(key: Pick): PromptReturnKeyAction { +export function getPromptReturnKeyAction( + key: Pick +): PromptReturnKeyAction { if (!key.return) { return null; } - if (key.shift || key.meta) { + if (key.shift || key.meta || key.ctrl) { return "newline"; } return "submit";