diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json
index a59fc633..4494746e 100644
--- a/src/ClaudeDo.Localization/locales/de.json
+++ b/src/ClaudeDo.Localization/locales/de.json
@@ -266,13 +266,8 @@
"reviewResetTip": "Alle Änderungen verwerfen und die Aufgabe auf Leerlauf zurücksetzen"
},
"missionControl": {
- "openInApp": "In App öffnen",
- "cancel": "Abbrechen",
- "detach": "Abdocken",
- "redock": "Andocken",
"windowTitle": "Mission Control",
"newSession": "Neue Sitzung",
- "clearFinished": "Erledigte entfernen",
"empty": "Keine laufenden Aufgaben",
"settings": "Einstellungen",
"queue": "Warteschlange",
diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json
index 7f0a9821..f92a5cd3 100644
--- a/src/ClaudeDo.Localization/locales/en.json
+++ b/src/ClaudeDo.Localization/locales/en.json
@@ -266,13 +266,8 @@
"reviewResetTip": "Discard all changes and reset the task to Idle"
},
"missionControl": {
- "openInApp": "Open in app",
- "cancel": "Cancel",
- "detach": "Detach",
- "redock": "Re-dock",
"windowTitle": "Mission Control",
"newSession": "New session",
- "clearFinished": "Clear finished",
"empty": "No running tasks",
"settings": "Settings",
"queue": "Queue",
diff --git a/src/ClaudeDo.Ui/Services/IDialogService.cs b/src/ClaudeDo.Ui/Services/IDialogService.cs
index b8a433c9..cc222018 100644
--- a/src/ClaudeDo.Ui/Services/IDialogService.cs
+++ b/src/ClaudeDo.Ui/Services/IDialogService.cs
@@ -1,8 +1,6 @@
-using System;
using System.Threading.Tasks;
using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Conflicts;
-using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.Modals;
namespace ClaudeDo.Ui.Services;
@@ -37,7 +35,4 @@ public interface IDialogService
/// Show (or re-show + focus) the modeless Mission Control window. Lazily created; hides on close.
void ShowMissionControl(MissionControlViewModel vm);
-
- /// Show a detached monitor in its own window; re-docks it when that window closes.
- void ShowDetachedMonitor(TaskMonitorViewModel monitor, Action onClosed);
}
diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs
index 147c064a..1cdf4637 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs
@@ -8,12 +8,11 @@ using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Helpers;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
-using ClaudeDo.Ui.ViewModels.MissionControl;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Ui.ViewModels.Islands;
-public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionControlPane, IDisposable
+public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
{
private readonly IDbContextFactory _dbFactory;
private readonly IWorkerClient _worker;
@@ -28,13 +27,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
[ObservableProperty] private string _agentState = "idle";
- [ObservableProperty]
- [NotifyPropertyChangedFor(nameof(DisplayTitle))]
- private string? _title;
-
- public string DisplayTitle =>
- string.IsNullOrWhiteSpace(Title) ? (SubscribedTaskId ?? "task") : Title!;
-
public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
public bool IsIdle => AgentState == "idle";
public bool IsQueued => AgentState == "queued";
@@ -205,43 +197,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
ClearPendingQuestion();
}
- [ObservableProperty]
- [NotifyPropertyChangedFor(nameof(DetachTooltip))]
- private bool _isDetached;
-
- // Localized tooltip for the detach/re-dock toggle button.
- public string DetachTooltip => Loc.T(IsDetached ? "missionControl.redock" : "missionControl.detach");
-
- // Set by the detached window so the re-dock action can close it.
- public Action? CloseWindowRequested { get; set; }
-
- // Set by the host (e.g. Mission Control) to navigate the main app to this task.
- public Action? OpenInAppRequested { get; set; }
-
- // Set by the host (Mission Control) to pop this monitor out into its own window.
- public Action? DetachRequested { get; set; }
-
- [RelayCommand]
- private void Detach()
- {
- if (IsDetached) CloseWindowRequested?.Invoke(); // re-dock: close the detached window
- else DetachRequested?.Invoke(this); // detach: pop out to its own window
- }
-
- [RelayCommand]
- private void OpenInApp()
- {
- if (!string.IsNullOrEmpty(_subscribedTaskId))
- OpenInAppRequested?.Invoke(_subscribedTaskId);
- }
-
- [RelayCommand]
- private async System.Threading.Tasks.Task CancelTask()
- {
- if (!string.IsNullOrEmpty(_subscribedTaskId) && (IsRunning || IsQueued))
- await _worker.CancelTaskAsync(_subscribedTaskId);
- }
-
public void SetTaskId(string id) => _subscribedTaskId = id;
public void ApplyState(ClaudeDo.Data.Models.TaskStatus status) =>
diff --git a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
index aedb31ed..204d74f2 100644
--- a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
@@ -213,7 +213,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
MissionControl = missionControl;
MissionControl.OpenInApp = id => _ = RevealTaskAsync(id);
- MissionControl.ShowDetached = (monitor, reDock) => Dialogs?.ShowDetachedMonitor(monitor, reDock);
MissionControl.OpenSettingsRequested = () => Lists.OpenSettingsCommand.Execute(null);
MissionControl.ErrorReported += FlashFooterError;
// Keep the task rows' "Interactive" chip in step with Mission Control's open ConPTY panes.
diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs
index 23257b72..12d68113 100644
--- a/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs
@@ -9,9 +9,7 @@ namespace ClaudeDo.Ui.ViewModels.MissionControl;
///
/// Command Center pane hosting an embedded ConPTY terminal for either one task's interactive
/// Claude session, or an ad-hoc/free session in a user-chosen directory (no task,
-/// is null — ad-hoc panes are never deduped, unlike task-based ones). Distinct from the streamed-log
-/// pane; the two coexist until
-/// the streaming interactive stack is removed.
+/// is null — ad-hoc panes are never deduped, unlike task-based ones).
///
public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControlPane, IDisposable
{
diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControl/IMissionControlPane.cs b/src/ClaudeDo.Ui/ViewModels/MissionControl/IMissionControlPane.cs
index 7ea5ad02..b7fcf281 100644
--- a/src/ClaudeDo.Ui/ViewModels/MissionControl/IMissionControlPane.cs
+++ b/src/ClaudeDo.Ui/ViewModels/MissionControl/IMissionControlPane.cs
@@ -1,10 +1,9 @@
namespace ClaudeDo.Ui.ViewModels.MissionControl;
///
-/// Common contract for anything hosted as a pane in the Command Center — implemented by both
-/// the streamed-log and the
-/// embedded ConPTY — so the layout toggle (grid/tabs) can
-/// bind one heterogeneous pane collection.
+/// Common contract for anything hosted as a pane in the Command Center — currently only
+/// , kept as its own abstraction so the grid/tabs layout
+/// toggle binds a pane collection rather than a concrete ConPTY-specific type.
///
public interface IMissionControlPane
{
diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
index 1a227667..1a8bc9c4 100644
--- a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
@@ -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 _onTaskUpdated;
private readonly Action _onConnectionRestored;
- public ObservableCollection 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 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 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.
public event Action? ErrorReported;
- private Action? _openInApp;
- public Action? 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? ShowDetached { get; set; }
+ public Action? 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)
@@ -381,42 +286,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().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().ToList())
- Panes.Remove(p);
+ Panes.Clear();
break;
}
}
@@ -442,11 +324,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;
diff --git a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml
index d7c26da4..255a3408 100644
--- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml
+++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml
@@ -81,8 +81,7 @@
-
+
-
-
-
-
-
+
-
diff --git a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml.cs b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml.cs
index 626acfee..34c1c6d4 100644
--- a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml.cs
@@ -1,25 +1,15 @@
-using System.Linq;
using Avalonia.Controls;
-using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
-using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels;
-using ClaudeDo.Ui.ViewModels.Islands;
namespace ClaudeDo.Ui.Views.MissionControl;
public partial class MissionControlView : UserControl
{
- // Shared with MonitorPaneView (the drag source).
- public static readonly DataFormat PaneFormat =
- DataFormat.CreateStringApplicationFormat("claudedo-monitor-pane");
-
public MissionControlView()
{
InitializeComponent();
- AddHandler(DragDrop.DragOverEvent, OnPaneDragOver);
- AddHandler(DragDrop.DropEvent, OnPaneDrop);
}
// Ad-hoc ConPTY session: the view owns the folder picker, the VM only takes the chosen path.
@@ -38,31 +28,4 @@ public partial class MissionControlView : UserControl
await vm.OpenAdHocConPtySessionAsync(folders[0].Path.LocalPath);
}
-
- private void OnPaneDragOver(object? sender, DragEventArgs e)
- {
- var dt = e.DataTransfer;
- e.DragEffects = (dt?.Contains(PaneFormat) ?? false) ? DragDropEffects.Move : DragDropEffects.None;
- }
-
- private void OnPaneDrop(object? sender, DragEventArgs e)
- {
- if (DataContext is not MissionControlViewModel vm) return;
- var dt = e.DataTransfer;
- if (dt is null) return;
-
- // A pane dragged within the grid → reorder. (Drag-to-queue from the main app now arrives
- // via the custom ghost drag's screen hit-test, not an OLE drop.)
- var draggedId = dt.TryGetValue(PaneFormat);
- if (string.IsNullOrEmpty(draggedId)) return;
- if (e.Source is not Avalonia.Visual src) return;
-
- var targetPane = src.FindAncestorOfType();
- if (targetPane?.DataContext is not TaskMonitorViewModel target) return;
-
- var dragged = vm.Monitors.FirstOrDefault(m => m.SubscribedTaskId == draggedId);
- if (dragged is null) return;
-
- vm.MoveMonitor(dragged, target);
- }
}
diff --git a/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml b/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml
deleted file mode 100644
index 50365504..00000000
--- a/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml
+++ /dev/null
@@ -1,114 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml.cs b/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml.cs
deleted file mode 100644
index 9a0a3c30..00000000
--- a/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Input;
-using Avalonia.VisualTree;
-using ClaudeDo.Ui.ViewModels.Islands;
-
-namespace ClaudeDo.Ui.Views.MissionControl;
-
-public partial class MonitorPaneView : UserControl
-{
- public MonitorPaneView() => InitializeComponent();
-
- private async void OnHeaderPressed(object? sender, PointerPressedEventArgs e)
- {
- if (DataContext is not TaskMonitorViewModel m || m.SubscribedTaskId is not { } id) return;
- if (e.Source is not Avalonia.Visual src) return;
- if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
-
- // Don't start a drag when the press landed on a header action button.
- var button = src as Button ?? src.FindAncestorOfType