fix: use bracketed paste mode for chat terminal tool to avoid macOS PTY corruption#301133
fix: use bracketed paste mode for chat terminal tool to avoid macOS PTY corruption#301133jcansdale wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new node-based terminal integration test suite intended to exercise TerminalProcess.input() with multiline payloads across a small shell/size matrix, aiming to catch regressions related to PTY multiline writes (notably on macOS).
Changes:
- Introduces a new
TerminalProcess - multiline writetest suite that spawns real PTY shells (skipped on Windows). - Builds and sends a multiline
printfcommand to write deterministic output to a temp file, then asserts the written file matches expected lines. - Runs the same scenario across multiple shells (
bash,zsh,sh, etc.) and payload sizes (10,20lines).
| await new Promise<void>(resolve => { | ||
| const timer = setTimeout(() => { | ||
| listener.dispose(); | ||
| resolve(); | ||
| }, 10000); | ||
| const listener = terminalProcess.onProcessData(() => { | ||
| clearTimeout(timer); | ||
| listener.dispose(); | ||
| resolve(); | ||
| }); |
There was a problem hiding this comment.
This promise references listener inside the setTimeout callback before listener is declared. TypeScript will error with "Block-scoped variable 'listener' used before its declaration". Declare let listener first (or create the listener before the timer) so both callbacks can dispose it safely.
| this.skip(); | ||
| } | ||
|
|
||
| this.timeout(10000); |
There was a problem hiding this comment.
this.timeout(10000) is likely too low given the test can wait up to 10s for initial output plus up to 4s for the output file (and then still needs to shut down/await exit). This can cause frequent Mocha timeouts on slower CI. Increase the test timeout (or reduce the internal waits) so the overall upper bound fits within this.timeout(...).
| this.timeout(10000); | |
| this.timeout(20000); |
| { ...process.env } as Record<string, string>, | ||
| { ...process.env } as Record<string, string>, |
There was a problem hiding this comment.
The environment is being forced to Record<string, string> via a cast, but process.env (and IProcessEnvironment) allows string | undefined values. This cast hides missing env vars and removes type safety in the test. Prefer passing process.env directly (it matches IProcessEnvironment) or explicitly filtering/normalizing undefined values if a strict Record<string, string> is required.
| { ...process.env } as Record<string, string>, | |
| { ...process.env } as Record<string, string>, | |
| process.env, | |
| process.env, |
| teardown(() => { | ||
| fs.rmSync(outputDir, { recursive: true, force: true }); | ||
| }); |
There was a problem hiding this comment.
ensureNoDisposablesAreLeakedInTestSuite() registers its own teardown that disposes the DisposableStore. In Mocha, teardown hooks run in reverse registration order, so this fs.rmSync(outputDir, ...) teardown will likely run before the TerminalProcess in the store is disposed (especially when a test fails early), which can cause flaky cleanup. Consider registering the directory cleanup with the disposable store (eg via store.add(toDisposable(...))) or otherwise ensure the terminal process is disposed before deleting outputDir.
When the chat terminal tool forwards multiline commands to the live terminal, use bracketed paste mode (when the shell supports it) to avoid macOS PTY canonical-mode buffer corruption with input exceeding ~1024 bytes. Shells that support bracketed paste (zsh, bash 5.1+) will receive the input wrapped in ESC[200~ / ESC[201~ sequences, treating it as a single paste operation. For shells that don't support it (bash 3.2, ksh), this is a no-op since sendText only wraps when bracketedPasteMode is enabled. Refs microsoft#296955
Add tests proving that wrapping multiline input in bracketed paste sequences (ESC[200~ / ESC[201~) prevents macOS PTY canonical-mode buffer corruption for shells that support it (zsh). The trailing carriage return must come after the paste end marker, as content inside the markers is treated as literal buffer input.
Run multiple iterations per test in parallel to increase I/O contention, and write input in small chunks (4 bytes) with event loop yields to better simulate xterm.js write patterns. Note: zsh still passes at the PTY level even with these changes. The real-world zsh failure requires the full xterm.js/terminal UI rendering loop (as shown by vscode-extension test-electron tests). The PTY-level tests reliably reproduce the bug for bash 3.2/sh/ksh.
46dbe34 to
26267fb
Compare
| // Use bracketed paste mode when available to avoid macOS PTY canonical-mode | ||
| // buffer corruption with multiline input exceeding ~1024 bytes. | ||
| this._register(mirror.onDidInput(data => { | ||
| if (!liveTerminalInstance.isDisposed) { |
There was a problem hiding this comment.
shouldn't we restrict this to macOS?
meganrogge
left a comment
There was a problem hiding this comment.
It seems to work based on my testing. One change and incorporating Copilot's comments would be good.
|
This seems to work for me as well! |
|
Closing this in favor of #302526. |
|
@connor4312 fyi 👉🏻 #302526 |
Summary
Use bracketed paste mode when the chat terminal tool forwards multiline commands to the live terminal. This mitigates the macOS PTY canonical-mode buffer corruption that occurs with multiline input exceeding ~1024 bytes.
The Problem
On macOS, the kernel's PTY line discipline has a
MAX_INPUTbuffer of ~1024 bytes in canonical mode. When VS Code's chat terminal tool sends large multiline commands (e.g., longechostatements, inline scripts), the data corrupts at the 1024-byte boundary — characters repeat, lines get mangled, or the shell hangs.This affects all shells on macOS, though the severity varies. See #296955 for details.
The Fix
One-line change in
chatTerminalToolProgressPart.ts:The third parameter (
bracketedPasteMode: true) tellssendTextto wrap the input in\x1b[200~...\x1b[201~escape sequThe third parameter (bracketedPasteraThe thirdsteThe third parameter (bracketedPasteMode: true) tellssendTextto wrap the input in\x1b[200~...\x1b[201~escape sequThe third parameter (bracketedPasteraThe thirdsteThe third parameter (bracketedPasteMode: truee vThe third parmodes.bracketedPasteMode`sendText()already supports wrapping input in bracketed paste sequencesCoverage
/bin/bash3.2): fails — confirms the bugRefs #296955, #298993, #300762