feat(ui): add MissionControlViewModel
This commit is contained in:
@@ -34,6 +34,8 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
|
||||
string? LastApproveTarget { get; }
|
||||
|
||||
IReadOnlyList<ActiveTask> GetActiveTasks();
|
||||
|
||||
Task WakeQueueAsync();
|
||||
Task RunNowAsync(string taskId);
|
||||
Task ContinueTaskAsync(string taskId, string followUpPrompt);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Avalonia.Threading;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
@@ -68,6 +69,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
|
||||
public string? LastApproveTarget { get; private set; }
|
||||
|
||||
public IReadOnlyList<ActiveTask> GetActiveTasks() => ActiveTasks.ToList();
|
||||
|
||||
public WorkerClient(string signalRUrl)
|
||||
{
|
||||
_hub = new HubConnectionBuilder()
|
||||
|
||||
104
src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
Normal file
104
src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels;
|
||||
|
||||
public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
{
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly IWorkerClient _worker;
|
||||
private readonly Action<string, string, DateTime> _onTaskStarted;
|
||||
private readonly Action _onConnectionRestored;
|
||||
|
||||
public ObservableCollection<TaskMonitorViewModel> Monitors { get; } = new();
|
||||
|
||||
[ObservableProperty] private int _columnCount = 1;
|
||||
|
||||
public bool HasMonitors => Monitors.Count > 0;
|
||||
|
||||
public MissionControlViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_worker = worker;
|
||||
|
||||
Monitors.CollectionChanged += OnMonitorsChanged;
|
||||
|
||||
_onTaskStarted = (slot, taskId, startedAt) => EnsureMonitor(taskId);
|
||||
_worker.TaskStartedEvent += _onTaskStarted;
|
||||
|
||||
_onConnectionRestored = SeedActive;
|
||||
_worker.ConnectionRestoredEvent += _onConnectionRestored;
|
||||
|
||||
SeedActive();
|
||||
}
|
||||
|
||||
private void SeedActive()
|
||||
{
|
||||
foreach (var a in _worker.GetActiveTasks())
|
||||
EnsureMonitor(a.TaskId);
|
||||
}
|
||||
|
||||
private 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);
|
||||
Monitors.Add(monitor);
|
||||
_ = HydrateAsync(monitor, taskId);
|
||||
}
|
||||
|
||||
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);
|
||||
var latestRun = await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId);
|
||||
monitor.ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown);
|
||||
await monitor.ReplayLogFileAsync(entity.LogPath, CancellationToken.None);
|
||||
}
|
||||
catch { /* best-effort hydrate */ }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearFinished()
|
||||
{
|
||||
foreach (var m in Monitors.Where(m => m.IsDone || m.IsFailed || m.IsCancelled).ToList())
|
||||
{
|
||||
Monitors.Remove(m);
|
||||
m.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMonitorsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
ColumnCount = Monitors.Count switch
|
||||
{
|
||||
<= 1 => 1,
|
||||
<= 4 => 2,
|
||||
_ => 3,
|
||||
};
|
||||
OnPropertyChanged(nameof(HasMonitors));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_worker.TaskStartedEvent -= _onTaskStarted;
|
||||
_worker.ConnectionRestoredEvent -= _onConnectionRestored;
|
||||
Monitors.CollectionChanged -= OnMonitorsChanged;
|
||||
foreach (var m in Monitors) m.Dispose();
|
||||
Monitors.Clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user