Skip to content

Release exp-v0.52.278: stop tearing live progress words (#7082, @ruizanthony) - #7473

Merged
nesquena-hermes merged 7 commits into
masterfrom
stage/7082
Sep 8, 2026
Merged

nesquena-hermes merged 7 commits into
masterfrom
stage/7082

Conversation

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Release exp-v0.52.278 — stop tearing live progress words across block boundaries (#7082, @ruizanthony)

Ships #7082: fixes a client-side render artifact where live progress text in Transparent Stream could split a word across a block boundary and render the tail on its own line (e.g. Ces deu / x fichiers passent.). The streamed text was always intact — the tearing was purely in the live fade/reconcile render path.

The renderer now adopts the parser-owned DOM once after a parser-owner replacement (keeping the per-token fast path for normal ticks) and handles the source-space cursor at block boundaries, so live prose stays whole.

Gate (crown-jewel streaming surface — full gate)

  • Codex: SAFE TO SHIP — no lost/duplicated/reordered/raw-Markdown text; normal per-token renders keep the fast path (O(n) adoption only once after parser-owner replacement, no perf regression); live-to-final, MEDIA tails, reduced motion, multi-block, Unicode/emoji/CJK, prefix-dedupe, classic-script collision all intact; base/head repro confirms base tears Ces de | ux… while head preserves the complete text; 227 focused tests + 18 subtests pass.
  • Crown-jewel stream gate: GREEN across all modes (transparent_stream, compact_worklog, hide_all_activity) — proseReversals=0, nonProseReanims=0, no alternating (modeFlips=0). This is the definitive motion proof for a live-render fix.
  • Full suite: 15103 passed (two pre-existing box-environment artifacts unrelated to this diff).

Credited via Co-authored-by in the release commit.

ruizanthony and others added 7 commits August 27, 2026 23:58
Transparent-stream progress rows are built incrementally by
`_anchorProseIncrementalNode`, which streams source text into the vendored
streaming-markdown parser. That parser always holds the most recently written
character in its pending buffer, so a live row's rendered `.msg-body` text
trails its source text by one character.

`_refreshTransparentFadeProseRow` resumed appending from the
`data-stream-fade-text` cursor but fell back to `body.textContent` when the
attribute was absent — exactly the case for incrementally built rows, which
never set it. That fallback mixed two coordinate spaces (rendered text vs
source text), so the computed delta started one character early, in the middle
of a word. The delta was then appended as a sibling of the parser's open `<p>`,
and since `<p>` is a block box the tail rendered on its own line: a single word
split across two lines ("Ces deu" / "x fichiers").

Two independent fixes:

1. Resume strictly from the source-space cursor. When no cursor exists we
   cannot know how much source is already rendered, so rebuild the body
   instead of guessing a delta from rendered text.
2. Append into the trailing block element rather than the `.msg-body` root, so
   a continuation of the current sentence stays on the same line even if a
   cursor is ever stale.

Regression tests drive the real vendored parser to build a live row exactly as
production does, assert the parser really does lag its source, and fail if any
word is torn across a block boundary. The existing reconcile harness gains the
new helper dependency.
…w rebuild

Review follow-ups for #7082:

- Maintainer should-fix: the no-cursor rebuild branch of
  _refreshTransparentFadeProseRow cleared the body and re-wrapped every
  word as .is-new, so the whole visible row dipped to opacity 0 and
  faded back (~620ms), and wholesale node replacement invited the
  one-time scroll-anchor bounce (#6257). Now the rendered text is
  snapshotted before the clear and the messages.js
  _streamFadeMuteRenderedPrefix idiom is re-applied after the rebuild,
  so only genuinely-new tail words animate. The helper is exported on
  window (__streamFadeMuteRenderedPrefix) the same way
  __anchorProseIncrementalNode is.

- Greptile P1: the same branch passed raw source text to
  _appendTransparentFadeText, flattening live markdown rows to literal
  syntax (links/emphasis/headings lost until settlement). The rebuild
  now adopts a deep clone of the candidate's parsed .msg-body (the
  incremental streaming-markdown output) when one exists, so the parsed
  DOM survives; the plain-text rebuild remains only as fallback for
  candidates without a parsed body. The resume cursor stays in source
  space when rendered text diverges from source (markdown), and shrinks
  to the rendered prefix for plain prose so the parser's held-back
  character is re-appended on the next delta instead of dropped.

Regression tests: rebuild must not re-animate the already-rendered
prefix (tail-only .is-new), and rebuild must keep parsed <a>/<strong>
DOM with no literal markdown in the visible text. The two existing
tests of this PR are unchanged and still pass.
… markdown rebuild

Greptile P1 follow-up on the fade-row rebuild: when a cursorless live
markdown row adopts the incremental node's cloned DOM, that DOM lags the
source text by the streaming parser's held-back tail (pending/text
buffers). Keeping the full source-length resume cursor made the next
reconciliation append nothing, so the pending characters stayed missing
from the live row until settlement.

Read the held-back length from the parser bound on the candidate body
(_smdBindParserIdentity) and trim the source-space cursor by exactly
that length, so the cursor matches the adopted content and the next
reconciliation re-appends the pending tail.

Regression: rebuild while the parser holds an unrendered tail, then
reconcile again — the pending text must appear in the live row before
settlement.
Cursorless markdown rebuilds cloned the live parser tree, then later
source growth appended raw bytes through _appendTransparentFadeText.
Incomplete emphasis (Hello ** / Hello **world**) became literal
**world** while the candidate already had <strong>world</strong>.

Parser-owned candidates now keep reconciling from the parsed DOM.
Pending and MEDIA tails stay on the parser. Rendered-prefix mute is
unchanged.
After rewind/cache-miss the keyed visible row stayed distinct from the
persistent parser candidate, so every later growth frame deep-cloned the
parser DOM into the old row. That cut off the previous tail word's
~620ms fade and broke word-node identity.

Promote the parser-owned candidate into the existing row's DOM position
once, mute the already-rendered prefix on the real parser body, and
return the adopted node so later keyed renders hit existing === node.

Regression: two post-transition growth frames keep the first new tail
span as the same .is-new node until an explicit animationend.
@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This release fixes mid-word tearing in Transparent Stream by preserving parser ownership across live prose reconciliation and avoiding rendered-text/source-text cursor confusion.

  • Exposes the existing rendered-prefix mute helper for transparent-stream reconciliation.
  • Promotes the parser-owned live row after an owner replacement, preserving Markdown structure, fade-node identity, and pending parser tails.
  • Appends cursor-based text inside the active trailing block rather than beside it.
  • Adds browserless regression coverage for word boundaries, Markdown, pending tails, repeated growth, fade behavior, and row reconciliation.

Confidence Score: 5/5

The PR appears safe to merge; no actionable correctness, security, or repository-rule violations remain.

The changed reconciliation path preserves parser authority and stable node identity while the added tests exercise the reported tear, Markdown structure, pending parser state, animation behavior, and subsequent growth.

Important Files Changed

Filename Overview
static/ui.js Reconciles transparent live prose using parser-owned DOM, source-space cursors, block-aware appends, and rendered-prefix fade muting.
static/messages.js Exposes the stateless rendered-prefix mute helper for reuse by the transparent-stream reconciler.
tests/test_live_prose_fade_cursor_source_space.py Adds extensive browserless regression coverage for source/render cursor boundaries, Markdown ownership, pending tails, fade identity, and repeated growth.
tests/test_issue5367_transparent_live_row_reconcile.py Updates the transparent-row reconciliation harness for the new block-aware append helper.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Streamed prose source] --> B[Incremental Markdown parser]
    B --> C[Parser-owned live row]
    C --> D{Existing keyed row uses same owner?}
    D -->|No, owner replaced| E[Adopt parser-owned row once]
    E --> F[Mute already-rendered prefix]
    D -->|Yes| G[Keep existing node identity]
    F --> G
    G --> H[Parser appends later source deltas]
    H --> I[Stable Transparent Stream prose]
    I --> J[Settled durable transcript replaces live view]
Loading

Reviews (1): Last reviewed commit: "changelog: #7082 stop tearing live progr..." | Re-trigger Greptile

@nesquena-hermes
nesquena-hermes merged commit 7d56234 into master Sep 8, 2026
24 checks passed
@nesquena-hermes
nesquena-hermes deleted the stage/7082 branch September 8, 2026 21:53
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.

2 participants