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:
@@ -266,13 +266,8 @@
|
|||||||
"reviewResetTip": "Alle Änderungen verwerfen und die Aufgabe auf Leerlauf zurücksetzen"
|
"reviewResetTip": "Alle Änderungen verwerfen und die Aufgabe auf Leerlauf zurücksetzen"
|
||||||
},
|
},
|
||||||
"missionControl": {
|
"missionControl": {
|
||||||
"openInApp": "In App öffnen",
|
|
||||||
"cancel": "Abbrechen",
|
|
||||||
"detach": "Abdocken",
|
|
||||||
"redock": "Andocken",
|
|
||||||
"windowTitle": "Mission Control",
|
"windowTitle": "Mission Control",
|
||||||
"newSession": "Neue Sitzung",
|
"newSession": "Neue Sitzung",
|
||||||
"clearFinished": "Erledigte entfernen",
|
|
||||||
"empty": "Keine laufenden Aufgaben",
|
"empty": "Keine laufenden Aufgaben",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"queue": "Warteschlange",
|
"queue": "Warteschlange",
|
||||||
|
|||||||
@@ -266,13 +266,8 @@
|
|||||||
"reviewResetTip": "Discard all changes and reset the task to Idle"
|
"reviewResetTip": "Discard all changes and reset the task to Idle"
|
||||||
},
|
},
|
||||||
"missionControl": {
|
"missionControl": {
|
||||||
"openInApp": "Open in app",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"detach": "Detach",
|
|
||||||
"redock": "Re-dock",
|
|
||||||
"windowTitle": "Mission Control",
|
"windowTitle": "Mission Control",
|
||||||
"newSession": "New session",
|
"newSession": "New session",
|
||||||
"clearFinished": "Clear finished",
|
|
||||||
"empty": "No running tasks",
|
"empty": "No running tasks",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"queue": "Queue",
|
"queue": "Queue",
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
using ClaudeDo.Ui.ViewModels.Conflicts;
|
using ClaudeDo.Ui.ViewModels.Conflicts;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
|
||||||
using ClaudeDo.Ui.ViewModels.Modals;
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Services;
|
namespace ClaudeDo.Ui.Services;
|
||||||
@@ -37,7 +35,4 @@ public interface IDialogService
|
|||||||
|
|
||||||
/// <summary>Show (or re-show + focus) the modeless Mission Control window. Lazily created; hides on close.</summary>
|
/// <summary>Show (or re-show + focus) the modeless Mission Control window. Lazily created; hides on close.</summary>
|
||||||
void ShowMissionControl(MissionControlViewModel vm);
|
void ShowMissionControl(MissionControlViewModel vm);
|
||||||
|
|
||||||
/// <summary>Show a detached monitor in its own window; <paramref name="onClosed"/> re-docks it when that window closes.</summary>
|
|
||||||
void ShowDetachedMonitor(TaskMonitorViewModel monitor, Action onClosed);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ using ClaudeDo.Data.Repositories;
|
|||||||
using ClaudeDo.Ui.Helpers;
|
using ClaudeDo.Ui.Helpers;
|
||||||
using ClaudeDo.Ui.Localization;
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels.MissionControl;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels.Islands;
|
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionControlPane, IDisposable
|
public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
private readonly IWorkerClient _worker;
|
private readonly IWorkerClient _worker;
|
||||||
@@ -28,13 +27,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
|
|||||||
|
|
||||||
[ObservableProperty] private string _agentState = "idle";
|
[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 string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
|
||||||
public bool IsIdle => AgentState == "idle";
|
public bool IsIdle => AgentState == "idle";
|
||||||
public bool IsQueued => AgentState == "queued";
|
public bool IsQueued => AgentState == "queued";
|
||||||
@@ -205,43 +197,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
|
|||||||
ClearPendingQuestion();
|
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<string>? OpenInAppRequested { get; set; }
|
|
||||||
|
|
||||||
// Set by the host (Mission Control) to pop this monitor out into its own window.
|
|
||||||
public Action<TaskMonitorViewModel>? 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 SetTaskId(string id) => _subscribedTaskId = id;
|
||||||
|
|
||||||
public void ApplyState(ClaudeDo.Data.Models.TaskStatus status) =>
|
public void ApplyState(ClaudeDo.Data.Models.TaskStatus status) =>
|
||||||
|
|||||||
@@ -213,7 +213,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
|
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
|
||||||
MissionControl = missionControl;
|
MissionControl = missionControl;
|
||||||
MissionControl.OpenInApp = id => _ = RevealTaskAsync(id);
|
MissionControl.OpenInApp = id => _ = RevealTaskAsync(id);
|
||||||
MissionControl.ShowDetached = (monitor, reDock) => Dialogs?.ShowDetachedMonitor(monitor, reDock);
|
|
||||||
MissionControl.OpenSettingsRequested = () => Lists.OpenSettingsCommand.Execute(null);
|
MissionControl.OpenSettingsRequested = () => Lists.OpenSettingsCommand.Execute(null);
|
||||||
MissionControl.ErrorReported += FlashFooterError;
|
MissionControl.ErrorReported += FlashFooterError;
|
||||||
// Keep the task rows' "Interactive" chip in step with Mission Control's open ConPTY panes.
|
// Keep the task rows' "Interactive" chip in step with Mission Control's open ConPTY panes.
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ namespace ClaudeDo.Ui.ViewModels.MissionControl;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command Center pane hosting an embedded ConPTY terminal for either one task's interactive
|
/// 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, <see cref="TaskId"/>
|
/// Claude session, or an ad-hoc/free session in a user-chosen directory (no task, <see cref="TaskId"/>
|
||||||
/// is null — ad-hoc panes are never deduped, unlike task-based ones). Distinct from the streamed-log
|
/// is null — ad-hoc panes are never deduped, unlike task-based ones).
|
||||||
/// <see cref="ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel"/> pane; the two coexist until
|
|
||||||
/// the streaming interactive stack is removed.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControlPane, IDisposable
|
public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControlPane, IDisposable
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
namespace ClaudeDo.Ui.ViewModels.MissionControl;
|
namespace ClaudeDo.Ui.ViewModels.MissionControl;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Common contract for anything hosted as a pane in the Command Center — implemented by both
|
/// Common contract for anything hosted as a pane in the Command Center — currently only
|
||||||
/// the streamed-log <see cref="ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel"/> and the
|
/// <see cref="ConPtyPaneViewModel"/>, kept as its own abstraction so the grid/tabs layout
|
||||||
/// embedded ConPTY <see cref="ConPtyPaneViewModel"/> — so the layout toggle (grid/tabs) can
|
/// toggle binds a pane collection rather than a concrete ConPTY-specific type.
|
||||||
/// bind one heterogeneous pane collection.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMissionControlPane
|
public interface IMissionControlPane
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ using System.Linq;
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Data.Repositories;
|
|
||||||
using ClaudeDo.Ui.Localization;
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
|
||||||
using ClaudeDo.Ui.ViewModels.MissionControl;
|
using ClaudeDo.Ui.ViewModels.MissionControl;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -23,14 +21,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
private readonly Action<string> _onTaskUpdated;
|
private readonly Action<string> _onTaskUpdated;
|
||||||
private readonly Action _onConnectionRestored;
|
private readonly Action _onConnectionRestored;
|
||||||
|
|
||||||
public ObservableCollection<TaskMonitorViewModel> Monitors { get; } = new();
|
|
||||||
|
|
||||||
// Embedded ConPTY sessions (task-based only) — a manual cockpit detached from the
|
// 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();
|
public ObservableCollection<ConPtyPaneViewModel> ConPtySessions { get; } = new();
|
||||||
|
|
||||||
// Unified view of Monitors ++ ConPtySessions (in that order) so the layout toggle can
|
// Mirror of ConPtySessions typed as the pane abstraction so the layout toggle (grid/tabs)
|
||||||
// present one heterogeneous collection as either a grid or tabs.
|
// binds one contract rather than a ConPTY-specific type.
|
||||||
public ObservableCollection<IMissionControlPane> Panes { get; } = new();
|
public ObservableCollection<IMissionControlPane> Panes { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty] private int _columnCount = 1;
|
[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>
|
/// wires this into the footer error strip, same as the island view models' ErrorReported.</summary>
|
||||||
public event Action<string>? ErrorReported;
|
public event Action<string>? ErrorReported;
|
||||||
|
|
||||||
private Action<string>? _openInApp;
|
public Action<string>? OpenInApp { get; set; }
|
||||||
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; }
|
|
||||||
|
|
||||||
// View-layer seam: open the app Settings modal from the Mission Control window.
|
// View-layer seam: open the app Settings modal from the Mission Control window.
|
||||||
public Action? OpenSettingsRequested { get; set; }
|
public Action? OpenSettingsRequested { get; set; }
|
||||||
|
|
||||||
public bool HasMonitors => Monitors.Count > 0;
|
|
||||||
public bool HasPanes => Panes.Count > 0;
|
public bool HasPanes => Panes.Count > 0;
|
||||||
|
|
||||||
// Read-only view of the worker queue (tasks waiting to run), shown as a side strip.
|
// 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;
|
_dbFactory = dbFactory;
|
||||||
_worker = worker;
|
_worker = worker;
|
||||||
|
|
||||||
Monitors.CollectionChanged += OnMonitorsChanged;
|
|
||||||
ConPtySessions.CollectionChanged += OnConPtySessionsChanged;
|
ConPtySessions.CollectionChanged += OnConPtySessionsChanged;
|
||||||
Panes.CollectionChanged += OnPanesChanged;
|
Panes.CollectionChanged += OnPanesChanged;
|
||||||
|
|
||||||
@@ -119,7 +100,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
Title = r.Title ?? string.Empty,
|
Title = r.Title ?? string.Empty,
|
||||||
IsBlocked = r.BlockedByTaskId != null,
|
IsBlocked = r.BlockedByTaskId != null,
|
||||||
IsRunning = r.Status == ClaudeDo.Data.Models.TaskStatus.Running,
|
IsRunning = r.Status == ClaudeDo.Data.Models.TaskStatus.Running,
|
||||||
OpenInAppCommand = new RelayCommand(() => _openInApp?.Invoke(id)),
|
OpenInAppCommand = new RelayCommand(() => OpenInApp?.Invoke(id)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
OnPropertyChanged(nameof(HasQueued));
|
OnPropertyChanged(nameof(HasQueued));
|
||||||
@@ -147,88 +128,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
await RefreshQueueAsync();
|
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]
|
[RelayCommand]
|
||||||
private void OpenSettings() => OpenSettingsRequested?.Invoke();
|
private void OpenSettings() => OpenSettingsRequested?.Invoke();
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ToggleLayout() => IsFocusMode = !IsFocusMode;
|
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
|
// Fetches the launch spec for a task's worktree and hosts an embedded ConPTY session as a
|
||||||
// Command Center pane (task-based only).
|
// Command Center pane (task-based only).
|
||||||
public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId)
|
public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId)
|
||||||
@@ -359,42 +264,19 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
pane.Dispose();
|
pane.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirrors Monitors' add/remove/move into the front (Monitors-prefix) section of Panes.
|
// Mirrors ConPtySessions' add/remove 1:1 into Panes (typed as the pane abstraction).
|
||||||
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.
|
|
||||||
private void OnConPtySessionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
private void OnConPtySessionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case NotifyCollectionChangedAction.Add:
|
case NotifyCollectionChangedAction.Add:
|
||||||
Panes.Insert(Monitors.Count + e.NewStartingIndex, (ConPtyPaneViewModel)e.NewItems![0]!);
|
Panes.Insert(e.NewStartingIndex, (ConPtyPaneViewModel)e.NewItems![0]!);
|
||||||
break;
|
break;
|
||||||
case NotifyCollectionChangedAction.Remove:
|
case NotifyCollectionChangedAction.Remove:
|
||||||
Panes.RemoveAt(Monitors.Count + e.OldStartingIndex);
|
Panes.RemoveAt(e.OldStartingIndex);
|
||||||
break;
|
break;
|
||||||
default: // Reset
|
default: // Reset
|
||||||
foreach (var p in Panes.OfType<ConPtyPaneViewModel>().ToList())
|
Panes.Clear();
|
||||||
Panes.Remove(p);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,11 +302,8 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
_worker.TaskFinishedEvent -= _onTaskFinished;
|
_worker.TaskFinishedEvent -= _onTaskFinished;
|
||||||
_worker.TaskUpdatedEvent -= _onTaskUpdated;
|
_worker.TaskUpdatedEvent -= _onTaskUpdated;
|
||||||
_worker.ConnectionRestoredEvent -= _onConnectionRestored;
|
_worker.ConnectionRestoredEvent -= _onConnectionRestored;
|
||||||
Monitors.CollectionChanged -= OnMonitorsChanged;
|
|
||||||
ConPtySessions.CollectionChanged -= OnConPtySessionsChanged;
|
ConPtySessions.CollectionChanged -= OnConPtySessionsChanged;
|
||||||
Panes.CollectionChanged -= OnPanesChanged;
|
Panes.CollectionChanged -= OnPanesChanged;
|
||||||
foreach (var m in Monitors) m.Dispose();
|
|
||||||
Monitors.Clear();
|
|
||||||
foreach (var c in ConPtySessions.ToList())
|
foreach (var c in ConPtySessions.ToList())
|
||||||
{
|
{
|
||||||
c.ErrorReported -= OnConPtyPaneError;
|
c.ErrorReported -= OnConPtyPaneError;
|
||||||
|
|||||||
@@ -81,8 +81,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Question prompt (AskUser): answer the running task's question inline, mirroring
|
<!-- Question prompt (AskUser): answer the running task's question inline. -->
|
||||||
the Mission Control banner. State is shared via the same TaskMonitorViewModel. -->
|
|
||||||
<Border DockPanel.Dock="Top"
|
<Border DockPanel.Dock="Top"
|
||||||
IsVisible="{Binding Monitor.HasPendingQuestion}"
|
IsVisible="{Binding Monitor.HasPendingQuestion}"
|
||||||
Margin="0,0,0,10" Padding="12,8"
|
Margin="0,0,0,10" Padding="12,8"
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
||||||
xmlns:vmi="using:ClaudeDo.Ui.ViewModels.Islands"
|
|
||||||
xmlns:vmm="using:ClaudeDo.Ui.ViewModels.MissionControl"
|
xmlns:vmm="using:ClaudeDo.Ui.ViewModels.MissionControl"
|
||||||
xmlns:mc="using:ClaudeDo.Ui.Views.MissionControl"
|
xmlns:mc="using:ClaudeDo.Ui.Views.MissionControl"
|
||||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||||
x:DataType="vm:MissionControlViewModel"
|
x:DataType="vm:MissionControlViewModel"
|
||||||
x:Class="ClaudeDo.Ui.Views.MissionControl.MissionControlView">
|
x:Class="ClaudeDo.Ui.Views.MissionControl.MissionControlView">
|
||||||
<UserControl.DataTemplates>
|
<UserControl.DataTemplates>
|
||||||
<!-- Polymorphic pane templates: both the grid ItemsControl and the focus-mode TabControl
|
|
||||||
resolve per-item content through these (no explicit ItemTemplate on either). -->
|
|
||||||
<DataTemplate DataType="vmi:TaskMonitorViewModel">
|
|
||||||
<mc:MonitorPaneView Margin="6" />
|
|
||||||
</DataTemplate>
|
|
||||||
<DataTemplate DataType="vmm:ConPtyPaneViewModel">
|
<DataTemplate DataType="vmm:ConPtyPaneViewModel">
|
||||||
<mc:ConPtyPaneView Margin="6" />
|
<mc:ConPtyPaneView Margin="6" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</UserControl.DataTemplates>
|
</UserControl.DataTemplates>
|
||||||
<DockPanel LastChildFill="True" Background="{DynamicResource VoidBrush}"
|
<DockPanel LastChildFill="True" Background="{DynamicResource VoidBrush}">
|
||||||
DragDrop.AllowDrop="True">
|
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<Border DockPanel.Dock="Top"
|
<Border DockPanel.Dock="Top"
|
||||||
@@ -50,9 +43,6 @@
|
|||||||
<PathIcon Data="{StaticResource Icon.Settings}" Width="15" Height="15"
|
<PathIcon Data="{StaticResource Icon.Settings}" Width="15" Height="15"
|
||||||
Foreground="{DynamicResource TextMuteBrush}"/>
|
Foreground="{DynamicResource TextMuteBrush}"/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Classes="btn"
|
|
||||||
Content="{loc:Tr missionControl.clearFinished}"
|
|
||||||
Command="{Binding ClearFinishedCommand}" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
using System.Linq;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.VisualTree;
|
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views.MissionControl;
|
namespace ClaudeDo.Ui.Views.MissionControl;
|
||||||
|
|
||||||
public partial class MissionControlView : UserControl
|
public partial class MissionControlView : UserControl
|
||||||
{
|
{
|
||||||
// Shared with MonitorPaneView (the drag source).
|
|
||||||
public static readonly DataFormat<string> PaneFormat =
|
|
||||||
DataFormat.CreateStringApplicationFormat("claudedo-monitor-pane");
|
|
||||||
|
|
||||||
public MissionControlView()
|
public MissionControlView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
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.
|
// 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);
|
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<MonitorPaneView>();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
|
||||||
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
|
|
||||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
|
||||||
x:DataType="vm:TaskMonitorViewModel"
|
|
||||||
x:Class="ClaudeDo.Ui.Views.MissionControl.MonitorPaneView">
|
|
||||||
<Border Classes="monitor-pane"
|
|
||||||
DragDrop.AllowDrop="True"
|
|
||||||
Classes.mon-review="{Binding IsWaitingForReview}"
|
|
||||||
Classes.mon-done="{Binding IsDone}"
|
|
||||||
Classes.mon-roadblock="{Binding HasRoadblock}"
|
|
||||||
Classes.mon-failed="{Binding IsFailed}"
|
|
||||||
BorderThickness="1" CornerRadius="10" ClipToBounds="True">
|
|
||||||
<DockPanel LastChildFill="True">
|
|
||||||
|
|
||||||
<!-- Header: per-task action icons -->
|
|
||||||
<Border DockPanel.Dock="Top"
|
|
||||||
Background="{DynamicResource Surface2Brush}"
|
|
||||||
BorderBrush="{DynamicResource LineBrush}"
|
|
||||||
BorderThickness="0,0,0,1" Padding="6,3"
|
|
||||||
PointerPressed="OnHeaderPressed">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="2"
|
|
||||||
HorizontalAlignment="Right" VerticalAlignment="Center">
|
|
||||||
<Button Classes="title-ctrl"
|
|
||||||
Command="{Binding OpenInAppCommand}"
|
|
||||||
ToolTip.Tip="{loc:Tr missionControl.openInApp}">
|
|
||||||
<PathIcon Data="{StaticResource Icon.ArrowOut}" Width="12" Height="12"/>
|
|
||||||
</Button>
|
|
||||||
<Button Classes="title-ctrl"
|
|
||||||
Command="{Binding DetachCommand}"
|
|
||||||
ToolTip.Tip="{Binding DetachTooltip}">
|
|
||||||
<PathIcon Data="{StaticResource Icon.WinMax}" Width="12" Height="12"/>
|
|
||||||
</Button>
|
|
||||||
<Button Classes="title-ctrl"
|
|
||||||
Command="{Binding CancelTaskCommand}"
|
|
||||||
IsVisible="{Binding IsRunning}"
|
|
||||||
ToolTip.Tip="{loc:Tr missionControl.cancel}">
|
|
||||||
<PathIcon Data="{StaticResource Icon.WinClose}" Width="12" Height="12"/>
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Roadblock banner (amber) -->
|
|
||||||
<Border DockPanel.Dock="Top"
|
|
||||||
IsVisible="{Binding HasRoadblock}"
|
|
||||||
Background="{DynamicResource RoadblockTintBrush}"
|
|
||||||
BorderBrush="{DynamicResource AmberBrush}"
|
|
||||||
BorderThickness="0,0,0,1" Padding="12,6">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
||||||
<PathIcon Data="{StaticResource Icon.Warning}"
|
|
||||||
Foreground="{DynamicResource AmberBrush}"
|
|
||||||
Width="13" Height="13" VerticalAlignment="Center" />
|
|
||||||
<TextBlock Classes="meta" Text="{Binding Roadblocks}"
|
|
||||||
Foreground="{DynamicResource AmberBrush}"
|
|
||||||
TextWrapping="Wrap" VerticalAlignment="Center" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Failure banner (red) -->
|
|
||||||
<Border DockPanel.Dock="Top"
|
|
||||||
IsVisible="{Binding ShowRoadblock}"
|
|
||||||
Background="{DynamicResource ErrorTintBrush}"
|
|
||||||
BorderBrush="{DynamicResource BloodBrush}"
|
|
||||||
BorderThickness="0,0,0,1" Padding="12,6">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
||||||
<PathIcon Data="{StaticResource Icon.Warning}"
|
|
||||||
Foreground="{DynamicResource BloodBrush}"
|
|
||||||
Width="13" Height="13" VerticalAlignment="Center" />
|
|
||||||
<TextBlock Classes="meta" Text="{Binding RoadblockMessage}"
|
|
||||||
Foreground="{DynamicResource BloodBrush}"
|
|
||||||
TextWrapping="Wrap" VerticalAlignment="Center" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Question prompt (AskUser): answer a running task's question inline -->
|
|
||||||
<Border DockPanel.Dock="Top"
|
|
||||||
IsVisible="{Binding HasPendingQuestion}"
|
|
||||||
Background="{DynamicResource AccentSoftBrush}"
|
|
||||||
BorderBrush="{DynamicResource AccentBrush}"
|
|
||||||
BorderThickness="0,0,0,1" Padding="12,8">
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<TextBlock Classes="meta"
|
|
||||||
Text="{loc:Tr missionControl.question.title}"
|
|
||||||
Foreground="{DynamicResource AccentBrush}" FontWeight="SemiBold" />
|
|
||||||
<TextBlock Text="{Binding PendingQuestion}" TextWrapping="Wrap"
|
|
||||||
Foreground="{DynamicResource TextBrush}" />
|
|
||||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="6">
|
|
||||||
<TextBox Grid.Column="0"
|
|
||||||
Text="{Binding AnswerDraft, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
PlaceholderText="{loc:Tr missionControl.question.placeholder}"
|
|
||||||
AcceptsReturn="False">
|
|
||||||
<TextBox.KeyBindings>
|
|
||||||
<KeyBinding Gesture="Enter" Command="{Binding SubmitAnswerCommand}" />
|
|
||||||
</TextBox.KeyBindings>
|
|
||||||
</TextBox>
|
|
||||||
<Button Grid.Column="1"
|
|
||||||
Content="{loc:Tr missionControl.question.send}"
|
|
||||||
Command="{Binding SubmitAnswerCommand}" />
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Console body: reuse SessionTerminalView -->
|
|
||||||
<islands:SessionTerminalView
|
|
||||||
Entries="{Binding Log}"
|
|
||||||
Label="{Binding DisplayTitle}"
|
|
||||||
IsRunning="{Binding IsRunning}"
|
|
||||||
IsDone="{Binding IsDone}"
|
|
||||||
IsFailed="{Binding IsFailed}" />
|
|
||||||
|
|
||||||
</DockPanel>
|
|
||||||
</Border>
|
|
||||||
</UserControl>
|
|
||||||
@@ -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<Button>();
|
|
||||||
if (button is not null) return;
|
|
||||||
|
|
||||||
var data = new DataTransfer();
|
|
||||||
data.Add(DataTransferItem.Create(MissionControlView.PaneFormat, id));
|
|
||||||
await DragDrop.DoDragDropAsync(e, data, DragDropEffects.Move);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
|
||||||
xmlns:mc="using:ClaudeDo.Ui.Views.MissionControl"
|
|
||||||
x:Class="ClaudeDo.Ui.Views.MissionControl.TaskMonitorWindow"
|
|
||||||
x:DataType="vm:TaskMonitorViewModel"
|
|
||||||
Title="{Binding DisplayTitle}"
|
|
||||||
Width="560" Height="620" MinWidth="360" MinHeight="320"
|
|
||||||
Background="{DynamicResource VoidBrush}"
|
|
||||||
Icon="avares://ClaudeDo.Ui/Assets/ClaudeTask.ico">
|
|
||||||
<mc:MonitorPaneView />
|
|
||||||
</Window>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views.MissionControl;
|
|
||||||
|
|
||||||
public partial class TaskMonitorWindow : Window
|
|
||||||
{
|
|
||||||
public TaskMonitorWindow() => InitializeComponent();
|
|
||||||
|
|
||||||
protected override void OnOpened(EventArgs e)
|
|
||||||
{
|
|
||||||
base.OnOpened(e);
|
|
||||||
if (DataContext is ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel vm)
|
|
||||||
vm.CloseWindowRequested = Close;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnClosed(EventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel vm)
|
|
||||||
vm.CloseWindowRequested = null;
|
|
||||||
base.OnClosed(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@ using Avalonia.Media;
|
|||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
using ClaudeDo.Ui.ViewModels.Conflicts;
|
using ClaudeDo.Ui.ViewModels.Conflicts;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
|
||||||
using ClaudeDo.Ui.ViewModels.Modals;
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
using ClaudeDo.Ui.Views.Conflicts;
|
using ClaudeDo.Ui.Views.Conflicts;
|
||||||
using ClaudeDo.Ui.Views.MissionControl;
|
using ClaudeDo.Ui.Views.MissionControl;
|
||||||
@@ -146,13 +145,6 @@ public sealed class WindowDialogService : IDialogService
|
|||||||
_missionControl.Activate(); // bring to front / focus
|
_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<bool> ConfirmAsync(string message)
|
public Task<bool> ConfirmAsync(string message)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ using ClaudeDo.Data;
|
|||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
|
||||||
using ClaudeDo.Ui.ViewModels.MissionControl;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
@@ -60,7 +58,6 @@ public class MissionControlViewModelTests : IDisposable
|
|||||||
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
|
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
|
||||||
worker.RaiseTaskStarted("slot-2", "t2", DateTime.UtcNow);
|
worker.RaiseTaskStarted("slot-2", "t2", DateTime.UtcNow);
|
||||||
|
|
||||||
Assert.Empty(vm.Monitors);
|
|
||||||
Assert.Empty(vm.Panes);
|
Assert.Empty(vm.Panes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +67,6 @@ public class MissionControlViewModelTests : IDisposable
|
|||||||
var worker = new SeededFakeWorker();
|
var worker = new SeededFakeWorker();
|
||||||
using var vm = BuildVm(worker);
|
using var vm = BuildVm(worker);
|
||||||
|
|
||||||
Assert.Empty(vm.Monitors);
|
|
||||||
Assert.Empty(vm.Panes);
|
Assert.Empty(vm.Panes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,161 +167,6 @@ public class MissionControlViewModelTests : IDisposable
|
|||||||
Assert.Equal("r1", opened);
|
Assert.Equal("r1", opened);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── monitor machinery tests (EnsureMonitor still works when called directly) ─
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void EnsureMonitor_TwoCalls_CreateTwoMonitors_ColumnCountTwo()
|
|
||||||
{
|
|
||||||
var worker = new FakeWorker();
|
|
||||||
using var vm = BuildVm(worker);
|
|
||||||
|
|
||||||
vm.EnsureMonitor("t1");
|
|
||||||
vm.EnsureMonitor("t2");
|
|
||||||
|
|
||||||
Assert.Equal(2, vm.Monitors.Count);
|
|
||||||
Assert.Equal(2, vm.ColumnCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void EnsureMonitor_DuplicateCall_DoesNotAddSecondMonitor()
|
|
||||||
{
|
|
||||||
var worker = new FakeWorker();
|
|
||||||
using var vm = BuildVm(worker);
|
|
||||||
|
|
||||||
vm.EnsureMonitor("t1");
|
|
||||||
vm.EnsureMonitor("t1");
|
|
||||||
|
|
||||||
Assert.Equal(1, vm.Monitors.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Finish_KeepsPane_AndFlipsState()
|
|
||||||
{
|
|
||||||
var worker = new FakeWorker();
|
|
||||||
using var vm = BuildVm(worker);
|
|
||||||
|
|
||||||
vm.EnsureMonitor("t1");
|
|
||||||
worker.RaiseTaskFinished("slot-1", "t1", "done", DateTime.UtcNow);
|
|
||||||
|
|
||||||
Assert.Equal(1, vm.Monitors.Count);
|
|
||||||
Assert.True(vm.Monitors[0].IsDone);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ClearFinished_RemovesTerminalMonitors()
|
|
||||||
{
|
|
||||||
var worker = new FakeWorker();
|
|
||||||
using var vm = BuildVm(worker);
|
|
||||||
|
|
||||||
vm.EnsureMonitor("t1");
|
|
||||||
vm.EnsureMonitor("t2");
|
|
||||||
worker.RaiseTaskFinished("slot-1", "t1", "done", DateTime.UtcNow);
|
|
||||||
|
|
||||||
vm.ClearFinishedCommand.Execute(null);
|
|
||||||
|
|
||||||
Assert.Equal(1, vm.Monitors.Count);
|
|
||||||
Assert.Equal("t2", vm.Monitors[0].SubscribedTaskId);
|
|
||||||
Assert.Equal(1, vm.ColumnCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void OpenInApp_PropagatesToMonitors_AndCommandInvokesHook()
|
|
||||||
{
|
|
||||||
var worker = new FakeWorker();
|
|
||||||
using var vm = BuildVm(worker);
|
|
||||||
|
|
||||||
string? revealed = null;
|
|
||||||
vm.OpenInApp = id => revealed = id;
|
|
||||||
|
|
||||||
vm.EnsureMonitor("t1");
|
|
||||||
|
|
||||||
vm.Monitors[0].OpenInAppCommand.Execute(null);
|
|
||||||
|
|
||||||
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; };
|
|
||||||
|
|
||||||
vm.EnsureMonitor("t1");
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ClearFinished_AlsoRemoves_WaitingForReview()
|
|
||||||
{
|
|
||||||
var worker = new FakeWorker();
|
|
||||||
using var vm = BuildVm(worker);
|
|
||||||
vm.EnsureMonitor("t1");
|
|
||||||
worker.RaiseTaskFinished("slot-1", "t1", "waiting_for_review", DateTime.UtcNow);
|
|
||||||
|
|
||||||
Assert.True(vm.Monitors[0].IsWaitingForReview);
|
|
||||||
vm.ClearFinishedCommand.Execute(null);
|
|
||||||
Assert.Empty(vm.Monitors);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Detach_SetsIsDetached_AndReDockClearsIt()
|
|
||||||
{
|
|
||||||
var worker = new FakeWorker();
|
|
||||||
using var vm = BuildVm(worker);
|
|
||||||
Action? reDock = null;
|
|
||||||
vm.ShowDetached = (m, rd) => reDock = rd;
|
|
||||||
vm.EnsureMonitor("t1");
|
|
||||||
var monitor = vm.Monitors[0];
|
|
||||||
|
|
||||||
monitor.DetachCommand.Execute(null);
|
|
||||||
Assert.True(monitor.IsDetached);
|
|
||||||
Assert.Empty(vm.Monitors);
|
|
||||||
|
|
||||||
reDock!.Invoke();
|
|
||||||
Assert.False(monitor.IsDetached);
|
|
||||||
Assert.Single(vm.Monitors);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DetachCommand_WhenDetached_RequestsWindowClose()
|
|
||||||
{
|
|
||||||
var worker = new FakeWorker();
|
|
||||||
using var vm = BuildVm(worker);
|
|
||||||
vm.ShowDetached = (m, rd) => { };
|
|
||||||
vm.EnsureMonitor("t1");
|
|
||||||
var monitor = vm.Monitors[0];
|
|
||||||
|
|
||||||
var closeRequested = false;
|
|
||||||
monitor.DetachCommand.Execute(null); // detach (IsDetached = true)
|
|
||||||
monitor.CloseWindowRequested = () => closeRequested = true;
|
|
||||||
monitor.DetachCommand.Execute(null); // now acts as re-dock
|
|
||||||
Assert.True(closeRequested);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void MoveMonitor_ReordersCollection()
|
|
||||||
{
|
|
||||||
var worker = new FakeWorker();
|
|
||||||
using var vm = BuildVm(worker);
|
|
||||||
vm.EnsureMonitor("t1");
|
|
||||||
vm.EnsureMonitor("t2");
|
|
||||||
vm.EnsureMonitor("t3");
|
|
||||||
|
|
||||||
vm.MoveMonitor(vm.Monitors[0], vm.Monitors[2]); // move t1 to t3's slot
|
|
||||||
Assert.Equal(new[] { "t2", "t3", "t1" }, vm.Monitors.Select(m => m.SubscribedTaskId).ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Queue_ReflectsQueuedTasks_InSortOrder()
|
public async Task Queue_ReflectsQueuedTasks_InSortOrder()
|
||||||
{
|
{
|
||||||
@@ -423,17 +264,17 @@ public class MissionControlViewModelTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Panes_CombinesMonitorsThenConPtySessions_InOrder()
|
public async Task Panes_MirrorsConPtySessions_InOrder()
|
||||||
{
|
{
|
||||||
var worker = new FakeWorker();
|
var worker = new FakeWorker();
|
||||||
using var vm = BuildVm(worker);
|
using var vm = BuildVm(worker);
|
||||||
|
|
||||||
vm.EnsureMonitor("t1");
|
await vm.OpenConPtySessionAsync("t1");
|
||||||
await vm.OpenConPtySessionAsync("t2");
|
await vm.OpenConPtySessionAsync("t2");
|
||||||
|
|
||||||
Assert.Equal(2, vm.Panes.Count);
|
Assert.Equal(2, vm.Panes.Count);
|
||||||
Assert.IsType<TaskMonitorViewModel>(vm.Panes[0]);
|
Assert.Same(vm.ConPtySessions[0], vm.Panes[0]);
|
||||||
Assert.IsType<ConPtyPaneViewModel>(vm.Panes[1]);
|
Assert.Same(vm.ConPtySessions[1], vm.Panes[1]);
|
||||||
Assert.Equal(2, vm.ColumnCount);
|
Assert.Equal(2, vm.ColumnCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,18 +109,6 @@ public class TaskMonitorViewModelTests : IDisposable
|
|||||||
Assert.True(vm.HasRoadblock);
|
Assert.True(vm.HasRoadblock);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Detach_WhenNotDetached_InvokesDetachRequested()
|
|
||||||
{
|
|
||||||
var worker = new FakeWorker();
|
|
||||||
using var vm = Build(worker);
|
|
||||||
vm.SetTaskId("t1");
|
|
||||||
TaskMonitorViewModel? requested = null;
|
|
||||||
vm.DetachRequested = m => requested = m;
|
|
||||||
vm.DetachCommand.Execute(null);
|
|
||||||
Assert.Same(vm, requested);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TaskQuestionAsked_SurfacesQuestion()
|
public void TaskQuestionAsked_SurfacesQuestion()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ public class TasksIslandMoveToListTests : IDisposable
|
|||||||
public Task ShowLogVisualizerAsync(LogVisualizerViewModel vm) => throw new NotImplementedException();
|
public Task ShowLogVisualizerAsync(LogVisualizerViewModel vm) => throw new NotImplementedException();
|
||||||
public Task ShowErrorAsync(string message) => Task.CompletedTask;
|
public Task ShowErrorAsync(string message) => Task.CompletedTask;
|
||||||
public void ShowMissionControl(MissionControlViewModel vm) => throw new NotImplementedException();
|
public void ShowMissionControl(MissionControlViewModel vm) => throw new NotImplementedException();
|
||||||
public void ShowDetachedMonitor(TaskMonitorViewModel monitor, Action onClosed) => throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TasksIslandViewModel BuildViewModel() => new(new TestDbFactory(NewContext), worker: null);
|
private TasksIslandViewModel BuildViewModel() => new(new TestDbFactory(NewContext), worker: null);
|
||||||
|
|||||||
Reference in New Issue
Block a user