# perf\(web\): a streaming thread re-renders only its own sidebar row · PR \#22730 · pingdotgg/t3code

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

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

Visibility: public

State: merged

Source repository: pingdotgg/t3code

Source branch: perf/sidebar-rows-read-own-shell

Target branch: main

Head commit: bb231176c2a2fc535f3dd782244882bb0bd335ee

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

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

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

## Description

---

The sidebar body and `useSidebarProjectGroups` both subscribed to every thread shell. A running thread bumps its shell's `updatedAt` on every update, so each update re-rendered the whole sidebar: the header, the project-scope picker, every sortable row wrapper and every shelf marker. That happened even when the streaming thread was not the one on screen.

How this PR fixes it:
- **Quiet shell list.** A new `quietThreadShellsAtom` in client-runtime lists the existing per-thread `quietThreadShellAtom` values. A quiet shell stays the same while only streaming fields change (`updatedAt`, item counts, latest message), and the list keeps its identity with it. The web exposes it as `useQuietThreadShells`.
- **The sidebar reads the quiet list.** The sidebar's partition and `useSidebarProjectGroups` now read that list. Any real change (runtime, title, pin, settle, snooze, a new run) still produces a new shell, so sections and order still update.
- **Each row reads its own live shell.** `SortableThreadRow` and the search-result row read their thread's live shell, so a streaming thread re-renders only its own row: the relative time, the working state and so on.

One visible difference: with "sort projects by activity", the project order inside one running turn follows the turn's last real change, not every streaming tick. Turn start and turn end are real changes, so the project still moves up when the run starts.

**Measured** on a dev server seeded with `migrate-dev-db`: 9 visible rows, React Compiler build, render counter counting only fibers React rendered, median of 3 runs.

Thread A streams a reply while thread B is open, counted over 15 s:

| component renders | main | this PR |
|---|---|---|
| all sidebar components | 286 | 27 |
| `Sidebar` | 7 | 0 |
| `SortableThreadRow` | 63 | 2 |
| `SidebarThreadRow` (memo) | 6 | 2 |
| `SortableSidebarMarker` | 42 | 0 |

Switching threads (4 clicks): 363 → 364 sidebar component renders, `Sidebar` 14 → 13, `SidebarThreadRow` 10 → 14. That is unchanged within noise, which is expected: a route change is a real change for the sidebar.

Tests run:
- `threadShell.test.ts`, including a new test: the quiet list keeps its identity through a streaming tick and follows a rename.
- React Compiler coverage for the sidebar and entities files (0 bailouts).
- `entities.relationshipShells.test.ts`
- `Sidebar.logic.test.ts`
- `tsc` for apps/web and client-runtime
- oxlint on the changed files

Claude Opus 5.5 in Claude Code.

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

