Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Uses the termio tokenizer for escape sequence boundary detection,
* then interprets sequences as keypresses.
*/
import { readFileSync } from 'node:fs'
import { Buffer } from 'buffer'

import { PASTE_END, PASTE_START } from './termio/csi.js'
Expand Down Expand Up @@ -696,6 +697,31 @@ function parseTextWithSgrMouseFragments(text: string): ParsedInput[] | null {
return parsed
}

/**
* Detect environments where bare LF (\\n, Ctrl+J) means Ctrl+Enter rather
* than plain Enter. On these platforms, plain Enter arrives as CR (\\r) in
* raw mode, and Ctrl+Enter/Ctrl+J arrive as LF — so LF is the user's
* multi-line newline keystroke and must not submit.
*
* Mirrors the Python CLI's _preserve_ctrl_enter_newline() in cli.py.
*/
function isCtrlEnterAsLF(): boolean {
// Native Windows
if (process.platform === 'win32') return true
// Windows Terminal (covers WSL-in-WT)
if (process.env.WT_SESSION) return true
// SSH sessions
if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY) return true
// WSL — env var may be absent under sudo
if (process.env.WSL_DISTRO_NAME) return true
// WSL fallback: peek at /proc/version
try {
const version = readFileSync('/proc/version', 'utf8')
if (version.toLowerCase().includes('microsoft')) return true
} catch {}
return false
}

function parseKeypress(s: string = ''): ParsedKey {
let parts

Expand Down Expand Up @@ -807,6 +833,21 @@ function parseKeypress(s: string = ''): ParsedKey {
if (s === '\r' || s === '\n') {
key.raw = undefined
key.name = 'return'
// On Windows, WSL, SSH, and Windows Terminal, bare LF means
// Ctrl+Enter — the user's multi-line newline keystroke.
// Plain Enter arrives as CR on those platforms. Set the ctrl
// flag so downstream handlers insert a newline, not submit.
if (s === '\n' && isCtrlEnterAsLF()) {
key.ctrl = true
}
} else if (s === '\x1b\r' || s === '\x1b\n') {
// ESC+CR / ESC+LF: Shift+Enter fallback encoding used by
// terminals that don't support kitty keyboard protocol or
// xterm modifyOtherKeys. Treat as return with shift flag
// so the textInput handler inserts a newline.
key.raw = undefined
key.name = 'return'
key.shift = true
} else if (s === '\t') {
key.name = 'tab'
} else if (s === '\b' || s === '\x1b\b') {
Expand Down
20 changes: 17 additions & 3 deletions ui-tui/packages/hermes-ink/src/ink/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,26 @@ export function needsAltScreenResizeScrollbackClear(env: NodeJS.ProcessEnv = pro
// 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', 'vscode']
const EXTENDED_KEYS_TERMINALS = ['iTerm.app', 'kitty', 'WezTerm', 'ghostty', 'tmux', 'windows-terminal', 'vscode', 'Tabby']

/** True if this terminal correctly handles extended key reporting
* (Kitty keyboard protocol + xterm modifyOtherKeys). */
* (Kitty keyboard protocol + xterm modifyOtherKeys).
*
* Also returns true on WSL, native Windows, Windows Terminal, and
* SSH sessions — all terminals on these platforms (Tabby, Windows
* Terminal, etc.) are xterm.js or conhost-based and support both
* protocols. Without this, terminals that don't set TERM_PROGRAM
* (like Tabby) never get extended keys, and Ctrl+Enter/Shift+Enter
* arrive as undifferentiated bytes or not at all. */
export function supportsExtendedKeys(): boolean {
return EXTENDED_KEYS_TERMINALS.includes(env.terminal ?? '')
if (EXTENDED_KEYS_TERMINALS.includes(env.terminal ?? '')) return true
// WSL / Windows / SSH / Windows Terminal — all terminals here
// support the protocols even if they don't self-identify.
if (process.platform === 'win32') return true
if (process.env.WT_SESSION) return true
if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY) return true
if (process.env.WSL_DISTRO_NAME) return true
return false
}

/** True if the terminal scrolls the viewport when it receives cursor-up
Expand Down