# Continue & Reset Buttons for Failed Tasks — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add two buttons (Continue, Reset) to the details pane for `Failed` tasks so the user can either nudge the agent to continue or discard the worktree and return the task to `Manual`. **Architecture:** Spec is at `docs/superpowers/specs/2026-04-21-continue-and-reset-failed-tasks-design.md`. Backend adds one git-discard helper, one task-repository method, a small orchestration service, and a new hub method `ResetTask`. `ContinueTask` is already wired in the hub. UI adds two commands in `DetailsIslandViewModel` and a button row in `DetailsIslandView`. **Tech Stack:** .NET 8, EF Core (SQLite), ASP.NET Core SignalR, Avalonia 12, CommunityToolkit.Mvvm, xUnit 2.5 (integration tests with real SQLite + real git). --- ## File Structure **New files:** - `src/ClaudeDo.Worker/Services/TaskResetService.cs` — orchestrates the reset (load task, discard worktree, reset DB row, broadcast). - `tests/ClaudeDo.Worker.Tests/Services/TaskResetServiceTests.cs` — integration tests for the orchestration. **Modified files:** - `src/ClaudeDo.Worker/Runner/WorktreeManager.cs` — add `DiscardAsync`. - `src/ClaudeDo.Data/Repositories/TaskRepository.cs` — add `ResetToManualAsync`. - `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — add `ResetTask` endpoint; DI-inject the new service. - `src/ClaudeDo.Worker/Program.cs` — register `TaskResetService` in DI. - `src/ClaudeDo.Ui/Services/WorkerClient.cs` — add `ContinueTaskAsync` and `ResetTaskAsync` wrappers. - `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` — add observable properties and commands. - `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml` — add button row bound to the new commands. - `tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs` — add `DiscardAsync` test. - `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryTests.cs` — add `ResetToManualAsync` test. --- ## Task 1: `WorktreeManager.DiscardAsync` (TDD) **Files:** - Modify: `src/ClaudeDo.Worker/Runner/WorktreeManager.cs` - Test: `tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs` `GitService` already exposes `WorktreeRemoveAsync(workingDir, path, force, ct)` and `BranchDeleteAsync(workingDir, branch, force, ct)` — verify via `git grep -n "public async Task WorktreeRemoveAsync\|public async Task BranchDeleteAsync" src/ClaudeDo.Data/Git`. If either is missing, stop and add the git wrapper first. - [ ] **Step 1: Add the failing test** Add at the bottom of `tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs` (before the `Dispose` method): ```csharp [Fact] public async Task DiscardAsync_RemovesWorktreeAndBranch_AndSetsStateDiscarded() { if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; } var repo = CreateRepo(); var (task, list) = MakeEntities(repo.RepoDir); var (mgr, db) = await CreateManagerAsync(task, list); var ctx = await mgr.CreateAsync(task, list, CancellationToken.None); var worktreePath = ctx.WorktreePath; WorktreeEntity wt; using (var readCtx = db.CreateContext()) wt = (await new WorktreeRepository(readCtx).GetByTaskIdAsync(task.Id))!; await mgr.DiscardAsync(wt, list.WorkingDir!, CancellationToken.None); Assert.False(Directory.Exists(worktreePath), "worktree directory should be gone"); using var readCtx2 = db.CreateContext(); var row = await new WorktreeRepository(readCtx2).GetByTaskIdAsync(task.Id); Assert.NotNull(row); Assert.Equal(WorktreeState.Discarded, row!.State); // Branch should no longer exist on the main repo. var branchList = await new GitService().RunForOutputAsync(repo.RepoDir, new[] { "branch", "--list", ctx.BranchName }, CancellationToken.None); Assert.True(string.IsNullOrWhiteSpace(branchList), $"branch {ctx.BranchName} should be deleted, got: {branchList}"); } ``` Note on `RunForOutputAsync`: if `GitService` does not expose a generic run helper, replace the branch-check with a direct `System.Diagnostics.Process` invocation of `git branch --list ` in the test. If such a helper exists with a different name, use it. - [ ] **Step 2: Run test, verify it fails** Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~DiscardAsync_RemovesWorktreeAndBranch" -v minimal` Expected: FAIL — `DiscardAsync` does not exist. - [ ] **Step 3: Implement `DiscardAsync`** Add to `src/ClaudeDo.Worker/Runner/WorktreeManager.cs` after `CommitIfChangedAsync`: ```csharp public async Task DiscardAsync(WorktreeEntity wt, string workingDir, CancellationToken ct) { // Remove the git worktree first; --force drops uncommitted changes (user already confirmed). try { await _git.WorktreeRemoveAsync(workingDir, wt.Path, force: true, ct); } catch (Exception ex) { _logger.LogError(ex, "git worktree remove failed for {Path}", wt.Path); throw; } // Delete the branch. If worktree removal succeeded but branch delete fails, // we still record the worktree as Discarded — the folder is gone, and a dangling // branch is recoverable; leaving the DB out of sync is worse. try { await _git.BranchDeleteAsync(workingDir, wt.BranchName, force: true, ct); } catch (Exception ex) { _logger.LogWarning(ex, "git branch -D {Branch} failed after worktree removal; continuing", wt.BranchName); } using var context = _dbFactory.CreateDbContext(); var wtRepo = new WorktreeRepository(context); await wtRepo.SetStateAsync(wt.TaskId, WorktreeState.Discarded, ct); _logger.LogInformation("Discarded worktree for task {TaskId} (branch {Branch})", wt.TaskId, wt.BranchName); } ``` - [ ] **Step 4: Run test, verify it passes** Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~DiscardAsync_RemovesWorktreeAndBranch" -v minimal` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/ClaudeDo.Worker/Runner/WorktreeManager.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs git commit -m "feat(worker): add WorktreeManager.DiscardAsync for task reset" ``` --- ## Task 2: `TaskRepository.ResetToManualAsync` (TDD) **Files:** - Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs` - Test: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryTests.cs` - [ ] **Step 1: Add the failing test** Add to `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryTests.cs` (follow the existing test style in that file — reuse any helpers it already has for creating a list + task): ```csharp [Fact] public async Task ResetToManualAsync_ClearsResultFields_AndSetsStatusManual() { using var db = new DbFixture(); using var ctx = db.CreateContext(); var listRepo = new ListRepository(ctx); var taskRepo = new TaskRepository(ctx); var list = new ListEntity { Id = Guid.NewGuid().ToString(), Name = "L", CreatedAt = DateTime.UtcNow }; await listRepo.AddAsync(list); var task = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = list.Id, Title = "T", CreatedAt = DateTime.UtcNow, Status = TaskStatus.Failed, StartedAt = DateTime.UtcNow.AddMinutes(-5), FinishedAt = DateTime.UtcNow, Result = "boom", }; await taskRepo.AddAsync(task); await taskRepo.ResetToManualAsync(task.Id); using var readCtx = db.CreateContext(); var after = await new TaskRepository(readCtx).GetByIdAsync(task.Id); Assert.NotNull(after); Assert.Equal(TaskStatus.Manual, after!.Status); Assert.Null(after.StartedAt); Assert.Null(after.FinishedAt); Assert.Null(after.Result); } ``` - [ ] **Step 2: Run test, verify it fails** Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~ResetToManualAsync_ClearsResultFields" -v minimal` Expected: FAIL — `ResetToManualAsync` does not exist. - [ ] **Step 3: Implement `ResetToManualAsync`** Add to `src/ClaudeDo.Data/Repositories/TaskRepository.cs` inside the `#region Status transitions` block, after `FlipAllRunningToFailedAsync`: ```csharp public async Task ResetToManualAsync(string taskId, CancellationToken ct = default) { await _context.Tasks .Where(t => t.Id == taskId) .ExecuteUpdateAsync(s => s .SetProperty(t => t.Status, TaskStatus.Manual) .SetProperty(t => t.StartedAt, (DateTime?)null) .SetProperty(t => t.FinishedAt, (DateTime?)null) .SetProperty(t => t.Result, (string?)null), ct); } ``` - [ ] **Step 4: Run test, verify it passes** Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~ResetToManualAsync_ClearsResultFields" -v minimal` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/ClaudeDo.Data/Repositories/TaskRepository.cs tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryTests.cs git commit -m "feat(data): add TaskRepository.ResetToManualAsync" ``` --- ## Task 3: `TaskResetService` (TDD) **Files:** - Create: `src/ClaudeDo.Worker/Services/TaskResetService.cs` - Create: `tests/ClaudeDo.Worker.Tests/Services/TaskResetServiceTests.cs` This service orchestrates Task 1 + Task 2, plus the "reject if Running" safety check and the SignalR broadcast. - [ ] **Step 1: Add the failing test — happy path** Create `tests/ClaudeDo.Worker.Tests/Services/TaskResetServiceTests.cs`: ```csharp using ClaudeDo.Data; using ClaudeDo.Data.Git; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Config; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.Services; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.Extensions.Logging.Abstractions; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.Services; public class TaskResetServiceTests : IDisposable { private readonly List _fixtures = new(); private readonly List _dbFixtures = new(); private static bool GitAvailable => GitRepoFixture.IsGitAvailable(); [Fact] public async Task ResetAsync_FailedTaskWithWorktree_ClearsEverything_AndPreservesRunHistory() { if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; } var repo = new GitRepoFixture(); _fixtures.Add(repo); var db = new DbFixture(); _dbFixtures.Add(db); var list = new ListEntity { Id = Guid.NewGuid().ToString(), Name = "L", WorkingDir = repo.RepoDir, CreatedAt = DateTime.UtcNow }; var task = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = list.Id, Title = "T", CreatedAt = DateTime.UtcNow }; using (var seed = db.CreateContext()) { await new ListRepository(seed).AddAsync(list); await new TaskRepository(seed).AddAsync(task); } var wtMgr = new WorktreeManager(new GitService(), db.CreateFactory(), new WorkerConfig(), NullLogger.Instance); var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None); // Seed a Failed task with a run row (we'll assert it's preserved). using (var ctx = db.CreateContext()) { await new TaskRepository(ctx).MarkFailedAsync(task.Id, DateTime.UtcNow, "it broke"); await new TaskRunRepository(ctx).AddAsync(new TaskRunEntity { Id = Guid.NewGuid().ToString(), TaskId = task.Id, RunNumber = 1, IsRetry = false, Prompt = "p", SessionId = "s1", FinishedAt = DateTime.UtcNow, }); } var broadcaster = new FakeHubBroadcaster(); var svc = new TaskResetService(db.CreateFactory(), wtMgr, broadcaster, NullLogger.Instance); await svc.ResetAsync(task.Id, CancellationToken.None); using var readCtx = db.CreateContext(); var after = await new TaskRepository(readCtx).GetByIdAsync(task.Id); Assert.Equal(TaskStatus.Manual, after!.Status); Assert.Null(after.Result); Assert.Null(after.StartedAt); Assert.Null(after.FinishedAt); var wtAfter = await new WorktreeRepository(readCtx).GetByTaskIdAsync(task.Id); Assert.Equal(WorktreeState.Discarded, wtAfter!.State); Assert.False(Directory.Exists(wtCtx.WorktreePath)); var runs = await new TaskRunRepository(readCtx).GetByTaskIdAsync(task.Id); Assert.Single(runs); Assert.Contains(task.Id, broadcaster.TaskUpdatedIds); Assert.Contains(task.Id, broadcaster.WorktreeUpdatedIds); } [Fact] public async Task ResetAsync_RunningTask_Throws_AndDoesNotMutate() { var db = new DbFixture(); _dbFixtures.Add(db); var list = new ListEntity { Id = Guid.NewGuid().ToString(), Name = "L", CreatedAt = DateTime.UtcNow }; var task = new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = list.Id, Title = "T", CreatedAt = DateTime.UtcNow, Status = TaskStatus.Running, StartedAt = DateTime.UtcNow }; using (var seed = db.CreateContext()) { await new ListRepository(seed).AddAsync(list); await new TaskRepository(seed).AddAsync(task); } var wtMgr = new WorktreeManager(new GitService(), db.CreateFactory(), new WorkerConfig(), NullLogger.Instance); var svc = new TaskResetService(db.CreateFactory(), wtMgr, new FakeHubBroadcaster(), NullLogger.Instance); await Assert.ThrowsAsync(() => svc.ResetAsync(task.Id, CancellationToken.None)); using var readCtx = db.CreateContext(); var after = await new TaskRepository(readCtx).GetByIdAsync(task.Id); Assert.Equal(TaskStatus.Running, after!.Status); } public void Dispose() { foreach (var f in _fixtures) f.Dispose(); foreach (var d in _dbFixtures) d.Dispose(); } private sealed class FakeHubBroadcaster : HubBroadcaster { public List TaskUpdatedIds { get; } = new(); public List WorktreeUpdatedIds { get; } = new(); public FakeHubBroadcaster() : base(new FakeHubContext()) { } public new Task TaskUpdated(string taskId) { TaskUpdatedIds.Add(taskId); return Task.CompletedTask; } public new Task WorktreeUpdated(string taskId) { WorktreeUpdatedIds.Add(taskId); return Task.CompletedTask; } } } ``` Check existing fakes: the test file assumes `FakeHubContext` exists under `ClaudeDo.Worker.Tests.Infrastructure` (the Worker.Tests CLAUDE.md lists `FakeHubContext`, `FakeHubClients`, `FakeClientProxy`). If `HubBroadcaster` methods are not virtual, the `new` keyword above will not intercept calls — instead, use the real `HubBroadcaster` with `FakeHubContext` and inspect the fake's recorded calls. Adjust the test implementation to use whichever approach matches the existing test conventions (see `QueueServiceTests` for precedent). - [ ] **Step 2: Run tests, verify they fail** Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskResetServiceTests" -v minimal` Expected: FAIL — `TaskResetService` does not exist. - [ ] **Step 3: Implement `TaskResetService`** Create `src/ClaudeDo.Worker/Services/TaskResetService.cs`: ```csharp using ClaudeDo.Data; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Runner; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Services; public sealed class TaskResetService { private readonly IDbContextFactory _dbFactory; private readonly WorktreeManager _wtManager; private readonly HubBroadcaster _broadcaster; private readonly ILogger _logger; public TaskResetService( IDbContextFactory dbFactory, WorktreeManager wtManager, HubBroadcaster broadcaster, ILogger logger) { _dbFactory = dbFactory; _wtManager = wtManager; _broadcaster = broadcaster; _logger = logger; } public async Task ResetAsync(string taskId, CancellationToken ct) { bool worktreeChanged = false; using (var ctx = _dbFactory.CreateDbContext()) { var taskRepo = new TaskRepository(ctx); var task = await taskRepo.GetByIdAsync(taskId, ct) ?? throw new KeyNotFoundException($"Task '{taskId}' not found."); if (task.Status == TaskStatus.Running) throw new InvalidOperationException("Cannot reset a running task. Cancel it first."); var listRepo = new ListRepository(ctx); var list = await listRepo.GetByIdAsync(task.ListId, ct) ?? throw new InvalidOperationException("List not found."); var wtRepo = new WorktreeRepository(ctx); var wt = await wtRepo.GetByTaskIdAsync(taskId, ct); if (wt is not null && wt.State == Data.Models.WorktreeState.Active && list.WorkingDir is not null) { // DiscardAsync uses its own DbContext internally; we close this one first. ctx.Dispose(); await _wtManager.DiscardAsync(wt, list.WorkingDir, ct); worktreeChanged = true; } } using (var ctx = _dbFactory.CreateDbContext()) { var taskRepo = new TaskRepository(ctx); await taskRepo.ResetToManualAsync(taskId, ct); } await _broadcaster.TaskUpdated(taskId); if (worktreeChanged) await _broadcaster.WorktreeUpdated(taskId); _logger.LogInformation("Reset task {TaskId} to Manual (worktree discarded: {Discarded})", taskId, worktreeChanged); } } ``` Note: the `ctx.Dispose()` inside a `using` block works because `Dispose` is idempotent. If you prefer, refactor to scope the first block with `{ }` + explicit `await using` and move the dispose before the call. - [ ] **Step 4: Run tests, verify they pass** Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskResetServiceTests" -v minimal` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/ClaudeDo.Worker/Services/TaskResetService.cs tests/ClaudeDo.Worker.Tests/Services/TaskResetServiceTests.cs git commit -m "feat(worker): add TaskResetService for discard + reset flow" ``` --- ## Task 4: Wire `TaskResetService` into DI and add `WorkerHub.ResetTask` **Files:** - Modify: `src/ClaudeDo.Worker/Program.cs` - Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs` - [ ] **Step 1: Register the service in DI** Open `src/ClaudeDo.Worker/Program.cs`. Locate the block where `QueueService`, `WorktreeManager`, `HubBroadcaster`, `WorktreeMaintenanceService`, etc. are registered (look for `builder.Services.AddSingleton` or similar). Add next to them: ```csharp builder.Services.AddSingleton(); ``` Match the lifetime of sibling services (most are `AddSingleton`). If the sibling services use a different lifetime, match it. - [ ] **Step 2: Inject it into `WorkerHub`** Modify `src/ClaudeDo.Worker/Hub/WorkerHub.cs`: In the field block (near `_wtMaintenance`): ```csharp private readonly TaskResetService _resetService; ``` In the constructor signature, append `TaskResetService resetService` and assign it. The full updated constructor: ```csharp public WorkerHub( QueueService queue, AgentFileService agentService, HubBroadcaster broadcaster, IDbContextFactory dbFactory, WorktreeMaintenanceService wtMaintenance, TaskResetService resetService) { _queue = queue; _agentService = agentService; _broadcaster = broadcaster; _dbFactory = dbFactory; _wtMaintenance = wtMaintenance; _resetService = resetService; } ``` - [ ] **Step 3: Add the `ResetTask` hub method** Add inside `WorkerHub` (place it near `ContinueTask` for symmetry): ```csharp public async Task ResetTask(string taskId) { try { await _resetService.ResetAsync(taskId, CancellationToken.None); } catch (InvalidOperationException ex) { throw new HubException(ex.Message); } catch (KeyNotFoundException) { throw new HubException("task not found"); } } ``` - [ ] **Step 4: Build the worker to verify wiring** Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj` Expected: SUCCESS (no compile errors). Note: `dotnet build ClaudeDo.slnx` requires .NET 9 — build individual csproj files instead. - [ ] **Step 5: Run the full worker test suite** Run: `dotnet test tests/ClaudeDo.Worker.Tests -v minimal` Expected: PASS (all existing tests plus the new ones from Tasks 1-3). - [ ] **Step 6: Commit** ```bash git add src/ClaudeDo.Worker/Program.cs src/ClaudeDo.Worker/Hub/WorkerHub.cs git commit -m "feat(worker): expose ResetTask hub method" ``` --- ## Task 5: Add `ContinueTaskAsync` and `ResetTaskAsync` to `WorkerClient` **Files:** - Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs` - [ ] **Step 1: Add both methods** Open `src/ClaudeDo.Ui/Services/WorkerClient.cs`. Next to `RunNowAsync` (around line 166): ```csharp public async Task ContinueTaskAsync(string taskId, string followUpPrompt) { await _hub.InvokeAsync("ContinueTask", taskId, followUpPrompt); } public async Task ResetTaskAsync(string taskId) { await _hub.InvokeAsync("ResetTask", taskId); } ``` If the existing `RunNowAsync` fires a local event first (e.g. `RunNowRequestedEvent?.Invoke(taskId)`), do **not** mirror that — Continue/Reset don't need UI-local optimistic state; we rely on `TaskUpdated` broadcasts. - [ ] **Step 2: Build the UI project** Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` Expected: SUCCESS. - [ ] **Step 3: Commit** ```bash git add src/ClaudeDo.Ui/Services/WorkerClient.cs git commit -m "feat(ui): add ContinueTaskAsync and ResetTaskAsync to WorkerClient" ``` --- ## Task 6: Add commands and state to `DetailsIslandViewModel` **Files:** - Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` Background you need before editing: - The VM already has a `Task` property (`TaskRowViewModel?`) that represents the selected task. - Status is tracked via `AgentStatusLabel` and exposed as `IsRunning`/`IsDone`/`IsFailed`. - `TaskRowViewModel` may not currently hold the latest `SessionId`. You need a way to read the latest run's `SessionId` for the selected task — query `TaskRunRepository` during the existing task-load flow. If the VM already loads task runs (search for `TaskRunRepository` usage in `DetailsIslandViewModel`), piggyback on that; otherwise add a DB query inside the task-load method. - [ ] **Step 1: Add observable properties for button visibility/enablement** In the observable-property block (after `_promptInput`, around line 29), add: ```csharp [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ContinueCommand))] [NotifyCanExecuteChangedFor(nameof(ResetCommand))] private bool _showFailedActions; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ContinueCommand))] private string? _latestRunSessionId; ``` Also hook `AgentStatusLabel` changes to refresh `ShowFailedActions`. Update the existing `OnAgentStatusLabelChanged` partial method: ```csharp partial void OnAgentStatusLabelChanged(string value) { OnPropertyChanged(nameof(IsRunning)); OnPropertyChanged(nameof(IsDone)); OnPropertyChanged(nameof(IsFailed)); ShowFailedActions = value == "Failed"; } ``` - [ ] **Step 2: Populate `LatestRunSessionId` during task load** Find the method in `DetailsIslandViewModel` that loads details for the selected task (likely named `LoadAsync`, `OnTaskChanged`, or similar — search for where `Turns`, `Tokens`, or `AgentStatusLabel` are assigned). Inside that method, after loading the task entity: ```csharp using var runCtx = _dbFactory.CreateDbContext(); var runRepo = new TaskRunRepository(runCtx); var latestRun = await runRepo.GetLatestByTaskIdAsync(Task.Id); LatestRunSessionId = latestRun?.SessionId; ``` Verify the method name `GetLatestByTaskIdAsync` exists on `TaskRunRepository` (it is used in `TaskRunner.ContinueAsync`). If the name differs, use whatever is exposed. Make sure this runs inside the same cancellation-safe block as the other loads — copy the existing pattern verbatim. Also ensure `LatestRunSessionId` is reset to `null` when the selected task clears. If the VM has an `OnTaskChanged` partial method that clears other fields, add `LatestRunSessionId = null;` there too. - [ ] **Step 3: Add the two commands** Add at the end of the class (next to `RunNowAsync` / `CanRunNow`): ```csharp [RelayCommand(CanExecute = nameof(CanContinue))] private async System.Threading.Tasks.Task ContinueAsync() { if (Task == null) return; await _worker.ContinueTaskAsync(Task.Id, "Continue working on this task."); } private bool CanContinue() => Task != null && _worker.IsConnected && ShowFailedActions && !string.IsNullOrEmpty(LatestRunSessionId); [RelayCommand(CanExecute = nameof(CanReset))] private async System.Threading.Tasks.Task ResetAsync() { if (Task == null) return; if (ConfirmAsync == null) return; var confirmed = await ConfirmAsync( $"Discard worktree and reset task?\nThis deletes branch claudedo/{Task.Id.Replace("-", "")} and all uncommitted changes."); if (!confirmed) return; await _worker.ResetTaskAsync(Task.Id); } private bool CanReset() => Task != null && _worker.IsConnected && ShowFailedActions; ``` Also update the worker-connection PropertyChanged handler (around line 112) to notify the new commands: ```csharp _worker.PropertyChanged += (_, e) => { if (e.PropertyName == nameof(WorkerClient.IsConnected)) { RunNowCommand.NotifyCanExecuteChanged(); ContinueCommand.NotifyCanExecuteChanged(); ResetCommand.NotifyCanExecuteChanged(); } }; ``` - [ ] **Step 4: Build the UI project** Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` Expected: SUCCESS. - [ ] **Step 5: Commit** ```bash git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs git commit -m "feat(ui): add Continue and Reset commands to DetailsIslandViewModel" ``` --- ## Task 7: Add the button row to `DetailsIslandView.axaml` **Files:** - Modify: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml` - [ ] **Step 1: Inspect the existing layout** Read `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml` end-to-end so you understand the grid layout. The `AgentStripView` sits at `Grid.Row="0"`. Decide whether to add a new grid row below it or to extend the agent strip itself. Simplest: add the button row to `AgentStripView.axaml`, since that control already contains `RunNowCommand` / `StopCommand` buttons and is bound to the same VM. - [ ] **Step 2: Add the buttons to `AgentStripView.axaml`** Open `src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml`. Locate the existing `RunNowCommand` button (around line 49). After it, add: ```xml