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;
|
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)
|
public async Task<bool> HasChangesAsync(string worktreePath, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath, ["status", "--porcelain"], ct);
|
var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath, ["status", "--porcelain"], ct);
|
||||||
|
|||||||
@@ -569,6 +569,7 @@ public sealed record WorktreeOverviewDto(
|
|||||||
string ListName,
|
string ListName,
|
||||||
string Path,
|
string Path,
|
||||||
string BranchName,
|
string BranchName,
|
||||||
|
string BaseCommit,
|
||||||
WorktreeState State,
|
WorktreeState State,
|
||||||
string? DiffStat,
|
string? DiffStat,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public sealed partial class WorktreeModalViewModel : ViewModelBase
|
|||||||
public ObservableCollection<WorktreeNodeViewModel> Root { get; } = new();
|
public ObservableCollection<WorktreeNodeViewModel> Root { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty] private string _worktreePath = "";
|
[ObservableProperty] private string _worktreePath = "";
|
||||||
|
[ObservableProperty] private string? _baseCommit;
|
||||||
|
|
||||||
// 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; }
|
||||||
@@ -37,7 +38,13 @@ public sealed partial class WorktreeModalViewModel : ViewModelBase
|
|||||||
Root.Clear();
|
Root.Clear();
|
||||||
|
|
||||||
string stdout;
|
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; }
|
catch { return; }
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(stdout)) 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))
|
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)
|
||||||
var xy = line[..2];
|
{
|
||||||
// Pick staged char first, fall back to unstaged
|
// diff --name-status format: <status>\t<path>
|
||||||
var statusChar = xy[0] != ' ' ? xy[0] : xy[1];
|
var tab = line.IndexOf('\t');
|
||||||
var status = statusChar != ' ' ? statusChar.ToString() : null;
|
if (tab < 0) continue;
|
||||||
var path = line[3..].Trim().Replace('\\', '/');
|
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);
|
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
if (segments.Length == 0) continue;
|
if (segments.Length == 0) continue;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public sealed partial class WorktreeOverviewRowViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private string _listName = "";
|
[ObservableProperty] private string _listName = "";
|
||||||
[ObservableProperty] private string _path = "";
|
[ObservableProperty] private string _path = "";
|
||||||
[ObservableProperty] private string _branchName = "";
|
[ObservableProperty] private string _branchName = "";
|
||||||
|
[ObservableProperty] private string _baseCommit = "";
|
||||||
[ObservableProperty] private WorktreeState _state;
|
[ObservableProperty] private WorktreeState _state;
|
||||||
[ObservableProperty] private string? _diffStat;
|
[ObservableProperty] private string? _diffStat;
|
||||||
[ObservableProperty] private DateTime _createdAt;
|
[ObservableProperty] private DateTime _createdAt;
|
||||||
@@ -149,6 +150,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
|||||||
if (row is null) return;
|
if (row is null) return;
|
||||||
var diffVm = _diffVmFactory();
|
var diffVm = _diffVmFactory();
|
||||||
diffVm.WorktreePath = row.Path;
|
diffVm.WorktreePath = row.Path;
|
||||||
|
diffVm.BaseCommit = string.IsNullOrEmpty(row.BaseCommit) ? null : row.BaseCommit;
|
||||||
ShowDiffAction?.Invoke(diffVm);
|
ShowDiffAction?.Invoke(diffVm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +233,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
TaskId = d.TaskId, TaskTitle = d.TaskTitle, TaskStatus = d.TaskStatus,
|
TaskId = d.TaskId, TaskTitle = d.TaskTitle, TaskStatus = d.TaskStatus,
|
||||||
ListId = d.ListId, ListName = d.ListName,
|
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,
|
DiffStat = d.DiffStat, CreatedAt = d.CreatedAt, PathExistsOnDisk = d.PathExistsOnDisk,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
x:DataType="vm:WorktreesOverviewModalViewModel"
|
x:DataType="vm:WorktreesOverviewModalViewModel"
|
||||||
Title="{Binding Title}"
|
Title="{Binding Title}"
|
||||||
Width="900" Height="560" MinWidth="640" MinHeight="360"
|
Width="900" Height="560" MinWidth="640" MinHeight="360"
|
||||||
|
CanResize="True"
|
||||||
WindowStartupLocation="CenterOwner"
|
WindowStartupLocation="CenterOwner"
|
||||||
Background="{DynamicResource VoidBrush}"
|
Background="{DynamicResource VoidBrush}"
|
||||||
SystemDecorations="None"
|
SystemDecorations="None"
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
CommandParameter="{Binding}"/>
|
CommandParameter="{Binding}"/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</Border.ContextMenu>
|
</Border.ContextMenu>
|
||||||
<Grid ColumnDefinitions="*,200,90,80,80">
|
<Grid ColumnDefinitions="*,90,80,80">
|
||||||
<StackPanel Grid.Column="0" Orientation="Vertical" Spacing="2">
|
<StackPanel Grid.Column="0" Orientation="Vertical" Spacing="2">
|
||||||
<TextBlock Text="{Binding TaskTitle}" FontWeight="SemiBold"/>
|
<TextBlock Text="{Binding TaskTitle}" FontWeight="SemiBold"/>
|
||||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||||
@@ -65,18 +66,15 @@
|
|||||||
ToolTip.Tip="Directory missing on disk"/>
|
ToolTip.Tip="Directory missing on disk"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Grid.Column="1" Text="{Binding BranchName}"
|
<Border Grid.Column="1" CornerRadius="3" Padding="6,2" VerticalAlignment="Center"
|
||||||
FontFamily="{DynamicResource MonoFont}" FontSize="11"
|
|
||||||
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
|
|
||||||
<Border Grid.Column="2" CornerRadius="3" Padding="6,2" VerticalAlignment="Center"
|
|
||||||
Background="{Binding State, Converter={StaticResource WorktreeStateColor}}">
|
Background="{Binding State, Converter={StaticResource WorktreeStateColor}}">
|
||||||
<TextBlock Text="{Binding State}" FontSize="10" Foreground="White"
|
<TextBlock Text="{Binding State}" FontSize="10" Foreground="White"
|
||||||
HorizontalAlignment="Center"/>
|
HorizontalAlignment="Center"/>
|
||||||
</Border>
|
</Border>
|
||||||
<TextBlock Grid.Column="3" Text="{Binding DiffStat}" VerticalAlignment="Center"
|
<TextBlock Grid.Column="2" Text="{Binding DiffStat}" VerticalAlignment="Center"
|
||||||
FontFamily="{DynamicResource MonoFont}" FontSize="11"
|
FontFamily="{DynamicResource MonoFont}" FontSize="11"
|
||||||
Foreground="{DynamicResource TextDimBrush}"/>
|
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}"/>
|
FontSize="11" Foreground="{DynamicResource TextDimBrush}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -154,20 +152,17 @@
|
|||||||
<ScrollViewer Grid.Row="2" Padding="12,8">
|
<ScrollViewer Grid.Row="2" Padding="12,8">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<!-- Column headers -->
|
<!-- 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"
|
<TextBlock Grid.Column="0" Text="TASK"
|
||||||
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
|
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
|
||||||
Foreground="{DynamicResource TextFaintBrush}"/>
|
Foreground="{DynamicResource TextFaintBrush}"/>
|
||||||
<TextBlock Grid.Column="1" Text="BRANCH"
|
<TextBlock Grid.Column="1" Text="STATE"
|
||||||
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
|
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
|
||||||
Foreground="{DynamicResource TextFaintBrush}"/>
|
Foreground="{DynamicResource TextFaintBrush}"/>
|
||||||
<TextBlock Grid.Column="2" Text="STATE"
|
<TextBlock Grid.Column="2" Text="DIFF"
|
||||||
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
|
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
|
||||||
Foreground="{DynamicResource TextFaintBrush}"/>
|
Foreground="{DynamicResource TextFaintBrush}"/>
|
||||||
<TextBlock Grid.Column="3" Text="DIFF"
|
<TextBlock Grid.Column="3" Text="AGE"
|
||||||
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
|
|
||||||
Foreground="{DynamicResource TextFaintBrush}"/>
|
|
||||||
<TextBlock Grid.Column="4" Text="AGE"
|
|
||||||
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
|
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
|
||||||
Foreground="{DynamicResource TextFaintBrush}"/>
|
Foreground="{DynamicResource TextFaintBrush}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ public record WorktreeOverviewDto(
|
|||||||
string ListName,
|
string ListName,
|
||||||
string Path,
|
string Path,
|
||||||
string BranchName,
|
string BranchName,
|
||||||
|
string BaseCommit,
|
||||||
WorktreeState State,
|
WorktreeState State,
|
||||||
string? DiffStat,
|
string? DiffStat,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
@@ -252,7 +253,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
var rows = await _wtMaintenance.GetOverviewAsync(listId, Context.ConnectionAborted);
|
var rows = await _wtMaintenance.GetOverviewAsync(listId, Context.ConnectionAborted);
|
||||||
return rows.Select(r => new WorktreeOverviewDto(
|
return rows.Select(r => new WorktreeOverviewDto(
|
||||||
r.TaskId, r.TaskTitle, r.TaskStatus, r.ListId, r.ListName,
|
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)
|
public async Task<bool> SetWorktreeState(string taskId, WorktreeState newState)
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public sealed class WorktreeMaintenanceService
|
|||||||
select new
|
select new
|
||||||
{
|
{
|
||||||
w.TaskId, t.Title, t.Status, ListId = l.Id, ListName = l.Name,
|
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))
|
if (!string.IsNullOrEmpty(listId))
|
||||||
@@ -92,7 +92,7 @@ public sealed class WorktreeMaintenanceService
|
|||||||
|
|
||||||
return rows.Select(x => new WorktreeOverviewRow(
|
return rows.Select(x => new WorktreeOverviewRow(
|
||||||
x.TaskId, x.Title, x.Status, x.ListId, x.ListName,
|
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();
|
PathExistsOnDisk: !string.IsNullOrWhiteSpace(x.Path) && Directory.Exists(x.Path))).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public sealed record WorktreeOverviewRow(
|
|||||||
string ListName,
|
string ListName,
|
||||||
string Path,
|
string Path,
|
||||||
string BranchName,
|
string BranchName,
|
||||||
|
string BaseCommit,
|
||||||
WorktreeState State,
|
WorktreeState State,
|
||||||
string? DiffStat,
|
string? DiffStat,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
|
|||||||
Reference in New Issue
Block a user