feat(ui): polish worktrees overview modal

- Restyle to match ListSettingsModalView: custom title bar, DeepBrush
  toolbar, LineBrush footer, SurfaceBrush outer border, no system chrome
- Add column header row (TASK / BRANCH / STATE / DIFF / AGE) with
  TextFaintBrush + MonoFont + LetterSpacing, separator line below
- Replace wt-row style with task-row-equivalent: transparent bg,
  CornerRadius 8, 1px border, :pointerover + .selected transitions
- Add IsSelected to WorktreeOverviewRowViewModel; SelectRow() helper
  on modal VM clears previous selection before setting new one
- Wire OnRowTapped in code-behind for click-to-select
- Wire ShowDiff: VM takes Func<WorktreeModalViewModel> factory, builds
  diffVm and delegates window creation to both call sites (MainWindow
  and ListsIslandView); register Func<WorktreeModalViewModel> in DI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-05-19 10:38:19 +02:00
parent 8f4e37ef56
commit ca71275fc4
6 changed files with 181 additions and 48 deletions

View File

@@ -95,6 +95,7 @@ sealed class Program
// ViewModels
sc.AddTransient<WorktreeModalViewModel>();
sc.AddTransient<Func<WorktreeModalViewModel>>(sp => () => sp.GetRequiredService<WorktreeModalViewModel>());
sc.AddTransient<WorktreesOverviewModalViewModel>();
sc.AddTransient<Func<WorktreesOverviewModalViewModel>>(sp => () => sp.GetRequiredService<WorktreesOverviewModalViewModel>());
sc.AddSingleton<IPrimeScheduleApi, WorkerPrimeScheduleApi>();

View File

@@ -24,6 +24,7 @@ public sealed partial class WorktreeOverviewRowViewModel : ViewModelBase
[ObservableProperty] private string? _diffStat;
[ObservableProperty] private DateTime _createdAt;
[ObservableProperty] private bool _pathExistsOnDisk;
[ObservableProperty] private bool _isSelected;
public string AgeText => FormatAge(DateTime.UtcNow - CreatedAt);
public bool IsActive => State == WorktreeState.Active;
@@ -48,24 +49,34 @@ public sealed partial class WorktreesGroupViewModel : ViewModelBase
public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
{
private readonly WorkerClient _worker;
private readonly Func<WorktreeModalViewModel> _diffVmFactory;
[ObservableProperty] private string? _listIdFilter;
[ObservableProperty] private string _title = "Worktrees";
[ObservableProperty] private bool _isGlobal;
[ObservableProperty] private bool _isBusy;
[ObservableProperty] private string? _statusMessage;
[ObservableProperty] private WorktreeOverviewRowViewModel? _selectedRow;
public ObservableCollection<WorktreeOverviewRowViewModel> Rows { get; } = new();
public ObservableCollection<WorktreesGroupViewModel> Groups { get; } = new();
public Action? CloseAction { get; set; }
public Action<WorktreeOverviewRowViewModel>? ShowDiffAction { get; set; }
public Action<WorktreeModalViewModel>? ShowDiffAction { get; set; }
public Action<string, string>? JumpToTaskAction { get; set; }
public Func<string, Task<bool>>? ConfirmAction { get; set; }
public WorktreesOverviewModalViewModel(WorkerClient worker)
public WorktreesOverviewModalViewModel(WorkerClient worker, Func<WorktreeModalViewModel> diffVmFactory)
{
_worker = worker;
_diffVmFactory = diffVmFactory;
}
public void SelectRow(WorktreeOverviewRowViewModel row)
{
if (SelectedRow is not null) SelectedRow.IsSelected = false;
SelectedRow = row;
row.IsSelected = true;
}
public void Configure(string? listId, string? listName)
@@ -136,7 +147,9 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
private void ShowDiff(WorktreeOverviewRowViewModel? row)
{
if (row is null) return;
ShowDiffAction?.Invoke(row);
var diffVm = _diffVmFactory();
diffVm.WorktreePath = row.Path;
ShowDiffAction?.Invoke(diffVm);
}
[RelayCommand]

View File

@@ -38,7 +38,15 @@ public partial class ListsIslandView : UserControl
if (item is not null) v.SelectedList = item;
}
};
// TODO: ShowDiffAction and ConfirmAction not wired in v1
modal.ShowDiffAction = diffVm =>
{
var top2 = TopLevel.GetTopLevel(this) as Window;
if (top2 is null) return;
var dlg = new WorktreeModalView { DataContext = diffVm };
diffVm.CloseAction = () => dlg.Close();
_ = diffVm.LoadAsync();
_ = dlg.ShowDialog(top2);
};
var top = TopLevel.GetTopLevel(this) as Window;
if (top is null) window.Show();
else await window.ShowDialog(top);

