diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json
index eaa3810..5d83afe 100644
--- a/src/ClaudeDo.Localization/locales/de.json
+++ b/src/ClaudeDo.Localization/locales/de.json
@@ -236,6 +236,7 @@
"missionControl": {
"openInApp": "In App öffnen",
"cancel": "Abbrechen",
+ "detach": "Abdocken",
"windowTitle": "Mission Control",
"clearFinished": "Erledigte entfernen",
"empty": "Keine laufenden Aufgaben"
diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json
index a684c2f..231de52 100644
--- a/src/ClaudeDo.Localization/locales/en.json
+++ b/src/ClaudeDo.Localization/locales/en.json
@@ -236,6 +236,7 @@
"missionControl": {
"openInApp": "Open in app",
"cancel": "Cancel",
+ "detach": "Detach",
"windowTitle": "Mission Control",
"clearFinished": "Clear finished",
"empty": "No running tasks"
diff --git a/src/ClaudeDo.Ui/Services/IDialogService.cs b/src/ClaudeDo.Ui/Services/IDialogService.cs
index 69e3b54..d1cb8ea 100644
--- a/src/ClaudeDo.Ui/Services/IDialogService.cs
+++ b/src/ClaudeDo.Ui/Services/IDialogService.cs
@@ -1,6 +1,8 @@
+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;
@@ -32,4 +34,7 @@ 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 ce891f6..752a65c 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs
@@ -142,6 +142,12 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
// 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() => DetachRequested?.Invoke(this);
+
[RelayCommand]
private void OpenInApp()
{
diff --git a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
index 7cef067..f9571c8 100644
--- a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
@@ -213,6 +213,7 @@ 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);
_updateCheck = updateCheck;
_installerLocator = installerLocator;
_workerLocator = workerLocator;
diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
index 298dca4..f339b6e 100644
--- a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
@@ -33,6 +33,10 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
}
}
+ // 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 bool HasMonitors => Monitors.Count > 0;
public MissionControlViewModel(IDbContextFactory dbFactory, IWorkerClient worker)
@@ -65,10 +69,24 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
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;
+ Monitors.Remove(monitor); // drop from grid — do NOT dispose; it keeps streaming
+ ShowDetached?.Invoke(monitor, () => ReDock(monitor));
+ }
+
+ private void ReDock(TaskMonitorViewModel monitor)
+ {
+ 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
diff --git a/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml b/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml
index 366b07e..9b92f75 100644
--- a/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml
+++ b/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml
@@ -18,6 +18,8 @@
+
diff --git a/src/ClaudeDo.Ui/Views/MissionControl/TaskMonitorWindow.axaml b/src/ClaudeDo.Ui/Views/MissionControl/TaskMonitorWindow.axaml
new file mode 100644
index 0000000..3dd1945
--- /dev/null
+++ b/src/ClaudeDo.Ui/Views/MissionControl/TaskMonitorWindow.axaml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/src/ClaudeDo.Ui/Views/MissionControl/TaskMonitorWindow.axaml.cs b/src/ClaudeDo.Ui/Views/MissionControl/TaskMonitorWindow.axaml.cs
new file mode 100644
index 0000000..807f19f
--- /dev/null
+++ b/src/ClaudeDo.Ui/Views/MissionControl/TaskMonitorWindow.axaml.cs
@@ -0,0 +1,8 @@
+using Avalonia.Controls;
+
+namespace ClaudeDo.Ui.Views.MissionControl;
+
+public partial class TaskMonitorWindow : Window
+{
+ public TaskMonitorWindow() => InitializeComponent();
+}
diff --git a/src/ClaudeDo.Ui/Views/WindowDialogService.cs b/src/ClaudeDo.Ui/Views/WindowDialogService.cs
index 83b95ec..15cbd98 100644
--- a/src/ClaudeDo.Ui/Views/WindowDialogService.cs
+++ b/src/ClaudeDo.Ui/Views/WindowDialogService.cs
@@ -1,3 +1,4 @@
+using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
@@ -6,6 +7,7 @@ using Avalonia.Media;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Conflicts;
+using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.Modals;
using ClaudeDo.Ui.Views.Conflicts;
using ClaudeDo.Ui.Views.MissionControl;
@@ -121,6 +123,13 @@ public sealed class WindowDialogService : IDialogService
_missionControl.Activate(); // bring to front / focus
}
+ public void ShowDetachedMonitor(TaskMonitorViewModel monitor, Action onClosed)
+ {
+ var win = new TaskMonitorWindow { DataContext = monitor };
+ win.Closed += (_, _) => onClosed(); // closing re-docks into the grid
+ win.Show(); // modeless, independent
+ }
+
public Task ConfirmAsync(string message)
{
var tcs = new TaskCompletionSource();
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs
index 01fb797..26158b1 100644
--- a/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs
@@ -1,6 +1,7 @@
using ClaudeDo.Data;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels;
+using ClaudeDo.Ui.ViewModels.Islands;
using Microsoft.EntityFrameworkCore;
using Xunit;
@@ -130,4 +131,25 @@ public class MissionControlViewModelTests : IDisposable
Assert.Equal("t1", revealed);
}
+
+ [Fact]
+ public void Detach_RemovesFromGrid_ThenReDockRestores()
+ {
+ var worker = new FakeWorker();
+ using var vm = BuildVm(worker);
+ TaskMonitorViewModel? detached = null;
+ Action? reDock = null;
+ vm.ShowDetached = (m, rd) => { detached = m; reDock = rd; };
+
+ worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
+ var monitor = vm.Monitors[0];
+
+ monitor.DetachCommand.Execute(null);
+ Assert.Empty(vm.Monitors);
+ Assert.Same(monitor, detached);
+
+ reDock!.Invoke();
+ Assert.Single(vm.Monitors);
+ Assert.Same(monitor, vm.Monitors[0]);
+ }
}