6.8 KiB
Continue & Reset Buttons for Failed Tasks
Problem
When a task ends in Failed status (Claude exited without marking the work done, cancelled mid-run, crashed, etc.), the user has no way to act on it from the UI:
- Nudging the agent is only possible via the hub method
ContinueTask, which is not wired into the UI. - Rolling back the worktree requires shelling into git manually to remove the branch and folder, then editing the task in the DB. In practice the worktree is just abandoned.
We want two explicit actions in the details pane for a failed task: Continue (resume the Claude session with a follow-up prompt) and Reset (discard the worktree and return the task to an editable Manual state).
Scope
- Actions are shown only when the selected task has
Status == Failed. Continueis the multi-turn mechanism already implemented inTaskRunner.ContinueAsync— this spec only wires it into the UI.Resetis new end-to-end (hub method, worktree discard, task status reset).- Run history (
task_runsrows) is preserved across a Reset for audit. - Out of scope: Continue/Reset on
Donetasks, undo of Reset, modifying the follow-up prompt before sending.
UX
Both buttons live in DetailsIslandView, inside a new horizontal button row that is visible only when the currently selected task is Failed.
Continue
- One-click. Sends the canned prompt
"Continue working on this task."viaWorkerHub.ContinueTask(taskId, prompt). - Enabled only if the task's latest
TaskRunEntityhas a non-nullSessionId. - When disabled, a tooltip reads
No session to resume. - No confirmation dialog.
Reset
- Always enabled when the task is
Failed. - Opens a confirmation dialog:
Discard worktree and reset task? This deletes branch
claudedo/<id>and all uncommitted changes. - On confirm, calls
WorkerHub.ResetTask(taskId).
Backend
New hub method — WorkerHub.ResetTask(string taskId)
Preconditions:
- Task exists.
- Task status is not
Running. If it is, throw — resetting a task that is actively executing would race with the runner.
Steps:
- Load the task and its worktree (if any).
- If a worktree exists and its
State == Active, callWorktreeManager.DiscardAsync(worktree, ct)(see below). - Call
TaskRepository.ResetToManualAsync(taskId, ct)to clear the result fields and flip the status. - Broadcast
TaskUpdated(taskId); broadcastWorktreeUpdated(taskId)if the worktree state changed.
If WorktreeManager.DiscardAsync throws (e.g. folder locked, branch checked out elsewhere), the hub method surfaces the error to the caller and leaves the task as Failed with the worktree still Active, so the user can retry. TaskRepository.ResetToManualAsync is not called in the failure path.
New — WorktreeManager.DiscardAsync(WorktreeEntity wt, CancellationToken ct)
Shape mirrors the existing CommitIfChangedAsync. Steps:
git worktree remove --force <wt.Path>viaGitService. The--forceflag drops any uncommitted changes — expected, since the user already confirmed.git branch -D <wt.BranchName>viaGitService.- Update
WorktreeRepository: setState = Discarded.
GitService gains two thin wrappers if they do not already exist: WorktreeRemoveAsync(path, force: true) and BranchDeleteForceAsync(branch).
New — TaskRepository.ResetToManualAsync(string taskId, CancellationToken ct)
Single UPDATE that sets:
Status = ManualResult = nullStartedAt = nullFinishedAt = null
LogPath and the task_runs rows are left intact — they are the audit trail.
Continue wiring
No backend changes. The UI calls WorkerHub.ContinueTask(taskId, prompt) and TaskRunner.ContinueAsync handles the rest.
UI
DetailsIslandViewModel
New members:
[ObservableProperty] bool showFailedActions— true when the selected task's status isFailed.[ObservableProperty] bool canContinue— true whenshowFailedActionsand the latest run of the selected task has a non-nullSessionId.[RelayCommand(CanExecute = nameof(CanContinue))] Task ContinueAsync()— callsHubClient.ContinueTask(task.Id, "Continue working on this task.").[RelayCommand(CanExecute = nameof(ShowFailedActions))] Task ResetAsync()— opens confirmation; on confirm, callsHubClient.ResetTask(task.Id).
ShowFailedActions and CanContinue recompute whenever the selected task or its runs change (subscribe to the existing selection / task-updated signals).
DetailsIslandView.axaml
A single StackPanel (orientation horizontal) inside the existing details layout, bound to ShowFailedActions for visibility, with two Buttons wired to the commands.
Confirmation dialog
Reuse the existing modal pattern (see WorktreeModalView for the shape). A minimal ConfirmDialog with title, body, Cancel + Confirm buttons is acceptable and reusable; if a simpler inline approach is idiomatic in this codebase, use that instead.
HubClient
Add Task ResetTask(string taskId) alongside the existing ContinueTask wrapper.
Error handling
| Failure | Behaviour |
|---|---|
ResetTask called on a Running task |
Hub throws; UI shows the error. The Reset button is CanExecute-gated anyway, so this is a defensive check. |
git worktree remove fails |
Hub throws; task stays Failed, worktree stays Active, user can retry or clean up manually. |
git branch -D fails after worktree removal succeeded |
Worktree state still gets set to Discarded (the folder is gone; leaving the branch dangling is less bad than leaving the DB out of sync). Log a warning. |
Continue with no session_id |
Button is disabled — the call cannot happen from the UI. Hub still guards with the existing InvalidOperationException in ContinueAsync for safety. |
Testing
Integration tests (real SQLite, real git) in ClaudeDo.Worker.Tests:
WorktreeManager_DiscardAsync_removes_worktree_and_branch— create a worktree, call Discard, assert branch is gone fromgit branch --list, folder is gone, DB state isDiscarded.TaskRepository_ResetToManualAsync_clears_result_fields— seed a Failed task with Result/FinishedAt/StartedAt, call Reset, assert all cleared and status is Manual.ResetTask_full_flow— seed a Failed task with an Active worktree and run history; invoke the hub method; assert status=Manual, worktree=Discarded,task_runsrows still present.ResetTask_rejects_running_task— seed a Running task, assert the hub method throws and nothing is modified.ResetTask_worktree_remove_failure_leaves_task_failed— simulate a git failure (e.g. lock the folder), assert task stays Failed and worktree stays Active.
No new UI tests — the commands are thin forwarders and are exercised manually.
Open questions
None.