feat(ui): pick up a task's session in a terminal
This commit is contained in:
@@ -116,6 +116,7 @@
|
|||||||
"ctxMarkDone": "Erledigt",
|
"ctxMarkDone": "Erledigt",
|
||||||
"ctxMarkCancelled": "Abgebrochen",
|
"ctxMarkCancelled": "Abgebrochen",
|
||||||
"ctxRunInteractively": "Interaktiv ausführen",
|
"ctxRunInteractively": "Interaktiv ausführen",
|
||||||
|
"ctxPickUpInTerminal": "Im Terminal fortsetzen",
|
||||||
"ctxOpenPlanningSession": "Planungssitzung öffnen",
|
"ctxOpenPlanningSession": "Planungssitzung öffnen",
|
||||||
"ctxResumePlanningSession": "Planungssitzung fortsetzen",
|
"ctxResumePlanningSession": "Planungssitzung fortsetzen",
|
||||||
"ctxFinalizePlanningSession": "Plan finalisieren",
|
"ctxFinalizePlanningSession": "Plan finalisieren",
|
||||||
@@ -162,6 +163,7 @@
|
|||||||
"addReposTip": "Repos als Listen hinzufügen"
|
"addReposTip": "Repos als Listen hinzufügen"
|
||||||
},
|
},
|
||||||
"details": {
|
"details": {
|
||||||
|
"pickUpInTerminalTip": "Diese Sitzung im Terminal fortsetzen",
|
||||||
"deleteTaskTip": "Aufgabe löschen",
|
"deleteTaskTip": "Aufgabe löschen",
|
||||||
"killSessionTip": "Laufende Sitzung beenden",
|
"killSessionTip": "Laufende Sitzung beenden",
|
||||||
"closeTip": "Schließen",
|
"closeTip": "Schließen",
|
||||||
@@ -492,7 +494,7 @@
|
|||||||
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Verbesserungen", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt" },
|
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Verbesserungen", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt" },
|
||||||
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
|
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
|
||||||
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
|
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
|
||||||
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "runInteractiveFailed": "Interaktiv ausführen fehlgeschlagen: {0}", "planningOpenFailed": "Planungssitzung konnte nicht geöffnet werden: {0}" },
|
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "runInteractiveFailed": "Interaktiv ausführen fehlgeschlagen: {0}", "planningOpenFailed": "Planungssitzung konnte nicht geöffnet werden: {0}", "pickUpInTerminalFailed": "Im Terminal fortsetzen fehlgeschlagen: {0}" },
|
||||||
"diff": { "loadFailed": "Diff konnte nicht geladen werden: {0}", "noChanges": "Keine Änderungen anzuzeigen.", "unavailable": "Diff nicht mehr verfügbar — Commit-Bereich unvollständig." },
|
"diff": { "loadFailed": "Diff konnte nicht geladen werden: {0}", "noChanges": "Keine Änderungen anzuzeigen.", "unavailable": "Diff nicht mehr verfügbar — Commit-Bereich unvollständig." },
|
||||||
"planningDiff": { "hubError": "Kombinierte Vorschau konnte nicht erstellt werden (Hub-Fehler).", "conflict": "Kombinierte Vorschau nicht möglich: Teilaufgabe {0} steht im Konflikt mit einer früheren Teilaufgabe ({1} Dateien)." },
|
"planningDiff": { "hubError": "Kombinierte Vorschau konnte nicht erstellt werden (Hub-Fehler).", "conflict": "Kombinierte Vorschau nicht möglich: Teilaufgabe {0} steht im Konflikt mit einer früheren Teilaufgabe ({1} Dateien)." },
|
||||||
"merge": { "commitMessage": "Merge-Aufgabe: {0}", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "conflict": "Merge-Konflikt — Ziel-Branch wiederhergestellt. Manuell oder über Fortsetzen lösen, dann erneut versuchen.", "blocked": "Blockiert: {0}", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
|
"merge": { "commitMessage": "Merge-Aufgabe: {0}", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "conflict": "Merge-Konflikt — Ziel-Branch wiederhergestellt. Manuell oder über Fortsetzen lösen, dann erneut versuchen.", "blocked": "Blockiert: {0}", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
|
||||||
|
|||||||
@@ -116,6 +116,7 @@
|
|||||||
"ctxMarkDone": "Done",
|
"ctxMarkDone": "Done",
|
||||||
"ctxMarkCancelled": "Cancelled",
|
"ctxMarkCancelled": "Cancelled",
|
||||||
"ctxRunInteractively": "Run interactively",
|
"ctxRunInteractively": "Run interactively",
|
||||||
|
"ctxPickUpInTerminal": "Pick up in terminal",
|
||||||
"ctxOpenPlanningSession": "Open planning Session",
|
"ctxOpenPlanningSession": "Open planning Session",
|
||||||
"ctxResumePlanningSession": "Resume planning Session",
|
"ctxResumePlanningSession": "Resume planning Session",
|
||||||
"ctxFinalizePlanningSession": "Finalize plan",
|
"ctxFinalizePlanningSession": "Finalize plan",
|
||||||
@@ -162,6 +163,7 @@
|
|||||||
"addReposTip": "Add repos as lists"
|
"addReposTip": "Add repos as lists"
|
||||||
},
|
},
|
||||||
"details": {
|
"details": {
|
||||||
|
"pickUpInTerminalTip": "Pick up this session in a terminal",
|
||||||
"deleteTaskTip": "Delete task",
|
"deleteTaskTip": "Delete task",
|
||||||
"killSessionTip": "Kill the running session",
|
"killSessionTip": "Kill the running session",
|
||||||
"closeTip": "Close",
|
"closeTip": "Close",
|
||||||
@@ -492,7 +494,7 @@
|
|||||||
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Improvements", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked" },
|
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Improvements", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked" },
|
||||||
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
|
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
|
||||||
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
|
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
|
||||||
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "runInteractiveFailed": "Run interactively failed: {0}", "planningOpenFailed": "Couldn't open planning session: {0}" },
|
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "runInteractiveFailed": "Run interactively failed: {0}", "planningOpenFailed": "Couldn't open planning session: {0}", "pickUpInTerminalFailed": "Pick up in terminal failed: {0}" },
|
||||||
"diff": { "loadFailed": "Failed to load diff: {0}", "noChanges": "No changes to show.", "unavailable": "Diff no longer available — commit range incomplete." },
|
"diff": { "loadFailed": "Failed to load diff: {0}", "noChanges": "No changes to show.", "unavailable": "Diff no longer available — commit range incomplete." },
|
||||||
"planningDiff": { "hubError": "Could not build combined preview (hub error).", "conflict": "Cannot build combined preview: subtask {0} conflicts with an earlier subtask ({1} files)." },
|
"planningDiff": { "hubError": "Could not build combined preview (hub error).", "conflict": "Cannot build combined preview: subtask {0} conflicts with an earlier subtask ({1} files)." },
|
||||||
"merge": { "commitMessage": "Merge task: {0}", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "conflict": "Merge conflict — target branch restored. Resolve manually or via Continue, then retry.", "blocked": "Blocked: {0}", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
|
"merge": { "commitMessage": "Merge task: {0}", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "conflict": "Merge conflict — target branch restored. Resolve manually or via Continue, then retry.", "blocked": "Blocked: {0}", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task AbortConflictMergeAsync(string taskId);
|
Task AbortConflictMergeAsync(string taskId);
|
||||||
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
Task OpenInteractiveTerminalAsync(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 ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default);
|
Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default);
|
||||||
Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, 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)
|
public async Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync("OpenInteractiveTerminalAsync", taskId, ct);
|
=> 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)
|
public async Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync<DiscardPlanningOutcome>("DiscardPlanningSessionAsync", taskId, dequeueQueuedChildren, ct);
|
=> 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);
|
Merge.SyncTaskContext(Task?.Id, Task?.Title, Task?.IsPlanningParent == true);
|
||||||
NotifySessionSections();
|
NotifySessionSections();
|
||||||
OnPropertyChanged(nameof(CanAcceptDrop));
|
OnPropertyChanged(nameof(CanAcceptDrop));
|
||||||
|
OnPropertyChanged(nameof(CanPickUpInTerminal));
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -892,6 +893,24 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
catch { /* offline */ }
|
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))]
|
[RelayCommand(CanExecute = nameof(CanEnqueue))]
|
||||||
private async System.Threading.Tasks.Task EnqueueAsync()
|
private async System.Threading.Tasks.Task EnqueueAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
&& !IsChild;
|
&& !IsChild;
|
||||||
public bool CanResumeOrDiscardPlanning => PlanningPhase == PlanningPhase.Active;
|
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
|
public string? PlanningBadge => PlanningPhase switch
|
||||||
{
|
{
|
||||||
PlanningPhase.Active => Loc.T("vm.planningBadge.active"),
|
PlanningPhase.Active => Loc.T("vm.planningBadge.active"),
|
||||||
@@ -141,6 +146,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
OnPropertyChanged(nameof(StatusLabel));
|
OnPropertyChanged(nameof(StatusLabel));
|
||||||
OnPropertyChanged(nameof(IsRunning));
|
OnPropertyChanged(nameof(IsRunning));
|
||||||
OnPropertyChanged(nameof(IsWaitingForReview));
|
OnPropertyChanged(nameof(IsWaitingForReview));
|
||||||
|
OnPropertyChanged(nameof(CanPickUpInTerminal));
|
||||||
OnPropertyChanged(nameof(IsParked));
|
OnPropertyChanged(nameof(IsParked));
|
||||||
OnPropertyChanged(nameof(IsQueued));
|
OnPropertyChanged(nameof(IsQueued));
|
||||||
OnPropertyChanged(nameof(IsWaiting));
|
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)); }
|
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]
|
[RelayCommand]
|
||||||
private async Task ResumePlanningSessionAsync(TaskRowViewModel? row)
|
private async Task ResumePlanningSessionAsync(TaskRowViewModel? row)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
x:Class="ClaudeDo.Ui.Views.Islands.Detail.TaskHeaderBar"
|
x:Class="ClaudeDo.Ui.Views.Islands.Detail.TaskHeaderBar"
|
||||||
x:DataType="vm:DetailsIslandViewModel">
|
x:DataType="vm:DetailsIslandViewModel">
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
|
||||||
|
|
||||||
<!-- Column 0: id badge + editable title -->
|
<!-- Column 0: id badge + editable title -->
|
||||||
<StackPanel Grid.Column="0" Spacing="0">
|
<StackPanel Grid.Column="0" Spacing="0">
|
||||||
@@ -27,8 +27,18 @@
|
|||||||
Padding="0"/>
|
Padding="0"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Column 1: trash button (not running) -->
|
<!-- Column 1: pick up in terminal (review / failed) -->
|
||||||
<Button Grid.Column="1" Classes="icon-btn"
|
<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}"
|
Command="{Binding DeleteTaskCommand}"
|
||||||
ToolTip.Tip="{loc:Tr details.deleteTaskTip}"
|
ToolTip.Tip="{loc:Tr details.deleteTaskTip}"
|
||||||
IsVisible="{Binding !IsRunning}"
|
IsVisible="{Binding !IsRunning}"
|
||||||
@@ -38,8 +48,8 @@
|
|||||||
Foreground="{DynamicResource BloodBrush}"/>
|
Foreground="{DynamicResource BloodBrush}"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- Column 1: skull button (running) -->
|
<!-- Column 2: skull button (running) -->
|
||||||
<Button Grid.Column="1" Classes="icon-btn"
|
<Button Grid.Column="2" Classes="icon-btn"
|
||||||
Command="{Binding StopCommand}"
|
Command="{Binding StopCommand}"
|
||||||
ToolTip.Tip="{loc:Tr details.killSessionTip}"
|
ToolTip.Tip="{loc:Tr details.killSessionTip}"
|
||||||
IsVisible="{Binding IsRunning}"
|
IsVisible="{Binding IsRunning}"
|
||||||
@@ -49,8 +59,8 @@
|
|||||||
Foreground="{DynamicResource BloodBrush}"/>
|
Foreground="{DynamicResource BloodBrush}"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- Column 2: gear button with agent settings flyout -->
|
<!-- Column 3: gear button with agent settings flyout -->
|
||||||
<Button Grid.Column="2" Classes="icon-btn"
|
<Button Grid.Column="3" Classes="icon-btn"
|
||||||
ToolTip.Tip="{loc:Tr details.agentSettingsTip}"
|
ToolTip.Tip="{loc:Tr details.agentSettingsTip}"
|
||||||
IsEnabled="{Binding AgentSettings.IsEnabled}"
|
IsEnabled="{Binding AgentSettings.IsEnabled}"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
|
|||||||
@@ -51,6 +51,9 @@
|
|||||||
<Separator/>
|
<Separator/>
|
||||||
<MenuItem Header="{loc:Tr tasks.ctxRunInteractively}"
|
<MenuItem Header="{loc:Tr tasks.ctxRunInteractively}"
|
||||||
Click="OnRunInteractivelyClick"/>
|
Click="OnRunInteractivelyClick"/>
|
||||||
|
<MenuItem Header="{loc:Tr tasks.ctxPickUpInTerminal}"
|
||||||
|
Click="OnPickUpInTerminalClick"
|
||||||
|
IsVisible="{Binding CanPickUpInTerminal}"/>
|
||||||
<MenuItem Header="{loc:Tr tasks.ctxOpenPlanningSession}"
|
<MenuItem Header="{loc:Tr tasks.ctxOpenPlanningSession}"
|
||||||
Click="OnOpenPlanningSessionClick"
|
Click="OnOpenPlanningSessionClick"
|
||||||
IsVisible="{Binding CanOpenPlanningSession}"/>
|
IsVisible="{Binding CanOpenPlanningSession}"/>
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ public partial class TaskRowView : UserControl
|
|||||||
await vm.RunInteractivelyCommand.ExecuteAsync(row);
|
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)
|
private async void OnResumePlanningSessionClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
|||||||
public virtual Task AbortConflictMergeAsync(string taskId) => Task.CompletedTask;
|
public virtual Task AbortConflictMergeAsync(string taskId) => Task.CompletedTask;
|
||||||
public virtual Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public virtual Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public virtual Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public virtual Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
|
public virtual Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public virtual Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public virtual Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public virtual Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
|
public virtual Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
|
||||||
=> Task.FromResult(new DiscardPlanningOutcome(DiscardPlanningResult.Discarded, 0, 0));
|
=> Task.FromResult(new DiscardPlanningOutcome(DiscardPlanningResult.Discarded, 0, 0));
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ sealed class FakeWorkerClient : IWorkerClient
|
|||||||
public Task WakeQueueAsync() { WakeQueueCalls++; return Task.CompletedTask; }
|
public Task WakeQueueAsync() { WakeQueueCalls++; return Task.CompletedTask; }
|
||||||
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) { StartPlanningCalls++; return Task.CompletedTask; }
|
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) { StartPlanningCalls++; return Task.CompletedTask; }
|
||||||
public Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
|
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 QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask;
|
public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) { ResumePlanningCalls++; return Task.CompletedTask; }
|
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) { ResumePlanningCalls++; return Task.CompletedTask; }
|
||||||
public Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
|
public Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
|
||||||
@@ -177,6 +180,28 @@ public class TasksIslandViewModelPlanningTests
|
|||||||
PlanningPhase phase = PlanningPhase.None)
|
PlanningPhase phase = PlanningPhase.None)
|
||||||
=> new TaskRowViewModel { Id = id, Status = status, ParentTaskId = parentId, PlanningPhase = phase };
|
=> new TaskRowViewModel { Id = id, Status = status, ParentTaskId = parentId, PlanningPhase = phase };
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PickUpInTerminal_InvokesWorkerWithTaskId()
|
||||||
|
{
|
||||||
|
var row = MakeRow("t1", TaskStatus.WaitingForReview);
|
||||||
|
var (vm, worker) = VmFactory.Create([row]);
|
||||||
|
|
||||||
|
await vm.PickUpInTerminalCommand.ExecuteAsync(row);
|
||||||
|
|
||||||
|
Assert.Equal(1, worker.PickUpInTerminalCalls);
|
||||||
|
Assert.Equal("t1", worker.LastPickUpTaskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanPickUpInTerminal_OnlyForReviewOrFailed()
|
||||||
|
{
|
||||||
|
Assert.True(MakeRow("a", TaskStatus.WaitingForReview).CanPickUpInTerminal);
|
||||||
|
Assert.True(MakeRow("b", TaskStatus.Failed).CanPickUpInTerminal);
|
||||||
|
Assert.False(MakeRow("c", TaskStatus.Idle).CanPickUpInTerminal);
|
||||||
|
Assert.False(MakeRow("d", TaskStatus.Running).CanPickUpInTerminal);
|
||||||
|
Assert.False(MakeRow("e", TaskStatus.Done).CanPickUpInTerminal);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PlanningParentWithChildren_CollapsedByDefault_ToggleExpands()
|
public void PlanningParentWithChildren_CollapsedByDefault_ToggleExpands()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user