DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/loomworks-list-loading-cr-2-frontend-load-more-v0_3.html

Loomworks list-loading — CR-2: frontend load-more + page size — v0.3

Document: loomworks-list-loading-cr-2-frontend-load-more-v0_3 Version: v0.3 Status: working draft — Steps 1–2 built; Steps 3–6 in progress (split per surface); D-4 resolved empirically at build Date: 2026-06-27 Supersedes: loomworks-list-loading-cr-2-frontend-load-more-v0_2 (2026-06-27), …-v0_1 Queue item: Companion build queue Item 1 — list-loading (cross-cutting pagination), frontend half Handoff: loomworks-list-loading-cr-drafting-handoff-v0_1 Step-0 source: loomworks-list-loading-cr-2-step0-report-v0_1 (live OL + merged-engine inspection — build CR-2 steps from THIS) Engine contract: CR-1 merged at eeae9c2 / alembic 0096 — the ten endpoints return limit + cursor + has_more + total_count + next_cursor Repo: loomworks (Operator Layer) — branch off current feat-companion-surface 17263c9. Engine touched only for the lists.page_size setting plumbing (one KNOWN_SETTINGS entry + one _LABEL_TO_KEY synonym set). Two-role note: This CR is scoping/drafting. CC executes. Nothing commits or pushes without the Operator's explicit authorisation — explicit-path staging, per-step commits, suite green at each step, push on separate authorisation.


1. What this CR does

CR-1 brought ten engine endpoints to one keyset contract. CR-2 builds the frontend that consumes it: a reusable explicit load-more control, applied to the list surfaces that exist today, plus the per-surface page-size defaults and the voice-adjustable global override.

Build status (2026-06-27)

Step State
1 — engine lists.page_size Built. Nullable bounded [10,100] setting on the three-place pattern; shared numeric tune path made None-safe (zero regression); orientation catalog 3→4. Engine branch list-loading-cr-2-page-size @ e5bbf05 (main eeae9c2 untouched, not pushed). Engine suite 3041/0/63.
2 — reusable spine Built. lib/pagination.ts + hooks/usePagedList.ts (append/prepend, dedup, caller-resolved pageSize) + components/lists/LoadMoreControl.tsx (explicit, has_more-gated, count always shown). 12 isolation tests. OL branch list-loading-cr-2-frontend-load-more @ 5973ff8 (off feat-companion-surface, not pushed). OL suite 390/76.
3–6 In progress, split per surface (blast-radius discipline — see below).

Why 3–6 are split. Steps 3–6 collectively touch well over 30 existing tests — each surface is embedded in parents whose tests mock the fetch adapters, so rewiring one fetch contract cascades mock updates across that surface's test and every embedder. The CR's >30-test halt threshold fired during the build; CC stopped at a clean boundary (both repos green, spine proven, nothing half-wired). Steps 3–6 proceed as bounded per-surface sessions against the built spine, each committed separately, each under the threshold: renders, Memory/assertions, engagements (Step 3's three surfaces, each its own session), home+defaults (Steps 4+5), CenterPane (Step 6). The spine — the high-value, hard part — is done; each remaining step is a surface-wiring.

Scope is set by what is live. Step-0 D found only 4 of the 10 endpoints have an Operator Layer surface today (engagements, assertions/Memory, renders/Rendering, conversation-history/CenterPane). The other six (considerations, compositions, shape-events, assertion-groups, manifestations, shapings) have no frontend consumer — their rooms are empty stubs (RoomView.tsx:37). CR-2 paginates what is live and defers the six roomless endpoints to a bounded follow-on: they get the load-more control when their rooms are built, not before. Building pagination for a surface that does not exist would be building against nothing.

Five pieces: 1. A reusable load-more control + paged-list state — explicit (not infinite scroll), has_more-gated, total_count surfaced, parameterised by item-key. 2. Applied to the 4 live endpoint surfaces (three swap wholesale limit=200 → cursor; CenterPane migrates beforecursor). 3. Home lenses re-wired to stop discarding next_cursor. 4. Per-surface page-size defaults. 5. lists.page_size voice-adjustable global override (nullable; the three-place settings extension).


2. The two decisions the Operator confirmed at drafting

Defer the six roomless endpoints. CR-2 covers only surfaces that exist. The six endpoints whose rooms are empty stubs are a named follow-on — paginated when their rooms are built. CR-2 stays a pagination CR, not a room-building CR.

The limit=200 → paginated swap is intended. Three surfaces (engagements, Memory/assertions, Rendering/renders) currently fetch wholesale limit=200. CR-2 swaps each to cursor pagination with a per-surface default (~15–40). This is a visible behaviour change — a user who today sees up to 200 rows at once will see one page plus a load-more control. Named explicitly because it is the one place CR-2 changes what an existing surface shows. It is the intended change — the whole point of the work — and satisfies the seed's "only show what is available" + total_count-surfaced shape.


3. The reusable load-more control (the spine)

One control, built once, applied to every paged surface. Generalised from the precedent that already exists: CenterPane's "load earlier" control (ConversationPane.tsx:179).

Behaviour (settled pattern, from the handoff): - Explicit, not infinite scroll. A control the Operator clicks/taps to load the next page. (Operator-authority posture: the surface signals "more exists"; the Operator chooses to load it.) - has_more-gated. The control appears only when has_more is true. When has_more is false, the control does not render — no disabled state, no greyed-out control (seed: "only show what is available"). - total_count always visible. The surface shows "showing N of TOTAL" (or equivalent) whenever a list is paged, so no truncation is silent (handoff rule 2). - Parameterised by item-key. Step-0 A found the response item-array key varies per endpoint — most use the endpoint's natural key, compositions uses items, conversation-history uses turns. The control takes the item-key as a prop rather than assuming a fixed field, so one control serves all surfaces. (Also ignore assertions' vestigial limit response field — Step-0 A; rely on has_more/next_cursor.)

