Skip to content

Refactor App.fs into focused view and state-slice modules#82

Merged
0101 merged 12 commits into
mainfrom
code-improvement
Jun 22, 2026
Merged

Refactor App.fs into focused view and state-slice modules#82
0101 merged 12 commits into
mainfrom
code-improvement

Conversation

@0101

@0101 0101 commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Problem

src/Client/App.fs had grown to 1861 lines, mixing the Elmish update, subscriptions, and three large families of pure-render view code (worktree cards, the mascot eyes, the status overview/footer) plus the canvas-pane wiring. The mascot "slice" also secretly co-owned the app's user-activity/idle state — which actually drives polling cadence, server telemetry, and canvas auto-display, not the eyes.

This is a behavior-preserving code-quality branch: no features added or changed. The E2E suite (Playwright, asserting on DOM/CSS classes) proves the rendered UI is identical before and after.

Changes

App.fs view extraction (1861 -> 795 lines) — an evidence-driven hybrid: a vertical slice only where a feature owns cohesive state + behaviour; props/callback records where it is render-over-shared-state; plain extraction where there is no owned state.

  • OverviewViews.fs — status-overview row + scheduler footer (pure render).
  • CardViews.fs — all worktree-card rendering, now taking CardViewProps + CardCallbacks records instead of repoSection's former 9 positional args.
  • MascotState.fs / MascotView.fs — the mascot eyes as a gaze slice + eye SVGs.
  • CanvasView.fs — the canvas-pane view wiring lifted out of the top-level view.
  • Shared formatters relocated into Components.fs (single source of truth).
  • Flat Msg + single update preserved throughout (no Cmd.map sub-component split), mirroring the existing canvas seam.

Activity / mascot separation of concerns — split user-activity / idle-detection state out of the mascot into a dedicated ActivityState.fs + ActivityUpdate.fs slice (owns LastActivityTime/ActivityLevel, computeActivityLevel, the Tick/UserActivity bodies, and the activity subscription). The mascot is now a pure widget that observes ActivityLevel; MascotUpdate.fs fell empty and was deleted.

Review-rule + docs housekeeping

  • review/rules/immutability.md: forbid using a ref cell to dodge the rule — let mutable is the sanctioned, comment-justified mechanism when mutation is genuinely required.
  • Deleted the one-off app-fs-view-extraction specs (red-flag -extraction filename; transient implementation plan).
  • Refreshed Key Files references in canvas-pane.md, user-idle-detection.md, worktree-monitor.md, resume-last-session.md, and contextual-actions.md to the new module layout (behaviour text unchanged).
  • Added docs/spec/future/code-improvements.md — a running improvement backlog + the worktree -> /bd-plan -> /bd-execute -> PR workflow.

Tests

Behaviour-preserving; verified green at every step and end-to-end:

  • npm run build (Fable + vite): clean.
  • Unit: 755/755, Fast: 869/869, E2E (Playwright): 331 passed, 1 pre-existing skip, 0 failed.
  • Structure: App.fs 795 lines; all seven new modules wired into Client.fsproj in valid compile order.

Each extraction task additionally passed a focused multi-model code review before the final verification.

0101 added 10 commits June 19, 2026 17:41
…cheduler footer)

Relocate status-overview + scheduler-footer pure views into new src/Client/OverviewViews.fs (compiles before AppTypes, after CanvasState.fs). Move shared formatters stepStatusClassName/stepStatusText/relativeEventTime into Components.fs per spec Decision 7; App.fs keeps thin re-export aliases so card-side call sites stay untouched. Repoint App.fs view to OverviewViews.schedulerFooter. Pure relocation, no logic change.
…-place card refactor)

