feat(ui): richer diff viewer + surface child roadblocks on parents
All checks were successful
Changelog / changelog (push) Successful in 1s
Release / release (push) Successful in 38s

- 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) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-09 16:40:59 +02:00
parent c300f8c313
commit f21c65be18
28 changed files with 509 additions and 119 deletions

View File

@@ -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.

View File

@@ -27,6 +27,7 @@
<OutputType>WinExe</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationIcon>ClaudeTaskWorker.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -325,7 +325,9 @@ public sealed class TaskMergeService
: targetBranch;
// MergeAsync transitions the task WaitingForReview -> 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) =>

View File

@@ -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=<predecessor>,
// 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=<predecessor>,
// 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<int> SetupChainAsync(string parentTaskId, CancellationToken ct = default)
internal async Task<int> 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<string?> OnChildFinishedAsync(

View File

@@ -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<int> 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);

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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<ClaudeDoDbContext> _dbFactory;