fix(worker): keep interactive & planning prompts intact past Windows Terminal

wt.exe treats ';' as a command/tab delimiter in every argument, with no escape
that survives quoting (microsoft/terminal#13264), so a task description
containing ';' spawned extra terminals on "Run interactively" and planning start.
Route the launch as wt -> powershell -> claude and pass the free-text prompt via
$env:CLAUDEDO_LAUNCH_PROMPT so it never reaches the wt command line; PowerShell
binds the variable as a single argument (embedded quotes escaped for PS 5.1).

Also clarify the launcher, which serves interactive runs too (not just planning):
IPlanningTerminalLauncher -> ITerminalLauncher, WindowsTerminalPlanningLauncher ->
WindowsTerminalLauncher, LaunchStart/Resume -> LaunchPlanning{Start,Resume}Async.
This commit is contained in:
Mika Kuns
2026-06-24 13:19:03 +02:00
parent f86b78593e
commit ea16da2756
9 changed files with 233 additions and 206 deletions

View File

@@ -2,7 +2,7 @@ using ClaudeDo.Worker.Planning;
namespace ClaudeDo.Worker.Tests.Planning;
public sealed class WindowsTerminalPlanningLauncherTests
public sealed class WindowsTerminalLauncherTests
{
private static PlanningSessionStartContext MakeStartCtx(string? wd = null)
{
@@ -22,27 +22,27 @@ public sealed class WindowsTerminalPlanningLauncherTests
}
[Fact]
public async Task LaunchStartAsync_WorkingDirMissing_Throws()
public async Task LaunchPlanningStartAsync_WorkingDirMissing_Throws()
{
var ctx = MakeStartCtx(wd: Path.Combine(Path.GetTempPath(), "nonexistent_" + Guid.NewGuid()));
var sut = new WindowsTerminalPlanningLauncher(wtPath: "wt", claudePath: "claude");
var ex = await Assert.ThrowsAsync<PlanningLaunchException>(() =>
sut.LaunchStartAsync(ctx, CancellationToken.None));
var sut = new WindowsTerminalLauncher(wtPath: "wt", claudePath: "claude");
var ex = await Assert.ThrowsAsync<TerminalLaunchException>(() =>
sut.LaunchPlanningStartAsync(ctx, CancellationToken.None));
Assert.Contains("Working directory", ex.Message);
}
[Fact]
public async Task LaunchStartAsync_WtMissing_Throws()
public async Task LaunchPlanningStartAsync_WtMissing_Throws()
{
var ctx = MakeStartCtx();
File.WriteAllText(ctx.Files.SystemPromptPath, "sp");
File.WriteAllText(ctx.Files.InitialPromptPath, "ip");
var sut = new WindowsTerminalPlanningLauncher(
var sut = new WindowsTerminalLauncher(
wtPath: "C:/no/such/wt.exe",
claudePath: "claude");
var ex = await Assert.ThrowsAsync<PlanningLaunchException>(() =>
sut.LaunchStartAsync(ctx, CancellationToken.None));
var ex = await Assert.ThrowsAsync<TerminalLaunchException>(() =>
sut.LaunchPlanningStartAsync(ctx, CancellationToken.None));
Assert.Contains("Windows Terminal", ex.Message);
}
}