You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of the ADR-0004 vanilla-shell investment track (see docs/ADR-0004-ui-shell.md); scoped from the 2026-08-03 architecture review.
Goal
src/ui/dashboard.ts is 3,232 lines, of which renderDashboard (dashboard.ts:538–3232) is ONE closure containing 46 nested functions over 34 shared mutable let bindings — nothing inside is unit-testable in isolation, which is why the module's DOM-driven test file has grown to 6,494 lines. Extract two pieces of that closure into standalone, independently testable modules.
Context
The architecture around renderDashboard is already well-factored: pure math lives in core/tile-reorder.js and core/dashboard-autoscroll.js, and dashboard/layouts/* plus dashboard/application/* are properly layered with an enforced boundary test. This issue is extraction of already-identifiable seams out of the one oversized closure, not a rewrite of the surrounding architecture.
The shared mutable lets cluster cleanly enough to extract: only currentDoc (27 references) is genuinely cross-cutting across the whole closure; the rest belong to one of the two extractions below.
wireTileDrag (dashboard.ts:1684–1998, 315 lines — the single largest function in the repo)
wireGridResize (dashboard.ts:1568–1682)
the gesture-flag state cluster: gestureActive, clickSuppressCard, reorderModifierHeld, autoScroll, scrollHost, owedScrollTop (dashboard.ts:1355–1419)
→ src/ui/dashboard-tile-gestures.ts, exposing createTileGestureController({ runCommand, ... }) returning { wireTileDrag, wireGridResize, dispose }. Touches roughly 6 locals from the parent closure; the pure reorder/autoscroll math stays in core/ where it already lives — this extraction moves only the DOM/gesture wiring around it.
Repaint arbitration: the ~220-line effect() callback at dashboard.ts:2834–3053 hand-maintains five memo signatures — lastLayoutSig (:2621), lastGridSig (:2729), barSig (:2789), lastOptionsSig (:2799), lastVariablePersistSig (:2833) — to decide what to repaint. Extract a pure function
into src/dashboard/application/ (alongside the existing pure layout/application modules), leaving the effect callback as a thin caller that applies the plan. Include as an explicit test case the invariant currently documented only in a comment at dashboard.ts:2846–2858: never rebuild the variable bar on optionsRev alone — it would eat in-progress typing. That invariant deserves a real unit test, not just a comment.
Optional third extraction (do if time allows, file separately if not)
Tile chrome (dashboard.ts:1999–2348, ~350 lines: showTileMenu, runTileAction, removeTile, duplicateTile, widenTile — already backed by pure modules) → src/ui/dashboard-tile-chrome.ts.
Tests
dashboard-tile-gestures.ts: createTileGestureController unit-tested directly (drag start/move/end sequences, resize sequences, gesture-flag transitions) without needing the full renderDashboard DOM harness — this is the concrete payoff, replacing DOM-driven assertions with direct calls where possible.
dashboardRepaintPlan(prev, next): pure unit tests at 100/100/100/100 (it's a core/application-style pure function) covering each of the five memo signatures independently and in combination, including the optionsRev-must-not-rebuild-bar invariant as an explicit named test case.
Existing dashboard.ts test file shrinks correspondingly as gesture and repaint-decision assertions move to the new modules' direct unit tests instead of full-render DOM simulation; net test-file size should decrease even though total assertions don't.
The existing pure-module boundary test (that enforces core/dashboard/layouts/dashboard/application don't import DOM) is extended to cover the new dashboard/application/ repaint-plan module.
npm test (coverage gate) and tsc --noEmit pass.
Acceptance criteria
wireTileDrag and wireGridResize no longer live inside renderDashboard's closure; they're callable and testable via createTileGestureController in dashboard-tile-gestures.ts.
Repaint decisions (rebuildBar/republishFlow/repaintGrid/persistVars) are computed by a pure, directly-unit-tested dashboardRepaintPlan function, not inline inside the effect() callback.
The optionsRev-must-not-rebuild-variable-bar invariant has a real test, not just a comment.
renderDashboard's closure shrinks by roughly 500–700 lines (gesture controller + repaint plan, plus tile chrome if done).
dashboard.ts's DOM-driven test file shrinks as a consequence of assertions moving to direct unit tests on the new modules.
npm test (coverage gate) and tsc --noEmit pass.
Non-goals
Rewriting or redesigning drag/resize/repaint behavior — this is structural extraction of existing logic, no functional change.
Extracting currentDoc or other genuinely cross-cutting closure state into a shared service — it stays in renderDashboard since it's used across both extractions and more.
The optional tile-chrome extraction is not required for this issue to be considered done; it may be filed/landed separately.
Part of the ADR-0004 vanilla-shell investment track (see
docs/ADR-0004-ui-shell.md); scoped from the 2026-08-03 architecture review.Goal
src/ui/dashboard.tsis 3,232 lines, of whichrenderDashboard(dashboard.ts:538–3232) is ONE closure containing 46 nested functions over 34 shared mutableletbindings — nothing inside is unit-testable in isolation, which is why the module's DOM-driven test file has grown to 6,494 lines. Extract two pieces of that closure into standalone, independently testable modules.Context
The architecture around
renderDashboardis already well-factored: pure math lives incore/tile-reorder.jsandcore/dashboard-autoscroll.js, anddashboard/layouts/*plusdashboard/application/*are properly layered with an enforced boundary test. This issue is extraction of already-identifiable seams out of the one oversized closure, not a rewrite of the surrounding architecture.The shared mutable
lets cluster cleanly enough to extract: onlycurrentDoc(27 references) is genuinely cross-cutting across the whole closure; the rest belong to one of the two extractions below.Two extractions
Drag/resize gesture controller (~500 lines total):
wireTileDrag(dashboard.ts:1684–1998, 315 lines — the single largest function in the repo)wireGridResize(dashboard.ts:1568–1682)gestureActive,clickSuppressCard,reorderModifierHeld,autoScroll,scrollHost,owedScrollTop(dashboard.ts:1355–1419)→
src/ui/dashboard-tile-gestures.ts, exposingcreateTileGestureController({ runCommand, ... })returning{ wireTileDrag, wireGridResize, dispose }. Touches roughly 6 locals from the parent closure; the pure reorder/autoscroll math stays incore/where it already lives — this extraction moves only the DOM/gesture wiring around it.Repaint arbitration: the ~220-line
effect()callback atdashboard.ts:2834–3053hand-maintains five memo signatures —lastLayoutSig(:2621),lastGridSig(:2729),barSig(:2789),lastOptionsSig(:2799),lastVariablePersistSig(:2833) — to decide what to repaint. Extract a pure functioninto
src/dashboard/application/(alongside the existing pure layout/application modules), leaving the effect callback as a thin caller that applies the plan. Include as an explicit test case the invariant currently documented only in a comment atdashboard.ts:2846–2858: never rebuild the variable bar onoptionsRevalone — it would eat in-progress typing. That invariant deserves a real unit test, not just a comment.Optional third extraction (do if time allows, file separately if not)
Tile chrome (
dashboard.ts:1999–2348, ~350 lines:showTileMenu,runTileAction,removeTile,duplicateTile,widenTile— already backed by pure modules) →src/ui/dashboard-tile-chrome.ts.Tests
dashboard-tile-gestures.ts:createTileGestureControllerunit-tested directly (drag start/move/end sequences, resize sequences, gesture-flag transitions) without needing the fullrenderDashboardDOM harness — this is the concrete payoff, replacing DOM-driven assertions with direct calls where possible.dashboardRepaintPlan(prev, next): pure unit tests at 100/100/100/100 (it's acore/application-style pure function) covering each of the five memo signatures independently and in combination, including theoptionsRev-must-not-rebuild-bar invariant as an explicit named test case.dashboard.tstest file shrinks correspondingly as gesture and repaint-decision assertions move to the new modules' direct unit tests instead of full-render DOM simulation; net test-file size should decrease even though total assertions don't.core/dashboard/layouts/dashboard/applicationdon't import DOM) is extended to cover the newdashboard/application/repaint-plan module.npm test(coverage gate) andtsc --noEmitpass.Acceptance criteria
wireTileDragandwireGridResizeno longer live insiderenderDashboard's closure; they're callable and testable viacreateTileGestureControllerindashboard-tile-gestures.ts.rebuildBar/republishFlow/repaintGrid/persistVars) are computed by a pure, directly-unit-testeddashboardRepaintPlanfunction, not inline inside theeffect()callback.optionsRev-must-not-rebuild-variable-bar invariant has a real test, not just a comment.renderDashboard's closure shrinks by roughly 500–700 lines (gesture controller + repaint plan, plus tile chrome if done).dashboard.ts's DOM-driven test file shrinks as a consequence of assertions moving to direct unit tests on the new modules.npm test(coverage gate) andtsc --noEmitpass.Non-goals
dashboard-viewer-session.ts— already tracked by Split dashboard-viewer-session.ts: a 1857-line file whose 1354-line closure holds every concern #453; cross-reference it, don't duplicate the work here.currentDocor other genuinely cross-cutting closure state into a shared service — it stays inrenderDashboardsince it's used across both extractions and more.Related
dashboard-viewer-session.ts; cross-reference, don't duplicate.docs/ADR-0004-ui-shell.md)