docs(ui): add Mission Control multi-task monitoring spec + plan
This commit is contained in:
98
docs/superpowers/plans/2026-06-25-mission-control.md
Normal file
98
docs/superpowers/plans/2026-06-25-mission-control.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# 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.
|
||||||
144
docs/superpowers/specs/2026-06-25-mission-control-design.md
Normal file
144
docs/superpowers/specs/2026-06-25-mission-control-design.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# Mission Control — multi-task live monitoring
|
||||||
|
|
||||||
|
Date: 2026-06-25
|
||||||
|
Status: approved (design); implementation not started
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The UI can observe only **one** running task at a time. `DetailsIslandViewModel` is hard 1:1
|
||||||
|
(single `Task`, single `_subscribedTaskId`); selecting another task in the middle pane *replaces*
|
||||||
|
what Details shows. Yet the worker runs several tasks concurrently (`MaxParallelExecutions`) and
|
||||||
|
already broadcasts every task's live output to all clients keyed by `taskId`. So the user cannot
|
||||||
|
watch multiple in-flight sessions, and monitoring blocks normal work (adding tasks, reviewing).
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Watch several running tasks at once **without** giving up the normal app. Requirements drawn from
|
||||||
|
the brainstorm:
|
||||||
|
|
||||||
|
- A **live console grid** — multiple full Claude output streams side by side.
|
||||||
|
- Each pane also shows **task details, blocking reasons**, and a **navigation helper** to open the
|
||||||
|
monitored task in the main app.
|
||||||
|
- Lives in a **separate, always-available window** so the main window stays fully usable (adding
|
||||||
|
tasks must never be blocked). Combines "full window" + "detachable".
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- No worker/SignalR changes. The broadcast layer is already N-capable (`TaskMessage(taskId,line)`,
|
||||||
|
`TaskStarted/Finished/Updated`, `GetActive()`). This is a UI/VM-only feature.
|
||||||
|
- No second SignalR connection. The new window shares the existing singleton `IWorkerClient`.
|
||||||
|
- No new merge/review engine. Review/merge stays in the main window's Details pane; Mission Control
|
||||||
|
is read-mostly (monitor + cancel + navigate).
|
||||||
|
|
||||||
|
## Hard constraint: no duplicated components or features
|
||||||
|
|
||||||
|
This feature is an **extract-and-reuse** exercise, not a rebuild. The single biggest risk is
|
||||||
|
forking a second live-streaming/parsing/status implementation. The reuse map below is binding.
|
||||||
|
|
||||||
|
### Reuse map (what already exists — use it, do not copy it)
|
||||||
|
|
||||||
|
| Concern | Existing asset | Location | How Mission Control uses it |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Live console body (log list, LIVE/DONE/FAILED chip, auto-scroll) | `SessionTerminalView` (StyledProps `Entries`, `Label`, `IsRunning/IsDone/IsFailed`) | `Views/Islands/SessionTerminalView.axaml(.cs)` | Bind a pane's `Entries`→its `Log`, status flags + label. **No new console control.** |
|
||||||
|
| Log line model | `LogLineViewModel` + `LogKind` | `ViewModels/Islands/DetailsIslandViewModel.cs` (top) | Shared model — move to its own file so both consumers reference one type. |
|
||||||
|
| Live stream parse/replay | `OnTaskMessage` / `AppendStdoutLine` / `FlushClaudeBuffer` / `ReplayLogFileAsync` + `StreamLineFormatter` + `ExpandUserPath` | private in `DetailsIslandViewModel.cs` | **Extract to `TaskMonitorViewModel`** (Phase 1). One streaming engine, two consumers. |
|
||||||
|
| Status state machine | `AgentState` + `Is*` flags + `StatusToStateKey` / `FinishedStatusToStateKey` | `DetailsIslandViewModel.cs` | Extract into `TaskMonitorViewModel`. |
|
||||||
|
| Outcome / roadblock split | `ApplyOutcome` + `RoadblockMarker` constant | `DetailsIslandViewModel.cs` | Extract into `TaskMonitorViewModel`. |
|
||||||
|
| Status chip / terminal styling | `live-chip`, `terminal`, `log-*` style classes | `Design/IslandStyles.axaml` | Reuse the classes as-is. |
|
||||||
|
| Add a new task | `TasksIslandViewModel.AddAsync` (`NewTaskTitle`, user-list only, direct `TaskRepository`) | `TasksIslandViewModel.cs:406` | Optional quick-add reuses this path; **must not** introduce a second insert path. |
|
||||||
|
| Live task list | `IWorkerClient.GetActive()` + `TaskStarted/Finished` events | worker hub / `WorkerClient` | Populate the grid; add/remove panes. |
|
||||||
|
| DI / singletons | `IslandsShellViewModel`, `DetailsIslandViewModel`, `IWorkerClient` all singletons | `App/Program.cs` | Register `MissionControlViewModel` singleton; inject existing singletons. |
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### TaskMonitorViewModel (the reusable core — new, but carved out of DetailsIslandViewModel)
|
||||||
|
|
||||||
|
One instance == one monitored task. Owns:
|
||||||
|
|
||||||
|
- `Log` (`ObservableCollection<LogLineViewModel>`), the filtered `TaskMessageEvent` subscription
|
||||||
|
(by `taskId`), stdout buffering, and NDJSON replay from disk on attach.
|
||||||
|
- `AgentState` + `Is*` flags; `SessionOutcome` / `Roadblocks` (the outcome split).
|
||||||
|
- Lightweight display: `Title`, `TaskIdBadge`, `Model`, `TurnsText`, `TokensFormatted`,
|
||||||
|
diff add/del, elapsed.
|
||||||
|
- `BlockingReason` (string/visible flag) derived from existing data: `BlockedByTaskId`
|
||||||
|
(planning/child chain), `WaitingForReview` / `WaitingForChildren` status, and roadblock markers.
|
||||||
|
- Commands: `OpenInApp`, `Detach`, `Cancel`.
|
||||||
|
- `IDisposable` — unsubscribes all worker events (mirror DetailsIslandViewModel.Dispose).
|
||||||
|
|
||||||
|
`DetailsIslandViewModel` is refactored to **own one `TaskMonitorViewModel` (`public Monitor`)** and
|
||||||
|
delegate streaming/status/outcome to it. Its heavy concerns (subtasks, attachments, editing, merge
|
||||||
|
cockpit, review verbs, child outcomes, notes/prep modes) stay put. **Phase 1 must be a no-behavior-
|
||||||
|
change refactor** — all existing Ui.Tests stay green.
|
||||||
|
|
||||||
|
> Binding-surface decision (Phase 1): repoint `WorkConsole.axaml`'s Output-tab bindings that
|
||||||
|
> reference streaming/status (`Log`, `IsRunning/IsDone/IsFailed`, `SessionOutcome`, `TurnsText`,
|
||||||
|
> diff text, `Model`) to `Monitor.*`. `x:DataType` stays `DetailsIslandViewModel`; compiled bindings
|
||||||
|
> handle the nested path. Review/merge/session bindings are untouched. Prefer repointing over adding
|
||||||
|
> ~15 forwarding properties (one source of truth, no boilerplate).
|
||||||
|
|
||||||
|
### MissionControlViewModel (new)
|
||||||
|
|
||||||
|
- `ObservableCollection<TaskMonitorViewModel> Monitors`, keyed by `taskId`.
|
||||||
|
- On open: seed from `GetActive()`. On `TaskStarted`: add a monitor. On `TaskFinished`: keep the
|
||||||
|
pane (so the final output stays readable) but flip its state; a "clear finished" action prunes them.
|
||||||
|
- Adaptive layout signal (column count) from `Monitors.Count`:
|
||||||
|
`1→1col, 2→2col, 3–4→2col(2 rows), 5+→fixed-width panes, horizontal scroll`. Least-active panes
|
||||||
|
beyond a threshold collapse to a compact card (title + last line + chip), click to expand — this is
|
||||||
|
the readability fallback so we never render N unreadable slivers.
|
||||||
|
- Optional `QuickAdd` (deferred within Phase 2): title + target user-list → the **same** creation
|
||||||
|
path as `TasksIslandViewModel.AddAsync` (shared method, not a copy).
|
||||||
|
- Disposes every monitor on window close.
|
||||||
|
|
||||||
|
### Windowing (new plumbing — thin)
|
||||||
|
|
||||||
|
- `MissionControlWindow` (Avalonia `Window`) hosting `MissionControlView`; DataContext =
|
||||||
|
the singleton `MissionControlViewModel`.
|
||||||
|
- No non-modal secondary-window precedent exists (all current dialogs use `ShowDialog(owner)`), so
|
||||||
|
this is genuinely new but small:
|
||||||
|
- Set `desktop.ShutdownMode = OnMainWindowClose` in `App.OnFrameworkInitializationCompleted` so
|
||||||
|
closing Mission Control never quits the app, and closing the main window does.
|
||||||
|
- Open via a **title-bar button in MainWindow** (toggle: show / focus-if-open). The window is
|
||||||
|
created lazily and hidden (not destroyed) on close so its monitors persist cheaply.
|
||||||
|
- Persist size/position (reuse the ui.config.json mechanism if present; otherwise defer).
|
||||||
|
|
||||||
|
### MonitorPaneView (new view, reuses SessionTerminalView)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ #142 Refactor auth module ───────── ● running ─┐ header: title, live chip, tok/turn/elapsed
|
||||||
|
│ ⏱ 4m12s ◆ 18.3k tok ↻ turn 6 │
|
||||||
|
├───────────────────────────────────────────────────┤
|
||||||
|
│ ⚠ Blocked: waiting on #141 (planning parent) │ blocking banner (visible only when blocked)
|
||||||
|
├───────────────────────────────────────────────────┤
|
||||||
|
│ <SessionTerminalView Entries={Log} .../> │ the REUSED console
|
||||||
|
├───────────────────────────────────────────────────┤
|
||||||
|
│ [↗ Open in app] [⧉ Detach] [✕ Cancel] │ footer
|
||||||
|
└───────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigation helper "Open in app" (new shell method)
|
||||||
|
|
||||||
|
No select-by-id exists today. Add `IslandsShellViewModel.RevealTaskAsync(taskId)`:
|
||||||
|
1. resolve the task's list, set `Lists.SelectedList`; 2. await `Tasks.LoadForList`; 3. find the row in
|
||||||
|
`Tasks.Items` by id, set `Tasks.SelectedTask` (→ `Details.Bind`); 4. bring MainWindow to front.
|
||||||
|
`TaskMonitorViewModel.OpenInApp` calls this. Single navigation entry point — no duplicate selection logic.
|
||||||
|
|
||||||
|
### Detach (Phase 3)
|
||||||
|
|
||||||
|
`Detach` moves a `TaskMonitorViewModel` out of the grid into a small `TaskMonitorWindow`
|
||||||
|
(reuses `MonitorPaneView`), optionally always-on-top; closing it re-docks. Lowest priority.
|
||||||
|
|
||||||
|
## Risks / open items
|
||||||
|
|
||||||
|
- **Phase 1 binding repoint** is the main risk: a missed `WorkConsole` binding shows as a blank
|
||||||
|
field, not a build error. Mitigation: Ui.Tests + a manual visual pass on the Details pane.
|
||||||
|
- **Localization parity** (Localization.Tests): every new visible string needs en + de keys under a
|
||||||
|
`missionControl.*` namespace.
|
||||||
|
- **Quick-add coupling** across windows is the weakest part; kept optional/deferrable.
|
||||||
|
- Detached windows = most plumbing, least daily payoff → Phase 3, last.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Build `ClaudeDo.App` + run Ui.Tests / Localization.Tests after each phase.
|
||||||
|
- Manual visual pass (cannot be auto-verified): Details pane unchanged after Phase 1; grid populates
|
||||||
|
with 2+ concurrent tasks, blocking banner shows, Open-in-app surfaces the task, adding a task in the
|
||||||
|
main window works while Mission Control is open.
|
||||||
Reference in New Issue
Block a user