feat(ui): move review feedback to the Output tab + review/worktree polish

- Feedback box + a new "Resume session" button move from the Git tab to the
  Output tab; the Git review block keeps Approve & Merge / Park / Cancel / Reset.
- Add a "Parked" chip for Idle tasks that still hold an Active worktree.
- Stop showing the "Session was Cancelled" band on cancel (failed-only now).
- Fix the Worktrees-overview state-chip contrast (dark text on the colour).
This commit is contained in:
Mika Kuns
2026-06-19 09:31:53 +02:00
parent 92767c646e
commit 3e4e4a03f7
8 changed files with 50 additions and 22 deletions

View File

@@ -442,7 +442,7 @@
"connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" }, "connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" },
"shell": { "restartingWorker": "Worker wird neu gestartet…" }, "shell": { "restartingWorker": "Worker wird neu gestartet…" },
"agentStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "review": "Prüfung", "children": "Wartet auf Verbesserungen", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" }, "agentStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "review": "Prüfung", "children": "Wartet auf Verbesserungen", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Verbesserungen", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" }, "taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Verbesserungen", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt" },
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" }, "planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" }, "taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}" }, "tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}" },

View File

@@ -442,7 +442,7 @@
"connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" }, "connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" },
"shell": { "restartingWorker": "Restarting worker…" }, "shell": { "restartingWorker": "Restarting worker…" },
"agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "review": "Review", "children": "Waiting for Improvements", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" }, "agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "review": "Review", "children": "Waiting for Improvements", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Improvements", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" }, "taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Improvements", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked" },
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" }, "planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" }, "taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}" }, "tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}" },

View File

@@ -229,6 +229,15 @@
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
</Style> </Style>
<!-- parked → slate-blue: an Idle task still holding its Active worktree -->
<Style Selector="Border.chip.parked">
<Setter Property="Background" Value="#22303A" />
<Setter Property="BorderBrush" Value="#3A5060" />
</Style>
<Style Selector="Border.chip.parked > TextBlock">
<Setter Property="Foreground" Value="#8FB9D6" />
</Style>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- BUTTONS --> <!-- BUTTONS -->
<!-- ============================================================ --> <!-- ============================================================ -->

View File

@@ -166,10 +166,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
public string DiffAddText => $"+{DiffAdditions}"; public string DiffAddText => $"+{DiffAdditions}";
public string DiffDelText => $"-{DiffDeletions}"; public string DiffDelText => $"-{DiffDeletions}";
public bool ShowRoadblock => IsFailed || IsCancelled; public bool ShowRoadblock => IsFailed;
public string RoadblockMessage => public string RoadblockMessage =>
IsFailed ? "The session ended with an error." : IsFailed ? "The session ended with an error." : "";
IsCancelled ? "The session was cancelled." : "";
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowSessionOutcome))] [NotifyPropertyChangedFor(nameof(ShowSessionOutcome))]

View File

