From 8b8ba0d2070945456fb09adf0e296d1dfb0ea6e1 Mon Sep 17 00:00:00 2001 From: Ryan Harrington Date: Mon, 16 Mar 2026 16:45:21 -0400 Subject: [PATCH] fix/gsd-bg-shell-stale-cwd: resync bg-shell cwd after auto-worktree exit --- src/resources/extensions/bg-shell/index.ts | 16 +++++++-- .../extensions/bg-shell/utilities.ts | 16 +++++++++ src/tests/bg-shell-persistence-cwd.test.ts | 36 +++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 src/tests/bg-shell-persistence-cwd.test.ts diff --git a/src/resources/extensions/bg-shell/index.ts b/src/resources/extensions/bg-shell/index.ts index e126c12f1..8f950d28d 100644 --- a/src/resources/extensions/bg-shell/index.ts +++ b/src/resources/extensions/bg-shell/index.ts @@ -64,7 +64,7 @@ import { } from "./output-formatter.js"; import { waitForReady } from "./readiness-detector.js"; import { queryShellEnv, sendAndWait, runOnSession } from "./interaction.js"; -import { formatUptime, formatTokenCount } from "./utilities.js"; +import { formatUptime, formatTokenCount, resolveBgShellPersistenceCwd } from "./utilities.js"; import { BgManagerOverlay } from "./overlay.js"; // ── Re-exports for consumers ─────────────────────────────────────────────── @@ -81,6 +81,14 @@ export { BgManagerOverlay } from "./overlay.js"; export default function (pi: ExtensionAPI) { let latestCtx: ExtensionContext | null = null; + function syncLatestCtxCwd(): void { + if (!latestCtx) return; + const syncedCwd = resolveBgShellPersistenceCwd(latestCtx.cwd); + if (syncedCwd !== latestCtx.cwd) { + latestCtx = { ...latestCtx, cwd: syncedCwd }; + } + } + // Clean up on session shutdown pi.on("session_shutdown", async () => { cleanupAll(); @@ -1519,6 +1527,7 @@ export default function (pi: ExtensionAPI) { refreshWidget(); // Persist manifest periodically if (latestCtx) { + syncLatestCtxCwd(); persistManifest(latestCtx.cwd); } }, 2000); @@ -1567,7 +1576,10 @@ export default function (pi: ExtensionAPI) { // Clean up on shutdown pi.on("session_shutdown", async () => { clearInterval(maintenanceInterval); - if (latestCtx) persistManifest(latestCtx.cwd); + if (latestCtx) { + syncLatestCtxCwd(); + persistManifest(latestCtx.cwd); + } cleanupAll(); }); } diff --git a/src/resources/extensions/bg-shell/utilities.ts b/src/resources/extensions/bg-shell/utilities.ts index b33c68b50..9d534a2ee 100644 --- a/src/resources/extensions/bg-shell/utilities.ts +++ b/src/resources/extensions/bg-shell/utilities.ts @@ -3,6 +3,8 @@ */ import { createRequire } from "node:module"; +import { existsSync } from "node:fs"; +import { sep } from "node:path"; // ── Windows VT Input Restoration ──────────────────────────────────────────── // Child processes (esp. Git Bash / MSYS2) can strip the ENABLE_VIRTUAL_TERMINAL_INPUT @@ -53,3 +55,17 @@ export function formatTokenCount(count: number): string { if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`; return `${Math.round(count / 1000000)}M`; } + +export function resolveBgShellPersistenceCwd( + cachedCwd: string, + liveCwd = process.cwd(), + pathExists: (path: string) => boolean = existsSync, +): string { + const worktreeMarker = `${sep}.gsd${sep}worktrees${sep}`; + const cachedIsAutoWorktree = cachedCwd.includes(worktreeMarker); + if (!cachedIsAutoWorktree) return cachedCwd; + if (cachedCwd === liveCwd && pathExists(cachedCwd)) return cachedCwd; + if (!pathExists(cachedCwd)) return liveCwd; + if (liveCwd !== cachedCwd) return liveCwd; + return cachedCwd; +} diff --git a/src/tests/bg-shell-persistence-cwd.test.ts b/src/tests/bg-shell-persistence-cwd.test.ts new file mode 100644 index 000000000..f686beb28 --- /dev/null +++ b/src/tests/bg-shell-persistence-cwd.test.ts @@ -0,0 +1,36 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { resolveBgShellPersistenceCwd } from "../resources/extensions/bg-shell/utilities.ts"; + +test("keeps non-worktree cwd unchanged", () => { + const cached = "/repo"; + const live = "/repo"; + assert.equal(resolveBgShellPersistenceCwd(cached, live, () => true), cached); +}); + +test("rewrites stale auto-worktree cwd to live cwd after exit", () => { + const cached = "/repo/.gsd/worktrees/M001"; + const live = "/repo"; + assert.equal( + resolveBgShellPersistenceCwd(cached, live, (path) => path === live), + live, + ); +}); + +test("rewrites mismatched auto-worktree cwd to live cwd even if old path still exists", () => { + const cached = "/repo/.gsd/worktrees/M001"; + const live = "/repo"; + assert.equal( + resolveBgShellPersistenceCwd(cached, live, () => true), + live, + ); +}); + +test("keeps current auto-worktree cwd when it still matches process cwd", () => { + const cached = "/repo/.gsd/worktrees/M001"; + assert.equal( + resolveBgShellPersistenceCwd(cached, cached, () => true), + cached, + ); +});