feat(worker): surface rebase and worktree-maintenance progress on OperationProgress

RebaseOthersAfterMergeAsync now broadcasts a "rebasing" phase with i/n over the
WaitingForReview branches it checks, so the merge/continue_merge callers stop
showing the stalled "Merging…" phase while the best-effort rebase loop runs
(rebase still runs before the verify gate; a failed rebase still leaves the
merge itself successful). WorktreeMaintenanceService gained an optional
HubBroadcaster to report the same i/n shape per worktree during
cleanup/reset, with no new UI surface (deliberately out of scope). Both
review-action viewmodels now also listen on OperationProgressEvent (which
carries the total that the elapsed-seconds-only MergeProgressEvent drops) to
render "Rebasing other worktrees… (i/n)".
This commit is contained in:
mika kuns
2026-08-21 13:33:02 +02:00
parent dcda067b48
commit 42de97e16a
8 changed files with 183 additions and 9 deletions
@@ -1290,7 +1290,15 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
if (progressTaskId != taskId || phase != MergePhaseVerifying) return;
ApproveOp.Report(Loc.T("ops.merge.verifying", FormatElapsed(elapsedSeconds)));
}
// Separate event: MergeProgressEvent forwards elapsed-seconds-only (verifying), but the
// rebase phase needs the branch count too, which only rides the untrimmed OperationProgress.
void OnOperationProgress(string opKey, string phase, int current, int total)
{
if (opKey != taskId || phase != MergePhaseRebasing) return;
ApproveOp.Report(FormatRebasing(current, total));
}
_worker.MergeProgressEvent += OnMergeProgress;
_worker.OperationProgressEvent += OnOperationProgress;
try
{
var hasChildren = Subtasks.Count > 0 || ChildOutcomes.Count > 0;
@@ -1314,15 +1322,24 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
finally
{
_worker.MergeProgressEvent -= OnMergeProgress;
_worker.OperationProgressEvent -= OnOperationProgress;
}
}
/// Mirrors TaskMergeService.PhaseVerifying — a hub payload token, not a display string.
private const string MergePhaseVerifying = "verifying";
/// Mirrors TaskMergeService.PhaseRebasing — a hub payload token, not a display string.
private const string MergePhaseRebasing = "rebasing";
private static string FormatElapsed(int seconds) =>
TimeSpan.FromSeconds(Math.Max(0, seconds)).ToString(@"mm\:ss");
private static string FormatRebasing(int current, int total) =>
total > 0
? $"{Loc.T("ops.worker.rebasingAfterMerge")} ({current}/{total})"
: Loc.T("ops.worker.rebasingAfterMerge");
// Force the diff to have been opened before a merge can happen — but only when
// there is actually something to inspect (a childless sandbox run with no worktree
// has no diff, so it approves straight through).
@@ -95,6 +95,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase
// client, and a transient VM left on that event would outlive its window.
ProgressMessage = Loc.T("vm.merge.progressMerging");
_worker.MergeProgressEvent += OnMergeProgress;
_worker.OperationProgressEvent += OnOperationProgress;
try
{
var result = await _worker.MergeTaskAsync(
@@ -148,6 +149,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase
finally
{
_worker.MergeProgressEvent -= OnMergeProgress;
_worker.OperationProgressEvent -= OnOperationProgress;
ProgressMessage = null;
IsBusy = false;
}
@@ -156,6 +158,9 @@ public sealed partial class MergeModalViewModel : ViewModelBase
private void OnMergeProgress(string taskId, string phase, int elapsedSeconds)
{
if (taskId != TaskId) return;
// Rebasing is handled by OnOperationProgress (needs the branch total, which this
// elapsed-seconds-only forwarder drops) -- don't overwrite it with the generic fallback.
if (phase == MergePhaseRebasing) return;
ProgressMessage = phase switch
{
MergePhaseVerifying => Loc.T("vm.merge.progressVerifying", FormatElapsed(elapsedSeconds)),
@@ -163,9 +168,22 @@ public sealed partial class MergeModalViewModel : ViewModelBase
};
}
// Separate event: MergeProgressEvent forwards elapsed-seconds-only (verifying), but the
// rebase phase needs the branch count too, which only rides the untrimmed OperationProgress.
private void OnOperationProgress(string opKey, string phase, int current, int total)
{
if (opKey != TaskId || phase != MergePhaseRebasing) return;
ProgressMessage = total > 0
? $"{Loc.T("ops.worker.rebasingAfterMerge")} ({current}/{total})"
: Loc.T("ops.worker.rebasingAfterMerge");
}
/// Mirrors TaskMergeService.PhaseVerifying — a hub payload token, not a display string.
private const string MergePhaseVerifying = "verifying";
/// Mirrors TaskMergeService.PhaseRebasing — a hub payload token, not a display string.
private const string MergePhaseRebasing = "rebasing";
private static string FormatElapsed(int seconds) =>
TimeSpan.FromSeconds(Math.Max(0, seconds)).ToString(@"mm\:ss");