Paged-list state (the reusable hook/store): - Holds: accumulated items, next_cursor, has_more, total_count, loading flag. - First fetch on mount: limit=<surface default>, no cursor. - Load-more: fetch limit=<default>&cursor=<next_cursor>, append to accumulated items, update next_cursor/has_more. - Direction note: most surfaces prepend nothing (append newest-first or oldest-first per the surface); CenterPane is the exception — it loads earlier turns and prepends (existing behaviour, preserved). The reusable state supports both append and prepend modes.

Settled (D-3): extract a shared control; CenterPane adopts it at Step 6. Build the reusable control to the general list shape; CenterPane migrates onto it, keeping its dedup guard as a CenterPane-level concern. The precedent becomes the reusable thing rather than a parallel implementation. [CC reads the existing CenterPane control's shape (ConversationPane.tsx:179) to extract the general control cleanly and identify what stays CenterPane-specific — the prepend bias is a mode the shared control supports; the dedup guard stays at CenterPane.]


4. Step plan — built from Step-0's live surface inventory

Engine setting plumbing first (it is the only engine touch and unblocks the override), then the control, then each live surface, then home, then the global override, then CenterPane migration last (it is both a surface and the precedent).

Step 1 — engine: lists.page_size setting plumbing

Repo loomworks-engine (the only engine touch in CR-2)
What Add lists.page_size as a bounded-numeric setting on the confirmed three-place pattern (Step-0 C).
The three places (1) KNOWN_SETTINGS entry in preferences/person_settings.pySettingSpec(key='lists.page_size', display_name=…, default=None, validator=…, bounds=(10,100)); nullable default (unset → per-surface defaults govern). (2) _LABEL_TO_KEY synonym set in orchestration/tune_setting.py (e.g. "list size", "page size", "rows per page"). (3) the frontend applySettingChange branch (useConversation.ts:71) — Step 4 of this CR.
Bounds [10, 100] (handoff lean; confirm).
Mechanics Inherits tune_setting (bounded, named-back on change, FORAY setting_change audit) + direct PUT /me/settings/{setting_key} (me_settings.py:96, confirmed live). No new setting mechanics — lists.page_size is an ordinary entry in the existing system.
Test setting accepts in-bounds, rejects out-of-bounds, named-back on change, FORAY audit emitted, nullable unset state honoured. Engine suite green.

Step 2 — the reusable load-more control + paged-list state

Repo loomworks (OL)
What Build the control + state per §3. No surface wired yet — this step is the reusable primitive + its tests in isolation.
Resolves page size The control's effective page size = lists.page_size if the Operator has set it (non-null), else the surface's per-surface default (passed as a prop). The global override, when set, uniformises; unset, per-surface governs.
Test mount fetches first page; load-more appends and advances cursor; control hidden when has_more false; total_count rendered; item-key prop selects the right array; append + prepend modes both work.

Step 3 — apply to the three wholesale endpoint surfaces

Surfaces engagements (home "Your engagements" list), Memory room (assertions), Rendering room (renders) — each currently fetches wholesale limit=200 (Step-0 D)
What Replace the limit=200 wholesale fetch with the Step-2 paged-list state + control. Per-surface defaults (Step 5). The limit=200→paginated swap is the intended visible change (§2).
Per-endpoint item-key engagements, assertions, renders each use their natural item key — [CC confirms each from the live response]. Renders direction (D-4 — RESOLVED at build, land-on-oldest): the v0.2 lean was "land newest, page older." Build found the engine renders endpoint sorts created_at ASC and pages forward (oldest→newer) — a forward-only keyset cannot land on the newest page, so the fallback the CR reserved applies: page 1 = oldest, load-more = newer. This matches what RenderingRoom shows today (no reversal) — so it is not even a display change. The mechanics governed; the lean was set aside.
Test each surface pages; first page ≤ default; load-more works; total_count shown; empty and single-page states render without a stray control.

Step 4 — home lenses re-wire (frontend-only, no engine)

Surfaces the two across-engagements home lenses — consume /me/dashboard/needs_you + /recent (lib/api/dashboard.ts:72,119) + /engagements
Step-0 B finding These dashboard sections are already keyset-conformant (Group A on the engine); the frontend parses next_cursor then throws it away. So home is a pure re-wire — no engine work, no new fetch shape.
What Stop discarding next_cursor; wire the Step-2 control to the existing dashboard fetches. Also wire the applySettingChange branch for lists.page_size here (useConversation.ts:71) — the third of the three settings places.
Test home lenses page; the discarded-cursor regression is closed; lists.page_size change applies live to the home lists.

Step 5 — per-surface page-size defaults

What Set each live surface's default page size, surface-purpose-tuned (handoff decision (b)).
Defaults (lean, set with surfaces in view) reading surfaces (Memory room, Rendering room) ~15; glance surfaces (home engagement list) ~40; urgency lists (needs_you) ~15. [Confirm the actual numbers at this step with the live surfaces — not load-bearing for the structure.]
Override interaction these are the defaults lists.page_size overrides when the Operator sets it; unset, these govern.
Test each surface opens at its default; setting the global override uniformises all; clearing it restores per-surface.

Step 6 — CenterPane migration: beforecursor (the breaking site + the precedent)

Surface CenterPane / conversation-history (ConversationPane.tsx:179, the existing "load earlier" control)
Step-0 A finding conversation-history's before param is gone — the live param is cursor (CR-1 Step 10 replaced it). CenterPane currently calls with before — this is the one breaking frontend site.
The two call sites useConversation.ts (Axis 11, passes before) and ChatView.tsx (home, no before) — both updated to the cursor param. [CC confirms both call sites against the live OL — the handoff named these two; Step-0 confirms ConversationPane.tsx:179 as the control.]
What Migrate the call from before=<oldest raw created_at> to cursor=<next_cursor>. CenterPane's load-earlier (prepend) behaviour is preserved. If §3's lean was taken — extract the shared control — CenterPane adopts the Step-2 control here, so the precedent becomes the reusable thing. Also: conversation-history now returns total_count (CR-1 added it) — surface it.
R2 note CR-1's composite (created_at, id) cursor closes the R2 boundary-tie skip the Axis 11 phase left open — this migration is what realises that fix on the surface. The boundary-tie is now handled engine-side; the frontend just passes the opaque cursor.
Test load-earlier still works (no skip, no duplicate — the R2 case now engine-handled); total_count shown; both call sites pass cursor; the dedup guard from Axis 11 still holds as a belt-and-braces.

5. Out of scope (named follow-ons)


6. The four decisions — settled

All four resolved with the Operator at drafting. Rationale recorded per corrections-preserved discipline.

# Decision Settled
D-1 lists.page_size bounds [10, 100]. Floor 10 keeps a page meaningful (fewer fires the control constantly); ceiling 100 stops the override recreating the wholesale limit=200 problem CR-2 fixes.
D-2 Per-surface defaults reading ~15 (Memory, Rendering), glance ~40 (home engagement list), urgency ~15 (needs_you) — starting numbers, set at Step 5 with surfaces in view, tunable in one place after. Not structural.
D-3 Shared control: extract vs generalise CenterPane in place Extract. Build the clean reusable control to the list shape (append/prepend mode, item-key prop, has_more-gate, total_count); CenterPane adopts it at Step 6, keeping its dedup guard as a CenterPane-level concern layered on top. See rationale below.
D-4 Renders load-more direction (sort is ASC/oldest-first per CR-1) Lean was land-newest/page-older; RESOLVED at build to land-on-oldest. The renders keyset is forward-only ASC (oldest→newer); it cannot land on the newest page. So page 1 = oldest, load-more = newer — the fallback the CR named. Matches current RenderingRoom behaviour (no reversal), so no display change. Mechanics governed the lean.

D-3 rationale (the one structural, expensive-to-reverse call). CenterPane's existing control is shaped for a conversation stream: it loads earlier turns, prepends, and carries a dedup guard. The other five surfaces append, no dedup. Generalising CenterPane's control in place would bend a conversation-stream control to cover list surfaces, leaving list pagination dependent on conversation-specific machinery — a seam that drifts (a later change to conversation dedup silently changes how the Memory room pages). Extracting inverts it: the shared control is genuinely general (the list shape), CenterPane's conversation behaviour stays at CenterPane. Same discipline as CR-1's codec lift — generalise the shared thing properly rather than propagating a special case. CenterPane's dedup guard is now redundant (CR-1's composite cursor handles the R2 skip it guarded against) but kept as harmless belt-and-braces.


7. Done criteria

CR-2 done when: lists.page_size lands on the three-place pattern (engine suite green); the reusable control + paged-list state built and tested in isolation; the three wholesale surfaces (engagements, Memory, Rendering) swapped to cursor pagination; the two home lenses re-wired to stop discarding next_cursor; per-surface defaults set; CenterPane migrated beforecursor with load-earlier preserved and total_count surfaced; both conversation-history call sites pass cursor; OL suite green at each step; verified against playground_dev. The six roomless endpoints remain a named follow-on. Push on separate authorisation.


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks list-loading — CR-2: frontend load-more + page size — v0.3 — 2026-06-27 Frontend CR against merged CR-1 (eeae9c2/0096). Steps 1–2 built (engine setting + reusable spine); Steps 3–6 split per surface for blast-radius. D-4 resolved at build to land-on-oldest (forward-only ASC keyset). Supersedes v0.2, v0.1.