feat(worker): resume a task's claude session in a terminal
This commit is contained in:
@@ -575,6 +575,39 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
public Task OpenInteractiveTerminalAsync(string taskId) =>
|
||||
_interactive.StartAsync(taskId, Context.ConnectionAborted);
|
||||
|
||||
// Picks up a task's Claude session in a real terminal window (--resume) so the user can
|
||||
// drive it by hand — distinct from OpenInteractiveTerminalAsync (the in-app streaming
|
||||
// session). Only for tasks the worker isn't actively running, with a persisted session
|
||||
// id and a live worktree.
|
||||
public Task ResumeTaskInTerminal(string taskId) => HubGuard(async () =>
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||
|
||||
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, Context.ConnectionAborted)
|
||||
?? throw new KeyNotFoundException();
|
||||
if (task.Status is TaskStatus.Running or TaskStatus.Queued)
|
||||
throw new InvalidOperationException("Can't pick up a running or queued task — interrupt it first.");
|
||||
|
||||
var run = await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId, Context.ConnectionAborted);
|
||||
if (run?.SessionId is not { Length: > 0 } sessionId)
|
||||
throw new InvalidOperationException("This task has no resumable Claude session yet.");
|
||||
|
||||
var worktree = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, Context.ConnectionAborted);
|
||||
if (worktree is null || worktree.State is not (WorktreeState.Active or WorktreeState.Kept))
|
||||
throw new InvalidOperationException("This task has no active worktree to resume in.");
|
||||
if (!Directory.Exists(worktree.Path))
|
||||
throw new InvalidOperationException("The task's worktree directory no longer exists.");
|
||||
|
||||
try
|
||||
{
|
||||
await _launcher.LaunchResumeAsync(worktree.Path, sessionId, Context.ConnectionAborted);
|
||||
}
|
||||
catch (TerminalLaunchException ex)
|
||||
{
|
||||
throw new InvalidOperationException(ex.Message);
|
||||
}
|
||||
});
|
||||
|
||||
public Task SendInteractiveMessage(string taskId, string text) =>
|
||||
_interactive.SendAsync(taskId, text, Context.ConnectionAborted);
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ public interface ITerminalLauncher
|
||||
{
|
||||
Task LaunchPlanningStartAsync(PlanningSessionStartContext ctx, CancellationToken cancellationToken);
|
||||
Task LaunchPlanningResumeAsync(PlanningSessionResumeContext ctx, CancellationToken cancellationToken);
|
||||
|
||||
// Resumes an arbitrary task's Claude session (--resume <id>) in a visible terminal so
|
||||
// the user can pick up the conversation by hand — the "pick up in terminal" action.
|
||||
Task LaunchResumeAsync(string workingDir, string claudeSessionId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class TerminalLaunchException : Exception
|
||||
|
||||
@@ -88,6 +88,26 @@ public sealed class WindowsTerminalLauncher : ITerminalLauncher
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task LaunchResumeAsync(string workingDir, string claudeSessionId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Directory.Exists(workingDir))
|
||||
throw new TerminalLaunchException($"Working directory does not exist: {workingDir}");
|
||||
|
||||
var resolvedWt = ResolveWtOrThrow();
|
||||
var resolvedClaude = ResolveClaudeOrThrow();
|
||||
|
||||
var command = BuildResumeCommand(resolvedClaude, claudeSessionId);
|
||||
|
||||
StartInWindowsTerminal(resolvedWt, workingDir, command, static _ => { });
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Resumes a session by id in default (interactive) permission mode: the user drives
|
||||
// tool approvals in the terminal, unlike planning which pins --permission-mode plan.
|
||||
internal static string BuildResumeCommand(string claudePath, string claudeSessionId) =>
|
||||
BuildPwshCommand(claudePath, new[] { "--resume", claudeSessionId });
|
||||
|
||||
// Builds the PowerShell command that launches an interactive planning session.
|
||||
// Arg order matters: variadic flags (--allowedTools, --add-dir) come first; the
|
||||
// single-line kickoff prompt is positional, so it must follow a single-value flag
|
||||
|
||||
Reference in New Issue
Block a user