Merge pull request #2612 from qqqys/fix/ime_vscode

fix(cli): enhance KeypressProvider with kitty sequence timeout manage…
This commit is contained in:
tanzhenxin 2026-04-01 16:10:02 +08:00 committed by GitHub
commit 449a421ca7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

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,12 +177,44 @@ export function KeypressProvider({
let isPaste = false;
let pasteBuffer = Buffer.alloc(0);
let kittySequenceBuffer = '';
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);
kittySequenceTimeout = null;
}
};
const startKittyTimeout = () => {
clearKittyTimeout();
kittySequenceTimeout = setTimeout(() => {
if (kittySequenceBufferRef.current) {
if (debugKeystrokeLogging) {
debugLogger.debug(
'[DEBUG] Kitty buffer timeout, clearing:',
kittySequenceBufferRef.current,
);
}
kittySequenceBufferRef.current = '';
}
}, KITTY_SEQUENCE_TIMEOUT_MS);
};
const clearKittyBufferAndTimeout = () => {
clearKittyTimeout();
kittySequenceBufferRef.current = '';
};
const createPrintableKey = (char: string): Key => {
const printableName =
char === ' '
@ -661,13 +698,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',
@ -686,19 +723,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;
updateKittyBuffer(kittySequenceBufferRef.current + key.sequence);
startKittyTimeout();
if (debugKeystrokeLogging) {
debugLogger.debug(
'[DEBUG] Kitty buffer accumulating:',
kittySequenceBuffer,
kittySequenceBufferRef.current,
);
}
@ -707,15 +745,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,
@ -728,29 +766,45 @@ export function KeypressProvider({
}
}
// Consume the parsed prefix and broadcast it.
kittySequenceBuffer = kittySequenceBuffer.slice(parsed.length);
updateKittyBuffer(
kittySequenceBufferRef.current.slice(parsed.length),
);
if (!kittySequenceBufferRef.current) {
clearKittyTimeout();
}
broadcast(parsed.key);
bufferedInputHandled = true;
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,
),
);
if (!kittySequenceBufferRef.current) {
clearKittyTimeout();
}
bufferedInputHandled = true;
continue;
}
const plainTextPrefix = parsePlainTextPrefix(kittySequenceBuffer);
const plainTextPrefix = parsePlainTextPrefix(
kittySequenceBufferRef.current,
);
if (plainTextPrefix) {
if (debugKeystrokeLogging) {
debugLogger.debug(
@ -758,24 +812,35 @@ export function KeypressProvider({
plainTextPrefix.key.sequence,
);
}
kittySequenceBuffer = kittySequenceBuffer.slice(
plainTextPrefix.length,
updateKittyBuffer(
kittySequenceBufferRef.current.slice(plainTextPrefix.length),
);
if (!kittySequenceBufferRef.current) {
clearKittyTimeout();
}
broadcast(plainTextPrefix.key);
bufferedInputHandled = true;
continue;
}
// 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);
updateKittyBuffer(
kittySequenceBufferRef.current.slice(nextStart),
);
if (!kittySequenceBufferRef.current) {
clearKittyTimeout();
}
bufferedInputHandled = true;
continue;
}
@ -784,27 +849,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 = '';
clearKittyBufferAndTimeout();
} else {
return;
}
@ -965,6 +1032,8 @@ export function KeypressProvider({
backslashTimeout = null;
}
clearKittyBufferAndTimeout();
if (rawFlushTimeout) {
clearTimeout(rawFlushTimeout);
rawFlushTimeout = null;