feat(review): submit interactive (ConPTY) work for review
An embedded ConPTY session leaves its worktree changed but never touches task status, so hand-driven work had no path into the review/merge flow. Add SubmitTaskForReview: commit the worktree (same auto-commit as a headless run), then transition Idle/Failed -> WaitingForReview via the new TaskStateService.SubmitInteractiveForReviewAsync. Approve then merges it. Surfaces: a 'Submit for review' button in the detail work console (shown for an Idle/Failed task with a worktree) and on the ConPTY Command Center pane header (task-based panes; closes the pane on success). Tests cover the new transition (Idle/Failed accepted, Running/Queued/Done/Review rejected).
This commit is contained in:
@@ -266,6 +266,9 @@
|
||||
"overviewMode": "Übersicht",
|
||||
"closeSession": "Sitzung schließen",
|
||||
"conptyLaunchFailed": "ConPTY-Sitzung konnte nicht geöffnet werden: {0}",
|
||||
"submitForReviewFailed": "Einreichen zum Review fehlgeschlagen: {0}",
|
||||
"submitForReview": "Zum Review einreichen",
|
||||
"submitForReviewTip": "Diesen Worktree committen und den Task ins Review bringen, damit er gemergt werden kann",
|
||||
"planningTitleSuffix": " (Planung)",
|
||||
"question": {
|
||||
"title": "Claude fragt nach",
|
||||
|
||||
@@ -266,6 +266,9 @@
|
||||
"overviewMode": "Overview",
|
||||
"closeSession": "Close session",
|
||||
"conptyLaunchFailed": "Couldn't open ConPTY session: {0}",
|
||||
"submitForReviewFailed": "Couldn't submit for review: {0}",
|
||||
"submitForReview": "Submit for review",
|
||||
"submitForReviewTip": "Commit this worktree and move the task to review so it can be merged",
|
||||
"planningTitleSuffix": " (Planning)",
|
||||
"question": {
|
||||
"title": "Claude is asking",
|
||||
|
||||
@@ -76,6 +76,9 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||
// Picks up a task's Claude session in a real terminal window (--resume).
|
||||
Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default);
|
||||
/// <summary>Commits an interactively-worked task's worktree and moves it to WaitingForReview
|
||||
/// (the only path that flips a hand-driven ConPTY session into the review/merge pipeline).</summary>
|
||||
Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default);
|
||||
/// <summary>Launch spec for an embedded ConPTY terminal to open an interactive session
|
||||
/// in a task's worktree (same worktree prep as an autonomous run).</summary>
|
||||
Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default);
|
||||
|
||||
@@ -513,6 +513,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public async Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync("ResumeTaskInTerminal", taskId, ct);
|
||||
|
||||
public async Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync("SubmitTaskForReview", taskId, ct);
|
||||
|
||||
public async Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<LaunchSpec>("GetInteractiveLaunchSpec", taskId, ct);
|
||||
|
||||
|
||||
@@ -413,6 +413,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
ReviewDiffViewed = false;
|
||||
ApproveReviewCommand.NotifyCanExecuteChanged();
|
||||
OnPropertyChanged(nameof(ShowReviewDiffHint));
|
||||
OnPropertyChanged(nameof(CanSubmitForReview));
|
||||
SubmitForReviewCommand.NotifyCanExecuteChanged();
|
||||
AgentSettings.IsRunning = IsRunning;
|
||||
NotifySessionSections();
|
||||
OnPropertyChanged(nameof(CanAcceptDrop));
|
||||
@@ -758,6 +760,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
Merge.SyncWorktree(WorktreePath, WorktreeBaseCommit, WorktreeHeadCommit,
|
||||
WorktreeStateLabel, _listWorkingDir);
|
||||
NotifySessionSections();
|
||||
OnPropertyChanged(nameof(CanSubmitForReview));
|
||||
SubmitForReviewCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
partial void OnWorktreeHeadCommitChanged(string? value) =>
|
||||
@@ -1034,6 +1038,23 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
Task != null && _worker.IsConnected && IsWaitingForReview
|
||||
&& (!Merge.HasReviewableDiff || ReviewDiffViewed);
|
||||
|
||||
// An interactive (ConPTY) session leaves its worktree changed but never flips the task
|
||||
// status. Offer "Submit for review" for an Idle/Failed task that still has a worktree, so
|
||||
// its hand-driven work can enter the normal review/merge flow (commits first, server-side).
|
||||
public bool CanSubmitForReview =>
|
||||
Task != null && _worker.IsConnected && (IsIdle || IsFailed) && !string.IsNullOrEmpty(WorktreePath);
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanSubmitForReview))]
|
||||
private async System.Threading.Tasks.Task SubmitForReviewAsync()
|
||||
{
|
||||
if (Task is null || !_worker.IsConnected) return;
|
||||
try { await _worker.SubmitTaskForReviewAsync(Task.Id); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ShowErrorAsync != null) await ShowErrorAsync(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasReviewFeedback))]
|
||||
private async System.Threading.Tasks.Task RejectReviewAsync()
|
||||
{
|
||||
|
||||
@@ -17,6 +17,9 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControl
|
||||
{
|
||||
public string? TaskId { get; }
|
||||
|
||||
// Only a task-based pane can be submitted for review (an ad-hoc directory session has no task).
|
||||
public bool IsTaskBased => TaskId is not null;
|
||||
|
||||
[ObservableProperty] private string _displayTitle;
|
||||
|
||||
public InteractiveTerminalViewModel Terminal { get; } = new();
|
||||
@@ -27,6 +30,10 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControl
|
||||
/// <summary>Set by the host (Mission Control) to remove this pane from its collection.</summary>
|
||||
public Action<ConPtyPaneViewModel>? CloseRequested { get; set; }
|
||||
|
||||
/// <summary>Raised when the user submits this task's hand-driven work for review; the host
|
||||
/// commits the worktree and moves the task to WaitingForReview.</summary>
|
||||
public event Action<string>? SubmitForReviewRequested;
|
||||
|
||||
/// <summary>Task-based pane — dedup'd by <see cref="TaskId"/>. Pass null for an ad-hoc pane
|
||||
/// (no task, never deduped); prefer <see cref="CreateAdHoc"/> at ad-hoc call sites.</summary>
|
||||
public ConPtyPaneViewModel(string? taskId, string displayTitle, TerminalLaunchDescriptor descriptor)
|
||||
@@ -50,6 +57,14 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControl
|
||||
[RelayCommand]
|
||||
private void Close() => CloseRequested?.Invoke(this);
|
||||
|
||||
private bool CanSubmitForReview() => IsTaskBased;
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanSubmitForReview))]
|
||||
private void SubmitForReview()
|
||||
{
|
||||
if (TaskId is { } id) SubmitForReviewRequested?.Invoke(id);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Terminal.PropertyChanged -= OnTerminalPropertyChanged;
|
||||
|
||||
@@ -248,6 +248,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
var pane = new ConPtyPaneViewModel(taskId, title, descriptor);
|
||||
pane.ErrorReported += OnConPtyPaneError;
|
||||
pane.CloseRequested += CloseConPtySession;
|
||||
pane.SubmitForReviewRequested += OnPaneSubmitForReview;
|
||||
ConPtySessions.Add(pane);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -287,6 +288,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
var pane = new ConPtyPaneViewModel(taskId, title, descriptor);
|
||||
pane.ErrorReported += OnConPtyPaneError;
|
||||
pane.CloseRequested += CloseConPtySession;
|
||||
pane.SubmitForReviewRequested += OnPaneSubmitForReview;
|
||||
ConPtySessions.Add(pane);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -311,6 +313,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
var pane = ConPtyPaneViewModel.CreateAdHoc(title, descriptor);
|
||||
pane.ErrorReported += OnConPtyPaneError;
|
||||
pane.CloseRequested += CloseConPtySession;
|
||||
pane.SubmitForReviewRequested += OnPaneSubmitForReview;
|
||||
ConPtySessions.Add(pane);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -321,6 +324,22 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
|
||||
private void OnConPtyPaneError(string message) => ErrorReported?.Invoke(message);
|
||||
|
||||
// Submit a task's hand-driven ConPTY work for review, then close the pane (the interactive
|
||||
// session is finished). The worker commits the worktree and moves the task to WaitingForReview.
|
||||
private async void OnPaneSubmitForReview(string taskId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _worker.SubmitTaskForReviewAsync(taskId);
|
||||
if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is { } pane)
|
||||
CloseConPtySession(pane);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorReported?.Invoke(Loc.T("missionControl.submitForReviewFailed", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseConPtySession(ConPtyPaneViewModel pane)
|
||||
{
|
||||
if (!ConPtySessions.Contains(pane)) return;
|
||||
|
||||
@@ -372,6 +372,17 @@
|
||||
ToolTip.Tip="{loc:Tr session.reviewResetTip}"
|
||||
Command="{Binding ResetReviewCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Submit an interactive (ConPTY) session for review: an Idle/Failed task that
|
||||
still has a worktree. Commits the worktree, then moves it to WaitingForReview. -->
|
||||
<StackPanel Spacing="8" IsVisible="{Binding CanSubmitForReview}">
|
||||
<Border Height="1" Background="{DynamicResource LineBrush}" />
|
||||
<TextBlock Classes="meta" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
Text="Worked on this by hand? Submit the worktree for review to merge it." />
|
||||
<Button Classes="btn accent" Content="Submit for review" HorizontalAlignment="Left"
|
||||
Command="{Binding SubmitForReviewCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
|
||||
@@ -14,13 +14,18 @@
|
||||
Background="{DynamicResource Surface2Brush}"
|
||||
BorderBrush="{DynamicResource LineBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="8,3">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="meta" Text="{Binding DisplayTitle}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip.Tip="{Binding DisplayTitle}"
|
||||
Foreground="{DynamicResource TextDimBrush}"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0" />
|
||||
<Button Grid.Column="1" Classes="title-ctrl"
|
||||
<Button Grid.Column="1" Classes="btn" Margin="0,0,6,0"
|
||||
IsVisible="{Binding IsTaskBased}"
|
||||
Content="{loc:Tr missionControl.submitForReview}"
|
||||
ToolTip.Tip="{loc:Tr missionControl.submitForReviewTip}"
|
||||
Command="{Binding SubmitForReviewCommand}" />
|
||||
<Button Grid.Column="2" Classes="title-ctrl"
|
||||
Command="{Binding CloseCommand}"
|
||||
ToolTip.Tip="{loc:Tr missionControl.closeSession}">
|
||||
<PathIcon Data="{StaticResource Icon.WinClose}" Width="12" Height="12"/>
|
||||
|
||||
@@ -130,6 +130,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
private readonly LogRingBuffer? _logBuffer;
|
||||
private readonly ISessionSkillRegistry _skillRegistry;
|
||||
private readonly IInteractiveLaunchSpecService? _interactiveLaunchSpec;
|
||||
private readonly WorktreeManager? _worktreeManager;
|
||||
|
||||
public WorkerHub(
|
||||
QueueService queue,
|
||||
@@ -157,7 +158,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
Runner.PendingQuestionRegistry pendingQuestions,
|
||||
ISessionSkillRegistry skillRegistry,
|
||||
LogRingBuffer? logBuffer = null,
|
||||
IInteractiveLaunchSpecService? interactiveLaunchSpec = null)
|
||||
IInteractiveLaunchSpecService? interactiveLaunchSpec = null,
|
||||
WorktreeManager? worktreeManager = null)
|
||||
{
|
||||
_queue = queue;
|
||||
_waker = waker;
|
||||
@@ -185,6 +187,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
_skillRegistry = skillRegistry;
|
||||
_logBuffer = logBuffer;
|
||||
_interactiveLaunchSpec = interactiveLaunchSpec;
|
||||
_worktreeManager = worktreeManager;
|
||||
}
|
||||
|
||||
// Persistence boundary for the session_skills JSON-array columns (task/list/global).
|
||||
@@ -705,6 +708,40 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
return _interactiveLaunchSpec.BuildPlanningResume(ctx);
|
||||
});
|
||||
|
||||
// Submits an interactively-worked task for review: commits whatever the ConPTY session left
|
||||
// in the worktree (so there is a diff to merge), then transitions Idle/Failed -> WaitingForReview.
|
||||
// The normal Approve flow then merges it. This is the only path that flips a hand-driven session
|
||||
// into the review pipeline — a ConPTY session never touches task status on its own.
|
||||
public Task SubmitTaskForReview(string taskId) => HubGuard(async () =>
|
||||
{
|
||||
if (_worktreeManager is null)
|
||||
throw new InvalidOperationException("Worktree manager is not configured.");
|
||||
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, Context.ConnectionAborted)
|
||||
?? throw new KeyNotFoundException();
|
||||
if (task.Status is TaskStatus.Running or TaskStatus.Queued)
|
||||
throw new InvalidOperationException("Can't submit a running or queued task — interrupt it first.");
|
||||
if (task.Status is TaskStatus.WaitingForReview or TaskStatus.WaitingForChildren)
|
||||
throw new InvalidOperationException("Task is already awaiting review.");
|
||||
|
||||
var worktree = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, Context.ConnectionAborted);
|
||||
if (worktree is null || worktree.State is not (WorktreeState.Active or WorktreeState.Kept))
|
||||
throw new InvalidOperationException("This task has no active worktree to submit.");
|
||||
if (!Directory.Exists(worktree.Path))
|
||||
throw new InvalidOperationException("The task's worktree directory no longer exists.");
|
||||
|
||||
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, Context.ConnectionAborted)
|
||||
?? throw new InvalidOperationException("Task list not found.");
|
||||
|
||||
var wtCtx = new WorktreeContext(worktree.Path, worktree.BranchName, worktree.BaseCommit);
|
||||
await _worktreeManager.CommitIfChangedAsync(wtCtx, task, list, Context.ConnectionAborted);
|
||||
|
||||
var result = await _state.SubmitInteractiveForReviewAsync(taskId, DateTime.UtcNow, Context.ConnectionAborted);
|
||||
if (!result.Ok)
|
||||
throw new InvalidOperationException(result.Reason ?? "Could not submit for review.");
|
||||
});
|
||||
|
||||
public async Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false)
|
||||
{
|
||||
var outcome = await _planning.DiscardAsync(taskId, dequeueQueuedChildren, Context.ConnectionAborted);
|
||||
|
||||
@@ -6,6 +6,7 @@ public interface ITaskStateService
|
||||
Task<TransitionResult> StartRunningAsync(string taskId, DateTime startedAt, CancellationToken ct);
|
||||
Task<TransitionResult> CompleteAsync(string taskId, DateTime finishedAt, string? result, CancellationToken ct);
|
||||
Task<TransitionResult> SubmitForReviewAsync(string taskId, DateTime finishedAt, string? result, CancellationToken ct);
|
||||
Task<TransitionResult> SubmitInteractiveForReviewAsync(string taskId, DateTime finishedAt, CancellationToken ct);
|
||||
Task<TransitionResult> SubmitForChildrenAsync(string taskId, DateTime finishedAt, string? result, CancellationToken ct);
|
||||
Task<TransitionResult> FailAsync(string taskId, DateTime finishedAt, string? error, CancellationToken ct);
|
||||
Task<TransitionResult> CancelAsync(string taskId, DateTime finishedAt, CancellationToken ct);
|
||||
|
||||
@@ -110,6 +110,26 @@ public sealed class TaskStateService : ITaskStateService
|
||||
return new TransitionResult(true, null);
|
||||
}
|
||||
|
||||
// Submit an interactively-worked task (a ConPTY session left its worktree with commits/changes)
|
||||
// for review. Unlike SubmitForReviewAsync — which only fires from the headless Running state —
|
||||
// this transitions from Idle or Failed, the states an interactive task sits in after the user
|
||||
// finishes the session by hand. The caller commits the worktree first so there is a diff to merge.
|
||||
public async Task<TransitionResult> SubmitInteractiveForReviewAsync(string taskId, DateTime finishedAt, CancellationToken ct)
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var affected = await ctx.Tasks
|
||||
.Where(t => t.Id == taskId && (t.Status == TaskStatus.Idle || t.Status == TaskStatus.Failed))
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(t => t.Status, TaskStatus.WaitingForReview)
|
||||
.SetProperty(t => t.FinishedAt, finishedAt), ct);
|
||||
|
||||
if (affected == 0)
|
||||
return new TransitionResult(false, "Task is not Idle or Failed; cannot submit for review.");
|
||||
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
return new TransitionResult(true, null);
|
||||
}
|
||||
|
||||
public async Task<TransitionResult> SubmitForChildrenAsync(string taskId, DateTime finishedAt, string? result, CancellationToken ct)
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
@@ -95,6 +95,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public virtual Task AbortConflictMergeAsync(string taskId) => Task.CompletedTask;
|
||||
public virtual Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public virtual Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public virtual Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public virtual Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public virtual Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||
|
||||
@@ -87,6 +87,38 @@ public sealed class ReviewTransitionTests : IDisposable
|
||||
Assert.Equal(TaskStatus.Queued, (await GetTaskAsync(id)).Status);
|
||||
}
|
||||
|
||||
// ─── SubmitInteractiveForReviewAsync ──────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData(TaskStatus.Idle)]
|
||||
[InlineData(TaskStatus.Failed)]
|
||||
public async Task SubmitInteractiveForReviewAsync_FromIdleOrFailed_TransitionsToWaitingForReview(TaskStatus from)
|
||||
{
|
||||
var id = await SeedTaskAsync(from);
|
||||
|
||||
var result = await _sut.SubmitInteractiveForReviewAsync(id, DateTime.UtcNow, default);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
var t = await GetTaskAsync(id);
|
||||
Assert.Equal(TaskStatus.WaitingForReview, t.Status);
|
||||
Assert.NotNull(t.FinishedAt);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TaskStatus.Running)]
|
||||
[InlineData(TaskStatus.Queued)]
|
||||
[InlineData(TaskStatus.Done)]
|
||||
[InlineData(TaskStatus.WaitingForReview)]
|
||||
public async Task SubmitInteractiveForReviewAsync_FromOtherStates_Rejects(TaskStatus from)
|
||||
{
|
||||
var id = await SeedTaskAsync(from);
|
||||
|
||||
var result = await _sut.SubmitInteractiveForReviewAsync(id, DateTime.UtcNow, default);
|
||||
|
||||
Assert.False(result.Ok);
|
||||
Assert.Equal(from, (await GetTaskAsync(id)).Status);
|
||||
}
|
||||
|
||||
// ─── ApproveReviewAsync ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -72,6 +72,7 @@ sealed class FakeWorkerClient : IWorkerClient
|
||||
public int PickUpInTerminalCalls { get; private set; }
|
||||
public string? LastPickUpTaskId { get; private set; }
|
||||
public Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default) { PickUpInTerminalCalls++; LastPickUpTaskId = taskId; return Task.CompletedTask; }
|
||||
public Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||
|
||||
Reference in New Issue
Block a user