From d80a57836cdaa5675e511685fbac588f83906c80 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Thu, 25 Jun 2026 13:44:58 +0200 Subject: [PATCH] docs(ui): add Mission Control multi-task monitoring spec + plan --- .../plans/2026-06-25-mission-control.md | 98 ++++++++++++ .../2026-06-25-mission-control-design.md | 144 ++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-25-mission-control.md create mode 100644 docs/superpowers/specs/2026-06-25-mission-control-design.md diff --git a/docs/superpowers/plans/2026-06-25-mission-control.md b/docs/superpowers/plans/2026-06-25-mission-control.md new file mode 100644 index 0000000..d544e07 --- /dev/null +++ b/docs/superpowers/plans/2026-06-25-mission-control.md @@ -0,0 +1,98 @@ +# Plan — Mission Control (multi-task live monitoring) + +Spec: `docs/superpowers/specs/2026-06-25-mission-control-design.md` + +Execution: subagent-driven, **sonnet** model, TDD where a test is meaningful, build + test before +each commit, one Conventional Commit per task. Stage files explicitly by path (never `git add -A`). +**No duplication** — every task reuses the assets named in the spec's reuse map. + +--- + +## Phase 1 — Extract the reusable monitor core (no behavior change) + +### Task 1.1 — Move `LogLineViewModel` + `LogKind` to their own file +- Cut `LogKind` enum and `LogLineViewModel` from `DetailsIslandViewModel.cs` into + `ViewModels/Islands/LogLineViewModel.cs` (same namespace). No logic change. +- Build `ClaudeDo.App`; run Ui.Tests. Commit: `refactor(ui): split LogLineViewModel into own file`. + +### Task 1.2 — Create `TaskMonitorViewModel` owning the streaming/status/outcome core +- New `ViewModels/Islands/TaskMonitorViewModel.cs`. Move from `DetailsIslandViewModel`: + `Log`, `_subscribedTaskId`, `_formatter`, `_claudeBuf`, `OnTaskMessage`, `AppendStdoutLine`, + `FlushClaudeBuffer`, `ReplayLogFileAsync`, `ExpandUserPath`; `AgentState` + all `Is*` flags + + `OnAgentStateChanged`; `StatusToStateKey` / `FinishedStatusToStateKey`; `SessionOutcome` / + `Roadblocks` + `ApplyOutcome` + `RoadblockMarker`; the worker `TaskMessage/Started/Finished/Updated` + subscriptions for the streaming concern; `Title`/`TaskIdBadge`/`Model`/`TurnsText`/`TokensFormatted`/ + diff text/elapsed; `BlockingReason` (+visible flag) from `BlockedByTaskId`/review/children/roadblocks. +- Ctor takes `IDbContextFactory`, `IWorkerClient`. `Attach(taskId)` / + `AttachAsync(entity)` to (re)bind + replay; `IDisposable` unsubscribes (mirror existing Dispose). +- Unit test (Ui.Tests): feed `[stdout]`/`[claude]`/`[tool]` lines via the worker fake → `Log` + accumulates correctly; `TaskFinished` flips `AgentState`; `ApplyOutcome` splits the roadblock marker. + Reuse the existing IWorkerClient fake (see `iworkerclient_fakes_sync`). +- Build + test. Commit: `feat(ui): extract TaskMonitorViewModel streaming core`. + +### Task 1.3 — `DetailsIslandViewModel` delegates to `Monitor` +- Add `public TaskMonitorViewModel Monitor { get; }`; construct it; route `Bind`/`BindAsync` to + `Monitor.Attach`. Remove the moved members; keep subtasks/attachments/editing/merge/review/child + outcomes/notes/prep intact. Dispose `Monitor`. +- Repoint `WorkConsole.axaml` Output-tab bindings (`Log`, `IsRunning/IsDone/IsFailed`, + `SessionOutcome`, `TurnsText`, `DiffAddText`/`DiffDelText`, `Model`) to `Monitor.*`. Leave + review/merge/session bindings unchanged. +- Build + test. **Manual visual pass: Details pane behaves exactly as before** (flag for Mika). + Commit: `refactor(ui): route DetailsIsland streaming through Monitor`. + +--- + +## Phase 2 — Mission Control window + +### Task 2.1 — `MissionControlViewModel` +- New `ViewModels/MissionControlViewModel.cs`: `ObservableCollection Monitors` + keyed by id; seed from `GetActive()`; add on `TaskStarted`, flip-state-and-keep on `TaskFinished`; + `ClearFinished` command; `ColumnCount`/layout signal from `Monitors.Count`; least-active collapse. + `IDisposable` disposes all monitors. Inject `IDbContextFactory`, `IWorkerClient`, `IServiceProvider`. +- Register `AddSingleton` in `App/Program.cs`. +- Unit test: simulate two `TaskStarted` → two monitors; `TaskFinished` keeps the pane; `ColumnCount` + matches count. Commit: `feat(ui): add MissionControlViewModel`. + +### Task 2.2 — `RevealTaskAsync` navigation on the shell +- Add `IslandsShellViewModel.RevealTaskAsync(taskId)` (resolve list → select → await load → select row). +- Wire `TaskMonitorViewModel.OpenInApp` to it (via an `Action?` set by the shell, like the + existing `CloseDetail`/`DeleteFromList` hooks — no new DI cycle). +- Unit test for the select-by-id path. Commit: `feat(ui): reveal a task by id from anywhere`. + +### Task 2.3 — `MonitorPaneView` (reuses `SessionTerminalView`) +- New `Views/MissionControl/MonitorPaneView.axaml(.cs)`: header (title/chip/tok/turn/elapsed), + blocking banner (`live-chip`/`terminal`/error-tint classes from IslandStyles — reuse), body = + ``, footer (Open in app / Detach / Cancel). + `x:DataType=TaskMonitorViewModel`. No new console control. Add `missionControl.*` en+de keys. +- Build + Localization.Tests. Commit: `feat(ui): add MonitorPaneView`. + +### Task 2.4 — `MissionControlView` grid + `MissionControlWindow` +- `MissionControlView.axaml`: `ItemsControl`/`UniformGrid` of `MonitorPaneView` driven by `ColumnCount`, + horizontal scroll fallback, header with `ClearFinished` (+ optional QuickAdd, deferrable). +- `MissionControlWindow.axaml(.cs)`: hosts the view; lazy-create + hide-on-close. +- Build. Commit: `feat(ui): add MissionControl window + grid`. + +### Task 2.5 — Launch button + lifetime +- Title-bar toggle button in `MainWindow.axaml` → shell command that shows/focuses the window + (created lazily, owns the singleton VM). +- Set `desktop.ShutdownMode = OnMainWindowClose` in `App.OnFrameworkInitializationCompleted`. +- Build. **Manual visual pass** (flag for Mika): open with 2+ running tasks; main window still adds + tasks; blocking banner; Open-in-app. Commit: `feat(ui): open Mission Control from the title bar`. + +--- + +## Phase 3 — Per-pane detach (lowest priority) + +### Task 3.1 — `TaskMonitorWindow` + detach/re-dock +- `Views/MissionControl/TaskMonitorWindow.axaml(.cs)` hosting `MonitorPaneView`; `Detach` removes the + monitor from the grid and shows it in the window (optional always-on-top); close re-docks. +- Build. Manual visual pass. Commit: `feat(ui): detach a monitor into its own window`. + +--- + +## Cross-cutting checklist (every task) +- Stage by explicit path; sonnet subagents; reuse per the spec's map — no new console/streaming/insert path. +- en.json + de.json parity for any new string (Localization.Tests). +- If `IWorkerClient`/ctor signatures change, update the hand-rolled fakes in **both** test projects. +- Build `ClaudeDo.App` (`-c Release` if Worker is running) before marking a task done. +- Never push without asking. diff --git a/docs/superpowers/specs/2026-06-25-mission-control-design.md b/docs/superpowers/specs/2026-06-25-mission-control-design.md new file mode 100644 index 0000000..2c24223 --- /dev/null +++ b/docs/superpowers/specs/2026-06-25-mission-control-design.md @@ -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`), 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.