fix(merge): conventional merge-commit default and live verify progress
The merge commit message was hand-rolled per caller ("Merge task: <title>",
"Merge <branch>", "Merge subtask") and ignored the task's commit type. Every
caller now passes a blank message and TaskMergeService fills in
CommitMessageBuilder.BuildMerge -> {commitType}(list-slug): merge <title> plus the
ClaudeDo-Task trailer; the merge modal prefills it from GetMergeTargets.
A merge whose list has a verify command holds the MergeTask call for minutes (5m46s
on this repo), during which the modal only disabled its button - no spinner, no
message, so a landed merge looked like a dead app. TaskMergeService now broadcasts
MergeProgress(taskId, phase, elapsedSeconds) for the merging and verifying phases
(re-reported every 30s) plus a WorkerLog line when verify starts; the modal shows a
spinner and the localized phase.
This commit is contained in:
@@ -34,6 +34,11 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
event Action<string>? PrepLineEvent;
|
||||
event Action<bool>? PrepFinishedEvent;
|
||||
|
||||
/// <summary>(taskId, phase, elapsedSeconds) — phase of an in-flight single-task merge
|
||||
/// ("merging" | "verifying"). Fires while the MergeTask call itself is still pending, so the
|
||||
/// waiting UI can show what it's blocked on; the verify phase re-fires every 30 s.</summary>
|
||||
event Action<string, string, int>? MergeProgressEvent;
|
||||
|
||||
event Action<string, string>? PlanningMergeStartedEvent;
|
||||
event Action<string, string>? PlanningSubtaskMergedEvent;
|
||||
/// <summary>(planningTaskId, subtaskId, conflictedFiles, externallyDriven). externallyDriven
|
||||
|
||||
@@ -64,6 +64,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
|
||||
public event Action<UsageSnapshotDto>? UsageUpdatedEvent;
|
||||
|
||||
public event Action<string, string, int>? MergeProgressEvent;
|
||||
|
||||
public event Action<string, string>? PlanningMergeStartedEvent;
|
||||
public event Action<string, string>? PlanningSubtaskMergedEvent;
|
||||
public event Action<string, string, IReadOnlyList<string>, bool>? PlanningMergeConflictEvent;
|
||||
@@ -172,6 +174,11 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
WorkerLogReceivedEvent?.Invoke(new WorkerLogEntry(message, level, timestampUtc)));
|
||||
});
|
||||
|
||||
_hub.On<string, string, int>("MergeProgress", (taskId, phase, elapsedSeconds) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => MergeProgressEvent?.Invoke(taskId, phase, elapsedSeconds));
|
||||
});
|
||||
|
||||
_hub.On<string, string>("PlanningMergeStarted", (planningTaskId, targetBranch) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => PlanningMergeStartedEvent?.Invoke(planningTaskId, targetBranch));
|
||||
@@ -694,7 +701,8 @@ public record MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles,
|
||||
public record BaseDirtyWarningDto(int ModifiedCount, int UntrackedCount);
|
||||
public record SetTaskStatusResultDto(BaseDirtyWarningDto? BaseDirty);
|
||||
public record MergePreviewDto(string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount);
|
||||
public record MergeTargetsDto(string DefaultBranch, IReadOnlyList<string> LocalBranches);
|
||||
public record MergeTargetsDto(
|
||||
string DefaultBranch, IReadOnlyList<string> LocalBranches, string DefaultCommitMessage);
|
||||
public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList<ConflictDocumentDto> Files);
|
||||
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<MergeSegmentDto> Segments);
|
||||
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
|
||||
|
||||
@@ -21,6 +21,12 @@ public sealed partial class MergeModalViewModel : ViewModelBase
|
||||
[ObservableProperty] private string _commitMessage = "";
|
||||
|
||||
[ObservableProperty][NotifyCanExecuteChangedFor(nameof(SubmitCommand))] private bool _isBusy;
|
||||
|
||||
/// What the pending merge is doing right now, fed by the worker's MergeProgress broadcast.
|
||||
/// A merge whose list has a verify command can occupy this call for minutes — without this the
|
||||
/// modal only greys the button out and looks dead.
|
||||
[ObservableProperty] private string? _progressMessage;
|
||||
|
||||
[ObservableProperty] private string? _errorMessage;
|
||||
[ObservableProperty] private string? _warningMessage;
|
||||
[ObservableProperty] private string? _successMessage;
|
||||
@@ -58,6 +64,10 @@ public sealed partial class MergeModalViewModel : ViewModelBase
|
||||
ErrorMessage = Loc.T("vm.merge.workerOfflineBranches");
|
||||
return;
|
||||
}
|
||||
// The worker owns the default message — only it knows the task's commit type and the
|
||||
// list name the scope is slugged from. The locale string stays as the offline fallback.
|
||||
if (!string.IsNullOrWhiteSpace(targets.DefaultCommitMessage))
|
||||
CommitMessage = targets.DefaultCommitMessage;
|
||||
foreach (var b in targets.LocalBranches) Branches.Add(b);
|
||||
SelectedBranch = Branches.Contains(targets.DefaultBranch)
|
||||
? targets.DefaultBranch
|
||||
@@ -81,6 +91,10 @@ public sealed partial class MergeModalViewModel : ViewModelBase
|
||||
ErrorMessage = null;
|
||||
WarningMessage = null;
|
||||
SuccessMessage = null;
|
||||
// Subscribed only for the duration of the call: the worker's broadcast reaches every
|
||||
// client, and a transient VM left on that event would outlive its window.
|
||||
ProgressMessage = Loc.T("vm.merge.progressMerging");
|
||||
_worker.MergeProgressEvent += OnMergeProgress;
|
||||
try
|
||||
{
|
||||
var result = await _worker.MergeTaskAsync(
|
||||
@@ -133,10 +147,28 @@ public sealed partial class MergeModalViewModel : ViewModelBase
|
||||
}
|
||||
finally
|
||||
{
|
||||
_worker.MergeProgressEvent -= OnMergeProgress;
|
||||
ProgressMessage = null;
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMergeProgress(string taskId, string phase, int elapsedSeconds)
|
||||
{
|
||||
if (taskId != TaskId) return;
|
||||
ProgressMessage = phase switch
|
||||
{
|
||||
MergePhaseVerifying => Loc.T("vm.merge.progressVerifying", FormatElapsed(elapsedSeconds)),
|
||||
_ => Loc.T("vm.merge.progressMerging"),
|
||||
};
|
||||
}
|
||||
|
||||
/// Mirrors TaskMergeService.PhaseVerifying — a hub payload token, not a display string.
|
||||
private const string MergePhaseVerifying = "verifying";
|
||||
|
||||
private static string FormatElapsed(int seconds) =>
|
||||
TimeSpan.FromSeconds(Math.Max(0, seconds)).ToString(@"mm\:ss");
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel() => CloseAction?.Invoke();
|
||||
}
|
||||
|
||||
@@ -440,8 +440,8 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
||||
MergeResultDto result;
|
||||
try
|
||||
{
|
||||
result = await mergeFn(row.TaskId, target!, false,
|
||||
Loc.T("vm.merge.commitMessage", row.TaskTitle));
|
||||
// Blank message: the worker builds the conventional default per task.
|
||||
result = await mergeFn(row.TaskId, target!, false, "");
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -22,6 +22,15 @@
|
||||
<ctl:ModalShell.Footer>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<!-- A merge into a list with a verify command holds this dialog for minutes; without a
|
||||
live phase here the disabled button is the only feedback and reads as a dead app. -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center"
|
||||
IsVisible="{Binding IsBusy}">
|
||||
<Ellipse Classes="spinner" VerticalAlignment="Center"/>
|
||||
<TextBlock Classes="meta" VerticalAlignment="Center"
|
||||
Text="{Binding ProgressMessage}"
|
||||
IsVisible="{Binding ProgressMessage, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
</StackPanel>
|
||||
<Button Classes="btn" Content="{loc:Tr modals.merge.cancel}" Command="{Binding CancelCommand}" MinWidth="90"/>
|
||||
<Button Content="{loc:Tr modals.merge.merge}" Classes="primary"
|
||||
Command="{Binding SubmitCommand}"
|
||||
|
||||
Reference in New Issue
Block a user