diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx index 9887415a57..7ba451d538 100644 --- a/packages/cli/src/ui/components/SettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx @@ -44,7 +44,7 @@ enum TerminalKeys { LEFT_ARROW = '\u001B[D', RIGHT_ARROW = '\u001B[C', ESCAPE = '\u001B', - BACKSPACE = '\u0008', + BACKSPACE = '\x7f', CTRL_P = '\u0010', CTRL_N = '\u000E', } diff --git a/packages/cli/src/ui/components/shared/BaseSettingsDialog.test.tsx b/packages/cli/src/ui/components/shared/BaseSettingsDialog.test.tsx index f66af9fd17..c49c967714 100644 --- a/packages/cli/src/ui/components/shared/BaseSettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/shared/BaseSettingsDialog.test.tsx @@ -24,7 +24,7 @@ enum TerminalKeys { LEFT_ARROW = '\u001B[D', RIGHT_ARROW = '\u001B[C', ESCAPE = '\u001B', - BACKSPACE = '\u0008', + BACKSPACE = '\x7f', CTRL_L = '\u000C', } diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx index e7d0406dd7..26f1c1cf35 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.test.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx @@ -9,7 +9,17 @@ import { act } from 'react'; import { renderHookWithProviders } from '../../test-utils/render.js'; import { createMockSettings } from '../../test-utils/settings.js'; import { waitFor } from '../../test-utils/async.js'; -import { vi, afterAll, beforeAll, type Mock } from 'vitest'; +import type { Mock } from 'vitest'; +import { + vi, + afterAll, + beforeAll, + describe, + it, + expect, + beforeEach, + afterEach, +} from 'vitest'; import { useKeypressContext, ESC_TIMEOUT, @@ -431,6 +441,80 @@ describe('KeypressContext', () => { ); }); + describe('Windows Terminal Backspace handling', () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('should NOT treat \\b as ctrl when WT_SESSION is NOT present and OS is not Windows_NT', async () => { + vi.stubEnv('WT_SESSION', ''); + vi.stubEnv('OS', 'Linux'); + const { keyHandler } = await setupKeypressTest(); + + act(() => { + stdin.write('\b'); + }); + + expect(keyHandler).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'backspace', + ctrl: false, + }), + ); + }); + + it('should treat \\b as ctrl when WT_SESSION IS present (even if not Windows_NT)', async () => { + vi.stubEnv('WT_SESSION', 'some-id'); + vi.stubEnv('OS', 'Linux'); + const { keyHandler } = await setupKeypressTest(); + + act(() => { + stdin.write('\b'); + }); + + expect(keyHandler).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'backspace', + ctrl: true, + }), + ); + }); + + it('should treat \\b as ctrl when OS is Windows_NT', async () => { + vi.stubEnv('WT_SESSION', ''); + vi.stubEnv('OS', 'Windows_NT'); + const { keyHandler } = await setupKeypressTest(); + + act(() => { + stdin.write('\b'); + }); + + expect(keyHandler).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'backspace', + ctrl: true, + }), + ); + }); + + it('should treat \\x7f as regular backspace regardless of WT_SESSION or OS', async () => { + vi.stubEnv('WT_SESSION', 'some-id'); + vi.stubEnv('OS', 'Windows_NT'); + const { keyHandler } = await setupKeypressTest(); + + act(() => { + stdin.write('\x7f'); + }); + + expect(keyHandler).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'backspace', + ctrl: false, + }), + ); + }); + }); + describe('paste mode', () => { it.each([ { diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index 3a3961221f..d834608fbe 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -651,8 +651,20 @@ function* emitKeys( // tab name = 'tab'; alt = escaped; - } else if (ch === '\b' || ch === '\x7f') { - // backspace or ctrl+h + } else if (ch === '\b') { + // ctrl+h / ctrl+backspace (windows terminals send \x08 for ctrl+backspace) + name = 'backspace'; + // In Windows environments, \b is sent for Ctrl+Backspace (standard backspace is translated to \x7f). + // We scope this to Windows/WT_SESSION to avoid breaking other unixes where \b is a plain backspace. + if ( + typeof process !== 'undefined' && + (process.env?.['OS'] === 'Windows_NT' || !!process.env?.['WT_SESSION']) + ) { + ctrl = true; + } + alt = escaped; + } else if (ch === '\x7f') { + // backspace name = 'backspace'; alt = escaped; } else if (ch === ESC) {