Fix #143: milestone removal actually works #145

Merged
crueber merged 1 commit from fix/issue-143 into main 2026-09-05 17:03:02 +00:00
Owner

Root cause: the thread route reuses ONE component instance across issue numbers, so a sidebar mutation's async tail (optimistic paint, PATCH, reconcile) ran against whatever issue the view showed when the PATCH resolved. Consequences: (1) the mutated issue could strand on its optimistic guess until an SSE frame arrived (reload() invalidated the NEW issue's key); (2) worse, an in-flight busy guard inherited from the previous issue SILENTLY swallowed clicks on the new issue - the busy early-return fires no PATCH and posts no tray entry, so the + menu just closes and the milestone stays put. That matches the report exactly: the server log shows the set-PATCH landing but no clear-PATCH ever arriving, while the UI gave zero feedback.

Evidence gathered while reproducing (read-only against the running stack + fresh local build):

  • Served bundle _ui/assets/index-DToewiwZ.js contains the #119 clear row and #132 title events; the image (built 16:08:59Z, container recreated 16:09:00Z) postdates the #136 merge. Fresh vite build from origin/main differs ONLY by the unrelated #142 owners change - git diff confirms the whole milestone path (picker, milestones.js, Issue.jsx, data.js, issue-events.js, SDK) is byte-identical. No stale deploy on the milestone path.
  • Backend null-clear is airtight: HTTP RawMessage decoding (explicit null vs absent), service clear incl. after close with counter fixups - all covered by existing tests (TestPatchMilestoneMove, TestPatchIssueMilestoneHTTP).
  • Fresh-browser drives on the live stack clear fine on OPEN (#1) and CLOSED (#2) issues: PATCH 200, sidebar to none, honest 'removed this from v1.1' timeline event, zero console errors. So the remaining silent-drop window is the cross-navigation one fixed here.

Fix (web/src/pages/Issue.jsx only): pin num()/key() up front in toggleLabelApply + selectMilestone; reconcile the pinned key when the view moved on; reset both busy guards on num change. No-op selects still skip silently by design; every real click PATCHes or reports. Decision appended to docs/features/02_issues.md.

Verification: node --test 317/317 green; vite build clean; fresh local server from this branch driven over CDP - full set->clear->set cycle dark+light with API confirmation at each step (000001 -> null -> 000001, counters 1/0 -> 0/0), title-based timeline events, zero console/page errors. Screenshots: /tmp/opencode/proof143b-{dark,light}.png (plus API cycle proof).

Live-stack note: issue #1 left at milestone v1.1 (as screenshotted); issue #2's milestone is now cleared, which is the reporter's desired end state.

Root cause: the thread route reuses ONE component instance across issue numbers, so a sidebar mutation's async tail (optimistic paint, PATCH, reconcile) ran against whatever issue the view showed when the PATCH resolved. Consequences: (1) the mutated issue could strand on its optimistic guess until an SSE frame arrived (reload() invalidated the NEW issue's key); (2) worse, an in-flight busy guard inherited from the previous issue SILENTLY swallowed clicks on the new issue - the busy early-return fires no PATCH and posts no tray entry, so the + menu just closes and the milestone stays put. That matches the report exactly: the server log shows the set-PATCH landing but no clear-PATCH ever arriving, while the UI gave zero feedback. Evidence gathered while reproducing (read-only against the running stack + fresh local build): - Served bundle _ui/assets/index-DToewiwZ.js contains the #119 clear row and #132 title events; the image (built 16:08:59Z, container recreated 16:09:00Z) postdates the #136 merge. Fresh vite build from origin/main differs ONLY by the unrelated #142 owners change - git diff confirms the whole milestone path (picker, milestones.js, Issue.jsx, data.js, issue-events.js, SDK) is byte-identical. No stale deploy on the milestone path. - Backend null-clear is airtight: HTTP RawMessage decoding (explicit null vs absent), service clear incl. after close with counter fixups - all covered by existing tests (TestPatchMilestoneMove, TestPatchIssueMilestoneHTTP). - Fresh-browser drives on the live stack clear fine on OPEN (#1) and CLOSED (#2) issues: PATCH 200, sidebar to none, honest 'removed this from v1.1' timeline event, zero console errors. So the remaining silent-drop window is the cross-navigation one fixed here. Fix (web/src/pages/Issue.jsx only): pin num()/key() up front in toggleLabelApply + selectMilestone; reconcile the pinned key when the view moved on; reset both busy guards on num change. No-op selects still skip silently by design; every real click PATCHes or reports. Decision appended to docs/features/02_issues.md. Verification: node --test 317/317 green; vite build clean; fresh local server from this branch driven over CDP - full set->clear->set cycle dark+light with API confirmation at each step (000001 -> null -> 000001, counters 1/0 -> 0/0), title-based timeline events, zero console/page errors. Screenshots: /tmp/opencode/proof143b-{dark,light}.png (plus API cycle proof). Live-stack note: issue #1 left at milestone v1.1 (as screenshotted); issue #2's milestone is now cleared, which is the reporter's desired end state.
The thread route reuses one component instance across issue numbers, so a sidebar mutation started on one issue finished its async tail (optimistic paint, PATCH, reconcile) against whatever issue the view showed when the PATCH resolved: the mutated issue could strand on its optimistic guess, and an in-flight busy guard on the previous issue silently swallowed clicks on the new one (busy early-return fires no PATCH and posts no tray entry - the menu just closes). Capture num()/key() up front in the label toggle and milestone select; reconcile the pinned key when the view moved on; reset the per-issue busy guards on navigation. Appends the decision to 02_issues.md in the same change.
Sign in to join this conversation.
No description provided.