diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index d6b425f9..9d0eb667 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 86db406c..e5b7d428 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 d7a751dd..8f1d309d 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) @@ -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().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; } } @@ -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; 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 @@ - + - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -