Skip to content

Progressive transcript hydration exposes its moving watermark to unrelated live renders (vertical motion on resume) #1076

Description

@code-yeongyu

Context

Found during the 2026-08-22 TUI vertical-jitter forensics (follow-up to #1064; the primary status-dock fix ships separately). This is an independent vertical-motion path for resumed / post-compaction transcripts, NOT the cause of the captured turn-boundary jitter.

Mechanism (HEAD 98234d1)

12d771dbc (perf(coding-agent): render resumed transcripts progressively) introduced progressive transcript hydration:

  • packages/coding-agent/src/modes/interactive/components/progressive-transcript-container.ts:76-96 renders from hydratedFrom to the tail.
  • :142-176 warms a head chunk per setImmediate and moves hydratedFrom BACKWARD after every chunk.
  • Any unrelated assistant/tool render between chunks calls render() and exposes the newly lowered watermark, inserting a warmed batch ABOVE already-painted tail content -> painted content moves vertically in multiple geometry-changing batches while the user watches.

Affects resumed sessions and compaction rebuilds whose history exceeds the tail budget.

Proposed fix (preserves the perf intent)

Split warming state from visible state:

  1. Keep warmedFrom for background cache population (Markdown work stays spread across bounded macrotasks).
  2. Keep visibleFrom fixed at the initial tail boundary while warming is incomplete; unrelated live renders keep painting visibleFrom..total including appended live children.
  3. When warmedFrom reaches zero, atomically set visibleFrom = 0 and issue the existing single completion render (one atomic height increase).

Failing-first proof

Deterministic case for packages/coding-agent/test/suite/progressive-transcript-container.test.ts:

  1. Populate more children than tailBudget.
  2. Render once; record the first visible component index.
  3. Await exactly ONE setImmediate (one warm chunk completes, hydration unfinished).
  4. Append one live component; render again.
  5. Assert the first visible index is unchanged and the live component is at the tail.

Current HEAD fails: warmNextChunk() has already lowered hydratedFrom, so the second render starts one chunk earlier. No sleeps needed.

Risk notes

Medium: the final full-history reveal remains one atomic height increase and may still trigger one main-screen scrollback replay; verify resume, compaction rebuild, append-during-hydration, clear/dispose cancellation, and full transcript output.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions