Skip to content

SPEC-173 — Implementation Report

Status: OK (Clean)

Spec: docs/specs/173-dashboard-worktree-panel.md Plan: docs/plans/173-dashboard-worktree-panel.plan.md Acceptance test: src/tests/acceptance/173-dashboard-worktree-panel.acceptance.test.ts

Files Created

PathDescription
src/modules/worktree-management/entities/sweep/lastSweepSummary.schema.tsZod schema + LastSweepSummary type (ranAt + counters)
src/modules/worktree-management/entities/worktree/worktreeSizeProbe.gateway.tsGateway contract — probe(path) → Promise<number | null>
src/modules/worktree-management/interface-adapters/gateways/worktreeSizeProbe.cli.gateway.tsCLI impl — du -sb via injectable process runner
src/modules/worktree-management/interface-adapters/presenters/worktreePanel.presenter.tsStatus thresholds (24h/7d), grouping, 30s size cache, null aggregation
src/modules/worktree-management/interface-adapters/controllers/http/worktreeOverview.routes.tsGET /api/worktrees + POST /api/worktrees/sweep (200/409/500/503)
src/dashboard/modules/worktreePanel.jsHumble object — render section / empty state / status badge + fetch helpers
src/dashboard/vendor/anime.esm.min.jsVendored anime.js v4.4.1 ESM bundle (copied from node_modules at build time)
src/tests/stubs/worktreeSizeProbe.stub.tsStub gateway with per-path size mapping + default
src/tests/factories/lastSweepSummary.factory.tsFactory for sweep summaries
src/tests/units/modules/worktree-management/entities/sweep/lastSweepSummary.schema.test.tsSchema validation tests (4)
src/tests/units/modules/worktree-management/interface-adapters/presenters/worktreePanel.presenter.test.tsPresenter tests (14) — thresholds, grouping, cache, aggregation
src/tests/units/modules/worktree-management/interface-adapters/controllers/http/worktreeOverview.routes.test.tsRoutes tests (7) — GET/POST/409/500/503
src/tests/units/modules/worktree-management/interface-adapters/gateways/worktreeSizeProbe.cli.gateway.test.tsCLI gateway tests (5) — parsing, errors, command shape
src/tests/units/dashboard/modules/worktreePanel.test.tsHumble-object tests (26) — render, escape, badges, fetch, sweep
src/tests/acceptance/173-dashboard-worktree-panel.acceptance.test.tsOuter-loop acceptance test (6 scenarios)

Files Modified

