# 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`, `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 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` 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?` 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 = ``, 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.