From 327ae2b69c9cfd03189e7776ceddd0a30a355e1b Mon Sep 17 00:00:00 2001 From: mika kuns Date: Fri, 24 Jul 2026 14:14:10 +0200 Subject: [PATCH] feat(ui): add merge-helper task selection dialog --- src/ClaudeDo.App/Program.cs | 1 + src/ClaudeDo.Localization/locales/de.json | 14 ++ src/ClaudeDo.Localization/locales/en.json | 14 ++ src/ClaudeDo.Ui/Services/IDialogService.cs | 3 + .../MergeHelperSelectionModalViewModel.cs | 124 ++++++++++++ .../Modals/MergeHelperSelectionModal.axaml | 81 ++++++++ .../Modals/MergeHelperSelectionModal.axaml.cs | 11 ++ src/ClaudeDo.Ui/Views/WindowDialogService.cs | 9 + ...MergeHelperSelectionModalViewModelTests.cs | 183 ++++++++++++++++++ 9 files changed, 440 insertions(+) create mode 100644 src/ClaudeDo.Ui/ViewModels/Modals/MergeHelperSelectionModalViewModel.cs create mode 100644 src/ClaudeDo.Ui/Views/Modals/MergeHelperSelectionModal.axaml create mode 100644 src/ClaudeDo.Ui/Views/Modals/MergeHelperSelectionModal.axaml.cs create mode 100644 tests/ClaudeDo.Ui.Tests/ViewModels/MergeHelperSelectionModalViewModelTests.cs diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs index 2575faac..2fe3d046 100644 --- a/src/ClaudeDo.App/Program.cs +++ b/src/ClaudeDo.App/Program.cs @@ -125,6 +125,7 @@ sealed class Program sc.AddTransient>(sp => () => sp.GetRequiredService()); sc.AddTransient(); sc.AddTransient>(sp => () => sp.GetRequiredService()); + sc.AddTransient(); sc.AddSingleton(); sc.AddSingleton(); sc.AddSingleton(); diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index 5c806957..548d2eaa 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -365,6 +365,20 @@ "forgetFolders": "Ordner vergessen", "alreadyAdded": "(bereits hinzugefügt)" }, + "mergeHelper": { + "title": "CLAUDE MACHEN LASSEN", + "windowTitle": "Claude machen lassen", + "scopeAll": "Alle Listen", + "scopeList": "Liste: {0}", + "confirm": "Claude machen lassen", + "cancel": "Abbrechen", + "selectAll": "Alle auswählen", + "selectNone": "Keine auswählen", + "empty": "Keine Aufgaben zu übergeben — alles erledigt oder abgebrochen.", + "columnTask": "AUFGABE", + "columnStatus": "STATUS", + "columnList": "LISTE" + }, "unfinishedPlanning": { "title": "UNVOLLENDETE PLANUNGSSITZUNG", "windowTitle": "Unvollendete Planungssitzung", diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 5e1c1b7e..2ec04a2c 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -365,6 +365,20 @@ "forgetFolders": "Forget folders", "alreadyAdded": "(already added)" }, + "mergeHelper": { + "title": "LET CLAUDE HANDLE IT", + "windowTitle": "Let Claude handle it", + "scopeAll": "All lists", + "scopeList": "List: {0}", + "confirm": "Let Claude handle it", + "cancel": "Cancel", + "selectAll": "Select all", + "selectNone": "Select none", + "empty": "No tasks to hand over — everything is done or cancelled.", + "columnTask": "TASK", + "columnStatus": "STATUS", + "columnList": "LIST" + }, "unfinishedPlanning": { "title": "UNFINISHED PLANNING SESSION", "windowTitle": "Unfinished planning session", diff --git a/src/ClaudeDo.Ui/Services/IDialogService.cs b/src/ClaudeDo.Ui/Services/IDialogService.cs index d1cb8ead..b8a433c9 100644 --- a/src/ClaudeDo.Ui/Services/IDialogService.cs +++ b/src/ClaudeDo.Ui/Services/IDialogService.cs @@ -22,6 +22,9 @@ public interface IDialogService Task ShowListSettingsAsync(ListSettingsModalViewModel vm); Task ShowRepoImportAsync(RepoImportModalViewModel vm); Task ShowWorktreesOverviewAsync(WorktreesOverviewModalViewModel vm); + + /// Merge-helper task picker. Returns the ordered selected task ids, or null on cancel. + Task?> ShowMergeHelperSelectionAsync(MergeHelperSelectionModalViewModel vm); Task ShowWorkerConnectionAsync(WorkerConnectionModalViewModel vm); Task ShowConflictResolverAsync(ConflictResolverViewModel vm); Task ShowLogVisualizerAsync(LogVisualizerViewModel vm); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/MergeHelperSelectionModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/MergeHelperSelectionModalViewModel.cs new file mode 100644 index 00000000..857710a9 --- /dev/null +++ b/src/ClaudeDo.Ui/ViewModels/Modals/MergeHelperSelectionModalViewModel.cs @@ -0,0 +1,124 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using ClaudeDo.Data; +using ClaudeDo.Ui.Localization; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.EntityFrameworkCore; +using TaskStatus = ClaudeDo.Data.Models.TaskStatus; + +namespace ClaudeDo.Ui.ViewModels.Modals; + +public sealed partial class MergeHelperTaskRowViewModel : ViewModelBase +{ + public required string Id { get; init; } + public required string Title { get; init; } + public required string StatusText { get; init; } + public required string ListName { get; init; } + + [ObservableProperty] private bool _isSelected; +} + +/// +/// "Let Claude handle it" task picker: lists the non-terminal tasks of a list (or all lists), +/// pre-ticks the actionable ones, and resolves with the ordered selected +/// task ids (null on cancel). +/// +public sealed partial class MergeHelperSelectionModalViewModel : ViewModelBase +{ + private readonly IDbContextFactory _dbFactory; + private string? _listId; + + public ObservableCollection Tasks { get; } = new(); + + [ObservableProperty] private string _scopeLabel = ""; + [ObservableProperty] private bool _isGlobal; + + public bool HasTasks => Tasks.Count > 0; + public bool CanConfirm => Tasks.Any(t => t.IsSelected); + + public TaskCompletionSource?> Result { get; } = new(); + public Action? CloseAction { get; set; } + + public MergeHelperSelectionModalViewModel(IDbContextFactory dbFactory) + => _dbFactory = dbFactory; + + public void Configure(string? listId, string? listName) + { + _listId = listId; + IsGlobal = listId is null; + ScopeLabel = listId is null + ? Loc.T("modals.mergeHelper.scopeAll") + : Loc.T("modals.mergeHelper.scopeList", listName ?? ""); + } + + public async Task LoadAsync(CancellationToken ct = default) + { + foreach (var row in Tasks) row.PropertyChanged -= OnRowChanged; + Tasks.Clear(); + + await using var ctx = await _dbFactory.CreateDbContextAsync(ct); + var query = ctx.Tasks.AsNoTracking() + .Where(t => t.Status != TaskStatus.Done && t.Status != TaskStatus.Cancelled); + if (_listId is not null) + query = query.Where(t => t.ListId == _listId); + + var candidates = await query + .OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt) + .Select(t => new { t.Id, t.Title, t.Status, ListName = t.List.Name }) + .ToListAsync(ct); + + foreach (var c in candidates) + { + var row = new MergeHelperTaskRowViewModel + { + Id = c.Id, + Title = c.Title, + StatusText = c.Status.ToString(), + ListName = c.ListName, + IsSelected = IsTickedByDefault(c.Status), + }; + row.PropertyChanged += OnRowChanged; + Tasks.Add(row); + } + OnPropertyChanged(nameof(HasTasks)); + OnPropertyChanged(nameof(CanConfirm)); + } + + // Actionable statuses are pre-ticked; Running/WaitingForChildren are listed but unticked + // (the helper would only poll them). Done/Cancelled never make it into the list. + internal static bool IsTickedByDefault(TaskStatus status) => status + is TaskStatus.Idle or TaskStatus.Queued or TaskStatus.WaitingForReview or TaskStatus.Failed; + + private void OnRowChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(MergeHelperTaskRowViewModel.IsSelected)) + OnPropertyChanged(nameof(CanConfirm)); + } + + [RelayCommand] + private void SelectAll() + { + foreach (var t in Tasks) t.IsSelected = true; + } + + [RelayCommand] + private void SelectNone() + { + foreach (var t in Tasks) t.IsSelected = false; + } + + [RelayCommand] + private void Confirm() + { + Result.TrySetResult(Tasks.Where(t => t.IsSelected).Select(t => t.Id).ToList()); + CloseAction?.Invoke(); + } + + [RelayCommand] + private void Cancel() + { + Result.TrySetResult(null); + CloseAction?.Invoke(); + } +} diff --git a/src/ClaudeDo.Ui/Views/Modals/MergeHelperSelectionModal.axaml b/src/ClaudeDo.Ui/Views/Modals/MergeHelperSelectionModal.axaml new file mode 100644 index 00000000..09a431e3 --- /dev/null +++ b/src/ClaudeDo.Ui/Views/Modals/MergeHelperSelectionModal.axaml @@ -0,0 +1,81 @@ + + + + + + + + +