docs: add spec for continue and reset buttons on failed tasks
This commit is contained in:
@@ -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/<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:
|
||||
|
||||
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 <wt.Path>` via `GitService`. The `--force` flag drops any uncommitted changes — expected, since the user already confirmed.
|
||||
2. `git branch -D <wt.BranchName>` 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.
|
||||
Reference in New Issue
Block a user