[BUG] VSCode: background agent output streams into foreground chat, disrupting active conversation

Status Open
Maintainer reply None cached
Activity 9 comments · opened Jun 2, 2026

Preflight

  • [x] I have searched existing issues and this specific angle isn't reported
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

Summary

In the VSCode extension, when Claude spawns subagents with run_in_background: true (or when fork mode is active and all agents become background-by-default), the agent's complete output — all tool calls, all intermediate text responses, all reasoning — streams directly into the main foreground chat window. This defeats the stated purpose of background execution.

Additionally, when multiple background agents run simultaneously, their outputs intermingle with each other AND with the actual conversation in the chat window, making it very difficult to follow any of them or the foreground conversation.

Environment

  • Claude Code VSCode Extension: v2.1.159
  • Platform: macOS (Darwin 24.6.0)
  • Shell: bash
  • Models affected: claude-opus-4-6 (1M context), claude-opus-4-8 (1M context)

Expected behavior

Background agents should run concurrently while the user can continue working in the foreground chat. Per the documentation:

"Background subagents run concurrently while you continue working." "The fork appears in a panel below your prompt and runs in the background while you keep working."

In the terminal/TUI, this is implemented as a separate panel below the prompt input. The user can keep reading and responding in the main chat while the agent works separately.

Actual behavior

In the VSCode extension, there is no equivalent of the terminal's "panel below the prompt." Instead:

  1. ALL background agent output — tool call descriptions, intermediate text, every response turn — streams directly into the main foreground chat window, inline with the user's conversation.
  2. Multiple simultaneous background agents intermingle their output with each other and with the actual conversation, creating an unreadable jumble.
  3. The user cannot read previous messages while agents are running (the chat is being actively modified/scrolled by background agent output).
  4. The background/foreground distinction is meaningless from the user's perspective — a background agent is experientially identical to a foreground agent in VSCode.

Distinction from #9512 and #11950

Those issues are about subagent verbosity (showing tool call details vs. summaries). Both were closed as "not planned." This issue is different: the background vs. foreground separation is entirely absent in VSCode. Even a perfectly-summarized background agent that only produced a one-line result would still write that result into the main chat mid-conversation, interrupting whatever the user was reading.

The verbosity is a secondary concern. The primary concern is that background agents have no separate display space in VSCode and therefore cannot be background in any meaningful sense.

Related issues

  • #54595 — Fork mode silently removed run_in_background from tool schema, making all agents background-by-default. Under fork mode, this display issue affects every single agent spawn.
  • #9512 — Subagent output control settings (closed as not planned) — general verbosity, different problem
  • #11950 — VSCode: hide subagent internals (closed as not planned) — similar complaint but framed as verbosity
  • #60095 — Background task chips persist as Running — related background agent UI issues

Suggested fix directions

  1. Display background agent output in a collapsible section in the VSCode chat (collapsed by default, expandable for inspection)
  2. Show only a chip/status indicator while the agent runs; reveal output on click/expand
  3. Route background agent output to a dedicated VSCode panel (e.g., "Claude Agents" sidebar view) rather than the main chat
  4. At minimum, visually separate background agent output from the foreground conversation so intermingled output from multiple agents + conversation is distinguishable
  5. Revert to original behavior of simply logging all background agent output to a log file instead of the updated behavior of all agent output going to the log file AND directly into the chat window. While 1-4 are better options (really would be nice to have agent output captured & easily accessible), simply reverting to the prior behavior would result in background agents actually allowing the user to continue working instead of completely derailing work until all background agents complete.

View original on GitHub ↗

9 Comments

thatChadM · 2 months ago

Additional information after testing with version 2.1.160:

  • running an agent __with__ "run_in_background": true resulted in ALL output (tools calls AND agent response text) being dumped into the chat window
  • running a foreground agent (__without__ run_in_background set), resulted in tools calls (tool description, input, and output, at least for bash) from the agent being dumped into the chat window BUT the agent's actual response text was NOT dumped into the chat window

This is the crux of the recent behavior change. Tool calls for background agents in the past seemed to occasionally log a tool call description in the chat window but the actual agent OUTPUT did NOT just get dumped directly into the chat window.

Confirmed that a foreground agent is still a foreground agent and is blocking Claude (and the user) from continuing until the agent returns. Confirmed that background agents are actually in the background, allowing Claude to continue working while the agent works. Unfortunately, while Claude can continue while the background agent works, the same can NOT be said for the user. Output from the primary Claude instance and any background agent(s) intermingle in the chat window, making it difficult or impossible for the user to follow

BeeFlea · 2 months ago

+1
It becomes impossible to tell which feedback comes form the orchestrator and which from the sub-agent. Aslo impossible to hold a conversation because content keeps drifting off screen.

