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:
@@ -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);
|
||||
|
||||
@@ -569,6 +569,7 @@ public sealed record WorktreeOverviewDto(
|
||||
string ListName,
|
||||
string Path,
|
||||
string BranchName,
|
||||
string BaseCommit,
|
||||
WorktreeState State,
|
||||
string? DiffStat,
|
||||
DateTime CreatedAt,
|
||||
|
||||
@@ -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)
|
||||
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];
|
||||
// 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('\\', '/');
|
||||
status = statusChar != ' ' ? statusChar.ToString() : null;
|
||||
path = line[3..].Trim().Replace('\\', '/');
|
||||
}
|
||||
|
||||
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (segments.Length == 0) continue;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ public sealed record WorktreeOverviewRow(
|
||||
string ListName,
|
||||
string Path,
|
||||
string BranchName,
|
||||
string BaseCommit,
|
||||
WorktreeState State,
|
||||
string? DiffStat,
|
||||
DateTime CreatedAt,
|
||||
|
||||
Reference in New Issue
Block a user