Skip to content

Split workspace runtime.ts into runtime + debug + snapshot + workflows + app #1080

@gfargo

Description

@gfargo

Summary

src/workstation/surfaces/workspace/runtime.ts is a 900-LOC file that mixes the Ink boot sequence, the diagnostic ring buffer, the non-TTY snapshot fallback, the React component, and six async workflow callbacks. Splitting it into focused modules makes each piece testable in isolation and brings the file closer to src/workstation/runtime/app.ts's shape.

Current state

runtime.ts contains:

  • Boot sequence: loadWorkspaceInkRuntime (~10 LOC), startWorkspace (~80 LOC).
  • Snapshot fallback: renderWorkspaceSnapshot (~25 LOC) — used in non-TTY environments.
  • Debug ring buffer: WORKSPACE_DEBUG_BUFFER, flushWorkspaceTrace, installWorkspaceDebugHandlers, workspaceDebug, plus six process.on handlers (~70 LOC, lines 295-374).
  • Component (WorkspaceInkApp): ~500 LOC. Inside it:
    • Six async workflow callbacks: refresh, refreshRow, drillIn, requestDelete, confirmDelete, commitAddRepo, openAddRepo.
    • Eight useRef mirrors so the stable useInput handler can reach the latest closure.
    • Multiple useEffect blocks: mount discovery, spinner tick, resume ref, preferences persistence.
    • The Ink input handler with the modal-focus dispatch.

The mount-effect, refresh, confirmDelete's refresh-tail, and commitAddRepo's refresh-tail all repeat the same pattern: set-loading → load → replace-overview → write-cache → load-prs (with onRepoComplete) → set-status. That's ~120 LOC of repetition.

Proposed split

runtime/
  index.ts           — re-exports startWorkspace / WorkspaceExitResult / WorkspaceResumeState
  app.tsx            — WorkspaceInkApp (~300 LOC after extraction)
  boot.ts            — startWorkspace + loadWorkspaceInkRuntime
  snapshot.ts        — renderWorkspaceSnapshot (non-TTY path)
  debugTrace.ts      — ring buffer + flushWorkspaceTrace + process handlers
  workflows.ts       — Six async callbacks rewritten as pure functions:
                       (deps, state) => Promise<WorkspaceAction[]>
                       Tested without React. Each returns the list of
                       actions the component should dispatch.
  refreshCycle.ts    — Shared "discover + PR fetch with per-repo callback"
                       helper. Eliminates the ~120 LOC duplication.

The eight useRef mirrors disappear once workflows are factored out — they only exist because the component owns async work and needs to dodge React's stale-closure trap.

Acceptance criteria

  • runtime.ts is gone (or shrinks to a single-line re-export from runtime/index.ts).
  • WorkspaceInkApp is under 350 LOC.
  • Each workflow (refresh, refreshRow, drillIn, requestDelete, confirmDelete, commitAddRepo, openAddRepo) is a pure function with its own unit tests (no React, no Ink).
  • The shared refreshCycle helper is tested independently.
  • Debug trace module has its own test (file write on flush, ring-buffer bound at 500).
  • Public API at src/workstation/surfaces/workspace/index.ts unchanged — startWorkspace, WorkspaceExitResult, WorkspaceResumeState still re-exported with identical shapes.
  • No snapshot drift (the rendered tree is identical).
  • Full Jest suite still passes.
  • Manual TTY check: drill-in loop, refresh cycle, add-repo, remove-repo, debug log all still work.

Files affected

  • src/workstation/surfaces/workspace/runtime.ts (split into the new directory layout)
  • src/workstation/surfaces/workspace/runtime.test.ts (likely splits to match new module boundaries)
  • src/workstation/surfaces/workspace/index.ts (re-export paths)
  • src/commands/workspace/handler.ts (imports workspaceDebug from runtime.ts; will need to follow the new path)

Risk

  • High. This is the riskiest item on the audit punch list. The runtime is the integration point between the input system, the reducer, Ink, async workflows, and the lifecycle handlers. A subtle regression here (missed dispatch, double-fire on a workflow, lifecycle handler not installed) is hard to catch without manual TTY testing.
  • The ring-buffer + signal handlers are global side effects — moving them needs care to avoid double-installation if a future caller starts two workspaces in one process.
  • The drill-in loop's process.exit(0) from commands/workspace/handler.ts already shapes the lifecycle; the split mustn't change the exit semantics.

Effort estimate

~4 hours including test refactoring + manual TTY verification.

Why defer

  • The current runtime works. It's ugly but stable — 191 suites pass, 13 PRs of polish are landed, and we just shipped to users.
  • Memoization (Memoize re-render scope in the workspace Ink runtime #1079) overlaps with the component split here — both want to break WorkspaceInkApp into smaller pieces. Picking one up should probably coordinate with the other to avoid two consecutive disruptive PRs against the same file.
  • Best timing: when we're about to add another major workflow to the surface (e.g., cross-repo commit / multi-select / shared workspace cache). The split pays off when we have a new thing to integrate cleanly.

Coordination

When this is picked up, do it BEFORE or COMBINED WITH:


Sourced from the post-#880 workspace surface audit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions