Add support for debug logging of keystrokes to investigate #6227 (#6844)

Co-authored-by: Arya Gummadi <aryagummadi@google.com>
This commit is contained in:
Deepankar Sharma
2025-08-22 19:31:55 -04:00
committed by GitHub
parent fef89f5429
commit 53067fda74
7 changed files with 333 additions and 2 deletions

View File

@@ -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.',
);
});
});
});

View File

@@ -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<T extends SettingsSchema> = {

View File

@@ -1504,4 +1504,55 @@ describe('App UI', () => {
expect(output).toContain('esc to cancel');
});
});
describe('debug keystroke logging', () => {
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
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(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettingsWithDebug}
version={mockVersion}
/>,
);
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(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
version={mockVersion}
/>,
);
currentUnmount = unmount;
const output = lastFrame();
expect(output).toBeDefined();
expect(mockSettings.merged.debugKeystrokeLogging).toBeUndefined();
});
});
});

View File

@@ -119,6 +119,7 @@ export const AppWrapper = (props: AppProps) => {
<KeypressProvider
kittyProtocolEnabled={kittyProtocolStatus.enabled}
config={props.config}
debugKeystrokeLogging={props.settings.merged.debugKeystrokeLogging}
>
<SessionStatsProvider>
<VimModeProvider settings={props.settings}>
@@ -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,
],
);

View File

@@ -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<typeof vi.spyOn>;
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
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 }) => (
<KeypressProvider
kittyProtocolEnabled={true}
debugKeystrokeLogging={false}
>
{children}
</KeypressProvider>
);
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 }) => (
<KeypressProvider
kittyProtocolEnabled={true}
debugKeystrokeLogging={true}
>
{children}
</KeypressProvider>
);
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 }) => (
<KeypressProvider
kittyProtocolEnabled={true}
debugKeystrokeLogging={true}
>
{children}
</KeypressProvider>
);
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 }) => (
<KeypressProvider
kittyProtocolEnabled={true}
debugKeystrokeLogging={true}
>
{children}
</KeypressProvider>
);
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 }) => (
<KeypressProvider
kittyProtocolEnabled={true}
debugKeystrokeLogging={true}
>
{children}
</KeypressProvider>
);
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],
);
});
});
});

View File

@@ -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<Set<KeypressHandler>>(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 (
<KeypressContext.Provider value={{ subscribe, unsubscribe }}>

View File

@@ -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;