From e9e1a6d05576af19ac3d72f08c7aceabdf4083a1 Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Thu, 28 May 2026 17:24:38 -0700 Subject: [PATCH 1/3] Handle Windows Shift Enter newlines --- docs/specs/layout.md | 1 + lib/src/lib/platform/index.ts | 6 ++ lib/src/lib/terminal-keyboard.test.ts | 53 ++++++++++++ lib/src/lib/terminal-keyboard.ts | 27 ++++++ lib/src/lib/terminal-lifecycle.ts | 92 ++++++++++++--------- lib/src/lib/terminal-registry.alert.test.ts | 46 +++++++++++ 6 files changed, 188 insertions(+), 37 deletions(-) create mode 100644 lib/src/lib/terminal-keyboard.test.ts create mode 100644 lib/src/lib/terminal-keyboard.ts diff --git a/docs/specs/layout.md b/docs/specs/layout.md index a137d925..fda0af3c 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -141,6 +141,7 @@ Wall starts in `command` mode by default. Embedders may pass `initialMode="passt - All keyboard input routes to the active session's xterm.js instance - Only the mode-exit gesture (LCmd → RCmd) is intercepted - In the VS Code host, selected workbench chords are mirrored: xterm still processes the key, and Dormouse also asks the extension host to run the matching VS Code command. See [the VS Code host spec](vscode.md) for the allowlist. +- On Windows, `Shift+Enter` is normalized before xterm's default Enter handling. If the foreground app has enabled bracketed paste, Dormouse sends a bracketed-paste LF (`\x1b[200~\n\x1b[201~`); otherwise it sends bare LF (`\x0a`). This preserves the common multiline-input contract used by terminal TUIs such as Codex, where plain `Enter` submits and pasted/LF newline input inserts a newline. - Selection overlay shows 2px solid border with glow - Terminal has DOM focus diff --git a/lib/src/lib/platform/index.ts b/lib/src/lib/platform/index.ts index 3b0a544b..880e2e2e 100644 --- a/lib/src/lib/platform/index.ts +++ b/lib/src/lib/platform/index.ts @@ -34,6 +34,12 @@ export const PLATFORM_STRING: string = (() => { */ export const IS_MAC: boolean = /Mac|iPhone|iPad/i.test(PLATFORM_STRING); +/** + * True when running on Windows. Used for keyboard behavior that Windows + * terminals commonly normalize differently from macOS terminals. + */ +export const IS_WINDOWS: boolean = /Win/i.test(PLATFORM_STRING); + let adapter: PlatformAdapter | null = null; /** Set an externally-created platform adapter (e.g. TauriAdapter from standalone). */ diff --git a/lib/src/lib/terminal-keyboard.test.ts b/lib/src/lib/terminal-keyboard.test.ts new file mode 100644 index 00000000..3a0f7003 --- /dev/null +++ b/lib/src/lib/terminal-keyboard.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest'; +import { + BRACKETED_PASTE_NEWLINE_INPUT, + SHIFT_ENTER_NEWLINE_INPUT, + shiftEnterInputForEvent, + shouldHandleWindowsShiftEnter, +} from './terminal-keyboard'; + +function keydown(init: Partial = {}): KeyboardEvent { + return { + altKey: false, + ctrlKey: false, + isComposing: false, + key: 'Enter', + metaKey: false, + shiftKey: true, + type: 'keydown', + ...init, + } as KeyboardEvent; +} + +describe('terminal keyboard normalization', () => { + it('uses LF for the Shift+Enter newline override', () => { + expect(SHIFT_ENTER_NEWLINE_INPUT).toBe('\n'); + }); + + it('matches plain Shift+Enter on Windows', () => { + expect(shouldHandleWindowsShiftEnter(keydown(), { isWindows: true })).toBe(true); + }); + + it('uses bracketed paste for Shift+Enter when the foreground app enabled bracketed paste', () => { + expect(shiftEnterInputForEvent(keydown(), { isWindows: true, bracketedPasteMode: true })) + .toBe(BRACKETED_PASTE_NEWLINE_INPUT); + }); + + it('falls back to LF when bracketed paste is not enabled', () => { + expect(shiftEnterInputForEvent(keydown(), { isWindows: true, bracketedPasteMode: false })) + .toBe(SHIFT_ENTER_NEWLINE_INPUT); + }); + + it('does not match normal Enter, composing input, or non-Windows platforms', () => { + expect(shouldHandleWindowsShiftEnter(keydown({ shiftKey: false }), { isWindows: true })).toBe(false); + expect(shouldHandleWindowsShiftEnter(keydown({ isComposing: true }), { isWindows: true })).toBe(false); + expect(shouldHandleWindowsShiftEnter(keydown(), { isWindows: false })).toBe(false); + expect(shiftEnterInputForEvent(keydown(), { isWindows: false, bracketedPasteMode: true })).toBe(null); + }); + + it('leaves modified Enter chords alone', () => { + expect(shouldHandleWindowsShiftEnter(keydown({ ctrlKey: true }), { isWindows: true })).toBe(false); + expect(shouldHandleWindowsShiftEnter(keydown({ altKey: true }), { isWindows: true })).toBe(false); + expect(shouldHandleWindowsShiftEnter(keydown({ metaKey: true }), { isWindows: true })).toBe(false); + }); +}); diff --git a/lib/src/lib/terminal-keyboard.ts b/lib/src/lib/terminal-keyboard.ts new file mode 100644 index 00000000..95deb0ed --- /dev/null +++ b/lib/src/lib/terminal-keyboard.ts @@ -0,0 +1,27 @@ +type KeyboardEventLike = Pick< + KeyboardEvent, + 'altKey' | 'ctrlKey' | 'isComposing' | 'key' | 'metaKey' | 'shiftKey' | 'type' +>; + +export const SHIFT_ENTER_NEWLINE_INPUT = '\n'; +export const BRACKETED_PASTE_NEWLINE_INPUT = '\x1b[200~\n\x1b[201~'; + +export function shouldHandleWindowsShiftEnter( + event: KeyboardEventLike, + options: { isWindows: boolean }, +): boolean { + return shiftEnterInputForEvent(event, { ...options, bracketedPasteMode: false }) !== null; +} + +export function shiftEnterInputForEvent( + event: KeyboardEventLike, + options: { isWindows: boolean; bracketedPasteMode: boolean }, +): string | null { + if (!options.isWindows) return null; + if (event.type !== 'keydown') return null; + if (event.isComposing) return null; + if (event.key !== 'Enter') return null; + if (!event.shiftKey) return null; + if (event.ctrlKey || event.altKey || event.metaKey) return null; + return options.bracketedPasteMode ? BRACKETED_PASTE_NEWLINE_INPUT : SHIFT_ENTER_NEWLINE_INPUT; +} diff --git a/lib/src/lib/terminal-lifecycle.ts b/lib/src/lib/terminal-lifecycle.ts index 76f254b5..9865bb91 100644 --- a/lib/src/lib/terminal-lifecycle.ts +++ b/lib/src/lib/terminal-lifecycle.ts @@ -1,7 +1,7 @@ import { Terminal, type IBufferRange } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import { UnicodeGraphemesAddon } from '@xterm/addon-unicode-graphemes'; -import { getPlatform, IS_MAC } from './platform'; +import { getPlatform, IS_MAC, IS_WINDOWS } from './platform'; import { requestExternalLinkConfirmation } from './external-link-confirmation'; import { attachMouseModeObserver } from './mouse-mode-observer'; import { @@ -28,6 +28,7 @@ import { writeReplay, } from './terminal-report-filter'; import { getTerminalTheme, paintTerminalHost, startThemeObserver } from './terminal-theme'; +import { shiftEnterInputForEvent } from './terminal-keyboard'; import { ensureTerminalPaneState, fillTerminalProcessCwdByPtyId, @@ -93,7 +94,7 @@ function readDisplayTextFromBuffer(terminal: Terminal, range: IBufferRange): str } } -function createXtermHost(): { terminal: Terminal; fit: FitAddon; element: HTMLDivElement } { +function createXtermHost(id: string): { terminal: Terminal; fit: FitAddon; element: HTMLDivElement } { const styles = getComputedStyle(document.body); const editorFontSize = parseInt(styles.getPropertyValue('--vscode-editor-font-size'), 10) || 12; const editorFontFamily = styles.getPropertyValue('--vscode-editor-font-family').trim() || "'SF Mono', Menlo, Monaco, monospace"; @@ -116,16 +117,31 @@ function createXtermHost(): { terminal: Terminal; fit: FitAddon; element: HTMLDi }, }); - // Only hosts that can run workbench commands (the VS Code adapter) opt in; - // on every other platform runWorkbenchCommand is undefined, so the chords - // stay in xterm exactly as before. - if (getPlatform().runWorkbenchCommand) { + // Two independent reasons to intercept keydown: + // - Windows Shift+Enter needs newline normalization so terminal TUIs see + // multiline input instead of Enter. + // - Hosts that run workbench commands (the VS Code adapter) opt in to + // forwarding F1/Ctrl+P/etc. up to the workbench; non-VS-Code hosts + // leave those chords alone so the browser default still fires. + if (IS_WINDOWS || getPlatform().runWorkbenchCommand) { terminal.attachCustomKeyEventHandler((event) => { + const shiftEnterInput = shiftEnterInputForEvent(event, { + isWindows: IS_WINDOWS, + bracketedPasteMode: terminal.modes.bracketedPasteMode, + }); + if (shiftEnterInput !== null) { + event.preventDefault(); + event.stopPropagation(); + handleTerminalInput(id, terminal, shiftEnterInput); + return false; + } + const runWorkbenchCommand = getPlatform().runWorkbenchCommand; + if (!runWorkbenchCommand) return true; const command = vscodeWorkbenchCommandForKeydown(event, { isMac: IS_MAC }); if (!command) return true; event.preventDefault(); event.stopPropagation(); - getPlatform().runWorkbenchCommand?.(command); + runWorkbenchCommand(command); return true; }); } @@ -163,6 +179,36 @@ function wirePtyEvents(id: string, terminal: Terminal): () => void { }; } +function handleTerminalInput(id: string, terminal: Terminal, data: string): void { + let input = data; + if (getMouseSelectionState(id).override !== 'off') { + input = stripMouseReportsFromInput(input); + if (input.length === 0) return; + } + + const isReplayTerminalReport = inputIsReplayTerminalReport(input); + + if (isReplayTerminalReport && registry.get(id)?.isReplaying) return; + + if (!isReplayTerminalReport) { + markSessionTouched(id); + } + + const isSyntheticTerminalReport = inputIsSyntheticTerminalReport(input); + + if (!isSyntheticTerminalReport) { + recordTerminalUserInputByPtyId(id, input, makePromptLineReader(terminal)); + const entry = registry.get(id); + const hadTodo = entry?.todo === true; + getPlatform().alertAttend(id); + if (hadTodo && inputContainsEnter(input)) { + getPlatform().alertClearTodo(id); + } + } + + getPlatform().writePty(id, input); +} + /** xterm input/resize/render handlers. Returns a dispose. The render * handler watches selectionBaseline (mutated by the mouse router) so the * baseline is read by reference rather than captured. */ @@ -171,35 +217,7 @@ function wireXtermHandlers( terminal: Terminal, selectionBaselineRef: { current: string | null }, ): () => void { - const inputDisposable = terminal.onData((data) => { - let input = data; - if (getMouseSelectionState(id).override !== 'off') { - input = stripMouseReportsFromInput(input); - if (input.length === 0) return; - } - - const isReplayTerminalReport = inputIsReplayTerminalReport(input); - - if (isReplayTerminalReport && registry.get(id)?.isReplaying) return; - - if (!isReplayTerminalReport) { - markSessionTouched(id); - } - - const isSyntheticTerminalReport = inputIsSyntheticTerminalReport(input); - - if (!isSyntheticTerminalReport) { - recordTerminalUserInputByPtyId(id, input, makePromptLineReader(terminal)); - const entry = registry.get(id); - const hadTodo = entry?.todo === true; - getPlatform().alertAttend(id); - if (hadTodo && inputContainsEnter(input)) { - getPlatform().alertClearTodo(id); - } - } - - getPlatform().writePty(id, input); - }); + const inputDisposable = terminal.onData((data) => handleTerminalInput(id, terminal, data)); const resizeDisposable = terminal.onResize(({ cols, rows }) => { getPlatform().alertResize(id); @@ -232,7 +250,7 @@ function wireXtermHandlers( } function setupTerminalEntry(id: string, options: { untouched?: boolean } = {}): TerminalEntry { - const { terminal, fit, element } = createXtermHost(); + const { terminal, fit, element } = createXtermHost(id); const selectionBaselineRef = { current: null as string | null }; const disposePty = wirePtyEvents(id, terminal); diff --git a/lib/src/lib/terminal-registry.alert.test.ts b/lib/src/lib/terminal-registry.alert.test.ts index 8422d851..e74c26a1 100644 --- a/lib/src/lib/terminal-registry.alert.test.ts +++ b/lib/src/lib/terminal-registry.alert.test.ts @@ -23,6 +23,7 @@ vi.mock('@xterm/xterm', () => { writes: string[] = []; private dataListeners = new Set<(data: string) => void>(); private resizeListeners = new Set<(size: { cols: number; rows: number }) => void>(); + private keyHandler: ((event: KeyboardEvent) => boolean) | null = null; parser = { registerCsiHandler: () => ({ dispose: () => {} }), @@ -62,6 +63,10 @@ vi.mock('@xterm/xterm', () => { return { dispose: () => {} }; } + attachCustomKeyEventHandler(handler: (event: KeyboardEvent) => boolean): void { + this.keyHandler = handler; + } + focus(): void {} blur(): void {} @@ -72,6 +77,21 @@ vi.mock('@xterm/xterm', () => { this.dataListeners.forEach((listener) => listener(data)); } + emitKeyDown(init: Partial = {}): boolean | null { + return this.keyHandler?.({ + altKey: false, + ctrlKey: false, + isComposing: false, + key: 'Enter', + metaKey: false, + preventDefault: vi.fn(), + shiftKey: true, + stopPropagation: vi.fn(), + type: 'keydown', + ...init, + } as KeyboardEvent) ?? null; + } + emitResize(cols: number, rows: number): void { this.resizeListeners.forEach((listener) => listener({ cols, rows })); } @@ -83,8 +103,11 @@ vi.mock('@xterm/xterm', () => { vi.mock('./platform', async () => { const actual = await vi.importActual('./platform'); const fakePlatform = new actual.FakePtyAdapter(); + // Force IS_WINDOWS so the Shift+Enter handler is attached regardless of + // the host OS this test runs on (Linux CI evaluates it false at module load). return { ...actual, + IS_WINDOWS: true, getPlatform: () => fakePlatform, __fakePlatform: fakePlatform, }; @@ -121,8 +144,10 @@ import { import { pasteFilePaths } from './clipboard'; interface MockTerminalInstance { + modes: { bracketedPasteMode: boolean }; writes: string[]; emitInput(data: string): void; + emitKeyDown(init?: Partial): boolean | null; emitResize(cols: number, rows: number): void; } @@ -279,6 +304,27 @@ describe('terminal-registry alert behavior', () => { expect(isUntouched(id)).toBe(false); }); + it('sends LF for Windows Shift+Enter before xterm handles Enter normally', () => { + const id = 'shift-enter-lf'; + const entry = createSession(id); + + const handled = entry.terminal.emitKeyDown(); + + expect(handled).toBe(false); + expect(entry.terminal.writes).toContain('\n'); + }); + + it('sends bracketed-paste newline for Windows Shift+Enter when the app enabled bracketed paste', () => { + const id = 'shift-enter-bracketed'; + const entry = createSession(id); + entry.terminal.modes.bracketedPasteMode = true; + + const handled = entry.terminal.emitKeyDown(); + + expect(handled).toBe(false); + expect(entry.terminal.writes).toContain('\x1b[200~\n\x1b[201~'); + }); + it('does not mark synthetic terminal reports as touched', () => { const id = 'synthetic-report-untouched'; const entry = createSession(id); From de7dfcaaeb4fc2ce4847fcf7e354289f65c9ab02 Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Thu, 28 May 2026 19:52:27 -0700 Subject: [PATCH 2/3] Try bracketed paste for Windows Shift Enter --- docs/specs/layout.md | 2 +- lib/src/lib/terminal-keyboard.test.ts | 11 +++-------- lib/src/lib/terminal-keyboard.ts | 6 +++--- lib/src/lib/terminal-lifecycle.ts | 1 - lib/src/lib/terminal-registry.alert.test.ts | 13 +------------ 5 files changed, 8 insertions(+), 25 deletions(-) diff --git a/docs/specs/layout.md b/docs/specs/layout.md index fda0af3c..ddf96a17 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -141,7 +141,7 @@ Wall starts in `command` mode by default. Embedders may pass `initialMode="passt - All keyboard input routes to the active session's xterm.js instance - Only the mode-exit gesture (LCmd → RCmd) is intercepted - In the VS Code host, selected workbench chords are mirrored: xterm still processes the key, and Dormouse also asks the extension host to run the matching VS Code command. See [the VS Code host spec](vscode.md) for the allowlist. -- On Windows, `Shift+Enter` is normalized before xterm's default Enter handling. If the foreground app has enabled bracketed paste, Dormouse sends a bracketed-paste LF (`\x1b[200~\n\x1b[201~`); otherwise it sends bare LF (`\x0a`). This preserves the common multiline-input contract used by terminal TUIs such as Codex, where plain `Enter` submits and pasted/LF newline input inserts a newline. +- On Windows, `Shift+Enter` is normalized before xterm's default Enter handling and sends a bracketed-paste LF (`\x1b[200~\n\x1b[201~`). This mirrors the VS Code `workbench.action.terminal.sendSequence` workaround used for terminal TUIs such as Codex, where plain `Enter` submits and pasted newline input inserts a newline. - Selection overlay shows 2px solid border with glow - Terminal has DOM focus diff --git a/lib/src/lib/terminal-keyboard.test.ts b/lib/src/lib/terminal-keyboard.test.ts index 3a0f7003..02084a47 100644 --- a/lib/src/lib/terminal-keyboard.test.ts +++ b/lib/src/lib/terminal-keyboard.test.ts @@ -28,21 +28,16 @@ describe('terminal keyboard normalization', () => { expect(shouldHandleWindowsShiftEnter(keydown(), { isWindows: true })).toBe(true); }); - it('uses bracketed paste for Shift+Enter when the foreground app enabled bracketed paste', () => { - expect(shiftEnterInputForEvent(keydown(), { isWindows: true, bracketedPasteMode: true })) + it('uses bracketed paste for Shift+Enter on Windows', () => { + expect(shiftEnterInputForEvent(keydown(), { isWindows: true })) .toBe(BRACKETED_PASTE_NEWLINE_INPUT); }); - it('falls back to LF when bracketed paste is not enabled', () => { - expect(shiftEnterInputForEvent(keydown(), { isWindows: true, bracketedPasteMode: false })) - .toBe(SHIFT_ENTER_NEWLINE_INPUT); - }); - it('does not match normal Enter, composing input, or non-Windows platforms', () => { expect(shouldHandleWindowsShiftEnter(keydown({ shiftKey: false }), { isWindows: true })).toBe(false); expect(shouldHandleWindowsShiftEnter(keydown({ isComposing: true }), { isWindows: true })).toBe(false); expect(shouldHandleWindowsShiftEnter(keydown(), { isWindows: false })).toBe(false); - expect(shiftEnterInputForEvent(keydown(), { isWindows: false, bracketedPasteMode: true })).toBe(null); + expect(shiftEnterInputForEvent(keydown(), { isWindows: false })).toBe(null); }); it('leaves modified Enter chords alone', () => { diff --git a/lib/src/lib/terminal-keyboard.ts b/lib/src/lib/terminal-keyboard.ts index 95deb0ed..32ddc5c9 100644 --- a/lib/src/lib/terminal-keyboard.ts +++ b/lib/src/lib/terminal-keyboard.ts @@ -10,12 +10,12 @@ export function shouldHandleWindowsShiftEnter( event: KeyboardEventLike, options: { isWindows: boolean }, ): boolean { - return shiftEnterInputForEvent(event, { ...options, bracketedPasteMode: false }) !== null; + return shiftEnterInputForEvent(event, options) !== null; } export function shiftEnterInputForEvent( event: KeyboardEventLike, - options: { isWindows: boolean; bracketedPasteMode: boolean }, + options: { isWindows: boolean }, ): string | null { if (!options.isWindows) return null; if (event.type !== 'keydown') return null; @@ -23,5 +23,5 @@ export function shiftEnterInputForEvent( if (event.key !== 'Enter') return null; if (!event.shiftKey) return null; if (event.ctrlKey || event.altKey || event.metaKey) return null; - return options.bracketedPasteMode ? BRACKETED_PASTE_NEWLINE_INPUT : SHIFT_ENTER_NEWLINE_INPUT; + return BRACKETED_PASTE_NEWLINE_INPUT; } diff --git a/lib/src/lib/terminal-lifecycle.ts b/lib/src/lib/terminal-lifecycle.ts index 9865bb91..8f8d5cdd 100644 --- a/lib/src/lib/terminal-lifecycle.ts +++ b/lib/src/lib/terminal-lifecycle.ts @@ -127,7 +127,6 @@ function createXtermHost(id: string): { terminal: Terminal; fit: FitAddon; eleme terminal.attachCustomKeyEventHandler((event) => { const shiftEnterInput = shiftEnterInputForEvent(event, { isWindows: IS_WINDOWS, - bracketedPasteMode: terminal.modes.bracketedPasteMode, }); if (shiftEnterInput !== null) { event.preventDefault(); diff --git a/lib/src/lib/terminal-registry.alert.test.ts b/lib/src/lib/terminal-registry.alert.test.ts index e74c26a1..7cd58ad5 100644 --- a/lib/src/lib/terminal-registry.alert.test.ts +++ b/lib/src/lib/terminal-registry.alert.test.ts @@ -304,20 +304,9 @@ describe('terminal-registry alert behavior', () => { expect(isUntouched(id)).toBe(false); }); - it('sends LF for Windows Shift+Enter before xterm handles Enter normally', () => { - const id = 'shift-enter-lf'; - const entry = createSession(id); - - const handled = entry.terminal.emitKeyDown(); - - expect(handled).toBe(false); - expect(entry.terminal.writes).toContain('\n'); - }); - - it('sends bracketed-paste newline for Windows Shift+Enter when the app enabled bracketed paste', () => { + it('sends bracketed-paste newline for Windows Shift+Enter before xterm handles Enter normally', () => { const id = 'shift-enter-bracketed'; const entry = createSession(id); - entry.terminal.modes.bracketedPasteMode = true; const handled = entry.terminal.emitKeyDown(); From 187d4c07b2808c5594452f7038210a31b3e89686 Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Thu, 28 May 2026 20:34:42 -0700 Subject: [PATCH 3/3] Route Windows Shift Enter through xterm input --- docs/specs/layout.md | 2 +- lib/src/lib/terminal-keyboard.test.ts | 5 ++--- lib/src/lib/terminal-keyboard.ts | 3 +-- lib/src/lib/terminal-lifecycle.ts | 6 +++--- lib/src/lib/terminal-registry.alert.test.ts | 11 ++++++++--- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/specs/layout.md b/docs/specs/layout.md index ddf96a17..ddbe75ee 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -141,7 +141,7 @@ Wall starts in `command` mode by default. Embedders may pass `initialMode="passt - All keyboard input routes to the active session's xterm.js instance - Only the mode-exit gesture (LCmd → RCmd) is intercepted - In the VS Code host, selected workbench chords are mirrored: xterm still processes the key, and Dormouse also asks the extension host to run the matching VS Code command. See [the VS Code host spec](vscode.md) for the allowlist. -- On Windows, `Shift+Enter` is normalized before xterm's default Enter handling and sends a bracketed-paste LF (`\x1b[200~\n\x1b[201~`). This mirrors the VS Code `workbench.action.terminal.sendSequence` workaround used for terminal TUIs such as Codex, where plain `Enter` submits and pasted newline input inserts a newline. +- On Windows, `Shift+Enter` is normalized before xterm's default Enter handling by injecting LF (`\x0a`) through xterm's user-input path. This mirrors the `Ctrl+J`/terminal `sendInput` workaround used for terminal TUIs such as Codex, where plain `Enter` submits and LF inserts a newline. - Selection overlay shows 2px solid border with glow - Terminal has DOM focus diff --git a/lib/src/lib/terminal-keyboard.test.ts b/lib/src/lib/terminal-keyboard.test.ts index 02084a47..ef028a9a 100644 --- a/lib/src/lib/terminal-keyboard.test.ts +++ b/lib/src/lib/terminal-keyboard.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; import { - BRACKETED_PASTE_NEWLINE_INPUT, SHIFT_ENTER_NEWLINE_INPUT, shiftEnterInputForEvent, shouldHandleWindowsShiftEnter, @@ -28,9 +27,9 @@ describe('terminal keyboard normalization', () => { expect(shouldHandleWindowsShiftEnter(keydown(), { isWindows: true })).toBe(true); }); - it('uses bracketed paste for Shift+Enter on Windows', () => { + it('uses LF for Shift+Enter on Windows', () => { expect(shiftEnterInputForEvent(keydown(), { isWindows: true })) - .toBe(BRACKETED_PASTE_NEWLINE_INPUT); + .toBe(SHIFT_ENTER_NEWLINE_INPUT); }); it('does not match normal Enter, composing input, or non-Windows platforms', () => { diff --git a/lib/src/lib/terminal-keyboard.ts b/lib/src/lib/terminal-keyboard.ts index 32ddc5c9..ee4d7d27 100644 --- a/lib/src/lib/terminal-keyboard.ts +++ b/lib/src/lib/terminal-keyboard.ts @@ -4,7 +4,6 @@ type KeyboardEventLike = Pick< >; export const SHIFT_ENTER_NEWLINE_INPUT = '\n'; -export const BRACKETED_PASTE_NEWLINE_INPUT = '\x1b[200~\n\x1b[201~'; export function shouldHandleWindowsShiftEnter( event: KeyboardEventLike, @@ -23,5 +22,5 @@ export function shiftEnterInputForEvent( if (event.key !== 'Enter') return null; if (!event.shiftKey) return null; if (event.ctrlKey || event.altKey || event.metaKey) return null; - return BRACKETED_PASTE_NEWLINE_INPUT; + return SHIFT_ENTER_NEWLINE_INPUT; } diff --git a/lib/src/lib/terminal-lifecycle.ts b/lib/src/lib/terminal-lifecycle.ts index 8f8d5cdd..c4933bff 100644 --- a/lib/src/lib/terminal-lifecycle.ts +++ b/lib/src/lib/terminal-lifecycle.ts @@ -94,7 +94,7 @@ function readDisplayTextFromBuffer(terminal: Terminal, range: IBufferRange): str } } -function createXtermHost(id: string): { terminal: Terminal; fit: FitAddon; element: HTMLDivElement } { +function createXtermHost(): { terminal: Terminal; fit: FitAddon; element: HTMLDivElement } { const styles = getComputedStyle(document.body); const editorFontSize = parseInt(styles.getPropertyValue('--vscode-editor-font-size'), 10) || 12; const editorFontFamily = styles.getPropertyValue('--vscode-editor-font-family').trim() || "'SF Mono', Menlo, Monaco, monospace"; @@ -131,7 +131,7 @@ function createXtermHost(id: string): { terminal: Terminal; fit: FitAddon; eleme if (shiftEnterInput !== null) { event.preventDefault(); event.stopPropagation(); - handleTerminalInput(id, terminal, shiftEnterInput); + terminal.input(shiftEnterInput, true); return false; } const runWorkbenchCommand = getPlatform().runWorkbenchCommand; @@ -249,7 +249,7 @@ function wireXtermHandlers( } function setupTerminalEntry(id: string, options: { untouched?: boolean } = {}): TerminalEntry { - const { terminal, fit, element } = createXtermHost(id); + const { terminal, fit, element } = createXtermHost(); const selectionBaselineRef = { current: null as string | null }; const disposePty = wirePtyEvents(id, terminal); diff --git a/lib/src/lib/terminal-registry.alert.test.ts b/lib/src/lib/terminal-registry.alert.test.ts index 7cd58ad5..cf0c1a64 100644 --- a/lib/src/lib/terminal-registry.alert.test.ts +++ b/lib/src/lib/terminal-registry.alert.test.ts @@ -63,6 +63,10 @@ vi.mock('@xterm/xterm', () => { return { dispose: () => {} }; } + input(data: string): void { + this.emitInput(data); + } + attachCustomKeyEventHandler(handler: (event: KeyboardEvent) => boolean): void { this.keyHandler = handler; } @@ -149,6 +153,7 @@ interface MockTerminalInstance { emitInput(data: string): void; emitKeyDown(init?: Partial): boolean | null; emitResize(cols: number, rows: number): void; + input(data: string): void; } class MockElement { @@ -304,14 +309,14 @@ describe('terminal-registry alert behavior', () => { expect(isUntouched(id)).toBe(false); }); - it('sends bracketed-paste newline for Windows Shift+Enter before xterm handles Enter normally', () => { - const id = 'shift-enter-bracketed'; + it('sends LF through xterm input for Windows Shift+Enter before xterm handles Enter normally', () => { + const id = 'shift-enter-lf'; const entry = createSession(id); const handled = entry.terminal.emitKeyDown(); expect(handled).toBe(false); - expect(entry.terminal.writes).toContain('\x1b[200~\n\x1b[201~'); + expect(entry.terminal.writes).toContain('\n'); }); it('does not mark synthetic terminal reports as touched', () => {