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:
mika kuns
2026-08-11 19:14:36 +02:00
parent fc9df7f9ac
commit cad0582b37
22 changed files with 405 additions and 31 deletions
@@ -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
+9 -1
View File
@@ -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);