feat(ui): unfinished planning session dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,4 +7,5 @@ public interface IWorkerClient
|
|||||||
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default);
|
Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default);
|
||||||
|
Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -371,6 +371,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
=> await DiscardPlanningSessionAsync(taskId, ct);
|
=> await DiscardPlanningSessionAsync(taskId, ct);
|
||||||
async Task IWorkerClient.FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks, CancellationToken ct)
|
async Task IWorkerClient.FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks, CancellationToken ct)
|
||||||
=> await FinalizePlanningSessionAsync(taskId, queueAgentTasks, ct);
|
=> await FinalizePlanningSessionAsync(taskId, queueAgentTasks, ct);
|
||||||
|
async Task<int> IWorkerClient.GetPendingDraftCountAsync(string taskId, CancellationToken ct)
|
||||||
|
=> await GetPendingDraftCountAsync(taskId, ct);
|
||||||
|
|
||||||
// DTOs for deserializing hub responses
|
// DTOs for deserializing hub responses
|
||||||
private sealed class ActiveTaskDto
|
private sealed class ActiveTaskDto
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input;
|
|||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
|
|
||||||
@@ -42,6 +43,8 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _showOpenLabel;
|
[ObservableProperty] private bool _showOpenLabel;
|
||||||
[ObservableProperty] private string _completedHeader = "COMPLETED";
|
[ObservableProperty] private string _completedHeader = "COMPLETED";
|
||||||
|
|
||||||
|
public Func<UnfinishedPlanningModalViewModel, Task>? ShowUnfinishedPlanningModal { get; set; }
|
||||||
|
|
||||||
public TasksIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient? worker = null)
|
public TasksIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient? worker = null)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
@@ -390,7 +393,38 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
private async Task ResumePlanningSessionAsync(TaskRowViewModel? row)
|
private async Task ResumePlanningSessionAsync(TaskRowViewModel? row)
|
||||||
{
|
{
|
||||||
if (row is null || !row.IsPlanningParent) return;
|
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 { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<UnfinishedPlanningModalResult> 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(); }
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ using Avalonia.Input;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
using ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
using ClaudeDo.Ui.Views.Modals;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views.Islands;
|
namespace ClaudeDo.Ui.Views.Islands;
|
||||||
|
|
||||||
@@ -19,7 +21,19 @@ public partial class TasksIslandView : UserControl
|
|||||||
DataContextChanged += (_, _) =>
|
DataContextChanged += (_, _) =>
|
||||||
{
|
{
|
||||||
if (DataContext is TasksIslandViewModel vm)
|
if (DataContext is TasksIslandViewModel vm)
|
||||||
|
{
|
||||||
vm.FocusAddTaskRequested += (_, _) => AddTaskBox.Focus();
|
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).
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals"
|
||||||
|
x:Class="ClaudeDo.Ui.Views.Modals.UnfinishedPlanningModalView"
|
||||||
|
x:DataType="vm:UnfinishedPlanningModalViewModel"
|
||||||
|
Title="Unfinished planning session"
|
||||||
|
Width="440" Height="200"
|
||||||
|
CanResize="False"
|
||||||
|
SystemDecorations="None"
|
||||||
|
ExtendClientAreaToDecorationsHint="True"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Background="{DynamicResource SurfaceBrush}">
|
||||||
|
|
||||||
|
<Window.KeyBindings>
|
||||||
|
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
|
||||||
|
</Window.KeyBindings>
|
||||||
|
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="Button.primary">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource DeepBrush}"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource SurfaceBrush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="1">
|
||||||
|
<Grid RowDefinitions="36,*,52">
|
||||||
|
|
||||||
|
<!-- Title bar -->
|
||||||
|
<Border Grid.Row="0"
|
||||||
|
x:Name="TitleBar"
|
||||||
|
Background="{DynamicResource DeepBrush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="0,0,0,1"
|
||||||
|
PointerPressed="TitleBar_PointerPressed">
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
|
||||||
|
<TextBlock Text="UNFINISHED PLANNING SESSION"
|
||||||
|
FontFamily="{DynamicResource MonoFont}"
|
||||||
|
FontSize="11"
|
||||||
|
LetterSpacing="1.4"
|
||||||
|
Foreground="{DynamicResource TextBrush}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Classes="icon-btn"
|
||||||
|
Content="✕"
|
||||||
|
FontSize="12"
|
||||||
|
Command="{Binding CancelCommand}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<StackPanel Grid.Row="1" Margin="20,16" Spacing="8">
|
||||||
|
<TextBlock Text="{Binding TaskTitle}"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource TextBrush}"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
|
<TextBlock Foreground="{DynamicResource TextDimBrush}">
|
||||||
|
<Run Text="{Binding DraftCount}"/>
|
||||||
|
<Run Text=" draft task(s) waiting to be finalized."/>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<Border Grid.Row="2"
|
||||||
|
Background="{DynamicResource DeepBrush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="0,1,0,0">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8"
|
||||||
|
HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
|
Margin="16,0">
|
||||||
|
<Button Content="Discard" Command="{Binding DiscardCommand}" MinWidth="80"/>
|
||||||
|
<Button Content="Finalize" Command="{Binding FinalizeNowCommand}" MinWidth="80"/>
|
||||||
|
<Button Content="Resume" Command="{Binding ResumeCommand}" Classes="primary" MinWidth="80"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Views.Modals;
|
||||||
|
|
||||||
|
public partial class UnfinishedPlanningModalView : Window
|
||||||
|
{
|
||||||
|
public UnfinishedPlanningModalView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContextChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
if (DataContext is ViewModels.Modals.UnfinishedPlanningModalViewModel vm)
|
||||||
|
vm.CloseAction = () => Close();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||||
|
BeginMoveDrag(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ sealed class FakeWorkerClient : IWorkerClient
|
|||||||
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) { ResumePlanningCalls++; return Task.CompletedTask; }
|
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) { ResumePlanningCalls++; return Task.CompletedTask; }
|
||||||
public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) { DiscardPlanningCalls++; return Task.CompletedTask; }
|
public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) { DiscardPlanningCalls++; return Task.CompletedTask; }
|
||||||
public Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) { FinalizePlanningCalls++; return Task.CompletedTask; }
|
public Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) { FinalizePlanningCalls++; return Task.CompletedTask; }
|
||||||
|
public Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helper to build VM with pre-seeded Items ──────────────────────────────────
|
// ── Helper to build VM with pre-seeded Items ──────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user