From a3bb557d7675c09224a011a993d8333f2fcd8388 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 21 Apr 2026 16:43:54 +0200 Subject: [PATCH] docs: add spec for continue and reset buttons on failed tasks --- ...-continue-and-reset-failed-tasks-design.md | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-21-continue-and-reset-failed-tasks-design.md diff --git a/docs/superpowers/specs/2026-04-21-continue-and-reset-failed-tasks-design.md b/docs/superpowers/specs/2026-04-21-continue-and-reset-failed-tasks-design.md new file mode 100644 index 0000000..8376abd --- /dev/null +++ b/docs/superpowers/specs/2026-04-21-continue-and-reset-failed-tasks-design.md @@ -0,0 +1,130 @@ +# 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`**. +- `Continue` is the multi-turn mechanism already implemented in `TaskRunner.ContinueAsync` — this spec only wires it into the UI. +- `Reset` is new end-to-end (hub method, worktree discard, task status reset). +- Run history (`task_runs` rows) is **preserved** across a Reset for audit. +- Out of scope: Continue/Reset on `Done` tasks, 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."` via `WorkerHub.ContinueTask(taskId, prompt)`. +- Enabled **only if** the task's latest `TaskRunEntity` has a non-null `SessionId`. +- 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/` 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: + +1. Load the task and its worktree (if any). +2. If a worktree exists and its `State == Active`, call `WorktreeManager.DiscardAsync(worktree, ct)` (see below). +3. Call `TaskRepository.ResetToManualAsync(taskId, ct)` to clear the result fields and flip the status. +4. Broadcast `TaskUpdated(taskId)`; broadcast `WorktreeUpdated(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: + +1. `git worktree remove --force ` via `GitService`. The `--force` flag drops any uncommitted changes — expected, since the user already confirmed. +2. `git branch -D ` via `GitService`. +3. Update `WorktreeRepository`: set `State = 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 = Manual` +- `Result = null` +- `StartedAt = null` +- `FinishedAt = 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 is `Failed`. +- `[ObservableProperty] bool canContinue` — true when `showFailedActions` **and** the latest run of the selected task has a non-null `SessionId`. +- `[RelayCommand(CanExecute = nameof(CanContinue))] Task ContinueAsync()` — calls `HubClient.ContinueTask(task.Id, "Continue working on this task.")`. +- `[RelayCommand(CanExecute = nameof(ShowFailedActions))] Task ResetAsync()` — opens confirmation; on confirm, calls `HubClient.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 `Button`s 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`: + +1. **`WorktreeManager_DiscardAsync_removes_worktree_and_branch`** — create a worktree, call Discard, assert branch is gone from `git branch --list`, folder is gone, DB state is `Discarded`. +2. **`TaskRepository_ResetToManualAsync_clears_result_fields`** — seed a Failed task with Result/FinishedAt/StartedAt, call Reset, assert all cleared and status is Manual. +3. **`ResetTask_full_flow`** — seed a Failed task with an Active worktree and run history; invoke the hub method; assert status=Manual, worktree=Discarded, `task_runs` rows still present. +4. **`ResetTask_rejects_running_task`** — seed a Running task, assert the hub method throws and nothing is modified. +5. **`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.