fix(ui): resizable modal, drop branch column, show committed diff

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-05-19 11:08:52 +02:00
parent ca71275fc4
commit bc15c16e44
8 changed files with 55 additions and 26 deletions

View File

@@ -35,6 +35,15 @@ public sealed class GitService
return stdout;
}
public async Task<string> 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<bool> HasChangesAsync(string worktreePath, CancellationToken ct = default)
{
var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath, ["status", "--porcelain"], ct);

View File

@@ -569,6 +569,7 @@ public sealed record WorktreeOverviewDto(
string ListName,
string Path,
string BranchName,
string BaseCommit,
WorktreeState State,
string? DiffStat,
DateTime CreatedAt,

View File

@@ -20,6 +20,7 @@ public sealed partial class WorktreeModalViewModel : ViewModelBase
public ObservableCollection<WorktreeNodeViewModel> 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: XY<space>path (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: <status>\t<path>
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: XY<space>path
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;

View File

@@ -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,
};
}

View File

@@ -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}"/>
</ContextMenu>
</Border.ContextMenu>
<Grid ColumnDefinitions="*,200,90,80,80">
<Grid ColumnDefinitions="*,90,80,80">
<StackPanel Grid.Column="0" Orientation="Vertical" Spacing="2">
<TextBlock Text="{Binding TaskTitle}" FontWeight="SemiBold"/>
<StackPanel Orientation="Horizontal" Spacing="4">
@@ -65,18 +66,15 @@
ToolTip.Tip="Directory missing on disk"/>
</StackPanel>
</StackPanel>
<TextBlock Grid.Column="1" Text="{Binding BranchName}"
FontFamily="{DynamicResource MonoFont}" FontSize="11"
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
<Border Grid.Column="2" CornerRadius="3" Padding="6,2" VerticalAlignment="Center"
<Border Grid.Column="1" CornerRadius="3" Padding="6,2" VerticalAlignment="Center"
Background="{Binding State, Converter={StaticResource WorktreeStateColor}}">
<TextBlock Text="{Binding State}" FontSize="10" Foreground="White"
HorizontalAlignment="Center"/>
</Border>
<TextBlock Grid.Column="3" Text="{Binding DiffStat}" VerticalAlignment="Center"
<TextBlock Grid.Column="2" Text="{Binding DiffStat}" VerticalAlignment="Center"
FontFamily="{DynamicResource MonoFont}" FontSize="11"
Foreground="{DynamicResource TextDimBrush}"/>
<TextBlock Grid.Column="4" Text="{Binding AgeText}" VerticalAlignment="Center"
<TextBlock Grid.Column="3" Text="{Binding AgeText}" VerticalAlignment="Center"
FontSize="11" Foreground="{DynamicResource TextDimBrush}"/>
</Grid>
</Border>
@@ -154,20 +152,17 @@
<ScrollViewer Grid.Row="2" Padding="12,8">
<StackPanel>
<!-- Column headers -->
<Grid ColumnDefinitions="*,200,90,80,80" Margin="12,0,12,4">
<Grid ColumnDefinitions="*,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"
<TextBlock Grid.Column="1" Text="STATE"
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
Foreground="{DynamicResource TextFaintBrush}"/>
<TextBlock Grid.Column="2" Text="STATE"
<TextBlock Grid.Column="2" Text="DIFF"
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"
<TextBlock Grid.Column="3" Text="AGE"
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
Foreground="{DynamicResource TextFaintBrush}"/>
</Grid>

View File

@@ -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<bool> SetWorktreeState(string taskId, WorktreeState newState)

View File

@@ -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();
}

View File

@@ -11,6 +11,7 @@ public sealed record WorktreeOverviewRow(
string ListName,
string Path,
string BranchName,
string BaseCommit,
WorktreeState State,
string? DiffStat,
DateTime CreatedAt,