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.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -143,6 +143,12 @@
|
||||
<TextBlock Text="{Binding StatusLabel}"/>
|
||||
</Border>
|
||||
|
||||
<!-- Transient creation-phase chip: fills the silent gap between Queued and the
|
||||
first agent output while the worker creates this task's worktree. -->
|
||||
<Border Classes="chip chip-tag" IsVisible="{Binding HasCreationPhase}">
|
||||
<TextBlock Text="{Binding CreationPhaseLabel}"/>
|
||||
</Border>
|
||||
|
||||
<!-- Dequeue button (visible when row is Queued, or planning parent has queued subtasks) -->
|
||||
<Button Classes="icon-btn dequeue-btn"
|
||||
IsVisible="{Binding CanRemoveFromQueue}"
|
||||
|
||||
@@ -4,6 +4,7 @@ using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Config;
|
||||
using ClaudeDo.Worker.Git;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Worker.Runner;
|
||||
@@ -12,19 +13,31 @@ public sealed record WorktreeContext(string WorktreePath, string BranchName, str
|
||||
|
||||
public sealed class WorktreeManager
|
||||
{
|
||||
// Phase token for the OperationProgress broadcast while `git worktree add` (+ the
|
||||
// self-heal retry) is in flight -- stable identifier, localized by the UI via the
|
||||
// pre-existing "ops.worker.creatingWorktree" key. opKey is the task id.
|
||||
public const string PhaseCreatingWorktree = "creating_worktree";
|
||||
|
||||
private readonly GitService _git;
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly WorkerConfig _cfg;
|
||||
private readonly ILogger<WorktreeManager> _logger;
|
||||
private readonly HubBroadcaster? _broadcaster;
|
||||
|
||||
public WorktreeManager(GitService git, IDbContextFactory<ClaudeDoDbContext> dbFactory, WorkerConfig cfg, ILogger<WorktreeManager> logger)
|
||||
public WorktreeManager(
|
||||
GitService git, IDbContextFactory<ClaudeDoDbContext> dbFactory, WorkerConfig cfg,
|
||||
ILogger<WorktreeManager> logger, HubBroadcaster? broadcaster = null)
|
||||
{
|
||||
_git = git;
|
||||
_dbFactory = dbFactory;
|
||||
_cfg = cfg;
|
||||
_logger = logger;
|
||||
_broadcaster = broadcaster;
|
||||
}
|
||||
|
||||
private Task BroadcastCreating(string taskId) =>
|
||||
_broadcaster?.OperationProgress(taskId, PhaseCreatingWorktree, 0, 0) ?? Task.CompletedTask;
|
||||
|
||||
public async Task<WorktreeContext> CreateAsync(TaskEntity task, ListEntity list, CancellationToken ct)
|
||||
{
|
||||
var workingDir = list.WorkingDir
|
||||
@@ -63,6 +76,7 @@ public sealed class WorktreeManager
|
||||
|
||||
// Create the worktree. If a stale branch from a previous run remains
|
||||
// (e.g. after force-remove), delete it and retry once.
|
||||
await BroadcastCreating(task.Id);
|
||||
try
|
||||
{
|
||||
await _git.WorktreeAddAsync(workingDir, branchName, worktreePath, baseCommit, ct);
|
||||
@@ -70,6 +84,9 @@ public sealed class WorktreeManager
|
||||
catch (InvalidOperationException ex) when (ex.Message.Contains("already exists", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogWarning("Branch {Branch} already exists; cleaning phantom worktrees and retrying", branchName);
|
||||
// Self-heal is the slow section (list -> remove -> prune -> branch delete -> retry);
|
||||
// report it as its own section rather than staying silent through the retry.
|
||||
await BroadcastCreating(task.Id);
|
||||
|
||||
// Find and forcefully remove any existing worktree registered against this branch.
|
||||
List<string> stalePaths;
|
||||
|
||||
Reference in New Issue
Block a user