Files
ClaudeDo/docs/superpowers/specs/2026-06-25-mission-control-design.md

9.5 KiB
Raw Blame History

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.