fix(claude-do): merge Mission Control: show post-merge verify as "Verifying" inste

ClaudeDo-Task: eec77f99-1986-4db7-80d9-8294dcc1c146
This commit is contained in:
mika kuns
2026-10-01 14:52:07 +02:00
4 changed files with 51 additions and 6 deletions
+2 -1
View File
@@ -384,7 +384,7 @@
"settings": "Einstellungen", "settings": "Einstellungen",
"activity": "Aktivität", "activity": "Aktivität",
"runningNow": "Läuft gerade", "runningNow": "Läuft gerade",
"opVerifyMerge": "Verify (Merge)", "opVerifyMerge": "Prüfung nach Merge",
"opVerifyBatch": "Verify (Batch)", "opVerifyBatch": "Verify (Batch)",
"opMerging": "Merge", "opMerging": "Merge",
"opRebasing": "Rebase", "opRebasing": "Rebase",
@@ -393,6 +393,7 @@
"blocked": "Blockiert", "blocked": "Blockiert",
"running": "Läuft", "running": "Läuft",
"review": "Im Review", "review": "Im Review",
"verifying": "Wird geprüft",
"failed": "Fehlgeschlagen", "failed": "Fehlgeschlagen",
"roadblock": "Blockade gemeldet", "roadblock": "Blockade gemeldet",
"openSession": "Sitzung öffnen", "openSession": "Sitzung öffnen",
+2 -1
View File
@@ -384,7 +384,7 @@
"settings": "Settings", "settings": "Settings",
"activity": "Activity", "activity": "Activity",
"runningNow": "Running now", "runningNow": "Running now",
"opVerifyMerge": "Verify (merge)", "opVerifyMerge": "Verifying after merge",
"opVerifyBatch": "Verify (batch)", "opVerifyBatch": "Verify (batch)",
"opMerging": "Merging", "opMerging": "Merging",
"opRebasing": "Rebasing", "opRebasing": "Rebasing",
@@ -393,6 +393,7 @@
"blocked": "Blocked", "blocked": "Blocked",
"running": "Running", "running": "Running",
"review": "In review", "review": "In review",
"verifying": "Verifying",
"failed": "Failed", "failed": "Failed",
"roadblock": "Roadblock reported", "roadblock": "Roadblock reported",
"openSession": "Open session", "openSession": "Open session",
@@ -157,10 +157,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
try try
{ {
var snapshot = await _worker.GetActiveOperationsAsync(); var snapshot = await _worker.GetActiveOperationsAsync();
var hadVerify = RunningOperations.Any(IsVerifyMerge);
RunningOperations.Clear(); RunningOperations.Clear();
foreach (var op in snapshot.Where(o => !_endedOperationIds.Contains(o.Id)).OrderBy(o => o.StartedAt)) foreach (var op in snapshot.Where(o => !_endedOperationIds.Contains(o.Id)).OrderBy(o => o.StartedAt))
RunningOperations.Add(new RunningOperationViewModel(op)); RunningOperations.Add(new RunningOperationViewModel(op));
NotifyOperationsChanged(); NotifyOperationsChanged();
if (hadVerify || RunningOperations.Any(IsVerifyMerge)) _ = RefreshQueueAsync();
} }
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("missionControl.operationsRefreshFailed", ex.Message)); } 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) private void AddOperation(RunningOperationDto op)
{ {
if (_endedOperationIds.Contains(op.Id) || RunningOperations.Any(r => r.Id == op.Id)) return; 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(); NotifyOperationsChanged();
if (IsVerifyMerge(row)) _ = RefreshQueueAsync();
} }
private void RemoveOperation(string id) private void RemoveOperation(string id)
@@ -182,8 +186,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
if (row is null) return; if (row is null) return;
RunningOperations.Remove(row); RunningOperations.Remove(row);
NotifyOperationsChanged(); NotifyOperationsChanged();
if (IsVerifyMerge(row)) _ = RefreshQueueAsync();
} }
/// <summary>A post-merge verify keeps its task in WaitingForReview, so only this op marks the activity row "Verifying".</summary>
private static bool IsVerifyMerge(RunningOperationViewModel r) => r.Kind == OperationKinds.VerifyMerge;
private void NotifyOperationsChanged() private void NotifyOperationsChanged()
{ {
OnPropertyChanged(nameof(HasRunningOperations)); OnPropertyChanged(nameof(HasRunningOperations));
@@ -220,7 +228,8 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
{ {
var id = r.Id; var id = r.Id;
var running = r.Status == ClaudeDo.Data.Models.TaskStatus.Running; 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 Queued.Add(new QueuedTaskViewModel
{ {
Id = id, 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 // Row tint + status label for the activity strip. Keys are resolved to brushes by
// DotBrushConverter ("<key>Brush"), so they must match tokens in Tokens.axaml. // DotBrushConverter ("<key>Brush"), so they must match tokens in Tokens.axaml.
private static (string? Tint, string? ColorKey, string? LabelKey) Decorate( 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 => status switch
{ {
ClaudeDo.Data.Models.TaskStatus.Failed => ("ErrorTint", "StatusError", "missionControl.failed"), ClaudeDo.Data.Models.TaskStatus.Failed => ("ErrorTint", "StatusError", "missionControl.failed"),
ClaudeDo.Data.Models.TaskStatus.WaitingForReview when roadblockCount > 0 ClaudeDo.Data.Models.TaskStatus.WaitingForReview when roadblockCount > 0
=> ("RoadblockTint", "Amber", "missionControl.roadblock"), => ("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.WaitingForReview => ("DoneTint", "StatusReview", "missionControl.review"),
ClaudeDo.Data.Models.TaskStatus.Running => ("RunningTint", "StatusRunning", "missionControl.running"), ClaudeDo.Data.Models.TaskStatus.Running => ("RunningTint", "StatusRunning", "missionControl.running"),
_ when blocked => (null, "Amber", "missionControl.blocked"), _ when blocked => (null, "Amber", "missionControl.blocked"),
@@ -890,7 +901,11 @@ public sealed partial class RunningOperationViewModel : ObservableObject
private readonly DateTime _startedAtUtc; private readonly DateTime _startedAtUtc;
public string Id { get; } public string Id { get; }
/// <summary>Localized operation kind ("Verify (merge)", "Rebasing", ...).</summary> /// <summary>Wire kind token (<see cref="OperationKinds"/>).</summary>
public string Kind { get; }
/// <summary>The task the operation runs for; null for a batch verify.</summary>
public string? TaskId { get; }
/// <summary>Localized operation kind ("Verifying after merge", "Rebasing", ...).</summary>
public string KindLabel { get; } public string KindLabel { get; }
/// <summary><c>#Number Title</c> for a task operation, the list name for a batch verify.</summary> /// <summary><c>#Number Title</c> for a task operation, the list name for a batch verify.</summary>
public string Subject { get; } public string Subject { get; }
@@ -900,6 +915,8 @@ public sealed partial class RunningOperationViewModel : ObservableObject
public RunningOperationViewModel(RunningOperationDto op) public RunningOperationViewModel(RunningOperationDto op)
{ {
Id = op.Id; Id = op.Id;
Kind = op.Kind;
TaskId = op.TaskId;
_startedAtUtc = op.StartedAt; _startedAtUtc = op.StartedAt;
KindLabel = op.Kind switch KindLabel = op.Kind switch
{ {
@@ -1,4 +1,6 @@
using ClaudeDo.Data; using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels; using ClaudeDo.Ui.ViewModels;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -126,6 +128,30 @@ public class MissionControlRunningOperationsTests : IDisposable
Assert.Equal("c", Assert.Single(vm.RunningOperations).Id); 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] [Fact]
public async Task TickOperations_UpdatesElapsed() public async Task TickOperations_UpdatesElapsed()
{ {