docs(ui): add Mission Control multi-task monitoring spec + plan

This commit is contained in:
Mika Kuns
2026-06-25 13:44:58 +02:00
parent 178fd25b55
commit d80a57836c
2 changed files with 242 additions and 0 deletions

View 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, 34→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.