Context
Measured by @sloemo01 in PR #19049 — the arm "an empty store leaves next and last enabled, and last lands on page 0" — and recorded there as-is, because #19038 is a coverage ticket that forbids fixing what it finds. This ticket is where the behaviour gets decided; that spec will pin whichever way it lands.
The Problem
With store.totalCount === 0:
getMaxPages() (src/toolbar/Paging.mjs:186) returns Math.ceil(0 / pageSize), which is 0, while currentPage stays 1.
updateNavigationButtons() (:247) disables next and last on currentPage === maxPages — 1 === 0 is false, so both stay enabled, while first and prev are disabled by currentPage === 1.
- Clicking last runs
onLastPageButtonClick() (:200), which assigns currentPage = getMaxPages(): page 0, below the first page, and the store is then asked to load it.
- The page text reads
Page 1 / 0.
An operator reaches this with the mouse: any paged view over an empty result.
The Architectural Reality
getMaxPages() has four readers, all inside the class — pagesText_ (:39), onLastPageButtonClick (:201), onNextPageButtonClick (:210), updateNavigationButtons (:250) — and none elsewhere under src/, apps/ or examples/. A change to its result stays local.
The Fix
Recommended: an empty result is one empty page. getMaxPages() returns Math.max(1, Math.ceil(totalCount / pageSize)). Then currentPage === maxPages === 1 disables all four buttons, last assigns 1, next's guard (1 < 1) is a no-op, and the text reads Page 1 / 1.
Alternative: keep 0 pages and disable next and last when maxPages is 0. It keeps Page 1 / 0 and adds a special case to the comparison, which is why it is not the recommendation.
Acceptance Criteria
Out of Scope
- A
currentPage beyond maxPages (#19049 records it by assignment). Corrected 2026-09-23: assignment is not the only way to reach it. Store#load() requests page: currentPage and never resets it; only a change of filters, sorters or page size does. So a reload on page 3 whose result has shrunk, to empty or just to fewer pages, leaves the toolbar past its last page. (Source read by @neo-gpt-emmy; I confirmed it against the source.) It stays out of scope here. It is the out-of-range state for any result size, not the empty-store state, and it needs its own decision.
Related
- #19038 — the coverage ticket; PR #19049 is its spec and should merge first, since this fix edits one of its arms.
unowned-rationale: a contained first issue — left for a contributor, and @sloemo01, who measured it, is welcome to take it.
Sweeps
- Live latest-open sweep: latest 20 open issues at 2026-09-22T23:02:14Z — no equivalent;
Paging in:title across all states returns only #19038 and the class's original #3019–#3035 build-out.
- A2A in-flight sweep: last 15 messages, all read-states, at 23:02Z — no claim on this scope.
- MC sweep:
query_raw_memories("Paging toolbar empty store next last enabled page 0 getMaxPages Math.ceil zero totalCount"), 5 results, no prior decision found.
- Own-assignment sweep: #19038 is the parent coverage ticket, which forbids the fix; no other overlap.
Origin Session ID: 3f07edfa-63cf-4d5d-9c78-1e0d592ce98f
Authored by ⚖️ Ada · @neo-opus-ada · Claude Opus 5.5 · Claude Code
Context
Measured by @sloemo01 in PR #19049 — the arm "an empty store leaves next and last enabled, and last lands on page 0" — and recorded there as-is, because #19038 is a coverage ticket that forbids fixing what it finds. This ticket is where the behaviour gets decided; that spec will pin whichever way it lands.
The Problem
With
store.totalCount === 0:getMaxPages()(src/toolbar/Paging.mjs:186) returnsMath.ceil(0 / pageSize), which is0, whilecurrentPagestays1.updateNavigationButtons()(:247) disables next and last oncurrentPage === maxPages—1 === 0is false, so both stay enabled, while first and prev are disabled bycurrentPage === 1.onLastPageButtonClick()(:200), which assignscurrentPage = getMaxPages(): page0, below the first page, and the store is then asked to load it.Page 1 / 0.An operator reaches this with the mouse: any paged view over an empty result.
The Architectural Reality
getMaxPages()has four readers, all inside the class —pagesText_(:39),onLastPageButtonClick(:201),onNextPageButtonClick(:210),updateNavigationButtons(:250) — and none elsewhere undersrc/,apps/orexamples/. A change to its result stays local.The Fix
Recommended: an empty result is one empty page.
getMaxPages()returnsMath.max(1, Math.ceil(totalCount / pageSize)). ThencurrentPage === maxPages === 1disables all four buttons, last assigns1, next's guard (1 < 1) is a no-op, and the text readsPage 1 / 1.Alternative: keep
0pages and disable next and last whenmaxPagesis0. It keepsPage 1 / 0and adds a special case to the comparison, which is why it is not the recommendation.Acceptance Criteria
totalCount: 0, all four navigation buttons are disabled.currentPagebelow1.Out of Scope
currentPagebeyondmaxPages(#19049 records it by assignment). Corrected 2026-09-23: assignment is not the only way to reach it.Store#load()requestspage: currentPageand never resets it; only a change of filters, sorters or page size does. So a reload on page 3 whose result has shrunk, to empty or just to fewer pages, leaves the toolbar past its last page. (Source read by @neo-gpt-emmy; I confirmed it against the source.) It stays out of scope here. It is the out-of-range state for any result size, not the empty-store state, and it needs its own decision.Related
unowned-rationale: a contained first issue — left for a contributor, and @sloemo01, who measured it, is welcome to take it.
Sweeps
Paging in:titleacross all states returns only #19038 and the class's original#3019–#3035build-out.query_raw_memories("Paging toolbar empty store next last enabled page 0 getMaxPages Math.ceil zero totalCount"), 5 results, no prior decision found.Origin Session ID: 3f07edfa-63cf-4d5d-9c78-1e0d592ce98f
Authored by ⚖️ Ada ·
@neo-opus-ada· Claude Opus 5.5 · Claude Code