diff --git a/docs/open.md b/docs/open.md index a12020d..15baac9 100644 --- a/docs/open.md +++ b/docs/open.md @@ -207,3 +207,22 @@ Preconditions: a working Gitea release at `git.kuns.dev/releases/ClaudeDo` with 7. Repeat step 6 with **Continue anyway** → wizard opens without self-update. 8. Repeat step 6 with **Cancel** → installer exits without any action. 9. Kill network during startup in both app and installer → confirm silent fallback (no errors, no banner, wizard opens normally). + +--- + +## Planning Sessions — Manual Verification (Plan C UI) + +Requires Plan B (worker hub endpoints) merged. Until then, only the UI structure/styling checks are meaningful. + +1. Create a Manual task with a title and a TODO-ish description. +2. Right-click the task → **Open planning Session** — Windows Terminal opens with Claude CLI running (Plan B). +3. Ask Claude to create two child tasks via `mcp__claudedo__create_child_task`. +4. Watch the UI: drafts appear indented under the parent, italic, reduced opacity, with a `DRAFT` badge. +5. The parent shows a `PLANNING` badge. Click the chevron → children collapse; click again → children expand. +6. Ask Claude to `finalize` — drafts flip to Manual/Queued children; parent flips to `PLANNED` badge. +7. In a new planning task, close the terminal without finalize. Right-click the Planning task → the unfinished-session modal opens with Resume / Finalize now / Discard. +8. Attempt to delete a parent with children via the details panel — confirm the friendly error dialog appears and the task is NOT deleted. + +**Known followups (non-blocking):** +- `Border.badge.planned` style (blue) is defined in `IslandStyles.axaml` but never applied — `TaskRowView` keeps the `planning` class for both Planning and Planned, so Planned gets the amber badge. Either make the view swap `Classes.planned` when status is Planned, or remove the unused style + brush. +- Dead `Instance` statics on `BoolToItalicConverter` and `BoolToDraftOpacityConverter` — App.axaml registers instances via the resource dictionary; the static members can be removed. diff --git a/src/ClaudeDo.App/App.axaml b/src/ClaudeDo.App/App.axaml index 0c7445f..e7e3f98 100644 --- a/src/ClaudeDo.App/App.axaml +++ b/src/ClaudeDo.App/App.axaml @@ -17,7 +17,9 @@ - + + + diff --git a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj index 2ea6f07..7a19886 100644 --- a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj +++ b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj @@ -15,6 +15,7 @@ + diff --git a/src/ClaudeDo.Ui/Converters/BoolToDraftOpacityConverter.cs b/src/ClaudeDo.Ui/Converters/BoolToDraftOpacityConverter.cs new file mode 100644 index 0000000..e2d8d04 --- /dev/null +++ b/src/ClaudeDo.Ui/Converters/BoolToDraftOpacityConverter.cs @@ -0,0 +1,15 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace ClaudeDo.Ui.Converters; + +public sealed class BoolToDraftOpacityConverter : IValueConverter +{ + public static BoolToDraftOpacityConverter Instance { get; } = new(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + => value is true ? 0.7 : 1.0; + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotSupportedException(); +} diff --git a/src/ClaudeDo.Ui/Converters/BoolToItalicConverter.cs b/src/ClaudeDo.Ui/Converters/BoolToItalicConverter.cs new file mode 100644 index 0000000..0017101 --- /dev/null +++ b/src/ClaudeDo.Ui/Converters/BoolToItalicConverter.cs @@ -0,0 +1,16 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace ClaudeDo.Ui.Converters; + +public sealed class BoolToItalicConverter : IValueConverter +{ + public static BoolToItalicConverter Instance { get; } = new(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + => value is true ? FontStyle.Italic : FontStyle.Normal; + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotSupportedException(); +} diff --git a/src/ClaudeDo.Ui/Design/IslandStyles.axaml b/src/ClaudeDo.Ui/Design/IslandStyles.axaml index 0cd8837..5cbc092 100644 --- a/src/ClaudeDo.Ui/Design/IslandStyles.axaml +++ b/src/ClaudeDo.Ui/Design/IslandStyles.axaml @@ -84,6 +84,11 @@ M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65a.5.5 0 0 0 .12-.64l-2-3.46a.5.5 0 0 0-.61-.22l-2.49 1a7.03 7.03 0 0 0-1.69-.98l-.38-2.65a.5.5 0 0 0-.5-.42h-4a.5.5 0 0 0-.5.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1a.5.5 0 0 0-.61.22l-2 3.46a.5.5 0 0 0 .12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65a.5.5 0 0 0-.12.64l2 3.46a.5.5 0 0 0 .61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65a.5.5 0 0 0 .5.42h4a.5.5 0 0 0 .5-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1a.5.5 0 0 0 .61-.22l2-3.46a.5.5 0 0 0-.12-.64l-2.11-1.65z + + + + + @@ -866,4 +871,31 @@ + + + + + + + + + + + + + diff --git a/src/ClaudeDo.Ui/Services/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/IWorkerClient.cs new file mode 100644 index 0000000..08cf2fc --- /dev/null +++ b/src/ClaudeDo.Ui/Services/IWorkerClient.cs @@ -0,0 +1,11 @@ +namespace ClaudeDo.Ui.Services; + +public interface IWorkerClient +{ + Task WakeQueueAsync(); + Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default); + 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/PlanningDtos.cs b/src/ClaudeDo.Ui/Services/PlanningDtos.cs new file mode 100644 index 0000000..71a553e --- /dev/null +++ b/src/ClaudeDo.Ui/Services/PlanningDtos.cs @@ -0,0 +1,18 @@ +namespace ClaudeDo.Ui.Services; + +public sealed record PlanningSessionFilesDto( + string SessionDirectory, + string McpConfigPath, + string SystemPromptPath, + string InitialPromptPath); + +public sealed record PlanningSessionStartInfo( + string ParentTaskId, + string WorkingDir, + PlanningSessionFilesDto Files); + +public sealed record PlanningSessionResumeInfo( + string ParentTaskId, + string WorkingDir, + string ClaudeSessionId, + string McpConfigPath); diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index 0612c37..a330714 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -25,7 +25,7 @@ sealed class IndefiniteRetryPolicy : IRetryPolicy _delays[Math.Min(retryContext.PreviousRetryCount, _delays.Length - 1)]; } -public partial class WorkerClient : ObservableObject, IAsyncDisposable +public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerClient { private readonly HubConnection _hub; private CancellationTokenSource? _startCts; @@ -347,6 +347,33 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable } } + public async Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) + => await _hub.InvokeAsync("StartPlanningSessionAsync", taskId, ct); + + public async Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) + => await _hub.InvokeAsync("ResumePlanningSessionAsync", taskId, ct); + + public async Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) + => await _hub.InvokeAsync("DiscardPlanningSessionAsync", taskId, ct); + + public async Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) + => await _hub.InvokeAsync("FinalizePlanningSessionAsync", taskId, queueAgentTasks, ct); + + public async Task GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) + => await _hub.InvokeAsync("GetPendingDraftCountAsync", taskId, ct); + + // IWorkerClient explicit implementations (drop typed return values) + async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct) + => await StartPlanningSessionAsync(taskId, ct); + async Task IWorkerClient.ResumePlanningSessionAsync(string taskId, CancellationToken ct) + => await ResumePlanningSessionAsync(taskId, ct); + async Task IWorkerClient.DiscardPlanningSessionAsync(string taskId, CancellationToken ct) + => 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/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index 461710b..29cc32f 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -139,6 +139,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase // Set by the view so DeleteTaskCommand can prompt yes/no before deleting public Func>? ConfirmAsync { get; set; } + // Set by the view so DeleteTaskCommand can show an error message + public Func? ShowErrorAsync { get; set; } + public DetailsIslandViewModel(IDbContextFactory dbFactory, WorkerClient worker, IServiceProvider services) { _dbFactory = dbFactory; @@ -537,9 +540,20 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase var ok = await ConfirmAsync($"Delete \"{row.Title}\"? This cannot be undone."); if (!ok) return; } - await using var ctx = _dbFactory.CreateDbContext(); - var repo = new TaskRepository(ctx); - await repo.DeleteAsync(row.Id); + try + { + await using var ctx = _dbFactory.CreateDbContext(); + var repo = new TaskRepository(ctx); + await repo.DeleteAsync(row.Id); + } + catch (DbUpdateException ex) when ( + ex.Message.Contains("FOREIGN KEY", StringComparison.OrdinalIgnoreCase) + || ex.InnerException?.Message.Contains("FOREIGN KEY", StringComparison.OrdinalIgnoreCase) == true) + { + if (ShowErrorAsync != null) + await ShowErrorAsync("This task has child tasks. Discard the planning session or delete child tasks first."); + return; + } if (DeleteFromList != null) await DeleteFromList(row); CloseDetail?.Invoke(); diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs index 2fe8ddc..3d4a5bc 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs @@ -23,6 +23,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase [ObservableProperty] private int _diffDeletions; [ObservableProperty] private bool _dropHintAbove; [ObservableProperty] private bool _dropHintBelow; + [ObservableProperty] private string? _parentTaskId; + [ObservableProperty] private bool _isExpanded = true; public DateTime CreatedAt { get; init; } public string CreatedAtFormatted => CreatedAt == default ? "—" : $"Created {CreatedAt:MMM d}"; @@ -31,6 +33,20 @@ public sealed partial class TaskRowViewModel : ViewModelBase public int StepsCount { get; init; } public int StepsCompleted { get; init; } + public bool IsChild => !string.IsNullOrEmpty(ParentTaskId); + public bool IsPlanningParent => Status == TaskStatus.Planning || Status == TaskStatus.Planned; + public bool IsDraft => Status == TaskStatus.Draft; + + public bool CanOpenPlanningSession => Status == TaskStatus.Manual && !IsChild; + public bool CanResumeOrDiscardPlanning => Status == TaskStatus.Planning; + + public string? PlanningBadge => Status switch + { + TaskStatus.Planning => "PLANNING", + TaskStatus.Planned => "PLANNED", + _ => null, + }; + public bool HasBranch => !string.IsNullOrWhiteSpace(Branch); public bool HasDiff => DiffAdditions > 0 || DiffDeletions > 0; public bool HasTags => Tags.Count > 0; @@ -60,6 +76,17 @@ public sealed partial class TaskRowViewModel : ViewModelBase OnPropertyChanged(nameof(IsRunning)); OnPropertyChanged(nameof(IsQueued)); OnPropertyChanged(nameof(HasLiveTail)); + OnPropertyChanged(nameof(IsPlanningParent)); + OnPropertyChanged(nameof(PlanningBadge)); + OnPropertyChanged(nameof(IsDraft)); + OnPropertyChanged(nameof(CanOpenPlanningSession)); + OnPropertyChanged(nameof(CanResumeOrDiscardPlanning)); + } + + partial void OnParentTaskIdChanged(string? value) + { + OnPropertyChanged(nameof(IsChild)); + OnPropertyChanged(nameof(CanOpenPlanningSession)); } partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch)); @@ -91,6 +118,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase DiffAdditions = add, DiffDeletions = del, CreatedAt = t.CreatedAt, + ParentTaskId = t.ParentTaskId, }; } diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index 904a654..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; @@ -13,7 +14,8 @@ namespace ClaudeDo.Ui.ViewModels.Islands; public sealed partial class TasksIslandViewModel : ViewModelBase { private readonly IDbContextFactory _dbFactory; - private readonly WorkerClient? _worker; + private readonly IWorkerClient? _worker; + private readonly Dictionary _expandedState = new(); private ListNavItemViewModel? _currentList; private CancellationTokenSource? _loadCts; @@ -41,7 +43,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase [ObservableProperty] private bool _showOpenLabel; [ObservableProperty] private string _completedHeader = "COMPLETED"; - public TasksIslandViewModel(IDbContextFactory dbFactory, WorkerClient? worker = null) + public Func? ShowUnfinishedPlanningModal { get; set; } + + public TasksIslandViewModel(IDbContextFactory dbFactory, IWorkerClient? worker = null) { _dbFactory = dbFactory; _worker = worker; @@ -105,14 +109,35 @@ public sealed partial class TasksIslandViewModel : ViewModelBase catch (OperationCanceledException) { } } - private void Regroup() + internal void Regroup() { OverdueItems.Clear(); OpenItems.Clear(); CompletedItems.Clear(); - var today = DateTime.Today; + // Restore IsExpanded from saved state foreach (var r in Items) + { + if (_expandedState.TryGetValue(r.Id, out var saved)) + r.IsExpanded = saved; + } + + // Build hierarchy-aware flat list: top-level rows interleaved with visible children. + // Items is already ordered by SortOrder from the DB query. + var topLevel = Items.Where(r => !r.IsChild); + var flat = new List(); + foreach (var parent in topLevel) + { + flat.Add(parent); + if (parent.IsPlanningParent && parent.IsExpanded) + { + var children = Items.Where(r => r.ParentTaskId == parent.Id); + flat.AddRange(children); + } + } + + var today = DateTime.Today; + foreach (var r in flat) { if (r.Done) CompletedItems.Add(r); @@ -356,6 +381,79 @@ public sealed partial class TasksIslandViewModel : ViewModelBase [RelayCommand] private void OpenListSettings() => OpenListSettingsRequested?.Invoke(this, EventArgs.Empty); + [RelayCommand] + private async Task OpenPlanningSessionAsync(TaskRowViewModel? row) + { + if (row is null || row.Status != TaskStatus.Manual) return; + try { await _worker!.StartPlanningSessionAsync(row.Id); } + catch { } + } + + [RelayCommand] + private async Task ResumePlanningSessionAsync(TaskRowViewModel? row) + { + if (row is null || !row.IsPlanningParent) return; + 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 { } + } + + [RelayCommand] + private async Task DiscardPlanningSessionAsync(TaskRowViewModel? row) + { + if (row is null) return; + try { await _worker!.DiscardPlanningSessionAsync(row.Id); } + catch { } + } + + [RelayCommand] + private async Task FinalizePlanningSessionAsync(TaskRowViewModel? row) + { + if (row is null) return; + try { await _worker!.FinalizePlanningSessionAsync(row.Id, queueAgentTasks: true); } + catch { } + } + + [RelayCommand] + private void ToggleExpand(TaskRowViewModel? row) + { + if (row is null) return; + var next = !(_expandedState.TryGetValue(row.Id, out var current) ? current : row.IsExpanded); + _expandedState[row.Id] = next; + row.IsExpanded = next; + Regroup(); + } + partial void OnSelectedTaskChanged(TaskRowViewModel? value) { foreach (var i in Items) i.IsSelected = ReferenceEquals(i, value); 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/DetailsIslandView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs index 9d345a8..62016b7 100644 --- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs @@ -45,9 +45,49 @@ public partial class DetailsIslandView : UserControl }; vm.ConfirmAsync = ShowConfirmAsync; + vm.ShowErrorAsync = ShowErrorDialogAsync; } } + private async System.Threading.Tasks.Task ShowErrorDialogAsync(string message) + { + var owner = TopLevel.GetTopLevel(this) as Window; + if (owner == null) return; + + var ok = new Button { Content = "OK", MinWidth = 90 }; + + var dialog = new Window + { + Title = "Error", + Width = 360, + SizeToContent = SizeToContent.Height, + CanResize = false, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + ShowInTaskbar = false, + Background = this.FindResource("SurfaceBrush") as IBrush, + Content = new StackPanel + { + Spacing = 16, + Margin = new Thickness(20), + Children = + { + new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap }, + new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 8, + HorizontalAlignment = HorizontalAlignment.Right, + Children = { ok } + } + } + } + }; + + ok.Click += (_, _) => dialog.Close(); + + await dialog.ShowDialog(owner); + } + private async System.Threading.Tasks.Task ShowConfirmAsync(string message) { var owner = TopLevel.GetTopLevel(this) as Window; diff --git a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml index f7a7ed1..1e4bdb1 100644 --- a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml @@ -15,134 +15,189 @@ Background="{DynamicResource MossBrush}" CornerRadius="1" IsVisible="{Binding DropHintAbove}"/> - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + - - - + + + + + + + - - + + + + + + + - - - - + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - + 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 @@ + + + + + + + + + + + + + + + + + +