feat(interactive): seed fresh task session with the task prompt
A fresh (non-resume) task-based ConPTY session now opens claude on the task's prompt (title + description) as the positional argument, so the session starts on the task instead of an empty prompt. Resume sessions and ad-hoc sessions are unchanged.
This commit is contained in:
@@ -89,9 +89,12 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
|||||||
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
||||||
?? throw new InvalidOperationException($"claude executable not found: {_claudePath}");
|
?? throw new InvalidOperationException($"claude executable not found: {_claudePath}");
|
||||||
|
|
||||||
|
// Resume an existing session as-is; for a fresh session, seed the interactive TUI with
|
||||||
|
// the task's prompt (title + description) as claude's positional prompt so it starts on
|
||||||
|
// the task -- the user then supervises/answers rather than retyping it.
|
||||||
var args = run?.SessionId is { Length: > 0 } sessionId
|
var args = run?.SessionId is { Length: > 0 } sessionId
|
||||||
? WindowsTerminalLauncher.BuildResumeArgs(sessionId)
|
? WindowsTerminalLauncher.BuildResumeArgs(sessionId)
|
||||||
: Array.Empty<string>();
|
: BuildFreshPromptArgs(task);
|
||||||
|
|
||||||
// Same run environment variable ClaudeProcess sets for every headless run: the
|
// Same run environment variable ClaudeProcess sets for every headless run: the
|
||||||
// AskUser MCP tool call caps at 60s unless raised, and lifting it is harmless for
|
// AskUser MCP tool call caps at 60s unless raised, and lifting it is harmless for
|
||||||
@@ -120,6 +123,22 @@ public sealed class InteractiveLaunchSpecService : IInteractiveLaunchSpecService
|
|||||||
return Task.FromResult(new LaunchSpec(directory, resolvedClaude, Array.Empty<string>(), env));
|
return Task.FromResult(new LaunchSpec(directory, resolvedClaude, Array.Empty<string>(), env));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The positional prompt claude opens the interactive session on. Empty (no positional arg)
|
||||||
|
// if the task has neither a title nor a description.
|
||||||
|
private static IReadOnlyList<string> BuildFreshPromptArgs(TaskEntity task)
|
||||||
|
{
|
||||||
|
var title = task.Title?.Trim();
|
||||||
|
var description = task.Description?.Trim();
|
||||||
|
var prompt = (string.IsNullOrEmpty(title), string.IsNullOrEmpty(description)) switch
|
||||||
|
{
|
||||||
|
(false, false) => $"{title}\n\n{description}",
|
||||||
|
(false, true) => title!,
|
||||||
|
(true, false) => description!,
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
return string.IsNullOrEmpty(prompt) ? Array.Empty<string>() : new[] { prompt };
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<IReadOnlyList<string>> FilterToInstalledSkillsAsync(IReadOnlyList<string> requested, CancellationToken ct)
|
private async Task<IReadOnlyList<string>> FilterToInstalledSkillsAsync(IReadOnlyList<string> requested, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (requested.Count == 0) return requested;
|
if (requested.Count == 0) return requested;
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
Assert.True(Directory.Exists(wtRow.Path));
|
Assert.True(Directory.Exists(wtRow.Path));
|
||||||
|
|
||||||
Assert.Equal(wtRow.Path, spec.Cwd);
|
Assert.Equal(wtRow.Path, spec.Cwd);
|
||||||
Assert.Empty(spec.Args);
|
Assert.Equal(new[] { "T" }, spec.Args); // fresh session seeds the task title as the prompt
|
||||||
Assert.Equal(_claudeStubPath, spec.Exe);
|
Assert.Equal(_claudeStubPath, spec.Exe);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +258,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Empty(spec.Args);
|
Assert.Equal(new[] { "T" }, spec.Args); // fresh: seeds the task title as the prompt
|
||||||
Assert.Equal(_worktreeDir, spec.Cwd);
|
Assert.Equal(_worktreeDir, spec.Cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +274,27 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Empty(spec.Args);
|
Assert.Equal(new[] { "T" }, spec.Args); // fresh: seeds the task title as the prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildForTaskAsync_FreshTask_WithDescription_SeedsTitleAndDescriptionPrompt()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
var taskId = Guid.NewGuid().ToString();
|
||||||
|
await SeedTaskAsync(taskId, listId, TaskStatus.Idle);
|
||||||
|
await SeedWorktreeAsync(taskId, WorktreeState.Active);
|
||||||
|
using (var ctx = _db.CreateContext())
|
||||||
|
{
|
||||||
|
var t = await ctx.Tasks.FindAsync(taskId);
|
||||||
|
t!.Description = "Do the thing";
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var svc = BuildService();
|
||||||
|
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(new[] { "T\n\nDo the thing" }, spec.Args);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user