From eba0d842b51a51baf3195a72d25fcb1d789685cb Mon Sep 17 00:00:00 2001 From: mika kuns Date: Fri, 21 Aug 2026 13:35:26 +0200 Subject: [PATCH] feat(worker): report worktree-creation phase on OperationProgress Fills the silent gap between Queued and the first agent output: WorktreeManager now broadcasts a "creating_worktree" phase (before the initial git worktree add and again for the self-heal retry section) through the existing OperationProgress channel, and the task row shows it via the pre-existing but unused ops.worker.creatingWorktree locale key until the next entity refresh clears it. Confirmed the 2026-08-07 triage finding still holds: TaskRunner already broadcasts WorktreeUpdated right after WorktreeManager.CreateAsync (TaskRunner.cs:342-346, :501) -- no second broadcast added there. --- .../ViewModels/Islands/TaskRowViewModel.cs | 24 +++++++ .../Islands/TasksIslandViewModel.cs | 13 ++++ .../Views/Islands/TaskRowView.axaml | 6 ++ src/ClaudeDo.Worker/Runner/WorktreeManager.cs | 19 +++++- .../Runner/WorktreeManagerTests.cs | 68 +++++++++++++++++++ .../UiVm/TasksIslandViewModelPlanningTests.cs | 27 ++++++++ 6 files changed, 156 insertions(+), 1 deletion(-) diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs index b5c16431..57b52327 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs @@ -58,6 +58,12 @@ public sealed partial class TaskRowViewModel : ViewModelBase // Set by the custom drag while this row is being dragged — drives the "grabbed" row style. [ObservableProperty] private bool _isDragging; + // Transient: set from HubBroadcaster's OperationProgress while the worker is still creating + // this task's worktree (the silent gap between Queued and the first agent output). Cleared + // by the next entity refresh — UpdateFromEntity always reflects a settled state, so there's + // nothing left to show past that point. + [ObservableProperty] private string? _creationPhase; + // True while a drag is hovering this row (i.e. it would show a drop-hint gap). Used to // suppress the ordinary hover highlight/transitions so they don't fight the hint. public bool IsDropTarget => DropHintAbove || DropHintBelow; @@ -145,6 +151,16 @@ public sealed partial class TaskRowViewModel : ViewModelBase ? "1 roadblock reported during the run — see details" : $"{RoadblockCount} roadblocks reported during the run — see details"; + /// Mirrors WorktreeManager.PhaseCreatingWorktree — a hub payload token, not a display string. + private const string PhaseCreatingWorktree = "creating_worktree"; + + public bool HasCreationPhase => CreationPhase is not null; + public string? CreationPhaseLabel => CreationPhase switch + { + PhaseCreatingWorktree => Loc.T("ops.worker.creatingWorktree"), + _ => null, + }; + // True for every Failed task, even one that predates this field — FailureReasonLabel then // falls back to "unknown" instead of leaving the tooltip blank. public bool HasFailureReason => Status == TaskStatus.Failed; @@ -345,6 +361,11 @@ public sealed partial class TaskRowViewModel : ViewModelBase partial void OnRoadblockCountChanged(int value) { OnPropertyChanged(nameof(HasRoadblock)); OnPropertyChanged(nameof(RoadblockTooltip)); } partial void OnDropHintAboveChanged(bool value) => OnPropertyChanged(nameof(IsDropTarget)); partial void OnDropHintBelowChanged(bool value) => OnPropertyChanged(nameof(IsDropTarget)); + partial void OnCreationPhaseChanged(string? value) + { + OnPropertyChanged(nameof(HasCreationPhase)); + OnPropertyChanged(nameof(CreationPhaseLabel)); + } public void RefreshLocalized() { @@ -363,6 +384,9 @@ public sealed partial class TaskRowViewModel : ViewModelBase public void UpdateFromEntity(TaskEntity t) { + // Any entity-backed refresh reflects a settled state, so the transient creation-phase + // banner (set from the OperationProgress broadcast) has nothing left to announce. + CreationPhase = null; var (add, del) = ParseDiffStat(t.Worktree?.DiffStat); Number = t.Number; Title = t.Title; diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index 0b5fb0a2..17d1920f 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -188,6 +188,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable _worker.ConnectionRestoredEvent += () => LoadForList(_currentList); _worker.RefineStartedEvent += OnRefineStarted; _worker.RefineFinishedEvent += OnRefineFinished; + _worker.OperationProgressEvent += OnWorkerOperationProgress; } _langChangedHandler = (_, _) => RefreshLocalizedText(); Loc.LanguageChanged += _langChangedHandler; @@ -1563,6 +1564,18 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable if (row is not null) row.IsRefining = false; } + // OperationProgress is a generic channel shared with merge phases (TaskMergeService), whose + // opKey is also the task id — filter on the phase token so a merge in flight can't clobber + // this row's creation-phase banner. + private const string CreationPhaseCreatingWorktree = "creating_worktree"; + + private void OnWorkerOperationProgress(string opKey, string phase, int current, int total) + { + if (phase != CreationPhaseCreatingWorktree) return; + var row = Items.FirstOrDefault(r => r.Id == opKey); + if (row is not null) row.CreationPhase = phase; + } + partial void OnSelectedTaskChanged(TaskRowViewModel? value) { foreach (var i in Items) i.IsSelected = ReferenceEquals(i, value); diff --git a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml index 07af2efa..928b059e 100644 --- a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml @@ -143,6 +143,12 @@ + + + + +