diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index e81747957..142a0c63c 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,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;