refactor(ui): one store per fact, read by every surface #106
No reviewers
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lz/agent-nexus!106
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/store-consolidation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Sixth in the #96 stack — targets #101, not
main. No new UI; this closes functional gaps left by the audit-fix pass, all of the same shape: one fact, several surfaces, more than one owner.Session health moves into the store
Sessions.svelteowned the per-session claude probe. That component is unmounted for exactly as long as the collapsed rail is showing — so the rail's hover card could not report a live agent state, and menus opened from a rail segment or a dock tab had no answer at all.Now on
WorkspacesStore, driven from the tail ofrefresh()(past everyawait, so it can never register as a dependency of a caller's$effect). The 8s TTL, thestatus === 'running'filter and the plain-Mapbookkeeping carry over with their reasoning;Sessions.svelte'suntrackcould be deleted rather than moved.agentRunning()is three-valued on purpose —undefinedmeans "not probed", which is notfalse.menuContextForreads the storeportCountandagentRunningwere the last two fields still handed in by a caller, and only one of the four callers had them. So the rail, the dock tab and the workspace card all showed no count on the Ports row, and all three offered "Stop agent" for an agent that was already stopped. Theextraparameter is deleted, not merely unused.Browser-verified across all three surfaces:
alphaclaudeRunning: falsePorts 2, no "Stop agent"betaundefinedPorts 0, "Stop agent" presentOne
/api/quotapollThe stats store and
QuotaBadgeeach polled it on their own 10s timer, for two halves of one payload. The poll moves into the store behind a subscriber refcount, because neither caller can drive the other: the badge renders on/settingswhereMainSplitis absent, andMainSplitrenders in states where the header hides the badge.Measured in a browser — request stamps on
/: 281, 10282, 20281 ms. One timer. On/settingswithMainSplitunmounted, the badge alone still polls at 10s.The teardown path is pinned by five unit tests, each negative-controlled by breaking the guard it covers — including
expected 6 to be 3, which is the two-polls-per-tick regression this removes. That path had never executed anywhere: a browser reaches "both mounted" trivially and "neither mounted" almost never.Also
NewWorkerModaluses the sharedworkerImagesstore;load()coalesces onto the in-flight promise so the modal cannot observe a not-yet-populated list and pick the wrong default image.menu-actions.tsandtarget-menu.tsare framework-free TypeScript and move to$lib/menu/.TargetMenu.sveltestays undercomponents/.Gates
The refcount was the one part of the poll consolidation argued from the code rather than observed. A browser reaches "both subscribers mounted" trivially and "neither mounted" almost never — the only route to it is the lock screen — so the branch that actually clears the interval had never run under test or in a browser. A stranded interval polls forever and surfaces nowhere. Five cases, each negative-controlled by breaking the guard it covers: - drop the `--count === 0` guard, so any leave clears the timer -> "keeps polling while ONE subscriber remains" fails - drop the idempotent-leave guard -> "survives a double leave without stranding the timer" fails - never clearInterval -> "STOPS polling when the last subscriber leaves" fails, "expected 7 to be 1" - drop the `++count === 1` guard, so every subscribe starts a timer -> "runs ONE timer for two subscribers" fails, "expected 6 to be 3" — which is the two-polls-per-tick regression this change removed