@@ -18,6 +18,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
[ObservableProperty] private PlanningPhase _planningPhase; [ObservableProperty] private PlanningPhase _planningPhase;
[ObservableProperty] private string? _branch; [ObservableProperty] private string? _branch;
[ObservableProperty] private string? _diffStat; [ObservableProperty] private string? _diffStat;
[ObservableProperty] private ClaudeDo.Data.Models.WorktreeState? _worktreeState;
[ObservableProperty] private DateTime? _scheduledFor; [ObservableProperty] private DateTime? _scheduledFor;
[ObservableProperty] private int _diffAdditions; [ObservableProperty] private int _diffAdditions;
[ObservableProperty] private int _diffDeletions; [ObservableProperty] private int _diffDeletions;
@@ -76,6 +77,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
public bool IsOverdue => ScheduledFor is { } d && d.Date < DateTime.Today && !Done; public bool IsOverdue => ScheduledFor is { } d && d.Date < DateTime.Today && !Done;
public bool IsRunning => Status == TaskStatus.Running; public bool IsRunning => Status == TaskStatus.Running;
public bool IsWaitingForReview => Status == TaskStatus.WaitingForReview; public bool IsWaitingForReview => Status == TaskStatus.WaitingForReview;
// Parked = set aside from review: Idle but still holding its Active worktree (vs a plain Idle task).
public bool IsParked => Status == TaskStatus.Idle && WorktreeState == ClaudeDo.Data.Models.WorktreeState.Active;
public bool IsQueued => Status == TaskStatus.Queued && string.IsNullOrEmpty(BlockedByTaskId); public bool IsQueued => Status == TaskStatus.Queued && string.IsNullOrEmpty(BlockedByTaskId);
public bool IsWaiting => Status == TaskStatus.Queued && !string.IsNullOrEmpty(BlockedByTaskId); public bool IsWaiting => Status == TaskStatus.Queued && !string.IsNullOrEmpty(BlockedByTaskId);
public bool CanRemoveFromQueue => IsQueued || HasQueuedSubtasks; public bool CanRemoveFromQueue => IsQueued || HasQueuedSubtasks;
@@ -105,7 +108,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
public string DiffDeletionsText => $"{DiffDeletions}"; public string DiffDeletionsText => $"{DiffDeletions}";
public string StepsText => Loc.T("vm.taskRow.stepsText", StepsCompleted, StepsCount); public string StepsText => Loc.T("vm.taskRow.stepsText", StepsCompleted, StepsCount);
public string StatusLabel => Status switch public string StatusLabel => IsParked ? Loc.T("vm.taskStatus.parked") : Status switch
{ {
TaskStatus.Idle => Loc.T("vm.taskStatus.idle"), TaskStatus.Idle => Loc.T("vm.taskStatus.idle"),
TaskStatus.Queued => Loc.T("vm.taskStatus.queued"), TaskStatus.Queued => Loc.T("vm.taskStatus.queued"),
@@ -136,6 +139,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
OnPropertyChanged(nameof(StatusLabel)); OnPropertyChanged(nameof(StatusLabel));
OnPropertyChanged(nameof(IsRunning)); OnPropertyChanged(nameof(IsRunning));
OnPropertyChanged(nameof(IsWaitingForReview)); OnPropertyChanged(nameof(IsWaitingForReview));
OnPropertyChanged(nameof(IsParked));
OnPropertyChanged(nameof(IsQueued)); OnPropertyChanged(nameof(IsQueued));
OnPropertyChanged(nameof(IsWaiting)); OnPropertyChanged(nameof(IsWaiting));
OnPropertyChanged(nameof(IsDraft)); OnPropertyChanged(nameof(IsDraft));
@@ -210,6 +214,11 @@ public sealed partial class TaskRowViewModel : ViewModelBase
} }
partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch)); partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch));
partial void OnWorktreeStateChanged(ClaudeDo.Data.Models.WorktreeState? value)
{
OnPropertyChanged(nameof(IsParked));
OnPropertyChanged(nameof(StatusLabel));
}
partial void OnDoneChanged(bool value) partial void OnDoneChanged(bool value)
{ {
OnPropertyChanged(nameof(IsOverdue)); OnPropertyChanged(nameof(IsOverdue));
@@ -252,6 +261,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
PlanningPhase = t.PlanningPhase; PlanningPhase = t.PlanningPhase;
Branch = t.Worktree?.BranchName; Branch = t.Worktree?.BranchName;
DiffStat = t.Worktree?.DiffStat; DiffStat = t.Worktree?.DiffStat;
WorktreeState = t.Worktree?.State;
ScheduledFor = t.ScheduledFor; ScheduledFor = t.ScheduledFor;
DiffAdditions = add; DiffAdditions = add;
DiffDeletions = del; DiffDeletions = del;

View File

@@ -212,6 +212,28 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- Review footer: feedback + Resume session, shown while awaiting review.
Lives here (with the live log) rather than the Git tab. -->
<Border DockPanel.Dock="Bottom"
IsVisible="{Binding IsWaitingForReview}"
Margin="12,6,12,2">
<StackPanel Spacing="8">
<TextBox Name="ReviewInput"
KeyDown="OnReviewInputKeyDown"
Text="{Binding ReviewFeedback, Mode=TwoWay}"
AcceptsReturn="True"
TextWrapping="Wrap"
MaxHeight="120"
PlaceholderText="Feedback for a re-run…"
FontFamily="{StaticResource MonoFont}"
FontSize="{StaticResource FontSizeMono}" />
<Button Classes="btn" Content="Resume session"
HorizontalAlignment="Left"
ToolTip.Tip="{loc:Tr session.reviewContinueTip}"
Command="{Binding RejectReviewCommand}" />
</StackPanel>
</Border>
<ScrollViewer Name="LogScroll" <ScrollViewer Name="LogScroll"
VerticalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible"
AllowAutoHide="False" AllowAutoHide="False"
@@ -291,28 +313,15 @@
</WrapPanel> </WrapPanel>
</StackPanel> </StackPanel>
<!-- Review decision — feedback + the four review verbs. Always present while <!-- Review decision — the merge verbs. Feedback + Resume session moved to the
awaiting review, even for sandbox runs with no worktree to merge. --> Output tab. Present while awaiting review, even for sandbox runs. -->
<StackPanel Spacing="10" IsVisible="{Binding IsWaitingForReview}"> <StackPanel Spacing="10" IsVisible="{Binding IsWaitingForReview}">
<Border Height="1" Background="{DynamicResource LineBrush}" <Border Height="1" Background="{DynamicResource LineBrush}"
IsVisible="{Binding Merge.ShowMergeSection}" /> IsVisible="{Binding Merge.ShowMergeSection}" />
<TextBox Name="ReviewInput"
KeyDown="OnReviewInputKeyDown"
Text="{Binding ReviewFeedback, Mode=TwoWay}"
AcceptsReturn="True"
TextWrapping="Wrap"
MaxHeight="120"
PlaceholderText="Feedback for a re-run…"
FontFamily="{StaticResource MonoFont}"
FontSize="{StaticResource FontSizeMono}" />
<WrapPanel Orientation="Horizontal"> <WrapPanel Orientation="Horizontal">
<Button Classes="btn accent" Content="Approve &amp; Merge" Margin="0,0,8,8" <Button Classes="btn accent" Content="Approve &amp; Merge" Margin="0,0,8,8"
Command="{Binding ApproveReviewCommand}" /> Command="{Binding ApproveReviewCommand}" />
<Button Classes="btn" Content="Send back" Margin="0,0,8,8"
ToolTip.Tip="{loc:Tr session.reviewContinueTip}"
Command="{Binding RejectReviewCommand}" />
<Button Classes="btn" Content="Park" Margin="0,0,8,8" <Button Classes="btn" Content="Park" Margin="0,0,8,8"
ToolTip.Tip="Set aside — back to Idle, keeps the worktree" ToolTip.Tip="Set aside — back to Idle, keeps the worktree"
Command="{Binding ParkReviewCommand}" /> Command="{Binding ParkReviewCommand}" />

View File

@@ -152,6 +152,7 @@
<!-- Status chip --> <!-- Status chip -->
<Border Classes="chip" <Border Classes="chip"
Classes.parked="{Binding IsParked}"
Classes.running="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Running}" Classes.running="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Running}"
Classes.review="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=WaitingForReview}" Classes.review="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=WaitingForReview}"
Classes.children="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=WaitingForChildren}" Classes.children="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=WaitingForChildren}"

View File

@@ -81,7 +81,7 @@
IsVisible="{Binding HasOutcome}"/> IsVisible="{Binding HasOutcome}"/>
<Border Grid.Column="3" CornerRadius="3" Padding="6,2" VerticalAlignment="Center" <Border Grid.Column="3" CornerRadius="3" Padding="6,2" VerticalAlignment="Center"
Background="{Binding State, Converter={StaticResource WorktreeStateColor}}"> Background="{Binding State, Converter={StaticResource WorktreeStateColor}}">
<TextBlock Classes="meta" Text="{Binding State}" Foreground="{DynamicResource TextBrush}" <TextBlock Classes="meta" Text="{Binding State}" Foreground="{DynamicResource DeepBrush}"
HorizontalAlignment="Center"/> HorizontalAlignment="Center"/>
</Border> </Border>
<TextBlock Grid.Column="4" Classes="meta" Text="{Binding DiffStat}" VerticalAlignment="Center"/> <TextBlock Grid.Column="4" Classes="meta" Text="{Binding DiffStat}" VerticalAlignment="Center"/>