From 47b49743c073c689b9491cd42541c6f37db69c01 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 23 Apr 2026 19:12:59 +0200 Subject: [PATCH] feat(ui): unfinished planning session dialog Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.Ui/Services/IWorkerClient.cs | 1 + src/ClaudeDo.Ui/Services/WorkerClient.cs | 2 + .../Islands/TasksIslandViewModel.cs | 36 +++++++- .../UnfinishedPlanningModalViewModel.cs | 27 ++++++ .../Views/Islands/TasksIslandView.axaml.cs | 14 ++++ .../Modals/UnfinishedPlanningModalView.axaml | 82 +++++++++++++++++++ .../UnfinishedPlanningModalView.axaml.cs | 23 ++++++ .../UiVm/TasksIslandViewModelPlanningTests.cs | 1 + 8 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/ClaudeDo.Ui/ViewModels/Modals/UnfinishedPlanningModalViewModel.cs create mode 100644 src/ClaudeDo.Ui/Views/Modals/UnfinishedPlanningModalView.axaml create mode 100644 src/ClaudeDo.Ui/Views/Modals/UnfinishedPlanningModalView.axaml.cs diff --git a/src/ClaudeDo.Ui/Services/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/IWorkerClient.cs index a904c50..08cf2fc 100644 --- a/src/ClaudeDo.Ui/Services/IWorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/IWorkerClient.cs @@ -7,4 +7,5 @@ public interface IWorkerClient Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default); Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default); Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default); + Task GetPendingDraftCountAsync(string taskId, CancellationToken ct = default); } diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index 57566e6..a330714 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -371,6 +371,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC => await DiscardPlanningSessionAsync(taskId, ct); async Task IWorkerClient.FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks, CancellationToken ct) => await FinalizePlanningSessionAsync(taskId, queueAgentTasks, ct); + async Task IWorkerClient.GetPendingDraftCountAsync(string taskId, CancellationToken ct) + => await GetPendingDraftCountAsync(taskId, ct); // DTOs for deserializing hub responses private sealed class ActiveTaskDto diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index 4111a86..baa02c9 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input; using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Ui.Services; +using ClaudeDo.Ui.ViewModels.Modals; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; @@ -42,6 +43,8 @@ public sealed partial class TasksIslandViewModel : ViewModelBase [ObservableProperty] private bool _showOpenLabel; [ObservableProperty] private string _completedHeader = "COMPLETED"; + public Func? ShowUnfinishedPlanningModal { get; set; } + public TasksIslandViewModel(IDbContextFactory dbFactory, IWorkerClient? worker = null) { _dbFactory = dbFactory; @@ -390,7 +393,38 @@ public sealed partial class TasksIslandViewModel : ViewModelBase private async Task ResumePlanningSessionAsync(TaskRowViewModel? row) { if (row is null || !row.IsPlanningParent) return; - try { await _worker!.ResumePlanningSessionAsync(row.Id); } + if (_worker is null) return; + try + { + var draftCount = await _worker.GetPendingDraftCountAsync(row.Id); + var modalVm = new UnfinishedPlanningModalViewModel + { + TaskTitle = row.Title, + DraftCount = draftCount, + }; + + if (ShowUnfinishedPlanningModal is null) + return; + await ShowUnfinishedPlanningModal(modalVm); + + var choice = await modalVm.Result.Task; + + switch (choice) + { + case UnfinishedPlanningModalResult.Resume: + await _worker.ResumePlanningSessionAsync(row.Id); + break; + case UnfinishedPlanningModalResult.FinalizeNow: + await _worker.FinalizePlanningSessionAsync(row.Id); + break; + case UnfinishedPlanningModalResult.Discard: + await _worker.DiscardPlanningSessionAsync(row.Id); + break; + case UnfinishedPlanningModalResult.Cancel: + default: + break; + } + } catch { } } diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/UnfinishedPlanningModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/UnfinishedPlanningModalViewModel.cs new file mode 100644 index 0000000..d0f23a4 --- /dev/null +++ b/src/ClaudeDo.Ui/ViewModels/Modals/UnfinishedPlanningModalViewModel.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace ClaudeDo.Ui.ViewModels.Modals; + +public enum UnfinishedPlanningModalResult +{ + Cancel, + Resume, + FinalizeNow, + Discard, +} + +public sealed partial class UnfinishedPlanningModalViewModel : ViewModelBase +{ + [ObservableProperty] private string _taskTitle = ""; + [ObservableProperty] private int _draftCount; + + public TaskCompletionSource Result { get; } = new(); + public Action? CloseAction { get; set; } + + [RelayCommand] private void Resume() { Result.TrySetResult(UnfinishedPlanningModalResult.Resume); CloseAction?.Invoke(); } + [RelayCommand] private void FinalizeNow() { Result.TrySetResult(UnfinishedPlanningModalResult.FinalizeNow); CloseAction?.Invoke(); } + [RelayCommand] private void Discard() { Result.TrySetResult(UnfinishedPlanningModalResult.Discard); CloseAction?.Invoke(); } + [RelayCommand] private void Cancel() { Result.TrySetResult(UnfinishedPlanningModalResult.Cancel); CloseAction?.Invoke(); } +} diff --git a/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml.cs index 3137855..882efa1 100644 --- a/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml.cs @@ -4,6 +4,8 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.VisualTree; using ClaudeDo.Ui.ViewModels.Islands; +using ClaudeDo.Ui.ViewModels.Modals; +using ClaudeDo.Ui.Views.Modals; namespace ClaudeDo.Ui.Views.Islands; @@ -19,7 +21,19 @@ public partial class TasksIslandView : UserControl DataContextChanged += (_, _) => { if (DataContext is TasksIslandViewModel vm) + { vm.FocusAddTaskRequested += (_, _) => AddTaskBox.Focus(); + vm.ShowUnfinishedPlanningModal = async (modalVm) => + { + var owner = TopLevel.GetTopLevel(this) as Window; + if (owner is null) { modalVm.CancelCommand.Execute(null); return; } + var modal = new UnfinishedPlanningModalView { DataContext = modalVm }; + // Closing via the OS title-bar (if ever enabled) also resolves the TCS. + modal.Closed += (_, _) => modalVm.CancelCommand.Execute(null); + await modal.ShowDialog(owner); + // ShowDialog completes once the window is closed (CloseAction or OS close). + }; + } }; } diff --git a/src/ClaudeDo.Ui/Views/Modals/UnfinishedPlanningModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/UnfinishedPlanningModalView.axaml new file mode 100644 index 0000000..7e02abd --- /dev/null +++ b/src/ClaudeDo.Ui/Views/Modals/UnfinishedPlanningModalView.axaml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + +