code-yeongyu / code-yeongyu/senpi
Progressive transcript hydration exposes its moving watermark to unrelated live renders (vertical motion on resume)
- Dominant language
- TypeScript
- Stars
- 429
- Forks
- 98
- Avg merge
- 5h 3m
- Merged PRs (30d)
- 526
Description
## 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 98234d104)
`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.
Contributor guide
Research direction
Start with packages/coding-agent/src/modes/interactive/components/progressive-transcript-container.ts, especially the warming and render ranges around lines 76-96 and 142-176. Run packages/coding-agent/test/suite/progressive-transcript-container.test.ts and reproduce the one-setImmediate append case. Done means the visible first index stays fixed during warming, the live component remains at the tail, and the full-history reveal occurs only once hydration completes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli, testing
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100