refactor(main-page): one shared workspaces store (PR-C1) #62
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/dockview-pr-c1"
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?
Targets
feat/dockview-pr-b(#61), notmain. Stacked:main← PR-A #58 ← PR-B #61 ← this ← PR-C2.First half of PR-C. This one is invisible — no user-facing behaviour change. It exists so PR-C2 (per-panel
{workerId, sessionId}targets, retiring the singlescope) has a single source of truth to build on, and it fixes a real double-fetch on the way.Spec:
docs/superpowers/specs/2026-07-12-dockview-migration-pr-c-design.md§3Plan:
docs/superpowers/plans/2026-07-12-dockview-pr-c1.mdThe bug it fixes
Workers.sveltecalledapi.listSessions(w.id)for every running worker (to know whether the workspace shell was open). Each childSessions.svelte— one per worker — calledapi.listSessions(w.id)again for the same worker. Two identical requests per worker per tick, shipping onmaintoday.WorkspaceRail(PR-B) duplicated the logic a third time, safe only because it and<Workers>are never both mounted.Now one
WorkspacesStorepolls, driven by a single$effectinMainSplit, and everyone reads.Measured, not asserted
Driven in a real browser against a seeded instance (2 workspaces, 4 sessions), identical 32s window, same harness:
4 is the floor: one per refresh (mount + 3 ticks). The before figure is worse than the predicted 2× because
Workers.sveltealso had both anonMount(load)and atickeffect, so it double-loaded on mount on top of the double-fetch. Consolidating fixed that too.Two real bugs found in review
Both in the store, both fixed and pinned by tests that were mutation-verified (guard removed → exactly the right test fails → restored):
refresh()had no reentrancy protection. Overlapping calls committed in completion order, not start order — so a slow poll that began before a workspace was deleted could resurrect it, and one that began before a spawn could erase it. Both reproduced. Fixed with a generation counter: last-started wins, superseded calls discard rather than commit.Deliberately not fixed by coalescing onto the in-flight promise: mutation handlers do
await api.removeWorker(id); await workspaces.refresh();and must see their own write. Coalescing would hand them a promise that started before the delete, leaving a removed workspace on screen until the next tick.A superseded refresh that failed could paint a stale error over a newer success — an error banner sitting on top of correctly-loaded data. The guard existed but was pinned by no test; removing it left all 616 tests green.
The self-triggering
$effecttrapSessions.svelteread its health map for a TTL check and wrote it, from inside an effect. That was safe only by accident:await api.listSessions()ran first, pushing the read out of Svelte's synchronous tracking window. This PR removes that await.TTL timestamps therefore moved to a plain, non-reactive
Map. And the effect callsprobeHealth()insideuntrack()— load-bearing, not decoration:probeHealth()readslistsynchronously, so calling it bare would register the store's$derivedroster as a dependency, and the store hands back a fresh array reference on every refresh (committed in a later flush than the tick bump). That would fire the effect twice per poll and double-fetchsessionHealth— reintroducing, on a different endpoint, the exact duplicate request this PR exists to kill.Deliberate, please don't "clean up"
liveness(),label(),sessionName()have no caller. They are PR-C2 prerequisites and are commented as such.liveness()is three-valued (alive/dead/unknown) on purpose: C2 prunes dock panels for dead sessions, and a boolean would make a failed poll indistinguishable from a removed session — destroying the operator's layout on a transient error. It mirrorsconvergeSessions, which never hard-deletes a session row when the worktree probe fails (AGENTS.md fact #15). Delete it andloaded+sessionsLoadedbecome write-only and the whole stale-tolerance design goes with it.Dock.sveltestill has its ownlistSessions. Out of scope: PR-C2 rewrites that file wholesale.Behaviour change worth knowing
The workspace "start agent" / "stop agent" toggle no longer flips on a local optimistic flag; it awaits a store refresh and reads backend truth. One extra round trip before the button updates. Strictly more correct — the old optimism could disagree with the very next poll — but it is a real latency change.
Verification
pnpm typecheck→ 0 errors (1006 files)pnpm test→ 617 passed, of which 20 are the new store'sTest suite is node-only with no DOM, so
.sveltecomponents remain structurally untestable — the browser pass is the honest substitute, not an optional extra.label() and sessionName() have no caller in this PR. That is deliberate: PR-C2 gives each dock panel its own {workerId, sessionId} target, so a tab title must be resolved FROM the target rather than threaded down from the click that set it. Without this note a reviewer (or a code-quality pass) would reasonably delete them. Also gitignore .playwright-cli/ — regenerated on every browser-verification pass, and one stray 'git add -A' from being committed.