mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 20:50:34 +00:00
Merge branch 'main' into feature/arena-agent-collaboration
This commit is contained in:
commit
fe304a0c32
78 changed files with 3777 additions and 511 deletions
|
|
@ -1367,6 +1367,75 @@ describe('KeypressContext - Kitty Protocol', () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('drops unsupported Kitty CSI-u keys without blocking later input', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(`\x1b[57358u`)); // CAPS_LOCK
|
||||
act(() =>
|
||||
stdin.pressKey({
|
||||
name: 'a',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: 'a',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledTimes(1);
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'a',
|
||||
sequence: 'a',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('recovers plain text that arrives in the same chunk after an unsupported CSI-u key', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() =>
|
||||
stdin.pressKey({
|
||||
name: '',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: '\x1b[57358ua',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledTimes(1);
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'a',
|
||||
sequence: 'a',
|
||||
kittyProtocol: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('drops unsupported CSI-u variants with event metadata and keeps parsing', () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(`\x1b[57358;1:1u\x1b[100u`));
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledTimes(1);
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'd',
|
||||
sequence: 'd',
|
||||
kittyProtocol: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Kitty keypad private-use keys', () => {
|
||||
|
|
|
|||
|
|
@ -178,6 +178,25 @@ export function KeypressProvider({
|
|||
let rawDataBuffer = Buffer.alloc(0);
|
||||
let rawFlushTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
const createPrintableKey = (char: string): Key => {
|
||||
const printableName =
|
||||
char === ' '
|
||||
? 'space'
|
||||
: /^[A-Za-z]$/.test(char)
|
||||
? char.toLowerCase()
|
||||
: char;
|
||||
|
||||
return {
|
||||
name: printableName,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: char,
|
||||
kittyProtocol: true,
|
||||
};
|
||||
};
|
||||
|
||||
// Parse a single complete kitty sequence from the start (prefix) of the
|
||||
// buffer and return both the Key and the number of characters consumed.
|
||||
// This lets us "peel off" one complete event when multiple sequences arrive
|
||||
|
|
@ -415,22 +434,11 @@ export function KeypressProvider({
|
|||
keyCode <= 0x10ffff &&
|
||||
!(keyCode >= 0xe000 && keyCode <= 0xf8ff)
|
||||
) {
|
||||
const char = String.fromCodePoint(keyCode);
|
||||
const printableName =
|
||||
char === ' '
|
||||
? 'space'
|
||||
: /^[A-Za-z]$/.test(char)
|
||||
? char.toLowerCase()
|
||||
: char;
|
||||
return {
|
||||
key: {
|
||||
name: printableName,
|
||||
ctrl: false,
|
||||
...createPrintableKey(String.fromCodePoint(keyCode)),
|
||||
meta: alt,
|
||||
shift,
|
||||
paste: false,
|
||||
sequence: char,
|
||||
kittyProtocol: true,
|
||||
},
|
||||
length: m[0].length,
|
||||
};
|
||||
|
|
@ -490,6 +498,42 @@ export function KeypressProvider({
|
|||
return null;
|
||||
};
|
||||
|
||||
const getCompleteCsiSequenceLength = (buffer: string): number | null => {
|
||||
if (!buffer.startsWith(`${ESC}[`)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = 2; i < buffer.length; i++) {
|
||||
const code = buffer.charCodeAt(i);
|
||||
if (code >= 0x40 && code <= 0x7e) {
|
||||
return i + 1;
|
||||
}
|
||||
if (code < 0x20 || code > 0x3f) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const parsePlainTextPrefix = (
|
||||
buffer: string,
|
||||
): { key: Key; length: number } | null => {
|
||||
if (!buffer || buffer.startsWith(ESC)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [char] = Array.from(buffer);
|
||||
if (!char) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key: createPrintableKey(char),
|
||||
length: char.length,
|
||||
};
|
||||
};
|
||||
|
||||
const broadcast = (key: Key) => {
|
||||
for (const handler of subscribers) {
|
||||
handler(key);
|
||||
|
|
@ -653,47 +697,82 @@ export function KeypressProvider({
|
|||
// start of the buffer. This handles batched inputs cleanly. If the
|
||||
// prefix is incomplete or invalid, skip to the next CSI introducer
|
||||
// (ESC[) so that a following valid sequence can still be parsed.
|
||||
let parsedAny = false;
|
||||
let bufferedInputHandled = false;
|
||||
while (kittySequenceBuffer) {
|
||||
const parsed = parseKittyPrefix(kittySequenceBuffer);
|
||||
if (!parsed) {
|
||||
// Look for the next potential CSI start beyond index 0
|
||||
const nextStart = kittySequenceBuffer.indexOf(`${ESC}[`, 1);
|
||||
if (nextStart > 0) {
|
||||
if (debugKeystrokeLogging) {
|
||||
if (parsed) {
|
||||
if (debugKeystrokeLogging) {
|
||||
const parsedSequence = kittySequenceBuffer.slice(
|
||||
0,
|
||||
parsed.length,
|
||||
);
|
||||
if (kittySequenceBuffer.length > parsed.length) {
|
||||
debugLogger.debug(
|
||||
'[DEBUG] Skipping incomplete/invalid CSI prefix:',
|
||||
kittySequenceBuffer.slice(0, nextStart),
|
||||
'[DEBUG] Kitty sequence parsed successfully (prefix):',
|
||||
parsedSequence,
|
||||
);
|
||||
} else {
|
||||
debugLogger.debug(
|
||||
'[DEBUG] Kitty sequence parsed successfully:',
|
||||
parsedSequence,
|
||||
);
|
||||
}
|
||||
kittySequenceBuffer = kittySequenceBuffer.slice(nextStart);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
// Consume the parsed prefix and broadcast it.
|
||||
kittySequenceBuffer = kittySequenceBuffer.slice(parsed.length);
|
||||
broadcast(parsed.key);
|
||||
bufferedInputHandled = true;
|
||||
continue;
|
||||
}
|
||||
if (debugKeystrokeLogging) {
|
||||
const parsedSequence = kittySequenceBuffer.slice(
|
||||
0,
|
||||
parsed.length,
|
||||
|
||||
const completeUnsupportedCsiLength =
|
||||
getCompleteCsiSequenceLength(kittySequenceBuffer);
|
||||
if (completeUnsupportedCsiLength) {
|
||||
if (debugKeystrokeLogging) {
|
||||
debugLogger.debug(
|
||||
'[DEBUG] Dropping unsupported complete CSI sequence:',
|
||||
kittySequenceBuffer.slice(0, completeUnsupportedCsiLength),
|
||||
);
|
||||
}
|
||||
kittySequenceBuffer = kittySequenceBuffer.slice(
|
||||
completeUnsupportedCsiLength,
|
||||
);
|
||||
if (kittySequenceBuffer.length > parsed.length) {
|
||||
bufferedInputHandled = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const plainTextPrefix = parsePlainTextPrefix(kittySequenceBuffer);
|
||||
if (plainTextPrefix) {
|
||||
if (debugKeystrokeLogging) {
|
||||
debugLogger.debug(
|
||||
'[DEBUG] Kitty sequence parsed successfully (prefix):',
|
||||
parsedSequence,
|
||||
);
|
||||
} else {
|
||||
debugLogger.debug(
|
||||
'[DEBUG] Kitty sequence parsed successfully:',
|
||||
parsedSequence,
|
||||
'[DEBUG] Recovered plain text after kitty sequence:',
|
||||
plainTextPrefix.key.sequence,
|
||||
);
|
||||
}
|
||||
kittySequenceBuffer = kittySequenceBuffer.slice(
|
||||
plainTextPrefix.length,
|
||||
);
|
||||
broadcast(plainTextPrefix.key);
|
||||
bufferedInputHandled = true;
|
||||
continue;
|
||||
}
|
||||
// Consume the parsed prefix and broadcast it.
|
||||
kittySequenceBuffer = kittySequenceBuffer.slice(parsed.length);
|
||||
broadcast(parsed.key);
|
||||
parsedAny = true;
|
||||
|
||||
// Look for the next potential CSI start beyond index 0
|
||||
const nextStart = kittySequenceBuffer.indexOf(`${ESC}[`, 1);
|
||||
if (nextStart > 0) {
|
||||
if (debugKeystrokeLogging) {
|
||||
debugLogger.debug(
|
||||
'[DEBUG] Skipping incomplete/invalid CSI prefix:',
|
||||
kittySequenceBuffer.slice(0, nextStart),
|
||||
);
|
||||
}
|
||||
kittySequenceBuffer = kittySequenceBuffer.slice(nextStart);
|
||||
bufferedInputHandled = true;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (parsedAny) return;
|
||||
if (bufferedInputHandled) return;
|
||||
|
||||
if (config?.getDebugMode() || debugKeystrokeLogging) {
|
||||
const codes = Array.from(kittySequenceBuffer).map((ch) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue