fix(ui): make overview modal resizable; add diff content pane

Drop outer Border wrapper in WorktreesOverviewModalView so Avalonia edge
resize handles reach the window frame. Add split pane to WorktreeModalView
with file tree on left and per-file unified diff on right; wire SelectedNode
via SelectedItem TwoWay binding + SelectionChanged fallback; add
GetFileDiffAsync to GitService.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-05-19 11:33:00 +02:00
parent bc15c16e44
commit 6670771040
5 changed files with 98 additions and 29 deletions

View File

@@ -106,6 +106,15 @@ public sealed class GitService
return stdout.Trim(); return stdout.Trim();
} }
public async Task<string> GetFileDiffAsync(string worktreePath, string? baseCommit, string relativePath, CancellationToken ct = default)
{
string[] args = string.IsNullOrEmpty(baseCommit)
? ["diff", "--", relativePath]
: ["diff", $"{baseCommit}..HEAD", "--", relativePath];
var (_, stdout, _) = await RunGitAsync(worktreePath, args, ct);
return stdout;
}
public async Task WorktreeRemoveAsync(string repoDir, string worktreePath, bool force = false, CancellationToken ct = default) public async Task WorktreeRemoveAsync(string repoDir, string worktreePath, bool force = false, CancellationToken ct = default)
{ {
var args = new List<string> { "worktree", "remove" }; var args = new List<string> { "worktree", "remove" };

View File

@@ -10,6 +10,7 @@ public sealed partial class WorktreeNodeViewModel : ViewModelBase
public required string Name { get; init; } public required string Name { get; init; }
public string? Status { get; init; } public string? Status { get; init; }
public bool IsDirectory { get; init; } public bool IsDirectory { get; init; }
public string RelativePath { get; init; } = "";
public ObservableCollection<WorktreeNodeViewModel> Children { get; } = new(); public ObservableCollection<WorktreeNodeViewModel> Children { get; } = new();
} }
@@ -21,6 +22,8 @@ public sealed partial class WorktreeModalViewModel : ViewModelBase
[ObservableProperty] private string _worktreePath = ""; [ObservableProperty] private string _worktreePath = "";
[ObservableProperty] private string? _baseCommit; [ObservableProperty] private string? _baseCommit;
[ObservableProperty] private WorktreeNodeViewModel? _selectedNode;
[ObservableProperty] private string _selectedFileDiff = "";
// Set by the view (same pattern as DiffModalViewModel.CloseAction) // Set by the view (same pattern as DiffModalViewModel.CloseAction)
public Action? CloseAction { get; set; } public Action? CloseAction { get; set; }
@@ -30,6 +33,29 @@ public sealed partial class WorktreeModalViewModel : ViewModelBase
_git = git; _git = git;
} }
partial void OnSelectedNodeChanged(WorktreeNodeViewModel? value)
{
_ = LoadFileDiffAsync(value);
}
private async Task LoadFileDiffAsync(WorktreeNodeViewModel? node)
{
if (node is null || node.IsDirectory || string.IsNullOrEmpty(node.RelativePath))
{
SelectedFileDiff = "";
return;
}
try
{
SelectedFileDiff = await _git.GetFileDiffAsync(WorktreePath, BaseCommit, node.RelativePath);
}
catch
{
SelectedFileDiff = "";
}
}
[RelayCommand] [RelayCommand]
private void Close() => CloseAction?.Invoke(); private void Close() => CloseAction?.Invoke();
@@ -97,7 +123,8 @@ public sealed partial class WorktreeModalViewModel : ViewModelBase
{ {
Name = segments[^1], Name = segments[^1],
Status = status, Status = status,
IsDirectory = false IsDirectory = false,
RelativePath = path
}; };
if (parent == null) Root.Add(leaf); if (parent == null) Root.Add(leaf);
else parent.Children.Add(leaf); else parent.Children.Add(leaf);

View File

