From 5a17a727b99d9e371b706f5b8efe4b716434f47a Mon Sep 17 00:00:00 2001 From: mika kuns Date: Mon, 20 Apr 2026 10:41:14 +0200 Subject: [PATCH] chore(ui): remove obsolete pre-rewrite views and viewmodels Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.App/Program.cs | 26 - .../ViewModels/ListEditorViewModel.cs | 125 ---- .../ViewModels/ListItemViewModel.cs | 43 -- .../ViewModels/MainWindowViewModel.cs | 232 ------- .../ViewModels/StatusBarViewModel.cs | 55 -- .../ViewModels/SubtaskItemViewModel.cs | 23 - .../ViewModels/TaskDetailViewModel.cs | 583 ------------------ .../ViewModels/TaskEditorViewModel.cs | 264 -------- .../ViewModels/TaskItemViewModel.cs | 179 ------ .../ViewModels/TaskListViewModel.cs | 360 ----------- src/ClaudeDo.Ui/Views/ListEditorView.axaml | 62 -- src/ClaudeDo.Ui/Views/ListEditorView.axaml.cs | 40 -- src/ClaudeDo.Ui/Views/StatusBarView.axaml | 22 - src/ClaudeDo.Ui/Views/StatusBarView.axaml.cs | 11 - src/ClaudeDo.Ui/Views/TaskDetailView.axaml | 225 ------- src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs | 85 --- src/ClaudeDo.Ui/Views/TaskEditorView.axaml | 101 --- src/ClaudeDo.Ui/Views/TaskEditorView.axaml.cs | 29 - src/ClaudeDo.Ui/Views/TaskListView.axaml | 161 ----- src/ClaudeDo.Ui/Views/TaskListView.axaml.cs | 128 ---- 20 files changed, 2754 deletions(-) delete mode 100644 src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs delete mode 100644 src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs delete mode 100644 src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs delete mode 100644 src/ClaudeDo.Ui/ViewModels/StatusBarViewModel.cs delete mode 100644 src/ClaudeDo.Ui/ViewModels/SubtaskItemViewModel.cs delete mode 100644 src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs delete mode 100644 src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs delete mode 100644 src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs delete mode 100644 src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs delete mode 100644 src/ClaudeDo.Ui/Views/ListEditorView.axaml delete mode 100644 src/ClaudeDo.Ui/Views/ListEditorView.axaml.cs delete mode 100644 src/ClaudeDo.Ui/Views/StatusBarView.axaml delete mode 100644 src/ClaudeDo.Ui/Views/StatusBarView.axaml.cs delete mode 100644 src/ClaudeDo.Ui/Views/TaskDetailView.axaml delete mode 100644 src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs delete mode 100644 src/ClaudeDo.Ui/Views/TaskEditorView.axaml delete mode 100644 src/ClaudeDo.Ui/Views/TaskEditorView.axaml.cs delete mode 100644 src/ClaudeDo.Ui/Views/TaskListView.axaml delete mode 100644 src/ClaudeDo.Ui/Views/TaskListView.axaml.cs diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs index c1015e0..0cc1de7 100644 --- a/src/ClaudeDo.App/Program.cs +++ b/src/ClaudeDo.App/Program.cs @@ -1,10 +1,8 @@ using Avalonia; using ClaudeDo.Data; using ClaudeDo.Data.Git; -using ClaudeDo.Data.Repositories; using ClaudeDo.Ui; using ClaudeDo.Ui.Services; -using ClaudeDo.Ui.ViewModels; using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Modals; using Microsoft.EntityFrameworkCore; @@ -77,31 +75,7 @@ sealed class Program sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService().SignalRUrl)); // ViewModels - sc.AddTransient(); - sc.AddTransient(); sc.AddTransient(); - sc.AddSingleton(); - sc.AddSingleton(); - sc.AddSingleton(sp => - { - var dbFactory = sp.GetRequiredService>(); - var worker = sp.GetRequiredService(); - var statusBar = sp.GetRequiredService(); - return new TaskListViewModel( - dbFactory, worker, - () => sp.GetRequiredService(), - msg => statusBar.ShowMessage(msg)); - }); - sc.AddSingleton(sp => - { - return new MainWindowViewModel( - sp.GetRequiredService>(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - () => sp.GetRequiredService()); - }); // Islands shell VMs sc.AddSingleton(); diff --git a/src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs b/src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs deleted file mode 100644 index 520f95f..0000000 --- a/src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs +++ /dev/null @@ -1,125 +0,0 @@ -using ClaudeDo.Data.Models; -using ClaudeDo.Ui.Services; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using AgentInfo = ClaudeDo.Data.Models.AgentInfo; - -namespace ClaudeDo.Ui.ViewModels; - -public partial class ListEditorViewModel : ViewModelBase -{ - [ObservableProperty] private string _name = ""; - [ObservableProperty] private string? _workingDir; - [ObservableProperty] private string _defaultCommitType = "chore"; - [ObservableProperty] private string _windowTitle = "New List"; - - // Config fields - [ObservableProperty] private string _model = "Sonnet"; - [ObservableProperty] private string? _systemPrompt; - [ObservableProperty] private AgentInfo? _selectedAgent; - - private string? _editId; - private DateTime _createdAt; - private TaskCompletionSource _tcs = new(); - - public event Action? RequestClose; - - public static string[] CommitTypes { get; } = - ["chore", "feat", "fix", "refactor", "docs", "test", "perf", "style", "build", "ci"]; - - public static string[] ModelDisplayNames { get; } = ["Sonnet", "Opus", "Haiku"]; - - private static readonly Dictionary ModelToId = new() - { - ["Sonnet"] = "claude-sonnet-4-6", - ["Opus"] = "claude-opus-4-6", - ["Haiku"] = "claude-haiku-4-5", - }; - - private static readonly Dictionary IdToModel = - ModelToId.ToDictionary(kv => kv.Value, kv => kv.Key); - - public static string ModelIdToDisplay(string? modelId) => - modelId is not null && IdToModel.TryGetValue(modelId, out var display) ? display : "Sonnet"; - - public static string? ModelDisplayToId(string display) => - ModelToId.TryGetValue(display, out var id) ? id : null; - - public List AvailableAgents { get; set; } = []; - - public async Task LoadAgentsAsync(WorkerClient worker) - { - AvailableAgents = await worker.GetAgentsAsync(); - } - - public void InitForCreate() - { - _tcs = new TaskCompletionSource(); - _editId = null; - _createdAt = DateTime.UtcNow; - WindowTitle = "New List"; - } - - public void InitForEdit(ListEntity entity, ListConfigEntity? config) - { - _tcs = new TaskCompletionSource(); - _editId = entity.Id; - _createdAt = entity.CreatedAt; - Name = entity.Name; - WorkingDir = entity.WorkingDir; - DefaultCommitType = entity.DefaultCommitType; - WindowTitle = $"Edit List: {entity.Name}"; - - if (config is not null) - { - Model = ModelIdToDisplay(config.Model); - SystemPrompt = config.SystemPrompt; - SelectedAgent = AvailableAgents.FirstOrDefault(a => a.Path == config.AgentPath); - } - } - - public ListConfigEntity? BuildConfig(string listId) - { - var modelId = ModelDisplayToId(Model); - if (modelId is null && SystemPrompt is null && SelectedAgent is null) - return null; - - return new ListConfigEntity - { - ListId = listId, - Model = modelId, - SystemPrompt = string.IsNullOrWhiteSpace(SystemPrompt) ? null : SystemPrompt.Trim(), - AgentPath = SelectedAgent?.Path, - }; - } - - [RelayCommand] - private void Save() - { - if (string.IsNullOrWhiteSpace(Name)) return; - var entity = new ListEntity - { - Id = _editId ?? Guid.NewGuid().ToString(), - Name = Name.Trim(), - CreatedAt = _createdAt, - WorkingDir = string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir.Trim(), - DefaultCommitType = DefaultCommitType, - }; - _tcs.TrySetResult(entity); - RequestClose?.Invoke(); - } - - [RelayCommand] - private void Cancel() - { - _tcs.TrySetResult(null); - RequestClose?.Invoke(); - } - - public void OnWindowClosed() - { - _tcs.TrySetResult(null); - } - - public Task ShowAndWaitAsync() => _tcs.Task; -} diff --git a/src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs b/src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs deleted file mode 100644 index 8be03d5..0000000 --- a/src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Avalonia.Media; -using ClaudeDo.Data.Models; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace ClaudeDo.Ui.ViewModels; - -public partial class ListItemViewModel : ViewModelBase -{ - [ObservableProperty] private string _name; - [ObservableProperty] private string? _workingDir; - [ObservableProperty] private string _defaultCommitType; - - private static readonly IBrush[] DotPalette = - [ - new SolidColorBrush(Color.Parse("#3d9474")), // green - new SolidColorBrush(Color.Parse("#5571a1")), // blue - new SolidColorBrush(Color.Parse("#d4964a")), // amber - new SolidColorBrush(Color.Parse("#7c6aad")), // purple - new SolidColorBrush(Color.Parse("#c25d6a")), // rose - ]; - - public IBrush DotBrush => DotPalette[Math.Abs(Id.GetHashCode()) % DotPalette.Length]; - - public string Id { get; } - - public ListItemViewModel(ListEntity entity) - { - Id = entity.Id; - _name = entity.Name; - _workingDir = entity.WorkingDir; - _defaultCommitType = entity.DefaultCommitType; - } - - public ListEntity ToEntity() => new() - { - Id = Id, - Name = Name, - CreatedAt = DateTime.MinValue, // not used for update - WorkingDir = string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir, - DefaultCommitType = DefaultCommitType, - }; -} diff --git a/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs deleted file mode 100644 index 4a65e0d..0000000 --- a/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System.Collections.ObjectModel; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; -using ClaudeDo.Data; -using ClaudeDo.Data.Models; -using ClaudeDo.Data.Repositories; -using ClaudeDo.Ui.Services; -using ClaudeDo.Ui.Views; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Microsoft.EntityFrameworkCore; - -namespace ClaudeDo.Ui.ViewModels; - -public partial class MainWindowViewModel : ViewModelBase, IDisposable -{ - private readonly IDbContextFactory _dbFactory; - private readonly WorkerClient _worker; - private readonly Func _listEditorFactory; - - public ObservableCollection Lists { get; } = new(); - - [ObservableProperty] private ListItemViewModel? _selectedList; - - public TaskListViewModel TaskList { get; } - public TaskDetailViewModel TaskDetail { get; } - public StatusBarViewModel StatusBar { get; } - - private readonly Action _onTaskChanged; - private readonly Action _onTaskSubtasksChanged; - - public MainWindowViewModel( - IDbContextFactory dbFactory, - WorkerClient worker, - TaskListViewModel taskList, - TaskDetailViewModel taskDetail, - StatusBarViewModel statusBar, - Func listEditorFactory) - { - _dbFactory = dbFactory; - _worker = worker; - _listEditorFactory = listEditorFactory; - TaskList = taskList; - TaskDetail = taskDetail; - StatusBar = statusBar; - - _onTaskChanged = taskId => _ = TaskList.RefreshSingleAsync(taskId); - _onTaskSubtasksChanged = taskId => - { - if (TaskDetail.CurrentTaskId == taskId) - _ = TaskDetail.RefreshSubtasksFromDbAsync(); - }; - TaskList.SelectedTaskChanged += OnSelectedTaskChanged; - TaskList.TaskSubtasksChanged += _onTaskSubtasksChanged; - TaskDetail.TaskChanged += _onTaskChanged; - } - - public void Dispose() - { - TaskList.SelectedTaskChanged -= OnSelectedTaskChanged; - TaskList.TaskSubtasksChanged -= _onTaskSubtasksChanged; - TaskDetail.TaskChanged -= _onTaskChanged; - } - - public async Task InitializeAsync() - { - try - { - using var context = _dbFactory.CreateDbContext(); - var listRepo = new ListRepository(context); - var lists = await listRepo.GetAllAsync(); - foreach (var l in lists) - Lists.Add(new ListItemViewModel(l)); - } - catch (Exception ex) - { - StatusBar.ShowMessage($"Error loading lists: {ex.Message}"); - } - - _ = _worker.StartAsync().ContinueWith(t => - { - if (t.IsFaulted) - System.Diagnostics.Debug.WriteLine($"Worker connection failed: {t.Exception?.Message}"); - }, TaskScheduler.Default); - } - - partial void OnSelectedListChanged(ListItemViewModel? value) - { - _ = TaskList.LoadAsync(value?.Id); - TaskDetail.Clear(); - } - - private async void OnSelectedTaskChanged(TaskItemViewModel? task) - { - if (task is null) - TaskDetail.Clear(); - else - await TaskDetail.LoadAsync(task.Id); - } - - [RelayCommand] - private async Task AddList() - { - var editor = _listEditorFactory(); - await editor.LoadAgentsAsync(_worker); - editor.InitForCreate(); - - var window = new ListEditorView { DataContext = editor }; - editor.RequestClose += () => window.Close(); - window.Closed += (_, _) => editor.OnWindowClosed(); - _ = ShowDialogAsync(window); - - var entity = await editor.ShowAndWaitAsync(); - if (entity is null) return; - - try - { - using var context = _dbFactory.CreateDbContext(); - var listRepo = new ListRepository(context); - await listRepo.AddAsync(entity); - var configEntity = editor.BuildConfig(entity.Id); - if (configEntity is not null) - await listRepo.SetConfigAsync(configEntity); - Lists.Add(new ListItemViewModel(entity)); - } - catch (Exception ex) - { - StatusBar.ShowMessage($"Error creating list: {ex.Message}"); - } - } - - [RelayCommand] - private async Task EditList() - { - if (SelectedList is null) return; - - ListEntity? existing; - ListConfigEntity? config; - using (var context = _dbFactory.CreateDbContext()) - { - var listRepo = new ListRepository(context); - existing = await listRepo.GetByIdAsync(SelectedList.Id); - if (existing is null) return; - config = await listRepo.GetConfigAsync(existing.Id); - } - - var editor = _listEditorFactory(); - await editor.LoadAgentsAsync(_worker); - editor.InitForEdit(existing, config); - - var window = new ListEditorView { DataContext = editor }; - editor.RequestClose += () => window.Close(); - window.Closed += (_, _) => editor.OnWindowClosed(); - _ = ShowDialogAsync(window); - - var entity = await editor.ShowAndWaitAsync(); - if (entity is null) return; - - try - { - using var context = _dbFactory.CreateDbContext(); - var listRepo = new ListRepository(context); - await listRepo.UpdateAsync(entity); - var configEntity = editor.BuildConfig(entity.Id); - if (configEntity is not null) - await listRepo.SetConfigAsync(configEntity); - SelectedList.Name = entity.Name; - SelectedList.WorkingDir = entity.WorkingDir; - SelectedList.DefaultCommitType = entity.DefaultCommitType; - } - catch (Exception ex) - { - StatusBar.ShowMessage($"Error updating list: {ex.Message}"); - } - } - - [ObservableProperty] private bool _isDeleteConfirmVisible; - private ListItemViewModel? _pendingDeleteList; - - [RelayCommand] - private void DeleteList() - { - if (SelectedList is null) return; - _pendingDeleteList = SelectedList; - IsDeleteConfirmVisible = true; - } - - [RelayCommand] - private async Task ConfirmDeleteList() - { - IsDeleteConfirmVisible = false; - if (_pendingDeleteList is null) return; - try - { - using var context = _dbFactory.CreateDbContext(); - var listRepo = new ListRepository(context); - await listRepo.DeleteAsync(_pendingDeleteList.Id); - Lists.Remove(_pendingDeleteList); - if (SelectedList == _pendingDeleteList) - SelectedList = null; - } - catch (Exception ex) - { - StatusBar.ShowMessage($"Error deleting list: {ex.Message}"); - } - finally - { - _pendingDeleteList = null; - } - } - - [RelayCommand] - private void CancelDeleteList() - { - IsDeleteConfirmVisible = false; - _pendingDeleteList = null; - } - - private static async Task ShowDialogAsync(Window dialog) - { - if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop - && desktop.MainWindow is not null) - { - await dialog.ShowDialog(desktop.MainWindow); - } - else - { - dialog.Show(); - } - } -} diff --git a/src/ClaudeDo.Ui/ViewModels/StatusBarViewModel.cs b/src/ClaudeDo.Ui/ViewModels/StatusBarViewModel.cs deleted file mode 100644 index 792bd08..0000000 --- a/src/ClaudeDo.Ui/ViewModels/StatusBarViewModel.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Specialized; -using ClaudeDo.Ui.Services; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace ClaudeDo.Ui.ViewModels; - -public partial class StatusBarViewModel : ViewModelBase -{ - private readonly WorkerClient _worker; - - [ObservableProperty] private string _connectionStatus = "Offline"; - [ObservableProperty] private string _activeTasksSummary = ""; - [ObservableProperty] private string _statusMessage = ""; - - public StatusBarViewModel(WorkerClient worker) - { - _worker = worker; - - worker.PropertyChanged += (_, e) => - { - if (e.PropertyName == nameof(WorkerClient.IsConnected) || - e.PropertyName == nameof(WorkerClient.IsReconnecting)) - { - ConnectionStatus = worker.IsConnected ? "Online" - : worker.IsReconnecting ? "Connecting..." - : "Offline"; - } - }; - - worker.ActiveTasks.CollectionChanged += OnActiveTasksChanged; - RefreshActiveSummary(); - } - - private void OnActiveTasksChanged(object? sender, NotifyCollectionChangedEventArgs e) => - RefreshActiveSummary(); - - private void RefreshActiveSummary() - { - if (_worker.ActiveTasks.Count == 0) - { - ActiveTasksSummary = ""; - return; - } - - var parts = _worker.ActiveTasks - .Select(t => $"{t.Slot}: {Shorten(t.TaskId)}") - .ToList(); - ActiveTasksSummary = string.Join(" | ", parts); - } - - private static string Shorten(string id) => - id.Length > 8 ? id[..8] : id; - - public void ShowMessage(string msg) => StatusMessage = msg; -} diff --git a/src/ClaudeDo.Ui/ViewModels/SubtaskItemViewModel.cs b/src/ClaudeDo.Ui/ViewModels/SubtaskItemViewModel.cs deleted file mode 100644 index 7eea857..0000000 --- a/src/ClaudeDo.Ui/ViewModels/SubtaskItemViewModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using ClaudeDo.Data.Models; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace ClaudeDo.Ui.ViewModels; - -public partial class SubtaskItemViewModel : ObservableObject -{ - [ObservableProperty] private string _title = string.Empty; - [ObservableProperty] private bool _completed; - - public string Id { get; set; } = string.Empty; - public string? OriginalTitle { get; set; } - public bool OriginalCompleted { get; set; } - - public static SubtaskItemViewModel From(SubtaskEntity e) => new() - { - Id = e.Id, - Title = e.Title, - Completed = e.Completed, - OriginalTitle = e.Title, - OriginalCompleted = e.Completed, - }; -} diff --git a/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs b/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs deleted file mode 100644 index 8ae0b36..0000000 --- a/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs +++ /dev/null @@ -1,583 +0,0 @@ -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using ClaudeDo.Data; -using ClaudeDo.Data.Git; -using ClaudeDo.Data.Models; -using ClaudeDo.Data.Repositories; -using ClaudeDo.Ui.Helpers; -using ClaudeDo.Ui.Services; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Microsoft.EntityFrameworkCore; - -namespace ClaudeDo.Ui.ViewModels; - -public partial class TaskDetailViewModel : ViewModelBase -{ - private readonly IDbContextFactory _dbFactory; - private readonly GitService _git; - private readonly WorkerClient _worker; - - [ObservableProperty] private string _title = ""; - [ObservableProperty] private string? _description; - [ObservableProperty] private string? _result; - [ObservableProperty] private string? _logPath; - [ObservableProperty] private string _statusText = ""; - [ObservableProperty] private string _statusChoice = "Manual"; - [ObservableProperty] private string _commitType = "chore"; - [ObservableProperty] private string _modelChoice = "(list default)"; - [ObservableProperty] private string? _systemPromptOverride; - [ObservableProperty] private AgentInfo? _selectedAgent; - public List AvailableAgents { get; } = []; - - public static string[] StatusChoices { get; } = ["Manual", "Queued", "Running", "Done", "Failed"]; - public static string[] CommitTypes { get; } = ["feat", "fix", "refactor", "docs", "test", "chore", "ci", "perf", "style", "build"]; - public static string[] ModelChoices { get; } = ["(list default)", "Sonnet", "Opus", "Haiku"]; - - // Worktree - [ObservableProperty] private bool _hasWorktree; - [ObservableProperty] private string? _branchName; - [ObservableProperty] private string? _diffStat; - [ObservableProperty] private string? _worktreePath; - [ObservableProperty] private string _worktreeState = ""; - - // Live stream - [ObservableProperty] private string _liveText = ""; - private StreamLineFormatter _formatter = new(); - public ObservableCollection Tags { get; } = new(); - [ObservableProperty] private string _newTagInput = ""; - public ObservableCollection Subtasks { get; } = new(); - - private string? _taskId; - public string? CurrentTaskId => _taskId; - private string? _listId; - private bool _isLoading; - // Cancels an in-flight LoadAsync when a new TaskUpdated event arrives - // before the previous load finished — prevents torn state on _taskId, - // Subtasks, Tags, etc. - private CancellationTokenSource? _loadCts; - - public event Action? TaskChanged; - - public TaskDetailViewModel(IDbContextFactory dbFactory, GitService git, WorkerClient worker) - { - _dbFactory = dbFactory; - _git = git; - _worker = worker; - - worker.TaskMessageEvent += OnTaskMessage; - worker.WorktreeUpdatedEvent += OnWorktreeUpdated; - worker.TaskUpdatedEvent += OnTaskUpdated; - worker.RunNowRequestedEvent += OnRunNowRequested; - worker.TaskStartedEvent += OnTaskStarted; - } - - public async Task LoadAsync(string taskId) - { - // Cancel any in-flight load so rapid TaskUpdated events don't race - // on _taskId / Subtasks / Tags. The newest caller wins. - var oldCts = _loadCts; - var cts = new CancellationTokenSource(); - _loadCts = cts; - oldCts?.Cancel(); - oldCts?.Dispose(); - var ct = cts.Token; - - _taskId = taskId; - HasWorktree = false; - WorktreeState = ""; - BranchName = null; - DiffStat = null; - WorktreePath = null; - OnPropertyChanged(nameof(CanWorktreeAction)); - LiveText = ""; - _formatter = new StreamLineFormatter(); - - try - { - TaskEntity? task; - List tags; - List subtasks; - - using (var context = _dbFactory.CreateDbContext()) - { - var taskRepo = new TaskRepository(context); - task = await taskRepo.GetByIdAsync(taskId, ct); - if (task is null) return; - ct.ThrowIfCancellationRequested(); - - tags = await taskRepo.GetTagsAsync(taskId, ct); - ct.ThrowIfCancellationRequested(); - - var subtaskRepo = new SubtaskRepository(context); - subtasks = await subtaskRepo.GetByTaskIdAsync(taskId, ct); - } - - ct.ThrowIfCancellationRequested(); - - if (AvailableAgents.Count == 0) - { - var agents = await _worker.GetAgentsAsync(); - ct.ThrowIfCancellationRequested(); - AvailableAgents.AddRange(agents); - OnPropertyChanged(nameof(AvailableAgents)); - } - - _isLoading = true; - try - { - _listId = task.ListId; - Title = task.Title; - Description = task.Description; - Result = task.Result; - LogPath = task.LogPath; - if (task.LogPath is not null - && task.Status is Data.Models.TaskStatus.Done or Data.Models.TaskStatus.Failed - && File.Exists(task.LogPath)) - { - _formatter = new StreamLineFormatter(); - LiveText = await Task.Run(() => _formatter.FormatFile(task.LogPath), ct); - } - StatusText = task.Status.ToString().ToLowerInvariant(); - StatusChoice = task.Status.ToString(); - CommitType = task.CommitType; - ModelChoice = task.Model is not null - ? ListEditorViewModel.ModelIdToDisplay(task.Model) - : "(list default)"; - SystemPromptOverride = task.SystemPrompt; - if (task.AgentPath is not null) - { - var match = AvailableAgents.FirstOrDefault(a => a.Path == task.AgentPath); - if (match is null) - { - match = new AgentInfo(Path.GetFileNameWithoutExtension(task.AgentPath), "(external)", task.AgentPath); - AvailableAgents.Add(match); - OnPropertyChanged(nameof(AvailableAgents)); - } - SelectedAgent = match; - } - else - { - SelectedAgent = null; - } - - Tags.Clear(); - foreach (var tag in tags) - Tags.Add(tag); - - // Tear down old subtask subscriptions before replacing them. - foreach (var old in Subtasks) old.PropertyChanged -= OnSubtaskPropertyChanged; - Subtasks.Clear(); - foreach (var s in subtasks) - { - var vm = SubtaskItemViewModel.From(s); - vm.PropertyChanged += OnSubtaskPropertyChanged; - Subtasks.Add(vm); - } - } - finally - { - _isLoading = false; - } - - await LoadWorktreeAsync(taskId); - } - catch (OperationCanceledException) - { - // Superseded by a newer LoadAsync — nothing to do. - } - } - - public async Task SaveAsync() - { - if (_isLoading || _taskId is null) return; - - using var context = _dbFactory.CreateDbContext(); - var taskRepo = new TaskRepository(context); - var entity = await taskRepo.GetByIdAsync(_taskId); - if (entity is null) return; - - entity.Title = Title; - entity.Description = Description; - entity.CommitType = CommitType; - entity.Model = ModelChoice != "(list default)" - ? ListEditorViewModel.ModelDisplayToId(ModelChoice) - : null; - entity.SystemPrompt = string.IsNullOrWhiteSpace(SystemPromptOverride) ? null : SystemPromptOverride.Trim(); - entity.AgentPath = SelectedAgent?.Path; - - if (Enum.TryParse(StatusChoice, true, out var status)) - entity.Status = status; - - await taskRepo.UpdateAsync(entity); - StatusText = entity.Status.ToString().ToLowerInvariant(); - TaskChanged?.Invoke(_taskId); - } - - [RelayCommand] - private async Task AddTag() - { - var name = NewTagInput.Trim(); - if (string.IsNullOrEmpty(name) || _taskId is null) return; - - using var context = _dbFactory.CreateDbContext(); - var tagRepo = new TagRepository(context); - var taskRepo = new TaskRepository(context); - - var tagId = await tagRepo.GetOrCreateAsync(name); - await taskRepo.AddTagAsync(_taskId, tagId); - - Tags.Clear(); - var tags = await taskRepo.GetTagsAsync(_taskId); - foreach (var tag in tags) - Tags.Add(tag); - - NewTagInput = ""; - TaskChanged?.Invoke(_taskId); - } - - [RelayCommand] - private async Task RemoveTag(TagEntity tag) - { - if (_taskId is null) return; - using var context = _dbFactory.CreateDbContext(); - var taskRepo = new TaskRepository(context); - await taskRepo.RemoveTagAsync(_taskId, tag.Id); - Tags.Remove(tag); - TaskChanged?.Invoke(_taskId); - } - - [RelayCommand] - private async Task AddSubtask() - { - if (_taskId is null) return; - var entity = new SubtaskEntity - { - Id = Guid.NewGuid().ToString(), - TaskId = _taskId, - Title = "", - Completed = false, - OrderNum = Subtasks.Count, - CreatedAt = DateTime.UtcNow, - }; - using var context = _dbFactory.CreateDbContext(); - var subtaskRepo = new SubtaskRepository(context); - await subtaskRepo.AddAsync(entity); - var vm = SubtaskItemViewModel.From(entity); - vm.PropertyChanged += OnSubtaskPropertyChanged; - Subtasks.Add(vm); - TaskChanged?.Invoke(_taskId); - } - - [RelayCommand] - private async Task RemoveSubtask(SubtaskItemViewModel item) - { - if (_taskId is null) return; - if (!string.IsNullOrEmpty(item.Id)) - { - using var context = _dbFactory.CreateDbContext(); - var subtaskRepo = new SubtaskRepository(context); - await subtaskRepo.DeleteAsync(item.Id); - } - item.PropertyChanged -= OnSubtaskPropertyChanged; - Subtasks.Remove(item); - TaskChanged?.Invoke(_taskId); - } - - private async void OnSubtaskPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) - { - if (_isLoading || sender is not SubtaskItemViewModel vm || string.IsNullOrEmpty(vm.Id)) return; - if (e.PropertyName is not (nameof(SubtaskItemViewModel.Title) or nameof(SubtaskItemViewModel.Completed))) return; - try - { - if (_taskId is null) return; - using var context = _dbFactory.CreateDbContext(); - var orig = await context.Subtasks.AsNoTracking().FirstOrDefaultAsync(s => s.Id == vm.Id); - var subtaskRepo = new SubtaskRepository(context); - await subtaskRepo.UpdateAsync(new SubtaskEntity - { - Id = vm.Id, - TaskId = _taskId, - Title = vm.Title, - Completed = vm.Completed, - OrderNum = Subtasks.IndexOf(vm), - CreatedAt = orig?.CreatedAt ?? DateTime.UtcNow, - }); - if (e.PropertyName == nameof(SubtaskItemViewModel.Completed)) - TaskChanged?.Invoke(_taskId); - } - catch (Exception ex) - { - // async void must never throw — surface via Debug. - Debug.WriteLine($"[TaskDetailViewModel] Subtask update failed for {vm.Id}: {ex}"); - } - } - - public async Task RefreshSubtasksFromDbAsync() - { - if (_taskId is null) return; - - List subtasks; - using (var context = _dbFactory.CreateDbContext()) - { - var subtaskRepo = new SubtaskRepository(context); - subtasks = await subtaskRepo.GetByTaskIdAsync(_taskId); - } - - _isLoading = true; - try - { - foreach (var old in Subtasks) old.PropertyChanged -= OnSubtaskPropertyChanged; - Subtasks.Clear(); - foreach (var s in subtasks) - { - var vm = SubtaskItemViewModel.From(s); - vm.PropertyChanged += OnSubtaskPropertyChanged; - Subtasks.Add(vm); - } - } - finally - { - _isLoading = false; - } - } - - public void SetAgentFromPath(string path) - { - var existing = AvailableAgents.FirstOrDefault(a => a.Path == path); - if (existing is null) - { - existing = new AgentInfo(Path.GetFileNameWithoutExtension(path), "(external)", path); - AvailableAgents.Add(existing); - OnPropertyChanged(nameof(AvailableAgents)); - } - SelectedAgent = existing; - } - - public void Clear() - { - // Cancel any load in flight so it doesn't resurrect state after Clear. - _loadCts?.Cancel(); - _loadCts?.Dispose(); - _loadCts = null; - - _taskId = null; - _listId = null; - Title = ""; - Description = null; - Result = null; - LogPath = null; - StatusText = ""; - HasWorktree = false; - LiveText = ""; - _formatter = new StreamLineFormatter(); - Tags.Clear(); - NewTagInput = ""; - foreach (var s in Subtasks) s.PropertyChanged -= OnSubtaskPropertyChanged; - Subtasks.Clear(); - StatusChoice = "Manual"; - CommitType = "chore"; - ModelChoice = "(list default)"; - SystemPromptOverride = null; - SelectedAgent = null; - } - - private async Task LoadWorktreeAsync(string taskId) - { - using var context = _dbFactory.CreateDbContext(); - var wtRepo = new WorktreeRepository(context); - var wt = await wtRepo.GetByTaskIdAsync(taskId); - HasWorktree = wt is not null; - if (wt is not null) - { - BranchName = wt.BranchName; - DiffStat = wt.DiffStat; - WorktreePath = wt.Path; - WorktreeState = wt.State.ToString().ToLowerInvariant(); - } - else - { - BranchName = null; - DiffStat = null; - WorktreePath = null; - WorktreeState = ""; - } - OnPropertyChanged(nameof(CanWorktreeAction)); - } - - public bool CanWorktreeAction => HasWorktree && WorktreeState == "active"; - - [RelayCommand] - private void OpenWorktree() - { - if (WorktreePath is null) return; - try - { - Process.Start(new ProcessStartInfo - { - FileName = WorktreePath, - UseShellExecute = true, - }); - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to open worktree: {ex.Message}"); - } - } - - [RelayCommand] - private void ShowDiff() - { - if (WorktreePath is null) return; - try - { - Process.Start(new ProcessStartInfo - { - FileName = "cmd.exe", - Arguments = $"/k git -C \"{WorktreePath}\" diff HEAD~1", - UseShellExecute = true, - }); - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to show diff: {ex.Message}"); - } - } - - [RelayCommand] - private async Task MergeIntoMainAsync() - { - if (_taskId is null || _listId is null) return; - - WorktreeEntity? wt; - ListEntity? list; - using (var context = _dbFactory.CreateDbContext()) - { - var wtRepo = new WorktreeRepository(context); - wt = await wtRepo.GetByTaskIdAsync(_taskId); - var listRepo = new ListRepository(context); - list = await listRepo.GetByIdAsync(_listId); - } - if (wt is null || list?.WorkingDir is null) return; - - await _git.MergeFfOnlyAsync(list.WorkingDir, wt.BranchName); - await _git.WorktreeRemoveAsync(list.WorkingDir, wt.Path, force: true); - await _git.BranchDeleteAsync(list.WorkingDir, wt.BranchName, force: true); - - using (var context = _dbFactory.CreateDbContext()) - { - var wtRepo = new WorktreeRepository(context); - await wtRepo.SetStateAsync(_taskId, Data.Models.WorktreeState.Merged); - } - await LoadWorktreeAsync(_taskId); - } - - [RelayCommand] - private async Task KeepAsBranchAsync() - { - if (_taskId is null || _listId is null) return; - - WorktreeEntity? wt; - ListEntity? list; - using (var context = _dbFactory.CreateDbContext()) - { - var wtRepo = new WorktreeRepository(context); - wt = await wtRepo.GetByTaskIdAsync(_taskId); - var listRepo = new ListRepository(context); - list = await listRepo.GetByIdAsync(_listId); - } - if (wt is null || list?.WorkingDir is null) return; - - await _git.WorktreeRemoveAsync(list.WorkingDir, wt.Path, force: true); - - using (var context = _dbFactory.CreateDbContext()) - { - var wtRepo = new WorktreeRepository(context); - await wtRepo.SetStateAsync(_taskId, Data.Models.WorktreeState.Kept); - } - await LoadWorktreeAsync(_taskId); - } - - [RelayCommand] - private async Task DiscardAsync() - { - if (_taskId is null || _listId is null) return; - - WorktreeEntity? wt; - ListEntity? list; - using (var context = _dbFactory.CreateDbContext()) - { - var wtRepo = new WorktreeRepository(context); - wt = await wtRepo.GetByTaskIdAsync(_taskId); - var listRepo = new ListRepository(context); - list = await listRepo.GetByIdAsync(_listId); - } - if (wt is null || list?.WorkingDir is null) return; - - await _git.WorktreeRemoveAsync(list.WorkingDir, wt.Path, force: true); - await _git.BranchDeleteAsync(list.WorkingDir, wt.BranchName, force: true); - - using (var context = _dbFactory.CreateDbContext()) - { - var wtRepo = new WorktreeRepository(context); - await wtRepo.SetStateAsync(_taskId, Data.Models.WorktreeState.Discarded); - } - await LoadWorktreeAsync(_taskId); - } - - private void OnTaskMessage(string taskId, string line) - { - if (taskId != _taskId) return; - var formatted = _formatter.FormatLine(line); - if (formatted is not null) - { - LiveText += formatted; - if (LiveText.Length > 50_000) - LiveText = StreamLineFormatter.Trim(LiveText); - } - } - - private void OnRunNowRequested(string taskId) - { - if (taskId != _taskId) return; - StatusText = "starting..."; - LiveText = ""; - _formatter = new StreamLineFormatter(); - } - - private void OnTaskStarted(string slot, string taskId, DateTime startedAt) - { - if (taskId != _taskId) return; - StatusText = "running"; - } - - private async void OnWorktreeUpdated(string taskId) - { - if (taskId != _taskId) return; - try - { - await LoadWorktreeAsync(taskId); - } - catch (Exception ex) - { - // async void must never throw. - Debug.WriteLine($"[TaskDetailViewModel] OnWorktreeUpdated failed for {taskId}: {ex}"); - } - } - - private async void OnTaskUpdated(string taskId) - { - if (taskId != _taskId) return; - try - { - await LoadAsync(taskId); - } - catch (Exception ex) - { - // async void must never throw. - Debug.WriteLine($"[TaskDetailViewModel] OnTaskUpdated failed for {taskId}: {ex}"); - } - } -} diff --git a/src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs b/src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs deleted file mode 100644 index 4e39151..0000000 --- a/src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs +++ /dev/null @@ -1,264 +0,0 @@ -using System.Collections.ObjectModel; -using System.IO; -using ClaudeDo.Data; -using ClaudeDo.Data.Models; -using ClaudeDo.Data.Repositories; -using ClaudeDo.Ui.Services; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Microsoft.EntityFrameworkCore; -using TaskStatus = ClaudeDo.Data.Models.TaskStatus; - -namespace ClaudeDo.Ui.ViewModels; - -public partial class TaskEditorViewModel : ViewModelBase -{ - private readonly IDbContextFactory _dbFactory; - - [ObservableProperty] private string _title = ""; - [ObservableProperty] private string? _description; - [ObservableProperty] private string _commitType = "chore"; - [ObservableProperty] private string _statusChoice = "manual"; - [ObservableProperty] private string _tagsInput = ""; - [ObservableProperty] private string _windowTitle = "New Task"; - [ObservableProperty] private string _modelChoice = "(list default)"; - [ObservableProperty] private string? _systemPromptOverride; - [ObservableProperty] private AgentInfo? _selectedAgent; - public List AvailableAgents { get; set; } = []; - public ObservableCollection Subtasks { get; } = new(); - - private string? _editId; - private string _listId = ""; - private DateTime _createdAt; - private TaskCompletionSource _tcs = new(); - - public event Action? RequestClose; - - public static string[] ModelChoices { get; } = ["(list default)", "Sonnet", "Opus", "Haiku"]; - - public static string[] CommitTypes { get; } = - ["chore", "feat", "fix", "refactor", "docs", "test", "perf", "style", "build", "ci"]; - - public static string[] StatusChoices { get; } = - ["manual", "queued"]; - - public TaskEditorViewModel(IDbContextFactory dbFactory) - { - _dbFactory = dbFactory; - } - - public async Task LoadAgentsAsync(WorkerClient worker) - { - AvailableAgents = await worker.GetAgentsAsync(); - } - - public void SetAgentFromPath(string path) - { - var existing = AvailableAgents.FirstOrDefault(a => a.Path == path); - if (existing is null) - { - existing = new AgentInfo(Path.GetFileNameWithoutExtension(path), "(external)", path); - AvailableAgents.Add(existing); - OnPropertyChanged(nameof(AvailableAgents)); - } - SelectedAgent = existing; - } - - public IReadOnlyList SelectedTagNames => - TagsInput.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Distinct() - .ToList(); - - public void InitForCreate(string listId, string defaultCommitType = "chore") - { - _tcs = new TaskCompletionSource(); - _editId = null; - _listId = listId; - _createdAt = DateTime.UtcNow; - CommitType = defaultCommitType; - WindowTitle = "New Task"; - Subtasks.Clear(); - } - - public async Task InitForEditAsync(TaskEntity entity, IReadOnlyList taskTags, CancellationToken ct = default) - { - _tcs = new TaskCompletionSource(); - _editId = entity.Id; - _listId = entity.ListId; - _createdAt = entity.CreatedAt; - Title = entity.Title; - Description = entity.Description; - CommitType = entity.CommitType; - StatusChoice = entity.Status switch - { - TaskStatus.Manual => "manual", - TaskStatus.Queued => "queued", - _ => entity.Status.ToString().ToLowerInvariant(), - }; - TagsInput = string.Join(", ", taskTags.Select(t => t.Name)); - ModelChoice = entity.Model is not null - ? ListEditorViewModel.ModelIdToDisplay(entity.Model) - : "(list default)"; - SystemPromptOverride = entity.SystemPrompt; - - if (entity.AgentPath is not null) - { - var match = AvailableAgents.FirstOrDefault(a => a.Path == entity.AgentPath); - if (match is null) - { - match = new AgentInfo(Path.GetFileNameWithoutExtension(entity.AgentPath), "(external)", entity.AgentPath); - AvailableAgents.Add(match); - OnPropertyChanged(nameof(AvailableAgents)); - } - SelectedAgent = match; - } - else - { - SelectedAgent = null; - } - - WindowTitle = $"Edit Task: {entity.Title}"; - - Subtasks.Clear(); - using var context = _dbFactory.CreateDbContext(); - var subtaskRepo = new SubtaskRepository(context); - var list = await subtaskRepo.GetByTaskIdAsync(entity.Id, ct); - foreach (var s in list) - Subtasks.Add(SubtaskItemViewModel.From(s)); - } - - // Keep old sync overload for callers that haven't loaded agents yet - public void InitForEdit(TaskEntity entity, IReadOnlyList taskTags) - { - _tcs = new TaskCompletionSource(); - _editId = entity.Id; - _listId = entity.ListId; - _createdAt = entity.CreatedAt; - Title = entity.Title; - Description = entity.Description; - CommitType = entity.CommitType; - StatusChoice = entity.Status switch - { - TaskStatus.Manual => "manual", - TaskStatus.Queued => "queued", - _ => entity.Status.ToString().ToLowerInvariant(), - }; - TagsInput = string.Join(", ", taskTags.Select(t => t.Name)); - ModelChoice = entity.Model is not null - ? ListEditorViewModel.ModelIdToDisplay(entity.Model) - : "(list default)"; - SystemPromptOverride = entity.SystemPrompt; - - if (entity.AgentPath is not null) - { - var match = AvailableAgents.FirstOrDefault(a => a.Path == entity.AgentPath); - if (match is null) - { - match = new AgentInfo(Path.GetFileNameWithoutExtension(entity.AgentPath), "(external)", entity.AgentPath); - AvailableAgents.Add(match); - OnPropertyChanged(nameof(AvailableAgents)); - } - SelectedAgent = match; - } - else - { - SelectedAgent = null; - } - - WindowTitle = $"Edit Task: {entity.Title}"; - } - - [RelayCommand] - private void AddSubtask() => Subtasks.Add(new SubtaskItemViewModel()); - - [RelayCommand] - private void RemoveSubtask(SubtaskItemViewModel item) => Subtasks.Remove(item); - - [RelayCommand] - private async Task Save() - { - if (string.IsNullOrWhiteSpace(Title)) return; - var status = StatusChoice switch - { - "queued" => TaskStatus.Queued, - _ => TaskStatus.Manual, - }; - var taskId = _editId ?? Guid.NewGuid().ToString(); - var entity = new TaskEntity - { - Id = taskId, - ListId = _listId, - Title = Title.Trim(), - Description = string.IsNullOrWhiteSpace(Description) ? null : Description.Trim(), - Status = status, - CommitType = CommitType, - CreatedAt = _createdAt, - }; - entity.Model = ModelChoice != "(list default)" - ? ListEditorViewModel.ModelDisplayToId(ModelChoice) - : null; - entity.SystemPrompt = string.IsNullOrWhiteSpace(SystemPromptOverride) ? null : SystemPromptOverride.Trim(); - entity.AgentPath = SelectedAgent?.Path; - - // Persist subtask changes - if (_editId is not null) - { - using var context = _dbFactory.CreateDbContext(); - var subtaskRepo = new SubtaskRepository(context); - var existing = await subtaskRepo.GetByTaskIdAsync(taskId); - var existingIds = existing.Select(s => s.Id).ToHashSet(); - var currentIds = Subtasks.Where(s => s.Id != "").Select(s => s.Id).ToHashSet(); - - // Deleted - foreach (var id in existingIds.Except(currentIds)) - await subtaskRepo.DeleteAsync(id); - - // Updated - foreach (var (vm, idx) in Subtasks.Select((v, i) => (v, i))) - { - if (vm.Id == "") continue; - if (vm.Title != vm.OriginalTitle || vm.Completed != vm.OriginalCompleted) - { - var origSub = existing.FirstOrDefault(e => e.Id == vm.Id); - await subtaskRepo.UpdateAsync(new SubtaskEntity { Id = vm.Id, TaskId = taskId, Title = vm.Title, Completed = vm.Completed, OrderNum = idx, CreatedAt = origSub?.CreatedAt ?? DateTime.UtcNow }); - } - else - { - // update order_num if position changed - var orig = existing.FirstOrDefault(e => e.Id == vm.Id); - if (orig is not null && orig.OrderNum != idx) - await subtaskRepo.UpdateAsync(new SubtaskEntity { Id = vm.Id, TaskId = taskId, Title = vm.Title, Completed = vm.Completed, OrderNum = idx, CreatedAt = orig.CreatedAt }); - } - } - } - - // Added (id == "" means new) - { - using var context = _dbFactory.CreateDbContext(); - var subtaskRepo = new SubtaskRepository(context); - foreach (var (vm, idx) in Subtasks.Select((v, i) => (v, i)).Where(x => x.v.Id == "")) - { - if (string.IsNullOrWhiteSpace(vm.Title)) continue; - var newId = Guid.NewGuid().ToString(); - await subtaskRepo.AddAsync(new SubtaskEntity { Id = newId, TaskId = taskId, Title = vm.Title.Trim(), Completed = vm.Completed, OrderNum = idx, CreatedAt = DateTime.UtcNow }); - } - } - - _tcs.TrySetResult(entity); - RequestClose?.Invoke(); - } - - [RelayCommand] - private void Cancel() - { - _tcs.TrySetResult(null); - RequestClose?.Invoke(); - } - - public void OnWindowClosed() - { - _tcs.TrySetResult(null); - } - - public Task ShowAndWaitAsync() => _tcs.Task; -} diff --git a/src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs b/src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs deleted file mode 100644 index c6c44ca..0000000 --- a/src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System.Collections.ObjectModel; -using Avalonia.Media; -using ClaudeDo.Data; -using ClaudeDo.Data.Models; -using ClaudeDo.Data.Repositories; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Microsoft.EntityFrameworkCore; -using TaskStatus = ClaudeDo.Data.Models.TaskStatus; - -namespace ClaudeDo.Ui.ViewModels; - -public partial class TaskItemViewModel : ViewModelBase -{ - [ObservableProperty] private string _title; - [ObservableProperty] private string _statusText; - [ObservableProperty] private string _tagsText; - [ObservableProperty] private string _commitType; - [ObservableProperty] private string? _description; - [ObservableProperty] private TaskStatus _status; - [ObservableProperty] private bool _isStarting; - [ObservableProperty] private bool _isExpanded; - [ObservableProperty] private bool _hasSubtasks; - [ObservableProperty] private int _subtaskCount; - - public ObservableCollection Subtasks { get; } = new(); - - public string Id { get; } - public string ListId { get; } - public TaskEntity Entity { get; private set; } - - private readonly Func? _runNow; - private readonly Func _canRunNow; - private readonly Func? _toggleDone; - private readonly Action? _onSubtasksChanged; - private readonly IDbContextFactory _dbFactory; - private bool _subtasksLoaded; - - public TaskItemViewModel(TaskEntity entity, IReadOnlyList tags, - Func? runNow, Func canRunNow, - IDbContextFactory dbFactory, int subtaskCount, - Func? toggleDone = null, - Action? onSubtasksChanged = null) - { - Entity = entity; - Id = entity.Id; - ListId = entity.ListId; - _title = entity.Title; - _status = entity.Status; - _statusText = entity.Status.ToString().ToLowerInvariant(); - _tagsText = string.Join(", ", tags.Select(t => t.Name)); - _commitType = entity.CommitType; - _description = entity.Description; - _runNow = runNow; - _canRunNow = canRunNow; - _toggleDone = toggleDone; - _onSubtasksChanged = onSubtasksChanged; - _dbFactory = dbFactory; - _subtaskCount = subtaskCount; - _hasSubtasks = subtaskCount > 0; - } - - public bool IsDone => Status == TaskStatus.Done; - public bool IsRunning => Status == TaskStatus.Running; - public bool CanToggleDone => Status != TaskStatus.Running && Status != TaskStatus.Failed; - - public TextDecorationCollection? TitleDecorations => IsDone - ? TextDecorations.Strikethrough - : null; - - public IBrush TitleForeground => IsDone - ? new SolidColorBrush(Color.Parse("#5a6578")) - : new SolidColorBrush(Color.Parse("#e2e8f0")); - - public double RowOpacity => IsDone ? 0.6 : 1.0; - - public void Refresh(TaskEntity entity, IReadOnlyList tags) - { - Entity = entity; - Title = entity.Title; - Status = entity.Status; - StatusText = entity.Status.ToString().ToLowerInvariant(); - TagsText = string.Join(", ", tags.Select(t => t.Name)); - CommitType = entity.CommitType; - Description = entity.Description; - RunNowCommand.NotifyCanExecuteChanged(); - OnPropertyChanged(nameof(IsDone)); - OnPropertyChanged(nameof(IsRunning)); - IsStarting = false; - OnPropertyChanged(nameof(CanToggleDone)); - OnPropertyChanged(nameof(TitleDecorations)); - OnPropertyChanged(nameof(TitleForeground)); - OnPropertyChanged(nameof(RowOpacity)); - ToggleDoneCommand.NotifyCanExecuteChanged(); - } - - public void SetStarting() - { - IsStarting = true; - StatusText = "starting..."; - RunNowCommand.NotifyCanExecuteChanged(); - } - - public void ClearStarting() - { - IsStarting = false; - RunNowCommand.NotifyCanExecuteChanged(); - } - - [RelayCommand(CanExecute = nameof(CanRunNow))] - private async Task RunNowAsync() - { - if (_runNow is not null) - await _runNow(Id); - } - - private bool CanRunNow() => - _canRunNow() && Status != TaskStatus.Running && !IsStarting; - - [RelayCommand(CanExecute = nameof(CanToggleDone))] - private async Task ToggleDone() - { - if (_toggleDone is not null) - await _toggleDone(Id); - } - - [RelayCommand] - private async Task ToggleExpanded() - { - IsExpanded = !IsExpanded; - if (IsExpanded && !_subtasksLoaded) - await LoadSubtasksAsync(); - } - - private async Task LoadSubtasksAsync() - { - using var context = _dbFactory.CreateDbContext(); - var repo = new SubtaskRepository(context); - var entities = await repo.GetByTaskIdAsync(Id); - Subtasks.Clear(); - foreach (var e in entities) - Subtasks.Add(SubtaskItemViewModel.From(e)); - _subtasksLoaded = true; - } - - [RelayCommand] - private async Task ToggleSubtaskDone(string subtaskId) - { - var vm = Subtasks.FirstOrDefault(s => s.Id == subtaskId); - if (vm is null) return; - vm.Completed = !vm.Completed; - - using var context = _dbFactory.CreateDbContext(); - var entity = await context.Subtasks.FindAsync(subtaskId); - if (entity is not null) - { - entity.Completed = vm.Completed; - await context.SaveChangesAsync(); - } - - _onSubtasksChanged?.Invoke(Id); - } - - public async Task RefreshSubtasksAsync(int newCount) - { - SubtaskCount = newCount; - HasSubtasks = newCount > 0; - if (!HasSubtasks) - { - IsExpanded = false; - Subtasks.Clear(); - _subtasksLoaded = false; - } - else if (_subtasksLoaded || IsExpanded) - { - await LoadSubtasksAsync(); - } - } -} diff --git a/src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs b/src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs deleted file mode 100644 index 0bb6ecf..0000000 --- a/src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs +++ /dev/null @@ -1,360 +0,0 @@ -using System.Collections.ObjectModel; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; -using ClaudeDo.Data; -using ClaudeDo.Data.Models; -using ClaudeDo.Data.Repositories; -using ClaudeDo.Ui.Services; -using ClaudeDo.Ui.Views; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Microsoft.EntityFrameworkCore; -using TaskStatus = ClaudeDo.Data.Models.TaskStatus; - -namespace ClaudeDo.Ui.ViewModels; - -public partial class TaskListViewModel : ViewModelBase -{ - private readonly IDbContextFactory _dbFactory; - private readonly WorkerClient _worker; - private readonly Func _editorFactory; - private readonly Action _showMessage; - - public ObservableCollection Tasks { get; } = new(); - - [ObservableProperty] private TaskItemViewModel? _selectedTask; - [ObservableProperty, NotifyCanExecuteChangedFor(nameof(AddTaskCommand))] private string? _currentListId; - [ObservableProperty] private string _listName = "Tasks"; - [ObservableProperty] private string _inlineAddTitle = ""; - - public event Action? SelectedTaskChanged; - public event Action? TaskSubtasksChanged; - - partial void OnSelectedTaskChanged(TaskItemViewModel? value) => - SelectedTaskChanged?.Invoke(value); - - private void NotifySubtasksChanged(string taskId) => - TaskSubtasksChanged?.Invoke(taskId); - - public TaskListViewModel(IDbContextFactory dbFactory, WorkerClient worker, - Func editorFactory, Action showMessage) - { - _dbFactory = dbFactory; - _worker = worker; - _editorFactory = editorFactory; - _showMessage = showMessage; - - worker.TaskUpdatedEvent += OnTaskUpdated; - worker.TaskFinishedEvent += (_, taskId, _, _) => OnTaskUpdated(taskId); - worker.PropertyChanged += (_, e) => - { - if (e.PropertyName == nameof(WorkerClient.IsConnected)) - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - foreach (var t in Tasks) - t.RunNowCommand.NotifyCanExecuteChanged(); - }); - }; - - worker.RunNowRequestedEvent += taskId => - { - var item = Tasks.FirstOrDefault(t => t.Id == taskId); - item?.SetStarting(); - }; - - worker.TaskStartedEvent += (_, taskId, _) => - { - var item = Tasks.FirstOrDefault(t => t.Id == taskId); - item?.ClearStarting(); - }; - } - - public async Task LoadAsync(string? listId) - { - CurrentListId = listId; - Tasks.Clear(); - SelectedTask = null; - - if (listId is not null) - { - using var context = _dbFactory.CreateDbContext(); - var listRepo = new ListRepository(context); - var list = await listRepo.GetByIdAsync(listId); - ListName = list?.Name ?? "Tasks"; - } - else - { - ListName = "Tasks"; - } - - if (listId is null) return; - - try - { - using var context = _dbFactory.CreateDbContext(); - var taskRepo = new TaskRepository(context); - var entities = await taskRepo.GetByListIdAsync(listId); - var taskIds = entities.Select(e => e.Id).ToList(); - var subtaskCounts = await context.Subtasks - .Where(s => taskIds.Contains(s.TaskId)) - .GroupBy(s => s.TaskId) - .ToDictionaryAsync(g => g.Key, g => g.Count()); - foreach (var e in entities) - { - var tags = await taskRepo.GetEffectiveTagsAsync(e.Id); - subtaskCounts.TryGetValue(e.Id, out var count); - Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected, - _dbFactory, count, ToggleDoneAsync, NotifySubtasksChanged)); - } - } - catch (Exception ex) - { - _showMessage($"Error loading tasks: {ex.Message}"); - } - } - - private bool CanAddTask() => CurrentListId is not null; - - [RelayCommand(CanExecute = nameof(CanAddTask))] - private async Task InlineAdd() - { - var title = InlineAddTitle.Trim(); - if (string.IsNullOrEmpty(title) || CurrentListId is null) return; - - string defaultCommitType; - using (var context = _dbFactory.CreateDbContext()) - { - var listRepo = new ListRepository(context); - var list = await listRepo.GetByIdAsync(CurrentListId); - defaultCommitType = list?.DefaultCommitType ?? "chore"; - } - - var entity = new TaskEntity - { - Id = Guid.NewGuid().ToString(), - ListId = CurrentListId, - Title = title, - Status = TaskStatus.Manual, - CommitType = defaultCommitType, - CreatedAt = DateTime.UtcNow, - }; - - try - { - using var context = _dbFactory.CreateDbContext(); - var taskRepo = new TaskRepository(context); - await taskRepo.AddAsync(entity); - var tags = await taskRepo.GetEffectiveTagsAsync(entity.Id); - var vm = new TaskItemViewModel(entity, tags, RunNowAsync, () => _worker.IsConnected, - _dbFactory, 0, ToggleDoneAsync, NotifySubtasksChanged); - Tasks.Add(vm); - SelectedTask = vm; - InlineAddTitle = ""; - } - catch (Exception ex) - { - _showMessage($"Error creating task: {ex.Message}"); - } - } - - [RelayCommand(CanExecute = nameof(CanAddTask))] - private async Task AddTask() - { - var listId = CurrentListId; - if (listId is null) return; - - string defaultCommitType; - using (var context = _dbFactory.CreateDbContext()) - { - var listRepo = new ListRepository(context); - var list = await listRepo.GetByIdAsync(listId); - defaultCommitType = list?.DefaultCommitType ?? "chore"; - } - - var editor = _editorFactory(); - await editor.LoadAgentsAsync(_worker); - editor.InitForCreate(listId, defaultCommitType); - - var window = new TaskEditorView { DataContext = editor }; - editor.RequestClose += () => window.Close(); - window.Closed += (_, _) => editor.OnWindowClosed(); - _ = ShowDialogAsync(window); - - var saved = await editor.ShowAndWaitAsync(); - if (saved is null) return; - - try - { - using var context = _dbFactory.CreateDbContext(); - var taskRepo = new TaskRepository(context); - var tagRepo = new TagRepository(context); - await taskRepo.AddAsync(saved); - - foreach (var tagName in editor.SelectedTagNames) - { - var tagId = await tagRepo.GetOrCreateAsync(tagName); - await taskRepo.AddTagAsync(saved.Id, tagId); - } - - var tags = await taskRepo.GetEffectiveTagsAsync(saved.Id); - Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected, - _dbFactory, 0, ToggleDoneAsync, NotifySubtasksChanged)); - - // Auto wake-queue if agent+queued - if (saved.Status == TaskStatus.Queued && - tags.Any(t => t.Name == "agent")) - { - try { await _worker.WakeQueueAsync(); } - catch { /* worker offline is fine */ } - } - } - catch (Exception ex) - { - _showMessage($"Error creating task: {ex.Message}"); - } - } - - [RelayCommand] - private async Task EditTask() - { - if (SelectedTask is null || CurrentListId is null) return; - - TaskEntity? entity; - List taskTags; - using (var context = _dbFactory.CreateDbContext()) - { - var taskRepo = new TaskRepository(context); - entity = await taskRepo.GetByIdAsync(SelectedTask.Id); - if (entity is null) return; - taskTags = await taskRepo.GetTagsAsync(entity.Id); - } - - var editor = _editorFactory(); - await editor.LoadAgentsAsync(_worker); - await editor.InitForEditAsync(entity, taskTags); - - var window = new TaskEditorView { DataContext = editor }; - editor.RequestClose += () => window.Close(); - window.Closed += (_, _) => editor.OnWindowClosed(); - _ = ShowDialogAsync(window); - - var saved = await editor.ShowAndWaitAsync(); - if (saved is null) return; - - try - { - using var context = _dbFactory.CreateDbContext(); - var taskRepo = new TaskRepository(context); - var tagRepo = new TagRepository(context); - await taskRepo.UpdateAsync(saved); - - var existingTags = await taskRepo.GetTagsAsync(saved.Id); - foreach (var old in existingTags) - await taskRepo.RemoveTagAsync(saved.Id, old.Id); - foreach (var tagName in editor.SelectedTagNames) - { - var tagId = await tagRepo.GetOrCreateAsync(tagName); - await taskRepo.AddTagAsync(saved.Id, tagId); - } - - var newTags = await taskRepo.GetEffectiveTagsAsync(saved.Id); - SelectedTask.Refresh(saved, newTags); - } - catch (Exception ex) - { - _showMessage($"Error updating task: {ex.Message}"); - } - } - - [RelayCommand] - private async Task DeleteTask() - { - if (SelectedTask is null) return; - try - { - using var context = _dbFactory.CreateDbContext(); - var taskRepo = new TaskRepository(context); - await taskRepo.DeleteAsync(SelectedTask.Id); - Tasks.Remove(SelectedTask); - SelectedTask = null; - } - catch (Exception ex) - { - _showMessage($"Error deleting task: {ex.Message}"); - } - } - - public async Task RefreshSingleAsync(string taskId) - { - using var context = _dbFactory.CreateDbContext(); - var taskRepo = new TaskRepository(context); - var entity = await taskRepo.GetByIdAsync(taskId); - var existing = Tasks.FirstOrDefault(t => t.Id == taskId); - if (entity is null) - { - if (existing is not null) Tasks.Remove(existing); - return; - } - var tags = await taskRepo.GetEffectiveTagsAsync(taskId); - if (existing is not null) - { - existing.Refresh(entity, tags); - var subtaskCount = await context.Subtasks.CountAsync(s => s.TaskId == taskId); - await existing.RefreshSubtasksAsync(subtaskCount); - } - } - - private async Task RunNowAsync(string taskId) - { - try - { - await _worker.RunNowAsync(taskId); - } - catch (Exception ex) - { - _showMessage($"RunNow failed: {ex.Message}"); - } - } - - private async Task ToggleDoneAsync(string taskId) - { - using var context = _dbFactory.CreateDbContext(); - var taskRepo = new TaskRepository(context); - var entity = await taskRepo.GetByIdAsync(taskId); - if (entity is null) return; - - entity.Status = entity.Status == TaskStatus.Done ? TaskStatus.Manual : TaskStatus.Done; - if (entity.Status == TaskStatus.Done) - entity.FinishedAt = DateTime.UtcNow; - - await taskRepo.UpdateAsync(entity); - await RefreshSingleAsync(taskId); - } - - private async void OnTaskUpdated(string taskId) - { - if (CurrentListId is null) return; - try - { - await RefreshSingleAsync(taskId); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[TaskListViewModel] OnTaskUpdated failed for {taskId}: {ex}"); - } - } - - private static async Task ShowDialogAsync(Window dialog) - { - if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop - && desktop.MainWindow is not null) - { - await dialog.ShowDialog(desktop.MainWindow); - } - else - { - dialog.Show(); - } - } -} diff --git a/src/ClaudeDo.Ui/Views/ListEditorView.axaml b/src/ClaudeDo.Ui/Views/ListEditorView.axaml deleted file mode 100644 index d918674..0000000 --- a/src/ClaudeDo.Ui/Views/ListEditorView.axaml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/ClaudeDo.Ui/Views/TaskListView.axaml.cs b/src/ClaudeDo.Ui/Views/TaskListView.axaml.cs deleted file mode 100644 index 5e1ca90..0000000 --- a/src/ClaudeDo.Ui/Views/TaskListView.axaml.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System.Collections.ObjectModel; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.VisualTree; -using ClaudeDo.Ui.ViewModels; - -namespace ClaudeDo.Ui.Views; - -public partial class TaskListView : UserControl -{ - public TaskListView() - { - InitializeComponent(); - } - - private void OnInlineAddKeyDown(object? sender, KeyEventArgs e) - { - if (DataContext is not TaskListViewModel vm) return; - - if (e.Key == Key.Enter) - { - vm.InlineAddCommand.Execute(null); - e.Handled = true; - } - else if (e.Key == Key.Escape) - { - vm.InlineAddTitle = ""; - this.FindControl("TaskListBox")?.Focus(); - e.Handled = true; - } - } - - private void OnInlineAddGotFocus(object? sender, FocusChangedEventArgs e) - { - if (sender is TextBox tb) - tb.BorderBrush = Avalonia.Application.Current?.FindResource("AccentBrush") as Avalonia.Media.IBrush; - } - - private void OnInlineAddLostFocus(object? sender, RoutedEventArgs e) - { - if (sender is TextBox tb) - tb.BorderBrush = Avalonia.Application.Current?.FindResource("BorderSubtleBrush") as Avalonia.Media.IBrush; - } - - private void OnTaskListKeyDown(object? sender, KeyEventArgs e) - { - if (DataContext is not TaskListViewModel vm || vm.SelectedTask is null) return; - - switch (e.Key) - { - case Key.Delete: - vm.DeleteTaskCommand.Execute(null); - e.Handled = true; - break; - case Key.Space: - if (vm.SelectedTask.CanToggleDone) - { - vm.SelectedTask.ToggleDoneCommand.Execute(null); - e.Handled = true; - } - break; - case Key.Enter: - case Key.F2: - var detailView = this.GetVisualAncestors().OfType().FirstOrDefault() - ?.GetVisualDescendants().OfType().FirstOrDefault(); - detailView?.FocusTitle(); - e.Handled = true; - break; - } - } - - private void OnCheckboxPressed(object? sender, PointerPressedEventArgs e) - { - if (sender is not Border { DataContext: TaskItemViewModel task }) return; - if (task.CanToggleDone) - { - task.ToggleDoneCommand.Execute(null); - e.Handled = true; - } - } - - private void OnTaskItemDoubleTapped(object? sender, TappedEventArgs e) - { - if (DataContext is TaskListViewModel vm) - vm.EditTaskCommand.Execute(null); - } - - private void OnTaskItemPointerPressed(object? sender, PointerPressedEventArgs e) - { - var props = e.GetCurrentPoint(this).Properties; - if (!props.IsRightButtonPressed) return; - - if (sender is Grid { DataContext: TaskItemViewModel item } - && DataContext is TaskListViewModel vm) - { - vm.SelectedTask = item; - } - } - - private void OnSubtaskPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (e.GetCurrentPoint(null).Properties.IsRightButtonPressed - && sender is Control { DataContext: SubtaskItemViewModel subtask } - && DataContext is TaskListViewModel vm) - { - var parent = vm.Tasks.FirstOrDefault(t => t.Subtasks.Contains(subtask)); - if (parent is not null) - vm.SelectedTask = parent; - } - } - - private async void OnSubtaskCheckboxClick(object? sender, RoutedEventArgs e) - { - if (sender is CheckBox { DataContext: SubtaskItemViewModel subtask } - && DataContext is TaskListViewModel vm) - { - var parent = vm.Tasks.FirstOrDefault(t => t.Subtasks.Contains(subtask)); - if (parent is not null) - await parent.ToggleSubtaskDoneCommand.ExecuteAsync(subtask.Id); - } - } - - public void FocusInlineAdd() - { - this.FindControl("InlineAddBox")?.Focus(); - } -}