mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
fix(cli): prevent SGR mouse events from being swallowed as paste on Windows (#7988)
On Windows (pasteWorkaround path), shouldFlushRawDataAsPaste misclassifies raw stdin data containing both a carriage return (0x0d) and an SGR mouse sequence as pasted content. The synthetic paste wrapper sets isPaste=true in handleKeypress, which discards SGR mouse data via resetSgrMouse(), breaking wheel scrolling in VP mode. This commonly triggers when Enter (\r) and a subsequent mouse wheel event arrive in the same stdin chunk — a frequent occurrence on Windows Terminal. macOS is unaffected because handleStdinData does not use this heuristic. Skip the paste heuristic when the buffer contains ANSI escape bytes (0x1b) so SGR mouse sequences always reach readline for proper parsing and dispatch to mouse subscribers. Closes #7964 Co-authored-by: 秦奇 <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]>
This commit is contained in:
2 files changed
+112
No files matched your search
@@ -1031,6 +1031,111 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
expect(mouseHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('dispatches a standalone SGR wheel event to mouse subscribers (pasteWorkaround path)', async () => {
|
||||
const mouseHandler = vi.fn();
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), {
|
||||
wrapper: ({ children }) =>
|
||||
wrapper({ children, pasteWorkaround: true }),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.subscribeMouse(mouseHandler);
|
||||
});
|
||||
|
||||
// A pure SGR scroll-down event (button 65) arriving alone.
|
||||
act(() => {
|
||||
stdin.emit('data', Buffer.from('\x1b[<65;10;20M'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mouseHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(mouseHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'scroll-down', col: 10, row: 20 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('dispatches SGR wheel event arriving in fragmented chunks (pasteWorkaround path)', async () => {
|
||||
const mouseHandler = vi.fn();
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), {
|
||||
wrapper: ({ children }) =>
|
||||
wrapper({ children, pasteWorkaround: true }),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.subscribeMouse(mouseHandler);
|
||||
});
|
||||
|
||||
// SGR sequence split across two stdin chunks.
|
||||
act(() => {
|
||||
stdin.emit('data', Buffer.from('\x1b[<64;5'));
|
||||
stdin.emit('data', Buffer.from(';15M'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mouseHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(mouseHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'scroll-up', col: 5, row: 15 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('dispatches SGR wheel event when \\r arrives in the same chunk (pasteWorkaround path)', async () => {
|
||||
const mouseHandler = vi.fn();
|
||||
const keyHandler = vi.fn();
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), {
|
||||
wrapper: ({ children }) =>
|
||||
wrapper({ children, pasteWorkaround: true }),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
result.current.subscribeMouse(mouseHandler);
|
||||
});
|
||||
|
||||
// Windows Terminal may deliver a preceding Enter (\r) in the same
|
||||
// stdin chunk as the SGR mouse sequence. The \r must not cause
|
||||
// shouldFlushRawDataAsPaste to misclassify the SGR data as paste.
|
||||
act(() => {
|
||||
stdin.emit('data', Buffer.from('\r\x1b[<65;10;20M'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mouseHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(mouseHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'scroll-down', col: 10, row: 20 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('dispatches SGR wheel event when \\r\\n arrives in the same chunk (pasteWorkaround path)', async () => {
|
||||
const mouseHandler = vi.fn();
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), {
|
||||
wrapper: ({ children }) =>
|
||||
wrapper({ children, pasteWorkaround: true }),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.subscribeMouse(mouseHandler);
|
||||
});
|
||||
|
||||
// Windows-style \r\n followed by SGR in one chunk.
|
||||
act(() => {
|
||||
stdin.emit('data', Buffer.from('\r\n\x1b[<65;3;7M'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mouseHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(mouseHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'scroll-down', col: 3, row: 7 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty paste sequence', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
|
||||
|
||||
@@ -1201,6 +1201,13 @@ export function KeypressProvider({
|
||||
});
|
||||
|
||||
const shouldFlushRawDataAsPaste = (data: Buffer) => {
|
||||
// ANSI escape sequences (SGR mouse events, cursor keys) must reach
|
||||
// readline for proper parsing. Wrapping them in synthetic paste events
|
||||
// causes handleKeypress's isPaste guard to discard SGR mouse data,
|
||||
// breaking wheel scrolling on Windows where \r from Enter commonly
|
||||
// shares a stdin chunk with a subsequent mouse event.
|
||||
if (data.includes(0x1b)) return false;
|
||||
|
||||
const hasReturn = data.includes(0x0d);
|
||||
const hasEmbeddedTab = data.length > 1 && data.includes(0x09);
|
||||
const isSingleReturn = data.length <= 2 && hasReturn;
|
||||
|
||||
Reference in new issue
Block a user