claude-code - 💡(How to fix) Fix Streamed assistant text re-lays-out on terminal redraws, producing overlapping/duplicated fragments

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

When Claude streams a long assistant response in the terminal, the rendered text appears to re-layout multiple times as the terminal repaints, producing overlapping, duplicated, and progressively corrupted fragments. The user has to "slog through" the scrambled output to find what was actually said. The underlying response content from the model is fine — only the on-screen rendering is corrupted.

Root Cause

When Claude streams a long assistant response in the terminal, the rendered text appears to re-layout multiple times as the terminal repaints, producing overlapping, duplicated, and progressively corrupted fragments. The user has to "slog through" the scrambled output to find what was actually said. The underlying response content from the model is fine — only the on-screen rendering is corrupted.

Fix Action

Workaround

User has asked me to keep responses shorter and more bulleted to reduce the surface area for the rendering bug. This works, but it imposes a content-shape constraint on the model that shouldn't be needed.

Code Example

On virtual-text + offset-map — answering
  since you asked, then confirming we no
  longer need them.

  The idea was: to support multi-line search
  across block boundaries, build an in-memory
  string representing the document's text
  where each <p> block's text contents are
  concatenated, separated by a synthetic \n at
   block boundaries. Alongside it, keep a
  parallel array recording "character N of
  this virtual string corresponds to (DOM text
   node X, offset Y)." Then run the regex
  against the flat virtual string; for each
  match, translate the match's start/end
  character indices back to DOM positions via
  the parallel array; construct a Range that
  may span multiple DOM text nodes; pass to
  CSS Custom Highlight for rendering.
  Ephemeral construction — built per search,
  never written to DOM, no parallel state.

  It's no longer needed. With the
  paste-handler approach you proposed: the
  Find textarea is guaranteed \n-free at all
  times → the regex string has no \n → no need
   to match across block boundaries → the
  existing per-text-node TreeWalker scan
  (which is already in the code and was the
  s82 design) just works against the new
  drawer UI. The search engine code is reused
  verbatim. The drawer is purely a UI change.

   no longer need them.

  The idea was: to support multi-line search across block boundaries, build an
   in-memory string representing the document's text where each <p> block's
  text contents are concatenated, separated by a synthetic \n at block
  boundaries. Alongside it, keep a parallel array recording "character N of
  this virtual string corresponds to (DOM text node X, offset Y)." Then run
  the regex against the flat virtual string; for each match, translate the
  match's start/end character indices back to DOM positions via the parallel
  corresponds to (DOM text node X, offset Y)." Then run the regex against the flat
  virtual string; for each match, translate the match's start/end character indices
  back to DOM positions via the parallel array; construct a Range that may span
  multiple DOM text nodes; pass to CSS Custom Highlight for rendering. Ephemeral
  construction — built per search, never written to DOM, no parallel state.

  It's no longer needed. With the paste-handler approach you proposed: the Find
  textarea is guaranteed \n-free at all times → the regex string has no \n → no need
  to match across block boundaries → the existing per-text-node TreeWalker scan
  (which is already in the code and was the s82 design) just works against the new
  drawer UI. The search engine code is reused verbatim. The drawer is purely a UI
  change.

  ---
  So the corrected and simpler design:

  1. Pipeline tightening (cleanup stage): unify _trim_lines +
  _collapse_interior_whitespace into one readable function; add _tabs_to_spaces;
  consider _parse_blocks rename for _build_blocks; add fail-loud assertion at
  render_transcript_html (no \t, \n, or double-space in output)  node X, offset Y)." Then run the regex against the flat
  virtual string; for each match, translate the match's
  start/end character indices back to DOM positions via the
  parallel array; construct a Range that may span multiple DOM
  text nodes; pass to CSS Custom Highlight for rendering.
  Ephemeral construction — built per search, never written to
  DOM, no parallel state.

  It's no longer needed. With the paste-handler approach you
  proposed: the Find textarea is guaranteed \n-free at all
  times → the regex string has no \n → no need to match across
  block boundaries → the existing per-text-node TreeWalker scan
   (which is already in the code and was the s82 design) just
  works against the new drawer UI. The search engine code is
  reused verbatim. The drawer is purely a UI change.

  ---
  So the corrected and simpler design:
  character indices back to DOM positions via the parallel array; construct a
  Range that may span multiple DOM text nodes; pass to CSS Custom Highlight for
  rendering. Ephemeral construction — built per search, never written to DOM, no
   parallel state.

  It's no longer needed. With the paste-handler approach you proposed: the Find
  textarea is guaranteed \n-free at all times → the regex string has no \n → no
  need to match across block boundaries → the existing per-text-node TreeWalker
  textarea is guaranteed \n-free at all times → the regex string has no \n → no need
  to match across block boundaries → the existing per-text-node TreeWalker scan (which
   is already in the code and was the s82 design) just works against the new drawer
  UI. The search engine code is reused verbatim. The drawer is purely a UI change.

  ---
  So the corrected and simpler design:

  1. Pipeline tightening (cleanup stage): unify _trim_lines +
  _collapse_interior_whitespace into one readable function; add _tabs_to_spaces;
  consider _parse_blocks rename for _build_blocks; add fail-loud assertion at
  render_transcript_html (no \t, \n, or double-space in output).
  1. Pipeline tightening (cleanup stage): unify _trim_lines +
  _collapse_interior_whitespace into one readable function; add _tabs_to_spaces;
  consider _parse_blocks rename for _build_blocks; add fail-loud assertion at
  render_transcript_html (no \t, \n, or double-space in output).
  2. Find/Replace drawer: BBEdit-style UI; existing per-text-node scan unchanged; Find
   textarea has a paste handler that strips \n from clipboard content (THE fix for the
   cross-block-paste UX problem).
  3. Stash cleanup: delete _legacy_grid_era_editor.js and _legacy_js_undo.js; remove
  the dangling comment reference in edit.html.

  No virtual-text, no offset-map, no cross-block search engine. Existing search code
  reused.
