feat(mission-control): give the list handler its own review task
"Let Claude handle it" now creates one ClaudeDo task per run to host the ConPTY session (Idle/IsManual, never queued) instead of an untracked ad-hoc tile, so the run has a real title, diff, and review outcome. Since the handler merges its own changes straight into the list's working dir, the task never gets a WorktreeEntity; its review range lives as new HandlerBaseCommit/HandlerHeadCommit columns on TaskEntity instead, reusing the existing commit-range diff machinery and keeping it out of the worktrees overview entirely.
This commit is contained in:
@@ -135,6 +135,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
private readonly ISessionSkillRegistry _skillRegistry;
|
||||
private readonly IInteractiveLaunchSpecService? _interactiveLaunchSpec;
|
||||
private readonly WorktreeManager? _worktreeManager;
|
||||
private readonly Data.Git.GitService? _git;
|
||||
|
||||
public WorkerHub(
|
||||
QueueService queue,
|
||||
@@ -163,7 +164,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
ISessionSkillRegistry skillRegistry,
|
||||
LogRingBuffer? logBuffer = null,
|
||||
IInteractiveLaunchSpecService? interactiveLaunchSpec = null,
|
||||
WorktreeManager? worktreeManager = null)
|
||||
WorktreeManager? worktreeManager = null,
|
||||
Data.Git.GitService? git = null)
|
||||
{
|
||||
_queue = queue;
|
||||
_waker = waker;
|
||||
@@ -192,6 +194,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
_logBuffer = logBuffer;
|
||||
_interactiveLaunchSpec = interactiveLaunchSpec;
|
||||
_worktreeManager = worktreeManager;
|
||||
_git = git;
|
||||
}
|
||||
|
||||
// Persistence boundary for the session_skills JSON-array columns (task/list/global).
|
||||
@@ -698,6 +701,19 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
return _interactiveLaunchSpec.BuildForMergeHelperAsync(taskIds, listId, Context.ConnectionAborted);
|
||||
});
|
||||
|
||||
// Creates the ClaudeDo task that owns a list-handler run, before the ConPTY tile opens --
|
||||
// one task per run, never queued (Idle/IsManual). Broadcasts TaskUpdated so it shows up in
|
||||
// the list immediately.
|
||||
public Task<string> CreateMergeHelperTask(string[] taskIds, string listId, string title, string descriptionHeader) => HubGuard(async () =>
|
||||
{
|
||||
if (_interactiveLaunchSpec is null)
|
||||
throw new InvalidOperationException("Interactive launch spec service is not configured.");
|
||||
var taskId = await _interactiveLaunchSpec.CreateMergeHelperTaskAsync(
|
||||
taskIds, listId, title, descriptionHeader, Context.ConnectionAborted);
|
||||
await Clients.All.SendAsync("TaskUpdated", taskId);
|
||||
return taskId;
|
||||
});
|
||||
|
||||
// Starts a planning session (worktree + prompt files + token, task -> Planning) and returns
|
||||
// the launch spec for an embedded ConPTY planning terminal -- the ConPTY replacement for
|
||||
// StartPlanningSessionAsync's external wt window. On any spec-build failure the just-started
|
||||
@@ -730,17 +746,16 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
return _interactiveLaunchSpec.BuildPlanningResume(ctx);
|
||||
});
|
||||
|
||||
// Submits an interactively-worked task for review: commits whatever the ConPTY session left
|
||||
// in the worktree (so there is a diff to merge), then transitions Idle/Failed -> WaitingForReview.
|
||||
// The normal Approve flow then merges it. This is the only path that flips a hand-driven session
|
||||
// into the review pipeline — a ConPTY session never touches task status on its own.
|
||||
// Submits an interactively-worked task for review, then transitions Idle/Failed ->
|
||||
// WaitingForReview. The normal Approve flow then merges it (or, for a worktree-less host
|
||||
// task below, just flips to Done — there's nothing to merge). This is the only path that
|
||||
// flips a hand-driven session into the review pipeline — a ConPTY session never touches
|
||||
// task status on its own.
|
||||
public Task SubmitTaskForReview(string taskId) => HubGuard(async () =>
|
||||
{
|
||||
if (_worktreeManager is null)
|
||||
throw new InvalidOperationException("Worktree manager is not configured.");
|
||||
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, Context.ConnectionAborted)
|
||||
var taskRepo = new TaskRepository(ctx);
|
||||
var task = await taskRepo.GetByIdAsync(taskId, Context.ConnectionAborted)
|
||||
?? throw new KeyNotFoundException();
|
||||
if (task.Status is TaskStatus.Running or TaskStatus.Queued)
|
||||
throw new InvalidOperationException("Can't submit a running or queued task — interrupt it first.");
|
||||
@@ -748,16 +763,41 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
throw new InvalidOperationException("Task is already awaiting review.");
|
||||
|
||||
var worktree = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, Context.ConnectionAborted);
|
||||
if (worktree is null || worktree.State is not (WorktreeState.Active or WorktreeState.Kept))
|
||||
if (worktree is not null)
|
||||
{
|
||||
if (_worktreeManager is null)
|
||||
throw new InvalidOperationException("Worktree manager is not configured.");
|
||||
if (worktree.State is not (WorktreeState.Active or WorktreeState.Kept))
|
||||
throw new InvalidOperationException("This task has no active worktree to submit.");
|
||||
if (!Directory.Exists(worktree.Path))
|
||||
throw new InvalidOperationException("The task's worktree directory no longer exists.");
|
||||
|
||||
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, Context.ConnectionAborted)
|
||||
?? throw new InvalidOperationException("Task list not found.");
|
||||
|
||||
var wtCtx = new WorktreeContext(worktree.Path, worktree.BranchName, worktree.BaseCommit);
|
||||
await _worktreeManager.CommitIfChangedAsync(wtCtx, task, list, Context.ConnectionAborted);
|
||||
}
|
||||
else if (task.HandlerBaseCommit is { Length: > 0 })
|
||||
{
|
||||
// Worktree-less "list handler" host task (Mission Control's "Let Claude handle it"):
|
||||
// the handler commits its own changes straight to the list's working dir, so there
|
||||
// is nothing for us to commit here — just stamp the review range's head commit.
|
||||
if (_git is null)
|
||||
throw new InvalidOperationException("Git service is not configured.");
|
||||
|
||||
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, Context.ConnectionAborted)
|
||||
?? throw new InvalidOperationException("Task list not found.");
|
||||
if (string.IsNullOrEmpty(list.WorkingDir) || !Directory.Exists(list.WorkingDir))
|
||||
throw new InvalidOperationException("The list's working directory no longer exists.");
|
||||
|
||||
var headCommit = await _git.RevParseHeadAsync(list.WorkingDir, Context.ConnectionAborted);
|
||||
await taskRepo.SetHandlerHeadCommitAsync(taskId, headCommit, Context.ConnectionAborted);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("This task has no active worktree to submit.");
|
||||
if (!Directory.Exists(worktree.Path))
|
||||
throw new InvalidOperationException("The task's worktree directory no longer exists.");
|
||||
|
||||
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, Context.ConnectionAborted)
|
||||
?? throw new InvalidOperationException("Task list not found.");
|
||||
|
||||
var wtCtx = new WorktreeContext(worktree.Path, worktree.BranchName, worktree.BaseCommit);
|
||||
await _worktreeManager.CommitIfChangedAsync(wtCtx, task, list, Context.ConnectionAborted);
|
||||
}
|
||||
|
||||
var result = await _state.SubmitInteractiveForReviewAsync(taskId, DateTime.UtcNow, Context.ConnectionAborted);
|
||||
if (!result.Ok)
|
||||
|
||||
Reference in New Issue
Block a user