feat(ui): add Run interactively action to task context menu
Spawns Windows Terminal in the list working directory running `claude --permission-mode auto` with the task title and description prefilled as the initial prompt. Reuses the planning launcher infrastructure but skips worktree, system prompt, and MCP setup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task<ListConfigDto?> GetListConfigAsync(string listId);
|
Task<ListConfigDto?> GetListConfigAsync(string listId);
|
||||||
Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto);
|
Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto);
|
||||||
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
|
Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default);
|
||||||
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default);
|
Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default);
|
||||||
|
|||||||
@@ -386,6 +386,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task<PlanningSessionResumeInfo> ResumePlanningSessionAsync(string taskId, CancellationToken ct = default)
|
public async Task<PlanningSessionResumeInfo> ResumePlanningSessionAsync(string taskId, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", taskId, ct);
|
=> await _hub.InvokeAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", taskId, ct);
|
||||||
|
|
||||||
|
public async Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default)
|
||||||
|
=> await _hub.InvokeAsync("OpenInteractiveTerminalAsync", taskId, ct);
|
||||||
|
|
||||||
public async Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default)
|
public async Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync("DiscardPlanningSessionAsync", taskId, ct);
|
=> await _hub.InvokeAsync("DiscardPlanningSessionAsync", taskId, ct);
|
||||||
|
|
||||||
|
|||||||
@@ -516,6 +516,15 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task RunInteractivelyAsync(TaskRowViewModel? row)
|
||||||
|
{
|
||||||
|
if (row is null || _worker is null) return;
|
||||||
|
ForegroundHelper.AllowAny();
|
||||||
|
try { await _worker.OpenInteractiveTerminalAsync(row.Id); }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ResumePlanningSessionAsync(TaskRowViewModel? row)
|
private async Task ResumePlanningSessionAsync(TaskRowViewModel? row)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
IsVisible="{Binding IsQueued}"
|
IsVisible="{Binding IsQueued}"
|
||||||
Click="OnRemoveFromQueueClick"/>
|
Click="OnRemoveFromQueueClick"/>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
|
<MenuItem Header="Run interactively"
|
||||||
|
Click="OnRunInteractivelyClick"/>
|
||||||
<MenuItem Header="Open planning Session"
|
<MenuItem Header="Open planning Session"
|
||||||
Click="OnOpenPlanningSessionClick"
|
Click="OnOpenPlanningSessionClick"
|
||||||
IsVisible="{Binding CanOpenPlanningSession}"/>
|
IsVisible="{Binding CanOpenPlanningSession}"/>
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ public partial class TaskRowView : UserControl
|
|||||||
await vm.OpenPlanningSessionCommand.ExecuteAsync(row);
|
await vm.OpenPlanningSessionCommand.ExecuteAsync(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void OnRunInteractivelyClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||||
|
await vm.RunInteractivelyCommand.ExecuteAsync(row);
|
||||||
|
}
|
||||||
|
|
||||||
private async void OnResumePlanningSessionClick(object? sender, RoutedEventArgs e)
|
private async void OnResumePlanningSessionClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||||
|
|||||||
@@ -346,6 +346,12 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task OpenInteractiveTerminalAsync(string taskId)
|
||||||
|
{
|
||||||
|
var ctx = await _planning.OpenInteractiveAsync(taskId, Context.ConnectionAborted);
|
||||||
|
await _launcher.LaunchInteractiveAsync(ctx, Context.ConnectionAborted);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task DiscardPlanningSessionAsync(string taskId)
|
public async Task DiscardPlanningSessionAsync(string taskId)
|
||||||
{
|
{
|
||||||
await _planning.DiscardAsync(taskId, Context.ConnectionAborted);
|
await _planning.DiscardAsync(taskId, Context.ConnectionAborted);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ public interface IPlanningTerminalLauncher
|
|||||||
{
|
{
|
||||||
Task LaunchStartAsync(PlanningSessionStartContext ctx, CancellationToken cancellationToken);
|
Task LaunchStartAsync(PlanningSessionStartContext ctx, CancellationToken cancellationToken);
|
||||||
Task LaunchResumeAsync(PlanningSessionResumeContext ctx, CancellationToken cancellationToken);
|
Task LaunchResumeAsync(PlanningSessionResumeContext ctx, CancellationToken cancellationToken);
|
||||||
|
Task LaunchInteractiveAsync(InteractiveLaunchContext ctx, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PlanningLaunchException : Exception
|
public sealed class PlanningLaunchException : Exception
|
||||||
|
|||||||
6
src/ClaudeDo.Worker/Planning/InteractiveLaunchContext.cs
Normal file
6
src/ClaudeDo.Worker/Planning/InteractiveLaunchContext.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ClaudeDo.Worker.Planning;
|
||||||
|
|
||||||
|
public sealed record InteractiveLaunchContext(
|
||||||
|
string TaskId,
|
||||||
|
string WorkingDir,
|
||||||
|
string InitialPrompt);
|
||||||
@@ -138,6 +138,40 @@ public sealed class PlanningSessionManager
|
|||||||
Files: files);
|
Files: files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<InteractiveLaunchContext> OpenInteractiveAsync(string taskId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var (tasks, lists, _, ctx) = CreateRepos();
|
||||||
|
await using var __ = ctx;
|
||||||
|
|
||||||
|
var task = await tasks.GetByIdAsync(taskId, ct)
|
||||||
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
|
||||||
|
var list = await lists.GetByIdAsync(task.ListId, ct)
|
||||||
|
?? throw new InvalidOperationException($"List {task.ListId} not found.");
|
||||||
|
|
||||||
|
var workingDir = list.WorkingDir;
|
||||||
|
if (string.IsNullOrWhiteSpace(workingDir) || !Directory.Exists(workingDir))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"List '{list.Name}' has no valid working directory configured.");
|
||||||
|
|
||||||
|
return new InteractiveLaunchContext(
|
||||||
|
TaskId: taskId,
|
||||||
|
WorkingDir: workingDir,
|
||||||
|
InitialPrompt: BuildInteractivePrompt(task));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildInteractivePrompt(TaskEntity task)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"# Task: {task.Title}");
|
||||||
|
if (!string.IsNullOrWhiteSpace(task.Description))
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(task.Description);
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<int> FinalizeAsync(string taskId, bool queueAgentTasks, CancellationToken ct)
|
public async Task<int> FinalizeAsync(string taskId, bool queueAgentTasks, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var (tasks, lists, settings, ctx) = CreateRepos();
|
var (tasks, lists, settings, ctx) = CreateRepos();
|
||||||
@@ -287,7 +321,12 @@ public sealed class PlanningSessionManager
|
|||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
private static string BuildSystemPrompt() =>
|
private static string BuildSystemPrompt()
|
||||||
|
{
|
||||||
|
var fromFile = PromptFiles.ReadOrNull(PromptKind.Planning);
|
||||||
|
if (fromFile is not null) return fromFile;
|
||||||
|
|
||||||
|
return
|
||||||
"""
|
"""
|
||||||
You are a planning assistant for ClaudeDo.
|
You are a planning assistant for ClaudeDo.
|
||||||
Your role is to help break down a task into smaller, actionable subtasks.
|
Your role is to help break down a task into smaller, actionable subtasks.
|
||||||
@@ -306,6 +345,7 @@ public sealed class PlanningSessionManager
|
|||||||
|
|
||||||
Be concise and focused. Each subtask should be independently executable.
|
Be concise and focused. Each subtask should be independently executable.
|
||||||
""";
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildInitialPrompt(TaskEntity task)
|
private static string BuildInitialPrompt(TaskEntity task)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -74,6 +74,40 @@ public sealed class WindowsTerminalPlanningLauncher : IPlanningTerminalLauncher
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task LaunchInteractiveAsync(InteractiveLaunchContext ctx, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(ctx.WorkingDir))
|
||||||
|
throw new PlanningLaunchException($"Working directory does not exist: {ctx.WorkingDir}");
|
||||||
|
|
||||||
|
var resolvedWt = Resolve(_wtPath)
|
||||||
|
?? throw new PlanningLaunchException($"Windows Terminal not found: {_wtPath}");
|
||||||
|
var resolvedClaude = Resolve(_claudePath)
|
||||||
|
?? throw new PlanningLaunchException($"claude executable not found: {_claudePath}");
|
||||||
|
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = resolvedWt,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
psi.Environment["MAX_THINKING_TOKENS"] = "20000";
|
||||||
|
|
||||||
|
psi.ArgumentList.Add("-d");
|
||||||
|
psi.ArgumentList.Add(ctx.WorkingDir);
|
||||||
|
psi.ArgumentList.Add(resolvedClaude);
|
||||||
|
psi.ArgumentList.Add("--model");
|
||||||
|
psi.ArgumentList.Add(Model);
|
||||||
|
psi.ArgumentList.Add("--permission-mode");
|
||||||
|
psi.ArgumentList.Add("auto");
|
||||||
|
psi.ArgumentList.Add(ctx.InitialPrompt);
|
||||||
|
|
||||||
|
var proc = Process.Start(psi)
|
||||||
|
?? throw new PlanningLaunchException("Failed to start Windows Terminal process.");
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public Task LaunchResumeAsync(PlanningSessionResumeContext ctx, CancellationToken cancellationToken)
|
public Task LaunchResumeAsync(PlanningSessionResumeContext ctx, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(ctx.WorkingDir))
|
if (!Directory.Exists(ctx.WorkingDir))
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ internal sealed class FakePlanningLauncher : IPlanningTerminalLauncher
|
|||||||
public bool ShouldThrow { get; set; }
|
public bool ShouldThrow { get; set; }
|
||||||
public int LaunchStartCalls { get; private set; }
|
public int LaunchStartCalls { get; private set; }
|
||||||
public int LaunchResumeCalls { get; private set; }
|
public int LaunchResumeCalls { get; private set; }
|
||||||
|
public int LaunchInteractiveCalls { get; private set; }
|
||||||
|
|
||||||
public Task LaunchStartAsync(PlanningSessionStartContext ctx, CancellationToken cancellationToken)
|
public Task LaunchStartAsync(PlanningSessionStartContext ctx, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -182,6 +183,13 @@ internal sealed class FakePlanningLauncher : IPlanningTerminalLauncher
|
|||||||
LaunchResumeCalls++;
|
LaunchResumeCalls++;
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task LaunchInteractiveAsync(InteractiveLaunchContext ctx, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (ShouldThrow) throw new PlanningLaunchException("fake launch failure");
|
||||||
|
LaunchInteractiveCalls++;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class RecordingClientProxy : IClientProxy
|
internal sealed class RecordingClientProxy : IClientProxy
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ sealed class FakeWorkerClient : IWorkerClient
|
|||||||
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
||||||
public Task WakeQueueAsync() { WakeQueueCalls++; return Task.CompletedTask; }
|
public Task WakeQueueAsync() { WakeQueueCalls++; return Task.CompletedTask; }
|
||||||
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) { StartPlanningCalls++; return Task.CompletedTask; }
|
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) { StartPlanningCalls++; return Task.CompletedTask; }
|
||||||
|
public Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
|
public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) { ResumePlanningCalls++; return Task.CompletedTask; }
|
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) { ResumePlanningCalls++; return Task.CompletedTask; }
|
||||||
public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) { DiscardPlanningCalls++; return Task.CompletedTask; }
|
public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) { DiscardPlanningCalls++; return Task.CompletedTask; }
|
||||||
public Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) { FinalizePlanningCalls++; return Task.CompletedTask; }
|
public Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) { FinalizePlanningCalls++; return Task.CompletedTask; }
|
||||||
|
|||||||
Reference in New Issue
Block a user