docs(ui): add Mission Control multi-task monitoring spec + plan
This commit is contained in:
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