From b5315bfc208c754eea1204260bdbe0d10c14819b Mon Sep 17 00:00:00 2001 From: Tommaso Sciortino Date: Mon, 3 Nov 2025 20:30:42 -0800 Subject: [PATCH] Fix alt+left on ghostty (#12503) --- .../src/ui/contexts/KeypressContext.test.tsx | 2 -- .../cli/src/ui/contexts/KeypressContext.tsx | 6 +--- .../cli/src/ui/hooks/useKeypress.test.tsx | 34 ++++++++++++------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx index cb11f7e185..63b265ca26 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.test.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx @@ -906,7 +906,6 @@ describe('Kitty Sequence Parsing', () => { // Should broadcast immediately as it's not a valid kitty pattern expect(keyHandler).toHaveBeenCalledWith( expect.objectContaining({ - name: '', sequence: '\x1b[m', paste: false, }), @@ -1007,7 +1006,6 @@ describe('Kitty Sequence Parsing', () => { expect(keyHandler).toHaveBeenNthCalledWith( 2, expect.objectContaining({ - name: '', sequence: '\x1b[!', }), ); diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index 4c605fbab4..b7474e9879 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -686,12 +686,8 @@ export function KeypressProvider({ } // Always check if this could start a sequence we need to buffer (Kitty or Mouse) - // We only want to intercept if it starts with ESC[ (CSI) or is EXACTLY ESC (waiting for more). // Other ESC sequences (like Alt+Key which is ESC+Key) should be let through if readline parsed them. - const isCSI = key.sequence.startsWith(`${ESC}[`); - const isExactEsc = key.sequence === ESC; - const shouldBuffer = isCSI || isExactEsc; - + const shouldBuffer = couldBeKittySequence(key.sequence); const isExcluded = [ PASTE_MODE_START, PASTE_MODE_END, diff --git a/packages/cli/src/ui/hooks/useKeypress.test.tsx b/packages/cli/src/ui/hooks/useKeypress.test.tsx index c2aac50142..f22b9db530 100644 --- a/packages/cli/src/ui/hooks/useKeypress.test.tsx +++ b/packages/cli/src/ui/hooks/useKeypress.test.tsx @@ -38,7 +38,7 @@ class MockStdin extends EventEmitter { } } -describe('useKeypress', () => { +describe.each([true, false])(`useKeypress with useKitty=%s`, (useKitty) => { let stdin: MockStdin; const mockSetRawMode = vi.fn(); const onKeypress = vi.fn(); @@ -50,7 +50,7 @@ describe('useKeypress', () => { return null; } return render( - + , ); @@ -58,6 +58,7 @@ describe('useKeypress', () => { beforeEach(() => { vi.clearAllMocks(); + vi.useFakeTimers(); stdin = new MockStdin(); (useStdin as Mock).mockReturnValue({ stdin, @@ -186,21 +187,28 @@ describe('useKeypress', () => { expect(onKeypress).toHaveBeenCalledTimes(1); }); - it('should handle paste false alarm', () => { + it('should handle paste false alarm', async () => { renderKeypressHook(true); act(() => { stdin.write(PASTE_START.slice(0, 5)); stdin.write('do'); }); - expect(onKeypress).toHaveBeenCalledWith( - expect.objectContaining({ sequence: '\x1B[200d' }), - ); - expect(onKeypress).toHaveBeenCalledWith( - expect.objectContaining({ sequence: 'o' }), - ); - expect(onKeypress).toHaveBeenCalledTimes(2); + if (useKitty) { + vi.advanceTimersByTime(60); // wait for kitty timeout + expect(onKeypress).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ sequence: '\x1B[200do' }), + ); + } else { + expect(onKeypress).toHaveBeenCalledWith( + expect.objectContaining({ sequence: '\x1B[200d' }), + ); + expect(onKeypress).toHaveBeenCalledWith( + expect.objectContaining({ sequence: 'o' }), + ); + expect(onKeypress).toHaveBeenCalledTimes(2); + } }); it('should handle back to back pastes', () => { @@ -240,11 +248,11 @@ describe('useKeypress', () => { const pasteText = 'pasted'; await act(async () => { stdin.write(PASTE_START.slice(0, 3)); - await new Promise((r) => setTimeout(r, 50)); + vi.advanceTimersByTime(50); stdin.write(PASTE_START.slice(3) + pasteText.slice(0, 3)); - await new Promise((r) => setTimeout(r, 50)); + vi.advanceTimersByTime(50); stdin.write(pasteText.slice(3) + PASTE_END.slice(0, 3)); - await new Promise((r) => setTimeout(r, 50)); + vi.advanceTimersByTime(50); stdin.write(PASTE_END.slice(3)); }); expect(onKeypress).toHaveBeenCalledWith(