chore(ui): remove dead Mission Control monitor-pane stack

Auto-seeding of monitor tiles was disabled in 724814f; Mission Control now
only shows ConPTY panes. Removes Monitors/EnsureMonitor/SeedActive, the
detach/re-dock machinery, MonitorPaneView and the detached monitor window,
plus their tests and orphaned localization keys.

TaskMonitorViewModel itself stays: DetailsIslandViewModel still uses it as
the backing state for the task Log/AgentState/AskUser-question UI, so only
its Mission-Control-only members (Title/DisplayTitle, detach, cancel command,
IMissionControlPane) were stripped. IMissionControlPane/Panes were kept
(now a 1:1 mirror of ConPtySessions) rather than dissolved, to avoid
churning the still-live ConPTY pane tests and AXAML for a single-implementer
interface.

Visual verification still open: Mission Control with several open ConPTY
tiles, and the Focus/Overview toggle.
This commit is contained in:
mika kuns
2026-08-05 09:03:55 +02:00
parent 63d8b5c28d
commit c82ea2eea1
19 changed files with 20 additions and 608 deletions
@@ -5,10 +5,8 @@ using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.MissionControl;
using Microsoft.EntityFrameworkCore;
@@ -23,14 +21,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
private readonly Action<string> _onTaskUpdated;
private readonly Action _onConnectionRestored;
public ObservableCollection<TaskMonitorViewModel> Monitors { get; } = new();
// Embedded ConPTY sessions (task-based only) — a manual cockpit detached from the
// review/merge/status machinery. Mirrored into Panes alongside the streamed-log Monitors.
// review/merge/status machinery.
public ObservableCollection<ConPtyPaneViewModel> ConPtySessions { get; } = new();
// Unified view of Monitors ++ ConPtySessions (in that order) so the layout toggle can
// present one heterogeneous collection as either a grid or tabs.
// Mirror of ConPtySessions typed as the pane abstraction so the layout toggle (grid/tabs)
// binds one contract rather than a ConPTY-specific type.
public ObservableCollection<IMissionControlPane> Panes { get; } = new();
[ObservableProperty] private int _columnCount = 1;
@@ -47,25 +43,11 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
/// wires this into the footer error strip, same as the island view models' ErrorReported.</summary>
public event Action<string>? ErrorReported;
private Action<string>? _openInApp;
public Action<string>? OpenInApp
{
get => _openInApp;
set
{
_openInApp = value;
foreach (var m in Monitors) m.OpenInAppRequested = value;
}
}
// View-layer seam: show a detached monitor in its own window. Second arg is the re-dock callback
// invoked when that window closes.
public Action<TaskMonitorViewModel, Action>? ShowDetached { get; set; }
public Action<string>? OpenInApp { get; set; }
// View-layer seam: open the app Settings modal from the Mission Control window.
public Action? OpenSettingsRequested { get; set; }
public bool HasMonitors => Monitors.Count > 0;
public bool HasPanes => Panes.Count > 0;
// Read-only view of the worker queue (tasks waiting to run), shown as a side strip.
@@ -77,7 +59,6 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
_dbFactory = dbFactory;
_worker = worker;
Monitors.CollectionChanged += OnMonitorsChanged;
ConPtySessions.CollectionChanged += OnConPtySessionsChanged;
Panes.CollectionChanged += OnPanesChanged;
@@ -119,7 +100,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
Title = r.Title ?? string.Empty,
IsBlocked = r.BlockedByTaskId != null,
IsRunning = r.Status == ClaudeDo.Data.Models.TaskStatus.Running,
OpenInAppCommand = new RelayCommand(() => _openInApp?.Invoke(id)),
OpenInAppCommand = new RelayCommand(() => OpenInApp?.Invoke(id)),
});
}
OnPropertyChanged(nameof(HasQueued));
@@ -147,88 +128,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
await RefreshQueueAsync();
}
// Kept intentionally — unused since auto-seeding was disabled (2026-07-29).
internal void SeedActive()
{
foreach (var a in _worker.GetActiveTasks())
EnsureMonitor(a.TaskId);
}
// Kept intentionally — unused since auto-seeding was disabled (2026-07-29).
internal void EnsureMonitor(string taskId)
{
if (string.IsNullOrEmpty(taskId)) return;
if (Monitors.Any(m => m.SubscribedTaskId == taskId)) return;
var monitor = new TaskMonitorViewModel(_dbFactory, _worker);
monitor.SetTaskId(taskId);
monitor.OpenInAppRequested = _openInApp;
monitor.DetachRequested = Detach;
Monitors.Add(monitor);
_ = HydrateAsync(monitor, taskId);
}
private void Detach(TaskMonitorViewModel monitor)
{
if (!Monitors.Contains(monitor)) return;
monitor.IsDetached = true;
Monitors.Remove(monitor); // drop from grid — do NOT dispose; it keeps streaming
ShowDetached?.Invoke(monitor, () => ReDock(monitor));
}
private void ReDock(TaskMonitorViewModel monitor)
{
monitor.IsDetached = false;
if (!Monitors.Contains(monitor) && monitor.SubscribedTaskId is not null)
Monitors.Add(monitor); // back into the grid
}
private async System.Threading.Tasks.Task HydrateAsync(TaskMonitorViewModel monitor, string taskId)
{
try
{
await using var ctx = await _dbFactory.CreateDbContextAsync();
var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId);
if (entity is null || monitor.SubscribedTaskId != taskId) return;
monitor.ApplyState(entity.Status);
monitor.Title = entity.Title;
var latestRun = await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId);
monitor.ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown);
await monitor.ReplayLogFileAsync(entity.LogPath, CancellationToken.None);
// Re-attach: if the task is blocked on an AskUser question right now, surface it.
var pending = await _worker.GetPendingQuestionAsync(taskId);
if (pending is not null && monitor.SubscribedTaskId == taskId)
monitor.SetPendingQuestion(pending.QuestionId, pending.Question);
}
catch { /* best-effort hydrate */ }
}
[RelayCommand]
private void ClearFinished()
{
foreach (var m in Monitors.Where(m => m.IsDone || m.IsFailed || m.IsCancelled || m.IsWaitingForReview).ToList())
{
Monitors.Remove(m);
m.Dispose();
}
}
[RelayCommand]
private void OpenSettings() => OpenSettingsRequested?.Invoke();
[RelayCommand]
private void ToggleLayout() => IsFocusMode = !IsFocusMode;
public void MoveMonitor(TaskMonitorViewModel dragged, TaskMonitorViewModel target)
{
if (ReferenceEquals(dragged, target)) return;
var from = Monitors.IndexOf(dragged);
var to = Monitors.IndexOf(target);
if (from < 0 || to < 0) return;
Monitors.Move(from, to);
}
// Fetches the launch spec for a task's worktree and hosts an embedded ConPTY session as a
// Command Center pane (task-based only).
public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId)
@@ -359,42 +264,19 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
pane.Dispose();
}
// Mirrors Monitors' add/remove/move into the front (Monitors-prefix) section of Panes.
private void OnMonitorsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Panes.Insert(e.NewStartingIndex, (TaskMonitorViewModel)e.NewItems![0]!);
break;
case NotifyCollectionChangedAction.Remove:
Panes.RemoveAt(e.OldStartingIndex);
break;
case NotifyCollectionChangedAction.Move:
Panes.Move(e.OldStartingIndex, e.NewStartingIndex);
break;
default: // Reset (Dispose's Monitors.Clear())
foreach (var p in Panes.OfType<TaskMonitorViewModel>().ToList())
Panes.Remove(p);
break;
}
OnPropertyChanged(nameof(HasMonitors));
}
// Mirrors ConPtySessions' add/remove into the tail (ConPtySessions-suffix) section of Panes.
// Mirrors ConPtySessions' add/remove 1:1 into Panes (typed as the pane abstraction).
private void OnConPtySessionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Panes.Insert(Monitors.Count + e.NewStartingIndex, (ConPtyPaneViewModel)e.NewItems![0]!);
Panes.Insert(e.NewStartingIndex, (ConPtyPaneViewModel)e.NewItems![0]!);
break;
case NotifyCollectionChangedAction.Remove:
Panes.RemoveAt(Monitors.Count + e.OldStartingIndex);
Panes.RemoveAt(e.OldStartingIndex);
break;
default: // Reset
foreach (var p in Panes.OfType<ConPtyPaneViewModel>().ToList())
Panes.Remove(p);
Panes.Clear();
break;
}
}
@@ -420,11 +302,8 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
_worker.TaskFinishedEvent -= _onTaskFinished;
_worker.TaskUpdatedEvent -= _onTaskUpdated;
_worker.ConnectionRestoredEvent -= _onConnectionRestored;
Monitors.CollectionChanged -= OnMonitorsChanged;
ConPtySessions.CollectionChanged -= OnConPtySessionsChanged;
Panes.CollectionChanged -= OnPanesChanged;
foreach (var m in Monitors) m.Dispose();
Monitors.Clear();
foreach (var c in ConPtySessions.ToList())
{
c.ErrorReported -= OnConPtyPaneError;