Skip to content

Commit 14f0aa3

Browse files
yotsudaclaude
andcommitted
feat(streams): hybrid capture closes [Console]::* gap and merges Output+Error chronologically
The polling engine's Invoke-CommandWithAllStreams previously ran via \`Invoke-Expression $cmd -OutVariable -ErrorVariable -WarningVariable -InformationVariable\` and assembled the response from the four buckets independently. Two structural problems with that: 1. \`[Console]::WriteLine\` and \`[Console]::Error.WriteLine\` direct writes (and any .NET library that writes through System.Console without going through PowerShell's stream system) bypassed every capture variable. Those bytes still hit the visible terminal but were invisible to the AI side. Confirmed by side-by-side test against ripple, where \`[Console]::Error.WriteLine\` survives the tool response. 2. Output and Error landed in separate buckets with no chronology between them. The AI saw "here are 5 lines of output, here are 3 errors" but could not tell at WHICH point in the run an error happened — a problem the user flagged when reviewing the side-by-side comparison. Fix: hybrid capture wired inside Invoke-CommandWithAllStreams. * \`Invoke-Captured -Block $sb -WarningVariable -InformationVariable 2>&1 | Tee-Object -Variable pipelineStream | Out-Host\` Streams 1 (Output) and 2 (Error) merge into a single chronological PSObject sequence; Tee-Object captures while Out-Host renders to the visible console in real time with the host's standard coloring (red+cyan for ErrorRecord, plain for String). Captured items keep their PowerShell types so the formatter can render Errors as Exception.Message and outputs through Out-String. * Streams 3 (Warning) and 6 (Information / Write-Host) stay UN-merged. Merging them with \`*>&1\` would force their rendering through Out-Host's generic record renderer and lose Write-Host's chosen ForegroundColor on the visible console (tested and confirmed during the design phase). Capturing via -WarningVariable / -InformationVariable doesn't disturb the independent yellow / Write-Host-color rendering the user sees. * \`[Console]::SetOut\` / \`SetError\` swapped to PowerShell.MCP.Services.TeeTextWriter (new file) for the duration of the command. The tee writes to BOTH the original writer (preserves real-time visible output, no buffering delay) AND a StringBuilder. .NET-level direct writes that bypass the PowerShell stream system land in those StringBuilders and surface as new \`=== CONSOLE.OUT (direct) ===\` / \`=== CONSOLE.ERR (direct) ===\` sections in the AI response. On the happy path both buffers stay empty and the sections are omitted. * Console.Out / Console.Error are restored in finally so a thrown exception cannot leak the swap into subsequent commands. The downstream Format-McpOutput rewrite walks PipelineItems in emit order: outputs render via \`$item | Out-String\`, ErrorRecord renders as \`$item.Exception.Message\` (matches what's visible on the console minus the \`Write-Error: \` prefix and trace context PowerShell decorates the inline render with). Result is a single chronological text block that leads the response — sections for Warnings, Info, direct Console writes, and Exceptions follow only when non-empty. Removed the post-execute \`$streamResults.Success | Out-Default\` and the manual \`Write-Host $err.Exception.Message -ForegroundColor Red\` loops from the main event handler. They duplicated the visible-console output line-for-line under the new wiring (Out-Host inside the Tee pipe already streamed everything in real time). Terminating-exception messages from the inner catch still get a manual Write-Host pass because they bypass the streaming pipeline. Smoke-tested against a real PowerShell.MCP.dll: confirmed PipelineItems contains output and ErrorRecord items in emit order; Warning, Information, ConsoleOut, ConsoleErr buckets each captured their respective signals; visible console rendering preserved real-time streaming and per-stream coloring (Write-Host Red, Write-Warning Yellow, Write-Error red+cyan, native cmd /c stderr red). 251 xunit unit tests still pass with no expected-value adjustments. Pester integration tests target the cmdlets, not the polling engine, so they're unaffected. A test for the new behavior (assert PipelineItems chronology + Console.Out/Err capture) is deferred to a follow-up commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c79b55d commit 14f0aa3

2 files changed

Lines changed: 339 additions & 196 deletions

File tree

0 commit comments

Comments
 (0)