Repository navigation
fix(settings): give every capability row its own busy guard - #1881
Merged
Merged
Conversation
The Skills, Subagents and MCP pages shared one busyId slot across all row-keyed operations. Whichever request settled first cleared the slot while other rows were still in flight, re-arming their switches for a second click that double-fired from the optimistic state; the MCP toggle guarded with a plain if (busyId), which froze every row's switch while one request was pending. One independent guard key per row keeps each row double-click-safe while the others stay responsive.
SeanLiu326
force-pushed
the
fix/capability-row-busy-guards
branch
from
October 25, 2026 16:23
4d53713 to
f3e5cc3
Compare
The Electron E2E job's provider-client-certificate probe hung and was SIGKILLed after 65s (dbus failures on the headless runner) — the signature this job recovers from on a re-run, unrelated to this branch's settings-page changes.
SeanLiu326
added a commit
to SeanLiu326/PI-Desktop
that referenced
this pull request
Oct 27, 2026
The models page and the plugins page ran their row-keyed async operations through single shared busy slots — busyId, testingId and reloadingId — with unconditional finally clears and, on the models page, no entry guard at all. The first request to settle re-armed every row still in flight (a second click double-fires the host write from an optimistically flipped state), and the models list-wide busy gate froze all rows for one row's operation. Both pages now claim one independent key per row through the use-row-busy guard introduced for the capability pages (vastsa#1881): toggleEnabled, removeProvider, saveProviderKey and testProvider on the models page; confirmReview, runInstall and reloadPlugin on the plugins page. The plugins page model exposes isPluginBusy(id) instead of the busyId slot, and the models list keeps only its modal-dialog terms in the page-wide gate.
SeanLiu326
added a commit
to SeanLiu326/PI-Desktop
that referenced
this pull request
Oct 27, 2026
The models page and the plugins page ran their row-keyed async operations through single shared busy slots — busyId, testingId and reloadingId — with unconditional finally clears and, on the models page, no entry guard at all. The first request to settle re-armed every row still in flight (a second click double-fires the host write from an optimistically flipped state), and the models list-wide busy gate froze all rows for one row's operation. Both pages now claim one independent key per row through the use-row-busy guard introduced for the capability pages: toggleEnabled, removeProvider, saveProviderKey and testProvider on the models page; confirmReview, runInstall and reloadPlugin on the plugins page. The plugins page model exposes isPluginBusy(id) instead of the busyId slot, and the models list keeps only its modal-dialog terms in the page-wide gate. Stacked on vastsa#1881; rebase onto main once it lands.
SeanLiu326
added a commit
to SeanLiu326/PI-Desktop
that referenced
this pull request
Oct 27, 2026
The models page and the plugins page ran their row-keyed async operations through single shared busy slots — busyId, testingId and reloadingId — with unconditional finally clears and, on the models page, no entry guard at all. The first request to settle re-armed every row still in flight (a second click double-fires the host write from an optimistically flipped state), and the models list-wide busy gate froze all rows for one row's operation. Both pages now claim one independent key per row through the use-row-busy guard introduced for the capability pages (vastsa#1881): toggleEnabled, removeProvider, saveProviderKey and testProvider on the models page; confirmReview, runInstall and reloadPlugin on the plugins page. The plugins page model exposes isPluginBusy(id) instead of the busyId slot, and the models list keeps only its modal-dialog terms in the page-wide gate.
vastsa
added a commit
that referenced
this pull request
Oct 28, 2026
…rds (#1928) * fix(settings): give model-provider and plugin rows their own busy guards The models page and the plugins page ran their row-keyed async operations through single shared busy slots — busyId, testingId and reloadingId — with unconditional finally clears and, on the models page, no entry guard at all. The first request to settle re-armed every row still in flight (a second click double-fires the host write from an optimistically flipped state), and the models list-wide busy gate froze all rows for one row's operation. Both pages now claim one independent key per row through the use-row-busy guard introduced for the capability pages (#1881): toggleEnabled, removeProvider, saveProviderKey and testProvider on the models page; confirmReview, runInstall and reloadPlugin on the plugins page. The plugins page model exposes isPluginBusy(id) instead of the busyId slot, and the models list keeps only its modal-dialog terms in the page-wide gate. * test(plugins): pin the marketplace install gates to the per-row busy predicate plugin-runtime-risk asserted the marketplace/detail install buttons through the removed single busyId slot; the guard job moved to isPluginBusy in the parent commit, so the pins follow the predicate. * fix(settings): keep provider test spinners independent Track active provider tests by row so one settled request cannot clear another row's in-flight indicator. Cover the page-to-list wiring with a regression check. --------- Co-authored-by: Lan <48862574+vastsa@users.noreply.github.com> Co-authored-by: vastsa <hhxk666@gmail.com>
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
Fixes #1880 — the Skills, Subagents and MCP pages shared one
busyIdslot across all row-keyed operations, which fails in two directions:busyId === keybut thefinallyclears withsetBusyId(null)no matter whose request settled. Toggle A, then B, then let A finish: B's switch is re-armed while B's own request is still in flight, and a second click double-fires the host write from B's optimistically flipped value.importSkillhad no guard at all.if (busyId) returnblocks every row's switch while one request is pending — the opposite of the behavioragent-capability-settings.test.mjsalready pins for these pages ("a busy row does not lock the page").Change
src/lib/row-busy-guard.ts— dependency-freecreateRowBusyGuard()(begin/end/isBusyover aSet), following theplugin-session-push-gateconvention so tests drive it without a renderer.src/hooks/use-row-busy.ts— thin React wrapper; a revision counter re-renders consumers on every claim/release without exposing the Set.rowBusy.begin(key)/end(key), which adds double-click guards where there were none and keeps rows independent where they were serialized.Unrelated single-slot indicators (
testingId,authorizingId, the plugins page's dialog-scoped install job) are intentionally untouched — they are one-at-a-time by design, not row toggles.Verification
test/row-busy-guard.test.mjs— new: unit-drives the guard (independent rows, double-claim refused, first release leaves other rows guarded) and pins the wiring contracts on all three pages, including that the MCP toggle no longer page-freezes.test/agent-capability-settings.test.mjs— 26/26 pass unchanged;extensions-page/mcp-market-panel/settings-import-page— 29/29 pass.tsc --noEmitclean for@pi-desktop/desktop.One commit on current
main(4bedbb4).