Introduce Msg-free CardViewProps (8-field model read-slice) and CardCallbacks
records mirroring CanvasPaneCallbacks. Convert card leaf helpers and composite
views (compactWorktreeCard/worktreeCard/renderCard/repoSectionHeader/repoSection)
from raw dispatch to the records; build cardProps/cardCallbacks in view() and
update the repoSection call site. Drop pre-existing dead args (renderCard repoId,
worktreeCard canvasPaneOpen) and the archiveSection dispatch wrapper. Update spec
Decisions with the leaf-helper-conversion note.
…CardViews.fs

Created src/Client/CardViews.fs (module CardViews) holding the CardViewProps/
CardCallbacks records plus all card leaf helpers moved verbatim from App.fs:
icons, action buttons, beads/build badges, sync helpers, event-log and PR
helpers, class/text/format helpers, and canvasEventEntry/canvasEventLog.
CardViews.fs compiles before AppTypes.fs. App.fs adds 'open CardViews' and
drops the now-dead stepStatusClassName/stepStatusText aliases. Composite card
views intentionally remain in App.fs for Step 4.
…fs and wire view()

Move 10 composite card defs (canResumeSession, compactWorktreeCard, worktreeCard, renderCard, skeletonCard/skeletonGrid, sortLabel, providerIcon, repoSectionHeader, repoSection) verbatim from App.fs into CardViews.fs; drop 6 now-dead Components aliases. view() call sites resolve via existing 'open CardViews'.
…el.Mascot)

Introduce MascotState sub-state (EyeDirection/LastActivityTime/ActivityLevel + empty, randomEyeDirection, computeActivityLevel, idle thresholds) compiled before AppTypes. Embed as Model.Mascot (drop the 3 flat fields) and repoint init/DataLoaded/Tick/UserActivity/appSubscriptions/header to model.Mascot.*. Update 4 test Model literals/reads (CanvasAwareness, ConfirmModal, CreateWorktree, IdleDetection).
…ot slice)

MascotView.fs (before AppTypes): viewEyeOpen reshaped to take a MascotState
slice; viewEyeRolledBack/viewEyeClosed verbatim. Header wired to MascotView.*.
MascotUpdate.fs (after CanvasUpdate.fs): tickActivity + userActivity arm bodies
and the activityDetection subscription. Root update delegates (flat Msg kept;
Tick stays in root for canvas-event expiry + poll). appSubscriptions points to
MascotUpdate.activityDetection. Behavior-preserving extraction.
…portunity B)

Move focusedWorktreeCanvasDoc + the canvas-pane wiring block out of App.fs's
view into a new CanvasView.fs module exposing view(model, dispatch). Registered
in Client.fsproj after MascotUpdate.fs, before App.fs. App.fs 847->794 lines;
pure mechanical relocation, no behavior change.
…ction specs

- review/rules/immutability.md: never recommend or use a ref cell to dodge the
  rule; let mutable is the sanctioned mechanism when mutation is justified, and
  must carry an inline comment explaining why an immutable solution doesn't fit.
- MascotUpdate.fs: document the throttle-closure let mutable (per user-idle-detection).
- Delete docs/spec/app-fs-view-extraction.md and its future/ companion (red-flag
  one-off -extraction filenames; feature complete). Strip dangling spec refs from
  CanvasView/MascotView/MascotUpdate headers and fix the dead link in canvas-pane.md.
Move idle detection and activity updates into ActivityState and ActivityUpdate. Keep mascot state to gaze only and pass observed ActivityLevel into the eye view. Repoint tests and remove MascotUpdate.
After the App.fs view extraction + activity/mascot SoC split, several specs still
pointed at old App.fs locations. Update their Key Files tables / module references
(canvas-pane, user-idle-detection, worktree-monitor, resume-last-session,
contextual-actions) to the new modules (ActivityState/ActivityUpdate, CardViews,
OverviewViews, MascotState/MascotView, CanvasView). Behavior text unchanged.

