fix(app): a chat that shrinks to fit mid-turn keeps its top rows drawn
Summary
Mid-turn, a chat could go blank at the top: the prompt, the Working fold and the first steps were missing, with empty space where they belonged.
Cause. The chat draws only the rows near the view, and it learns where the view is from the browser's scroll events. When the agent's long reply folded into Working (a tool call and a newer reply arrived after it), the chat got short enough to fit and the view moved back to the top. WebKit, which runs the Mac app, does that without firing a scroll event. So the virtualizer kept drawing for the old, lower position, and nothing scrolled after that to correct it. I confirmed this by replaying the real session's record through the box's transcript reader: the blank band in the screenshot is exactly the prompt, the fold and the first locations.go edit.
Fix (app/src/components/conversation/chat-list.tsx). Whenever the list changes height, it compares the virtualizer's offset with the real scrollTop. If they differ, it updates the virtualizer with a scroll event.
Testing
- New browser test in
app/e2e/draft.spec.tsthat replays the turn's shape: five edits, then a reply taller than the screen, then a command and a short reply that fold it away. It drops the browser's own (trusted) scroll events, as WebKit does. Without the fix it fails 4 times in 4; with it, the whole spec passes 24 times in 24 (6 tests Ć 4).chat-long.spec.tspasses 5 out of 5. - The test needs to add edits and commands to the demo's record, so the demo's test hook (
window.__berthDraft, mock-only) hasrecordbesideland. - WebKit itself wasn't run: Playwright's WebKit needs system libraries on this box. The Mac app is worth a look on the next long turn.
- Gate:
go vetandgo test -race,cargo check, the app checks and build,npx tsc -p e2e, and the docs build all pass, apart from one Go flake.internal/leadline/apiTestDetachfails 3 times in 10 with-race. This branch changes no Go code, so the same flake is onmain. The package passes on a rerun.
Review
- Round 1 (Codex
review --base origin/main): no actionable findings. - cubic on the first push raised two, both fixed:
- P2: the synthetic scroll event also reached the chat's own scroll handler, which could unpin a chat you touched a moment before. The handler now ignores untrusted scroll events.
- P3: the mock hook's
Omit<TranscriptItem, "id">flattened the union. It's now distributive (Unsaved<T>).
- Round 2 (Codex, on the fixes): no actionable findings.
š¤ Generated with Claude Code