diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index dc1c2908..435f2b63 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -116,6 +116,7 @@ "ctxMarkDone": "Erledigt", "ctxMarkCancelled": "Abgebrochen", "ctxRunInteractively": "Interaktiv ausführen", + "ctxPickUpInTerminal": "Im Terminal fortsetzen", "ctxOpenPlanningSession": "Planungssitzung öffnen", "ctxResumePlanningSession": "Planungssitzung fortsetzen", "ctxFinalizePlanningSession": "Plan finalisieren", @@ -162,6 +163,7 @@ "addReposTip": "Repos als Listen hinzufügen" }, "details": { + "pickUpInTerminalTip": "Diese Sitzung im Terminal fortsetzen", "deleteTaskTip": "Aufgabe löschen", "killSessionTip": "Laufende Sitzung beenden", "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" }, "planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" }, "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." }, "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}" }, diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 679d6413..90b39cab 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -116,6 +116,7 @@ "ctxMarkDone": "Done", "ctxMarkCancelled": "Cancelled", "ctxRunInteractively": "Run interactively", + "ctxPickUpInTerminal": "Pick up in terminal", "ctxOpenPlanningSession": "Open planning Session", "ctxResumePlanningSession": "Resume planning Session", "ctxFinalizePlanningSession": "Finalize plan", @@ -162,6 +163,7 @@ "addReposTip": "Add repos as lists" }, "details": { + "pickUpInTerminalTip": "Pick up this session in a terminal", "deleteTaskTip": "Delete task", "killSessionTip": "Kill the running session", "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" }, "planningBadge": { "active": "PLANNING", "finalized": "PLANNED" }, "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." }, "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}" }, diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs index 77d74fd7..f2681b5d 100644 --- a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs @@ -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 DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default); Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default); diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index ca04d535..c9606501 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -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 DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default) => await _hub.InvokeAsync("DiscardPlanningSessionAsync", taskId, dequeueQueuedChildren, ct); diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index 4aaf4bfd..64f87d60 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -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() { diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs index e0721bc7..990c251d 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs @@ -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)); diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index dc98b1d3..5e3a3ece 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -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) { diff --git a/src/ClaudeDo.Ui/Views/Islands/Detail/TaskHeaderBar.axaml b/src/ClaudeDo.Ui/Views/Islands/Detail/TaskHeaderBar.axaml index 7de712ab..77ac818f 100644 --- a/src/ClaudeDo.Ui/Views/Islands/Detail/TaskHeaderBar.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/Detail/TaskHeaderBar.axaml @@ -6,7 +6,7 @@ x:Class="ClaudeDo.Ui.Views.Islands.Detail.TaskHeaderBar" x:DataType="vm:DetailsIslandViewModel"> - + @@ -27,8 +27,18 @@ Padding="0"/> - + + + + - - - -