Skip to content

fix(ui): deduplicate streaming chat segments to prevent growing duplicate bubbles#47399

Closed
LittleBreak wants to merge 1 commit into
openclaw:mainfrom
LittleBreak:fix/webchat-streaming-duplicate-bubbles
Closed

fix(ui): deduplicate streaming chat segments to prevent growing duplicate bubbles#47399
LittleBreak wants to merge 1 commit into
openclaw:mainfrom
LittleBreak:fix/webchat-streaming-duplicate-bubbles

Conversation

@LittleBreak

Copy link
Copy Markdown

Summary

Fixes #47188 — webchat streaming responses create multiple duplicate message bubbles with growing text instead of updating a single bubble in-place.

Root Cause

The gateway sends the full accumulated assistant text in each streaming delta event. When a tool call interrupts the stream, app-tool-stream.ts saves the current chatStream (full accumulated text) as a segment. After the tool completes, new delta events arrive containing the full text again (including text already in segments), causing each segment to show incrementally more duplicated text.

This was previously masked by a loadChatHistory() call after every tool result (removed in 0e8672a to fix a reload storm). Removing that call exposed this pre-existing segment duplication bug.

Fix

Introduce chatStreamSegmentOffset to track the length of text already committed to segments:

  • app-tool-stream.ts: When saving a segment on tool start, use chatStream.slice(offset) to store only the delta text (not the full accumulated text). Update offset = chatStream.length after each save.
  • views/chat.ts: When rendering the live stream bubble, use stream.slice(offset) to display only new text after the last segment.
  • Reset offset in resetToolStream().

Files Changed (8)

File Change
ui/src/ui/app-tool-stream.ts Segment save logic + offset tracking
ui/src/ui/views/chat.ts Render delta text, add streamSegmentOffset prop
ui/src/ui/app.ts Add @state() chatStreamSegmentOffset
ui/src/ui/app-view-state.ts Add type field
ui/src/ui/app-render.ts Pass offset prop
ui/src/ui/views/chat.test.ts Add dedup test
ui/src/ui/app-tool-stream.node.test.ts Update host fixture
ui/src/ui/app-gateway.node.test.ts Update host fixture

Testing

  • All 52 existing tests pass (27 chat controller + 25 chat view)
  • New test verifies segment offset stripping prevents duplicate text in stream bubbles
  • Format check passes

🤖 This PR was prepared with AI assistance.

