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

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