From 66707710406a20347d843a7ccbbd4024fbd91fe2 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Tue, 19 May 2026 11:33:00 +0200 Subject: [PATCH] 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 --- src/ClaudeDo.Data/Git/GitService.cs | 9 +++ .../Modals/WorktreeModalViewModel.cs | 29 +++++++- .../Views/Modals/WorktreeModalView.axaml | 69 +++++++++++++------ .../Views/Modals/WorktreeModalView.axaml.cs | 12 ++++ .../Modals/WorktreesOverviewModalView.axaml | 8 +-- 5 files changed, 98 insertions(+), 29 deletions(-) diff --git a/src/ClaudeDo.Data/Git/GitService.cs b/src/ClaudeDo.Data/Git/GitService.cs index 673583f..29c34e6 100644 --- a/src/ClaudeDo.Data/Git/GitService.cs +++ b/src/ClaudeDo.Data/Git/GitService.cs @@ -106,6 +106,15 @@ public sealed class GitService return stdout.Trim(); } + public async Task 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) { var args = new List { "worktree", "remove" }; diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs index 19d6e90..d0d1492 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs @@ -10,6 +10,7 @@ public sealed partial class WorktreeNodeViewModel : ViewModelBase public required string Name { get; init; } public string? Status { get; init; } public bool IsDirectory { get; init; } + public string RelativePath { get; init; } = ""; public ObservableCollection Children { get; } = new(); } @@ -21,6 +22,8 @@ public sealed partial class WorktreeModalViewModel : ViewModelBase [ObservableProperty] private string _worktreePath = ""; [ObservableProperty] private string? _baseCommit; + [ObservableProperty] private WorktreeNodeViewModel? _selectedNode; + [ObservableProperty] private string _selectedFileDiff = ""; // Set by the view (same pattern as DiffModalViewModel.CloseAction) public Action? CloseAction { get; set; } @@ -30,6 +33,29 @@ public sealed partial class WorktreeModalViewModel : ViewModelBase _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] private void Close() => CloseAction?.Invoke(); @@ -97,7 +123,8 @@ public sealed partial class WorktreeModalViewModel : ViewModelBase { Name = segments[^1], Status = status, - IsDirectory = false + IsDirectory = false, + RelativePath = path }; if (parent == null) Root.Add(leaf); else parent.Children.Add(leaf); diff --git a/src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml index 444fcb4..0bbf09a 100644 --- a/src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml @@ -4,12 +4,13 @@ x:Class="ClaudeDo.Ui.Views.Modals.WorktreeModalView" x:DataType="vm:WorktreeModalViewModel" Title="Worktree" - Width="640" Height="720" + Width="1100" Height="720" + MinWidth="640" MinHeight="400" WindowStartupLocation="CenterOwner" SystemDecorations="None" ExtendClientAreaToDecorationsHint="True" Background="Transparent" - CanResize="False" + CanResize="True" TransparencyLevelHint="AcrylicBlur"> @@ -39,27 +40,51 @@ TextTrimming="CharacterEllipsis"/> - - - - - - - - + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + diff --git a/src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml.cs b/src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml.cs index bb650ea..da5aa2b 100644 --- a/src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml.cs @@ -21,6 +21,18 @@ public partial class WorktreeModalView : Window base.OnDataContextChanged(e); if (DataContext is WorktreeModalViewModel vm) 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("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) diff --git a/src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml index 53ec248..cbf552a 100644 --- a/src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml @@ -8,7 +8,7 @@ Width="900" Height="560" MinWidth="640" MinHeight="360" CanResize="True" WindowStartupLocation="CenterOwner" - Background="{DynamicResource VoidBrush}" + Background="{DynamicResource SurfaceBrush}" SystemDecorations="None" ExtendClientAreaToDecorationsHint="True"> @@ -104,10 +104,7 @@ - - + -