fix: iterm2 keybindings (#429)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2025-12-30 15:05:27 -08:00
committed by GitHub
parent 5cfb9aac91
commit 73249c9bce
3 changed files with 77 additions and 1 deletions

View File

@@ -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[] = [

View File

@@ -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<void> {
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;

View File

@@ -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)