diff --git a/ui-tui/packages/hermes-ink/src/ink/events/cmd-shortcuts.test.ts b/ui-tui/packages/hermes-ink/src/ink/events/cmd-shortcuts.test.ts index 1abd7bbe00..250b262e8d 100644 --- a/ui-tui/packages/hermes-ink/src/ink/events/cmd-shortcuts.test.ts +++ b/ui-tui/packages/hermes-ink/src/ink/events/cmd-shortcuts.test.ts @@ -11,7 +11,25 @@ function parseOne(sequence: string) { return keys[0]! } -describe('InputEvent macOS command modifiers', () => { +describe('enhanced keyboard modifier parsing', () => { + it('detects modified Enter sequences for multiline composer shortcuts', () => { + const shiftEnter = new InputEvent(parseOne('\u001b[13;2u')) + const ctrlEnter = new InputEvent(parseOne('\u001b[13;5u')) + const modifyOtherShiftEnter = new InputEvent(parseOne('\u001b[27;2;13~')) + + expect(shiftEnter.key.return).toBe(true) + expect(shiftEnter.key.shift).toBe(true) + expect(shiftEnter.input).toBe('') + + expect(ctrlEnter.key.return).toBe(true) + expect(ctrlEnter.key.ctrl).toBe(true) + expect(ctrlEnter.input).toBe('') + + expect(modifyOtherShiftEnter.key.return).toBe(true) + expect(modifyOtherShiftEnter.key.shift).toBe(true) + expect(modifyOtherShiftEnter.input).toBe('') + }) + it('preserves Cmd as super for kitty keyboard CSI-u sequences', () => { const parsed = parseOne('\u001b[99;9u') const event = new InputEvent(parsed) diff --git a/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts b/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts index 293ecdbeec..a3cd3fabec 100644 --- a/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +++ b/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts @@ -116,11 +116,15 @@ function parseKey(keypress: ParsedKey): [Key, string] { // so the raw "[57358u" doesn't leak into the prompt. See #38781. input = '' } else { - // 'space' → ' '; 'escape' → '' (key.escape carries it; - // processedAsSpecialSequence bypasses the nonAlphanumericKeys - // clear below, so we must handle it explicitly here); - // otherwise use key name. - input = keypress.name === 'space' ? ' ' : keypress.name === 'escape' ? '' : keypress.name + // 'space' → ' '; functional keys like Enter/Escape carry their state + // through key.return/key.escape, and processedAsSpecialSequence bypasses + // the nonAlphanumericKeys clear below, so clear them explicitly here. + input = + keypress.name === 'space' + ? ' ' + : keypress.name === 'return' || keypress.name === 'escape' + ? '' + : keypress.name } processedAsSpecialSequence = true @@ -138,7 +142,12 @@ function parseKey(keypress: ParsedKey): [Key, string] { // guards against future terminal behavior. input = '' } else { - input = keypress.name === 'space' ? ' ' : keypress.name === 'escape' ? '' : keypress.name + input = + keypress.name === 'space' + ? ' ' + : keypress.name === 'return' || keypress.name === 'escape' + ? '' + : keypress.name } processedAsSpecialSequence = true diff --git a/ui-tui/packages/hermes-ink/src/ink/terminal.ts b/ui-tui/packages/hermes-ink/src/ink/terminal.ts index 8bdac62212..75637c76f8 100644 --- a/ui-tui/packages/hermes-ink/src/ink/terminal.ts +++ b/ui-tui/packages/hermes-ink/src/ink/terminal.ts @@ -176,7 +176,7 @@ export function isXtermJs(): boolean { // in xterm.js-based terminals like VS Code). tmux is allowlisted because it // accepts modifyOtherKeys and doesn't forward the kitty sequence to the outer // terminal. -const EXTENDED_KEYS_TERMINALS = ['iTerm.app', 'kitty', 'WezTerm', 'ghostty', 'tmux', 'windows-terminal'] +const EXTENDED_KEYS_TERMINALS = ['iTerm.app', 'kitty', 'WezTerm', 'ghostty', 'tmux', 'windows-terminal', 'vscode'] /** True if this terminal correctly handles extended key reporting * (Kitty keyboard protocol + xterm modifyOtherKeys). */ diff --git a/ui-tui/src/components/textInput.tsx b/ui-tui/src/components/textInput.tsx index b31f86e73e..984d217854 100644 --- a/ui-tui/src/components/textInput.tsx +++ b/ui-tui/src/components/textInput.tsx @@ -700,7 +700,7 @@ export function TextInput({ } if (k.return) { - if (k.shift || (isMac ? isActionMod(k) : k.meta)) { + if (k.shift || k.ctrl || (isMac ? isActionMod(k) : k.meta)) { flushParentChange() commit(ins(vRef.current, curRef.current, '\n'), curRef.current + 1) } else {