@@ -4,12 +4,13 @@
x:Class="ClaudeDo.Ui.Views.Modals.WorktreeModalView" x:Class="ClaudeDo.Ui.Views.Modals.WorktreeModalView"
x:DataType="vm:WorktreeModalViewModel" x:DataType="vm:WorktreeModalViewModel"
Title="Worktree" Title="Worktree"
Width="640" Height="720" Width="1100" Height="720"
MinWidth="640" MinHeight="400"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
SystemDecorations="None" SystemDecorations="None"
ExtendClientAreaToDecorationsHint="True" ExtendClientAreaToDecorationsHint="True"
Background="Transparent" Background="Transparent"
CanResize="False" CanResize="True"
TransparencyLevelHint="AcrylicBlur"> TransparencyLevelHint="AcrylicBlur">
<Window.KeyBindings> <Window.KeyBindings>
@@ -39,27 +40,51 @@
TextTrimming="CharacterEllipsis"/> TextTrimming="CharacterEllipsis"/>
</Border> </Border>
<!-- File tree --> <!-- Split: file tree | splitter | diff pane -->
<TreeView DockPanel.Dock="Top" ItemsSource="{Binding Root}" <Grid ColumnDefinitions="260,4,*">
Background="Transparent" Margin="8,0,8,8">
<TreeView.ItemTemplate> <!-- Left: file tree -->
<TreeDataTemplate DataType="vm:WorktreeNodeViewModel" <TreeView x:Name="FileTree"
ItemsSource="{Binding Children}"> Grid.Column="0"
<StackPanel Orientation="Horizontal" Spacing="8"> ItemsSource="{Binding Root}"
<TextBlock Text="{Binding Name}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}"
FontFamily="{DynamicResource MonoFont}" FontSize="12" Background="Transparent"
Foreground="{DynamicResource TextBrush}"/> Margin="8,0,4,8">
<Border Tag="{Binding Status}" CornerRadius="3" Padding="4,0" <TreeView.ItemTemplate>
VerticalAlignment="Center" <TreeDataTemplate DataType="vm:WorktreeNodeViewModel"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.IsNotNull}}"> ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Status}" <StackPanel Orientation="Horizontal" Spacing="8">
FontFamily="{DynamicResource MonoFont}" FontSize="10" <TextBlock Text="{Binding Name}"
FontFamily="{DynamicResource MonoFont}" FontSize="12"
Foreground="{DynamicResource TextBrush}"/> Foreground="{DynamicResource TextBrush}"/>
</Border> <Border Tag="{Binding Status}" CornerRadius="3" Padding="4,0"
</StackPanel> VerticalAlignment="Center"
</TreeDataTemplate> IsVisible="{Binding Status, Converter={x:Static ObjectConverters.IsNotNull}}">
</TreeView.ItemTemplate> <TextBlock Text="{Binding Status}"
</TreeView> FontFamily="{DynamicResource MonoFont}" FontSize="10"
Foreground="{DynamicResource TextBrush}"/>
</Border>
</StackPanel>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<!-- Splitter -->
<GridSplitter Grid.Column="1" ResizeDirection="Columns" Background="{DynamicResource LineBrush}"/>
<!-- Right: diff content -->
<ScrollViewer Grid.Column="2" Padding="8"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
Margin="4,0,8,8">
<SelectableTextBlock Text="{Binding SelectedFileDiff}"
FontFamily="{DynamicResource MonoFont}"
FontSize="11"
Foreground="{DynamicResource TextBrush}"
TextWrapping="NoWrap"/>
</ScrollViewer>
</Grid>
</DockPanel> </DockPanel>
</Border> </Border>

View File

@@ -21,6 +21,18 @@ public partial class WorktreeModalView : Window
base.OnDataContextChanged(e); base.OnDataContextChanged(e);
if (DataContext is WorktreeModalViewModel vm) if (DataContext is WorktreeModalViewModel vm)
vm.CloseAction = Close; vm.CloseAction = Close;
// Wire TreeView selection — SelectedItem TwoWay binding may not fire
// reliably in Avalonia 12 for TreeView; use SelectionChanged as backup.
var tree = this.FindControl<TreeView>("FileTree");
if (tree is not null)
tree.SelectionChanged += OnFileTreeSelectionChanged;
}
private void OnFileTreeSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (DataContext is WorktreeModalViewModel vm && sender is TreeView tree)
vm.SelectedNode = tree.SelectedItem as WorktreeNodeViewModel;
} }
protected override async void OnOpened(EventArgs e) protected override async void OnOpened(EventArgs e)

View File

@@ -8,7 +8,7 @@
Width="900" Height="560" MinWidth="640" MinHeight="360" Width="900" Height="560" MinWidth="640" MinHeight="360"
CanResize="True" CanResize="True"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
Background="{DynamicResource VoidBrush}" Background="{DynamicResource SurfaceBrush}"
SystemDecorations="None" SystemDecorations="None"
ExtendClientAreaToDecorationsHint="True"> ExtendClientAreaToDecorationsHint="True">
@@ -104,10 +104,7 @@
</Style> </Style>
</Window.Styles> </Window.Styles>
<Border Background="{DynamicResource SurfaceBrush}" <Grid RowDefinitions="36,Auto,*,52">
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1">
<Grid RowDefinitions="36,Auto,*,52">
<!-- Title bar --> <!-- Title bar -->
<Border Grid.Row="0" <Border Grid.Row="0"
@@ -208,5 +205,4 @@
</Border> </Border>
</Grid> </Grid>
</Border>
</Window> </Window>