fix: iterm2 keybindings (#429)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -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[] = [
|
||||
|
||||
@@ -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;
|
||||
|
||||
52
vendor/ink/build/hooks/use-input.js
vendored
52
vendor/ink/build/hooks/use-input.js
vendored
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user