refactor(#276): extract WorkbenchSession — phase 3a#281
Merged
Conversation
src/ui/workbench/workbench-session.ts is the route-scoped owner of the workbench running operation: run/runScript/runEntry/cancel orchestration moved verbatim (F6 wave-clock ordering, batch() discipline, finally teardown order, result.source-before-running-flip). The run bookkeeping fields (runT0/runQueryId/runTick) and the in-flight AbortController are PRIVATE session state — the RunState cast and AppState.abortController are deleted (issue rule 5: cancellation state belongs to its owner). The session is the sole production writer of `running`; shared signals (resultView/resultSort/forceExplain/...) stay on AppState via a narrow WorkbenchStateSlice (structurally satisfied by AppState — never the App object). DOM/render stays in the shell through injected hooks (renderResults/toast-owning varGate/recordHistory/tickElapsed/ onAuthFailed → chCtx.onSignedOut); pure helpers (tabPanel/savedForTab/ mergedSourceArgs/panelExecution/...) are direct imports, not hooks. savedForTab/recordScriptHistory in state.ts narrowed to Picks (type-only). The three run-coupled renderApp effects now register through session.attachShell(...) with captured disposers — idempotent on re-render, which also fixes a latent double-registration on the connect → renderApp path. destroy() disposes effects, clears the elapsed ticker, aborts + KILLs an in-flight operation; unit-proven (no production caller until Phase 5's route shells — documented). Split from the issue's Phase 3 by plan review: 3a = workbench (this), 3b = DashboardSession (next PR). build/check-boundaries.mjs generalized to a RULES list and now also enforces: ui/workbench ↔ ui/dashboard must not import each other; ui/dashboard must not import the editor. actions.run/cancel plus explainQuery/setExplainView/setResultRowLimit delegate to the session; app.elapsedMs delegates; 4 app.test.ts tests rewritten from private-state pokes to observable behavior (fetch counts, live query_id on the KILL wire). app.ts sheds ~430 lines. Gate: 105 files / 3052 tests green; build green; check:arch green (3 files / 2 active rules). New session spec: 52 tests, file at 100/98.7/100/100. Part of #276 (phase 3a; roadmap #68 updated). Claude-Session: ad39bf19-1690-43c7-abb4-f0084fca00a2 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
7 tasks
BorisTyshkevich
added a commit
that referenced
this pull request
Jul 17, 2026
src/ui/dashboard/dashboard-session.ts is the route-scoped owner of the
dashboard execution runtime: the 6-way tile pool, wave generations
(reserved at wave CREATION, not worker start — the race-closing
invariant), per-slot AbortControllers, runFavoriteSource's
gate/stream/classify core, filter waves + curated-field merge +
persistence ports, runAffected's cascade, and retryFilter.
Constructible without App (issue rule 1); input is an explicit
DashboardRuntimeInput built by the shell from the favorites list, so a
stored DashboardDocument can replace that source later without touching
the session. Every DOM write stays in the shell behind injected hooks;
slots keep their route's own DOM nodes for now (session-state/shell-DOM
split deferred to Phase 5, documented). Tile supersede remains
client-abort only — never a server KILL (intentional, preserved).
renderDashboard(app) keeps its signature as the integration entry —
tests/unit/dashboard.test.ts passed UNMODIFIED, the strongest
behavior-preservation signal this extraction has.
destroy() aborts all in-flight tile/KPI/filter work, tears down chart
instances, disposes the filter bar, and flips a destroyed flag that
turns runAll/runAffected/retryFilter into no-ops (re-checked after the
token-preflight await too) — so an orphaned filter-bar debounce timer
firing post-teardown can never issue a request or trigger a sign-out.
buildFilterBar now returns { el, dispose() }; the dashboard shell and
the detached Data view both dispose the previous bar on re-render,
closing today's orphan-timer gap. Review round also added a
stale-generation guard on streamed progress: a superseded request's
last buffered chunk can no longer write into the newer wave's live
loading label (the pre-refactor closure-captured label was immune;
the guard restores that).
Gate: 106 files / 3090 tests green (dashboard suite untouched), build
green, check:arch green (ui/dashboard rules active: no ui/workbench,
no editor imports). e2e chromium+webkit fully green; firefox 40/40
serialized (parallel-run flake is the documented environment issue).
New session spec: 36 tests; dashboard-session.ts 100/94.9/100/100,
dashboard.ts 100/97.9/97.4/100, filter-bar.ts 100/100/100/100.
Part of #276 (phase 3b — completes Phase 3 with #281; roadmap #68
updated).
Claude-Session: ad39bf19-1690-43c7-abb4-f0084fca00a2
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 17, 2026
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 & why
Part of #276 — Phase 3a of the app.ts → services refactor (PRs #278 Phase 0–1, #279 Phase 2 merged). The issue's Phase 3 covers both route sessions; plan review split it into 3a (WorkbenchSession, this PR) and 3b (DashboardSession, next) — the two domains are mutually non-importing by requirement and independently revertable.
WorkbenchSession(src/ui/workbench/workbench-session.ts, 52-test spec at 100/98.7/100/100) is the route-scoped owner of the workbench running operation:run/runScript/runEntry/cancelmoved verbatim (F6 wave-clock ordering,batch()discipline, finally teardown order,result.source-before-running-flip all preserved with their comments). Per issue rule 5, cancellation state now belongs to its owner:runT0/runQueryId/runTickand the in-flightAbortControllerare private session fields — theRunStatecast andAppState.abortControllerare deleted.running; shared signals stay onAppStatevia a narrowWorkbenchStateSlice(structurally satisfied byAppState; never theAppobject — rule 1).renderResults,varGateBlocked(toast-owning),recordHistory,tickElapsed,onAuthFailed → chCtx.onSignedOut()); pure helpers are direct imports, not hooks (no god-bag).savedForTab/recordScriptHistorynarrowed toPicks instate.ts(type-only).renderAppeffects now register viasession.attachShell(...)with captured disposers — idempotent on re-render, which incidentally fixes a latent double-registration on theconnect → renderApppath.destroy()disposes effects, clears the elapsed ticker, aborts +KILLs an in-flight operation — unit-proven per the acceptance criteria ("Both route sessions exposedestroy()and tests prove cleanup"); no production caller until Phase 5's route shells (documented in the module header).build/check-boundaries.mjsgeneralized to a rule list:src/application⇒ noui/editor(Phase 0 rule);ui/workbench⇔ui/dashboardmust not import each other;ui/dashboardmust not import the editor (Phase 3 rules, active as the directories appear).Call-site inventory (from plan review):
actions.run/cancelplus the three additionalrun()callers (explainQuery/setExplainView/setResultRowLimit) delegate to the session;app.elapsedMsdelegates; 4app.test.tstests rewritten from private-state pokes to observable behavior (fetch counts; the livequery_idasserted on the KILL QUERY wire).app.tssheds ~430 lines.dashboard.ts/results.ts/shortcuts.tsuntouched.Review: 4-angle pass — removed-behavior and cross-file angles clean (the latter re-ran the full gate itself); fixes applied: stale module header,
WorkbenchResultViewnow derived fromAppState['resultView']['value'](no drifting hand-copy), two unnecessary test double-casts removed.Verification: full gate green (105 files / 3,052 tests + tsc + check:arch), build green.
Acceptance criteria progress: Workbench route-scoped session with owned cancellation state ✅;
destroy()with proven cleanup ✅ (workbench half); route-session import boundary enforced ✅.Checklist
npm testpasses (the per-file coverage gate is non-negotiable)npm run buildsucceeds (single-filedist/sql.html)src/core/, network insrc/net/(injected fetch), DOM insrc/ui/CHANGELOG.md([Unreleased]) updated if behavior or the deployed surface changed🤖 Generated with Claude Code