feat(ui): host review actions in the details panel; show review state and diff meter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -384,7 +384,7 @@
|
|||||||
"vm": {
|
"vm": {
|
||||||
"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", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
|
"agentStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "review": "Prüfung", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
|
||||||
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
|
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
|
||||||
"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" },
|
||||||
|
|||||||
@@ -384,7 +384,7 @@
|
|||||||
"vm": {
|
"vm": {
|
||||||
"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", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
|
"agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "review": "Review", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
|
||||||
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
|
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
|
||||||
"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" },
|
||||||
|
|||||||
@@ -110,12 +110,13 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
|
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
|
||||||
private string _agentState = "idle";
|
private string _agentState = "idle";
|
||||||
public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
|
public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
|
||||||
public bool IsIdle => AgentState == "idle";
|
public bool IsIdle => AgentState == "idle";
|
||||||
public bool IsQueued => AgentState == "queued";
|
public bool IsQueued => AgentState == "queued";
|
||||||
public bool IsRunning => AgentState == "running";
|
public bool IsRunning => AgentState == "running";
|
||||||
public bool IsDone => AgentState == "done";
|
public bool IsWaitingForReview => AgentState == "review";
|
||||||
public bool IsFailed => AgentState == "failed";
|
public bool IsDone => AgentState == "done";
|
||||||
public bool IsCancelled => AgentState == "cancelled";
|
public bool IsFailed => AgentState == "failed";
|
||||||
|
public bool IsCancelled => AgentState == "cancelled";
|
||||||
|
|
||||||
// Recovery actions: Continue (resume session) for Failed/Cancelled.
|
// Recovery actions: Continue (resume session) for Failed/Cancelled.
|
||||||
public bool ShowContinue => IsFailed || IsCancelled;
|
public bool ShowContinue => IsFailed || IsCancelled;
|
||||||
@@ -132,6 +133,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
OnPropertyChanged(nameof(IsIdle));
|
OnPropertyChanged(nameof(IsIdle));
|
||||||
OnPropertyChanged(nameof(IsQueued));
|
OnPropertyChanged(nameof(IsQueued));
|
||||||
OnPropertyChanged(nameof(IsRunning));
|
OnPropertyChanged(nameof(IsRunning));
|
||||||
|
OnPropertyChanged(nameof(IsWaitingForReview));
|
||||||
OnPropertyChanged(nameof(IsDone));
|
OnPropertyChanged(nameof(IsDone));
|
||||||
OnPropertyChanged(nameof(IsFailed));
|
OnPropertyChanged(nameof(IsFailed));
|
||||||
OnPropertyChanged(nameof(IsCancelled));
|
OnPropertyChanged(nameof(IsCancelled));
|
||||||
@@ -279,7 +281,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
ClaudeDo.Data.Models.TaskStatus.Queued => "queued",
|
ClaudeDo.Data.Models.TaskStatus.Queued => "queued",
|
||||||
ClaudeDo.Data.Models.TaskStatus.Running => "running",
|
ClaudeDo.Data.Models.TaskStatus.Running => "running",
|
||||||
ClaudeDo.Data.Models.TaskStatus.WaitingForReview => "running",
|
ClaudeDo.Data.Models.TaskStatus.WaitingForReview => "review",
|
||||||
ClaudeDo.Data.Models.TaskStatus.Done => "done",
|
ClaudeDo.Data.Models.TaskStatus.Done => "done",
|
||||||
ClaudeDo.Data.Models.TaskStatus.Failed => "failed",
|
ClaudeDo.Data.Models.TaskStatus.Failed => "failed",
|
||||||
ClaudeDo.Data.Models.TaskStatus.Cancelled => "cancelled",
|
ClaudeDo.Data.Models.TaskStatus.Cancelled => "cancelled",
|
||||||
@@ -291,7 +293,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
"done" => "done",
|
"done" => "done",
|
||||||
"failed" => "failed",
|
"failed" => "failed",
|
||||||
"cancelled" => "cancelled",
|
"cancelled" => "cancelled",
|
||||||
"waiting_for_review" => "running",
|
"waiting_for_review" => "review",
|
||||||
_ => status.ToLowerInvariant(),
|
_ => status.ToLowerInvariant(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -880,6 +882,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
AgentState = StatusToStateKey(entity.Status);
|
AgentState = StatusToStateKey(entity.Status);
|
||||||
if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
|
if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
|
||||||
row.DiffStat = stat;
|
row.DiffStat = stat;
|
||||||
|
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
|
||||||
|
DiffAdditions = add;
|
||||||
|
DiffDeletions = del;
|
||||||
}
|
}
|
||||||
catch { /* best-effort refresh */ }
|
catch { /* best-effort refresh */ }
|
||||||
}
|
}
|
||||||
@@ -1131,6 +1136,52 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
[RelayCommand] private void ResetTaskModel() => TaskModelSelection = null;
|
[RelayCommand] private void ResetTaskModel() => TaskModelSelection = null;
|
||||||
[RelayCommand] private void ResetTaskTurns() => TaskMaxTurns = null;
|
[RelayCommand] private void ResetTaskTurns() => TaskMaxTurns = null;
|
||||||
[RelayCommand] private void ResetTaskAgent() => TaskSelectedAgent = TaskAgentOptions.Count > 0 ? TaskAgentOptions[0] : null;
|
[RelayCommand] private void ResetTaskAgent() => TaskSelectedAgent = TaskAgentOptions.Count > 0 ? TaskAgentOptions[0] : null;
|
||||||
|
|
||||||
|
// ── Review actions ──────────────────────────────────────────────────────────
|
||||||
|
[ObservableProperty] private string _reviewFeedback = "";
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async System.Threading.Tasks.Task ApproveReviewAsync()
|
||||||
|
{
|
||||||
|
if (Task is null || !_worker.IsConnected) return;
|
||||||
|
await _worker.ApproveReviewAsync(Task.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async System.Threading.Tasks.Task RejectReviewAsync()
|
||||||
|
{
|
||||||
|
if (Task is null || !_worker.IsConnected) return;
|
||||||
|
var feedback = ReviewFeedback;
|
||||||
|
if (string.IsNullOrWhiteSpace(feedback)) return;
|
||||||
|
await _worker.RejectReviewToQueueAsync(Task.Id, feedback);
|
||||||
|
ReviewFeedback = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async System.Threading.Tasks.Task ParkReviewAsync()
|
||||||
|
{
|
||||||
|
if (Task is null || !_worker.IsConnected) return;
|
||||||
|
await _worker.RejectReviewToIdleAsync(Task.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async System.Threading.Tasks.Task CancelReviewAsync()
|
||||||
|
{
|
||||||
|
if (Task is null || !_worker.IsConnected) return;
|
||||||
|
await _worker.CancelReviewAsync(Task.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Diff meter parser ───────────────────────────────────────────────────────
|
||||||
|
internal static (int Additions, int Deletions) ParseDiffStat(string? stat)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(stat)) return (0, 0);
|
||||||
|
int add = 0, del = 0;
|
||||||
|
var m1 = System.Text.RegularExpressions.Regex.Match(stat, @"(\d+)\s+insertion");
|
||||||
|
var m2 = System.Text.RegularExpressions.Regex.Match(stat, @"(\d+)\s+deletion");
|
||||||
|
if (m1.Success) int.TryParse(m1.Groups[1].Value, out add);
|
||||||
|
if (m2.Success) int.TryParse(m2.Groups[1].Value, out del);
|
||||||
|
return (add, del);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed partial class SubtaskRowViewModel : ViewModelBase
|
public sealed partial class SubtaskRowViewModel : ViewModelBase
|
||||||
|
|||||||
@@ -185,6 +185,44 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Review section — visible when task is WaitingForReview -->
|
||||||
|
<Border Classes="section-divider"
|
||||||
|
IsVisible="{Binding IsWaitingForReview}">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Classes="section-label" Text="{loc:Tr tasks.rejectRerunTitle}" Margin="0,0,0,2"/>
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr tasks.feedbackLabel}"/>
|
||||||
|
<TextBox Text="{Binding ReviewFeedback, Mode=TwoWay}"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
MinHeight="60"
|
||||||
|
MaxHeight="180"
|
||||||
|
PlaceholderText="{loc:Tr tasks.feedbackPlaceholder}"
|
||||||
|
Padding="8"
|
||||||
|
Background="{DynamicResource Surface2Brush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="8"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button Classes="btn accent"
|
||||||
|
Content="{loc:Tr tasks.approve}"
|
||||||
|
ToolTip.Tip="{loc:Tr tasks.approveTip}"
|
||||||
|
Command="{Binding ApproveReviewCommand}"/>
|
||||||
|
<Button Classes="btn"
|
||||||
|
Content="{loc:Tr tasks.reject}"
|
||||||
|
ToolTip.Tip="{loc:Tr tasks.rejectTip}"
|
||||||
|
Command="{Binding RejectReviewCommand}"/>
|
||||||
|
<Button Classes="btn"
|
||||||
|
Content="{loc:Tr tasks.park}"
|
||||||
|
ToolTip.Tip="{loc:Tr tasks.parkTip}"
|
||||||
|
Command="{Binding ParkReviewCommand}"/>
|
||||||
|
<Button Classes="btn"
|
||||||
|
Content="{loc:Tr tasks.cancel}"
|
||||||
|
ToolTip.Tip="{loc:Tr tasks.cancelTip}"
|
||||||
|
Command="{Binding CancelReviewCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Steps section -->
|
<!-- Steps section -->
|
||||||
<Border Classes="section-divider">
|
<Border Classes="section-divider">
|
||||||
<StackPanel Spacing="6">
|
<StackPanel Spacing="6">
|
||||||
|
|||||||
38
tests/ClaudeDo.Worker.Tests/UiVm/ParseDiffStatTests.cs
Normal file
38
tests/ClaudeDo.Worker.Tests/UiVm/ParseDiffStatTests.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.UiVm;
|
||||||
|
|
||||||
|
public class ParseDiffStatTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Null_Returns_Zero()
|
||||||
|
{
|
||||||
|
var (add, del) = DetailsIslandViewModel.ParseDiffStat(null);
|
||||||
|
Assert.Equal(0, add);
|
||||||
|
Assert.Equal(0, del);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Empty_Returns_Zero()
|
||||||
|
{
|
||||||
|
var (add, del) = DetailsIslandViewModel.ParseDiffStat("");
|
||||||
|
Assert.Equal(0, add);
|
||||||
|
Assert.Equal(0, del);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Full_Stat_Parses_Both()
|
||||||
|
{
|
||||||
|
var (add, del) = DetailsIslandViewModel.ParseDiffStat("2 files changed, 10 insertions(+), 3 deletions(-)");
|
||||||
|
Assert.Equal(10, add);
|
||||||
|
Assert.Equal(3, del);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Insertions_Only_Returns_Zero_Deletions()
|
||||||
|
{
|
||||||
|
var (add, del) = DetailsIslandViewModel.ParseDiffStat("1 file changed, 5 insertions(+)");
|
||||||
|
Assert.Equal(5, add);
|
||||||
|
Assert.Equal(0, del);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user