Files
ClaudeDo/docs/superpowers/plans/2026-06-25-mission-control.md

6.1 KiB

Plan — Mission Control (multi-task live monitoring)

Spec: docs/superpowers/specs/2026-06-25-mission-control-design.md

Execution: subagent-driven, sonnet model, TDD where a test is meaningful, build + test before each commit, one Conventional Commit per task. Stage files explicitly by path (never git add -A). No duplication — every task reuses the assets named in the spec's reuse map.


Phase 1 — Extract the reusable monitor core (no behavior change)

Task 1.1 — Move LogLineViewModel + LogKind to their own file

  • Cut LogKind enum and LogLineViewModel from DetailsIslandViewModel.cs into ViewModels/Islands/LogLineViewModel.cs (same namespace). No logic change.
  • Build ClaudeDo.App; run Ui.Tests. Commit: refactor(ui): split LogLineViewModel into own file.

Task 1.2 — Create TaskMonitorViewModel owning the streaming/status/outcome core

  • New ViewModels/Islands/TaskMonitorViewModel.cs. Move from DetailsIslandViewModel: Log, _subscribedTaskId, _formatter, _claudeBuf, OnTaskMessage, AppendStdoutLine, FlushClaudeBuffer, ReplayLogFileAsync, ExpandUserPath; AgentState + all Is* flags + OnAgentStateChanged; StatusToStateKey / FinishedStatusToStateKey; SessionOutcome / Roadblocks + ApplyOutcome + RoadblockMarker; the worker TaskMessage/Started/Finished/Updated subscriptions for the streaming concern; Title/TaskIdBadge/Model/TurnsText/TokensFormatted/ diff text/elapsed; BlockingReason (+visible flag) from BlockedByTaskId/review/children/roadblocks.
  • Ctor takes IDbContextFactory<ClaudeDoDbContext>, IWorkerClient. Attach(taskId) / AttachAsync(entity) to (re)bind + replay; IDisposable unsubscribes (mirror existing Dispose).
  • Unit test (Ui.Tests): feed [stdout]/[claude]/[tool] lines via the worker fake → Log accumulates correctly; TaskFinished flips AgentState; ApplyOutcome splits the roadblock marker. Reuse the existing IWorkerClient fake (see iworkerclient_fakes_sync).
  • Build + test. Commit: feat(ui): extract TaskMonitorViewModel streaming core.

Task 1.3 — DetailsIslandViewModel delegates to Monitor

  • Add public TaskMonitorViewModel Monitor { get; }; construct it; route Bind/BindAsync to Monitor.Attach. Remove the moved members; keep subtasks/attachments/editing/merge/review/child outcomes/notes/prep intact. Dispose Monitor.
  • Repoint WorkConsole.axaml Output-tab bindings (Log, IsRunning/IsDone/IsFailed, SessionOutcome, TurnsText, DiffAddText/DiffDelText, Model) to Monitor.*. Leave review/merge/session bindings unchanged.
  • Build + test. Manual visual pass: Details pane behaves exactly as before (flag for Mika). Commit: refactor(ui): route DetailsIsland streaming through Monitor.

Phase 2 — Mission Control window

Task 2.1 — MissionControlViewModel

  • New ViewModels/MissionControlViewModel.cs: ObservableCollection<TaskMonitorViewModel> Monitors keyed by id; seed from GetActive(); add on TaskStarted, flip-state-and-keep on TaskFinished; ClearFinished command; ColumnCount/layout signal from Monitors.Count; least-active collapse. IDisposable disposes all monitors. Inject IDbContextFactory, IWorkerClient, IServiceProvider.
  • Register AddSingleton<MissionControlViewModel> in App/Program.cs.
  • Unit test: simulate two TaskStarted → two monitors; TaskFinished keeps the pane; ColumnCount matches count. Commit: feat(ui): add MissionControlViewModel.

Task 2.2 — RevealTaskAsync navigation on the shell

  • Add IslandsShellViewModel.RevealTaskAsync(taskId) (resolve list → select → await load → select row).
  • Wire TaskMonitorViewModel.OpenInApp to it (via an Action<string>? set by the shell, like the existing CloseDetail/DeleteFromList hooks — no new DI cycle).
  • Unit test for the select-by-id path. Commit: feat(ui): reveal a task by id from anywhere.

Task 2.3 — MonitorPaneView (reuses SessionTerminalView)

  • New Views/MissionControl/MonitorPaneView.axaml(.cs): header (title/chip/tok/turn/elapsed), blocking banner (live-chip/terminal/error-tint classes from IslandStyles — reuse), body = <SessionTerminalView Entries="{Binding Log}" ... />, footer (Open in app / Detach / Cancel). x:DataType=TaskMonitorViewModel. No new console control. Add missionControl.* en+de keys.
  • Build + Localization.Tests. Commit: feat(ui): add MonitorPaneView.

Task 2.4 — MissionControlView grid + MissionControlWindow

  • MissionControlView.axaml: ItemsControl/UniformGrid of MonitorPaneView driven by ColumnCount, horizontal scroll fallback, header with ClearFinished (+ optional QuickAdd, deferrable).
  • MissionControlWindow.axaml(.cs): hosts the view; lazy-create + hide-on-close.
  • Build. Commit: feat(ui): add MissionControl window + grid.

Task 2.5 — Launch button + lifetime

  • Title-bar toggle button in MainWindow.axaml → shell command that shows/focuses the window (created lazily, owns the singleton VM).
  • Set desktop.ShutdownMode = OnMainWindowClose in App.OnFrameworkInitializationCompleted.
  • Build. Manual visual pass (flag for Mika): open with 2+ running tasks; main window still adds tasks; blocking banner; Open-in-app. Commit: feat(ui): open Mission Control from the title bar.

Phase 3 — Per-pane detach (lowest priority)

Task 3.1 — TaskMonitorWindow + detach/re-dock

  • Views/MissionControl/TaskMonitorWindow.axaml(.cs) hosting MonitorPaneView; Detach removes the monitor from the grid and shows it in the window (optional always-on-top); close re-docks.
  • Build. Manual visual pass. Commit: feat(ui): detach a monitor into its own window.

Cross-cutting checklist (every task)

  • Stage by explicit path; sonnet subagents; reuse per the spec's map — no new console/streaming/insert path.
  • en.json + de.json parity for any new string (Localization.Tests).
  • If IWorkerClient/ctor signatures change, update the hand-rolled fakes in both test projects.
  • Build ClaudeDo.App (-c Release if Worker is running) before marking a task done.
  • Never push without asking.