thatChadM · 2 months ago

2.1.163, same behavior, but testing confirmed that the flooding from the background agent occurs only after the first tool call. Any agent response prior to the first tool call is NOT sent to the main chat window. The first tool call seemed to trigger a very brief flash of that response text that immediately disappeared and was replaced with the tool call stub. From that point on all agent response text and tool calls get dumped into the main chat window.

iteplov · 2 months ago

fwiw, my Claude suggested to use Workflow until Agent is fixed. probably an overkill but at least no noisy output is leaking from background tasks.

vskrsl · 2 months ago

Downgrading Claude Code for VS Code to v2.1.145 helped me.

junpicogram · 2 months ago

Reproduces in the VS Code extension with a slightly different code path that may be worth noting:

Use case: a custom slash command that fans out 3 foreground subagents in parallel via the Agent tool (no run_in_background, and the subagents themselves call zero tools — they just return text opinions for a judge-panel pattern).

Observed: each subagent's response text shows up in the main foreground chat, drowning out the parent conversation. Even an opt-in design choice like collapsed-by-default cards (similar to thinking blocks) would already make iterative/judge-panel skills usable again in the extension.

+1 to routing background and large subagent outputs to a dedicated panel, or at minimum a collapsed/click-to-expand chip as suggested in #11950.

Environment: macOS, Claude Code VS Code extension (latest).

SixFive7 · 1 month ago
## ⚠️ Retracted — this analysis was wrong. Please do not act on it. Correction posted 2026-07-29. I subsequently traced the shipped bundle and the mechanism I described here is not what happens. Specifically wrong: - The completion notification is not the cause. During a live session it never renders at all — the handler consumes it and inserts nothing renderable. The flooding is the subagent's own assistant frames. - The fix I proposed — flagging that notification isSynthetic — would change nothing, because the notification isn't what reaches the screen. - The collapsed-notification behaviour I described as a side effect is an artifact of comparing a live session against a reloaded one, not a real effect. Why I got it wrong: live views and reloaded views render different things for the same event. Every comparison I made moved two variables at once — and produced conclusions that looked solid and weren't. The only part of the original that survives is the identification of the parent_tool_use_id ternary in the wire→UI converter. The story built around it was wrong. The corrected analysis — with a two-line fix, a full measurement matrix, and a regression window — is in my follow-up comment below. Original text is preserved in this comment's edit history rather than reproduced here, so the incorrect mechanism doesn't sit in the thread for triage to read.
SixFive7 · 1 month ago

Root cause, and a two-line fix

Replacing my earlier comment, which had the wrong mechanism (now retracted above). I traced the shipped bundle on v2.1.220 and the defect turns out to be very small — two lines, in a file that already contains the correct pattern a few lines above.

The defect

Every frame the CLI sends to the webview carries parent_tool_use_id: null for main-loop frames, set for subagent frames. Confirmed on the wire.

The wire→UI converter keeps that tag for user frames and discards it for assistant frames:

let t = e.type === "user" ? e.parent_tool_use_id : null,
    i = e.type === "user" ? e.isSynthetic       : void 0;

The render dispatcher then guards on the tag — but only on the user branch:

if (t.isEmpty) return null;
if (t.type === "user")      { if (t.parentToolUseId) return null;   // subagent user frames suppressed
                              if (t.isSynthetic)     return null;
                              return <UserMessage …> }
if (t.type === "assistant") { /* no parentToolUseId guard */
                              return <AssistantMessage …> }

Subagent user frames (tool results) are already suppressed correctly. Subagent assistant frames — thinking, tool calls, response text — leak into the main chat, because the tag that would catch them was deleted one function earlier.

That asymmetry is the whole bug. The intended behaviour already exists; it simply can't fire.

Why a guard-only fix appears to work, then fails

Streamed frames take a different path — the stream assembler, which does propagate parent_tool_use_id. So adding only the missing dispatcher guard suppresses subagent output while it streams; then the completed message is re-converted through the broken converter, the tag is stripped, and it appears. I measured this: guard-only leaves the flood exactly as it was.

This also explains @thatChadM's observation in this thread — "a very brief flash of that response text that immediately disappeared and was replaced". That flash is the correctly-tagged streaming frame being replaced by the untagged complete one. Same for "flooding occurs only after the first tool call": the first tool call is what forces the streamed message to complete and be re-converted.

The fix

  1. Propagate the tag for assistant frames: e.parent_tool_use_id instead of e.type === "user" ? e.parent_tool_use_id : null.
  2. Hoist the existing guard to cover all message types: if (t.isEmpty || t.parentToolUseId) return null;

No new concepts, no content inspection, no UI work — it makes the assistant branch behave the way the user branch already does. The same ternary also drops isSynthetic for assistant frames, which is likely worth repairing in the same edit.

