From 61fb9425a24e106580ada5830f3fb5287a12b662 Mon Sep 17 00:00:00 2001 From: qqqys Date: Mon, 23 Mar 2026 20:41:36 +0800 Subject: [PATCH 1/2] fix(cli): enhance KeypressProvider with kitty sequence timeout management --- .../cli/src/ui/contexts/KeypressContext.tsx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index 97db27563..9a42c8e86 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -173,11 +173,37 @@ 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 backslashTimeout: NodeJS.Timeout | null = null; let waitingForEnterAfterBackslash = false; let rawDataBuffer = Buffer.alloc(0); let rawFlushTimeout: NodeJS.Timeout | null = null; + 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 = ''; + kittySequenceBuffer = ''; + } + }, KITTY_SEQUENCE_TIMEOUT_MS); + }; + const createPrintableKey = (char: string): Key => { const printableName = char === ' ' @@ -685,6 +711,8 @@ export function KeypressProvider({ !key.sequence.startsWith(FOCUS_OUT)) ) { kittySequenceBuffer += key.sequence; + kittySequenceBufferRef.current = kittySequenceBuffer; + startKittyTimeout(); if (debugKeystrokeLogging) { debugLogger.debug( @@ -720,6 +748,10 @@ export function KeypressProvider({ } // Consume the parsed prefix and broadcast it. kittySequenceBuffer = kittySequenceBuffer.slice(parsed.length); + kittySequenceBufferRef.current = kittySequenceBuffer; + if (!kittySequenceBuffer) { + clearKittyTimeout(); + } broadcast(parsed.key); bufferedInputHandled = true; continue; @@ -737,6 +769,10 @@ export function KeypressProvider({ kittySequenceBuffer = kittySequenceBuffer.slice( completeUnsupportedCsiLength, ); + kittySequenceBufferRef.current = kittySequenceBuffer; + if (!kittySequenceBuffer) { + clearKittyTimeout(); + } bufferedInputHandled = true; continue; } @@ -752,6 +788,10 @@ export function KeypressProvider({ kittySequenceBuffer = kittySequenceBuffer.slice( plainTextPrefix.length, ); + kittySequenceBufferRef.current = kittySequenceBuffer; + if (!kittySequenceBuffer) { + clearKittyTimeout(); + } broadcast(plainTextPrefix.key); bufferedInputHandled = true; continue; @@ -767,6 +807,10 @@ export function KeypressProvider({ ); } kittySequenceBuffer = kittySequenceBuffer.slice(nextStart); + kittySequenceBufferRef.current = kittySequenceBuffer; + if (!kittySequenceBuffer) { + clearKittyTimeout(); + } bufferedInputHandled = true; continue; } @@ -796,6 +840,8 @@ export function KeypressProvider({ logKittySequenceOverflow(config, event); } kittySequenceBuffer = ''; + kittySequenceBufferRef.current = ''; + clearKittyTimeout(); } else { return; } @@ -956,6 +1002,8 @@ export function KeypressProvider({ backslashTimeout = null; } + clearKittyTimeout(); + if (rawFlushTimeout) { clearTimeout(rawFlushTimeout); rawFlushTimeout = null; From 7b1d2943247f9d2933b6ea88fbda4449ed4fa54b Mon Sep 17 00:00:00 2001 From: qqqys Date: Tue, 24 Mar 2026 10:57:11 +0800 Subject: [PATCH 2/2] fix(cli): add kitty sequence timeout management and buffer handling improvements in KeypressProvider --- .../cli/src/ui/contexts/KeypressContext.tsx | 111 +++++++++++------- 1 file changed, 66 insertions(+), 45 deletions(-) diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index 9a42c8e86..78207673c 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -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);