diff --git a/docs/agent-runs/pretext-feed-sizing/feature-list.json b/docs/agent-runs/pretext-feed-sizing/feature-list.json
new file mode 100644
index 00000000..cfb0fb1c
--- /dev/null
+++ b/docs/agent-runs/pretext-feed-sizing/feature-list.json
@@ -0,0 +1,78 @@
+{
+ "task": "pretext-feed-sizing",
+ "last_updated": "2026-04-02",
+ "items": [
+ {
+ "id": "F001",
+ "priority": 1,
+ "status": "completed",
+ "description": "Benchmark and harden Pretext-backed reply virtualization on the real thread reply Virtuoso path so the branch has trustworthy baseline data.",
+ "verification": [
+ "Open `?e2e=pretext-benchmark&variant=production&mode=dom&count=1200&seed=42` and run the scroll scenario on desktop and mobile.",
+ "Open `?e2e=pretext-benchmark&variant=production&mode=item-size&count=1200&seed=42` and compare rect calls, duration, long tasks, and visible estimate error."
+ ],
+ "files": [
+ "src/components/post-desktop/post-desktop.tsx",
+ "src/components/post-mobile/post-mobile.tsx",
+ "src/e2e/pretext-benchmark-harness.tsx",
+ "src/hooks/use-reply-height-estimates.ts",
+ "src/lib/utils/pretext-height-estimates.ts",
+ "src/views/post/post.tsx"
+ ],
+ "notes": "Reply virtualization is now production-credible on both desktop and mobile. Desktop production harness: `dom` -> `item-size` reduced rect reads from 2875 to 472 while keeping visible error low (`11.3px` mean, `30px` max). Mobile production harness: `dom` -> `item-size` reduced rect reads from 5059 to 1433, duration from 16863ms to 13452ms, max frame from 100.1ms to 59ms, and slow frames over 16ms from 227 to 99. The mobile follow-up audit sampled all 1200 replies and found `722411px` estimated vs `726218px` actual total height, so the Pretext reply tree is within about `0.5%` of the rendered total."
+ },
+ {
+ "id": "F002",
+ "priority": 2,
+ "status": "completed",
+ "description": "Add a benchmarkable Pretext experiment for board feed Virtuoso items and determine whether feed cards are a better production target than thread replies.",
+ "verification": [
+ "Run a deterministic board-feed benchmark on `board.tsx` item virtualization for `dom`, `estimates`, and `item-size` modes.",
+ "Confirm whether item measurement reads and frame metrics improve materially on desktop and mobile."
+ ],
+ "files": [
+ "src/lib/utils/pretext-height-estimates.ts",
+ "src/hooks/use-reply-height-estimates.ts",
+ "src/views/board/board.tsx",
+ "src/views/post/post.tsx",
+ "src/components/post-desktop/post-desktop.tsx",
+ "src/components/post-mobile/post-mobile.tsx",
+ "src/e2e/pretext-benchmark-harness.tsx"
+ ],
+ "notes": "The board path was the hardest rollout surface, but it is now in a shippable state. Earlier live profiling found multiple board issues that are now addressed: desktop feed cards were still subtracting `14px` per preview reply even after the preview-reply estimator itself had become accurate, board preview replies were still rendering under `content-visibility:auto` while the feed card relied on Pretext sizing, the branch had lost `master` commit `99c0bbfac perf(board): reduce mobile reverse-scroll jank`, the mobile board-card preview calibration was massively over-subtracting once preview replies were present, and mobile multiboard `off` mode was still passing `itemSize={undefined}`, which overrides React Virtuoso's default DOM measurer. Board and catalog now only pass `itemSize` when explicit `item-size` mode is active, so the fallback path measures correctly again. Final matched live mobile A/B probes show the worktree no longer trails master on `/all`, and `/biz` keeps the Pretext sizing path active while matching the same visible card heights and total scroll height as master."
+ },
+ {
+ "id": "F003",
+ "priority": 3,
+ "status": "completed",
+ "description": "Evaluate catalog virtualization for a Pretext-backed sizing strategy if board feed results are promising or if catalog-specific card layout needs a separate model.",
+ "verification": [
+ "Benchmark catalog Virtuoso rows under the same `dom` vs Pretext sizing modes.",
+ "Verify desktop and mobile catalog scrolling in a fresh browser session."
+ ],
+ "files": [
+ "src/views/catalog/catalog.tsx",
+ "src/components/catalog-row/catalog-row.tsx",
+ "src/e2e/pretext-benchmark-harness.tsx"
+ ],
+ "notes": "Catalog rows now use a dedicated Pretext estimator plus `item-size` by default, with pure derived matched-filter colors instead of render-time store writes. Benchmark pass: desktop catalog reduced rect reads from 439 to 73 with a small duration win (2272ms -> 2010ms), and mobile reduced rect reads from 1349 to 217 with comparable scroll timing."
+ },
+ {
+ "id": "F004",
+ "priority": 4,
+ "status": "completed",
+ "description": "Prepare a production-safe rollout plan once a Pretext path shows a clear performance win, including feature-flag strategy, regression checks, and default-mode decisions.",
+ "verification": [
+ "Run `corepack yarn build`, `corepack yarn lint`, `corepack yarn type-check`, `corepack yarn doctor`, and the relevant browser benchmark flows.",
+ "Document whether the winning mode should remain opt-in, ship mobile-first, or replace DOM sizing by default."
+ ],
+ "files": [
+ "src/lib/utils/pretext-height-estimates.ts",
+ "src/hooks/use-reply-height-estimates.ts",
+ "src/views/board/board.tsx",
+ "src/views/catalog/catalog.tsx"
+ ],
+ "notes": "The rollout decision is now clear. The current verification set passes (`corepack yarn build`, `corepack yarn lint` with existing warnings only, `corepack yarn type-check`, `corepack yarn doctor` at the repo baseline `86/100`, and `corepack yarn test --run`), deterministic benchmark matrices still show the Pretext wins on catalog/replies/board, and the last board-specific fallback bug on mobile multiboard feeds is fixed. Final live A/B on the RPC-backed mobile `/all` sessions shows the worktree at parity or slightly better than master on the same visible cards, while `/biz` keeps the Pretext path active without the earlier stutter regression. The retained query flags (`pretextFeed`, `pretextCatalog`, `pretextReplies`) still provide an escape hatch, but the branch itself is ready for merge review."
+ }
+ ]
+}
diff --git a/docs/agent-runs/pretext-feed-sizing/progress.md b/docs/agent-runs/pretext-feed-sizing/progress.md
new file mode 100644
index 00000000..83b34b48
--- /dev/null
+++ b/docs/agent-runs/pretext-feed-sizing/progress.md
@@ -0,0 +1,111 @@
+# Progress Log
+
+Append one entry per session.
+
+## 2026-03-30 15:07
+
+- Item: F001
+- Summary: Built a production-path benchmark harness for real thread replies, fixed multiple estimator mismatches, and collected final desktop/mobile `dom` vs `item-size` measurements.
+- Files: `src/components/post-desktop/post-desktop.tsx`, `src/components/post-mobile/post-mobile.tsx`, `src/e2e/pretext-benchmark-harness.tsx`, `src/hooks/use-reply-height-estimates.ts`, `src/lib/utils/pretext-height-estimates.ts`, `src/views/post/post.tsx`
+- Verification: `./scripts/agent-init.sh --smoke`, `corepack yarn build`, `corepack yarn lint`, `corepack yarn type-check`, `corepack yarn doctor`, `corepack yarn test --run src/lib/utils/__tests__/pretext-height-estimates.test.ts src/components/__tests__/post-community-address-compat.test.tsx`, Playwright benchmark runs on desktop and mobile for `dom`, `estimates`, and `item-size`
+- Blockers: Reply virtualization now reduces DOM measurement reads substantially, but the user-visible performance gain is only modest on desktop and still not accurate enough to call mobile safe by default.
+- Next: Start F002 by wiring a benchmarkable board-feed path and compare `dom` vs Pretext sizing on feed cards rather than thread replies.
+
+## 2026-03-30 16:00
+
+- Item: F002
+- Summary: Added a board-feed benchmark surface, calibrated feed-card estimates from sampled DOM error, and wired the real board Virtuoso behind `pretextFeed=item-size` with hash-router query parsing support.
+- Files: `src/components/post-desktop/post-desktop.tsx`, `src/components/post-mobile/post-mobile.tsx`, `src/e2e/pretext-benchmark-harness.tsx`, `src/hooks/use-reply-height-estimates.ts`, `src/lib/utils/__tests__/pretext-height-estimates.test.ts`, `src/lib/utils/pretext-height-estimates.ts`, `src/views/board/board.tsx`, `src/views/post/post.tsx`
+- Verification: `corepack yarn type-check`, `corepack yarn lint`, `corepack yarn build`, `corepack yarn doctor`, `corepack yarn test --run src/lib/utils/__tests__/pretext-height-estimates.test.ts src/components/__tests__/post-community-address-compat.test.tsx`, Playwright board benchmark runs on desktop/mobile for `dom`, `estimates`, and `item-size`, real-route smoke test on `#/mu?pretextFeed=item-size`
+- Blockers: The synthetic board benchmark is now clearly better than the reply path, but the live `/mu` route only loaded about 15 cards in this dev environment, so the production-route perf comparison is still too small and noisy to justify default-on rollout.
+- Next: Continue F004 by profiling larger live board feeds with `pretextFeed=item-size` and decide whether board feed can ship default-on, mobile-first, or stay flag-gated while catalog/reply work continues.
+
+## 2026-03-30 17:08
+
+- Item: F001, F003, F004
+- Summary: Finished catalog row sizing, removed catalog matched-filter render-time store writes, calibrated thread-reply estimates for the real Virtuoso path, extended the benchmark harness to cover catalog, and flipped board/catalog/reply virtualization defaults to `item-size`.
+- Files: `docs/agent-runs/pretext-feed-sizing/feature-list.json`, `src/components/catalog-row/__tests__/catalog-row.test.tsx`, `src/components/catalog-row/catalog-row.tsx`, `src/e2e/pretext-benchmark-harness.tsx`, `src/hooks/use-reply-height-estimates.ts`, `src/lib/utils/__tests__/pretext-height-estimates.test.ts`, `src/lib/utils/pretext-height-estimates.ts`, `src/views/board/board.tsx`, `src/views/catalog/__tests__/catalog.test.tsx`, `src/views/catalog/catalog.tsx`
+- Verification: `corepack yarn type-check`, `corepack yarn test --run`, `corepack yarn build`, `corepack yarn lint` (existing warnings only), `corepack yarn doctor` (baseline `86/100`), `corepack yarn knip`, Playwright benchmark matrices for board/catalog/replies on desktop and mobile, live-route smoke checks on `#/mu`, `#/mu/catalog`, and `#/mu/thread/QmWSMcz1TirxAcVJ9qKr8jygttVbXZ3bZ9mMpZyek5iu3n`
+- Blockers: Live-route perf deltas remain hard to observe in this dev environment because feeds are shallow and RPC noise is high, but the production-path benchmark harness now shows clear wins on all three surfaces and the real routes render correctly with the new defaults.
+- Next: Keep the URL overrides (`?pretextFeed=off`, `?pretextCatalog=off`, `?pretextReplies=off`) available for rollback while gathering post-merge feedback on deeper live boards and threads.
+
+## 2026-03-30 18:10
+
+- Item: F002, F004
+- Summary: Reproduced the user's `/all` board-feed regression against cached live data from a copied Helium Chromium profile, identified that reply-preview cards were massively overestimated in the strict board `item-size` path, and switched the board rollout to a hybrid strategy that only keeps cached Pretext heights on simple cards without preview replies.
+- Files: `src/components/__tests__/post-community-address-compat.test.tsx`, `src/components/post-desktop/post-desktop.tsx`, `src/components/post-mobile/post-mobile.tsx`, `src/lib/utils/pretext-height-estimates.ts`
+- Verification: `corepack yarn test --run`, `corepack yarn build`, `corepack yarn lint` (existing warnings only), `corepack yarn type-check`, `corepack yarn doctor` (baseline `86/100`), Helium-profile Playwright probes on `#/biz` and `#/all` for master vs worktree plus desktop/mobile geometry checks on the worktree
+- Blockers: Catalog remains the only clearly dramatic live win. Board feeds are now layout-safe and at least slightly smoother, but the hybrid fallback means the board path is a correctness-first improvement rather than a large DOM-read reduction.
+- Next: Keep catalog and replies on the current Pretext default path, and continue iterating on a lower-error preview-reply estimator if we want a bigger board-feed win than the new hybrid mode provides.
+
+## 2026-03-30 19:10
+
+- Item: F001, F002, F004
+- Summary: Removed the board-feed fallback again, separated preview-reply sizing from thread-reply sizing, preserved real line breaks in Pretext input, recalibrated desktop thread replies, compressed the `itemSize` lookup path, and reran deterministic desktop/mobile benchmark matrices. The board surface is now strict Pretext again with close geometry, desktop replies are materially better, and mobile replies remain the last blocker.
+- Files: `docs/agent-runs/pretext-feed-sizing/feature-list.json`, `src/components/__tests__/post-community-address-compat.test.tsx`, `src/components/post-desktop/post-desktop.tsx`, `src/components/post-mobile/post-mobile.tsx`, `src/hooks/use-reply-height-estimates.ts`, `src/lib/utils/pretext-height-estimates.ts`, `src/views/board/board.tsx`
+- Verification: `corepack yarn build`, `corepack yarn lint` (existing warnings only), `corepack yarn type-check`, `corepack yarn doctor` (`85/100`, one point below the earlier baseline because the repo still carries existing React Doctor findings), `corepack yarn test --run src/lib/utils/__tests__/pretext-height-estimates.test.ts src/components/__tests__/post-community-address-compat.test.tsx src/views/board/__tests__/board.test.tsx`, deterministic Playwright benchmark runs on desktop and mobile for board/catalog/replies `dom` vs `item-size`
+- Blockers: Mobile thread replies still have a bad aggregate scroll-height estimate in the benchmark harness even after the latest calibration passes. Desktop board sizing is now accurate enough, but its deterministic benchmark is measurement-cheaper rather than obviously faster, so the strongest “clear win” story remains catalog, mobile board, and desktop replies.
+- Next: Either keep iterating specifically on the mobile thread reply estimator or stop forcing `item-size` on that one surface while shipping the surfaces that are already clearly better.
+
+## 2026-03-31 15:45
+
+- Item: F001, F004
+- Summary: Re-profiled mobile thread replies, replaced the broken blanket `-350px` mobile thread calibration with a feature-based mobile thread model, and disabled `content-visibility:auto` on the virtualized reply roots only when the Pretext `item-size` path is active. The key follow-up was a row-by-row mobile audit across the full 1200-reply benchmark: the sampled `item-size` reply tree sums to `722411px` estimated vs `726218px` actual, so the Pretext reply sizing is now within about `0.5%` of the rendered total even though the old DOM Virtuoso path still reports a much smaller overall scroll height.
+- Files: `docs/agent-runs/pretext-feed-sizing/feature-list.json`, `src/components/post-desktop/post-desktop.tsx`, `src/components/post-mobile/post-mobile.tsx`, `src/lib/utils/pretext-height-estimates.ts`, `src/views/post/post.module.css`
+- Verification: `corepack yarn type-check`, `corepack yarn lint` (existing warnings only), `corepack yarn test --run src/lib/utils/__tests__/pretext-height-estimates.test.ts src/components/__tests__/post-community-address-compat.test.tsx src/views/board/__tests__/board.test.tsx`, `corepack yarn build`, `corepack yarn doctor` (`86/100`, repo baseline), `corepack yarn knip`, Playwright production-path reply benchmarks on mobile and desktop plus row-level mobile geometry sampling
+- Blockers: Live deep-thread route verification is still limited by RPC/cached-feed availability in this dev environment, so the strongest proof remains the deterministic production-path harness rather than a real cached thread with thousands of replies.
+- Next: If the user wants to ship, prepare the branch for commit/PR with the rollback query flags kept in place (`?pretextFeed=off`, `?pretextCatalog=off`, `?pretextReplies=off`) and do one more live-route smoke pass when cached live data is available.
+
+## 2026-04-02 16:48
+
+- Item: F002, F004
+- Summary: Tightened the desktop board-feed height model by matching the actual OP body render path more closely. The board estimator now adds the desktop blockquote padding, models the board-view truncated-comment notice, exposes an explicit multiboard board-label height hook for `/all`-style feeds, and applies a small desktop preview-reply calibration instead of treating preview replies like full thread rows. That moved the board benchmark from “measurement-cheaper but visibly off” to near-parity runtime with much lower DOM reads.
+- Files: `docs/agent-runs/pretext-feed-sizing/feature-list.json`, `src/components/post-desktop/post-desktop.tsx`, `src/lib/utils/__tests__/pretext-height-estimates.test.ts`, `src/lib/utils/pretext-height-estimates.ts`
+- Verification: `corepack yarn type-check`, `corepack yarn test --run src/lib/utils/__tests__/pretext-height-estimates.test.ts`, `corepack yarn build`, `corepack yarn lint` (existing warnings only), `corepack yarn doctor` (`86/100`, repo baseline), `corepack yarn test --run`, Playwright desktop board benchmark at `http://127.0.0.1:1356/?e2e=pretext-benchmark&surface=board&variant=production&count=800&seed=42&mode=dom` and `mode=item-size`, fresh-route Playwright smoke on `#/all` and `#/biz`
+- Blockers: Fresh browser route smoke still cannot validate deep live `/all` content because RPC/cached-feed availability is inconsistent in this environment, and `./scripts/agent-init.sh --smoke` is not yet worktree-aware because it waits for the canonical `http://5chan.localhost:1355` URL instead of the branch-scoped Portless route.
+- Next: Re-test cached live `/all` and `/biz` against the current branch to confirm the board-feed geometry feels better in real browsing, then decide whether the board path is finally strong enough to merge along with catalog/replies.
+
+## 2026-04-02 17:23
+
+- Item: F002, F004
+- Summary: Reprofiled live `/all` with the private Node RPC, found that desktop board cards were still underestimating almost exactly `14px` per preview reply, removed that stale feed-level subtraction, and added a regression test so desktop feed cards now add the preview reply estimates directly. The live result changed materially: a 99-card `/all` sample went from `39.7px` mean absolute error before the fix to `7.1px` after the fix, with the controlled board benchmark regaining a cleaner `item-size` win.
+- Files: `docs/agent-runs/pretext-feed-sizing/feature-list.json`, `src/lib/utils/__tests__/pretext-height-estimates.test.ts`, `src/lib/utils/pretext-height-estimates.ts`
+- Verification: `corepack yarn type-check`, `corepack yarn build`, `corepack yarn lint` (existing warnings only), `corepack yarn doctor` (`86/100`, repo baseline), `corepack yarn test --run src/lib/utils/__tests__/pretext-height-estimates.test.ts`, `corepack yarn test --run`, live Playwright `/all` sampling on `http://codex-feature-pretext-feed-sizing.5chan.localhost:1355/#/all`, live same-branch A/B on `#/all` vs `#/all?pretextFeed=off`, controlled Playwright board benchmark on `?e2e=pretext-benchmark&surface=board&variant=production&count=800&seed=42&mode=dom` and `mode=item-size`
+- Blockers: Live `/all` now looks much healthier, but the route-level perf delta versus `pretextFeed=off` is still modest/noisy rather than a dramatic blowout, and the master-origin comparison is not trustworthy yet because the master session is not loading the same feed depth as the worktree even after the settings pass.
+- Next: Do another user-driven real-feed pass on `/all` and `/biz` with the new desktop board fix in place, then decide whether the remaining work should focus on squeezing a larger live-route win or on preparing the branch for merge with the current measurable improvement.
+
+## 2026-04-02 17:27
+
+- Item: F002, F004
+- Summary: Confirmed that the remaining single-board feed glitching was not primarily a bad Pretext formula. Desktop board preview replies were still rendering under `content-visibility:auto` with the `120px` intrinsic placeholder while the feed card itself was relying on Pretext sizing. I disabled deferred layout for board preview replies when the feed estimate path is active, added dev-only preview reply audit attributes on desktop, and reran live `/biz` sampling. That change dropped `/biz` from `52px` mean absolute feed-card error to `6.7px`.
+- Files: `docs/agent-runs/pretext-feed-sizing/feature-list.json`, `src/components/post-desktop/post-desktop.tsx`, `src/components/post-mobile/post-mobile.tsx`
+- Verification: `corepack yarn type-check`, `corepack yarn build`, `corepack yarn lint` (existing warnings only), `corepack yarn doctor` (`86/100`, repo baseline), `corepack yarn test --run src/lib/utils/__tests__/pretext-height-estimates.test.ts`, `corepack yarn test --run`, live Playwright `/biz` sampling before and after the deferred-layout fix on `http://codex-feature-pretext-feed-sizing.5chan.localhost:1355/#/biz`
+- Blockers: The branch is now much closer to “merge-ready on real feeds,” but the strongest user-facing proof is still the live geometry/jank sampling plus the deterministic harness rather than a clean master-origin A/B, because the master session is still not consuming the same feed depth reliably.
+- Next: Do one more human-eye browsing pass on `/all`, `/biz`, and catalog with the current branch. If the route now feels consistently better than production, prepare the branch for merge with the rollback query flags retained.
+
+## 2026-04-02 18:02
+
+- Item: F002, F004
+- Summary: Tracked the mobile `/all` stutter to two separate regressions. First, the Pretext worktree was missing `master` commit `99c0bbfac perf(board): reduce mobile reverse-scroll jank`, so `board.tsx` was still snapshotting Virtuoso state on every `scroll` tick and `comment-media.tsx` was double-running the GIF first-frame hook per card. After porting that, live mobile `/all` audits still showed huge feed-card misses on preview-heavy media posts, so I reprofiled the live cards and found the mobile board preview calibration was stale: cards with five preview replies were underestimating by `+371px` / `+367.9px`. I replaced the old aggressive polynomial calibration with a bounded per-preview-count table, then skipped the full thread-reply estimate hook on board previews and memoized the expensive derived reply maps. That collapsed the live mobile `/all` mean absolute feed-card error from about `135px` to about `18.8px` and pulled the reverse-scroll probe much closer to master.
+- Files: `docs/agent-runs/pretext-feed-sizing/feature-list.json`, `src/components/comment-media/comment-media.tsx`, `src/components/post-desktop/post-desktop.tsx`, `src/components/post-mobile/post-mobile.tsx`, `src/hooks/use-reply-height-estimates.ts`, `src/lib/utils/__tests__/pretext-height-estimates.test.ts`, `src/lib/utils/pretext-height-estimates.ts`, `src/views/board/board.tsx`
+- Verification: `corepack yarn type-check`, `corepack yarn test --run src/lib/utils/__tests__/pretext-height-estimates.test.ts src/components/__tests__/post-community-address-compat.test.tsx src/views/board/__tests__/board.test.tsx`, `corepack yarn build`, `corepack yarn lint` (existing warnings only), `corepack yarn doctor` (`86/100`, repo baseline), live Playwright mobile `/all` geometry audit on `http://codex-feature-pretext-feed-sizing.5chan.localhost:1355/#/all`, live mobile reverse-scroll probe on `http://codex-feature-pretext-feed-sizing.5chan.localhost:1355/#/all`, and matched master control probe on `http://5chan.localhost:1355/#/all`
+- Blockers: Mobile `/all` is no longer catastrophically wrong, but the worktree still trails the current master control in the dev reverse-scroll probe. Latest matched sample: worktree `seenCount=23`, `scrollHeight=18209`, `maxFrameMs=133.3`, `slowFrames16=11`, `slowFrames32=9`; master `seenCount=23`, `scrollHeight=17735`, `maxFrameMs=108.3`, `slowFrames16=8`, `slowFrames32=6`.
+- Next: Keep tuning the mobile board path until the reverse-scroll probe and human-eye feel are at least on par with master, likely by profiling rerender/commit churn on mobile `/all` now that the geometry bug is fixed.
+
+## 2026-04-02 18:40
+
+- Item: F002, F004
+- Summary: Found the remaining mobile `/all` board regression in the board-level Virtuoso prop wiring rather than the Pretext formula. In `off` mode the worktree was still rendering ``, which overrides React Virtuoso's internal default DOM measurer. That left mobile multiboard cards stuck on the `defaultItemHeight` fallback (`420px`) instead of reconciling to real DOM sizes, which explained the bad `data-known-size` values, wrong scroll height, and the remaining stutter. I changed board and catalog to only pass `itemSize` when the explicit `item-size` mode is active.
+- Files: `docs/agent-runs/pretext-feed-sizing/feature-list.json`, `src/views/board/board.tsx`, `src/views/catalog/catalog.tsx`
+- Verification: `corepack yarn type-check`, `corepack yarn build`, `corepack yarn lint` (existing warnings only), `corepack yarn doctor` (`86/100`, repo baseline), `corepack yarn test --run src/views/board/__tests__/board.test.tsx src/lib/utils/__tests__/pretext-height-estimates.test.ts`, fresh Playwright mobile `/all` load on `http://codex-feature-pretext-feed-sizing.5chan.localhost:1355/#/all`, and persistent-session Playwright checks on `http://codex-feature-pretext-feed-sizing.5chan.localhost:1355/#/all`
+- Blockers: The biggest correctness bug is fixed, but the live mobile `/all` scroll probe in this noisy dev environment is still not a clean proof of parity versus master. The good signal is that board cards now reconcile correctly again: on the persistent RPC-backed session the first mobile `/all` card went from stale `data-known-size=420` before the fix to `data-known-size=413` after the fix, matching the real measured height.
+- Next: Re-run human-eye A/B on mobile `/all` and `/biz` against master. If the feel issue is gone, the branch is much closer to merge-ready; if not, the remaining work is runtime churn rather than height reconciliation.
+
+## 2026-04-02 18:46
+
+- Item: F002, F004
+- Summary: Re-ran matched live mobile A/B probes against `master` after the Virtuoso prop fix and confirmed the branch is no longer trailing on the board routes that mattered. On the persistent RPC-backed `/all` sessions, the worktree now renders the same visible card heights as `master` while posting slightly better reverse-scroll numbers in the probe (`maxFrameMs 83.3` vs `99.1`, `slowFrames16 6` vs `7`, `slowFrames32 2` vs `3`). On `/biz`, the branch keeps the Pretext path active (`data-pretext-height` nodes present) while matching the same visible card heights and total scroll height as `master`; probe timing is now in the same range instead of the clear regression the user saw earlier. I also added an inline comment in board/catalog so the `itemSize={undefined}` pitfall is documented in code.
+- Files: `docs/agent-runs/pretext-feed-sizing/feature-list.json`, `src/views/board/board.tsx`, `src/views/catalog/catalog.tsx`
+- Verification: `corepack yarn type-check`, `corepack yarn build`, `corepack yarn lint` (existing warnings only), `corepack yarn doctor` (`86/100`, repo baseline), `corepack yarn test --run`, matched Playwright mobile A/B on `http://codex-feature-pretext-feed-sizing.5chan.localhost:1355/#/all` vs `http://5chan.localhost:1355/#/all`, matched Playwright mobile A/B on `http://codex-feature-pretext-feed-sizing.5chan.localhost:1355/#/biz` vs `http://5chan.localhost:1355/#/biz`, and catalog smoke on both origins at `#/all/catalog`
+- Blockers: None in the implementation itself. Remaining work before merge is review/commit/PR hygiene, plus the follow-up GitHub issue about future custom virtualization once this branch lands.
+- Next: Prepare the branch for merge, keep `FeedCacheContainer`, and open the deferred “Pretext unlocks custom virtualization” issue only after the Pretext rollout is merged into `master`.
diff --git a/package.json b/package.json
index bd1f341d..90ccfcb5 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"@capacitor/app": "7.0.1",
"@capacitor/status-bar": "7.0.1",
"@capawesome/capacitor-android-edge-to-edge-support": "7.2.2",
+ "@chenglou/pretext": "0.0.3",
"@floating-ui/react": "0.26.1",
"@react-spring/web": "10.0.3",
"@types/node": "20.19.37",
diff --git a/src/components/__tests__/post-community-address-compat.test.tsx b/src/components/__tests__/post-community-address-compat.test.tsx
index 2d7721f8..964ff64c 100644
--- a/src/components/__tests__/post-community-address-compat.test.tsx
+++ b/src/components/__tests__/post-community-address-compat.test.tsx
@@ -45,9 +45,11 @@ type TestComment = {
const testState = vi.hoisted(() => ({
addChallengeMock: vi.fn(),
+ hasMoreReplies: false,
openReplyModalMock: vi.fn(),
replyComments: [] as Array,
setResetFunctionMock: vi.fn(),
+ virtuosoProps: [] as Array<{ defaultItemHeight?: number; heightEstimates?: number[]; itemSize?: unknown }>,
}));
const getMockPreloadedReplies = (comment?: TestComment, sortType?: string) => {
@@ -89,7 +91,7 @@ vi.mock('@bitsocialnet/bitsocial-react-hooks', () => ({
useReplies: ({ comment, sortType }: { comment?: TestComment; sortType?: string }) => {
testState.replyComments.push(comment);
return {
- hasMore: false,
+ hasMore: testState.hasMoreReplies,
loadMore: vi.fn(),
replies: getMockPreloadedReplies(comment, sortType),
};
@@ -102,14 +104,22 @@ vi.mock('react-virtuoso', () => ({
{
components,
data = [],
+ defaultItemHeight,
+ heightEstimates,
+ itemSize,
itemContent,
}: {
components?: { Footer?: React.ComponentType };
data?: TestComment[];
+ defaultItemHeight?: number;
+ heightEstimates?: number[];
+ itemSize?: unknown;
itemContent: (index: number, item: TestComment) => React.ReactNode;
},
ref: React.ForwardedRef<{ getState: (cb: (snapshot: { ranges: number[]; scrollTop: number }) => void) => void }>,
) => {
+ testState.virtuosoProps.push({ defaultItemHeight, heightEstimates, itemSize });
+
React.useImperativeHandle(ref, () => ({
getState: (cb) => cb({ ranges: [0], scrollTop: 0 }),
}));
@@ -315,6 +325,14 @@ vi.mock('../../hooks/use-fresh-replies', () => ({
default: (replies: TestComment[]) => replies,
}));
+vi.mock('../../hooks/use-reply-height-estimates', () => ({
+ default: ({ isMobile, replies }: { isMobile: boolean; replies: TestComment[] }) => ({
+ defaultItemHeight: isMobile ? 222 : 111,
+ heightEstimates: replies.map((_, index) => (isMobile ? 200 : 100) + index),
+ itemSize: vi.fn(),
+ }),
+}));
+
vi.mock('../../lib/constants', () => ({
BOARD_REPLIES_PREVIEW_FETCH_SIZE: 5,
BOARD_REPLIES_PREVIEW_VISIBLE_COUNT: 3,
@@ -406,10 +424,24 @@ const makeLegacyThread = (): TestComment => ({
timestamp: 1_710_000_000,
});
+const makeLegacyThreadWithoutReplies = (): TestComment => ({
+ ...makeLegacyThread(),
+ replyCount: 0,
+ replies: {
+ pages: {
+ new: {
+ comments: [],
+ },
+ },
+ },
+});
+
describe('post community address compatibility', () => {
beforeEach(() => {
vi.clearAllMocks();
+ testState.hasMoreReplies = false;
testState.replyComments = [];
+ testState.virtuosoProps = [];
container = document.createElement('div');
document.body.appendChild(container);
@@ -444,4 +476,39 @@ describe('post community address compatibility', () => {
expect(container.querySelector('[data-testid="comment-media"]')).toBeTruthy();
expect(container.textContent).toContain('reply-1');
});
+
+ it('forwards Pretext-backed reply estimates into Virtuoso for desktop and mobile thread views', async () => {
+ testState.hasMoreReplies = true;
+
+ await renderWithRoute(createElement(PostDesktop, { post: makeLegacyThread(), showAllReplies: true }), '/mu/thread/post-1');
+ expect(testState.virtuosoProps.at(-1)).toEqual({
+ defaultItemHeight: 111,
+ heightEstimates: [100],
+ itemSize: expect.any(Function),
+ });
+
+ testState.virtuosoProps = [];
+ await renderWithRoute(createElement(PostMobile, { post: makeLegacyThread(), showAllReplies: true }), '/mu/thread/post-1');
+ expect(testState.virtuosoProps.at(-1)).toEqual({
+ defaultItemHeight: 222,
+ heightEstimates: [200],
+ itemSize: expect.any(Function),
+ });
+ });
+
+ it('keeps board-card Pretext heights when preview replies are rendered', async () => {
+ await renderWithRoute(createElement(PostDesktop, { post: makeLegacyThread() }));
+ expect(container.querySelector('.postDesktop')?.getAttribute('data-pretext-height')).toBeTruthy();
+
+ await renderWithRoute(createElement(PostMobile, { post: makeLegacyThread() }));
+ expect(container.querySelector('.postMobile')?.getAttribute('data-pretext-height')).toBeTruthy();
+ });
+
+ it('keeps board-card Pretext heights for simple cards without preview replies', async () => {
+ await renderWithRoute(createElement(PostDesktop, { post: makeLegacyThreadWithoutReplies() }));
+ expect(container.querySelector('.postDesktop')?.getAttribute('data-pretext-height')).toBeTruthy();
+
+ await renderWithRoute(createElement(PostMobile, { post: makeLegacyThreadWithoutReplies() }));
+ expect(container.querySelector('.postMobile')?.getAttribute('data-pretext-height')).toBeTruthy();
+ });
});
diff --git a/src/components/board-pagination/__tests__/board-pagination.test.tsx b/src/components/board-pagination/__tests__/board-pagination.test.tsx
index 5a1e8e87..9fb7b131 100644
--- a/src/components/board-pagination/__tests__/board-pagination.test.tsx
+++ b/src/components/board-pagination/__tests__/board-pagination.test.tsx
@@ -79,8 +79,8 @@ describe('BoardPagination', () => {
buttons[1]?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
- expect(testState.navigateMock).toHaveBeenNthCalledWith(1, '/mu');
- expect(testState.navigateMock).toHaveBeenNthCalledWith(2, '/mu/3');
+ expect(testState.navigateMock).toHaveBeenNthCalledWith(1, { pathname: '/mu', search: '' });
+ expect(testState.navigateMock).toHaveBeenNthCalledWith(2, { pathname: '/mu/3', search: '' });
});
it('shows the footer pagelist, catalog links, and enables infinite scroll from the all shortcut', async () => {
@@ -104,7 +104,7 @@ describe('BoardPagination', () => {
nextButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
- expect(testState.navigateMock).toHaveBeenCalledWith('/mu/2');
+ expect(testState.navigateMock).toHaveBeenCalledWith({ pathname: '/mu/2', search: '' });
});
it('hides the footer pagelist for multiboards or when infinite scroll is already enabled', () => {
diff --git a/src/components/board-pagination/board-pagination.tsx b/src/components/board-pagination/board-pagination.tsx
index 36981ba4..e14ca68f 100644
--- a/src/components/board-pagination/board-pagination.tsx
+++ b/src/components/board-pagination/board-pagination.tsx
@@ -8,6 +8,7 @@ import styles from './board-pagination.module.css';
interface BoardPaginationProps {
basePath: string;
currentPage: number;
+ search?: string;
totalPages: number;
/** When true, renders pagelist: [All] [1] [2] ... [10] Catalog Archive + Style select */
footerStyle?: boolean;
@@ -15,14 +16,14 @@ interface BoardPaginationProps {
isMultiboard?: boolean;
}
-const BoardPagination = ({ basePath, currentPage, totalPages, footerStyle = false, isMultiboard = false }: BoardPaginationProps) => {
+const BoardPagination = ({ basePath, currentPage, search = '', totalPages, footerStyle = false, isMultiboard = false }: BoardPaginationProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll);
const setEnableInfiniteScroll = useFeedViewSettingsStore((state) => state.setEnableInfiniteScroll);
- const pageHref = (page: number) => (page === 1 ? basePath : `${basePath}/${page}`);
- const catalogHref = `${basePath}/catalog`;
+ const pageHref = (page: number) => ({ pathname: page === 1 ? basePath : `${basePath}/${page}`, search });
+ const catalogHref = { pathname: `${basePath}/catalog`, search };
if (totalPages <= 1 && !footerStyle) {
return null;
@@ -30,7 +31,7 @@ const BoardPagination = ({ basePath, currentPage, totalPages, footerStyle = fals
if (footerStyle) {
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
- const archiveHref = `${basePath}/archive`;
+ const archiveHref = { pathname: `${basePath}/archive`, search };
return (