Verified end to end

Applied as a local patch to v2.1.220 and measured every combination, identical subagent workload each time:

| Patch state | Live: subagent tool calls + text | After reload: notification block |
|---|---|---|
| unpatched | rendered (the flood) | present |
| guard only | rendered | present |
| both lines | gone | present |

No regressions observed. The Agent: <name> header still renders — it's a main-loop frame and is untouched — and the collapsed <task-notification> block still appears after a reload. Only the subagent frames go.

⚠️ Testing note: live and reloaded views render different things for the same event. Live shows the subagent's frames; reload shows the notification block instead. Hold that constant when testing, or you will attribute a rendering difference to your change. I reached three wrong conclusions before I did.

Regression window

@vskrsl reports v2.1.145 is clean, and the original report here is v2.1.159. That's a tight bisect range, and probably the most actionable thing in this thread.

Impact

Every subagent spawn in the VS Code extension is affected. A parallel fan-out makes the panel unusable: output from several agents interleaves with the conversation, and there is no visual distinction between an intermediate agent report and the assistant's final answer. Patterns built on subagents — judge panels, map-reduce, review fan-outs — are effectively unusable in the extension while this stands, which is why several people here have fallen back to the terminal or to Workflow.

Two lines against that seems an unusually lopsided cost/benefit.

Workarounds meanwhile

  1. CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 — removes run_in_background from the Agent tool schema entirely; results return as collapsed tool results.
  2. run_in_background: false per spawn. Note synchronous is not serial — multiple Agent calls in one assistant message run concurrently, so parallelism is preserved.
  3. Workflow — N agents produce one notification rather than N (thanks @iteplov).
  4. The terminal is unaffected — its panel-below-the-prompt behaves as documented.

One thing that doesn't fit

@junpicogram reports foreground subagents' response text reaching the main chat. In my testing on 2.1.220, every synchronous agent was silent — including one returning 22 KB. That may be a version difference, or something specific to three foreground agents fanned out at once. Worth re-testing on current: if it still reproduces, there is a second path I haven't found.

SixFive7 · 18 days ago

Still reproduces in 2.1.227 and 2.1.228.

Since my earlier comment the webview has grown most of the machinery this fix
needs — it just isn't wired into the render path. Identifiers below are minified
and build-specific; the shapes are what to search for.

What changed since ~2.1.220

The wire→UI converter used to derive one tag, and dropped it for assistant frames:

let t = e.type === "user" ? e.parent_tool_use_id : null,
    i = e.type === "user" ? e.isSynthetic       : void 0;

It now derives a second one unconditionally, for both frame types, stored on the
message as sdkParentToolUseId:

let t=e.type==="user"?e.parent_tool_use_id:null,
    i=e.parent_tool_use_id??null,          // <- new
    n=e.type==="user"?e.isSynthetic:void 0;

There is now also a helper meaning "came from a subagent", ORing both fields, plus
span tracking used by the routine that builds the render list:

function J1e(e){return !!e.parentToolUseId||!!e.sdkParentToolUseId}
... a.subagentSpans.some((y)=>y.use<x && ...)

What's still missing is one guard

The render dispatcher consults the tag only in the user branch. The assistant
branch filters hidden tool_use blocks and nothing else:

if(t.isEmpty)return null;
if(t.type==="user"){ if(t.parentToolUseId)return null; if(t.isSynthetic)return null; return ... }
if(t.type==="assistant"){
  if(t.content.every((d)=>d.content.type==="tool_use"&&$m(d.content.name,n).hidden))return null;
  return ...        // subagent assistant frames render here
}

Widening the first early-return covers every message type at once:

if(t.isEmpty||t.parentToolUseId||t.sdkParentToolUseId)return null;

Both fields matter — guarding on one fixes only half the cases

  • Streamed assistant frames are built by the stream assembler as

new Ap("assistant",[],{...,parentToolUseId:o}) and never receive sdkParentToolUseId.

  • Complete and reloaded frames go through the converter and receive sdkParentToolUseId.

So keying only on sdkParentToolUseId leaves live output leaking, and keying only
on parentToolUseId leaves reloaded history leaking.

Evidence

I've been running the equivalent as a local binary patch since late July, across
2.1.220 → 2.1.228. Inline subagent output disappears from both live rendering and
reloaded history, with no other visible change to the chat. Suppression is all it
does — it doesn't give agent output its own panel the way the TUI does, which
would be the better end state.

Still open from earlier in the thread: @junpicogram's report that foreground
subagents leak too. I couldn't reproduce it — every synchronous agent was silent
in my measurements, including one with 22 KB of output. If it still reproduces,
there's a second path neither of us has found.