diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json
index ffd67203..073bb51d 100644
--- a/src/ClaudeDo.Localization/locales/de.json
+++ b/src/ClaudeDo.Localization/locales/de.json
@@ -384,7 +384,7 @@
"settings": "Einstellungen",
"activity": "Aktivität",
"runningNow": "Läuft gerade",
- "opVerifyMerge": "Verify (Merge)",
+ "opVerifyMerge": "Prüfung nach Merge",
"opVerifyBatch": "Verify (Batch)",
"opMerging": "Merge",
"opRebasing": "Rebase",
@@ -393,6 +393,7 @@
"blocked": "Blockiert",
"running": "Läuft",
"review": "Im Review",
+ "verifying": "Wird geprüft",
"failed": "Fehlgeschlagen",
"roadblock": "Blockade gemeldet",
"openSession": "Sitzung öffnen",
diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json
index c12a6eb8..d5314640 100644
--- a/src/ClaudeDo.Localization/locales/en.json
+++ b/src/ClaudeDo.Localization/locales/en.json
@@ -384,7 +384,7 @@
"settings": "Settings",
"activity": "Activity",
"runningNow": "Running now",
- "opVerifyMerge": "Verify (merge)",
+ "opVerifyMerge": "Verifying after merge",
"opVerifyBatch": "Verify (batch)",
"opMerging": "Merging",
"opRebasing": "Rebasing",
@@ -393,6 +393,7 @@
"blocked": "Blocked",
"running": "Running",
"review": "In review",
+ "verifying": "Verifying",
"failed": "Failed",
"roadblock": "Roadblock reported",
"openSession": "Open session",
diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
index cc7d4dfc..a690b500 100644
--- a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
@@ -157,10 +157,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
try
{
var snapshot = await _worker.GetActiveOperationsAsync();
+ var hadVerify = RunningOperations.Any(IsVerifyMerge);
RunningOperations.Clear();
foreach (var op in snapshot.Where(o => !_endedOperationIds.Contains(o.Id)).OrderBy(o => o.StartedAt))
RunningOperations.Add(new RunningOperationViewModel(op));
NotifyOperationsChanged();
+ if (hadVerify || RunningOperations.Any(IsVerifyMerge)) _ = RefreshQueueAsync();
}
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("missionControl.operationsRefreshFailed", ex.Message)); }
}
@@ -171,8 +173,10 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
private void AddOperation(RunningOperationDto op)
{
if (_endedOperationIds.Contains(op.Id) || RunningOperations.Any(r => r.Id == op.Id)) return;
- RunningOperations.Add(new RunningOperationViewModel(op));
+ var row = new RunningOperationViewModel(op);
+ RunningOperations.Add(row);
NotifyOperationsChanged();
+ if (IsVerifyMerge(row)) _ = RefreshQueueAsync();
}
private void RemoveOperation(string id)
@@ -182,8 +186,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
if (row is null) return;
RunningOperations.Remove(row);
NotifyOperationsChanged();
+ if (IsVerifyMerge(row)) _ = RefreshQueueAsync();
}
+ /// A post-merge verify keeps its task in WaitingForReview, so only this op marks the activity row "Verifying".
+ private static bool IsVerifyMerge(RunningOperationViewModel r) => r.Kind == OperationKinds.VerifyMerge;
+
private void NotifyOperationsChanged()
{
OnPropertyChanged(nameof(HasRunningOperations));
@@ -220,7 +228,8 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
{
var id = r.Id;
var running = r.Status == ClaudeDo.Data.Models.TaskStatus.Running;
- var (tint, colorKey, labelKey) = Decorate(r.Status, r.RoadblockCount, r.BlockedByTaskId != null);
+ var (tint, colorKey, labelKey) = Decorate(r.Status, r.RoadblockCount, r.BlockedByTaskId != null,
+ RunningOperations.Any(o => o.Kind == OperationKinds.VerifyMerge && o.TaskId == id));
Queued.Add(new QueuedTaskViewModel
{
Id = id,
@@ -243,12 +252,14 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
// Row tint + status label for the activity strip. Keys are resolved to brushes by
// DotBrushConverter ("Brush"), so they must match tokens in Tokens.axaml.
private static (string? Tint, string? ColorKey, string? LabelKey) Decorate(
- ClaudeDo.Data.Models.TaskStatus status, int roadblockCount, bool blocked)
+ ClaudeDo.Data.Models.TaskStatus status, int roadblockCount, bool blocked, bool verifying)
=> status switch
{
ClaudeDo.Data.Models.TaskStatus.Failed => ("ErrorTint", "StatusError", "missionControl.failed"),
ClaudeDo.Data.Models.TaskStatus.WaitingForReview when roadblockCount > 0
=> ("RoadblockTint", "Amber", "missionControl.roadblock"),
+ ClaudeDo.Data.Models.TaskStatus.WaitingForReview when verifying
+ => ("RunningTint", "StatusRunning", "missionControl.verifying"),
ClaudeDo.Data.Models.TaskStatus.WaitingForReview => ("DoneTint", "StatusReview", "missionControl.review"),
ClaudeDo.Data.Models.TaskStatus.Running => ("RunningTint", "StatusRunning", "missionControl.running"),
_ when blocked => (null, "Amber", "missionControl.blocked"),
@@ -890,7 +901,11 @@ public sealed partial class RunningOperationViewModel : ObservableObject
private readonly DateTime _startedAtUtc;
public string Id { get; }
- /// Localized operation kind ("Verify (merge)", "Rebasing", ...).
+ /// Wire kind token ().
+ public string Kind { get; }
+ /// The task the operation runs for; null for a batch verify.
+ public string? TaskId { get; }
+ /// Localized operation kind ("Verifying after merge", "Rebasing", ...).
public string KindLabel { get; }
/// #Number Title for a task operation, the list name for a batch verify.
public string Subject { get; }
@@ -900,6 +915,8 @@ public sealed partial class RunningOperationViewModel : ObservableObject
public RunningOperationViewModel(RunningOperationDto op)
{
Id = op.Id;
+ Kind = op.Kind;
+ TaskId = op.TaskId;
_startedAtUtc = op.StartedAt;
KindLabel = op.Kind switch
{
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlRunningOperationsTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlRunningOperationsTests.cs
index e1304b25..47919d6d 100644
--- a/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlRunningOperationsTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlRunningOperationsTests.cs
@@ -1,4 +1,6 @@
using ClaudeDo.Data;
+using ClaudeDo.Data.Models;
+using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels;
using Microsoft.EntityFrameworkCore;
@@ -126,6 +128,30 @@ public class MissionControlRunningOperationsTests : IDisposable
Assert.Equal("c", Assert.Single(vm.RunningOperations).Id);
}
+ [Fact]
+ public async Task VerifyMergeOperation_ShowsReviewTaskAsVerifying_UntilItEnds()
+ {
+ await using (var db = NewContext())
+ {
+ db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
+ db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "rev1", ListId = "L1", Title = "review", Status = ClaudeDo.Data.Models.TaskStatus.WaitingForReview, CreatedAt = DateTime.UtcNow });
+ await db.SaveChangesAsync();
+ }
+ var worker = new FakeWorker();
+ using var vm = BuildVm(worker);
+ await vm.RefreshQueueAsync();
+ Assert.Equal(Loc.T("missionControl.review"), vm.Queued.Single().StatusLabel);
+
+ worker.RaiseOperationStarted(new RunningOperationDto("v1", OperationKinds.VerifyMerge, "rev1", 1, "L1", "review", DateTime.UtcNow));
+ await vm.RefreshQueueAsync();
+ Assert.Equal(Loc.T("missionControl.verifying"), vm.Queued.Single().StatusLabel);
+ Assert.Equal("StatusRunning", vm.Queued.Single().StatusColorKey);
+
+ worker.RaiseOperationEnded("v1");
+ await vm.RefreshQueueAsync();
+ Assert.Equal(Loc.T("missionControl.review"), vm.Queued.Single().StatusLabel);
+ }
+
[Fact]
public async Task TickOperations_UpdatesElapsed()
{