Add docs/spec/future/code-improvements.md: a running backlog of improvement
candidates plus the worktree -> /bd-plan -> /bd-execute -> PR workflow.
Copilot AI review requested due to automatic review settings June 22, 2026 13:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This is a behavior-preserving code-quality refactor that breaks the monolithic src/Client/App.fs into focused view and state-slice modules, and—separately—untangles user-activity/idle-detection state from the mascot. The Elmish architecture is unchanged: a flat Msg and a single update are preserved, and extracted functions are pure body/view relocations (mirroring the existing CanvasState/CanvasUpdate seam). The card views move to records (CardViewProps/CardCallbacks) instead of long positional argument lists, narrowing each leaf's capability from raw dispatch to named actions. I verified the new module compile order, that every callback/prop maps to the previous dispatch behavior, and that all references (tests included) were updated with no dangling symbols.

Changes:

  • Extract App.fs view code into OverviewViews.fs, CardViews.fs, MascotView.fs, MascotState.fs, and CanvasView.fs; relocate shared formatters into Components.fs.
  • Split activity/idle state out of the mascot into ActivityState.fs + ActivityUpdate.fs; Model now carries Activity and Mascot slices (replacing EyeDirection/LastActivityTime/ActivityLevel).
  • Documentation/review-rule housekeeping: refresh Key Files references, tighten immutability.md (forbid ref-cell dodges), delete the stale extraction spec, and add a code-improvements.md backlog.
Show a summary per file
File Description
src/Client/App.fs Slimmed to init/update/subscriptions/top-level view; wires CardViewProps/CardCallbacks and delegates to new modules
src/Client/AppTypes.fs Model gains Activity/Mascot slices, drops EyeDirection/LastActivityTime/ActivityLevel
src/Client/CardViews.fs New module: all worktree-card rendering via props/callback records (replaces 9 positional args)
src/Client/OverviewViews.fs New module: status-overview row + scheduler footer (pure render)
src/Client/CanvasView.fs New module: canvas-pane view wiring lifted from view
src/Client/MascotView.fs / MascotState.fs New modules: eye SVGs + gaze slice (randomEyeDirection)
src/Client/ActivityState.fs / ActivityUpdate.fs New modules: activity slice + thresholds/computeActivityLevel, and Tick/UserActivity/activityDetection bodies
src/Client/Components.fs Relocated stepStatusClassName/stepStatusText/relativeEventTime as single source of truth
src/Client/Client.fsproj Adds the seven new modules in valid compile order
src/Tests/{IdleDetection,CreateWorktree,ConfirmModal,CanvasAwareness}Tests.fs Update to new Model shape and ActivityState-qualified helpers
review/rules/immutability.md Clarifies justified, scoped let mutable is compliant; forbids ref-cell workarounds
docs/spec/{worktree-monitor,user-idle-detection,resume-last-session,contextual-actions,canvas-pane}.md Refresh Key Files/module references to new layout
docs/spec/future/code-improvements.md New improvement backlog (line-count figure is off—see comment)
docs/spec/future/app-fs-view-extraction.md Deleted (transient plan; no remaining references)

Copilot's findings

  • Files reviewed: 23/23 changed files
  • Comments generated: 1

Comment thread docs/spec/future/code-improvements.md Outdated

## Done

- **App.fs view extraction** — `src/Client/App.fs` 1685 → 705 lines. Extracted

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — confirmed and fixed. Root cause: the figures came from PowerShell's Measure-Object -Line, which silently undercounts blank lines (~10% here). The authoritative (Get-Content).Count is 1861 on main and 795 now — a 1066-line (57%) reduction. Corrected in the backlog doc (commit 5b94c21) and the PR description.

0101 added 2 commits June 22, 2026 17:02
The reported figures came from PowerShell's Measure-Object -Line, which
undercounts blank lines (~10% here). The authoritative count ((Get-Content).Count)
is 1861 on main and 795 now — a 1066-line (57%) reduction. Caught in PR review.
@0101 0101 enabled auto-merge (squash) June 22, 2026 15:12
@0101 0101 merged commit 69188b2 into main Jun 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants