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:
mika kuns
2026-07-24 12:12:23 +02:00
parent 2612831a5e
commit ef285b21fd
16 changed files with 249 additions and 24 deletions
@@ -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)