Merge claudedo/02777289600f499ab274e844eb070ff4
This commit is contained in:
@@ -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<ClaudeDoDbContext> _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<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 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;
|
||||
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.
|
||||
|
||||
@@ -9,9 +9,7 @@ namespace ClaudeDo.Ui.ViewModels.MissionControl;
|
||||
/// <summary>
|
||||
/// 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"/>
|
||||
/// is null — ad-hoc panes are never deduped, unlike task-based ones). Distinct from the streamed-log
|
||||
/// <see cref="ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel"/> pane; the two coexist until
|
||||
/// the streaming interactive stack is removed.
|
||||
/// is null — ad-hoc panes are never deduped, unlike task-based ones).
|
||||
/// </summary>
|
||||
public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControlPane, IDisposable
|
||||
{
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
namespace ClaudeDo.Ui.ViewModels.MissionControl;
|
||||
|
||||
/// <summary>
|
||||
/// Common contract for anything hosted as a pane in the Command Center — implemented by both
|
||||
/// the streamed-log <see cref="ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel"/> and the
|
||||
/// embedded ConPTY <see cref="ConPtyPaneViewModel"/> — 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
|
||||
/// <see cref="ConPtyPaneViewModel"/>, kept as its own abstraction so the grid/tabs layout
|
||||
/// toggle binds a pane collection rather than a concrete ConPTY-specific type.
|
||||
/// </summary>
|
||||
public interface IMissionControlPane
|
||||
{
|
||||
|
||||
@@ -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<string> _onTaskUpdated;
|
||||
private readonly Action _onConnectionRestored;
|
||||
|
||||
public ObservableCollection<TaskMonitorViewModel> 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<ConPtyPaneViewModel> 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<IMissionControlPane> 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.</summary>
|
||||
public event Action<string>? ErrorReported;
|
||||
|
||||
private Action<string>? _openInApp;
|
||||
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; }
|
||||
public Action<string>? 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<TaskMonitorViewModel>().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<ConPtyPaneViewModel>().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;
|
||||
|
||||
Reference in New Issue
Block a user