diff --git a/src/cli/components/InputRich.tsx b/src/cli/components/InputRich.tsx index 7157981..e2447fa 100644 --- a/src/cli/components/InputRich.tsx +++ b/src/cli/components/InputRich.tsx @@ -173,6 +173,13 @@ export function Input({ // Handle escape key for interrupt (when streaming) or double-escape-to-clear (when not) useInput((_input, key) => { if (!visible) return; + // Debug logging for escape key detection + if (process.env.LETTA_DEBUG_KEYS === "1" && key.escape) { + // eslint-disable-next-line no-console + console.error( + `[debug:InputRich:escape] escape=${key.escape} visible=${visible} onEscapeCancel=${!!onEscapeCancel} streaming=${streaming}`, + ); + } // Skip if onEscapeCancel is provided - handled by the confirmation handler above if (onEscapeCancel) return; @@ -232,6 +239,13 @@ export function Input({ // Handle Shift+Tab for permission mode cycling useInput((_input, key) => { if (!visible) return; + // Debug logging for shift+tab detection + if (process.env.LETTA_DEBUG_KEYS === "1" && (key.shift || key.tab)) { + // eslint-disable-next-line no-console + console.error( + `[debug:InputRich] shift=${key.shift} tab=${key.tab} visible=${visible}`, + ); + } if (key.shift && key.tab) { // Cycle through permission modes const modes: PermissionMode[] = [ diff --git a/src/cli/utils/kittyProtocolDetector.ts b/src/cli/utils/kittyProtocolDetector.ts index 7dd715a..c82e05b 100644 --- a/src/cli/utils/kittyProtocolDetector.ts +++ b/src/cli/utils/kittyProtocolDetector.ts @@ -10,16 +10,28 @@ let kittySupported = false; let kittyEnabled = false; const DEBUG = process.env.LETTA_DEBUG_KITTY === "1"; +const DISABLED = process.env.LETTA_DISABLE_KITTY === "1"; /** * Detects Kitty keyboard protocol support. * This function should be called once at app startup, before rendering. + * Set LETTA_DISABLE_KITTY=1 to skip enabling the protocol (useful for debugging). */ export async function detectAndEnableKittyProtocol(): Promise { if (detectionComplete) { return; } + // Allow disabling Kitty protocol for debugging terminal issues + if (DISABLED) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.error("[kitty] protocol disabled via LETTA_DISABLE_KITTY=1"); + } + detectionComplete = true; + return; + } + return new Promise((resolve) => { if (!process.stdin.isTTY || !process.stdout.isTTY) { detectionComplete = true; diff --git a/vendor/ink/build/hooks/use-input.js b/vendor/ink/build/hooks/use-input.js index 954a108..5a8609b 100644 --- a/vendor/ink/build/hooks/use-input.js +++ b/vendor/ink/build/hooks/use-input.js @@ -53,7 +53,48 @@ const useInput = (inputHandler, options = {}) => { return; } - const keypress = parseKeypress(data); + let keypress = parseKeypress(data); + + // CSI u fallback: iTerm2 3.5+, Kitty, and other modern terminals send + // keys in CSI u format: ESC [ keycode ; modifier u + // or with event type: ESC [ keycode ; modifier : event u + // parseKeypress doesn't handle this, so we parse it ourselves as a fallback + if (!keypress.name && typeof data === 'string') { + // Match CSI u: ESC [ keycode ; modifier u OR ESC [ keycode ; modifier : event u + const csiUMatch = data.match(/^\x1b\[(\d+)(?:;(\d+))?(?::(\d+))?u$/); + if (csiUMatch) { + const keycode = parseInt(csiUMatch[1], 10); + const modifier = parseInt(csiUMatch[2] || '1', 10) - 1; + const event = csiUMatch[3] ? parseInt(csiUMatch[3], 10) : 1; + + // Ignore key release events (event=3) + if (event === 3) { + return; + } + + // Map keycodes to names + const csiUKeyMap = { + 9: 'tab', + 13: 'return', + 27: 'escape', + 127: 'backspace', + }; + + const name = csiUKeyMap[keycode] || ''; + if (name) { + keypress = { + name, + ctrl: !!(modifier & 4), + meta: !!(modifier & 10), + shift: !!(modifier & 1), + option: false, + sequence: data, + raw: data, + }; + } + } + } + const key = { upArrow: keypress.name === 'up', downArrow: keypress.name === 'down', @@ -72,6 +113,15 @@ const useInput = (inputHandler, options = {}) => { isPasted: false }; + // Debug logging for key parsing (LETTA_DEBUG_KEYS=1) + if (process.env.LETTA_DEBUG_KEYS === '1') { + const rawHex = typeof data === 'string' + ? [...data].map(c => '0x' + c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ') + : '(non-string)'; + // eslint-disable-next-line no-console + console.error(`[debug:ink-keypress] raw=${rawHex} name="${keypress.name}" seq="${keypress.sequence}" key={escape:${key.escape},tab:${key.tab},shift:${key.shift},ctrl:${key.ctrl},meta:${key.meta}}`); + } + let input = keypress.ctrl ? keypress.name : keypress.sequence; const seq = typeof keypress.sequence === 'string' ? keypress.sequence : ''; // Filter xterm focus in/out sequences (ESC[I / ESC[O)