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',
|
'hasSeenIdeIntegrationNudge',
|
||||||
'folderTrustFeature',
|
'folderTrustFeature',
|
||||||
'useRipgrep',
|
'useRipgrep',
|
||||||
|
'debugKeystrokeLogging',
|
||||||
];
|
];
|
||||||
|
|
||||||
expectedSettings.forEach((setting) => {
|
expectedSettings.forEach((setting) => {
|
||||||
@@ -249,5 +250,17 @@ describe('SettingsSchema', () => {
|
|||||||
expect(SETTINGS_SCHEMA.folderTrustFeature.default).toBe(false);
|
expect(SETTINGS_SCHEMA.folderTrustFeature.default).toBe(false);
|
||||||
expect(SETTINGS_SCHEMA.folderTrustFeature.showInDialog).toBe(true);
|
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.',
|
'Enable AI-powered prompt completion suggestions while typing.',
|
||||||
showInDialog: true,
|
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;
|
} as const;
|
||||||
|
|
||||||
type InferSettings<T extends SettingsSchema> = {
|
type InferSettings<T extends SettingsSchema> = {
|
||||||
|
|||||||
@@ -1504,4 +1504,55 @@ describe('App UI', () => {
|
|||||||
expect(output).toContain('esc to cancel');
|
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
|
<KeypressProvider
|
||||||
kittyProtocolEnabled={kittyProtocolStatus.enabled}
|
kittyProtocolEnabled={kittyProtocolStatus.enabled}
|
||||||
config={props.config}
|
config={props.config}
|
||||||
|
debugKeystrokeLogging={props.settings.merged.debugKeystrokeLogging}
|
||||||
>
|
>
|
||||||
<SessionStatsProvider>
|
<SessionStatsProvider>
|
||||||
<VimModeProvider settings={props.settings}>
|
<VimModeProvider settings={props.settings}>
|
||||||
@@ -664,6 +665,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
|
|
||||||
const handleGlobalKeypress = useCallback(
|
const handleGlobalKeypress = useCallback(
|
||||||
(key: Key) => {
|
(key: Key) => {
|
||||||
|
// Debug log keystrokes if enabled
|
||||||
|
if (settings.merged.debugKeystrokeLogging) {
|
||||||
|
console.log('[DEBUG] Keystroke:', JSON.stringify(key));
|
||||||
|
}
|
||||||
|
|
||||||
let enteringConstrainHeightMode = false;
|
let enteringConstrainHeightMode = false;
|
||||||
if (!constrainHeight) {
|
if (!constrainHeight) {
|
||||||
enteringConstrainHeightMode = true;
|
enteringConstrainHeightMode = true;
|
||||||
@@ -727,6 +733,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
handleSlashCommand,
|
handleSlashCommand,
|
||||||
isAuthenticating,
|
isAuthenticating,
|
||||||
cancelOngoingRequest,
|
cancelOngoingRequest,
|
||||||
|
settings.merged.debugKeystrokeLogging,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import { EventEmitter } from 'events';
|
|||||||
import {
|
import {
|
||||||
KITTY_KEYCODE_ENTER,
|
KITTY_KEYCODE_ENTER,
|
||||||
KITTY_KEYCODE_NUMPAD_ENTER,
|
KITTY_KEYCODE_NUMPAD_ENTER,
|
||||||
|
CHAR_CODE_ESC,
|
||||||
|
CHAR_CODE_LEFT_BRACKET,
|
||||||
|
CHAR_CODE_1,
|
||||||
|
CHAR_CODE_2,
|
||||||
} from '../utils/platformConstants.js';
|
} from '../utils/platformConstants.js';
|
||||||
|
|
||||||
// Mock the 'ink' module to control stdin
|
// 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,
|
children,
|
||||||
kittyProtocolEnabled,
|
kittyProtocolEnabled,
|
||||||
config,
|
config,
|
||||||
|
debugKeystrokeLogging,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
kittyProtocolEnabled: boolean;
|
kittyProtocolEnabled: boolean;
|
||||||
config?: Config;
|
config?: Config;
|
||||||
|
debugKeystrokeLogging?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { stdin, setRawMode } = useStdin();
|
const { stdin, setRawMode } = useStdin();
|
||||||
const subscribers = useRef<Set<KeypressHandler>>(new Set()).current;
|
const subscribers = useRef<Set<KeypressHandler>>(new Set()).current;
|
||||||
@@ -245,6 +247,12 @@ export function KeypressProvider({
|
|||||||
(key.ctrl && key.name === 'c') ||
|
(key.ctrl && key.name === 'c') ||
|
||||||
key.sequence === `${ESC}${KITTY_CTRL_C}`
|
key.sequence === `${ESC}${KITTY_CTRL_C}`
|
||||||
) {
|
) {
|
||||||
|
if (kittySequenceBuffer && debugKeystrokeLogging) {
|
||||||
|
console.log(
|
||||||
|
'[DEBUG] Kitty buffer cleared on Ctrl+C:',
|
||||||
|
kittySequenceBuffer,
|
||||||
|
);
|
||||||
|
}
|
||||||
kittySequenceBuffer = '';
|
kittySequenceBuffer = '';
|
||||||
if (key.sequence === `${ESC}${KITTY_CTRL_C}`) {
|
if (key.sequence === `${ESC}${KITTY_CTRL_C}`) {
|
||||||
broadcast({
|
broadcast({
|
||||||
@@ -272,14 +280,28 @@ export function KeypressProvider({
|
|||||||
!key.sequence.startsWith(FOCUS_OUT))
|
!key.sequence.startsWith(FOCUS_OUT))
|
||||||
) {
|
) {
|
||||||
kittySequenceBuffer += key.sequence;
|
kittySequenceBuffer += key.sequence;
|
||||||
|
|
||||||
|
if (debugKeystrokeLogging) {
|
||||||
|
console.log(
|
||||||
|
'[DEBUG] Kitty buffer accumulating:',
|
||||||
|
kittySequenceBuffer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const kittyKey = parseKittySequence(kittySequenceBuffer);
|
const kittyKey = parseKittySequence(kittySequenceBuffer);
|
||||||
if (kittyKey) {
|
if (kittyKey) {
|
||||||
|
if (debugKeystrokeLogging) {
|
||||||
|
console.log(
|
||||||
|
'[DEBUG] Kitty sequence parsed successfully:',
|
||||||
|
kittySequenceBuffer,
|
||||||
|
);
|
||||||
|
}
|
||||||
kittySequenceBuffer = '';
|
kittySequenceBuffer = '';
|
||||||
broadcast(kittyKey);
|
broadcast(kittyKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config?.getDebugMode()) {
|
if (config?.getDebugMode() || debugKeystrokeLogging) {
|
||||||
const codes = Array.from(kittySequenceBuffer).map((ch) =>
|
const codes = Array.from(kittySequenceBuffer).map((ch) =>
|
||||||
ch.charCodeAt(0),
|
ch.charCodeAt(0),
|
||||||
);
|
);
|
||||||
@@ -287,6 +309,12 @@ export function KeypressProvider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (kittySequenceBuffer.length > MAX_KITTY_SEQUENCE_LENGTH) {
|
if (kittySequenceBuffer.length > MAX_KITTY_SEQUENCE_LENGTH) {
|
||||||
|
if (debugKeystrokeLogging) {
|
||||||
|
console.log(
|
||||||
|
'[DEBUG] Kitty buffer overflow, clearing:',
|
||||||
|
kittySequenceBuffer,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (config) {
|
if (config) {
|
||||||
const event = new KittySequenceOverflowEvent(
|
const event = new KittySequenceOverflowEvent(
|
||||||
kittySequenceBuffer.length,
|
kittySequenceBuffer.length,
|
||||||
@@ -404,7 +432,14 @@ export function KeypressProvider({
|
|||||||
pasteBuffer = Buffer.alloc(0);
|
pasteBuffer = Buffer.alloc(0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [stdin, setRawMode, kittyProtocolEnabled, config, subscribers]);
|
}, [
|
||||||
|
stdin,
|
||||||
|
setRawMode,
|
||||||
|
kittyProtocolEnabled,
|
||||||
|
config,
|
||||||
|
subscribers,
|
||||||
|
debugKeystrokeLogging,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeypressContext.Provider value={{ subscribe, unsubscribe }}>
|
<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.
|
* We use 12 to provide a small buffer.
|
||||||
*/
|
*/
|
||||||
export const MAX_KITTY_SEQUENCE_LENGTH = 12;
|
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