Skip to content

fix: keep a Work item selected when its save leaves it past the first page - #93

Merged
HemSoft merged 2 commits into
mainfrom
fix/issue-91-deep-page-selection
Nov 7, 2026
Merged

HemSoft merged 2 commits into
mainfrom
fix/issue-91-deep-page-selection

Conversation

@HemSoft

@HemSoft HemSoft commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Risk level: 2/10 (project-dashboard)
Worked on by: claude-opus-5-5 on laptop, for Chief dispatch 358679b9

Summary

#92 kept the selection on the saved item's identity, but a Work items save reloads from the first page. When the saved item stayed past page 1, the reload didn't contain it, so the selection fell back to row 49 and Back then Enter opened another item (the SFL Low on #92).

A reload under the same filters now reads on, page by page through pmd's own nextCursor, until the selected item turns up or pmd's listing really ends. Reading is bounded. If it stops for safety before the listing ends (at the page limit, or on a page that brings nothing or doesn't move the cursor on), the item is unread, not gone. That reload counts as failed, and nothing is reselected.

Reload outcome Rows Selection Message
Item found, on any page read, including the last allowed one The pages read The saved item, at its new row None
pmd's listing ended without the item The whole listing The row in its old place, within the list None
Stopped before the end without the item (page limit, empty page, cursor that doesn't move on) The rows from before the reload, kept Unchanged: still the saved item Error, with Retry
A later page failed The rows from before the reload, kept Unchanged Error, with Retry
  • dashboard-core/src/work.rs: WorkPage::beyond tells End (no next cursor) apart from More (read on from nextCursor) and Unread. Unread means a next cursor exists but the reload stops: an empty page, a cursor that didn't move on, or FOLLOW_PAGES = 20 pages.
  • dashboard-core/src/selection.rs: reload(previous, old, keys, beyond) answers Show(row), More or Incomplete. A found item always shows. A missing item counts as gone only at End.
  • manage/work_items.rs: the reload's pages accumulate in staged. They replace the rows only on Show. Incomplete drops them and keeps the rows on screen and their selection, and shows the error beside Retry, as a failed later page already did. The existing fences are reused: a filter change, Retry or another save starts a new generation, and a closed dialog drops late pages.

Not changed: the write path, If-Match, one write per save, no automatic retry, read-only mode, and pmd's order and data. No row is invented or edited to keep an index.

Acceptance Criteria

These are #91's criteria. #92 delivered the first-page and Projects parts.

Definition of Done

  • Fix in manage/work_items.rs, with the decisions in dashboard-core (selection::reload, WorkPage::beyond).
  • Headless UI tests through the real GPUI seam (keys, clicks, Save, Back, Enter) against FakeWork in pmd's order (effective priority, rank, newest). Opt-in fake modes cover a failed later page, later pages that return their own cursor, and later pages that come back empty but continue. 10 UI tests across both commits; 5 selection unit tests; 1 WorkPage::beyond test covering every stop reason, including a real last page at the limit.
  • The new tests fail on their base and pass at this head. Every one asserts exactly one write, to the edited item, with If-Match: 3.
  • fmt, clippy, workspace tests, advisories and standalone core pass locally on Linux (below). All five required CI checks pass for 456ae3e.
  • SFL Round 2: PASS, confidence 8/10, authenticated exact head 456ae3ef02b5277ea8b9976af092ed69d3e6768d; no blocking findings and zero unresolved threads.

Validation

Merged as fa8ffb7af7a4b1310e0c69e9247655713351f210 at 2026-11-06 23:36:23 EST using the normal squash helper with an exact-head refusal guard. Reviewed and merged tree e54ffe1b829e12ec616085470e231d2eb6bc3bf3 match. Exact-head SFL PASS, all five required CI jobs/all executed steps PASS, zero unresolved threads, and clean gh x status. Current-head Work items and Projects screenshot hashes, keyboard states, and the 119-file source snapshot were independently verified by the parent. Issue #91 auto-closed; bounded incomplete refreshes fail visibly without retargeting, with the filtering/staging limits retained in Risks. Source delivery verified on live main fa8ffb7af7a4b1310e0c69e9247655713351f210: CI run 42339191351 passed all five required jobs and every executed step. Mini primary is clean and current. Home sync preserved its pre-existing uncommitted audit material (eight commits behind); Laptop main audit was clean, but the guarded sync preserved it when live processes referenced the checkout. The owned Laptop PR worktree is clean and its tree equals merged main; source worker settled. Eight completed owned Mini target directories and two owned Laptop target directories were removed after path/live-process checks. Source archives, logs, captures, frame sequences, clean T3 worktrees and foreign worktrees remain retained. Deferred parent skill-history notes are recorded under a temporary Skills claim which was released. No installed-runtime delivery is claimed.

Local runs on Linux (Mini), not CI. Each tree is a git archive of the exact commit, with all 119 files checked against the commit's blob IDs. Each run has its own fresh CARGO_TARGET_DIR, --offline, CARGO_BUILD_JOBS=2 and nice 19 (Mini was busy). Windows evidence is CI.

RED for the bounded stops: 3567f1b production, with only 456ae3e's work_items_tests.rs and captures.rs, in target-bound-red.

  • cargo test -p dashboard-app --locked --offline work_items_tests: 35 passed, 3 failed.
    • Stuck cursor: Enter opened #256, not #255.
    • Empty continuing page: Enter opened #249, not #255.
    • Page limit: Enter opened #256, not #255.
    • The page-20 control (an_item_found_on_the_last_page…) passes there too, as a control should.
  • captures_work_reload_stopped: failed. The rows were replaced (100 rows, #256 selected) where it expected the 130 rows from before the save with #255 selected.

RED for the deep-page cases, from the first commit, f24f8b9 production plus that commit's tests: 28 passed, 7 failed. Enter opened #249 instead of #255, #310 and #256 (historical, 3567f1b's run).

GREEN at 456ae3e (target-bound-head):

  • work_items_tests: 38 passed. projects_tests: 21 passed.
  • cargo fmt --all --check and cargo clippy --workspace --all-targets --locked --offline -- -D warnings: pass.
  • cargo test --workspace --locked --offline: pass. dashboard-app had 207 passed and 18 ignored; every dashboard-core suite passed (44 unit tests).
  • cargo deny --locked check advisories: advisories ok.
  • Standalone core in target-bound-core: cargo check -p dashboard-core and cargo test -p dashboard-core: pass.
  • Captures: captures_work_ (deep page, stopped reload, fix: keep the Work items and Projects selection on the saved item after a reorder #92's Work items) 3 passed; captures_projects_selection_after_reorder 1 passed.

CI at 456ae3e: advisories, core (ubuntu-latest), core (windows-latest), rust (ubuntu-latest) and rust (windows-latest) all pass.

Screenshots, current head 456ae3e

Rendered offscreen on Linux (llvmpipe) by CAPTURE_DIR=$D/captures-bound-head cargo test -p dashboard-app --locked --offline captures_work_ -- --ignored --test-threads=1. Default 1100×760 window and theme, synthetic items listed in pmd's order. Each capture checks, after saving its frames, the write (v1/work/01JBXWORK0055), the selection and the cursors read.

Deep page, found (captures_work_deep_page_selection, 62 items). The flow: Load more, then the first row and Back by keyboard. Then Down ×55 to #255, Enter, rank 5 and Ctrl+Enter; the reload reads pages 1 and 2. Then Tab to Back to list and Enter.

456ae3e: Work items before the save, #255 selected at row 56 of 62

456ae3e: after the rank 5 save and Back, #255 highlighted at its new last row of 62

456ae3e: Enter reopens #255

Reload stopped before the list ended (captures_work_reload_stopped, 130 items, all three pages loaded). Same keyboard flow. pmd's second page then hands back its own cursor, so the reload stops after 2 pages. The 130 rows from before the save stay, with #255 (now rank 5) still selected at row 56, and the error shows beside Retry.

456ae3e: stopped reload, the rows from before the save with #255 still selected, the error and Retry

456ae3e: Enter after the stopped reload reopens #255

Historical, before the fixes. On f24f8b9, the deep-page capture after Back shows only page 1, with #249 highlighted:

Historical f24f8b9: after Back, #249 highlighted on the first page

On 3567f1b, the stopped-reload capture after Back shows 100 rows with #256 highlighted, reselected as if #255 were gone:

Historical 3567f1b: the stalled reload replaced the rows and highlighted #256

The six current-head frames as a frame sequence (deep page 1–4, then stopped 1–2; 1.5 s each). This is not a live screen recording; GPUI's headless renderer gives still frames.

bound-work-frames.mp4

Current-head Projects control

The parent independently verified all 119 source blobs in the 456ae3e capture snapshot. These retained current-head captures came from captures_projects_selection_after_reorder (passed), were uploaded, downloaded and hash-matched, and visually inspected: gh-x remains highlighted at row 0 after its P0 save and Back; Enter reopens gh-x. The Work items and Projects screenshots together cover #91.

456ae3e Projects: after P0 save and Back, gh-x is selected at row 0

456ae3e Projects: Enter reopens gh-x at P0

SHA-256: after Back 5d5a29bed9b5c4cac3479b7605c3b0f1fa47196adc310ef15384c69d4ffb5f35; Enter 3740daeec949a416e3b3ddc25143921646fa473950cb6e897065bddc201776f0.

Risks and deferred work

  • A reload reads at most 20 pages (1,000 items) while looking for the item. Past that, the refresh fails visibly with Retry, and the selection stays on the saved item; it is never moved to another one. Retry hits the same limit while the item stays that deep. The Round 2 non-blocking Low also covers a saved item that has left a status filter whose listing exceeds 1,000 items: absence cannot be established before the bound, so Retry repeats the visible failed refresh and retains stale rows until the selection or filters change. This is not fixed or claimed as a successful reload. The former silent retarget is fixed; incomplete listings never count as proof that the saved identity is gone.
  • While a reload reads on, the list shows the rows from before the save, with the saved row already updated, and Load more reads "Loading…".
  • SFL's non-blocking Low on 3567f1b: if a second, non-reordering save lands while a reload is still reading on, the reload's pages can show that row as it was before the save until the next reload. A later save of it gets 409 and a re-read. This is not changed here.
  • Manage > Fleet's positional selection stays out of scope, as Keep the Work items and Projects selection on the saved item after a reorder #91 says.

Closes #91

🤖 Generated with Claude Code

… page (#91)

A priority or rank save reloads the Work items list from its first page.
When the saved item stayed past that page, the reload didn't contain it,
so the selection fell back to row 49 and Back then Enter opened another
item.

A reload under the same filters now reads on, page by page, until it
finds the selected item or the listing ends. Only a listing that ended
without it counts as the item leaving the list. The rows on screen and
their selection stay until then. The read goes through the existing
cursor and generation fences: a filter change or a closed dialog drops
a late page, and a failed page keeps the rows on screen beside the error
and Retry. It stops on a page that brings nothing or doesn't move the
cursor on, and after FOLLOW_PAGES (20) pages. Saves still write once.

The decisions are dashboard_core::selection::reload and
WorkPage::follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@HemSoft HemSoft added the model:claude-opus-5-5 PR authored by claude-opus-5-5 label Nov 7, 2026
@HemSoft

HemSoft commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

@sfl review

@hemsoft-sfl-reviewer hemsoft-sfl-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Verdict: PASS • Round 1

Confidence: 8/10

Review done by claude-opus-5-5 (same family: same-family: codex gated: codex Weekly 167% SUS 60 🔴 pause, defer)

The PR changes the same-filter Work items reload so it reads on page by page through nextCursor, up to FOLLOW_PAGES, until the selected item turns up or the listing ends. Generation fences, failure handling and the stuck-cursor and page-limit stops all hold up on static reading. Tests cover each path. The only defect found is Low: rows held in staged can overwrite a row that a non-reordering save updated mid-reload. Low findings don't block approval, so the PR is approvable.

🔨 Full-Spectrum Review Summary

Effort: high
Review time: 1m 28s
Token count: Unavailable

Pass 🔴 Critical 🟠 High 🟡 Medium ⚪ Low
🔒 Security 0 0 0 0
✅ Accuracy 0 0 0 1
🧰 Quality & Maintainability 0 0 0 0
Total 0 0 0 1

Key Findings

  1. ⚪ LOW — Accuracy: Staged reload rows can overwrite a non-reordering save made mid-reload. crates/dashboard-app/src/manage/work_items.rs:321: Trigger: a reload is reading on and holding earlier pages in work.staged, which the code fills at line 315. During that time the user opens a row and saves a change that doesn't reorder the list, such as a status change with no status filter. The save handler at line 538 updates only work.items, not work.staged, and it starts no reload. When the reload finishes, work.items = rows at line 321 brings back that item as it was listed before the write. Impact (inferred): the list shows stale values until the next reload. If the row is reopened, the editor starts from the old version, so the next save gets 409 stale_version and a re-read, which recovers it. The same race existed before for one first-page request in flight. This PR widens the window to up to 20 sequential requests. (non-blocking)

Coverage

  • Files reviewed:
    • crates/dashboard-app/src/captures.rs
    • crates/dashboard-app/src/manage/work_items.rs
    • crates/dashboard-app/src/work_items_tests.rs
    • crates/dashboard-core/src/selection.rs
    • crates/dashboard-core/src/work.rs
    • crates/dashboard-core/tests/work.rs
  • Context read:
    • PR #93 title and body (as data)
    • crates/dashboard-app/src/manage/work_items.rs callers: `new_work_items`, `set_work_filter`, `open_work_item`, `leave_work_editor`, save and re-read paths, footer render
  • Verified:
    • Compare base_commit.sha is f24f8b910ed5344ab48740a052d419e76d3e2adf, the last commit is 3567f1b87e6c8bd4facd851de44b45c28e1d920c, ahead 1 and behind 0, and the merge base equals the base
    • A reset load bumps generation and clears staged, so late pages from a superseded reload are dropped by the generation check
    • read_on sets loading false before calling load_work(id, false), so the !work.loading guard lets the next page through, and Load more stays disabled while it runs
    • WorkPage::follow stops on an empty cursor, an empty page, a repeated cursor, or pages >= FOLLOW_PAGES, so at most 20 requests per reload
    • Reload::More only happens when previous is Some, and previous is set only when rows_filter == filter, so new filters show their first page at once
    • The Err branch with staged clears next_cursor and keeps the rows, and Retry does a reset load
    • A dialog closed mid-reload drops the update through work_of(id) returning None
    • No write-path, If-Match or credential handling changed
  • Not verified:
    • Runtime behavior and the PR's claimed local test, clippy and capture results (execution forbidden)
    • CI status for this head
    • That pmd's real nextCursor semantics match FakeWork's offset cursors (pmd source not read)
    • Screenshot content in the PR body
  • Tests run: None. Reviewer execution is disabled.

HemSoft full-spectrum review • 3 evidence-based passes • Reviewed head 3567f1b87e6c8bd4facd851de44b45c28e1d920c • SFL v0

…ecting (#91)

The reload that looks for the saved Work item stopped on four signals:
the last page, the page limit, an empty page and a cursor that doesn't
move on. It treated all four as the end of the list, so at the limit or
on a stalled listing a still-unread item counted as gone and the
selection moved to another one.

WorkPage::beyond now tells a real end (no next cursor) from a stop that
leaves the rest unread, and selection::reload answers Incomplete when
the item isn't found before such a stop. The dialog then keeps the rows
on screen and their selection, as for a failed page, and shows the error
beside Retry. Found on the last page it reads, the item still shows; a
real end without it still falls back to the row in its place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@HemSoft

HemSoft commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

@sfl review

@hemsoft-sfl-reviewer hemsoft-sfl-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Verdict: PASS • Round 2

Confidence: 8/10

Since round 1

New: 1; still open (repeated): 0; resolved: 1.
Resolved means no matching finding in this review, not independent proof of a fix.

New (1)

  • LOW An item that leaves the filter in a listing over 20 pages can never be reselected away from at crates/dashboard-app/src/manage/work_items.rs:319

Still open (repeated) (0)

  • None.

Resolved (1)

  • LOW Staged reload rows can overwrite a non-reordering save made mid-reload at crates/dashboard-app/src/manage/work_items.rs:321

Review done by claude-opus-5-5 (same family: same-family: codex gated: codex Weekly 167% SUS 60 🔴 pause, defer)

The PR changes a Work items reload after a save so it reads more pages through pmd's nextCursor until it finds the selected item or the listing ends. Reading stops at FOLLOW_PAGES=20 pages, on an empty page or on a cursor that doesn't move, and a stop without the item keeps the old rows and selection beside an error and Retry. The decision logic in selection::reload and WorkPage::beyond matches the fences, the error and filter paths, and the tests. The only finding is one Low edge case, which does not block approval.

🔨 Full-Spectrum Review Summary

Effort: high
Review time: 1m 49s
Token count: Unavailable

Pass 🔴 Critical 🟠 High 🟡 Medium ⚪ Low
🔒 Security 0 0 0 0
✅ Accuracy 0 0 0 1
🧰 Quality & Maintainability 0 0 0 0
Total 0 0 0 1

Key Findings

  1. ⚪ LOW — Accuracy: An item that leaves the filter in a listing over 20 pages can never be reselected away from. crates/dashboard-app/src/manage/work_items.rs:319: Inferred from code, not observed at runtime. Trigger: a status filter is active, the listing has more than FOLLOW_PAGES*PAGE (1,000) items, and the user saves a status that removes the selected item from the filter. Every reset reload reaches page 20 with a next cursor, so beyond returns Unread, the item is absent, and reload returns Incomplete. Retry calls load_work(id, true) with the same previous, so it fails the same way every time. Impact: the dialog keeps the stale rows, including the item updated in place to a status that no longer matches the filter, plus a permanent error, until the user moves the selection or changes a filter. The PR's Risks section covers the limit for an item that is still deep, but not this case, where the item is gone and Retry cannot succeed. (non-blocking)

Coverage

  • Files reviewed:
    • crates/dashboard-app/src/manage/work_items.rs
    • crates/dashboard-core/src/selection.rs
    • crates/dashboard-core/src/work.rs
    • crates/dashboard-core/tests/work.rs
    • crates/dashboard-app/src/work_items_tests.rs
    • crates/dashboard-app/src/captures.rs
  • Context read:
    • AGENTS.md
    • PR #93 description and commit messages
  • Verified:
    • base_commit.sha is f24f8b910ed5344ab48740a052d419e76d3e2adf, the last compare commit is 456ae3ef02b5277ea8b9976af092ed69d3e6768d, merge_base equals base, and the head is 2 commits ahead and 0 behind
    • A reset clears staged and next_cursor and bumps generation, so late pages after a filter change, Retry or another save are dropped at the generation check
    • read_on calls load_work(id, false) in the same update right after loading=false, so the Load more button stays disabled during staging and a user Load more can't interleave
    • The Ok(page) guard cursor.is_none() || staged.is_some() sends staged pages to reload and user Load more pages to the plain extend path
    • The Err path drops staged, clears next_cursor and keeps the same-filter rows; rows from other filters are still cleared on a first-page failure
    • WorkPage::beyond returns End only when there is no next cursor; an empty page, an unchanged cursor or pages >= FOLLOW_PAGES returns Unread, which matches the core tests, including a last page at the limit
    • reload always shows a found item, falls back to reselect only at End, and returns Show at once when there is no previous
    • Save paths at lines 554 and 620 reload only via load_work(id, true); the write path, If-Match and the single write are unchanged
    • The new UI tests check one write, the cursors read, the kept rows, Retry and Enter reopening the saved item for each stop reason
    • The capture tests are #[ignore] and write only under CAPTURE_DIR
  • Not verified:
    • Runtime and GPUI rendering behavior, and the PR's claims of local and CI test passes, because execution is forbidden
    • Screenshots in the PR body, because external assets were not inspected
    • pmd's real cursor semantics, for example whether nextCursor can cycle without repeating immediately, because the pmd source was not read
    • Every line of the 213-line captures.rs addition, which was skimmed for structure, assertions and file writes only
  • Tests run: None. Reviewer execution is disabled.

HemSoft full-spectrum review • 3 evidence-based passes • Reviewed head 456ae3ef02b5277ea8b9976af092ed69d3e6768d • SFL v0

@HemSoft
HemSoft merged commit fa8ffb7 into main Nov 7, 2026
5 checks passed
@HemSoft
HemSoft deleted the fix/issue-91-deep-page-selection branch November 7, 2026 04:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

model:claude-opus-5-5 PR authored by claude-opus-5-5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keep the Work items and Projects selection on the saved item after a reorder

1 participant