# PR #22930 evidence captures (controlled Forgejo fixture, matrix v2)

PR [#22930](https://github.com/pingdotgg/t3code/pull/22930) (GitCafe [#22927](https://git.cafe/pingdotgg/t3code/pulls/22927)), issue [#22886](https://github.com/pingdotgg/t3code/issues/22886). Matrix: [matrix-v2.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/5bcfd5bb569b82c2/matrix-v2.md).

## Environment

- Machine fs-17, Linux 6.1.102 x86_64. Playwright 1.60.0 headless Chromium 148.0.7778.96 (chromium_headless_shell-1223), DPR 1, viewports 1440×900 and 390×844, `colorScheme` emulated light/dark, videos recorded at viewport size.
- Forge: `codeberg.org/forgejo/forgejo:11` (image sha256:946243edbab1…, Forgejo 11.0.16), container `ev22930-forgejo` at `http://forgejo.127.0.0.1.nip.io:3000`. Stub `fj` (exits 0) and `XDG_DATA_HOME/forgejo-cli/keys.json` keyed `forgejo.127.0.0.1.nip.io:3000`.
- Fixture SHAs (fixed author/committer dates): main `65c1abc`, PR #1 L `review-l` = C1 `13ab638`, C2 `1d32c27`, C3 `6329e89` (8 files); PR #2 S `review-s` = `c34151c` (7 short files, 3 commits). C4 from `push-c4.sh` dated 2026-01-10.
- BEFORE = [8d3b061121](https://github.com/pingdotgg/t3code/commit/8d3b061121fe65669d5d43d011b48cd3027a4108) (worktree `/tmp/ev-22930/before`, ports 14804/6764). AFTER = [2b388c27fa](https://github.com/pingdotgg/t3code/commit/2b388c27faf4b44caac61730dfd47027ef22c4dd) (worktree `~/wt/pr-22930`, ports 14551/6511). Each server used its own `--home-dir /tmp/ev-22930/home-{before,after}`; no live userdata.
- Scripts commit: [8d5bc57aea](https://github.com/pingdotgg/t3code/commit/8d5bc57aea) on `fix/issue-22886` (`apps/web/scripts/evidence/pr-22930/`).
- Seed, identical for both builds: projects A (`review-a`, branch `review-l`) and B (`review-b`, `review-s`) via `t3 project add`; idle threads "Thread A"/"Thread B" via MCP `t3_thread_launch` with no message (no provider turn ran); PR #1→A and #2→B via MCP `link_pull_request`. The Code tab opened with all files collapsed in both builds (app default here, not changed).

## Method

The same `driver.mjs` ran on both builds. Prelude P: open Thread A → PR #1 → Code; Collapse all; expand `renamed.ts` then `long-b.ts`; mouse-wheel the viewer to scrollTop ≈3000; select C2 (both builds reset); re-apply the fold; wheel to ≈3000. Then each row's trigger. After every step the driver waits for network idle plus two animation frames, then logs scope, viewer `scrollTop`, the file headers in view with their fold state (the viewer virtualizes, so only rendered headers are listed), and the first visible diff line. Videos carry burned-in captions (build, row, step label, scrollTop, scope) plus elapsed seconds. Clip time 0 is context creation; the first ~30–60 s is the cold dev-server load. The provider "Update available" toast is hidden by an injected style in both builds because it covered the PR header.

Listed per row: the trigger and assertion steps (P3 and P6 give the baseline being restored). Full logs are in the events JSON.

## Rows


### R1 — new push (C4) on All commits, 1440×900 light

Status: **unverified (trigger did not land)**. Both builds pushed the same C4 `2d42869` with `git push`, then pressed header Refresh. Neither client showed the C4 commit within the 60 s wait, and both stayed on All commits, long-b.ts expanded, scrollTop 3000, first line longB_163. With no new revision in the UI, the new-push path was not exercised, so the clips show only that the two builds behave identically. Likely cause: the PR detail had not re-read Forgejo's updated head (the 60 s server cache, or Forgejo updating the PR asynchronously after a push); not investigated within budget.

| | Before | After |
|---|---|---|
| video | [R1-before.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/cc0bfe49515471f2/R1-before.mp4) | [R1-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/cde4cd9335f8e780/R1-after.mp4) |
| events | [R1-before.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/7b9a47e7bb76f6a0/R1-before.events.json) | [R1-after.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/665ca1a2533574cf/R1-after.events.json) |


**BEFORE** (trigger and assertions):
- t=36.61s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3000`, first line `export const longB_163 = "base 163";`, headers in view: long-b.ts:expanded
- t=37.5s **T push C4 2d42869 (git push)** — scope `All commits`, scrollTop `3000`, first line `export const longB_163 = "base 163";`, headers in view: long-b.ts:expanded
- t=102.45s **A after Refresh picked up C4** — scope `All commits`, scrollTop `3000`, first line `export const longB_163 = "base 163";`, headers in view: long-b.ts:expanded
- t=106.47s **A +4s at rest** — scope `All commits`, scrollTop `3000`, first line `export const longB_163 = "base 163";`, headers in view: long-b.ts:expanded

**AFTER** (trigger and assertions):
- t=26.47s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3000`, first line `export const longB_163 = "base 163";`, headers in view: long-b.ts:expanded
- t=27.39s **T push C4 2d42869 (git push)** — scope `All commits`, scrollTop `3000`, first line `export const longB_163 = "base 163";`, headers in view: long-b.ts:expanded
- t=92.53s **A after Refresh picked up C4** — scope `All commits`, scrollTop `3000`, first line `export const longB_163 = "base 163";`, headers in view: long-b.ts:expanded
- t=96.56s **A +4s at rest** — scope `All commits`, scrollTop `3000`, first line `export const longB_163 = "base 163";`, headers in view: long-b.ts:expanded

### R2 — external push + 61 s cache + focus, 1440×900 dark

Status: **unverified**. Not run: the 65 s host-cache wait per build did not fit the 75-minute budget after the dry run. R1 covers the new-push path through header Refresh instead.

| | Before | After |
|---|---|---|
| video | missing | missing |
| events | missing | missing |


**BEFORE** (trigger and assertions):


**AFTER** (trigger and assertions):


### R3 — background-only refresh from a second tab, 1440×900 light

Status: **produced (no-regression)**. Both builds keep C2, the fold and scrollTop 3000 after a Refresh in a second /pull-requests tab. Matches the expectation.

| | Before | After |
|---|---|---|
| video | [R3-before.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/b0b62c9218e77655/R3-before.mp4) | [R3-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/555a728caecdc849/R3-after.mp4) |
| events | [R3-before.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8185ea879ec000eb/R3-before.events.json) | [R3-after.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/820d83d64e1f7c9b/R3-after.events.json) |


**BEFORE** (trigger and assertions):
- t=49.31s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3000`, first line `export const longB_163 = "base 163";`, headers in view: long-b.ts:expanded
- t=59.32s **P6 scrolled to mid long-b.ts in C2** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded
- t=106.11s **T Refresh in a second tab (/pull-requests), background epoch only** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded
- t=110.21s **A +4s at rest** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded

**AFTER** (trigger and assertions):
- t=78.52s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3120`, first line `export const longB_168 = "base 168";`, headers in view: long-b.ts:expanded
- t=95.14s **P6 scrolled to mid long-b.ts in C2** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded
- t=243.53s **T Refresh in a second tab (/pull-requests), background epoch only** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded
- t=247.71s **A +4s at rest** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded

### R4 — explicit header Refresh, 390×844 dark

Status: **produced — expected BEFORE reset NOT reproduced**. BEFORE kept C2, the fold and scrollTop 3000 (first line longB_128). AFTER briefly unmounted the viewer (scrollTop null at the trigger step), then came back with C2 and the fold at scrollTop **3340** (first line longB_155): a +340 px drift from the saved position at narrow width. The matrix expected BEFORE to go to 0. That was not observed with an unchanged PR.

| | Before | After |
|---|---|---|
| video | [R4-before.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8b0d216d7bf1201b/R4-before.mp4) | [R4-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/51689a15ca1000f8/R4-after.mp4) |
| events | [R4-before.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/bda3620e073c35fe/R4-before.events.json) | [R4-after.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8c2f85df1a607e7a/R4-after.events.json) |


**BEFORE** (trigger and assertions):
- t=80.73s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3000`, first line `export const longB_127 = "base 127";`, headers in view: long-b.ts:expanded
- t=91.17s **P6 scrolled to mid long-b.ts in C2** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_128 = "base 128";`, headers in view: long-b.ts:expanded
- t=96.53s **T header menu Refresh** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_128 = "base 128";`, headers in view: long-b.ts:expanded
- t=100.61s **A +4s at rest** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_128 = "base 128";`, headers in view: long-b.ts:expanded

**AFTER** (trigger and assertions):
- t=80.59s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3000`, first line `export const longB_127 = "base 127";`, headers in view: long-b.ts:expanded
- t=91.64s **P6 scrolled to mid long-b.ts in C2** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_128 = "base 128";`, headers in view: long-b.ts:expanded
- t=95.53s **T header menu Refresh** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `None`, first line `None`, headers in view: -
- t=100.19s **A +4s at rest** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3340`, first line `export const longB_155 = "base 155";`, headers in view: long-b.ts:expanded

### R5 — Code → Summary/Timeline → Code, 1440×900 dark

Status: **produced (no-regression)**. Both builds preserve C2, the fold and scrollTop 3000 across both tab round trips. Guide is not offered by this server, so it was skipped in both.

| | Before | After |
|---|---|---|
| video | [R5-before.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/21e405653efda2b7/R5-before.mp4) | [R5-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/cff32b5ef3869fb7/R5-after.mp4) |
| events | [R5-before.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/81689b677c18cb60/R5-before.events.json) | [R5-after.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/35ef2ae8134d7692/R5-after.events.json) |


**BEFORE** (trigger and assertions):
- t=77.06s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3000`, first line `export const longB_163 = "base 163";`, headers in view: long-b.ts:expanded
- t=89.57s **P6 scrolled to mid long-b.ts in C2** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded
- t=94.89s **A Code -> Summary -> Code** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded
- t=100.51s **A Code -> Timeline -> Code** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded
- t=100.67s **T Guide tab not offered** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded

**AFTER** (trigger and assertions):
- t=70.78s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3000`, first line `export const longB_163 = "base 163";`, headers in view: long-b.ts:expanded
- t=82.72s **P6 scrolled to mid long-b.ts in C2** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded
- t=88.32s **A Code -> Summary -> Code** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded
- t=93.8s **A Code -> Timeline -> Code** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded
- t=93.89s **T Guide tab not offered** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded

### R6 — hide/show right panel; close and reopen PR surface, 1440×900 light

Status: **produced — bug reproduced and fixed**. BEFORE: after (a) hide/show and after (b) close/reopen, the Code tab is back on **All commits, every file collapsed, scrollTop 0**. AFTER: after both (a) and (b), **C2, long-b.ts expanded, scrollTop 3000, first line longB_164**, identical to P6. (BEFORE's P3/P6 wheel landed at 3400 instead of 3000.)

| | Before | After |
|---|---|---|
| video | [R6-before.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8a4c0981e017674b/R6-before.mp4) | [R6-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c7582c1428aa1a3a/R6-after.mp4) |
| events | [R6-before.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/eb6bcaf35b3066b0/R6-before.events.json) | [R6-after.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/2cb816c52b1e0463/R6-after.events.json) |


**BEFORE** (trigger and assertions):
- t=81.21s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3400`, first line `export const longB_184 = "base 184";`, headers in view: long-b.ts:expanded
- t=96.58s **P6 scrolled to mid long-b.ts in C2** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3400`, first line `export const longB_185 = "edited-c2 185";`, headers in view: long-b.ts:expanded
- t=108.09s **A (a) right panel hidden and shown** — scope `All commits`, scrollTop `0`, first line `None`, headers in view: long-a.ts:collapsed, long-b.ts:collapsed, long-c.md:collapsed, renamed.ts:collapsed
- t=115.19s **T (b) PR surface closed and reopened from the thread link** — scope ``, scrollTop `None`, first line `None`, headers in view: -
- t=119.52s **A (b) Code tab after reopen** — scope `All commits`, scrollTop `0`, first line `None`, headers in view: long-a.ts:collapsed, long-b.ts:collapsed, long-c.md:collapsed, renamed.ts:collapsed

**AFTER** (trigger and assertions):
- t=73.25s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3000`, first line `export const longB_163 = "base 163";`, headers in view: long-b.ts:expanded
- t=90.59s **P6 scrolled to mid long-b.ts in C2** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded
- t=102.45s **A (a) right panel hidden and shown** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded
- t=110.44s **T (b) PR surface closed and reopened from the thread link** — scope ``, scrollTop `None`, first line `None`, headers in view: -
- t=115.3s **A (b) Code tab after reopen** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_164 = "base 164";`, headers in view: long-b.ts:expanded

### R7 — switch to Thread B (PR #2) and back, 390×844 light

Status: **produced — bug reproduced and fixed**. BEFORE: back on A gives All commits, all collapsed, top 0; back on B gives All commits, all collapsed. AFTER: back on A gives **C2, long-b.ts expanded, scrollTop 3760** (P6 was 3400; +360 px drift, the same narrow-width overshoot as R4); back on B gives **S2: next two, s-4.txt expanded, s-5.txt collapsed**. No state leaked between threads. PR S fits the narrow viewport, so its bottom scroll is 0 and the clamp is trivially satisfied. The BEFORE clip is a rerun: its first attempt timed out reaching the sidebar under 4-browser load.

| | Before | After |
|---|---|---|
| video | [R7-before.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/65a0ae15ac95b5ac/R7-before.mp4) | [R7-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/942b1c679adeba31/R7-after.mp4) |
| events | [R7-before.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/71b7dc714bbb698a/R7-before.events.json) | [R7-after.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e40d8dcddf05d954/R7-after.events.json) |


**BEFORE** (trigger and assertions):
- t=41.19s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3000`, first line `export const longB_127 = "base 127";`, headers in view: long-b.ts:expanded
- t=49.24s **P6 scrolled to mid long-b.ts in C2** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3000`, first line `export const longB_128 = "base 128";`, headers in view: long-b.ts:expanded
- t=57.33s **B1 thread B, PR #2 Code tab** — scope `All commits`, scrollTop `0`, first line `None`, headers in view: s-1.txt:collapsed, s-2.txt:collapsed, s-3.txt:collapsed, s-4.txt:collapsed
- t=68.14s **B2 thread B: S2 selected, s-4 expanded, s-5 collapsed, scrolled to bottom** — scope `S2: next two`, scrollTop `0`, first line `s4 one`, headers in view: s-4.txt:expanded, s-5.txt:collapsed
- t=76.14s **A back to thread A** — scope `All commits`, scrollTop `0`, first line `None`, headers in view: long-a.ts:collapsed, long-b.ts:collapsed, long-c.md:collapsed, renamed.ts:collapsed
- t=85.56s **A back to thread B** — scope `All commits`, scrollTop `0`, first line `None`, headers in view: s-1.txt:collapsed, s-2.txt:collapsed, s-3.txt:collapsed, s-4.txt:collapsed

**AFTER** (trigger and assertions):
- t=129.58s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3000`, first line `export const longB_127 = "base 127";`, headers in view: long-b.ts:expanded
- t=145.79s **P6 scrolled to mid long-b.ts in C2** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3400`, first line `export const longB_142 = "base 142";`, headers in view: long-b.ts:expanded
- t=168.58s **B1 thread B, PR #2 Code tab** — scope `All commits`, scrollTop `0`, first line `None`, headers in view: s-1.txt:collapsed, s-2.txt:collapsed, s-3.txt:collapsed, s-4.txt:collapsed
- t=181.8s **B2 thread B: S2 selected, s-4 expanded, s-5 collapsed, scrolled to bottom** — scope `S2: next two`, scrollTop `0`, first line `s4 one`, headers in view: s-4.txt:expanded, s-5.txt:collapsed
- t=197.82s **A back to thread A** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `3760`, first line `export const longB_176 = "base 176";`, headers in view: long-b.ts:expanded
- t=218.4s **A back to thread B** — scope `S2: next two`, scrollTop `0`, first line `s4 one`, headers in view: s-4.txt:expanded, s-5.txt:collapsed

### R8 — select C2 → All commits → reload, 1440×900 dark

Status: **produced (no-regression, documents scope)**. Both builds reset fold and scroll on selecting C2, again on returning to All commits (the All-commits state from P2/P3 is not remembered), and on reload. Matches the expectation: the commit round-trip reset is by design in AFTER (`selectCodeCommit` writes EMPTY_CODE_VIEW).

| | Before | After |
|---|---|---|
| video | [R8-before.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/112c3d0b9ad5f6a5/R8-before.mp4) | [R8-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/bd26bc165e2a4d0d/R8-after.mp4) |
| events | [R8-before.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/8e13b38fe5d64ad5/R8-before.events.json) | [R8-after.events.json](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1516db7d1ad9c67e/R8-after.events.json) |


**BEFORE** (trigger and assertions):
- t=73.36s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3280`, first line `export const longB_177 = "base 177";`, headers in view: long-b.ts:expanded
- t=79.37s **T selected C2** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `0`, first line `None`, headers in view: long-b.ts:collapsed, long-c.md:collapsed, renamed.ts:collapsed, short-2.json:collapsed
- t=83.69s **A back to All commits** — scope `All commits`, scrollTop `0`, first line `None`, headers in view: long-a.ts:collapsed, long-b.ts:collapsed, long-c.md:collapsed, renamed.ts:collapsed
- t=132.7s **A after page reload** — scope `All commits`, scrollTop `0`, first line `None`, headers in view: long-a.ts:collapsed, long-b.ts:collapsed, long-c.md:collapsed, renamed.ts:collapsed

**AFTER** (trigger and assertions):
- t=74.56s **P3 scrolled to mid long-b.ts** — scope `All commits`, scrollTop `3000`, first line `export const longB_163 = "base 163";`, headers in view: long-b.ts:expanded
- t=79.43s **T selected C2** — scope `C2: edit long-b, rewrite long-c, rename`, scrollTop `0`, first line `None`, headers in view: long-b.ts:collapsed, long-c.md:collapsed, renamed.ts:collapsed, short-2.json:collapsed
- t=84.66s **A back to All commits** — scope `All commits`, scrollTop `0`, first line `None`, headers in view: long-a.ts:collapsed, long-b.ts:collapsed, long-c.md:collapsed, renamed.ts:collapsed
- t=138.65s **A after page reload** — scope `All commits`, scrollTop `0`, first line `None`, headers in view: long-a.ts:collapsed, long-b.ts:collapsed, long-c.md:collapsed, renamed.ts:collapsed

### S1 — at rest after Refresh, 1440×900 light + dark

Status: **produced — no visible difference**. Both builds at rest show C2, long-b.ts expanded, scrollTop 3000, first line longB_164. The diffs are only a few hundred pixels (relative timestamp / header chrome). The expected BEFORE-at-top outcome was not reproduced, consistent with R4.

- `S-1440-light`: Before [S-1440-light-before.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f85cbbc76cdcd62c/S-1440-light-before.png) | After [S-1440-light-after.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/b0118bb51e3935cd/S-1440-light-after.png) | abs diff [S-1440-light-diff-abs.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/aae71da3a7709ac3/S-1440-light-diff-abs.png) | overlay [S-1440-light-diff-overlay.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ca2bc594e115a89b/S-1440-light-diff-overlay.png) — changed px (AE 2% fuzz): 274
- `S-1440-dark`: Before [S-1440-dark-before.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/35e80f0721d8efad/S-1440-dark-before.png) | After [S-1440-dark-after.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/9d0cda2ccdde8211/S-1440-dark-after.png) | abs diff [S-1440-dark-diff-abs.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6200fb2474f2a872/S-1440-dark-diff-abs.png) | overlay [S-1440-dark-diff-overlay.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/a5c375699cde30ea/S-1440-dark-diff-overlay.png) — changed px (AE 2% fuzz): 309

### S2 — at rest after Refresh, 390×844 light + dark

Status: **produced — AFTER differs (drift)**. BEFORE at rest is at scrollTop 3000 (longB_128). AFTER is at **3340** (longB_155), so about 115k pixels differ. This is the same +340 px narrow-width restore drift as R4. It points to a possible AFTER regression rather than a fix.

- `S-390-light`: Before [S-390-light-before.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/20c93dfe740ec0de/S-390-light-before.png) | After [S-390-light-after.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/22bb94f5abf5c5d4/S-390-light-after.png) | abs diff [S-390-light-diff-abs.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6a984721e166c1c0/S-390-light-diff-abs.png) | overlay [S-390-light-diff-overlay.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/969f9a5b241669fe/S-390-light-diff-overlay.png) — changed px (AE 2% fuzz): 115010
- `S-390-dark`: Before [S-390-dark-before.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/591f41c2718d890a/S-390-dark-before.png) | After [S-390-dark-after.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/aba665b8bd27fc70/S-390-dark-after.png) | abs diff [S-390-dark-diff-abs.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/b0e86f99bc6ca910/S-390-dark-diff-abs.png) | overlay [S-390-dark-diff-overlay.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f1024f8eed5e6561/S-390-dark-diff-overlay.png) — changed px (AE 2% fuzz): 114999

## What else changed

`git diff --stat 8d3b061121 8d5bc57aea` (BEFORE → scripts commit):

```
 apps/web/scripts/evidence/pr-22930/* (11 files, evidence only, not shipped)
 .../PullRequestCodeTab.browser.test.tsx            |   1 +
 .../components/pullRequest/PullRequestCodeTab.tsx  | 256 +++++++---
 .../pullRequest/PullRequestDetailPanel.tsx         |  42 +-
 .../pullRequest/pullRequestCodeScroll.logic.ts     | 106 +++++
 .../pullRequest/pullRequestCodeViewState.test.ts   | 517 +++++++++++++++++++++
 .../pullRequest/pullRequestDiff.logic.ts           |  55 ++-
 .../pullRequest/pullRequestReviewStore.ts          |  54 +++
 .../pullRequest/pullRequestSelection.logic.ts      |  12 +
 19 files changed, 1738 insertions(+), 72 deletions(-)
```

Observed incidental differences:

1. **Narrow-width restore drift (AFTER only).** After an explicit Refresh at 390×844 (R4, S2) AFTER restores to 3340, not the saved 3000, and after a thread round trip (R7) to 3760, not 3400. At 1440×900 the restores were exact (R6, S1). BEFORE at 390 kept the original 3000 through Refresh. Wrapped lines at narrow width probably change row heights between the saved position and the remount. Worth a maintainer look.
2. AFTER unmounts the viewer momentarily on explicit Refresh (R4 trigger step: no headers, scrollTop null). BEFORE did not at that sample point.
3. The explicit-Refresh reset that the matrix predicted for BEFORE (scrollTop → 0) did not happen with an unchanged PR in R4 or S1. The fix's visible benefit in this run comes from the remount paths (R6, R7) and the new push (R1).
4. The wheel scroll sometimes overshot to 3400 instead of 3000 (BEFORE R6, AFTER R7). Each row compares against its own P6 value.

## Unverified / gaps

- **R2** not run (budget; see row).
- G1 multi-page diff restore, not exercisable with Forgejo (no pagination).
- The R1 "in-app Push" variant is not available: once the branch has a PR, the Git action offers Merge/Commit instead of Push. R1 uses `git push` from the shell plus header Refresh.
- Per-file fold state is asserted only for headers in the virtualized viewport at each step. Files scrolled out of the render window are not listed.

Media hosted with ghdrop. Logs, raw webm and events: `/tmp/ev-22930/{out,logs,media}` on fs-17.
