99 lines
6.1 KiB
Markdown
99 lines
6.1 KiB
Markdown
# 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.
|