From bc15c16e446a1de7e596ff95e852be93925098c6 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Tue, 19 May 2026 11:08:52 +0200 Subject: [PATCH] fix(ui): resizable modal, drop branch column, show committed diff Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.Data/Git/GitService.cs | 9 +++++ src/ClaudeDo.Ui/Services/WorkerClient.cs | 1 + .../Modals/WorktreeModalViewModel.cs | 36 ++++++++++++++----- .../Modals/WorktreesOverviewModalViewModel.cs | 4 ++- .../Modals/WorktreesOverviewModalView.axaml | 23 +++++------- src/ClaudeDo.Worker/Hub/WorkerHub.cs | 3 +- .../Worktrees/WorktreeMaintenanceService.cs | 4 +-- .../Worktrees/WorktreeOverviewRow.cs | 1 + 8 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/ClaudeDo.Data/Git/GitService.cs b/src/ClaudeDo.Data/Git/GitService.cs index b3bd3ea..673583f 100644 --- a/src/ClaudeDo.Data/Git/GitService.cs +++ b/src/ClaudeDo.Data/Git/GitService.cs @@ -35,6 +35,15 @@ public sealed class GitService return stdout; } + public async Task GetCommittedFilesAsync(string worktreePath, string baseCommit, CancellationToken ct = default) + { + var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath, + ["diff", "--name-status", $"{baseCommit}..HEAD"], ct); + if (exitCode != 0) + throw new InvalidOperationException($"git diff --name-status failed (exit {exitCode}): {stderr}"); + return stdout; + } + public async Task HasChangesAsync(string worktreePath, CancellationToken ct = default) { var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath, ["status", "--porcelain"], ct); diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index fd7d599..120dd53 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -569,6 +569,7 @@ public sealed record WorktreeOverviewDto( string ListName, string Path, string BranchName, + string BaseCommit, WorktreeState State, string? DiffStat, DateTime CreatedAt, diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs index 103fdab..19d6e90 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs @@ -20,6 +20,7 @@ public sealed partial class WorktreeModalViewModel : ViewModelBase public ObservableCollection Root { get; } = new(); [ObservableProperty] private string _worktreePath = ""; + [ObservableProperty] private string? _baseCommit; // Set by the view (same pattern as DiffModalViewModel.CloseAction) public Action? CloseAction { get; set; } @@ -37,7 +38,13 @@ public sealed partial class WorktreeModalViewModel : ViewModelBase Root.Clear(); string stdout; - try { stdout = await _git.GetStatusPorcelainAsync(WorktreePath, ct); } + bool committedMode = !string.IsNullOrEmpty(BaseCommit); + try + { + stdout = committedMode + ? await _git.GetCommittedFilesAsync(WorktreePath, BaseCommit!, ct) + : await _git.GetStatusPorcelainAsync(WorktreePath, ct); + } catch { return; } if (string.IsNullOrWhiteSpace(stdout)) return; @@ -46,14 +53,27 @@ public sealed partial class WorktreeModalViewModel : ViewModelBase foreach (var line in stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries)) { - if (line.Length < 4) continue; + string? path; + string? status; - // porcelain format: XYpath (XY = two-char status) - var xy = line[..2]; - // Pick staged char first, fall back to unstaged - var statusChar = xy[0] != ' ' ? xy[0] : xy[1]; - var status = statusChar != ' ' ? statusChar.ToString() : null; - var path = line[3..].Trim().Replace('\\', '/'); + if (committedMode) + { + // diff --name-status format: \t + var tab = line.IndexOf('\t'); + if (tab < 0) continue; + var statusChar = line[0]; + status = statusChar != ' ' ? statusChar.ToString() : null; + path = line[(tab + 1)..].Trim().Replace('\\', '/'); + } + else + { + // porcelain format: XYpath + if (line.Length < 4) continue; + var xy = line[..2]; + var statusChar = xy[0] != ' ' ? xy[0] : xy[1]; + status = statusChar != ' ' ? statusChar.ToString() : null; + path = line[3..].Trim().Replace('\\', '/'); + } var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); if (segments.Length == 0) continue; diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs index 4554ed4..8717257 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs @@ -20,6 +20,7 @@ public sealed partial class WorktreeOverviewRowViewModel : ViewModelBase [ObservableProperty] private string _listName = ""; [ObservableProperty] private string _path = ""; [ObservableProperty] private string _branchName = ""; + [ObservableProperty] private string _baseCommit = ""; [ObservableProperty] private WorktreeState _state; [ObservableProperty] private string? _diffStat; [ObservableProperty] private DateTime _createdAt; @@ -149,6 +150,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase if (row is null) return; var diffVm = _diffVmFactory(); diffVm.WorktreePath = row.Path; + diffVm.BaseCommit = string.IsNullOrEmpty(row.BaseCommit) ? null : row.BaseCommit; ShowDiffAction?.Invoke(diffVm); } @@ -231,7 +233,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase { TaskId = d.TaskId, TaskTitle = d.TaskTitle, TaskStatus = d.TaskStatus, ListId = d.ListId, ListName = d.ListName, - Path = d.Path, BranchName = d.BranchName, State = d.State, + Path = d.Path, BranchName = d.BranchName, BaseCommit = d.BaseCommit, State = d.State, DiffStat = d.DiffStat, CreatedAt = d.CreatedAt, PathExistsOnDisk = d.PathExistsOnDisk, }; } diff --git a/src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml index 7352ac8..53ec248 100644 --- a/src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml @@ -6,6 +6,7 @@ x:DataType="vm:WorktreesOverviewModalViewModel" Title="{Binding Title}" Width="900" Height="560" MinWidth="640" MinHeight="360" + CanResize="True" WindowStartupLocation="CenterOwner" Background="{DynamicResource VoidBrush}" SystemDecorations="None" @@ -52,7 +53,7 @@ CommandParameter="{Binding}"/> - + @@ -65,18 +66,15 @@ ToolTip.Tip="Directory missing on disk"/> - - - - @@ -154,20 +152,17 @@ - + - - - - diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs index ef97412..9910e51 100644 --- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs +++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs @@ -38,6 +38,7 @@ public record WorktreeOverviewDto( string ListName, string Path, string BranchName, + string BaseCommit, WorktreeState State, string? DiffStat, DateTime CreatedAt, @@ -252,7 +253,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub var rows = await _wtMaintenance.GetOverviewAsync(listId, Context.ConnectionAborted); return rows.Select(r => new WorktreeOverviewDto( r.TaskId, r.TaskTitle, r.TaskStatus, r.ListId, r.ListName, - r.Path, r.BranchName, r.State, r.DiffStat, r.CreatedAt, r.PathExistsOnDisk)).ToList(); + r.Path, r.BranchName, r.BaseCommit, r.State, r.DiffStat, r.CreatedAt, r.PathExistsOnDisk)).ToList(); } public async Task SetWorktreeState(string taskId, WorktreeState newState) diff --git a/src/ClaudeDo.Worker/Worktrees/WorktreeMaintenanceService.cs b/src/ClaudeDo.Worker/Worktrees/WorktreeMaintenanceService.cs index 71e1124..52afe83 100644 --- a/src/ClaudeDo.Worker/Worktrees/WorktreeMaintenanceService.cs +++ b/src/ClaudeDo.Worker/Worktrees/WorktreeMaintenanceService.cs @@ -82,7 +82,7 @@ public sealed class WorktreeMaintenanceService select new { w.TaskId, t.Title, t.Status, ListId = l.Id, ListName = l.Name, - w.Path, w.BranchName, w.State, w.DiffStat, w.CreatedAt, + w.Path, w.BranchName, w.BaseCommit, w.State, w.DiffStat, w.CreatedAt, }; if (!string.IsNullOrEmpty(listId)) @@ -92,7 +92,7 @@ public sealed class WorktreeMaintenanceService return rows.Select(x => new WorktreeOverviewRow( x.TaskId, x.Title, x.Status, x.ListId, x.ListName, - x.Path, x.BranchName, x.State, x.DiffStat, x.CreatedAt, + x.Path, x.BranchName, x.BaseCommit ?? "", x.State, x.DiffStat, x.CreatedAt, PathExistsOnDisk: !string.IsNullOrWhiteSpace(x.Path) && Directory.Exists(x.Path))).ToList(); } diff --git a/src/ClaudeDo.Worker/Worktrees/WorktreeOverviewRow.cs b/src/ClaudeDo.Worker/Worktrees/WorktreeOverviewRow.cs index da46ca6..655f3d7 100644 --- a/src/ClaudeDo.Worker/Worktrees/WorktreeOverviewRow.cs +++ b/src/ClaudeDo.Worker/Worktrees/WorktreeOverviewRow.cs @@ -11,6 +11,7 @@ public sealed record WorktreeOverviewRow( string ListName, string Path, string BranchName, + string BaseCommit, WorktreeState State, string? DiffStat, DateTime CreatedAt,