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
@@ -2,6 +2,7 @@ using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Planning;
using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Skills;
using ClaudeDo.Worker.Tests.Infrastructure;
@@ -342,4 +343,40 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
await Assert.ThrowsAsync<InvalidOperationException>(
() => svc.BuildForDirectoryAsync(Path.Combine(_tempDir, "does-not-exist"), CancellationToken.None));
}
[Fact]
public void BuildPlanningStart_MapsPlanningArgsAndEnv()
{
var sessionDir = Path.Combine(_tempDir, "sess");
Directory.CreateDirectory(sessionDir);
var ctx = new PlanningSessionStartContext(
ParentTaskId: "p1", WorkingDir: _worktreeDir, Token: "tok-1",
WorktreePath: _worktreeDir, BranchName: "claudedo/planning/p1",
Files: new PlanningSessionFiles(sessionDir,
Path.Combine(sessionDir, "system-prompt.md"),
Path.Combine(sessionDir, "initial-prompt.txt")));
var spec = BuildService().BuildPlanningStart(ctx);
Assert.Equal(_worktreeDir, spec.Cwd);
Assert.Equal(_claudeStubPath, spec.Exe);
Assert.Contains("--permission-mode", spec.Args);
Assert.Contains("plan", spec.Args);
Assert.Equal("tok-1", spec.Env["CLAUDEDO_PLANNING_TOKEN"]);
Assert.Equal("20000", spec.Env["MAX_THINKING_TOKENS"]);
}
[Fact]
public void BuildPlanningResume_MapsResumeArgsAndToken()
{
var ctx = new PlanningSessionResumeContext(
ParentTaskId: "p1", WorkingDir: _worktreeDir,
ClaudeSessionId: "sess-42", Token: "tok-2", WorktreePath: _worktreeDir);
var spec = BuildService().BuildPlanningResume(ctx);
Assert.Equal(new[] { "--permission-mode", "plan", "--resume", "sess-42" }, spec.Args);
Assert.Equal("tok-2", spec.Env["CLAUDEDO_PLANNING_TOKEN"]);
Assert.Equal(_worktreeDir, spec.Cwd);
}
}