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 @@