# refactor\(web\): timeline row derivation, position restore and find reader live in hooks · PR \#22682 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22682)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: merged

Source repository: pingdotgg/t3code

Source branch: refactor/messages-timeline-scroll-find-hooks

Target branch: refactor/messages-timeline-assistant-event-rows

Head commit: 52061dad19113a8357711aec08affb1671de7624

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/22682)

[Commits](https://git.cafe/pingdotgg/t3code/pull/22682/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/22682/diffs?format=markdown)

## Description

---

Part of the MessagesTimeline split. This layer moves the scroll and find machinery out of `ConversationTimeline` into hooks. Effect bodies, dependencies and effect order are unchanged.

- `useTimelinePositionRestore.ts`: the list-opened tracker and the layout effect that restores a thread's remembered scroll position. `setPositionedThreadKey` is passed straight through as `onPositioned`.
- `useThreadFindPositionReader.ts`: the layout effect that publishes the find position reader.
- `useTimelineRows.ts`: `useIncrementalTimelineRows`, `useStableRows`, `useStableHandoffRuns`. The pure `sameRowsInput` and `handoffRunsSignature` go to `MessagesTimeline.logic.ts`.

| File | Before | After |
|---|---:|---:|
| MessagesTimeline.tsx | 1,644 | 1,380 |

### Measurements

Dev app on a seeded copy of real data, median of 3, React renders counted by the commit-root counter (only fibers React actually rendered). "Before" is main at 6a6c84f990; "after" is the top of this stack. Nested update peak comes from a probe in react-dom's `getRootForUpdatedFiber`.

| Interaction | Before: commits / renders / timeline renders / nested peak | After |
|---|---|---|
| Scroll an 8.5k-item thread up 40 × 150px | 43 / 1,656 / 70 / 0 | 44 / 1,670 / 70 / 0 |
| Load earlier turns, then scroll up 60 × 300px | 107 / 5,316 / 356 / 0 | 104 / 5,275 / 356 / 0 |
| Agent streams a 20-line reply (561-item thread) | 63 / 3,092 / 164 / 1 | 55 / 2,728 / 139 / 0 |

The extractions keep every component, context, and hook boundary unchanged, so render counts match within run-to-run noise. Streaming numbers vary with reply length. The win is readability: each row family is now a file you can read on its own, and the React Compiler caches each one independently. The legend-list wiring is unchanged. That covers `getItemType` with its length buckets (#21269), `TimelineRowCtx` inside `renderItem` with `extraData` carrying `sharedState` (#21123), and scroll restore. No update loops appeared while loading earlier turns.

### Tests

- `vp test run` MessagesTimeline logic, active-tools, and browser tests, plus `timelineScrollAnchoring`
- `reactCompilerCoverage` for every touched and new file (0 bailouts)
- `tsc-rs --noEmit` for apps/web, `vp lint` on changed files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

