Skip to content

Commit a5081fa

Browse files
tim-smartgithub-actions[bot]
authored andcommitted
Apply removal confirmation to archive cleanup
1 parent e2981ea commit a5081fa

5 files changed

Lines changed: 36 additions & 12 deletions

File tree

apps/mobile/src/features/home/useThreadListActions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ export function useThreadListActions(): {
201201
});
202202
return preview._tag === "Success" ? preview.value.candidate : null;
203203
},
204+
removalPolicy: "confirm",
204205
confirmRemoval: ({ displayWorktreePath }) =>
205206
presentWorktreeCleanupConfirmation({
206207
isIos: process.env.EXPO_OS === "ios",

apps/web/src/components/settings/SettingsPanels.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1079,7 +1079,7 @@ export function GeneralSettingsPanel() {
10791079

10801080
<SettingsRow
10811081
title="Worktree remove confirmation"
1082-
description="Ask before removing a worktree when its last linked thread is deleted."
1082+
description="Ask before removing a worktree when its last linked thread is archived or deleted."
10831083
resetAction={
10841084
settings.confirmWorktreeRemoval !== DEFAULT_UNIFIED_SETTINGS.confirmWorktreeRemoval ? (
10851085
<SettingResetButton

apps/web/src/hooks/useThreadActions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export function useThreadActions() {
241241
});
242242
return previewResult._tag === "Success" ? previewResult.value.candidate : null;
243243
},
244+
removalPolicy: confirmWorktreeRemoval ? "confirm" : "remove",
244245
confirmRemoval: localApi
245246
? async ({ displayWorktreePath }) => {
246247
const confirmationResult = await settlePromise(() =>
@@ -326,6 +327,7 @@ export function useThreadActions() {
326327
[
327328
archiveThreadMutation,
328329
cleanupThreadWorktree,
330+
confirmWorktreeRemoval,
329331
getCurrentRouteThreadRef,
330332
previewWorktreeCleanup,
331333
resolveThreadTarget,

packages/client-runtime/src/state/worktreeCleanup.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const candidate: ArchiveWorktreeCleanupCandidate = {
1515

1616
function makeFlow(overrides?: {
1717
readonly previewCandidate?: () => Promise<ArchiveWorktreeCleanupCandidate | null>;
18+
readonly removalPolicy?: "confirm" | "remove";
1819
readonly confirmation?: WorktreeCleanupConfirmation<{ readonly _tag: "Failure" }> | null;
1920
readonly archiveSucceeds?: boolean;
2021
readonly cleanupOutcome?: WorktreeCleanupOutcome;
@@ -35,6 +36,7 @@ function makeFlow(overrides?: {
3536
const run = () =>
3637
runArchiveWithWorktreeCleanup({
3738
previewCandidate: overrides?.previewCandidate ?? (async () => candidate),
39+
removalPolicy: overrides?.removalPolicy ?? "confirm",
3840
confirmRemoval,
3941
archive,
4042
isArchiveSuccess: (result) => result._tag === "Success",
@@ -70,6 +72,17 @@ describe("runArchiveWithWorktreeCleanup", () => {
7072
expect(flow.cleanup).not.toHaveBeenCalled();
7173
});
7274

75+
it("removes the final active worktree without prompting when confirmation is disabled", async () => {
76+
const flow = makeFlow({ removalPolicy: "remove", confirmation: null });
77+
const outcome = await flow.run();
78+
expect(outcome).toEqual({ kind: "archived", result: { _tag: "Success" } });
79+
expect(flow.archive).toHaveBeenCalledTimes(1);
80+
expect(flow.cleanup).toHaveBeenCalledTimes(1);
81+
expect(flow.archive.mock.invocationCallOrder[0]).toBeLessThan(
82+
flow.cleanup.mock.invocationCallOrder[0] ?? 0,
83+
);
84+
});
85+
7386
it("archives without cleanup when the user declines", async () => {
7487
const flow = makeFlow({ confirmation: { kind: "declined" } });
7588
const outcome = await flow.run();
@@ -154,6 +167,7 @@ describe("runArchiveWithWorktreeCleanup", () => {
154167
);
155168
return shared ? null : target;
156169
},
170+
removalPolicy: "confirm",
157171
confirmRemoval: async ({ displayWorktreePath }) => {
158172
prompts.push(displayWorktreePath);
159173
return { kind: "declined" };

packages/client-runtime/src/state/worktreeCleanup.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
* Shared archive-time worktree cleanup flow.
33
*
44
* The server owns the safety decision (preview + conditional cleanup RPCs);
5-
* this module owns the client sequencing shared by web and mobile: prompt
6-
* only when the server reports a candidate and a confirmation surface
7-
* exists, archive regardless of the answer, run cleanup only after a
8-
* confirmed archive, and report cleanup problems without failing the
9-
* archive itself.
5+
* this module owns the client sequencing shared by web and mobile: act only
6+
* when the server reports a candidate, optionally confirm based on client
7+
* policy, archive before cleanup, and report cleanup problems without
8+
* failing the archive itself.
109
*/
1110
import type { WorktreeCleanupStatus } from "@t3tools/contracts";
1211

@@ -41,10 +40,14 @@ export type ArchiveWithWorktreeCleanupResult<TArchive, TAbort> =
4140
| { readonly kind: "archived"; readonly result: TArchive }
4241
| { readonly kind: "aborted"; readonly result: TAbort };
4342

43+
export type WorktreeRemovalPolicy = "confirm" | "remove";
44+
4445
export async function runArchiveWithWorktreeCleanup<TArchive, TAbort = never>(input: {
4546
/** Server-authoritative preview; null when ineligible or when the preview failed. */
4647
readonly previewCandidate: () => Promise<ArchiveWorktreeCleanupCandidate | null>;
47-
/** Confirmation surface, or null when none is available (no prompt, no cleanup). */
48+
/** Whether an eligible worktree is confirmed first or removed automatically. */
49+
readonly removalPolicy: WorktreeRemovalPolicy;
50+
/** Confirmation surface, or null when none is available. */
4851
readonly confirmRemoval:
4952
| ((prompt: {
5053
readonly candidate: ArchiveWorktreeCleanupCandidate;
@@ -60,13 +63,17 @@ export async function runArchiveWithWorktreeCleanup<TArchive, TAbort = never>(in
6063
const candidate = await input.previewCandidate();
6164
let shouldCleanup = false;
6265
let displayWorktreePath: string | null = null;
63-
if (candidate && input.confirmRemoval) {
66+
if (candidate) {
6467
displayWorktreePath = formatWorktreePathForDisplay(candidate.worktreePath);
65-
const confirmation = await input.confirmRemoval({ candidate, displayWorktreePath });
66-
if (confirmation.kind === "aborted") {
67-
return { kind: "aborted", result: confirmation.result };
68+
if (input.removalPolicy === "remove") {
69+
shouldCleanup = true;
70+
} else if (input.confirmRemoval) {
71+
const confirmation = await input.confirmRemoval({ candidate, displayWorktreePath });
72+
if (confirmation.kind === "aborted") {
73+
return { kind: "aborted", result: confirmation.result };
74+
}
75+
shouldCleanup = confirmation.kind === "confirmed";
6876
}
69-
shouldCleanup = confirmation.kind === "confirmed";
7077
}
7178

7279
const archiveResult = await input.archive();

0 commit comments

Comments
 (0)