# 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`), 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 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) ├───────────────────────────────────────────────────┤ │ │ 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.