code-yeongyu / code-yeongyu/senpi

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

Open
#1,076 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.