From 9bcf06c056fc291e0af4f43d31fc81281739e4c0 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 6 Mar 2026 00:33:58 +0100 Subject: [PATCH] fix(tui): add Kitty CSI-u printable decoding to Input component, closes #1857 --- packages/coding-agent/CHANGELOG.md | 1 + packages/tui/CHANGELOG.md | 5 +++ packages/tui/src/components/editor.ts | 44 +-------------------- packages/tui/src/components/input.ts | 11 ++++++ packages/tui/src/index.ts | 1 + packages/tui/src/keys.ts | 55 +++++++++++++++++++++++++++ 6 files changed, 74 insertions(+), 43 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 6409adde..a01951d9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -13,6 +13,7 @@ - Fixed Antigravity reliability: endpoint cascade on 403/404, added autopush sandbox fallback, removed extra fingerprint headers ([#1830](https://github.com/badlogic/pi-mono/issues/1830)). - Fixed `@mariozechner/pi-ai/oauth` extension imports in published installs by resolving the subpath directly from built `dist` files instead of package-root wrapper shims ([#1856](https://github.com/badlogic/pi-mono/issues/1856)). - Fixed Gemini 3 multi-turn tool use losing structured context by using `skip_thought_signature_validator` sentinel for unsigned function calls instead of text fallback ([#1829](https://github.com/badlogic/pi-mono/issues/1829)). +- Fixed model selector filter not accepting typed characters in VS Code 1.110+ due to missing Kitty CSI-u printable decoding in the `Input` component ([#1857](https://github.com/badlogic/pi-mono/issues/1857)) - Fixed editor/footer visibility drift during terminal resize by forcing full redraws when terminal width or height changes ([#1844](https://github.com/badlogic/pi-mono/pull/1844) by [@ghoulr](https://github.com/ghoulr)). - Fixed footer width truncation for wide Unicode text (session name, model, provider) to prevent TUI crashes from rendered lines exceeding terminal width ([#1833](https://github.com/badlogic/pi-mono/issues/1833)). - Fixed Windows write preview background artifacts by normalizing CRLF content (`\r\n`) to LF for display rendering in tool output previews ([#1854](https://github.com/badlogic/pi-mono/issues/1854)). diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 5e6d5cc9..54032bc4 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,8 +2,13 @@ ## [Unreleased] +### Added + +- Exported `decodeKittyPrintable()` from `keys.ts` for decoding Kitty CSI-u sequences into printable characters + ### Fixed +- Fixed `Input` component not accepting typed characters when Kitty keyboard protocol is active (e.g., VS Code 1.110+), causing model selector filter to ignore keystrokes ([#1857](https://github.com/badlogic/pi-mono/issues/1857)) - Fixed editor/footer visibility drift during terminal resize by forcing full redraws when terminal width or height changes ([#1844](https://github.com/badlogic/pi-mono/pull/1844) by [@ghoulr](https://github.com/ghoulr)). ## [0.56.1] - 2026-03-05 diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 8eb5773d..6aea0bfe 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -1,6 +1,6 @@ import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete.js"; import { getEditorKeybindings } from "../keybindings.js"; -import { matchesKey } from "../keys.js"; +import { decodeKittyPrintable, matchesKey } from "../keys.js"; import { KillRing } from "../kill-ring.js"; import { type Component, CURSOR_MARKER, type Focusable, type TUI } from "../tui.js"; import { UndoStack } from "../undo-stack.js"; @@ -92,48 +92,6 @@ export function wordWrapLine(line: string, maxWidth: number): TextChunk[] { } // Kitty CSI-u sequences for printable keys, including optional shifted/base codepoints. -const KITTY_CSI_U_REGEX = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u$/; -const KITTY_MOD_SHIFT = 1; -const KITTY_MOD_ALT = 2; -const KITTY_MOD_CTRL = 4; -const KITTY_LOCK_MASK = 64 + 128; // Caps Lock + Num Lock -const KITTY_ALLOWED_MODIFIERS = KITTY_MOD_SHIFT | KITTY_LOCK_MASK; - -// Decode a printable CSI-u sequence, preferring the shifted key when present. -function decodeKittyPrintable(data: string): string | undefined { - const match = data.match(KITTY_CSI_U_REGEX); - if (!match) return undefined; - - // CSI-u groups: [:[:]];u - const codepoint = Number.parseInt(match[1] ?? "", 10); - if (!Number.isFinite(codepoint)) return undefined; - - const shiftedKey = match[2] && match[2].length > 0 ? Number.parseInt(match[2], 10) : undefined; - const modValue = match[4] ? Number.parseInt(match[4], 10) : 1; - // Modifiers are 1-indexed in CSI-u; normalize to our bitmask. - const modifier = Number.isFinite(modValue) ? modValue - 1 : 0; - - // Only accept printable CSI-u input for plain or Shift-modified text keys. - // Reject unsupported modifier bits (e.g. Super/Meta) to avoid inserting - // characters from modifier-only terminal events. - if ((modifier & ~KITTY_ALLOWED_MODIFIERS) !== 0) return undefined; - if (modifier & (KITTY_MOD_ALT | KITTY_MOD_CTRL)) return undefined; - - // Prefer the shifted keycode when Shift is held. - let effectiveCodepoint = codepoint; - if (modifier & KITTY_MOD_SHIFT && typeof shiftedKey === "number") { - effectiveCodepoint = shiftedKey; - } - // Drop control characters or invalid codepoints. - if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32) return undefined; - - try { - return String.fromCodePoint(effectiveCodepoint); - } catch { - return undefined; - } -} - interface EditorState { lines: string[]; cursorLine: number; diff --git a/packages/tui/src/components/input.ts b/packages/tui/src/components/input.ts index e0c937b3..e5c3b4f7 100644 --- a/packages/tui/src/components/input.ts +++ b/packages/tui/src/components/input.ts @@ -1,4 +1,5 @@ import { getEditorKeybindings } from "../keybindings.js"; +import { decodeKittyPrintable } from "../keys.js"; import { KillRing } from "../kill-ring.js"; import { type Component, CURSOR_MARKER, type Focusable } from "../tui.js"; import { UndoStack } from "../undo-stack.js"; @@ -187,6 +188,16 @@ export class Input implements Component, Focusable { return; } + // Kitty CSI-u printable character (e.g. \x1b[97u for 'a'). + // Terminals with Kitty protocol flag 1 (disambiguate) send CSI-u for all keys, + // including plain printable characters. Decode before the control-char check + // since CSI-u sequences contain \x1b which would be rejected. + const kittyPrintable = decodeKittyPrintable(data); + if (kittyPrintable !== undefined) { + this.insertCharacter(kittyPrintable); + return; + } + // Regular character input - accept printable characters including Unicode, // but reject control characters (C0: 0x00-0x1F, DEL: 0x7F, C1: 0x80-0x9F) const hasControlChars = [...data].some((ch) => { diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index 44665da8..5e8c90ab 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -35,6 +35,7 @@ export { } from "./keybindings.js"; // Keyboard input handling export { + decodeKittyPrintable, isKeyRelease, isKeyRepeat, isKittyProtocolActive, diff --git a/packages/tui/src/keys.ts b/packages/tui/src/keys.ts index 829da434..185c6e3f 100644 --- a/packages/tui/src/keys.ts +++ b/packages/tui/src/keys.ts @@ -1152,3 +1152,58 @@ export function parseKey(data: string): string | undefined { return undefined; } + +// ============================================================================= +// Kitty CSI-u Printable Decoding +// ============================================================================= + +const KITTY_CSI_U_REGEX = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u$/; +const KITTY_PRINTABLE_ALLOWED_MODIFIERS = MODIFIERS.shift | LOCK_MASK; + +/** + * Decode a Kitty CSI-u sequence into a printable character, if applicable. + * + * When Kitty keyboard protocol flag 1 (disambiguate) is active, terminals send + * CSI-u sequences for all keys, including plain printable characters. This + * function extracts the printable character from such sequences. + * + * Only accepts plain or Shift-modified keys. Rejects Ctrl, Alt, and unsupported + * modifier combinations (those are handled by keybinding matching instead). + * Prefers the shifted keycode when Shift is held and a shifted key is reported. + * + * @param data - Raw input data from terminal + * @returns The printable character, or undefined if not a printable CSI-u sequence + */ +export function decodeKittyPrintable(data: string): string | undefined { + const match = data.match(KITTY_CSI_U_REGEX); + if (!match) return undefined; + + // CSI-u groups: [:[:]];[:]u + const codepoint = Number.parseInt(match[1] ?? "", 10); + if (!Number.isFinite(codepoint)) return undefined; + + const shiftedKey = match[2] && match[2].length > 0 ? Number.parseInt(match[2], 10) : undefined; + const modValue = match[4] ? Number.parseInt(match[4], 10) : 1; + // Modifiers are 1-indexed in CSI-u; normalize to our bitmask. + const modifier = Number.isFinite(modValue) ? modValue - 1 : 0; + + // Only accept printable CSI-u input for plain or Shift-modified text keys. + // Reject unsupported modifier bits (e.g. Super/Meta) to avoid inserting + // characters from modifier-only terminal events. + if ((modifier & ~KITTY_PRINTABLE_ALLOWED_MODIFIERS) !== 0) return undefined; + if (modifier & (MODIFIERS.alt | MODIFIERS.ctrl)) return undefined; + + // Prefer the shifted keycode when Shift is held. + let effectiveCodepoint = codepoint; + if (modifier & MODIFIERS.shift && typeof shiftedKey === "number") { + effectiveCodepoint = shiftedKey; + } + // Drop control characters or invalid codepoints. + if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32) return undefined; + + try { + return String.fromCodePoint(effectiveCodepoint); + } catch { + return undefined; + } +}