feat(ui): pick up a task's session in a terminal
This commit is contained in:
@@ -80,6 +80,9 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
Task AbortConflictMergeAsync(string taskId);
|
||||
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||
Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default);
|
||||
// Picks up a task's Claude session in a real terminal window (--resume) — distinct
|
||||
// from OpenInteractiveTerminalAsync (the in-app streaming session).
|
||||
Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default);
|
||||
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||
Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default);
|
||||
Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default);
|
||||
|
||||
@@ -549,6 +549,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public async Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync("OpenInteractiveTerminalAsync", taskId, ct);
|
||||
|
||||
public async Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync("ResumeTaskInTerminal", taskId, ct);
|
||||
|
||||
public async Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<DiscardPlanningOutcome>("DiscardPlanningSessionAsync", taskId, dequeueQueuedChildren, ct);
|
||||
|
||||
|
||||
@@ -751,6 +751,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
Merge.SyncTaskContext(Task?.Id, Task?.Title, Task?.IsPlanningParent == true);
|
||||
NotifySessionSections();
|
||||
OnPropertyChanged(nameof(CanAcceptDrop));
|
||||
OnPropertyChanged(nameof(CanPickUpInTerminal));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -892,6 +893,24 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
catch { /* offline */ }
|
||||
}
|
||||
|
||||
// Pick up in a terminal only where a session + worktree reliably still exist (parked
|
||||
// for review or failed mid-run); the worker reports a clear error otherwise.
|
||||
public bool CanPickUpInTerminal => Task is not null
|
||||
&& Task.Status is ClaudeDo.Data.Models.TaskStatus.WaitingForReview
|
||||
or ClaudeDo.Data.Models.TaskStatus.Failed;
|
||||
|
||||
[RelayCommand]
|
||||
private async System.Threading.Tasks.Task PickUpInTerminalAsync()
|
||||
{
|
||||
if (Task is null) return;
|
||||
ClaudeDo.Ui.Services.ForegroundHelper.AllowAny();
|
||||
try { await _worker.ResumeTaskInTerminalAsync(Task.Id); }
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
if (ShowErrorAsync != null) await ShowErrorAsync(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanEnqueue))]
|
||||
private async System.Threading.Tasks.Task EnqueueAsync()
|
||||
{
|
||||
|
||||
@@ -63,6 +63,11 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
&& !IsChild;
|
||||
public bool CanResumeOrDiscardPlanning => PlanningPhase == PlanningPhase.Active;
|
||||
|
||||
// Pick up in a terminal only where a session + worktree reliably still exist: a task
|
||||
// parked for review, or one that failed mid-run. The worker validates and reports a
|
||||
// clear error if there's no resumable session/worktree.
|
||||
public bool CanPickUpInTerminal => Status is TaskStatus.WaitingForReview or TaskStatus.Failed;
|
||||
|
||||
public string? PlanningBadge => PlanningPhase switch
|
||||
{
|
||||
PlanningPhase.Active => Loc.T("vm.planningBadge.active"),
|
||||
@@ -141,6 +146,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(StatusLabel));
|
||||
OnPropertyChanged(nameof(IsRunning));
|
||||
OnPropertyChanged(nameof(IsWaitingForReview));
|
||||
OnPropertyChanged(nameof(CanPickUpInTerminal));
|
||||
OnPropertyChanged(nameof(IsParked));
|
||||
OnPropertyChanged(nameof(IsQueued));
|
||||
OnPropertyChanged(nameof(IsWaiting));
|
||||
|
||||
@@ -819,6 +819,15 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("vm.tasksIsland.runInteractiveFailed", ex.Message)); }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task PickUpInTerminalAsync(TaskRowViewModel? row)
|
||||
{
|
||||
if (row is null || _worker is null) return;
|
||||
ForegroundHelper.AllowAny();
|
||||
try { await _worker.ResumeTaskInTerminalAsync(row.Id); }
|
||||
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("vm.tasksIsland.pickUpInTerminalFailed", ex.Message)); }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ResumePlanningSessionAsync(TaskRowViewModel? row)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
x:Class="ClaudeDo.Ui.Views.Islands.Detail.TaskHeaderBar"
|
||||
x:DataType="vm:DetailsIslandViewModel">
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
|
||||
|
||||
<!-- Column 0: id badge + editable title -->
|
||||
<StackPanel Grid.Column="0" Spacing="0">
|
||||
@@ -27,8 +27,18 @@
|
||||
Padding="0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Column 1: trash button (not running) -->
|
||||
<!-- Column 1: pick up in terminal (review / failed) -->
|
||||
<Button Grid.Column="1" Classes="icon-btn"
|
||||
Command="{Binding PickUpInTerminalCommand}"
|
||||
ToolTip.Tip="{loc:Tr details.pickUpInTerminalTip}"
|
||||
IsVisible="{Binding CanPickUpInTerminal}"
|
||||
VerticalAlignment="Top"
|
||||
Margin="6,0,0,0">
|
||||
<PathIcon Data="{StaticResource Icon.ArrowOut}" Width="14" Height="14"/>
|
||||
</Button>
|
||||
|
||||
<!-- Column 2: trash button (not running) -->
|
||||
<Button Grid.Column="2" Classes="icon-btn"
|
||||
Command="{Binding DeleteTaskCommand}"
|
||||
ToolTip.Tip="{loc:Tr details.deleteTaskTip}"
|
||||
IsVisible="{Binding !IsRunning}"
|
||||
@@ -38,8 +48,8 @@
|
||||
Foreground="{DynamicResource BloodBrush}"/>
|
||||
</Button>
|
||||
|
||||
<!-- Column 1: skull button (running) -->
|
||||
<Button Grid.Column="1" Classes="icon-btn"
|
||||
<!-- Column 2: skull button (running) -->
|
||||
<Button Grid.Column="2" Classes="icon-btn"
|
||||
Command="{Binding StopCommand}"
|
||||
ToolTip.Tip="{loc:Tr details.killSessionTip}"
|
||||
IsVisible="{Binding IsRunning}"
|
||||
@@ -49,8 +59,8 @@
|
||||
Foreground="{DynamicResource BloodBrush}"/>
|
||||
</Button>
|
||||
|
||||
<!-- Column 2: gear button with agent settings flyout -->
|
||||
<Button Grid.Column="2" Classes="icon-btn"
|
||||
<!-- Column 3: gear button with agent settings flyout -->
|
||||
<Button Grid.Column="3" Classes="icon-btn"
|
||||
ToolTip.Tip="{loc:Tr details.agentSettingsTip}"
|
||||
IsEnabled="{Binding AgentSettings.IsEnabled}"
|
||||
VerticalAlignment="Top"
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
<Separator/>
|
||||
<MenuItem Header="{loc:Tr tasks.ctxRunInteractively}"
|
||||
Click="OnRunInteractivelyClick"/>
|
||||
<MenuItem Header="{loc:Tr tasks.ctxPickUpInTerminal}"
|
||||
Click="OnPickUpInTerminalClick"
|
||||
IsVisible="{Binding CanPickUpInTerminal}"/>
|
||||
<MenuItem Header="{loc:Tr tasks.ctxOpenPlanningSession}"
|
||||
Click="OnOpenPlanningSessionClick"
|
||||
IsVisible="{Binding CanOpenPlanningSession}"/>
|
||||
|
||||
@@ -67,6 +67,12 @@ public partial class TaskRowView : UserControl
|
||||
await vm.RunInteractivelyCommand.ExecuteAsync(row);
|
||||
}
|
||||
|
||||
private async void OnPickUpInTerminalClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||
await vm.PickUpInTerminalCommand.ExecuteAsync(row);
|
||||
}
|
||||
|
||||
private async void OnResumePlanningSessionClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||
|
||||
Reference in New Issue
Block a user