View File

@@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Input;
using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.Modals;
using ClaudeDo.Ui.Views.Modals;
using ClaudeDo.Ui.Views.Planning;
@@ -45,7 +46,13 @@ public partial class MainWindow : Window
if (item is not null && s.Lists is not null) s.Lists.SelectedList = item;
}
};
// TODO: ShowDiffAction and ConfirmAction not wired in v1
modal.ShowDiffAction = diffVm =>
{
var diffDlg = new WorktreeModalView { DataContext = diffVm };
diffVm.CloseAction = () => diffDlg.Close();
_ = diffVm.LoadAsync();
_ = diffDlg.ShowDialog(this);
};
await dlg.ShowDialog(this);
};
}

View File

@@ -7,11 +7,16 @@
Title="{Binding Title}"
Width="900" Height="560" MinWidth="640" MinHeight="360"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource VoidBrush}">
Background="{DynamicResource VoidBrush}"
SystemDecorations="None"
ExtendClientAreaToDecorationsHint="True">
<Window.Resources>
<converters:WorktreeStateColorConverter x:Key="WorktreeStateColor"/>
<DataTemplate x:Key="WorktreeRowTemplate" x:DataType="vm:WorktreeOverviewRowViewModel">
<Border Classes="wt-row">
<Border Classes="wt-row"
Classes.selected="{Binding IsSelected}"
Tapped="OnRowTapped">
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="Show diff"
@@ -80,51 +85,133 @@
<Window.Styles>
<Style Selector="Border.wt-row">
<Setter Property="Background" Value="{DynamicResource DeepBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource LineBrush}"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="Padding" Value="12,10"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{DynamicResource LineBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Margin" Value="0,0,0,6"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="BorderBrush" Duration="0:0:0.10"/>
</Transitions>
</Setter>
</Style>
<Style Selector="Border.wt-row:pointerover">
<Setter Property="BorderBrush" Value="{DynamicResource LineBrightBrush}"/>
</Style>
<Style Selector="Border.wt-row.selected">
<Setter Property="BorderBrush" Value="{DynamicResource LineBrightBrush}"/>
</Style>
</Window.Styles>
<DockPanel LastChildFill="True" Margin="12">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="0,0,0,8">
<Button Content="Refresh" Command="{Binding RefreshCommand}" IsEnabled="{Binding !IsBusy}"/>
<Button Content="Cleanup finished" Command="{Binding CleanupFinishedCommand}" IsEnabled="{Binding !IsBusy}"/>
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center" Margin="12,0,0,0"
Foreground="{DynamicResource TextDimBrush}"/>
</StackPanel>
<Border Background="{DynamicResource SurfaceBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1">
<Grid RowDefinitions="36,Auto,*,52">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,8,0,0">
<Button Content="Close" Command="{Binding CloseCommand}"/>
</StackPanel>
<!-- Title bar -->
<Border Grid.Row="0"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1"
PointerPressed="OnTitleBarPressed">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0"
Text="{Binding Title}"
VerticalAlignment="Center"
Margin="14,0,0,0"
FontSize="12"
FontWeight="SemiBold"
Foreground="{DynamicResource TextBrush}"/>
<Button Grid.Column="1"
Content="✕"
Command="{Binding CloseCommand}"
Margin="0,0,8,0"
Width="28" Height="28"
FontSize="11"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"/>
</Grid>
</Border>
<ScrollViewer>
<Grid>
<ItemsControl ItemsSource="{Binding Rows}" IsVisible="{Binding !IsGlobal}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:WorktreeOverviewRowViewModel">
<ContentControl ContentTemplate="{StaticResource WorktreeRowTemplate}" Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Toolbar -->
<Border Grid.Row="1"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1"
Padding="12,8">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Refresh" Command="{Binding RefreshCommand}" IsEnabled="{Binding !IsBusy}"/>
<Button Content="Cleanup finished" Command="{Binding CleanupFinishedCommand}" IsEnabled="{Binding !IsBusy}"/>
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center" Margin="12,0,0,0"
Foreground="{DynamicResource TextDimBrush}"/>
</StackPanel>
</Border>
<ItemsControl ItemsSource="{Binding Groups}" IsVisible="{Binding IsGlobal}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:WorktreesGroupViewModel">
<Expander Header="{Binding ListName}" IsExpanded="True" Margin="0,0,0,6">
<ItemsControl ItemsSource="{Binding Rows}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:WorktreeOverviewRowViewModel">
<ContentControl ContentTemplate="{StaticResource WorktreeRowTemplate}" Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ScrollViewer>
</DockPanel>
<!-- Content -->
<ScrollViewer Grid.Row="2" Padding="12,8">
<StackPanel>
<!-- Column headers -->
<Grid ColumnDefinitions="*,200,90,80,80" Margin="12,0,12,4">
<TextBlock Grid.Column="0" Text="TASK"
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
Foreground="{DynamicResource TextFaintBrush}"/>
<TextBlock Grid.Column="1" Text="BRANCH"
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
Foreground="{DynamicResource TextFaintBrush}"/>
<TextBlock Grid.Column="2" Text="STATE"
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
Foreground="{DynamicResource TextFaintBrush}"/>
<TextBlock Grid.Column="3" Text="DIFF"
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
Foreground="{DynamicResource TextFaintBrush}"/>
<TextBlock Grid.Column="4" Text="AGE"
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
Foreground="{DynamicResource TextFaintBrush}"/>
</Grid>
<Border Height="1" Background="{DynamicResource LineBrush}" Margin="0,0,0,8"/>
<!-- Rows (per-list) -->
<ItemsControl ItemsSource="{Binding Rows}" IsVisible="{Binding !IsGlobal}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:WorktreeOverviewRowViewModel">
<ContentControl ContentTemplate="{StaticResource WorktreeRowTemplate}" Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Rows (global, grouped) -->
<ItemsControl ItemsSource="{Binding Groups}" IsVisible="{Binding IsGlobal}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:WorktreesGroupViewModel">
<Expander Header="{Binding ListName}" IsExpanded="True" Margin="0,0,0,6">
<ItemsControl ItemsSource="{Binding Rows}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:WorktreeOverviewRowViewModel">
<ContentControl ContentTemplate="{StaticResource WorktreeRowTemplate}" Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
<!-- Footer -->
<Border Grid.Row="3"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0"
Padding="12,10">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Close" Command="{Binding CloseCommand}"/>
</StackPanel>
</Border>
</Grid>
</Border>
</Window>

View File

@@ -1,8 +1,25 @@
using Avalonia.Controls;
using Avalonia.Input;
using ClaudeDo.Ui.ViewModels.Modals;
namespace ClaudeDo.Ui.Views.Modals;
public partial class WorktreesOverviewModalView : Window
{
public WorktreesOverviewModalView() => InitializeComponent();
private void OnRowTapped(object? sender, TappedEventArgs e)
{
if (sender is Border { DataContext: WorktreeOverviewRowViewModel row } &&
DataContext is WorktreesOverviewModalViewModel vm)
{
vm.SelectRow(row);
}
}
private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
}