fix(ui): Worktrees-Uebersicht zeigt Task-Nummern, DIFF-Spalte trimmt statt abzuschneiden

Befund F7 aus der Sichtpruefung 2026-09-16. TaskNumber war bereits im
WorktreeMaintenanceService-Join und in WorktreeOverviewRow vorhanden, fehlte
aber im Wire-DTO und in der UI-Kette; jetzt bis zur Row-/Conflict-Anzeige
durchgereicht (#<number> dimmed vor dem Titel, ausgeblendet bei 0). DIFF-Spalte
bekommt CharacterEllipsis-Trimming + Tooltip statt hartem Cut, Spalte von 80
auf 110px verbreitert (Header und Row synchron).
This commit is contained in:
Mika Kuns
2026-09-16 15:43:38 +02:00
committed by mika kuns
parent 56161e4e9b
commit 1325ab9438
5 changed files with 50 additions and 9 deletions
+2 -1
View File
@@ -72,7 +72,8 @@ public record WorktreeOverviewDto(
WorktreeState State, WorktreeState State,
string? DiffStat, string? DiffStat,
DateTime CreatedAt, DateTime CreatedAt,
bool PathExistsOnDisk); bool PathExistsOnDisk,
int TaskNumber = 0);
public record ForceRemoveResultDto(bool Removed, string? Reason); public record ForceRemoveResultDto(bool Removed, string? Reason);
@@ -16,6 +16,7 @@ public enum BatchMergeOutcome { None, Merging, Merged, Conflict, Blocked, Verify
public sealed partial class WorktreeOverviewRowViewModel : ViewModelBase public sealed partial class WorktreeOverviewRowViewModel : ViewModelBase
{ {
[ObservableProperty] private string _taskId = ""; [ObservableProperty] private string _taskId = "";
[ObservableProperty] private int _taskNumber;
[ObservableProperty] private string _taskTitle = ""; [ObservableProperty] private string _taskTitle = "";
[ObservableProperty][NotifyPropertyChangedFor(nameof(IsRunning))] private TaskStatus _taskStatus; [ObservableProperty][NotifyPropertyChangedFor(nameof(IsRunning))] private TaskStatus _taskStatus;
[ObservableProperty] private string _listId = ""; [ObservableProperty] private string _listId = "";
@@ -49,6 +50,9 @@ public sealed partial class WorktreeOverviewRowViewModel : ViewModelBase
public bool IsKept => State == WorktreeState.Kept; public bool IsKept => State == WorktreeState.Kept;
public bool IsRunning => TaskStatus == TaskStatus.Running; public bool IsRunning => TaskStatus == TaskStatus.Running;
// 0 = row without an allocated number (test seeds); a bare "#0" would be meaningless.
public bool ShowNumberBadge => TaskNumber > 0;
private static string FormatAge(TimeSpan ts) private static string FormatAge(TimeSpan ts)
{ {
if (ts.TotalDays >= 1) return $"{(int)ts.TotalDays}d ago"; if (ts.TotalDays >= 1) return $"{(int)ts.TotalDays}d ago";
@@ -366,7 +370,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
private static WorktreeOverviewRowViewModel Map(WorktreeOverviewDto d) => new() private static WorktreeOverviewRowViewModel Map(WorktreeOverviewDto d) => new()
{ {
TaskId = d.TaskId, TaskTitle = d.TaskTitle, TaskStatus = d.TaskStatus, TaskId = d.TaskId, TaskNumber = d.TaskNumber, TaskTitle = d.TaskTitle, TaskStatus = d.TaskStatus,
ListId = d.ListId, ListName = d.ListName, ListId = d.ListId, ListName = d.ListName,
Path = d.Path, BranchName = d.BranchName, BaseCommit = d.BaseCommit, 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,
@@ -58,13 +58,18 @@
CommandParameter="{Binding}"/> CommandParameter="{Binding}"/>
</ContextMenu> </ContextMenu>
</Border.ContextMenu> </Border.ContextMenu>
<Grid ColumnDefinitions="Auto,*,90,90,80,80"> <Grid ColumnDefinitions="Auto,*,90,90,110,80">
<CheckBox Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0" <CheckBox Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0"
IsChecked="{Binding IsChecked, Mode=TwoWay}" IsChecked="{Binding IsChecked, Mode=TwoWay}"
IsEnabled="{Binding IsActive}" IsEnabled="{Binding IsActive}"
IsVisible="{Binding IsActive}"/> IsVisible="{Binding IsActive}"/>
<StackPanel Grid.Column="1" Orientation="Vertical" Spacing="2"> <StackPanel Grid.Column="1" Orientation="Vertical" Spacing="2">
<TextBlock Classes="title" Text="{Binding TaskTitle}"/> <Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" Classes="meta" Text="{Binding TaskNumber, StringFormat='#{0}'}"
IsVisible="{Binding ShowNumberBadge}"
VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Grid.Column="1" Classes="title" Text="{Binding TaskTitle}"/>
</Grid>
<StackPanel Orientation="Horizontal" Spacing="4"> <StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Classes="meta" Text="{Binding TaskStatus}"/> <TextBlock Classes="meta" Text="{Binding TaskStatus}"/>
<TextBlock Classes="meta" Text="•" <TextBlock Classes="meta" Text="•"
@@ -85,7 +90,8 @@
HorizontalAlignment="Left" VerticalAlignment="Center"> HorizontalAlignment="Left" VerticalAlignment="Center">
<TextBlock Text="{Binding State}"/> <TextBlock Text="{Binding State}"/>
</Border> </Border>
<TextBlock Grid.Column="4" Classes="meta" Text="{Binding DiffStat}" VerticalAlignment="Center"/> <TextBlock Grid.Column="4" Classes="meta" Text="{Binding DiffStat}" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" ToolTip.Tip="{Binding DiffStat}"/>
<TextBlock Grid.Column="5" Classes="meta" Text="{Binding AgeText}" VerticalAlignment="Center"/> <TextBlock Grid.Column="5" Classes="meta" Text="{Binding AgeText}" VerticalAlignment="Center"/>
</Grid> </Grid>
</Border> </Border>
@@ -146,8 +152,11 @@
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:WorktreeOverviewRowViewModel"> <DataTemplate x:DataType="vm:WorktreeOverviewRowViewModel">
<Grid ColumnDefinitions="*,Auto" Margin="0,2"> <Grid ColumnDefinitions="*,Auto" Margin="0,2">
<TextBlock Grid.Column="0" Classes="meta" VerticalAlignment="Center" <StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
Text="{Binding TaskTitle}"/> <TextBlock Classes="meta" Text="{Binding TaskNumber, StringFormat='#{0}'}"
IsVisible="{Binding ShowNumberBadge}"/>
<TextBlock Classes="meta" Text="{Binding TaskTitle}"/>
</StackPanel>
<Button Grid.Column="1" Classes="btn" <Button Grid.Column="1" Classes="btn"
Content="{loc:Tr modals.worktreesOverview.resolve}" Content="{loc:Tr modals.worktreesOverview.resolve}"
Command="{Binding $parent[Window].((vm:WorktreesOverviewModalViewModel)DataContext).ResolveConflictCommand}" Command="{Binding $parent[Window].((vm:WorktreesOverviewModalViewModel)DataContext).ResolveConflictCommand}"
@@ -159,7 +168,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- Column headers --> <!-- Column headers -->
<Grid ColumnDefinitions="Auto,*,90,90,80,80" Margin="12,0,12,4"> <Grid ColumnDefinitions="Auto,*,90,90,110,80" Margin="12,0,12,4">
<TextBlock Grid.Column="1" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnTask}"/> <TextBlock Grid.Column="1" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnTask}"/>
<TextBlock Grid.Column="2" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnOutcome}"/> <TextBlock Grid.Column="2" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnOutcome}"/>
<TextBlock Grid.Column="3" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnState}"/> <TextBlock Grid.Column="3" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnState}"/>
+2 -1
View File
@@ -449,7 +449,8 @@ 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.BaseCommit, r.State, r.DiffStat, r.CreatedAt, r.PathExistsOnDisk)).ToList(); r.Path, r.BranchName, r.BaseCommit, r.State, r.DiffStat, r.CreatedAt, r.PathExistsOnDisk,
r.TaskNumber)).ToList();
} }
public async Task<bool> SetWorktreeState(string taskId, WorktreeState newState) public async Task<bool> SetWorktreeState(string taskId, WorktreeState newState)
@@ -114,6 +114,32 @@ public class WorktreesOverviewReconcileTickTests
Assert.Equal(1, vm.SelectedCount); Assert.Equal(1, vm.SelectedCount);
} }
[Fact]
public async Task Map_carries_the_task_number_and_hides_the_badge_when_zero()
{
var worker = new FakeWorker
{
Worktrees =
{
Wt("a") with { TaskNumber = 42 },
Wt("b") with { TaskNumber = 0 },
},
};
var vm = new WorktreesOverviewModalViewModel(
worker, () => throw new InvalidOperationException("no diff vm in this test"),
new ClaudeDo.Ui.Services.MergeCoordinator());
vm.Configure("L1", "Work");
await vm.LoadAsync();
var withNumber = vm.Rows.Single(r => r.TaskId == "a");
var withoutNumber = vm.Rows.Single(r => r.TaskId == "b");
Assert.Equal(42, withNumber.TaskNumber);
Assert.True(withNumber.ShowNumberBadge);
Assert.Equal(0, withoutNumber.TaskNumber);
Assert.False(withoutNumber.ShowNumberBadge);
}
[Theory] [Theory]
[InlineData("refresh")] [InlineData("refresh")]
[InlineData("cleanup")] [InlineData("cleanup")]