mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Co-authored-by: Arya Gummadi <aryagummadi@google.com>
This commit is contained in:
@@ -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.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user