feat(planning): run interactive planning sessions via embedded ConPTY
Planning start/resume opened an external Windows Terminal (wt) window. Route them through the embedded ConPTY Command Center pane instead, matching the existing interactive-session UX (no external window). - Extract bare planning arg builders (BuildPlanningStartArgs/ResumeArgs) from WindowsTerminalLauncher; the wt path still uses them (kept, not removed). - InteractiveLaunchSpecService.BuildPlanningStart/Resume map a planning context into a LaunchSpec (planning args + env: MAX_THINKING_TOKENS, CLAUDEDO_PLANNING_TOKEN). Hub GetPlanningStart/ResumeLaunchSpec run StartAsync/ResumeAsync then return the spec. - UI: OpenPlanningSession + the resume branch raise OpenPlanningConPtyRequested; the shell opens Mission Control and hosts a planning ConPTY pane. Env is process-global by design (sequential human-paced sessions). wt planning code retained. Tests added for the arg/env mapping.
This commit is contained in:
@@ -82,6 +82,11 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
/// <summary>Launch spec for an ad-hoc interactive session in an arbitrary directory --
|
||||
/// no task, no worktree.</summary>
|
||||
Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default);
|
||||
/// <summary>Starts a planning session and returns the launch spec for an embedded ConPTY
|
||||
/// planning terminal (replaces StartPlanningSessionAsync's external wt window).</summary>
|
||||
Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default);
|
||||
/// <summary>Resumes a planning session and returns its embedded-ConPTY launch spec.</summary>
|
||||
Task<LaunchSpec> GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default);
|
||||
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||
Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default);
|
||||
Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default);
|
||||
|
||||
@@ -519,6 +519,12 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public async Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<LaunchSpec>("GetAdHocLaunchSpec", directory, ct);
|
||||
|
||||
public async Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<LaunchSpec>("GetPlanningStartLaunchSpec", taskId, ct);
|
||||
|
||||
public async Task<LaunchSpec> GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<LaunchSpec>("GetPlanningResumeLaunchSpec", taskId, ct);
|
||||
|
||||
public async Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<DiscardPlanningOutcome>("DiscardPlanningSessionAsync", taskId, dequeueQueuedChildren, ct);
|
||||
|
||||
|
||||
@@ -813,19 +813,24 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
private void OpenListSettings() => OpenListSettingsRequested?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenPlanningSessionAsync(TaskRowViewModel? row)
|
||||
private void OpenPlanningSession(TaskRowViewModel? row)
|
||||
{
|
||||
if (row is null) return;
|
||||
if (row.Status != TaskStatus.Idle || row.PlanningPhase != PlanningPhase.None) return;
|
||||
ForegroundHelper.AllowAny();
|
||||
try { await _worker!.StartPlanningSessionAsync(row.Id); }
|
||||
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("vm.tasksIsland.planningOpenFailed", ex.Message)); }
|
||||
// Planning now runs as an embedded ConPTY pane in the Command Center (not an external wt
|
||||
// window). The shell owns Mission Control, so raise an event; the actual StartAsync happens
|
||||
// server-side inside GetPlanningStartLaunchSpec when the pane opens.
|
||||
OpenPlanningConPtyRequested?.Invoke(row.Id, false);
|
||||
}
|
||||
|
||||
// Opens the task in an embedded ConPTY terminal pane in the Command Center. The shell owns
|
||||
// the Mission Control view model, so this just raises an event for it to act on.
|
||||
public event Action<string>? OpenConPtySessionRequested;
|
||||
|
||||
// Opens (resume=false) or resumes (resume=true) a planning session as an embedded ConPTY
|
||||
// pane in the Command Center.
|
||||
public event Action<string, bool>? OpenPlanningConPtyRequested;
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenConPtySession(TaskRowViewModel? row)
|
||||
{
|
||||
@@ -865,8 +870,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
switch (choice)
|
||||
{
|
||||
case UnfinishedPlanningModalResult.Resume:
|
||||
ForegroundHelper.AllowAny();
|
||||
await _worker.ResumePlanningSessionAsync(row.Id);
|
||||
// Resume as an embedded ConPTY pane (server-side ResumeAsync runs inside
|
||||
// GetPlanningResumeLaunchSpec when the pane opens).
|
||||
OpenPlanningConPtyRequested?.Invoke(row.Id, true);
|
||||
break;
|
||||
case UnfinishedPlanningModalResult.FinalizeNow:
|
||||
await _worker.FinalizePlanningSessionAsync(row.Id, queueAgentTasks: false);
|
||||
|
||||
@@ -234,6 +234,11 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
OpenMissionControl();
|
||||
_ = MissionControl.OpenConPtySessionAsync(taskId);
|
||||
};
|
||||
Tasks.OpenPlanningConPtyRequested += (taskId, resume) =>
|
||||
{
|
||||
OpenMissionControl();
|
||||
_ = MissionControl.OpenPlanningConPtySessionAsync(taskId, resume);
|
||||
};
|
||||
Tasks.TasksChanged += (_, _) => _ = Lists.RefreshCountsAsync();
|
||||
Tasks.OpenListSettingsRequested += (_, _) =>
|
||||
{
|
||||
|
||||
@@ -256,6 +256,45 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Starts (or resumes) a planning session and hosts it as an embedded ConPTY Command Center
|
||||
// pane — the ConPTY replacement for the old external wt planning window. Deduped by TaskId
|
||||
// like OpenConPtySessionAsync.
|
||||
public async System.Threading.Tasks.Task OpenPlanningConPtySessionAsync(string taskId, bool resume)
|
||||
{
|
||||
if (string.IsNullOrEmpty(taskId)) return;
|
||||
if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is { } existing)
|
||||
{
|
||||
FocusedPane = existing;
|
||||
return;
|
||||
}
|
||||
|
||||
var title = taskId;
|
||||
try
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||
var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId);
|
||||
if (entity?.Title is { Length: > 0 } t) title = t;
|
||||
}
|
||||
catch { /* best-effort title lookup */ }
|
||||
title += Loc.T("missionControl.planningTitleSuffix");
|
||||
|
||||
try
|
||||
{
|
||||
var spec = resume
|
||||
? await _worker.GetPlanningResumeLaunchSpecAsync(taskId)
|
||||
: await _worker.GetPlanningStartLaunchSpecAsync(taskId);
|
||||
var descriptor = new TerminalLaunchDescriptor(spec.Cwd, spec.Exe, spec.Args, spec.Env);
|
||||
var pane = new ConPtyPaneViewModel(taskId, title, descriptor);
|
||||
pane.ErrorReported += OnConPtyPaneError;
|
||||
pane.CloseRequested += CloseConPtySession;
|
||||
ConPtySessions.Add(pane);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
// Ad-hoc (task-less) ConPTY session in a user-chosen directory. Never deduped — every call
|
||||
// opens a fresh pane, unlike the task-based OpenConPtySessionAsync above.
|
||||
public async System.Threading.Tasks.Task OpenAdHocConPtySessionAsync(string directory)
|
||||
|
||||
@@ -55,10 +55,10 @@ public partial class TaskRowView : UserControl
|
||||
await vm.RemoveFromMyDayCommand.ExecuteAsync(row);
|
||||
}
|
||||
|
||||
private async void OnOpenPlanningSessionClick(object? sender, RoutedEventArgs e)
|
||||
private void OnOpenPlanningSessionClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||
await vm.OpenPlanningSessionCommand.ExecuteAsync(row);
|
||||
vm.OpenPlanningSessionCommand.Execute(row);
|
||||
}
|
||||
|
||||
private void OnOpenConPtySessionClick(object? sender, RoutedEventArgs e)
|
||||
|
||||
Reference in New Issue
Block a user