Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/src/ui/components/SettingsDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ enum TerminalKeys {
LEFT_ARROW = '\u001B[D',
RIGHT_ARROW = '\u001B[C',
ESCAPE = '\u001B',
BACKSPACE = '\u0008',
BACKSPACE = '\x7f',
CTRL_L = '\u000C',
}

Expand Down
86 changes: 85 additions & 1 deletion packages/cli/src/ui/contexts/KeypressContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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([
{
Expand Down
16 changes: 14 additions & 2 deletions packages/cli/src/ui/contexts/KeypressContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading