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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<OutputType>WinExe</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ApplicationIcon>ClaudeTaskWorker.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
BIN
src/ClaudeDo.Worker/ClaudeTaskWorker.ico
Normal file
BIN
src/ClaudeDo.Worker/ClaudeTaskWorker.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -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) =>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user