From 53067fda7444d4e43ae33729577f89893dab8147 Mon Sep 17 00:00:00 2001 From: Deepankar Sharma Date: Fri, 22 Aug 2025 19:31:55 -0400 Subject: [PATCH] Add support for debug logging of keystrokes to investigate #6227 (#6844) Co-authored-by: Arya Gummadi --- .../cli/src/config/settingsSchema.test.ts | 13 ++ packages/cli/src/config/settingsSchema.ts | 9 + packages/cli/src/ui/App.test.tsx | 51 +++++ packages/cli/src/ui/App.tsx | 7 + .../src/ui/contexts/KeypressContext.test.tsx | 208 ++++++++++++++++++ .../cli/src/ui/contexts/KeypressContext.tsx | 39 +++- .../cli/src/ui/utils/platformConstants.ts | 8 + 7 files changed, 333 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts index 4c247e65..b522be94 100644 --- a/packages/cli/src/config/settingsSchema.test.ts +++ b/packages/cli/src/config/settingsSchema.test.ts @@ -53,6 +53,7 @@ describe('SettingsSchema', () => { 'hasSeenIdeIntegrationNudge', 'folderTrustFeature', 'useRipgrep', + 'debugKeystrokeLogging', ]; expectedSettings.forEach((setting) => { @@ -249,5 +250,17 @@ describe('SettingsSchema', () => { expect(SETTINGS_SCHEMA.folderTrustFeature.default).toBe(false); expect(SETTINGS_SCHEMA.folderTrustFeature.showInDialog).toBe(true); }); + + it('should have debugKeystrokeLogging setting in schema', () => { + expect(SETTINGS_SCHEMA.debugKeystrokeLogging).toBeDefined(); + expect(SETTINGS_SCHEMA.debugKeystrokeLogging.type).toBe('boolean'); + expect(SETTINGS_SCHEMA.debugKeystrokeLogging.category).toBe('General'); + expect(SETTINGS_SCHEMA.debugKeystrokeLogging.default).toBe(false); + expect(SETTINGS_SCHEMA.debugKeystrokeLogging.requiresRestart).toBe(false); + expect(SETTINGS_SCHEMA.debugKeystrokeLogging.showInDialog).toBe(true); + expect(SETTINGS_SCHEMA.debugKeystrokeLogging.description).toBe( + 'Enable debug logging of keystrokes to the console.', + ); + }); }); }); diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index dafc5a32..94f60288 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -564,6 +564,15 @@ export const SETTINGS_SCHEMA = { 'Enable AI-powered prompt completion suggestions while typing.', showInDialog: true, }, + debugKeystrokeLogging: { + type: 'boolean', + label: 'Debug Keystroke Logging', + category: 'General', + requiresRestart: false, + default: false, + description: 'Enable debug logging of keystrokes to the console.', + showInDialog: true, + }, } as const; type InferSettings = { diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index d5f2ba82..9961617b 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -1504,4 +1504,55 @@ describe('App UI', () => { expect(output).toContain('esc to cancel'); }); }); + + describe('debug keystroke logging', () => { + let consoleLogSpy: ReturnType; + + beforeEach(() => { + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleLogSpy.mockRestore(); + }); + + it('should pass debugKeystrokeLogging setting to KeypressProvider', () => { + const mockSettingsWithDebug = createMockSettings({ + workspace: { + theme: 'Default', + debugKeystrokeLogging: true, + }, + }); + + const { lastFrame, unmount } = renderWithProviders( + , + ); + currentUnmount = unmount; + + const output = lastFrame(); + + expect(output).toBeDefined(); + expect(mockSettingsWithDebug.merged.debugKeystrokeLogging).toBe(true); + }); + + it('should use default false value when debugKeystrokeLogging is not set', () => { + const { lastFrame, unmount } = renderWithProviders( + , + ); + currentUnmount = unmount; + + const output = lastFrame(); + + expect(output).toBeDefined(); + expect(mockSettings.merged.debugKeystrokeLogging).toBeUndefined(); + }); + }); }); diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index b4ec6a57..49fb3614 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -119,6 +119,7 @@ export const AppWrapper = (props: AppProps) => { @@ -664,6 +665,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const handleGlobalKeypress = useCallback( (key: Key) => { + // Debug log keystrokes if enabled + if (settings.merged.debugKeystrokeLogging) { + console.log('[DEBUG] Keystroke:', JSON.stringify(key)); + } + let enteringConstrainHeightMode = false; if (!constrainHeight) { enteringConstrainHeightMode = true; @@ -727,6 +733,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { handleSlashCommand, isAuthenticating, cancelOngoingRequest, + settings.merged.debugKeystrokeLogging, ], ); diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx index caed50be..292de77d 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.test.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx @@ -17,6 +17,10 @@ import { EventEmitter } from 'events'; import { KITTY_KEYCODE_ENTER, KITTY_KEYCODE_NUMPAD_ENTER, + CHAR_CODE_ESC, + CHAR_CODE_LEFT_BRACKET, + CHAR_CODE_1, + CHAR_CODE_2, } from '../utils/platformConstants.js'; // Mock the 'ink' module to control stdin @@ -310,4 +314,208 @@ describe('KeypressContext - Kitty Protocol', () => { ); }); }); + + describe('debug keystroke logging', () => { + let consoleLogSpy: ReturnType; + let consoleWarnSpy: ReturnType; + + beforeEach(() => { + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleLogSpy.mockRestore(); + consoleWarnSpy.mockRestore(); + }); + + it('should not log keystrokes when debugKeystrokeLogging is false', async () => { + const keyHandler = vi.fn(); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + const { result } = renderHook(() => useKeypressContext(), { wrapper }); + + act(() => { + result.current.subscribe(keyHandler); + }); + + // Send a kitty sequence + act(() => { + stdin.sendKittySequence('\x1b[27u'); + }); + + expect(keyHandler).toHaveBeenCalled(); + expect(consoleLogSpy).not.toHaveBeenCalledWith( + expect.stringContaining('[DEBUG] Kitty'), + ); + }); + + it('should log kitty buffer accumulation when debugKeystrokeLogging is true', async () => { + const keyHandler = vi.fn(); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + const { result } = renderHook(() => useKeypressContext(), { wrapper }); + + act(() => { + result.current.subscribe(keyHandler); + }); + + // Send a complete kitty sequence for escape + act(() => { + stdin.sendKittySequence('\x1b[27u'); + }); + + expect(consoleLogSpy).toHaveBeenCalledWith( + '[DEBUG] Kitty buffer accumulating:', + expect.stringContaining('\x1b[27u'), + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + '[DEBUG] Kitty sequence parsed successfully:', + expect.stringContaining('\x1b[27u'), + ); + }); + + it('should log kitty buffer overflow when debugKeystrokeLogging is true', async () => { + const keyHandler = vi.fn(); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + const { result } = renderHook(() => useKeypressContext(), { wrapper }); + + act(() => { + result.current.subscribe(keyHandler); + }); + + // Send an invalid long sequence to trigger overflow + const longInvalidSequence = '\x1b[' + 'x'.repeat(100); + act(() => { + stdin.sendKittySequence(longInvalidSequence); + }); + + expect(consoleLogSpy).toHaveBeenCalledWith( + '[DEBUG] Kitty buffer overflow, clearing:', + expect.any(String), + ); + }); + + it('should log kitty buffer clear on Ctrl+C when debugKeystrokeLogging is true', async () => { + const keyHandler = vi.fn(); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + const { result } = renderHook(() => useKeypressContext(), { wrapper }); + + act(() => { + result.current.subscribe(keyHandler); + }); + + // Send incomplete kitty sequence + act(() => { + stdin.pressKey({ + name: undefined, + ctrl: false, + meta: false, + shift: false, + sequence: '\x1b[1', + }); + }); + + // Send Ctrl+C + act(() => { + stdin.pressKey({ + name: 'c', + ctrl: true, + meta: false, + shift: false, + sequence: '\x03', + }); + }); + + expect(consoleLogSpy).toHaveBeenCalledWith( + '[DEBUG] Kitty buffer cleared on Ctrl+C:', + '\x1b[1', + ); + + // Verify Ctrl+C was handled + expect(keyHandler).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'c', + ctrl: true, + }), + ); + }); + + it('should show char codes when debugKeystrokeLogging is true even without debug mode', async () => { + const keyHandler = vi.fn(); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + const { result } = renderHook(() => useKeypressContext(), { wrapper }); + + act(() => { + result.current.subscribe(keyHandler); + }); + + // Send incomplete kitty sequence + const sequence = '\x1b[12'; + act(() => { + stdin.pressKey({ + name: undefined, + ctrl: false, + meta: false, + shift: false, + sequence, + }); + }); + + // Verify debug logging for accumulation + expect(consoleLogSpy).toHaveBeenCalledWith( + '[DEBUG] Kitty buffer accumulating:', + sequence, + ); + + // Verify warning for char codes + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Kitty sequence buffer has char codes:', + [CHAR_CODE_ESC, CHAR_CODE_LEFT_BRACKET, CHAR_CODE_1, CHAR_CODE_2], + ); + }); + }); }); diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index eaf1819b..28ed209b 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -68,10 +68,12 @@ export function KeypressProvider({ children, kittyProtocolEnabled, config, + debugKeystrokeLogging, }: { children: React.ReactNode; kittyProtocolEnabled: boolean; config?: Config; + debugKeystrokeLogging?: boolean; }) { const { stdin, setRawMode } = useStdin(); const subscribers = useRef>(new Set()).current; @@ -245,6 +247,12 @@ export function KeypressProvider({ (key.ctrl && key.name === 'c') || key.sequence === `${ESC}${KITTY_CTRL_C}` ) { + if (kittySequenceBuffer && debugKeystrokeLogging) { + console.log( + '[DEBUG] Kitty buffer cleared on Ctrl+C:', + kittySequenceBuffer, + ); + } kittySequenceBuffer = ''; if (key.sequence === `${ESC}${KITTY_CTRL_C}`) { broadcast({ @@ -272,14 +280,28 @@ export function KeypressProvider({ !key.sequence.startsWith(FOCUS_OUT)) ) { kittySequenceBuffer += key.sequence; + + if (debugKeystrokeLogging) { + console.log( + '[DEBUG] Kitty buffer accumulating:', + kittySequenceBuffer, + ); + } + const kittyKey = parseKittySequence(kittySequenceBuffer); if (kittyKey) { + if (debugKeystrokeLogging) { + console.log( + '[DEBUG] Kitty sequence parsed successfully:', + kittySequenceBuffer, + ); + } kittySequenceBuffer = ''; broadcast(kittyKey); return; } - if (config?.getDebugMode()) { + if (config?.getDebugMode() || debugKeystrokeLogging) { const codes = Array.from(kittySequenceBuffer).map((ch) => ch.charCodeAt(0), ); @@ -287,6 +309,12 @@ export function KeypressProvider({ } 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, @@ -404,7 +432,14 @@ export function KeypressProvider({ pasteBuffer = Buffer.alloc(0); } }; - }, [stdin, setRawMode, kittyProtocolEnabled, config, subscribers]); + }, [ + stdin, + setRawMode, + kittyProtocolEnabled, + config, + subscribers, + debugKeystrokeLogging, + ]); return ( diff --git a/packages/cli/src/ui/utils/platformConstants.ts b/packages/cli/src/ui/utils/platformConstants.ts index 8aff581e..2dcd85c0 100644 --- a/packages/cli/src/ui/utils/platformConstants.ts +++ b/packages/cli/src/ui/utils/platformConstants.ts @@ -48,3 +48,11 @@ export const BACKSLASH_ENTER_DETECTION_WINDOW_MS = 5; * We use 12 to provide a small buffer. */ export const MAX_KITTY_SEQUENCE_LENGTH = 12; + +/** + * Character codes for common escape sequences + */ +export const CHAR_CODE_ESC = 27; +export const CHAR_CODE_LEFT_BRACKET = 91; +export const CHAR_CODE_1 = 49; +export const CHAR_CODE_2 = 50;