Skip to content

fix(agent-mode): coalesce streamed prose split by an invisible trail part#2648

Open
zeroliu wants to merge 1 commit into
v4-previewfrom
zero/agent-trail-fix
Open

fix(agent-mode): coalesce streamed prose split by an invisible trail part#2648
zeroliu wants to merge 1 commit into
v4-previewfrom
zero/agent-trail-fix

Conversation

@zeroliu

@zeroliu zeroliu commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

In agent mode, a streamed assistant message occasionally shows a line break in the middle of a sentence in the live UI, e.g.:

…That's exactly the Pillar-2 "let the
kid feel the change, he proposes the fix himself" working in the wild.

The model never produced the break: the raw Claude Code session log has the text as one contiguous block, and the saved conversation .md (built from the flat displayText, deltas concatenated with no separator) has no break either. It is a render-only artifact that self-heals on reload.

Root cause

The live trail renders each streamed text part as its own block-level markdown <div>. When a part that is invisible at the top level lands between two prose deltas, one sentence splits into two stacked blocks. The three invisible triggers:

  • a dropped ToolSearch (deferred-tool loader, filtered from the trail),
  • an empty plan (PlanPill renders null),
  • a concurrent background sub-agent's event (rendered nested under its parent card, never as a top-level peer) — the reason this only showed up in a session that backgrounded sub-agents while still streaming prose.

Fix

One shared isTopLevelInvisible predicate, applied to all three consumers of the parts stream (src/agentMode/ui/agentTrail.ts):

  1. Live render (foldNodes) — skip empty-plan nodes and coalesce adjacent text nodes, so the streamed trail is byte-identical to the saved displayText.
  2. Copy/Insert (agentResponseText) — derived from the folded trail, so a split sentence no longer gets a \n\n blank line; genuinely separate prose (around a visible tool card / reasoning / non-empty plan) still gets \n\n.
  3. Collapsed "Worked for X" (splitTrailingText + AgentTrailView) — invisible parts are transparent to the trailing-run boundary, and the final run renders as one coalesced block, so the collapse can't strand half a sentence in the hidden research.

Testing

  • New unit tests cover all three invisible triggers (hidden ToolSearch, empty plan, sub-agent child) across buildAgentTrail, agentResponseText, and splitTrailingText; visible-separator behavior is preserved.
  • npx tsc --noEmit, npm run lint, npm run format clean.
  • 1350/1350 agentMode tests pass.

🤖 Generated with Claude Code

…part

A streamed agent message renders each `text` part as its own block-level
markdown div, so when a top-level-invisible part — a hidden ToolSearch, an
empty plan, or a concurrent background sub-agent's event — lands between two
prose deltas, one sentence splits into two stacked blocks and shows a spurious
mid-sentence line break in the live UI. The saved markdown, built from the flat
`displayText` (deltas concatenated with no separator), has no break, so the
artifact is render-only and self-heals on reload.

Add a shared `isTopLevelInvisible` predicate and apply it across all three
consumers of the parts stream:
- `foldNodes` skips empty plans and coalesces adjacent text nodes, so the live
  trail matches the saved `displayText`.
- `agentResponseText` derives from the folded trail, so Copy/Insert no longer
  injects a blank line into a split sentence (a visible tool card / reasoning /
  non-empty plan between prose still yields a real `\n\n` break).
- `splitTrailingText` treats invisible parts as transparent and the collapsed
  "Worked for X" view renders the final run as one block, so the collapse can't
  strand half a sentence in the hidden research.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d92e2d331

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +60 to +63
parts.forEach((p, i) => {
if (finalIdx.has(i)) final.push(p as TextPart);
else research.push(p);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep invisible parts from creating an empty collapse

When a completed turn is otherwise just prose split by an empty plan or hidden ToolSearch, the backward scan now treats both prose chunks as final, but this branch still pushes the invisible separator into research. AgentTrail then considers any non-text research item as content, enters the “Worked for X” collapse, and the expanded LinearTrail renders nothing because those parts are dropped by buildAgentTrail, leaving an empty accordion above a plain answer. Filter top-level-invisible parts out of research (or base the content check on rendered nodes) so invisible-only research does not trigger collapse.

Useful? React with 👍 / 👎.

if (prevNode && prevNode.type === "text") {
out[out.length - 1] = {
type: "text",
part: { kind: "text", text: prevNode.part.text + p.text },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve whitespace-only deltas when coalescing prose

When a streamed newline/space-only chunk is isolated by invisible events (for example text("A"), a hidden ToolSearch or sub-agent child, text("\n\n"), another invisible event, then text("B")), the whitespace-only text part is skipped and this coalescing turns the visible text into AB, even though the stored displayText is A\n\nB. Text parts are opened after any non-text event, so invisible events around a whitespace delta are enough to lose the model's intentional separator in the live trail and Copy/Insert output; preserve skipped whitespace in the pending prose run or avoid coalescing across it.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant