From f21c65be185801c831a89fafaebb9f01a2ec9341 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Tue, 9 Jun 2026 16:40:59 +0200 Subject: [PATCH] feat(ui): richer diff viewer + surface child roadblocks on parents - UnifiedDiffParser detects added/deleted/renamed/binary files; diff modal shows a file list, binary/empty placeholders, and can diff a merged task by commit range after its worktree is gone - DetailsIslandViewModel flags children needing attention (failed, cancelled, awaiting review, or with roadblocks) on the parent - GitService gains worktree head-commit/range support; planning chain, merge orchestration, and session manager tweaks with updated tests - refresh app/installer/worker icons Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ClaudeDo.App/Assets/ClaudeTask.ico | Bin 45573 -> 56340 bytes src/ClaudeDo.Data/Git/GitService.cs | 14 ++ src/ClaudeDo.Installer/ClaudeTaskSetup.ico | Bin 374 -> 58310 bytes src/ClaudeDo.Localization/locales/de.json | 5 +- src/ClaudeDo.Localization/locales/en.json | 5 +- src/ClaudeDo.Ui/Design/IslandStyles.axaml | 7 + .../Islands/DetailsIslandViewModel.cs | 109 ++++++++++++--- .../ViewModels/IslandsShellViewModel.cs | 12 +- .../ViewModels/Modals/DiffModalViewModel.cs | 36 ++++- .../ViewModels/Modals/MergeModalViewModel.cs | 5 + .../ViewModels/Modals/UnifiedDiffParser.cs | 40 +++++- .../Planning/ConflictResolutionViewModel.cs | 16 ++- .../Views/Islands/Detail/WorkConsole.axaml | 16 +++ .../Views/Modals/DiffModalView.axaml | 131 ++++++++++++------ src/ClaudeDo.Worker/CLAUDE.md | 6 +- src/ClaudeDo.Worker/ClaudeDo.Worker.csproj | 1 + src/ClaudeDo.Worker/ClaudeTaskWorker.ico | Bin 0 -> 59764 bytes .../Lifecycle/TaskMergeService.cs | 4 +- .../Planning/PlanningChainCoordinator.cs | 17 ++- .../Planning/PlanningMcpService.cs | 8 +- .../Planning/PlanningMergeOrchestrator.cs | 4 + .../Planning/PlanningSessionManager.cs | 11 +- src/ClaudeDo.Worker/Refine/RefineRunner.cs | 2 +- .../ConflictResolutionViewModelTests.cs | 2 +- .../ViewModels/UnifiedDiffParserTests.cs | 109 +++++++++++++++ .../Planning/PlanningChainCoordinatorTests.cs | 35 +++-- .../Planning/PlanningEndToEndTests.cs | 31 +++-- .../Services/TaskMergeServiceTests.cs | 2 + 28 files changed, 509 insertions(+), 119 deletions(-) create mode 100644 src/ClaudeDo.Worker/ClaudeTaskWorker.ico create mode 100644 tests/ClaudeDo.Ui.Tests/ViewModels/UnifiedDiffParserTests.cs diff --git a/src/ClaudeDo.App/Assets/ClaudeTask.ico b/src/ClaudeDo.App/Assets/ClaudeTask.ico index 1ca3aef6dab27259b6874fd75bd73d2862118402..058c5df97a5317e42627b81a9120cb053c8125e3 100644 GIT binary patch literal 56340 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 literal 45573 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 D{3CFV diff --git a/src/ClaudeDo.Data/Git/GitService.cs b/src/ClaudeDo.Data/Git/GitService.cs index 7ba2cf9..fc1edd5 100644 --- a/src/ClaudeDo.Data/Git/GitService.cs +++ b/src/ClaudeDo.Data/Git/GitService.cs @@ -124,6 +124,20 @@ public sealed class GitService return await GetDiffAsync(worktreePath, ct); } + /// + /// Diff between two commits, run in any repo that can reach them. Used to view a + /// task's changes after its worktree has been merged away (the commits survive on + /// the target branch even though the worktree directory and branch ref are gone). + /// + public async Task GetCommitRangeDiffAsync(string repoDir, string baseCommit, string headCommit, CancellationToken ct = default) + { + var (exitCode, stdout, stderr) = await RunGitAsync(repoDir, + ["diff", $"{baseCommit}..{headCommit}"], ct); + if (exitCode != 0) + throw new InvalidOperationException($"git diff {baseCommit}..{headCommit} failed (exit {exitCode}): {stderr}"); + return stdout; + } + public async Task DiffStatAsync(string worktreePath, string baseCommit, string headCommit, CancellationToken ct = default) { var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath, diff --git a/src/ClaudeDo.Installer/ClaudeTaskSetup.ico b/src/ClaudeDo.Installer/ClaudeTaskSetup.ico index 87fa5fa2bf14c2ef777e9d3fd8a4a5f084e8ec3c..0fa0d35def5dc914db7694cf3ee28c4492ff7469 100644 GIT binary patch literal 58310 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 U*P=zr6aWAK07*qoM6N<$f`#RnF8}}l diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index 4b8a1b2..d5737d5 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -238,7 +238,10 @@ "diff": { "title": "DIFF", "windowTitle": "Diff", - "merge": "Mergen…" + "merge": "Mergen…", + "filesHeader": "Dateien", + "binary": "Binärdatei — kein Text-Diff", + "empty": "Kein Inhalt" }, "worktree": { "title": "Worktree" diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index aae7ce7..4457637 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -238,7 +238,10 @@ "diff": { "title": "DIFF", "windowTitle": "Diff", - "merge": "Merge…" + "merge": "Merge…", + "filesHeader": "Files", + "binary": "Binary file — no text diff", + "empty": "No content" }, "worktree": { "title": "Worktree" diff --git a/src/ClaudeDo.Ui/Design/IslandStyles.axaml b/src/ClaudeDo.Ui/Design/IslandStyles.axaml index 2dbfe30..d4737d3 100644 --- a/src/ClaudeDo.Ui/Design/IslandStyles.axaml +++ b/src/ClaudeDo.Ui/Design/IslandStyles.axaml @@ -574,6 +574,13 @@ + + + diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index fdefcc9..480d17f 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -165,6 +165,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable { OnPropertyChanged(nameof(HasChildOutcomes)); OnPropertyChanged(nameof(ShowMergeSection)); + NotifyAttention(); // The Session tab is only visible when it has outcomes; if it just // emptied while selected, fall back to Output so the body isn't blank. @@ -354,7 +355,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable [ObservableProperty] private string? _worktreePath; [ObservableProperty] private string? _worktreeBaseCommit; + [ObservableProperty] private string? _worktreeHeadCommit; [ObservableProperty] private string? _worktreeStateLabel; + // Repo working dir of the selected task's list — used to diff a merged task's + // commit range after its worktree directory is gone. + private string? _listWorkingDir; [ObservableProperty] private string? _branchLine; [ObservableProperty] private int _turns; [ObservableProperty] private int _tokens; @@ -388,6 +393,27 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable public ObservableCollection ChildOutcomes { get; } = new(); public bool HasChildOutcomes => ChildOutcomes.Count > 0; + // Children that need the user's attention before the parent can be approved: + // failed, cancelled, still awaiting their own review, or that reported roadblocks. + // The parent deliberately stays in WaitingForChildren until these are resolved; + // this surfaces a flag so the roadblock is visible on the parent. + public int ChildrenNeedingAttention => ChildOutcomes.Count(c => + c.Status == ClaudeDo.Data.Models.TaskStatus.Failed + || c.Status == ClaudeDo.Data.Models.TaskStatus.Cancelled + || c.Status == ClaudeDo.Data.Models.TaskStatus.WaitingForReview + || c.RoadblockCount > 0); + public bool HasChildrenNeedingAttention => ChildrenNeedingAttention > 0; + public string ChildrenAttentionText => ChildrenNeedingAttention == 1 + ? "1 child needs attention" + : $"{ChildrenNeedingAttention} children need attention"; + + private void NotifyAttention() + { + OnPropertyChanged(nameof(ChildrenNeedingAttention)); + OnPropertyChanged(nameof(HasChildrenNeedingAttention)); + OnPropertyChanged(nameof(ChildrenAttentionText)); + } + [ObservableProperty] private string _newSubtaskTitle = ""; // Planning merge controls @@ -840,6 +866,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable EditableDescription = ""; Model = null; WorktreePath = null; + WorktreeHeadCommit = null; + _listWorkingDir = null; WorktreeStateLabel = null; BranchLine = null; DiffAdditions = 0; @@ -876,6 +904,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable var entity = await ctx.Tasks .AsNoTracking() .Include(t => t.Worktree) + .Include(t => t.List) .FirstOrDefaultAsync(t => t.Id == row.Id, ct); ct.ThrowIfCancellationRequested(); if (entity == null) return; @@ -885,8 +914,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable try { EditableDescription = entity.Description ?? ""; } finally { _suppressDescSave = false; } Model = entity.Model; + _listWorkingDir = entity.List?.WorkingDir; WorktreePath = entity.Worktree?.Path; WorktreeBaseCommit = entity.Worktree?.BaseCommit; + WorktreeHeadCommit = entity.Worktree?.HeadCommit; WorktreeStateLabel = entity.Worktree?.State.ToString(); BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null; var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat); @@ -914,13 +945,12 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable Subtasks.Add(new SubtaskRowViewModel { Id = s.Id, Title = s.Title, Done = s.Completed }); if (entity.PlanningPhase != ClaudeDo.Data.Models.PlanningPhase.None) - { await LoadPlanningChildrenAsync(row.Id, ct); - } - else - { - await LoadChildOutcomesAsync(row.Id, ct); - } + // Surface every parent's children — planning or improvement — in the + // Session tab with their live status + roadblock count. This is what + // makes the Session tab appear for planning parents and lets a child's + // roadblock register on the parent. + await LoadChildOutcomesAsync(row.Id, ct); if (entity.Worktree != null && entity.PlanningPhase == ClaudeDo.Data.Models.PlanningPhase.None @@ -1125,6 +1155,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable row.RoadblockCount = child.RoadblockCount; row.WorktreeState = child.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active; ReviewCombinedDiffCommand.NotifyCanExecuteChanged(); + NotifyAttention(); } catch { /* best-effort */ } } @@ -1148,11 +1179,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable var entity = await ctx.Tasks .AsNoTracking() .Include(t => t.Worktree) + .Include(t => t.List) .FirstOrDefaultAsync(t => t.Id == taskId); if (entity == null || Task?.Id != taskId) return; + _listWorkingDir = entity.List?.WorkingDir; WorktreePath = entity.Worktree?.Path; WorktreeBaseCommit = entity.Worktree?.BaseCommit; + WorktreeHeadCommit = entity.Worktree?.HeadCommit; WorktreeStateLabel = entity.Worktree?.State.ToString(); BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null; AgentState = StatusToStateKey(entity.Status); @@ -1190,21 +1224,53 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable [RelayCommand(CanExecute = nameof(CanOpenDiff))] private async System.Threading.Tasks.Task OpenDiffAsync() { - if (WorktreePath == null || ShowDiffModal == null) return; - var diffVm = new DiffModalViewModel(_services.GetRequiredService()) + if (ShowDiffModal == null) return; + var git = _services.GetRequiredService(); + + // Active worktree on disk → diff the worktree live (and allow merging from it). + var hasLiveWorktree = + WorktreePath != null + && WorktreeStateLabel == "Active" + && System.IO.Directory.Exists(WorktreePath); + + DiffModalViewModel diffVm; + if (hasLiveWorktree) { - WorktreePath = WorktreePath, - BaseRef = WorktreeBaseCommit, - TaskId = Task?.Id, - TaskTitle = Task?.Title ?? "", - ShowMergeModal = ShowMergeModal, - ResolveMergeVm = () => _services.GetRequiredService(), - }; + diffVm = new DiffModalViewModel(git) + { + WorktreePath = WorktreePath!, + BaseRef = WorktreeBaseCommit, + TaskId = Task?.Id, + TaskTitle = Task?.Title ?? "", + ShowMergeModal = ShowMergeModal, + ResolveMergeVm = () => _services.GetRequiredService(), + }; + } + else if (CanDiffMergedRange) + { + // Worktree is gone (merged/discarded) but the commits survive on the + // target branch — diff the captured base..head range in the repo. No + // merge action: the work is already integrated. + diffVm = new DiffModalViewModel(git) + { + WorktreePath = _listWorkingDir!, + BaseRef = WorktreeBaseCommit, + HeadCommit = WorktreeHeadCommit, + FromCommitRange = true, + TaskId = Task?.Id, + TaskTitle = Task?.Title ?? "", + }; + } + else return; + await diffVm.LoadAsync(); await ShowDiffModal(diffVm); } - private bool CanOpenDiff() => WorktreePath != null; + private bool CanDiffMergedRange => + WorktreeBaseCommit != null && WorktreeHeadCommit != null && _listWorkingDir != null; + + private bool CanOpenDiff() => WorktreePath != null || CanDiffMergedRange; [RelayCommand(CanExecute = nameof(CanOpenWorktree))] private void OpenWorktree() @@ -1235,6 +1301,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable NotifySessionSections(); } + partial void OnWorktreeHeadCommitChanged(string? value) => + OpenDiffCommand.NotifyCanExecuteChanged(); + partial void OnTaskChanged(TaskRowViewModel? value) => NotifySessionSections(); [RelayCommand] @@ -1473,7 +1542,13 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable } // hasChildren: conflicts arrive via PlanningMergeConflictEvent → conflict dialog } - catch { /* stale review action; broadcast reconciles */ } + catch (Exception ex) + { + // A real failure (e.g. a child still needs attention, so the unit can't + // be approved yet) must not vanish — tell the user why nothing happened. + if (ShowErrorAsync != null) + await ShowErrorAsync(ex.Message); + } } [RelayCommand] diff --git a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs index 31e78af..c25b2c3 100644 --- a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs @@ -154,21 +154,23 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable if (ShowConflictDialog == null || _dbFactory == null) return; string subtaskTitle = subtaskId; - string worktreePath = System.Environment.CurrentDirectory; + // The conflict lives in the list's working dir (the repo being merged into), + // not the subtask worktree. VS Code must open this folder to show the merge UI. + string repoDirectory = System.Environment.CurrentDirectory; string targetBranch = Worker?.LastApproveTarget ?? "main"; try { await using var ctx = await _dbFactory.CreateDbContextAsync(); var entity = await ctx.Tasks - .Include(t => t.Worktree) + .Include(t => t.List) .AsNoTracking() .FirstOrDefaultAsync(t => t.Id == subtaskId); if (entity != null) { subtaskTitle = entity.Title; - if (entity.Worktree?.Path is { } p) - worktreePath = p; + if (entity.List?.WorkingDir is { } dir && !string.IsNullOrWhiteSpace(dir)) + repoDirectory = dir; } } catch { /* Non-fatal: fall back to subtaskId and cwd */ } @@ -179,7 +181,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable subtaskTitle, targetBranch, conflictedFiles, - worktreePath); + repoDirectory); await ShowConflictDialog(vm); } diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs index d8f1129..c92caaf 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs @@ -8,6 +8,8 @@ namespace ClaudeDo.Ui.ViewModels.Modals; public enum DiffLineKind { Add, Del, Ctx, File } +public enum DiffFileStatus { Modified, Added, Deleted, Renamed } + public sealed class DiffLineViewModel { public required DiffLineKind Kind { get; init; } @@ -32,10 +34,27 @@ public sealed class DiffLineViewModel public sealed class DiffFileViewModel { - public required string Path { get; init; } + public required string Path { get; set; } + public string? OldPath { get; set; } + public DiffFileStatus Status { get; set; } = DiffFileStatus.Modified; + public bool IsBinary { get; set; } public int Additions { get; set; } public int Deletions { get; set; } public ObservableCollection Lines { get; } = new(); + + /// Single-letter badge for the file's change kind (A/M/D/R). + public string StatusCode => Status switch + { + DiffFileStatus.Added => "A", + DiffFileStatus.Deleted => "D", + DiffFileStatus.Renamed => "R", + _ => "M", + }; + + public bool HasLines => Lines.Count > 0; + + /// A text file that produced no diff hunks (e.g. a newly added empty file). + public bool IsEmptyContent => !IsBinary && Lines.Count == 0; } public sealed partial class DiffModalViewModel : ViewModelBase @@ -44,6 +63,11 @@ public sealed partial class DiffModalViewModel : ViewModelBase public required string WorktreePath { get; init; } public string? BaseRef { get; init; } + /// When set together with , the diff is computed as + /// BaseRef..HeadCommit inside (used as the repo + /// dir) — lets a merged task's diff be viewed after its worktree is gone. + public string? HeadCommit { get; init; } + public bool FromCommitRange { get; init; } public string? TaskId { get; init; } public string TaskTitle { get; init; } = ""; public Func? ShowMergeModal { get; set; } @@ -77,6 +101,8 @@ public sealed partial class DiffModalViewModel : ViewModelBase var vm = ResolveMergeVm(); await vm.InitializeAsync(TaskId, TaskTitle); await ShowMergeModal(vm); + // The diff is stale once the worktree has been merged away — close it too. + if (vm.Merged) CloseAction?.Invoke(); } public async Task LoadAsync(CancellationToken ct = default) @@ -87,9 +113,11 @@ public sealed partial class DiffModalViewModel : ViewModelBase string raw; try { - raw = BaseRef is not null - ? await _git.GetBranchDiffAsync(WorktreePath, BaseRef, ct) - : await _git.GetDiffAsync(WorktreePath, ct); + raw = FromCommitRange && BaseRef is not null && HeadCommit is not null + ? await _git.GetCommitRangeDiffAsync(WorktreePath, BaseRef, HeadCommit, ct) + : BaseRef is not null + ? await _git.GetBranchDiffAsync(WorktreePath, BaseRef, ct) + : await _git.GetDiffAsync(WorktreePath, ct); } catch (Exception ex) { diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs index 68ae36a..fb3f1a6 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs @@ -28,6 +28,10 @@ public sealed partial class MergeModalViewModel : ViewModelBase public Action? CloseAction { get; set; } + /// True once a merge has succeeded — lets the caller (e.g. the diff window) + /// close itself after this modal closes. + public bool Merged { get; private set; } + public MergeModalViewModel(WorkerClient worker) { _worker = worker; @@ -80,6 +84,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase switch (result.Status) { case "merged": + Merged = true; SuccessMessage = result.ErrorMessage is not null ? $"Merged with warning: {result.ErrorMessage}" : Loc.T("vm.merge.merged"); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/UnifiedDiffParser.cs b/src/ClaudeDo.Ui/ViewModels/Modals/UnifiedDiffParser.cs index b1e216e..21f6965 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/UnifiedDiffParser.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/UnifiedDiffParser.cs @@ -27,6 +27,36 @@ public static class UnifiedDiffParser if (current == null) continue; + // File-level metadata that carries the change kind. + if (line.StartsWith("new file", StringComparison.Ordinal)) + { + current.Status = DiffFileStatus.Added; + continue; + } + if (line.StartsWith("deleted file", StringComparison.Ordinal)) + { + current.Status = DiffFileStatus.Deleted; + continue; + } + if (line.StartsWith("rename from ", StringComparison.Ordinal)) + { + current.Status = DiffFileStatus.Renamed; + current.OldPath = line["rename from ".Length..]; + continue; + } + if (line.StartsWith("rename to ", StringComparison.Ordinal)) + { + current.Status = DiffFileStatus.Renamed; + current.Path = line["rename to ".Length..]; + continue; + } + if (line.StartsWith("Binary files", StringComparison.Ordinal) || + line.StartsWith("GIT binary patch", StringComparison.Ordinal)) + { + current.IsBinary = true; + continue; + } + if (line.StartsWith("@@ ", StringComparison.Ordinal)) { // e.g. "@@ -10,7 +10,9 @@" @@ -34,13 +64,15 @@ public static class UnifiedDiffParser continue; } - // Skip diff metadata lines + // Skip remaining diff metadata lines if (line.StartsWith("--- ", StringComparison.Ordinal) || line.StartsWith("+++ ", StringComparison.Ordinal) || line.StartsWith("index ", StringComparison.Ordinal) || - line.StartsWith("new file", StringComparison.Ordinal) || - line.StartsWith("deleted file", StringComparison.Ordinal) || - line.StartsWith("Binary ", StringComparison.Ordinal)) + line.StartsWith("old mode", StringComparison.Ordinal) || + line.StartsWith("new mode", StringComparison.Ordinal) || + line.StartsWith("similarity index", StringComparison.Ordinal) || + line.StartsWith("copy from", StringComparison.Ordinal) || + line.StartsWith("copy to", StringComparison.Ordinal)) continue; if (line.StartsWith('+')) diff --git a/src/ClaudeDo.Ui/ViewModels/Planning/ConflictResolutionViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Planning/ConflictResolutionViewModel.cs index 7b3023b..08ea33e 100644 --- a/src/ClaudeDo.Ui/ViewModels/Planning/ConflictResolutionViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Planning/ConflictResolutionViewModel.cs @@ -10,7 +10,10 @@ public sealed partial class ConflictResolutionViewModel : ObservableObject { private readonly IWorkerClient _worker; private readonly string _planningTaskId; - private readonly string _worktreePath; + // The repository directory that is currently mid-merge (the list's working dir), + // NOT the subtask worktree. Opening this folder is what makes VS Code show its + // merge-conflict resolution UI. + private readonly string _repoDirectory; public string SubtaskTitle { get; } public string TargetBranch { get; } @@ -29,11 +32,11 @@ public sealed partial class ConflictResolutionViewModel : ObservableObject string subtaskTitle, string targetBranch, IReadOnlyList conflictedFiles, - string worktreePath) + string repoDirectory) { _worker = worker; _planningTaskId = planningTaskId; - _worktreePath = worktreePath; + _repoDirectory = repoDirectory; SubtaskTitle = subtaskTitle; TargetBranch = targetBranch; ConflictedFiles = conflictedFiles; @@ -44,12 +47,13 @@ public sealed partial class ConflictResolutionViewModel : ObservableObject { try { - var args = string.Join(" ", ConflictedFiles.Select(f => $"\"{f}\"")); + // Open the folder that is mid-merge so VS Code shows the Source Control + // merge-conflict UI for every conflicted file. Opening individual files + // gives only a plain editor with no conflict resolution affordances. Process.Start(new ProcessStartInfo { FileName = "code", - Arguments = args, - WorkingDirectory = _worktreePath, + Arguments = $"\"{_repoDirectory}\"", UseShellExecute = true, }); VsCodeError = null; diff --git a/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml b/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml index 0405baf..849ddfb 100644 --- a/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml @@ -313,6 +313,22 @@ + + + + + + + + diff --git a/src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml index a12606a..7a7cb7f 100644 --- a/src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml @@ -26,51 +26,100 @@ - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ClaudeDo.Worker/CLAUDE.md b/src/ClaudeDo.Worker/CLAUDE.md index d3fb60e..4aa86af 100644 --- a/src/ClaudeDo.Worker/CLAUDE.md +++ b/src/ClaudeDo.Worker/CLAUDE.md @@ -102,8 +102,10 @@ approve is the single review+merge action. Review transitions live in `TaskState `PlanningSessionManager.FinalizeAsync` is the single path: 1. `_state.FinalizePlanningAsync(parent)` flips parent `PlanningPhase` to `Finalized` and sets `Status` to `WaitingForChildren` (or `WaitingForReview` if the parent has no children). -2. `PlanningChainCoordinator.SetupChainAsync` attaches the `agent` tag to every child, enqueues child[0], and `BlockOn`s child[i] → child[i-1]. -3. The first child is woken automatically; successors unblock as their predecessor reaches a terminal state via `OnChildFinishedAsync`. +2. `PlanningChainCoordinator.SetupChainAsync(parent, enqueue: false)` establishes the blocked-by chain (`BlockOn`s child[i] → child[i-1]) but **leaves children `Idle`** — finalize never auto-queues. Queueing is a deliberate user action: `QueuePlanAsync` (hub `QueuePlanningSubtasksAsync`, the "Queue plan" button) calls `SetupChainAsync(parent, enqueue: true)`, which sets every non-terminal child `Queued` and re-applies the chain. +3. Once queued, the first child is woken automatically; successors unblock as their predecessor reaches a terminal state via `OnChildFinishedAsync`. + +A child that hits a roadblock (fails, or reports `CLAUDEDO_BLOCKED` roadblocks) does **not** advance the parent — the parent stays in `WaitingForChildren` until every child is terminal. The UI surfaces blocked children on the parent's Session tab (`ChildOutcomes` + a "children need attention" band) so the roadblock is visible without forcing a transition. `TaskRepository.FinalizePlanningAsync` no longer exists. The `Mark*Async` repository helpers are `internal` — only `TaskStateService` calls them. diff --git a/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj b/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj index 45c64d3..aac05c3 100644 --- a/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj +++ b/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj @@ -27,6 +27,7 @@ WinExe enable enable + ClaudeTaskWorker.ico diff --git a/src/ClaudeDo.Worker/ClaudeTaskWorker.ico b/src/ClaudeDo.Worker/ClaudeTaskWorker.ico new file mode 100644 index 0000000000000000000000000000000000000000..27ae8ae89cb48de762c60a920f36ff765d4fd723 GIT binary patch literal 59764 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 Done on a successful merge. - return await MergeAsync(taskId, target, removeWorktree: false, $"Merge {wt.BranchName}", ct); + // Remove the worktree on approve (matching the unit-merge path) so merged + // worktrees don't pile up; the merge commit on the target branch is the record. + return await MergeAsync(taskId, target, removeWorktree: true, $"Merge {wt.BranchName}", ct); } private static MergeResult Blocked(string reason) => diff --git a/src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs b/src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs index 7e87de8..1275411 100644 --- a/src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs +++ b/src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs @@ -19,17 +19,21 @@ public sealed class PlanningChainCoordinator _state = state; } - // Sets up a sequential queue chain over a planning parent's children. - // - First non-terminal child gets Status=Queued, BlockedByTaskId=null. - // - Each subsequent non-terminal child gets Status=Queued + BlockedByTaskId=, + // Sets up a sequential chain over a planning parent's children. + // - First non-terminal child gets BlockedByTaskId=null. + // - Each subsequent non-terminal child gets BlockedByTaskId=, // so the picker skips them until the predecessor finishes. + // - When enqueue is true, each non-terminal child is also set to Status=Queued + // (the user-driven "Queue plan"). When false (finalize), children are left + // Idle and only the blocked-by links are established, so nothing runs until + // the user queues the plan. // - Terminal children (Done/Failed/Cancelled) are left untouched; they are // skipped when computing predecessors so a re-run on a partially executed // chain leaves history alone but still reshapes the tail. // - Running children abort the operation — the chain cannot be reshaped while // one of its members is mid-flight. // Returns the number of children placed in the chain. - internal async Task SetupChainAsync(string parentTaskId, CancellationToken ct = default) + internal async Task SetupChainAsync(string parentTaskId, bool enqueue, CancellationToken ct = default) { await using var ctx = await _dbFactory.CreateDbContextAsync(ct); var parent = await ctx.Tasks.FirstOrDefaultAsync(t => t.Id == parentTaskId, ct) @@ -56,7 +60,8 @@ public sealed class PlanningChainCoordinator var state = _state(); for (int i = 0; i < sequenceable.Count; i++) { - await state.EnqueueAsync(sequenceable[i].Id, ct); + if (enqueue) + await state.EnqueueAsync(sequenceable[i].Id, ct); if (i == 0) await state.UnblockAsync(sequenceable[i].Id, ct); else @@ -81,7 +86,7 @@ public sealed class PlanningChainCoordinator if (phase != PlanningPhase.Finalized) throw new InvalidOperationException("Plan must be finalized before it can be queued."); - return await SetupChainAsync(parentTaskId, ct); + return await SetupChainAsync(parentTaskId, enqueue: true, ct); } public async Task OnChildFinishedAsync( diff --git a/src/ClaudeDo.Worker/Planning/PlanningMcpService.cs b/src/ClaudeDo.Worker/Planning/PlanningMcpService.cs index 7d57df8..c694aee 100644 --- a/src/ClaudeDo.Worker/Planning/PlanningMcpService.cs +++ b/src/ClaudeDo.Worker/Planning/PlanningMcpService.cs @@ -135,7 +135,7 @@ public sealed class PlanningMcpService await BroadcastTaskUpdatedAsync(ctx.ParentTaskId, cancellationToken); } - [McpServerTool, Description("Finalize the planning session, promoting all draft child tasks to queued or manual status.")] + [McpServerTool, Description("Finalize the planning session. Child tasks are left idle and chain-linked (each blocked by its predecessor); they are NOT queued automatically — the user queues the plan from the app when ready. The queueAgentTasks argument is accepted for compatibility but ignored.")] public async Task Finalize( bool queueAgentTasks, CancellationToken cancellationToken) @@ -149,8 +149,10 @@ public sealed class PlanningMcpService var children = await _tasks.GetChildrenAsync(ctx.ParentTaskId, cancellationToken); int count = children.Count; - if (queueAgentTasks && children.Count > 0) - count = await _chain.SetupChainAsync(ctx.ParentTaskId, cancellationToken); + // Establish the blocked-by chain but leave children Idle; queueing is a + // deliberate user action ("Queue plan"), never an automatic finalize step. + if (children.Count > 0) + count = await _chain.SetupChainAsync(ctx.ParentTaskId, enqueue: false, cancellationToken); foreach (var c in children) await BroadcastTaskUpdatedAsync(c.Id, cancellationToken); diff --git a/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs b/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs index d4c0042..b40196b 100644 --- a/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs +++ b/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs @@ -199,6 +199,10 @@ public sealed class PlanningMergeOrchestrator parent.FinishedAt = DateTime.UtcNow; await ctx.SaveChangesAsync(ct); + // Surface the Done transition to the UI. Without this the parent row stays + // visibly stuck in WaitingForReview even though the unit merge completed. + await _broadcaster.TaskUpdated(parentTaskId); + // Only planning builds an integration branch via the aggregator; skip cleanup otherwise. if (isPlanning) { diff --git a/src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs b/src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs index e89ec95..7a56ca3 100644 --- a/src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs +++ b/src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs @@ -209,12 +209,13 @@ public sealed class PlanningSessionManager throw new InvalidOperationException( finalizeResult.Reason ?? $"Could not finalize planning for task {taskId}."); - int count = 0; + // Establish the blocked-by chain but leave children Idle; queueing is a + // deliberate user action ("Queue plan"), never an automatic finalize step. + // queueAgentTasks is accepted for compatibility but no longer auto-queues. var children = await tasks.GetChildrenAsync(taskId, ct); - if (queueAgentTasks && children.Count > 0) - count = await _chain.SetupChainAsync(taskId, ct); - else - count = children.Count; + int count = children.Count; + if (children.Count > 0) + count = await _chain.SetupChainAsync(taskId, enqueue: false, ct); // Best-effort cleanup — don't block finalization on git state. await TryCleanupWorktreeAsync(taskId, lists, settings, ct); diff --git a/src/ClaudeDo.Worker/Refine/RefineRunner.cs b/src/ClaudeDo.Worker/Refine/RefineRunner.cs index 14fdd1d..1439532 100644 --- a/src/ClaudeDo.Worker/Refine/RefineRunner.cs +++ b/src/ClaudeDo.Worker/Refine/RefineRunner.cs @@ -10,7 +10,7 @@ namespace ClaudeDo.Worker.Refine; public sealed class RefineRunner : IRefineRunner { private static readonly TimeSpan RunTimeout = TimeSpan.FromMinutes(5); - private const int MaxTurns = 25; + private const int MaxTurns = 5; private readonly IClaudeProcess _claude; private readonly IDbContextFactory _dbFactory; diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolutionViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolutionViewModelTests.cs index 01fb7fd..f8cd25c 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolutionViewModelTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolutionViewModelTests.cs @@ -36,7 +36,7 @@ public class ConflictResolutionViewModelTests subtaskTitle: "My subtask", targetBranch: "main", conflictedFiles: new[] { "src/Foo.cs", "src/Bar.cs" }, - worktreePath: "C:/worktrees/plan-1"); + repoDirectory: "C:/repos/plan-1"); // ------------------------------------------------------------------ tests diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/UnifiedDiffParserTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/UnifiedDiffParserTests.cs new file mode 100644 index 0000000..8a63803 --- /dev/null +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/UnifiedDiffParserTests.cs @@ -0,0 +1,109 @@ +using System.Linq; +using ClaudeDo.Ui.ViewModels.Modals; + +namespace ClaudeDo.Ui.Tests.ViewModels; + +public class UnifiedDiffParserTests +{ + [Fact] + public void Modified_file_counts_additions_and_deletions() + { + const string raw = + "diff --git a/src/Foo.cs b/src/Foo.cs\n" + + "index 111..222 100644\n" + + "--- a/src/Foo.cs\n" + + "+++ b/src/Foo.cs\n" + + "@@ -1,3 +1,3 @@\n" + + " ctx\n" + + "-old\n" + + "+new\n"; + + var file = Assert.Single(UnifiedDiffParser.Parse(raw)); + Assert.Equal("src/Foo.cs", file.Path); + Assert.Equal(DiffFileStatus.Modified, file.Status); + Assert.Equal("M", file.StatusCode); + Assert.Equal(1, file.Additions); + Assert.Equal(1, file.Deletions); + Assert.True(file.HasLines); + Assert.False(file.IsBinary); + } + + [Fact] + public void New_file_is_marked_added() + { + const string raw = + "diff --git a/New.cs b/New.cs\n" + + "new file mode 100644\n" + + "index 000..abc\n" + + "--- /dev/null\n" + + "+++ b/New.cs\n" + + "@@ -0,0 +1,1 @@\n" + + "+hello\n"; + + var file = Assert.Single(UnifiedDiffParser.Parse(raw)); + Assert.Equal(DiffFileStatus.Added, file.Status); + Assert.Equal("A", file.StatusCode); + } + + [Fact] + public void Deleted_file_is_marked_deleted() + { + const string raw = + "diff --git a/Gone.cs b/Gone.cs\n" + + "deleted file mode 100644\n" + + "index abc..000\n" + + "--- a/Gone.cs\n" + + "+++ /dev/null\n" + + "@@ -1,1 +0,0 @@\n" + + "-bye\n"; + + var file = Assert.Single(UnifiedDiffParser.Parse(raw)); + Assert.Equal(DiffFileStatus.Deleted, file.Status); + Assert.Equal("D", file.StatusCode); + } + + [Fact] + public void Rename_captures_old_and_new_path() + { + const string raw = + "diff --git a/Old.cs b/New.cs\n" + + "similarity index 100%\n" + + "rename from Old.cs\n" + + "rename to New.cs\n"; + + var file = Assert.Single(UnifiedDiffParser.Parse(raw)); + Assert.Equal(DiffFileStatus.Renamed, file.Status); + Assert.Equal("R", file.StatusCode); + Assert.Equal("Old.cs", file.OldPath); + Assert.Equal("New.cs", file.Path); + } + + [Fact] + public void Binary_file_is_flagged_with_no_lines() + { + const string raw = + "diff --git a/img.png b/img.png\n" + + "new file mode 100644\n" + + "index 000..abc\n" + + "Binary files /dev/null and b/img.png differ\n"; + + var file = Assert.Single(UnifiedDiffParser.Parse(raw)); + Assert.True(file.IsBinary); + Assert.False(file.HasLines); + Assert.False(file.IsEmptyContent); + } + + [Fact] + public void Empty_new_file_reports_empty_content() + { + const string raw = + "diff --git a/Empty.txt b/Empty.txt\n" + + "new file mode 100644\n" + + "index 000..000\n"; + + var file = Assert.Single(UnifiedDiffParser.Parse(raw)); + Assert.Equal(DiffFileStatus.Added, file.Status); + Assert.False(file.HasLines); + Assert.True(file.IsEmptyContent); + } +} diff --git a/tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs b/tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs index d2b7239..bd568e6 100644 --- a/tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs @@ -75,7 +75,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable { await SeedPlanningFamilyAsync("P", 3); - var count = await _sut.SetupChainAsync("P", default); + var count = await _sut.SetupChainAsync("P", enqueue: true, default); Assert.Equal(3, count); var kids = await GetChildrenAsync("P"); @@ -92,7 +92,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable { await SeedPlanningFamilyAsync("P", 2, childStatus: TaskStatus.Idle); - var count = await _sut.SetupChainAsync("P", default); + var count = await _sut.SetupChainAsync("P", enqueue: true, default); Assert.Equal(2, count); } @@ -101,7 +101,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable public async Task OnChildDone_UnblocksTheSuccessor() { await SeedPlanningFamilyAsync("P", 3); - await _sut.SetupChainAsync("P", default); + await _sut.SetupChainAsync("P", enqueue: true, default); // Mark the head child Done before announcing. await using (var ctx = _factory.CreateDbContext()) @@ -128,7 +128,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable public async Task OnChildFailed_CancelsPendingSuccessors_ChainIsNotWedged() { await SeedPlanningFamilyAsync("P", 3); - await _sut.SetupChainAsync("P", default); + await _sut.SetupChainAsync("P", enqueue: true, default); await using (var ctx = _factory.CreateDbContext()) { @@ -153,7 +153,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable { // Chain: c0 → c1 → c2 → c3. c1 fails mid-chain; c2 and c3 must be cancelled. await SeedPlanningFamilyAsync("P", 4); - await _sut.SetupChainAsync("P", default); + await _sut.SetupChainAsync("P", enqueue: true, default); // Mark c0 Done so c1 was unblocked; c1 ran and failed. await using (var ctx = _factory.CreateDbContext()) @@ -182,7 +182,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable public async Task OnChildDone_LastChild_ReturnsNull() { await SeedPlanningFamilyAsync("P", 2); - await _sut.SetupChainAsync("P", default); + await _sut.SetupChainAsync("P", enqueue: true, default); await using (var ctx = _factory.CreateDbContext()) { @@ -208,7 +208,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable } await Assert.ThrowsAsync( - () => _sut.SetupChainAsync("P", default)); + () => _sut.SetupChainAsync("P", enqueue: true, default)); } [Fact] @@ -230,7 +230,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable await ctx.SaveChangesAsync(); } - var count = await _sut.SetupChainAsync("P", default); + var count = await _sut.SetupChainAsync("P", enqueue: true, default); Assert.Equal(4, count); var kids = await GetChildrenAsync("P"); @@ -257,7 +257,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable await ctx.SaveChangesAsync(); } - var count = await _sut.SetupChainAsync("P", default); + var count = await _sut.SetupChainAsync("P", enqueue: true, default); // Only the two non-terminal tail children get chained. Assert.Equal(2, count); @@ -303,4 +303,21 @@ public sealed class PlanningChainCoordinatorTests : IDisposable var kids = await GetChildrenAsync("P"); Assert.All(kids, k => Assert.Equal(TaskStatus.Idle, k.Status)); } + + [Fact] + public async Task SetupChain_LinkOnly_LeavesChildrenIdle_ButEstablishesChain() + { + // Finalize path: children must stay Idle (nothing auto-queues) but still + // get the blocked-by chain so a later "Queue plan" runs them in order. + await SeedPlanningFamilyAsync("P", 3); + + var count = await _sut.SetupChainAsync("P", enqueue: false, default); + + Assert.Equal(3, count); + var kids = await GetChildrenAsync("P"); + Assert.All(kids, k => Assert.Equal(TaskStatus.Idle, k.Status)); + Assert.Null(kids[0].BlockedByTaskId); + Assert.Equal(kids[0].Id, kids[1].BlockedByTaskId); + Assert.Equal(kids[1].Id, kids[2].BlockedByTaskId); + } } diff --git a/tests/ClaudeDo.Worker.Tests/Planning/PlanningEndToEndTests.cs b/tests/ClaudeDo.Worker.Tests/Planning/PlanningEndToEndTests.cs index d99f5f6..eaad850 100644 --- a/tests/ClaudeDo.Worker.Tests/Planning/PlanningEndToEndTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Planning/PlanningEndToEndTests.cs @@ -121,19 +121,20 @@ public sealed class PlanningEndToEndTests : IDisposable Assert.Equal(PlanningPhase.Finalized, reload!.PlanningPhase); var kids = await _tasks.GetChildrenAsync(parent.Id); - // SetupChainAsync auto-attaches agent tag and queues all children; - // the first one is unblocked, the rest are BlockedBy their predecessor. - Assert.Equal(TaskStatus.Queued, kids[0].Status); + // Finalize no longer auto-queues. Children stay Idle and chain-linked + // (head unblocked, rest BlockedBy their predecessor) until the user + // explicitly queues the plan. + Assert.Equal(TaskStatus.Idle, kids[0].Status); Assert.Null(kids[0].BlockedByTaskId); - Assert.Equal(TaskStatus.Queued, kids[1].Status); + Assert.Equal(TaskStatus.Idle, kids[1].Status); Assert.Equal(kids[0].Id, kids[1].BlockedByTaskId); } - // Regression: original bug was "queue never picks up planning tasks". After Finalize - // with queueAgentTasks=true, the first child must be claimable by the queue picker - // automatically — without anyone calling WakeQueue() manually. + // Regression: original bug was "queue never picks up planning tasks". Finalize + // leaves children Idle; queueing the plan (the explicit user gate) must make the + // first child claimable by the picker automatically — without a manual WakeQueue(). [Fact] - public async Task FinalizeAsync_FirstChildIsClaimedByPicker_WithinDeadline() + public async Task QueuePlanAfterFinalize_FirstChildIsClaimedByPicker_WithinDeadline() { var listId = Guid.NewGuid().ToString(); var wd = Path.Combine(Path.GetTempPath(), $"cd_e2e_wd_{Guid.NewGuid():N}"); @@ -160,12 +161,18 @@ public sealed class PlanningEndToEndTests : IDisposable var kidsBefore = await _tasks.GetChildrenAsync(parent.Id); var firstChildId = kidsBefore[0].Id; - var wakesBefore = _built.WakeCount(); await _manager.FinalizeAsync(parent.Id, queueAgentTasks: true, CancellationToken.None); - // The picker should pick the first child immediately. Auto-wake fires inside - // _state.EnqueueAsync; we don't need a manual WakeQueue() for the bug to be fixed. + // Finalize leaves every child Idle — nothing is claimable yet. + var afterFinalize = await _tasks.GetChildrenAsync(parent.Id); + Assert.All(afterFinalize, k => Assert.Equal(TaskStatus.Idle, k.Status)); + + // Queueing the plan is the gate that enqueues + auto-wakes. The picker should + // then pick the first child immediately, without a manual WakeQueue(). + var wakesBefore = _built.WakeCount(); + await _built.Chain.QueuePlanAsync(parent.Id, CancellationToken.None); + var picker = new QueuePicker(_db.CreateFactory()); TaskEntity? claimed = null; @@ -181,6 +188,6 @@ public sealed class PlanningEndToEndTests : IDisposable Assert.Equal(firstChildId, claimed!.Id); Assert.Equal(TaskStatus.Running, claimed.Status); Assert.True(_built.WakeCount() > wakesBefore, - "TaskStateService.EnqueueAsync should auto-wake the queue."); + "QueuePlanAsync → EnqueueAsync should auto-wake the queue."); } } diff --git a/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs b/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs index 1444249..3b6dbe5 100644 --- a/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs @@ -531,6 +531,8 @@ public class TaskMergeServiceTests : IDisposable Assert.Equal(TaskStatus.Done, updated!.Status); var wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(task.Id); Assert.Equal(WorktreeState.Merged, wt!.State); + // Approve removes the worktree so merged worktrees don't pile up. + Assert.False(Directory.Exists(wtCtx.WorktreePath)); } [Fact]