diff --git a/docs-ai/019-worktree-creation-and-lifecycle/000-plan.md b/docs-ai/019-worktree-creation-and-lifecycle/000-plan.md index a6adb2e1..c69593b3 100644 --- a/docs-ai/019-worktree-creation-and-lifecycle/000-plan.md +++ b/docs-ai/019-worktree-creation-and-lifecycle/000-plan.md @@ -5,7 +5,7 @@ | **Status** | Implemented (retrospective) | | **Anchor date** | 2026-04-12 | | **Documented** | 2026-07-12 (backfilled) | -| **Primary PRs** | #167, #189, #190, #192 (initial wave); later waves #260/#419, #375/#383, #424/#427, #520 | +| **Primary PRs** | #167, #189, #190, #192 (initial wave); later waves #260/#419, #375/#383, #424/#427, #520, #592 | | **Sources** | PR descriptions #167/#189/#190/#192/#260/#375/#383/#419/#424/#427/#520; fork issues #166/#175/#176/#178; `docs-ai/017-upstream-sync-process/upstream-ledger.md` (2026-04-08 and 2026-05-08 review batches) | | **Related** | [018-archived-worktrees](../018-archived-worktrees/000-plan.md), [028-pr-status-tracking](../028-pr-status-tracking/000-plan.md), [034-worktree-watcher-correctness](../034-worktree-watcher-correctness/000-plan.md), [044-foundation-model-branch-names](../044-foundation-model-branch-names/000-plan.md), `docs/components/repositories-and-worktrees.md` | @@ -89,3 +89,7 @@ deletion-safety rework, clone-from-URL intake — all later waves (see Amendment - Updated 2026-07-12: worktree deletion now verifies Git registration removal and propagates cleanup failures (fork issue #454) — see [006-verified-worktree-deletion.md](006-verified-worktree-deletion.md) +- Updated 2026-07-16: manual delete dialog remembers the last confirmed branch choice; + automatic-cleanup branch deletion split into `deleteBranchOnAutomaticCleanup` (#592) — + see the follow-up section in + [003-safe-branch-deletion-and-cleanup.md](003-safe-branch-deletion-and-cleanup.md) diff --git a/docs-ai/019-worktree-creation-and-lifecycle/001-action.md b/docs-ai/019-worktree-creation-and-lifecycle/001-action.md index 12b58673..5b4308d4 100644 --- a/docs-ai/019-worktree-creation-and-lifecycle/001-action.md +++ b/docs-ai/019-worktree-creation-and-lifecycle/001-action.md @@ -16,8 +16,9 @@ | 2026-06-09 | Visible labels + caption for the Advanced fields (TextField labels are not rendered under `.roundedBorder`) | PR #427 | | 2026-06-27 | On-device Foundation Model branch-name suggestion added to the same dialog | PR #518 (owned by [044](../044-foundation-model-branch-names/000-plan.md)) | | 2026-06-28 | Add to Prowl popover redesign: drop zone, Browse, Clone-from-URL form with clipboard prefill, Add Workspace; auto-select after add — see [005-add-to-prowl-clone.md](005-add-to-prowl-clone.md) | PR #520 | +| 2026-07-16 | Manual delete dialog remembers the last confirmed branch choice; automatic-cleanup branch deletion split into `deleteBranchOnAutomaticCleanup` (default off) with legacy-key migration — see the follow-up section in [003-safe-branch-deletion-and-cleanup.md](003-safe-branch-deletion-and-cleanup.md) | PR #592 | -## Outcome & current state (as of 2026-07-12) +## Outcome & current state (as of 2026-07-16) - **Git plumbing** — `supacode/Clients/Git/GitClient.swift`: `branchRefs(for:)` returns local + upstream refs; `deleteLocalBranch(_:_:force:)` backs both `-d` and confirmed @@ -48,17 +49,20 @@ (default `.merge`); `supacode/Features/Settings/Models/MergedWorktreeAction.swift`; per-repo optionals in `supacode/Features/Settings/Models/RepositorySettings.swift`. UI: `supacode/Features/Settings/Views/WorktreeSettingsView.swift` (merged-action - picker, copy-flag toggles, branch-delete toggle), + picker, copy-flag toggles, automatic-cleanup branch toggle), `GithubSettingsView.swift` (merge strategy), `RepositorySettingsView.swift` ("Global (…)" override pickers). - **Merged-PR automation** — `supacode/Features/Repositories/Reducer/RepositoriesFeature+GithubIntegration.swift` switches on `state.mergedWorktreeAction`; `.delete` passes - `deleteBranch: deleteBranchOnDeleteWorktree && prowlCreatedWorktreeIDs.contains(id)`. + `deleteBranch: deleteBranchOnAutomaticCleanup && prowlCreatedWorktreeIDs.contains(id)`. - **Deletion** — `supacode/Features/Repositories/Reducer/RepositoriesFeature+WorktreeLifecycle.swift` - (delete + `ForceDeleteBranchRequest` flow) and + (manual last-choice persistence via `deleteBranchOnManualWorktreeDelete`, delete + + `ForceDeleteBranchRequest` flow) and `supacode/Features/Repositories/Views/DeleteWorktreeConfirmationView.swift`. + Manual confirmation remembers only submitted choices; automatic branch cleanup is + separately controlled and restricted to Prowl-created worktrees. - **History navigation** — stacks and `navigateWorktreeHistory` in `supacode/Features/Repositories/Reducer/RepositoriesFeature.swift` / `RepositoriesFeature+Selection.swift` (50-entry cap); menu commands in diff --git a/docs-ai/019-worktree-creation-and-lifecycle/003-safe-branch-deletion-and-cleanup.md b/docs-ai/019-worktree-creation-and-lifecycle/003-safe-branch-deletion-and-cleanup.md index 317423cb..f8fef7c4 100644 --- a/docs-ai/019-worktree-creation-and-lifecycle/003-safe-branch-deletion-and-cleanup.md +++ b/docs-ai/019-worktree-creation-and-lifecycle/003-safe-branch-deletion-and-cleanup.md @@ -1,4 +1,4 @@ -# 019 — Amendment: Safe Branch Deletion & Cleanup Hardening (#375, #383; follow-up 2026-07) +# 019 — Amendment: Safe Branch Deletion & Cleanup Hardening (#375, #383; follow-up #592) ## Context @@ -32,7 +32,7 @@ PR #383 (merged 2026-06-03, "Harden failed worktree cleanup"): - Only relocate existing worktree directories that actually contain `.git` metadata. - Failed-creation cleanup no longer requests branch deletion at all. -## Follow-up (2026-07): split manual preselect from automatic cleanup +## Follow-up (2026-07-16, #592): split manual preselect from automatic cleanup The single `deleteBranchOnDeleteWorktree` setting from #375 drove two unrelated behaviors: preselecting the manual dialog toggle (gated on Prowl-created worktrees) @@ -62,7 +62,7 @@ Decisions: ## Refs -- PRs #375, #383; follow-up PR splitting the settings (2026-07); tests in +- PRs #375, #383; follow-up PR #592 splitting the settings (2026-07-16); tests in `supacodeTests/GitClientRemoveWorktreeTests`, `RepositoriesFeatureTests`, and `SettingsFilePersistenceTests` (legacy-key migration). diff --git a/supacode/Features/Settings/Views/WorktreeSettingsView.swift b/supacode/Features/Settings/Views/WorktreeSettingsView.swift index 7d4b8966..9be36815 100644 --- a/supacode/Features/Settings/Views/WorktreeSettingsView.swift +++ b/supacode/Features/Settings/Views/WorktreeSettingsView.swift @@ -72,7 +72,8 @@ struct WorktreeSettingsView: View { case .archive: Text("Archives worktrees when their pull requests are merged.") case .delete: - Text("Deletes worktrees when their pull requests are merged. Uncommitted changes will be lost.") + Text("Deletes worktrees when their pull requests are merged. ") + + Text("Uncommitted changes will be lost.").foregroundStyle(.red) case nil: EmptyView() } diff --git a/supacodeTests/RepositoriesFeatureTests.swift b/supacodeTests/RepositoriesFeatureTests.swift index 225dc7ed..e7b5fd52 100644 --- a/supacodeTests/RepositoriesFeatureTests.swift +++ b/supacodeTests/RepositoriesFeatureTests.swift @@ -4202,6 +4202,47 @@ struct RepositoriesFeatureTests { await store.receive(\.worktreeLifecycle.worktreeDeleted) } + @Test(.dependencies) func dismissingDeletePromptDoesNotRememberChangedChoice() async { + let worktree = makeWorktree(id: "/tmp/wt", name: "owl") + let repository = makeRepository(id: "/tmp/repo", worktrees: [worktree]) + let store = TestStore(initialState: makeState(repositories: [repository])) { + RepositoriesFeature() + } + + await store.send(.worktreeLifecycle(.requestDeleteWorktree(worktree.id, repository.id))) { + $0.deleteWorktreeConfirmation = DeleteWorktreeConfirmation( + id: 0, + title: "Delete worktree?", + message: "Delete \(worktree.name)? The worktree directory will be removed.", + targets: [ + RepositoriesFeature.DeleteWorktreeTarget( + worktreeID: worktree.id, repositoryID: repository.id) + ], + deleteBranch: false + ) + $0.nextDeleteWorktreeConfirmationID = 1 + } + await store.send(.worktreeLifecycle(.deleteWorktreePromptDeleteBranchChanged(true))) { + $0.deleteWorktreeConfirmation?.deleteBranch = true + } + await store.send(.worktreeLifecycle(.deleteWorktreePromptDismissed)) { + $0.deleteWorktreeConfirmation = nil + } + await store.send(.worktreeLifecycle(.requestDeleteWorktree(worktree.id, repository.id))) { + $0.deleteWorktreeConfirmation = DeleteWorktreeConfirmation( + id: 1, + title: "Delete worktree?", + message: "Delete \(worktree.name)? The worktree directory will be removed.", + targets: [ + RepositoriesFeature.DeleteWorktreeTarget( + worktreeID: worktree.id, repositoryID: repository.id) + ], + deleteBranch: false + ) + $0.nextDeleteWorktreeConfirmationID = 2 + } + } + @Test(.dependencies) func deletePromptConfirmedAsksBeforeForceDeletingBranch() async { let repoRoot = "/tmp/repo" let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot) diff --git a/supacodeTests/SettingsFilePersistenceTests.swift b/supacodeTests/SettingsFilePersistenceTests.swift index d7581408..9a1bc845 100644 --- a/supacodeTests/SettingsFilePersistenceTests.swift +++ b/supacodeTests/SettingsFilePersistenceTests.swift @@ -265,6 +265,17 @@ struct SettingsFilePersistenceTests { #expect(settings.global.deleteBranchOnAutomaticCleanup == false) } + @Test func encodesAutomaticCleanupSettingWithoutLegacyKey() throws { + var settings = GlobalSettings.default + settings.deleteBranchOnAutomaticCleanup = true + + let encoded = try JSONEncoder().encode(settings) + let globalDict = try #require(try JSONSerialization.jsonObject(with: encoded) as? [String: Any]) + + #expect(globalDict["deleteBranchOnAutomaticCleanup"] as? Bool == true) + #expect(globalDict["deleteBranchOnDeleteWorktree"] == nil) + } + @Test(.dependencies) func roundTripsExplicitNotificationSound() throws { let storage = SettingsTestStorage()