# refactor\(web\): timeline user message rows and context chips live in their own files · PR \#22676 · pingdotgg/t3code

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

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

Visibility: public

State: merged

Source repository: pingdotgg/t3code

Source branch: refactor/messages-timeline-user-message-rows

Target branch: refactor/messages-timeline-work-group-rows

Head commit: 2ff9ebcfd1b473f4b058cfd0499186eb0ead8f35

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

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

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

## Description

---

Part of the MessagesTimeline split. This layer moves the user message row, its collapsible body, the revert button and video attachments into `TimelineUserMessageRow.tsx`. The context-chip presentation registry and its chips go to `UserMessageContextChips.tsx`, and `resolvePreviewAnnotationImage` goes to `TimelineUserMessageRow.logic.ts`.

| File | Before | After |
|---|---:|---:|
| MessagesTimeline.tsx | 3,653 | 2,436 |
| TimelineUserMessageRow.tsx | – | 619 |
| UserMessageContextChips.tsx | – | 592 |

### 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)

