fix(cli): add kitty sequence timeout management and buffer handling improvements in KeypressProvider

This commit is contained in:
qqqys 2026-03-24 10:57:11 +08:00
parent 61fb9425a2
commit 7b1d294324

View file

@ -44,6 +44,11 @@ const ESC = '\u001B';
export const PASTE_MODE_PREFIX = `${ESC}[200~`;
export const PASTE_MODE_SUFFIX = `${ESC}[201~`;
export const DRAG_COMPLETION_TIMEOUT_MS = 100; // Broadcast full path after 100ms if no more input
// Kitty sequence timeout: 200ms balances between:
// - Too short: prematurely clear valid sequences during slow input
// - Too long: delayed recovery from interrupted sequences (e.g., IME interruptions)
// Based on empirical testing with IME input patterns in VS Code integrated terminal.
export const KITTY_SEQUENCE_TIMEOUT_MS = 200;
export const SINGLE_QUOTE = "'";
export const DOUBLE_QUOTE = '"';
@ -172,15 +177,17 @@ export function KeypressProvider({
let isPaste = false;
let pasteBuffer = Buffer.alloc(0);
let kittySequenceBuffer = '';
let kittySequenceTimeout: NodeJS.Timeout | null = null;
const KITTY_SEQUENCE_TIMEOUT_MS = 200;
const kittySequenceBufferRef = { current: '' };
let kittySequenceTimeout: NodeJS.Timeout | null = null;
let backslashTimeout: NodeJS.Timeout | null = null;
let waitingForEnterAfterBackslash = false;
let rawDataBuffer = Buffer.alloc(0);
let rawFlushTimeout: NodeJS.Timeout | null = null;
const updateKittyBuffer = (value: string) => {
kittySequenceBufferRef.current = value;
};
const clearKittyTimeout = () => {
if (kittySequenceTimeout) {
clearTimeout(kittySequenceTimeout);
@ -199,11 +206,15 @@ export function KeypressProvider({
);
}
kittySequenceBufferRef.current = '';
kittySequenceBuffer = '';
}
}, KITTY_SEQUENCE_TIMEOUT_MS);
};
const clearKittyBufferAndTimeout = () => {
clearKittyTimeout();
kittySequenceBufferRef.current = '';
};
const createPrintableKey = (char: string): Key => {
const printableName =
char === ' '
@ -678,13 +689,13 @@ export function KeypressProvider({
(key.ctrl && key.name === 'c') ||
key.sequence === `${ESC}${KITTY_CTRL_C}`
) {
if (kittySequenceBuffer && debugKeystrokeLogging) {
if (kittySequenceBufferRef.current && debugKeystrokeLogging) {
debugLogger.debug(
'[DEBUG] Kitty buffer cleared on Ctrl+C:',
kittySequenceBuffer,
kittySequenceBufferRef.current,
);
}
kittySequenceBuffer = '';
clearKittyBufferAndTimeout();
if (key.sequence === `${ESC}${KITTY_CTRL_C}`) {
broadcast({
name: 'c',
@ -703,21 +714,20 @@ export function KeypressProvider({
if (kittyProtocolEnabled) {
if (
kittySequenceBuffer ||
kittySequenceBufferRef.current ||
(key.sequence.startsWith(`${ESC}[`) &&
!key.sequence.startsWith(PASTE_MODE_PREFIX) &&
!key.sequence.startsWith(PASTE_MODE_SUFFIX) &&
!key.sequence.startsWith(FOCUS_IN) &&
!key.sequence.startsWith(FOCUS_OUT))
) {
kittySequenceBuffer += key.sequence;
kittySequenceBufferRef.current = kittySequenceBuffer;
updateKittyBuffer(kittySequenceBufferRef.current + key.sequence);
startKittyTimeout();
if (debugKeystrokeLogging) {
debugLogger.debug(
'[DEBUG] Kitty buffer accumulating:',
kittySequenceBuffer,
kittySequenceBufferRef.current,
);
}
@ -726,15 +736,15 @@ export function KeypressProvider({
// prefix is incomplete or invalid, skip to the next CSI introducer
// (ESC[) so that a following valid sequence can still be parsed.
let bufferedInputHandled = false;
while (kittySequenceBuffer) {
const parsed = parseKittyPrefix(kittySequenceBuffer);
while (kittySequenceBufferRef.current) {
const parsed = parseKittyPrefix(kittySequenceBufferRef.current);
if (parsed) {
if (debugKeystrokeLogging) {
const parsedSequence = kittySequenceBuffer.slice(
const parsedSequence = kittySequenceBufferRef.current.slice(
0,
parsed.length,
);
if (kittySequenceBuffer.length > parsed.length) {
if (kittySequenceBufferRef.current.length > parsed.length) {
debugLogger.debug(
'[DEBUG] Kitty sequence parsed successfully (prefix):',
parsedSequence,
@ -747,9 +757,10 @@ export function KeypressProvider({
}
}
// Consume the parsed prefix and broadcast it.
kittySequenceBuffer = kittySequenceBuffer.slice(parsed.length);
kittySequenceBufferRef.current = kittySequenceBuffer;
if (!kittySequenceBuffer) {
updateKittyBuffer(
kittySequenceBufferRef.current.slice(parsed.length),
);
if (!kittySequenceBufferRef.current) {
clearKittyTimeout();
}
broadcast(parsed.key);
@ -757,27 +768,34 @@ export function KeypressProvider({
continue;
}
const completeUnsupportedCsiLength =
getCompleteCsiSequenceLength(kittySequenceBuffer);
const completeUnsupportedCsiLength = getCompleteCsiSequenceLength(
kittySequenceBufferRef.current,
);
if (completeUnsupportedCsiLength) {
if (debugKeystrokeLogging) {
debugLogger.debug(
'[DEBUG] Dropping unsupported complete CSI sequence:',
kittySequenceBuffer.slice(0, completeUnsupportedCsiLength),
kittySequenceBufferRef.current.slice(
0,
completeUnsupportedCsiLength,
),
);
}
kittySequenceBuffer = kittySequenceBuffer.slice(
completeUnsupportedCsiLength,
updateKittyBuffer(
kittySequenceBufferRef.current.slice(
completeUnsupportedCsiLength,
),
);
kittySequenceBufferRef.current = kittySequenceBuffer;
if (!kittySequenceBuffer) {
if (!kittySequenceBufferRef.current) {
clearKittyTimeout();
}
bufferedInputHandled = true;
continue;
}
const plainTextPrefix = parsePlainTextPrefix(kittySequenceBuffer);
const plainTextPrefix = parsePlainTextPrefix(
kittySequenceBufferRef.current,
);
if (plainTextPrefix) {
if (debugKeystrokeLogging) {
debugLogger.debug(
@ -785,11 +803,10 @@ export function KeypressProvider({
plainTextPrefix.key.sequence,
);
}
kittySequenceBuffer = kittySequenceBuffer.slice(
plainTextPrefix.length,
updateKittyBuffer(
kittySequenceBufferRef.current.slice(plainTextPrefix.length),
);
kittySequenceBufferRef.current = kittySequenceBuffer;
if (!kittySequenceBuffer) {
if (!kittySequenceBufferRef.current) {
clearKittyTimeout();
}
broadcast(plainTextPrefix.key);
@ -798,17 +815,21 @@ export function KeypressProvider({
}
// Look for the next potential CSI start beyond index 0
const nextStart = kittySequenceBuffer.indexOf(`${ESC}[`, 1);
const nextStart = kittySequenceBufferRef.current.indexOf(
`${ESC}[`,
1,
);
if (nextStart > 0) {
if (debugKeystrokeLogging) {
debugLogger.debug(
'[DEBUG] Skipping incomplete/invalid CSI prefix:',
kittySequenceBuffer.slice(0, nextStart),
kittySequenceBufferRef.current.slice(0, nextStart),
);
}
kittySequenceBuffer = kittySequenceBuffer.slice(nextStart);
kittySequenceBufferRef.current = kittySequenceBuffer;
if (!kittySequenceBuffer) {
updateKittyBuffer(
kittySequenceBufferRef.current.slice(nextStart),
);
if (!kittySequenceBufferRef.current) {
clearKittyTimeout();
}
bufferedInputHandled = true;
@ -819,29 +840,29 @@ export function KeypressProvider({
if (bufferedInputHandled) return;
if (config?.getDebugMode() || debugKeystrokeLogging) {
const codes = Array.from(kittySequenceBuffer).map((ch) =>
ch.charCodeAt(0),
const codes = Array.from(kittySequenceBufferRef.current).map(
(ch: string) => ch.charCodeAt(0),
);
debugLogger.warn('Kitty sequence buffer has char codes:', codes);
}
if (kittySequenceBuffer.length > MAX_KITTY_SEQUENCE_LENGTH) {
if (
kittySequenceBufferRef.current.length > MAX_KITTY_SEQUENCE_LENGTH
) {
if (debugKeystrokeLogging) {
debugLogger.debug(
'[DEBUG] Kitty buffer overflow, clearing:',
kittySequenceBuffer,
kittySequenceBufferRef.current,
);
}
if (config) {
const event = new KittySequenceOverflowEvent(
kittySequenceBuffer.length,
kittySequenceBuffer,
kittySequenceBufferRef.current.length,
kittySequenceBufferRef.current,
);
logKittySequenceOverflow(config, event);
}
kittySequenceBuffer = '';
kittySequenceBufferRef.current = '';
clearKittyTimeout();
clearKittyBufferAndTimeout();
} else {
return;
}
@ -1002,7 +1023,7 @@ export function KeypressProvider({
backslashTimeout = null;
}
clearKittyTimeout();
clearKittyBufferAndTimeout();
if (rawFlushTimeout) {
clearTimeout(rawFlushTimeout);