authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-18 15:02:55-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-18 22:09:13-08:00
log7de054fa4964e5cc6c406f39551dc62a2c214b38
tree34992cfb350c573dc4b7fcdfbc4ecb0a3664870a
parentbb9371d3c9500bf035b7df5da0969b82aac5a1fc

Surface worktree removal failures after thread deletion

- wrap `git worktree remove` errors with command and cwd context - add server test covering failed worktree removal messaging - show an error toast and log details when orphaned worktree cleanup fails

3 files changed, 43 insertions(+), 6 deletions(-)

apps/server/src/git.test.ts+18
......@@ -760,6 +760,24 @@ describe("git integration", () => {
760760 expect(dirty.hasWorkingTreeChanges).toBe(true);
761761 });
762762
763 it("includes command context when worktree removal fails", async () => {
764 await using tmp = await makeTmpDir();
765 await initRepoWithCommit(tmp.path);
766 const core = new GitCoreService();
767 const missingWorktreePath = path.join(tmp.path, "missing-worktree");
768
769 const error = await core
770 .removeWorktree({ cwd: tmp.path, path: missingWorktreePath })
771 .then(() => null)
772 .catch((err: unknown) => err);
773
774 expect(error).toBeInstanceOf(Error);
775 const message = (error as Error).message;
776 expect(message).toContain("git worktree remove");
777 expect(message).toContain(`cwd: ${tmp.path}`);
778 expect(message).toContain(missingWorktreePath);
779 });
780
763781 it("refreshes upstream before statusDetails so behind count reflects remote updates", async () => {
764782 await using remote = await makeTmpDir();
765783 await using source = await makeTmpDir();
apps/server/src/git.ts+12-4
......@@ -546,10 +546,18 @@ export class GitCoreService {
546546 }
547547
548548 async removeWorktree(input: GitRemoveWorktreeInput): Promise<void> {
549 await executeGit(input.cwd, ["worktree", "remove", input.path], {
550 timeoutMs: 15_000,
551 fallbackErrorMessage: "git worktree remove failed",
552 });
549 const args = ["worktree", "remove", input.path] as const;
550 try {
551 await executeGit(input.cwd, args, {
552 timeoutMs: 15_000,
553 fallbackErrorMessage: "git worktree remove failed",
554 });
555 } catch (error) {
556 const detail = error instanceof Error ? error.message : String(error);
557 throw new Error(`${commandLabel(args)} failed (cwd: ${input.cwd}): ${detail}`, {
558 cause: error,
559 });
560 }
553561 }
554562
555563 async createBranch(input: GitCreateBranchInput): Promise<void> {
apps/web/src/components/Sidebar.tsx+13-2
......@@ -336,8 +336,19 @@ export default function Sidebar() {
336336 cwd: threadProject.cwd,
337337 path: orphanedWorktreePath,
338338 });
339 } catch {
340 // Worktree deletion is best-effort and should not block thread deletion.
339 } catch (error) {
340 const message = error instanceof Error ? error.message : "Unknown error removing worktree.";
341 console.error("Failed to remove orphaned worktree after thread deletion", {
342 threadId,
343 projectCwd: threadProject.cwd,
344 worktreePath: orphanedWorktreePath,
345 error,
346 });
347 toastManager.add({
348 type: "error",
349 title: "Thread deleted, but worktree removal failed",
350 description: `Could not remove ${displayWorktreePath ?? orphanedWorktreePath}. ${message}`,
351 });
341352 }
342353 },
343354 [api, dispatch, removeWorktreeMutation, state.projects, state.threads],