Session List UI Constraints#
Three non-obvious constraints in src/ui/main.tsx affect testing, sidebar stats, and pagination. Misunderstanding any one of them causes subtle regressions.
1. createRoot fires at import time — UI tests read source text instead#
main.tsx calls createRoot(root).render(...) at module scope, outside any function. Importing the file in a test environment immediately tries to access document.getElementById("root"), which fails or mounts a live React tree.
Consequence: Every UI test for main.tsx reads the file as a raw string with readFileSync and asserts on source substrings:
(Additional files test/ui-restyle-contract.test.ts, test/ui-recovery-report.test.ts, and test/ui-dosu-copy.test.ts follow the same pattern.)
Corollary: A source-text assertion fails when a line is reworded, even if the behavior is unchanged. Treat a red source-text assertion as "restate the contract," not "you broke something."
Path forward: If you need a real render test, extract the logic into a standalone module first. src/ui/loading-state.ts and src/ui/navigation.ts exist for exactly this reason. test/ui-transcript-markdown.test.tsx demonstrates the pattern — it renders transcript-markdown.tsx with renderToStaticMarkup because that file has no module-level side effects.
2. Sidebar "latest date" comes from loaded rows, not a dedicated endpoint#
latestSessionDay() finds the first session with a non-null started_at using Array.find(). Because the session list is loaded newest-first, that first row is the most-recent session — but only when the loaded slice starts at offset: 0.
The sidebar usage guards for this explicitly :
loadedPage === 1 ? latestSessionDay(sessions) : null
?? formatDay(data.dateBounds?.max ?? null)
When on any page other than page 1, it falls back to data.dateBounds?.max from /api/date-bounds, which returns the archive-wide max date regardless of any active filter.
Risk area: Any change that reorders the session list, introduces client-side filtering before passing rows to latestSessionDay, or changes when loadedPage is 1 will silently shift the sidebar date. The /api/date-bounds fallback is not scoped to the active project filter.
3. pageSize + 1 rows are requested as a pagination probe#
planSessionPageLoad always requests pageSize + 1 rows. The extra row is a probe: if the API returns a full pageSize + 1 result, a next page exists. If it returns fewer, the list is exhausted. sessionPageExhausted encodes this check as receivedRows < requestedRows.
Why not use a total count? data.summary and the project-scoped summary are separate fetches. They are null during cold loads and while a project filter's scoped summary is in flight. A pagination control gated on a total would disappear or disable itself for the first moments of every such view.
Why the probe stays within server limits: SESSION_LIST_MAX_LIMIT is 100. At the default pageSize of 50, requesting 50 + 1 = 51 rows is well under that cap. The Math.min(SESSION_LIST_MAX_LIMIT, normalizedPageSize + 1) in planSessionPageLoad prevents the probe from exceeding the server's hard limit regardless of what pageSize is passed in.
Do not replace the probe with a total-derived control. The API's
GET /api/sessionsreturns a plain array with no envelope — there is no server-side total to rely on.