…cate bubbles (openclaw#47188)

The gateway sends the full accumulated assistant text in each streaming
delta event. When a tool call interrupts the stream, the client saves
the current chatStream as a segment. Subsequent deltas (still containing
the full accumulated text) then show overlapping content: each segment
contains all prior text, and the live stream bubble repeats it again.

This was previously masked by a loadChatHistory() call after every tool
result (removed in 0e8672a to fix a reload storm).

Fix: track chatStreamSegmentOffset — the length of text already committed
to segments. When saving a new segment, slice off the known prefix to
store only the delta. When rendering the live stream bubble, strip the
offset prefix so only new text appears.

Adds test coverage for the segment dedup in chat.test.ts.
@greptile-apps

greptile-apps Bot commented Mar 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a pre-existing segment duplication bug (#47188) where the gateway's full-accumulated-text streaming model caused each committed segment to contain incrementally more text from previous segments, producing growing duplicate bubbles after tool calls. The fix introduces a chatStreamSegmentOffset integer that tracks how many characters have already been committed to segments, and applies slice(offset) both when saving new segments and when rendering the live stream bubble.

The core logic is sound: all paths that reset streaming state (resetToolStream, loadChatHistory, session switches, reconnect) correctly reset the offset to 0, and handleSendChat calls resetToolStream before each new turn so stale offset values never bleed across conversation turns.

Key observations:

  • The fix is correct and well-scoped. The slice(offset) approach is the right model for a server that sends full accumulated text per delta.
  • The updated reading-indicator guard in chat.ts (else if (props.stream.trim().length === 0)) correctly avoids showing a spinner when the stream has text but that text is fully covered by committed segments — a subtle but important detail.
  • An unrelated binary file (.openclaw-workspace-image-A8W3EQ/workspace/photo.png) was accidentally committed alongside the code changes. This file is a local workspace artifact and should be removed before merging.

Confidence Score: 3/5

  • The streaming logic fix is correct, but the PR must not be merged with the unrelated binary file included.
  • The chatStreamSegmentOffset tracking logic is correct and all reset paths properly zero the offset. The test coverage is adequate. However, the accidental inclusion of an unrelated PNG binary file (.openclaw-workspace-image-A8W3EQ/workspace/photo.png) is a blocker — it pollutes the repository's git history with an artifact that has no place in a UI code fix. Removing this file and re-testing would raise confidence to 5.
  • .openclaw-workspace-image-A8W3EQ/workspace/photo.png — unrelated binary that must be removed before merging.

Comments Outside Diff (1)

  1. .openclaw-workspace-image-A8W3EQ/workspace/photo.png, line 1 (link)

    Unrelated binary file accidentally committed

    This photo.png file in a generated workspace directory (.openclaw-workspace-image-A8W3EQ/workspace/) is completely unrelated to the streaming chat deduplication fix. Binary files like this should not be committed to the repository.

    This appears to be an accidental commit of a workspace artifact. Please remove this file before merging — it will permanently inflate the git history and may be flagged by security tooling.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: .openclaw-workspace-image-A8W3EQ/workspace/photo.png
Line: 1

Comment:
**Unrelated binary file accidentally committed**

This `photo.png` file in a generated workspace directory (`.openclaw-workspace-image-A8W3EQ/workspace/`) is completely unrelated to the streaming chat deduplication fix. Binary files like this should not be committed to the repository.

This appears to be an accidental commit of a workspace artifact. Please remove this file before merging — it will permanently inflate the git history and may be flagged by security tooling.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 5671413

@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: 567141324a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +449 to +452
if (deltaText.trim().length > 0) {
host.chatStreamSegments = [...host.chatStreamSegments, { text: deltaText, ts: now }];
}
host.chatStreamSegmentOffset = host.chatStream.length;

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 committing segments

When a tool starts, the new logic drops deltaText if it is whitespace-only (deltaText.trim().length === 0) but still advances chatStreamSegmentOffset to the full stream length. If the only text emitted between two tool calls is a space or newline, that separator is permanently discarded, so subsequent streamed text can be concatenated without intended spacing/formatting (for example "Hello" + " world" becomes "Helloworld"). This regression is specific to runs that interleave tool events with whitespace-only assistant increments.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close as superseded: the WebChat cumulative-stream bug is still real on current main, but open PR #46985 already tracks the same remaining fix with a cleaner current-main implementation, while this PR has concrete merge blockers and an unrelated binary artifact.

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Review details

Best possible solution:

Use #46985, or an equivalent maintainer-selected branch, as the canonical fix: it should dedupe both committed stream segments and the live preview on the current build-chat-items path, preserve exact streamed whitespace, and avoid unrelated artifacts.

Do we have a high-confidence way to reproduce the issue?

Yes. The source-level reproduction path is high-confidence: the gateway sends cumulative chat delta text, the UI stores that cumulative text as chatStream, commits full snapshots into chatStreamSegments at tool boundaries, and renders the later cumulative live stream unchanged.

Is this the best way to solve the issue?

No. The offset/suffix approach is directionally right, but this branch is not the best current solution because it drops whitespace-only suffixes, includes an unrelated binary artifact, and targets an outdated render path; #46985 is the cleaner remaining implementation to review.

Security review:

Security review needs attention: The TypeScript streaming changes do not broaden permissions or secrets, but the PR adds an unrelated binary workspace artifact that should be removed rather than merged.

  • [low] Unrelated binary artifact — .openclaw-workspace-image-A8W3EQ/workspace/photo.png:1
    The new PNG under .openclaw-workspace-image-A8W3EQ/ is outside the stated UI fix and is not reviewable as source, creating avoidable repository and supply-chain hygiene risk.
    Confidence: 0.95

What I checked:

Likely related people:

  • steipete: Prior ClawSweeper feature-history for this same stream-segment path records Peter Steinberger maintaining the Control UI tool-streaming, chat item builder, and gateway chat projection areas; current local blame also maps the central sampled files to Peter at the checkout boundary. (role: recent maintainer and release owner; confidence: medium; commits: de2ccffec166, 68954f9c6ccb, f62a054ef1ac; files: ui/src/ui/app-tool-stream.ts, ui/src/ui/chat/build-chat-items.ts, ui/src/ui/views/chat.ts)
  • jakepresent: Related PR fix(ui): live tool call streaming in Control UI webchat #39104 introduced live Control UI tool streaming and stream segment ordering across the files central to this bug, making Jake a useful routing candidate for the behavior history. (role: original adjacent feature contributor; confidence: medium; commits: de2ccffec166; files: ui/src/ui/app-tool-stream.ts, ui/src/ui/views/chat.ts, ui/src/ui/app-render.ts)
  • BunsDev: The PR body and prior ClawSweeper review identify commit 0e8672af87f27c8685793ab202bcb027c792edee as removing the per-tool-result history reload that had masked this pre-existing segment duplication behavior. (role: adjacent reload-path maintainer; confidence: medium; commits: 0e8672af87f2; files: ui/src/ui/app-gateway.ts, ui/src/ui/app-gateway.node.test.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against c4a4c189f11e.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Webchat streaming renders duplicate bubbles instead of updating in-place

1 participant