Merge branch 'main' into feat/image-attachment

This commit is contained in:
LaZzyMan 2026-02-10 14:16:21 +08:00
commit 56030f9291
609 changed files with 26677 additions and 12343 deletions

View file

@ -1190,20 +1190,7 @@ 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 () => {
it('should handle kitty sequences when debugKeystrokeLogging is false', async () => {
const keyHandler = vi.fn();
const wrapper = ({ children }: { children: React.ReactNode }) => (
@ -1221,138 +1208,20 @@ describe('KeypressContext - Kitty Protocol', () => {
result.current.subscribe(keyHandler);
});
// Send a kitty sequence
// Send a kitty sequence - should work without debug logging
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'),
);
const parsedCall = consoleLogSpy.mock.calls.find(
(args) =>
typeof args[0] === 'string' &&
args[0].includes('[DEBUG] Kitty sequence parsed successfully'),
);
expect(parsedCall).toBeTruthy();
expect(parsedCall?.[1]).toEqual(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,
name: 'escape',
kittyProtocol: true,
}),
);
});
it('should show char codes when debugKeystrokeLogging is true even without debug mode', async () => {
it('should handle kitty sequences when debugKeystrokeLogging is true', async () => {
const keyHandler = vi.fn();
const wrapper = ({ children }: { children: React.ReactNode }) => (
@ -1370,29 +1239,44 @@ describe('KeypressContext - Kitty Protocol', () => {
result.current.subscribe(keyHandler);
});
// Send incomplete kitty sequence
const sequence = '\x1b[12';
// Send a complete kitty sequence for escape - should work with debug logging
act(() => {
stdin.pressKey({
name: undefined,
ctrl: false,
meta: false,
shift: false,
sequence,
});
stdin.sendKittySequence('\x1b[27u');
});
// Verify debug logging for accumulation
expect(consoleLogSpy).toHaveBeenCalledWith(
'[DEBUG] Kitty buffer accumulating:',
sequence,
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'escape',
kittyProtocol: true,
}),
);
});
it('should handle kitty buffer overflow without crashing', async () => {
const keyHandler = vi.fn();
const wrapper = ({ children }: { children: React.ReactNode }) => (
<KeypressProvider
kittyProtocolEnabled={true}
debugKeystrokeLogging={true}
>
{children}
</KeypressProvider>
);
// Verify warning for char codes
expect(consoleWarnSpy).toHaveBeenCalledWith(
'Kitty sequence buffer has char codes:',
[27, 91, 49, 50],
);
const { result } = renderHook(() => useKeypressContext(), { wrapper });
act(() => {
result.current.subscribe(keyHandler);
});
// Send an invalid long sequence to trigger overflow - should not crash
const longInvalidSequence = '\x1b[' + 'x'.repeat(100);
expect(() => {
act(() => {
stdin.sendKittySequence(longInvalidSequence);
});
}).not.toThrow();
});
});

View file

@ -8,6 +8,7 @@ import type { Config } from '@qwen-code/qwen-code-core';
import {
KittySequenceOverflowEvent,
logKittySequenceOverflow,
createDebugLogger,
} from '@qwen-code/qwen-code-core';
import { useStdin } from 'ink';
import type React from 'react';
@ -62,11 +63,13 @@ export type KeypressHandler = (key: Key) => void;
interface KeypressContextValue {
subscribe: (handler: KeypressHandler) => void;
unsubscribe: (handler: KeypressHandler) => void;
pasteWorkaround: boolean;
}
const KeypressContext = createContext<KeypressContextValue | undefined>(
undefined,
);
const debugLogger = createDebugLogger('KEYPRESS');
export function useKeypressContext() {
const context = useContext(KeypressContext);
@ -502,7 +505,7 @@ export function KeypressProvider({
key.sequence === `${ESC}${KITTY_CTRL_C}`
) {
if (kittySequenceBuffer && debugKeystrokeLogging) {
console.log(
debugLogger.debug(
'[DEBUG] Kitty buffer cleared on Ctrl+C:',
kittySequenceBuffer,
);
@ -536,7 +539,7 @@ export function KeypressProvider({
kittySequenceBuffer += key.sequence;
if (debugKeystrokeLogging) {
console.log(
debugLogger.debug(
'[DEBUG] Kitty buffer accumulating:',
kittySequenceBuffer,
);
@ -554,7 +557,7 @@ export function KeypressProvider({
const nextStart = kittySequenceBuffer.indexOf(`${ESC}[`, 1);
if (nextStart > 0) {
if (debugKeystrokeLogging) {
console.log(
debugLogger.debug(
'[DEBUG] Skipping incomplete/invalid CSI prefix:',
kittySequenceBuffer.slice(0, nextStart),
);
@ -570,12 +573,12 @@ export function KeypressProvider({
parsed.length,
);
if (kittySequenceBuffer.length > parsed.length) {
console.log(
debugLogger.debug(
'[DEBUG] Kitty sequence parsed successfully (prefix):',
parsedSequence,
);
} else {
console.log(
debugLogger.debug(
'[DEBUG] Kitty sequence parsed successfully:',
parsedSequence,
);
@ -592,12 +595,12 @@ export function KeypressProvider({
const codes = Array.from(kittySequenceBuffer).map((ch) =>
ch.charCodeAt(0),
);
console.warn('Kitty sequence buffer has char codes:', codes);
debugLogger.warn('Kitty sequence buffer has char codes:', codes);
}
if (kittySequenceBuffer.length > MAX_KITTY_SEQUENCE_LENGTH) {
if (debugKeystrokeLogging) {
console.log(
debugLogger.debug(
'[DEBUG] Kitty buffer overflow, clearing:',
kittySequenceBuffer,
);
@ -816,7 +819,9 @@ export function KeypressProvider({
]);
return (
<KeypressContext.Provider value={{ subscribe, unsubscribe }}>
<KeypressContext.Provider
value={{ subscribe, unsubscribe, pasteWorkaround }}
>
{children}
</KeypressContext.Provider>
);

View file

@ -53,6 +53,7 @@ export interface UIActions {
handleFolderTrustSelect: (choice: FolderTrustChoice) => void;
setConstrainHeight: (value: boolean) => void;
onEscapePromptChange: (show: boolean) => void;
onSuggestionsVisibilityChange: (visible: boolean) => void;
refreshStatic: () => void;
handleFinalSubmit: (value: string) => void;
handleClearScreen: () => void;

View file

@ -8,7 +8,6 @@ import { createContext, useContext } from 'react';
import type {
HistoryItem,
ThoughtSummary,
ConsoleMessageItem,
ShellConfirmationRequest,
ConfirmationRequest,
LoopDetectionConfirmationRequest,
@ -81,8 +80,6 @@ export interface UIState {
isFolderTrustDialogOpen: boolean;
isTrustedFolder: boolean | undefined;
constrainHeight: boolean;
showErrorDetails: boolean;
filteredConsoleMessages: ConsoleMessageItem[];
ideContextState: IdeContext | undefined;
showToolDescriptions: boolean;
ctrlCPressedOnce: boolean;
@ -96,7 +93,6 @@ export interface UIState {
// Quota-related state
currentModel: string;
contextFileNames: string[];
errorCount: number;
availableTerminalHeight: number | undefined;
mainAreaWidth: number;
staticAreaMaxItemHeight: number;