PathReason
src/frameworks/scheduler/worktreeSweepScheduler.tsAdded module state, getLastSweep, getNextSweepEta, runSweepNow (discriminated union return + concurrency guard)
src/tests/units/frameworks/scheduler/worktreeSweepScheduler.test.ts5 new tests covering the SPEC-173 extensions
src/main/dependencies.tsAdded worktreeSizeProbeGateway, worktreePanelPresenter, sweepSchedulerControls: null (filled in server.ts)
src/main/server.tsStarted scheduler before buildServer; populated deps.sweepSchedulerControls with discriminated-union runSweepNow proxy
src/main/routes.tsRegistered worktreeOverviewRoutes with the composition root options
src/dashboard/index.htmlInserted <section id="worktree-section"> between #cleanup-section and .refresh-info; imported worktreePanel.js; wired 30s polling, sweep button handler, anime.js choreography (broom-swipe, metric stagger, change-flash) gated by prefers-reduced-motion
src/dashboard/styles.cssAppended #worktree-section styles — scoped tokens, corner-bracket frames, monospace numerics, pulse keyframes, reduced-motion override
src/dashboard/modules/i18n.jsAdded EN + FR worktree.* keys
scripts/copyAssets.mjsCopies anime.esm.min.js into src/dashboard/vendor/ and dist/dashboard/vendor/ at build time
biome.jsonIgnores src/dashboard/vendor/** (minified anime.js bundle would fail lint)
package.jsonAdded animejs@^4 (locked-in choice for choreography)

Tests

SuiteCountResult
Acceptance — SPEC-1736PASS
lastSweepSummary.schema4PASS
worktreePanel.presenter14PASS
worktreeOverview.routes7PASS
worktreeSizeProbe.cli.gateway5PASS
dashboard/modules/worktreePanel26PASS
worktreeSweepScheduler (existing + 5 new)9PASS
Full suite1798PASS

yarn verify (typecheck + lint + test:ci) green.

Self-Review

CriterionStatus
Imports use @/ alias + .js extensionOK — no relative imports in new files
No any, no as Type assertions, no ! non-nullOK — grep clean
null used for absence in domain typesOK — LastSweepSummary | null, sizeBytes: number | null
Factories used (no hardcoded test data)OK — LastSweepSummaryFactory + stub builders
Stubs cover I/O boundaries onlyOK — StubWorktreeSizeProbeGateway, ConfigurableWorktreeGateway
State-based assertions (Detroit)OK — assertions on returned payloads/view models
Tests in EnglishOK
User-facing strings in French/English (i18n)OK — i18n.js keys, EN values now, FR ready
Visual DNA: // WORKTREE POOL · N, ●ACTIVE/○IDLE/◆STALE, corner brackets, monospace, scoped tokensOK
prefers-reduced-motion disables anime.js + CSS animationsOK — early return in loadAnimeApi, animation: none !important in CSS
Inline SVG total < 12 KBOK — ~1.2 KB of inline SVG
runSweepNow returns discriminated union (no throw for control flow)OK
Scheduler controls propagate via optional Dependencies fieldOK — sweepSchedulerControls set after startWorktreeSweepScheduler
Grouping by (platform, projectPath), not by statusOK — presenter groups; status is a per-row chip

Iterations: 1 review-fix pass — found Biome choking on the vendored minified anime.js; resolved by adding files.ignore to biome.json. No other violations.

Acceptance Test

File: src/tests/acceptance/173-dashboard-worktree-panel.acceptance.test.ts Status: GREEN (6/6)

Scenario from specTest in acceptance fileStatus
1 — list with active + idle + staleScenario 1 — list worktrees with active + idle + staleOK
2 — empty worktree poolScenario 2 — empty worktree poolOK
5 — manual sweep successScenario 5 — manual sweep successOK
6 — manual sweep conflict (409)Scenario 6 — manual sweep conflictOK
7 — lastSweep null on cold startScenario 7 — lastSweep null on cold startOK
FR-3 — scheduler exposes controls consumed by routesScheduler integration (FR-3)OK

Spec Coverage

Rule / ScenarioCovered byTest
Rule — gateway never decides "stale"/"active"; status from mtime thresholds in presenterPresenter status thresholdsworktreePanel.presenter.test.ts (status thresholds describe block, 4 cases)
Rule — same use case for scheduled + manual sweeprunSweepNow reuses sweepStaleWorktrees via runSweepInternalworktreeSweepScheduler.test.ts "runSweepNow runs the sweep and returns an ok result"
Rule — only one sweep at a time; concurrent → conflict, no queuerunningSince flag in schedulerworktreeSweepScheduler.test.ts "runSweepNow returns a conflict result when a sweep is already running"
Rule — size-probe failures degrade gracefully to nullWorktreeSizeProbeCliGateway returns null on exit≠0 / throwworktreeSizeProbe.cli.gateway.test.ts "returns null when du exits with a non-zero code" / "throws"
Rule — every animation honours prefers-reduced-motionloadAnimeApi early-returns null when reduced-motion is set; CSS media-query animation: none !importantVisual verification (manual) + the CSS rule is in styles.css
Rule — visual artifacts ship inline as SVG (no remote font / external SVG file)Inline <svg> markup in worktreePanel.js; anime.js is vendored locally, not loaded from a CDNManual review
Rule — 30s cache horizon on worktree statePresenter sizeCache with 30s TTLworktreePanel.presenter.test.ts "size cache (30s TTL)" — 2 tests
Scenario 1 — active/idle/stale rowsPresenter + routesacceptance Scenario 1
Scenario 2 — empty pool → empty-state SVGPresenter returns empty groups; dashboard module renders empty-stateacceptance Scenario 2 + dashboard renderWorktreeEmptyState test
Scenario 3 — grouping + sortPresenter groups & sortsworktreePanel.presenter.test.ts "grouping and sorting" — 2 tests
Scenario 4 — size probe failure on one rowPresenter aggregates skipping nullsworktreePanel.presenter.test.ts "sums total size skipping null entries"
Scenario 5 — manual sweep successRoutes POST 200 pathacceptance Scenario 5 + routes test
Scenario 6 — manual sweep conflictRoutes POST 409acceptance Scenario 6 + routes test
Scenario 7 — lastSweep: null on cold startScheduler returns null until first sweepacceptance Scenario 7 + scheduler test "returns null from getLastSweep when no sweep has run yet"
Scenario 8 — next sweep ETAScheduler getNextSweepEta mathworktreeSweepScheduler.test.ts "getNextSweepEta returns now + interval"
Scenario 9 — reduced motionAnime.js gated, CSS animation resetCode review (CSS + JS guard)
Scenario 10 — size cache hit within TTLPresenter caches in memoryworktreePanel.presenter.test.ts "does not re-probe the same path within the TTL window"
Scenario 11 — size cache miss after expiryPresenter expiry mathworktreePanel.presenter.test.ts "re-probes after the TTL window expires"
FR-1 GET schemaRoutes + presenterroutes test + acceptance
FR-2 POST sweep (200/409/500)Routesroutes test (4 cases)
FR-3 scheduler exposes lastSweep/nextSweep/runSweepNowScheduler extensionsscheduler test (5 new cases) + acceptance "Scheduler integration"
FR-4 size probe via du -sb, cached 30s, null on failureCLI gateway + presenter cacheCLI gateway tests + presenter cache tests
FR-5 presenter ISO formattingPresenterpresenter test "formats lastSweep ranAt as ISO string" / "formats nextSweepAt as ISO string"
FR-6 dashboard module exportsworktreePanel.jsdashboard module test (26 tests)
FR-7 visual DNACSS + JS module + inline SVGManual visual review (no automated assertion beyond DOM strings — by design for a humble UI module)
FR-8 i18n keysi18n.js updated for EN + FRManual code review

Open Issues

None blocking.

Notes for the orchestrator:

  1. Vendoring anime.js — the dashboard ships as static HTML/CSS/JS (no bundler), so anime.js is copied as src/dashboard/vendor/anime.esm.min.js and refreshed at build time by scripts/copyAssets.mjs. The browser imports it lazily via await import('./vendor/anime.esm.min.js') only when reduced-motion is not set — keeping the cold-load cost zero for the reduced-motion path.
  2. biome.json now ignores src/dashboard/vendor/** so the minified bundle does not block lint. Same exclusion applies to dist/** and node_modules/** (which were not previously ignored explicitly).
  3. FR-7 visual choices that are subjective and might be revisited: the corner-bracket frames are 12px and use border-top/border-left + border-bottom/border-right (2 corners each on ::before/::after). The metric tile grid is 5 columns at all viewport widths — narrow screens will get horizontal scroll on .worktree-table-wrapper. No responsive breakpoint added (out-of-scope for this spec).
  4. Dependencies.sweepSchedulerControls is intentionally null after createDependencies() and only filled by startServer() before buildServer(). Tests using createServer() will hit the 503 path on the new routes — acceptable per plan's "Open implementation decisions §2".

Released under the MIT License.