# 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.
