Add CommandLineMultilineWrapRewriter to handle multi-line POSIX commands#312263
Add CommandLineMultilineWrapRewriter to handle multi-line POSIX commands#312263meganrogge wants to merge 1 commit intomainfrom
Conversation
RichExecuteStrategy resolves on the first onCommandFinished shell integration marker. When the agent pastes a multi-line POSIX command (e.g. a python/node heredoc or a set -e; apt-get update script), shell integration emits one marker per top-level statement and the strategy returns partial or empty output after the first line while the rest of the command keeps running unattended. Wrap multi-line POSIX commands in <shell> -c '...' so shell integration sees a single command end marker. Skipped for PowerShell/Windows and for commands whose newlines are line-continuation (preceded by \). Input detection via OutputMonitor is unaffected because it reads PTY output independently of shell integration markers, so interactive prompts still surface for both foreground and background terminals.
There was a problem hiding this comment.
Pull request overview
This PR fixes run_in_terminal returning early for multi-line POSIX submissions by introducing a command-line rewriter that wraps multi-line commands in <shell> -c ..., ensuring shell integration emits a single onCommandFinished marker for the whole tool call.
Changes:
- Added
CommandLineMultilineWrapRewriterto wrap multi-line POSIX commands (with shell-specific quoting for fish vs bash/zsh/sh). - Registered the new rewriter early in
RunInTerminalTool’s rewriter chain to run before background detaching. - Added unit tests covering the new rewriting behavior and escaping rules.
Show a summary per file
| File | Description |
|---|---|
| src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLineMultilineWrapRewriter.ts | Implements multi-line detection and wrapping via <shell> -c ... with shell-specific escaping. |
| src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts | Registers the new multiline rewriter in the command-line rewriter pipeline. |
| src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineMultilineWrapRewriter.test.ts | Adds tests to validate wrapping/skip logic and escaping behavior across shells/OSes. |
Copilot's findings
- Files reviewed: 3/3 changed files
- Comments generated: 2
| // Detect a "real" newline that separates top-level statements. We require a bare LF | ||
| // that is NOT line-continuation (preceded by `\`). A single-line command with escaped | ||
| // newlines continues to be a single command; we must not wrap it. | ||
| if (!/(^|[^\\])\n\s*\S/.test(command)) { | ||
| return undefined; | ||
| } |
There was a problem hiding this comment.
The multi-line detection regex treats any newline preceded by a backslash as a line continuation. This is incorrect when the newline is preceded by an even number of backslashes (e.g. \\\n): in POSIX shells that newline is not continued, so the command is truly multi-line and should be wrapped. Consider switching to logic that checks for an odd number of trailing backslashes before the newline (and ideally supports \r?\n).
| if (isFish(options.shell, options.os)) { | ||
| // Fish does not support the POSIX `'\''` escape inside single-quoted strings. | ||
| // Use double quotes and escape backslash and double-quote. | ||
| const escaped = command.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); | ||
| return { | ||
| rewritten: `${options.shell} -c "${escaped}"`, | ||
| reasoning: 'Wrapped multi-line command with `fish -c` so shell integration sees a single command', | ||
| forDisplay: command, | ||
| }; | ||
| } | ||
|
|
||
| // bash/zsh: escape single quotes using the standard `'\''` sequence so the entire | ||
| // command can live inside a single-quoted `-c` argument without further interpretation. | ||
| const escaped = command.replace(/'/g, `'\\''`); | ||
| return { | ||
| rewritten: `${options.shell} -c '${escaped}'`, |
There was a problem hiding this comment.
This file duplicates the same fish vs bash/zsh -c quoting/escaping logic that already exists in CommandLineBackgroundDetachRewriter. To avoid the two implementations drifting (and to make it easier to add support for additional shells/escaping tweaks), consider extracting a small shared helper for building a safe <shell> -c <script> invocation.
|
this is a bad approach |
Fixes #312260.
Problem
RichExecuteStrategy.execute()resolves on the firstonCommandFinishedshell-integration marker:When the chat agent pastes a multi-line POSIX command, shell integration fires one marker per top-level statement. The tool returns partial/empty output after line 1 while the rest of the command keeps running unattended.
Fix
New
CommandLineMultilineWrapRewriterthat wraps multi-line POSIX commands in<shell> -c '...'before execution, so shell integration sees exactly one command end marker. Skips Windows/PowerShell and line-continuations (\<LF>). Not gated ondetachBackgroundProcesses— the race affects foreground commands too.OutputMonitorinput detection is unaffected (it reads the PTY directly).Proof — eval run
24857589683, terminalbench2, vscode agent, gpt-5.4Affected failing instances whose first tool call was a multi-line heredoc that returned early (
patch=0Bafter wasted follow-up turns):systemInitiatedLabel(first line of submission)bn-fit-modifycd /app && python3 - <<'PY'distribution-searchcd /app && python - <<'PY'gcode-to-textcd /app && python - <<'PY'protein-assemblypython - <<'PY'raman-fittingcd /app && python - <<'PY'rstan-to-pystancd /app && .venv/bin/python - <<'PY'regex-chesspython - <<'PY'schemelike-metacircular-evalpushd /app >/dev/null && python3 interp.py eval.scm <<'EOF'All 8 produced 0-byte patches after the first
run_in_terminalcall returned before the heredoc body finished. Logs under.vscode-eval/24857589683/<test>/output/vsc-output/chat-turns/chat-export-step-0.json.Tests
commandLineMultilineWrapRewriter.test.ts — 8/8 green: