From 251e103db3349143ddc4aaaec3a577c53741ee87 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Thu, 2 Apr 2026 19:45:55 +0700 Subject: [PATCH] feat: finalize pretext feed sizing rollout (#1120) * feat(feeds): add pretext-backed item sizing across board, catalog, and thread replies * feat: finalize pretext feed sizing rollout * fix(catalog): raise multiboard viewport buffer * fix(board): preserve pretext query overrides --- .../pretext-feed-sizing/feature-list.json | 78 ++ .../pretext-feed-sizing/progress.md | 111 +++ package.json | 1 + .../post-community-address-compat.test.tsx | 69 +- .../__tests__/board-pagination.test.tsx | 6 +- .../board-pagination/board-pagination.tsx | 9 +- .../__tests__/catalog-row.test.tsx | 31 +- src/components/catalog-row/catalog-row.tsx | 28 +- src/components/post-desktop/post-desktop.tsx | 142 ++- src/components/post-mobile/post-mobile.tsx | 152 ++- src/e2e/pretext-benchmark-harness.tsx | 869 ++++++++++++++++++ src/hooks/use-reply-height-estimates.ts | 57 ++ src/index.tsx | 15 +- .../pretext-height-estimates.test.ts | 258 ++++++ src/lib/utils/pretext-height-estimates.ts | 841 +++++++++++++++++ src/views/board/board.tsx | 44 +- src/views/catalog/__tests__/catalog.test.tsx | 21 +- src/views/catalog/catalog.tsx | 130 +-- src/views/post/post.module.css | 6 + src/views/post/post.tsx | 36 +- yarn.lock | 8 + 21 files changed, 2733 insertions(+), 179 deletions(-) create mode 100644 docs/agent-runs/pretext-feed-sizing/feature-list.json create mode 100644 docs/agent-runs/pretext-feed-sizing/progress.md create mode 100644 src/e2e/pretext-benchmark-harness.tsx create mode 100644 src/hooks/use-reply-height-estimates.ts create mode 100644 src/lib/utils/__tests__/pretext-height-estimates.test.ts create mode 100644 src/lib/utils/pretext-height-estimates.ts 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 (
diff --git a/src/components/catalog-row/__tests__/catalog-row.test.tsx b/src/components/catalog-row/__tests__/catalog-row.test.tsx index ee7b4241..75aa47c9 100644 --- a/src/components/catalog-row/__tests__/catalog-row.test.tsx +++ b/src/components/catalog-row/__tests__/catalog-row.test.tsx @@ -54,7 +54,6 @@ const testState = vi.hoisted(() => ({ hiddenCids: new Set(), imageSize: 'Small' as 'Large' | 'Small', linkCount: 0, - matchedFilters: new Map(), mediaInfoByLink: {} as Record, lastRepliesComment: undefined as TestComment | undefined, replies: [] as TestComment[], @@ -63,12 +62,6 @@ const testState = vi.hoisted(() => ({ showSnow: false, })); -function getCatalogFiltersState() { - return { - matchedFilters: testState.matchedFilters, - }; -} - vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, @@ -156,13 +149,6 @@ vi.mock('../../../hooks/use-directories', () => ({ useDirectories: () => testState.directories, })); -vi.mock('../../../stores/use-catalog-filters-store', () => ({ - default: (selector?: (state: ReturnType) => T) => { - const state = getCatalogFiltersState(); - return selector ? selector(state) : (state as T); - }, -})); - vi.mock('../../../stores/use-catalog-style-store', () => ({ default: () => ({ imageSize: testState.imageSize, @@ -232,7 +218,6 @@ describe('CatalogRow', () => { testState.hiddenCids = new Set(); testState.imageSize = 'Small'; testState.linkCount = 0; - testState.matchedFilters = new Map(); testState.mediaInfoByLink = {}; testState.lastRepliesComment = undefined; testState.replies = []; @@ -253,7 +238,6 @@ describe('CatalogRow', () => { it('renders gif frames with matched filter borders and falls back to deleted media on load errors', async () => { testState.gifFrameStatus = 'ready'; testState.gifFrameUrl = 'https://cdn.example/frame.png'; - testState.matchedFilters = new Map([['post-1', 'red']]); await act(async () => { root.render( @@ -262,6 +246,7 @@ describe('CatalogRow', () => { commentMediaInfo: { type: 'gif', url: 'https://example.com/source.gif' }, linkHeight: 200, linkWidth: 400, + matchedFilterColor: 'red', }), ); }); @@ -416,7 +401,7 @@ describe('CatalogRow', () => { expect(container.textContent).toContain('/ I: 3'); expect(container.textContent).not.toContain('/ L: 3'); expect(document.body.querySelector('a[href="/mu/thread/post-alias"]')).toBeTruthy(); - expect(container.querySelector('[title=\"(R)eplies / (I)mage Replies\"]')).toBeTruthy(); + expect(container.querySelector('[title="(R)eplies / (I)mage Replies"]')).toBeTruthy(); }); it('normalizes legacy board addresses before fetching hover preview replies', async () => { @@ -495,4 +480,16 @@ describe('CatalogRow', () => { expect(container.textContent).toContain('(hidden)'); expect(container.textContent).toContain('Text title: Plain thread body'); }); + + it('applies the estimated row height to the virtualization wrapper', async () => { + const post: TestComment = { + cid: 'estimated-post', + content: 'Estimated row body', + communityAddress: 'music-posting.eth', + }; + + await renderWithRouter(createElement(CatalogRow, { estimatedHeight: 246, row: [post] }), '/mu/catalog'); + + expect(container.querySelector('[data-pretext-height="246"]')).toBeTruthy(); + }); }); diff --git a/src/components/catalog-row/catalog-row.tsx b/src/components/catalog-row/catalog-row.tsx index fb219118..f71b567c 100644 --- a/src/components/catalog-row/catalog-row.tsx +++ b/src/components/catalog-row/catalog-row.tsx @@ -11,7 +11,6 @@ import { getFormattedTimeAgo } from '../../lib/utils/time-utils'; import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { findDirectoryByAddress, useDirectories } from '../../hooks/use-directories'; import { getBoardPath } from '../../lib/utils/route-utils'; -import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; import useCatalogStyleStore from '../../stores/use-catalog-style-store'; import useEditCommentPrivileges from '../../hooks/use-author-privileges'; import { useCommentMediaInfo } from '../../hooks/use-comment-media-info'; @@ -32,9 +31,11 @@ interface CatalogPostMediaProps { isOutOfFeed?: boolean; linkWidth?: number; linkHeight?: number; + matchedFilterColor?: string; } -export const CatalogPostMedia = ({ cid, commentMediaInfo, linkWidth, linkHeight }: CatalogPostMediaProps) => { +export const CatalogPostMedia = ({ cid, commentMediaInfo, linkWidth, linkHeight, matchedFilterColor }: CatalogPostMediaProps) => { + void cid; const { patternThumbnailUrl, thumbnail, type, url } = commentMediaInfo || {}; const iframeThumbnail = patternThumbnailUrl || thumbnail; const { frameUrl: gifFrameUrl, status: gifFrameStatus } = useFetchGifFirstFrame(type === 'gif' ? url : undefined); @@ -100,8 +101,6 @@ export const CatalogPostMedia = ({ cid, commentMediaInfo, linkWidth, linkHeight thumbnailComponent =