RAW_BUFFERClick to expand / collapse

Summary

When Claude streams a long assistant response in the terminal, the rendered text appears to re-layout multiple times as the terminal repaints, producing overlapping, duplicated, and progressively corrupted fragments. The user has to "slog through" the scrambled output to find what was actually said. The underlying response content from the model is fine — only the on-screen rendering is corrupted.

Environment

  • macOS 26.3.1
  • Apple Terminal 466
  • TERM=xterm-256color
  • Shell: zsh
  • Claude Code session running model claude-opus-4-7 (1M context)

Reproduction

Triggers most consistently when:

  • The streamed assistant message contains multi-line prose paragraphs (vs. short bullet-style lines).
  • The terminal window's width changes mid-stream OR the terminal does its normal repaints during a fast stream.

The user pasted the following as their lived evidence — what they saw on screen while trying to read a single assistant response. The actual response was one coherent block; this is the on-screen result:

On virtual-text + offset-map — answering
  since you asked, then confirming we no
  longer need them.

  The idea was: to support multi-line search
  across block boundaries, build an in-memory
  string representing the document's text
  where each <p> block's text contents are
  concatenated, separated by a synthetic \n at
   block boundaries. Alongside it, keep a
  parallel array recording "character N of
  this virtual string corresponds to (DOM text
   node X, offset Y)." Then run the regex
  against the flat virtual string; for each
  match, translate the match's start/end
  character indices back to DOM positions via
  the parallel array; construct a Range that
  may span multiple DOM text nodes; pass to
  CSS Custom Highlight for rendering.
  Ephemeral construction — built per search,
  never written to DOM, no parallel state.

  It's no longer needed. With the
  paste-handler approach you proposed: the
  Find textarea is guaranteed \n-free at all
  times → the regex string has no \n → no need
   to match across block boundaries → the
  existing per-text-node TreeWalker scan
  (which is already in the code and was the
  s82 design) just works against the new
  drawer UI. The search engine code is reused
  verbatim. The drawer is purely a UI change.

   no longer need them.

  The idea was: to support multi-line search across block boundaries, build an
   in-memory string representing the document's text where each <p> block's
  text contents are concatenated, separated by a synthetic \n at block
  boundaries. Alongside it, keep a parallel array recording "character N of
  this virtual string corresponds to (DOM text node X, offset Y)." Then run
  the regex against the flat virtual string; for each match, translate the
  match's start/end character indices back to DOM positions via the parallel
  corresponds to (DOM text node X, offset Y)." Then run the regex against the flat
  virtual string; for each match, translate the match's start/end character indices
  back to DOM positions via the parallel array; construct a Range that may span
  multiple DOM text nodes; pass to CSS Custom Highlight for rendering. Ephemeral
  construction — built per search, never written to DOM, no parallel state.

  It's no longer needed. With the paste-handler approach you proposed: the Find
  textarea is guaranteed \n-free at all times → the regex string has no \n → no need
  to match across block boundaries → the existing per-text-node TreeWalker scan
  (which is already in the code and was the s82 design) just works against the new
  drawer UI. The search engine code is reused verbatim. The drawer is purely a UI
  change.

  ---
  So the corrected and simpler design:

  1. Pipeline tightening (cleanup stage): unify _trim_lines +
  _collapse_interior_whitespace into one readable function; add _tabs_to_spaces;
  consider _parse_blocks rename for _build_blocks; add fail-loud assertion at
  render_transcript_html (no \t, \n, or double-space in output)  node X, offset Y)." Then run the regex against the flat
  virtual string; for each match, translate the match's
  start/end character indices back to DOM positions via the
  parallel array; construct a Range that may span multiple DOM
  text nodes; pass to CSS Custom Highlight for rendering.
  Ephemeral construction — built per search, never written to
  DOM, no parallel state.

  It's no longer needed. With the paste-handler approach you
  proposed: the Find textarea is guaranteed \n-free at all
  times → the regex string has no \n → no need to match across
  block boundaries → the existing per-text-node TreeWalker scan
   (which is already in the code and was the s82 design) just
  works against the new drawer UI. The search engine code is
  reused verbatim. The drawer is purely a UI change.

  ---
  So the corrected and simpler design:
  character indices back to DOM positions via the parallel array; construct a
  Range that may span multiple DOM text nodes; pass to CSS Custom Highlight for
  rendering. Ephemeral construction — built per search, never written to DOM, no
   parallel state.

  It's no longer needed. With the paste-handler approach you proposed: the Find
  textarea is guaranteed \n-free at all times → the regex string has no \n → no
  need to match across block boundaries → the existing per-text-node TreeWalker
  textarea is guaranteed \n-free at all times → the regex string has no \n → no need
  to match across block boundaries → the existing per-text-node TreeWalker scan (which
   is already in the code and was the s82 design) just works against the new drawer
  UI. The search engine code is reused verbatim. The drawer is purely a UI change.

  ---
  So the corrected and simpler design:

  1. Pipeline tightening (cleanup stage): unify _trim_lines +
  _collapse_interior_whitespace into one readable function; add _tabs_to_spaces;
  consider _parse_blocks rename for _build_blocks; add fail-loud assertion at
  render_transcript_html (no \t, \n, or double-space in output).
  1. Pipeline tightening (cleanup stage): unify _trim_lines +
  _collapse_interior_whitespace into one readable function; add _tabs_to_spaces;
  consider _parse_blocks rename for _build_blocks; add fail-loud assertion at
  render_transcript_html (no \t, \n, or double-space in output).
  2. Find/Replace drawer: BBEdit-style UI; existing per-text-node scan unchanged; Find
   textarea has a paste handler that strips \n from clipboard content (THE fix for the
   cross-block-paste UX problem).
  3. Stash cleanup: delete _legacy_grid_era_editor.js and _legacy_js_undo.js; remove
  the dangling comment reference in edit.html.

  No virtual-text, no offset-map, no cross-block search engine. Existing search code
  reused.

The same paragraph block appears multiple times at different indent widths — each apparently re-flowed for a different terminal width — overlapping with the previous render rather than replacing it.

Expected behavior

Streamed text should render cleanly with one final layout per logical paragraph. Mid-stream re-layouts (if needed for word-wrap on width changes) should clear the previous render before painting the new one.

Workaround

User has asked me to keep responses shorter and more bulleted to reduce the surface area for the rendering bug. This works, but it imposes a content-shape constraint on the model that shouldn't be needed.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Streamed text should render cleanly with one final layout per logical paragraph. Mid-stream re-layouts (if needed for word-wrap on width changes) should clear the previous render before painting the new one.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix Streamed assistant text re-lays-out on terminal redraws, producing overlapping/duplicated fragments