diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx index 8bef05596..1130f8352 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.test.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx @@ -1256,13 +1256,13 @@ describe('KeypressContext - Kitty Protocol', () => { }); expect(consoleLogSpy).toHaveBeenCalledWith( - '[DEBUG] CSI buffer accumulating:', + '[DEBUG] Kitty buffer accumulating:', expect.stringContaining('\x1b[27u'), ); const parsedCall = consoleLogSpy.mock.calls.find( (args) => typeof args[0] === 'string' && - args[0].includes('[DEBUG] CSI sequence parsed successfully'), + args[0].includes('[DEBUG] Kitty sequence parsed successfully'), ); expect(parsedCall).toBeTruthy(); expect(parsedCall?.[1]).toEqual(expect.stringContaining('\x1b[27u')); @@ -1293,7 +1293,7 @@ describe('KeypressContext - Kitty Protocol', () => { }); expect(consoleLogSpy).toHaveBeenCalledWith( - '[DEBUG] CSI buffer overflow, clearing:', + '[DEBUG] Kitty buffer overflow, clearing:', expect.any(String), ); }); @@ -1384,13 +1384,13 @@ describe('KeypressContext - Kitty Protocol', () => { // Verify debug logging for accumulation expect(consoleLogSpy).toHaveBeenCalledWith( - '[DEBUG] CSI buffer accumulating:', + '[DEBUG] Kitty buffer accumulating:', sequence, ); // Verify warning for char codes expect(consoleWarnSpy).toHaveBeenCalledWith( - 'CSI sequence buffer has char codes:', + 'Kitty sequence buffer has char codes:', [27, 91, 49, 50], ); }); @@ -1468,29 +1468,6 @@ describe('KeypressContext - Kitty Protocol', () => { ); }, ); - - it('should recognize Shift+Tab in non-Kitty protocol mode (Windows PowerShell)', () => { - const keyHandler = vi.fn(); - - // Create a wrapper with Kitty protocol disabled to simulate Windows PowerShell - const nonKittyWrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - - ); - - const { result } = renderHook(() => useKeypressContext(), { - wrapper: nonKittyWrapper, - }); - act(() => result.current.subscribe(keyHandler)); - - // Send legacy reverse Tab sequence (ESC [ Z) - act(() => stdin.sendKittySequence(`\x1b[Z`)); - - expect(keyHandler).toHaveBeenCalledWith( - expect.objectContaining({ name: 'tab', shift: true }), - ); - }); }); describe('Double-tap and batching', () => { diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index acf3b30d6..0f01712cc 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -508,96 +508,95 @@ export function KeypressProvider({ return; } - // Parse CSI sequences for both Kitty protocol and legacy terminals - // This ensures Shift+Tab and other special keys work correctly even when - // Kitty protocol is not available (e.g., Windows PowerShell) - if ( - kittySequenceBuffer || - (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; - - if (debugKeystrokeLogging) { - console.log('[DEBUG] CSI buffer accumulating:', kittySequenceBuffer); - } - - // Try to peel off as many complete sequences as are available at the - // start of the buffer. This handles batched inputs cleanly. If the - // prefix is incomplete or invalid, skip to the next CSI introducer - // (ESC[) so that a following valid sequence can still be parsed. - let parsedAny = false; - while (kittySequenceBuffer) { - const parsed = parseKittyPrefix(kittySequenceBuffer); - if (!parsed) { - // Look for the next potential CSI start beyond index 0 - const nextStart = kittySequenceBuffer.indexOf(`${ESC}[`, 1); - if (nextStart > 0) { - if (debugKeystrokeLogging) { - console.log( - '[DEBUG] Skipping incomplete/invalid CSI prefix:', - kittySequenceBuffer.slice(0, nextStart), - ); - } - kittySequenceBuffer = kittySequenceBuffer.slice(nextStart); - continue; - } - break; - } - if (debugKeystrokeLogging) { - const parsedSequence = kittySequenceBuffer.slice(0, parsed.length); - if (kittySequenceBuffer.length > parsed.length) { - console.log( - '[DEBUG] CSI sequence parsed successfully (prefix):', - parsedSequence, - ); - } else { - console.log( - '[DEBUG] CSI sequence parsed successfully:', - parsedSequence, - ); - } - } - // Consume the parsed prefix and broadcast it. - kittySequenceBuffer = kittySequenceBuffer.slice(parsed.length); - broadcast(parsed.key); - parsedAny = true; - } - if (parsedAny) return; - - if (config?.getDebugMode() || debugKeystrokeLogging) { - const codes = Array.from(kittySequenceBuffer).map((ch) => - ch.charCodeAt(0), - ); - console.warn('CSI sequence buffer has char codes:', codes); - } - + if (kittyProtocolEnabled) { if ( - kittyProtocolEnabled && - kittySequenceBuffer.length > MAX_KITTY_SEQUENCE_LENGTH + kittySequenceBuffer || + (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; + if (debugKeystrokeLogging) { console.log( - '[DEBUG] CSI buffer overflow, clearing:', + '[DEBUG] Kitty buffer accumulating:', kittySequenceBuffer, ); } - if (config) { - const event = new KittySequenceOverflowEvent( - kittySequenceBuffer.length, - kittySequenceBuffer, - ); - logKittySequenceOverflow(config, event); + + // Try to peel off as many complete sequences as are available at the + // start of the buffer. This handles batched inputs cleanly. If the + // prefix is incomplete or invalid, skip to the next CSI introducer + // (ESC[) so that a following valid sequence can still be parsed. + let parsedAny = false; + while (kittySequenceBuffer) { + const parsed = parseKittyPrefix(kittySequenceBuffer); + if (!parsed) { + // Look for the next potential CSI start beyond index 0 + const nextStart = kittySequenceBuffer.indexOf(`${ESC}[`, 1); + if (nextStart > 0) { + if (debugKeystrokeLogging) { + console.log( + '[DEBUG] Skipping incomplete/invalid CSI prefix:', + kittySequenceBuffer.slice(0, nextStart), + ); + } + kittySequenceBuffer = kittySequenceBuffer.slice(nextStart); + continue; + } + break; + } + if (debugKeystrokeLogging) { + const parsedSequence = kittySequenceBuffer.slice( + 0, + parsed.length, + ); + if (kittySequenceBuffer.length > parsed.length) { + console.log( + '[DEBUG] Kitty sequence parsed successfully (prefix):', + parsedSequence, + ); + } else { + console.log( + '[DEBUG] Kitty sequence parsed successfully:', + parsedSequence, + ); + } + } + // Consume the parsed prefix and broadcast it. + kittySequenceBuffer = kittySequenceBuffer.slice(parsed.length); + broadcast(parsed.key); + parsedAny = true; + } + if (parsedAny) return; + + if (config?.getDebugMode() || debugKeystrokeLogging) { + const codes = Array.from(kittySequenceBuffer).map((ch) => + ch.charCodeAt(0), + ); + console.warn('Kitty sequence buffer has char codes:', codes); + } + + if (kittySequenceBuffer.length > MAX_KITTY_SEQUENCE_LENGTH) { + if (debugKeystrokeLogging) { + console.log( + '[DEBUG] Kitty buffer overflow, clearing:', + kittySequenceBuffer, + ); + } + if (config) { + const event = new KittySequenceOverflowEvent( + kittySequenceBuffer.length, + kittySequenceBuffer, + ); + logKittySequenceOverflow(config, event); + } + kittySequenceBuffer = ''; + } else { + return; } - kittySequenceBuffer = ''; - } else if (!kittyProtocolEnabled) { - // For non-Kitty terminals, clear the buffer to avoid accumulation - kittySequenceBuffer = ''; - } else { - return; } }