Remove a worktree when its last workspace is archived (#1562)

* Remove a worktree when its last workspace is archived

Archiving is now workspace-centric: the UI always archives a workspace,
and a worktree's directory is removed only when its last referencing
workspace is archived (derived from ownership + a single reference
count). The "remove the worktree from disk?" prompt is gone; archiving
warns only when the workspace is dirty or has unpushed commits.

MCP archive_worktree and the CLI keep the explicit lower-level behavior:
archive every workspace backing the directory, then remove it.
Auto-archive-after-merge and create-agent auto-archive respect the
reference count like any other workspace archival.

The policy now lives in two modules - a server archive service and a
client archive hook - with the reference-count check in exactly one
place. The deleteWorktreeFromDisk request field is retained but ignored
for wire back-compat.

* Tidy archive-policy internals and close test gaps

Follow-up polish on the archive-policy consolidation; no behavior change.

Structure: the server archive service is renamed to reflect that it now
owns all archive policy (the file and its types no longer say "worktree"
where they mean "archive"); the client archive input derives the
workspace kind from the canonical type instead of re-listing it; the
not-found target is typed string|null instead of an empty-string
sentinel; a redundant workspace-update emit and the ambiguous
worktrees-root naming are cleaned up. Wire, SDK, and keybinding names are
deliberately left unchanged for back-compat.

Tests: re-home the archiving-state lifecycle and per-workspace snapshot
assertions; pin worktree-kind acceptance through archive_workspace, the
ignored deleteWorktreeFromDisk field, and three-workspace archive-all;
add a Playwright spec for the dirty/unpushed worktree archive flow (the
warning gates it, confirming removes the directory) and the bulk
remove-project confirm branch.

* Fix the hanging worktree-archive risk-warning e2e spec

The spec added in the previous commit hung deterministically, locally and
in CI. Two test-only bugs, no product change.

The archive click opens a synchronous window.confirm(); the spec
registered a passive dialog waiter but only dismissed the dialog after
awaiting the click, so the click never resolved (Playwright won't settle
a click while a dialog is open) - a circular wait. Answer the dialog
inline via page.once("dialog", ...) before the click resolves, matching
the proven worktree-archive spec.

The seed also committed on a never-pushed branch, so aheadOfOrigin was
null and the "1 unpushed commit" warning never appeared. Push the branch
to set its upstream before the local commit, so it reports exactly one
unpushed commit.

Verified: the spec now passes in ~6s (was a 45s timeout); both the
dismiss-gates and accept-removes paths run.
This commit is contained in:
Mohamed Boudra
2026-06-17 00:38:56 +08:00
committed by GitHub
parent 0fef66283a
commit e202ca5036
45 changed files with 2627 additions and 1610 deletions
-15
View File
@@ -254,21 +254,6 @@ describe("translation resources", () => {
expect(en.openProject.tiles.addProject.title).toBe("Add a project");
});
it("resolves the worktree delete prompt keys used by the sidebar prompt", () => {
const strings = flattenStrings(en);
expect(strings["sidebar.workspace.confirmations.deleteWorktreePrompt.title"]).toBe(
"Archive workspace",
);
expect(strings["sidebar.workspace.confirmations.deleteWorktreePrompt.message"]).toBe(
"Also remove the worktree from disk?",
);
expect(strings["sidebar.workspace.confirmations.deleteWorktreePrompt.keep"]).toBe(
"Keep on disk",
);
expect(strings["sidebar.workspace.confirmations.deleteWorktreePrompt.delete"]).toBe("Delete");
});
it("includes provider selector and pairing keys for the Batch 4D migration", () => {
expect(en.modelSelector.title).toBe("Select provider");
expect(en.modelSelector.favorites).toBe("Favorites");
-6
View File
@@ -814,12 +814,6 @@ export const ar: TranslationResources = {
'إخفاء "{{workspaceName}}" من الشريط الجانبي؟\n\n لن يتم تغيير الملفات الموجودة على القرص.',
hideConfirm: "يخفي",
cancel: "يلغي",
deleteWorktreePrompt: {
title: "أرشفة مساحة العمل",
message: "هل تريد أيضًا إزالة شجرة العمل من القرص؟",
keep: "الاحتفاظ على القرص",
delete: "حذف",
},
},
rename: {
title: "إعادة تسمية مساحة العمل",
-6
View File
@@ -820,12 +820,6 @@ export const en = {
'Hide "{{workspaceName}}" from the sidebar?\n\nFiles on disk will not be changed.',
hideConfirm: "Hide",
cancel: "Cancel",
deleteWorktreePrompt: {
title: "Archive workspace",
message: "Also remove the worktree from disk?",
keep: "Keep on disk",
delete: "Delete",
},
},
rename: {
title: "Rename workspace",
-6
View File
@@ -840,12 +840,6 @@ export const es: TranslationResources = {
'¿Ocultar "{{workspaceName}}" de la barra lateral?\n\nLos archivos en el disco no se cambiarán.',
hideConfirm: "Esconder",
cancel: "Cancelar",
deleteWorktreePrompt: {
title: "Archivar espacio de trabajo",
message: "¿También eliminar el worktree del disco?",
keep: "Conservar en disco",
delete: "Eliminar",
},
},
rename: {
title: "Cambiar nombre del espacio de trabajo",
-6
View File
@@ -839,12 +839,6 @@ export const fr: TranslationResources = {
"Masquer «{{workspaceName}}» dans la barre latérale?\n\nLes fichiers sur le disque ne seront pas modifiés.",
hideConfirm: "Cacher",
cancel: "Annuler",
deleteWorktreePrompt: {
title: "Archiver l'espace de travail",
message: "Supprimer aussi le worktree du disque?",
keep: "Conserver sur le disque",
delete: "Supprimer",
},
},
rename: {
title: "Renommer l'espace de travail",
-6
View File
@@ -832,12 +832,6 @@ export const ru: TranslationResources = {
"Скрыть «{{workspaceName}}» на боковой панели?\n\n Файлы на диске не будут изменены.",
hideConfirm: "Скрывать",
cancel: "Отмена",
deleteWorktreePrompt: {
title: "Архивировать рабочее пространство",
message: "Также удалить рабочее дерево с диска?",
keep: "Оставить на диске",
delete: "Удалить",
},
},
rename: {
title: "Переименовать рабочую область",
-6
View File
@@ -805,12 +805,6 @@ export const zhCN: TranslationResources = {
hideMessage: "从侧边栏隐藏「{{workspaceName}}」?\n\n磁盘上的文件不会被更改。",
hideConfirm: "隐藏",
cancel: "取消",
deleteWorktreePrompt: {
title: "归档 workspace",
message: "同时从磁盘删除 worktree?",
keep: "保留在磁盘上",
delete: "删除",
},
},
rename: {
title: "重命名 workspace",