diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e4d966d52..fb3c3ad48 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -14,6 +14,7 @@ - Fixed X11 clipboard text being misidentified as an image when the clipboard owner accepts unadvertised image targets ([#9786](https://github.com/earendil-works/pi/issues/9786)). - Prevented managed git packages from automatically installing Pi peer dependencies and added warnings for extension packages that list host-provided modules in `dependencies` ([#9863](https://github.com/earendil-works/pi/issues/9863)). +- Fixed pinned git extensions loaded with `-e` continuing to use the first downloaded commit after the ref changes ([#9982](https://github.com/earendil-works/pi/issues/9982)). ## [0.87.1] - 2026-09-22 diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index eb6f47825..be42365a9 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -2123,7 +2123,8 @@ export class DefaultPackageManager implements PackageManager { private getGitInstallPath(source: GitSource, scope: SourceScope): string { if (scope === "temporary") { - return this.getTemporaryDir(`git-${source.host}`, source.path); + // Include the ref in the hash so each pinned ref gets its own checkout. + return this.getTemporaryDir(`git-${source.host}`, source.path, source.ref); } const installRoot = this.getGitInstallRoot(scope); if (!installRoot) { @@ -2143,10 +2144,10 @@ export class DefaultPackageManager implements PackageManager { return join(this.agentDir, "git"); } - private getTemporaryDir(prefix: string, suffix?: string): string { + private getTemporaryDir(prefix: string, suffix?: string, ref?: string): string { const root = this.resolveManagedPath(getExtensionTempFolder(this.agentDir), prefix); const hash = createHash("sha256") - .update(`${prefix}-${suffix ?? ""}`) + .update(`${prefix}-${suffix ?? ""}${ref ? `@${ref}` : ""}`) .digest("hex") .slice(0, 8); return this.resolveManagedPath(root, hash, suffix ?? ""); diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index c93099b81..07cdc9d60 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -2615,6 +2615,35 @@ export default function(api) { api.registerTool({ name: "test", description: "te expect(refreshTemporaryGitSourceSpy).not.toHaveBeenCalled(); }); + // https://github.com/earendil-works/pi/issues/9982 + it("should load a new checkout when a pinned temporary git source changes ref", async () => { + const managerWithInternals = packageManager as unknown as PackageManagerInternals; + const oldSource = "git:github.com/example/repo@aaaaaaa"; + const newSource = "git:github.com/example/repo@bbbbbbb"; + const oldParsed = managerWithInternals.parseSource(oldSource); + const newParsed = managerWithInternals.parseSource(newSource); + if (oldParsed.type !== "git" || newParsed.type !== "git") { + throw new Error("Expected git sources"); + } + + const oldPath = managerWithInternals.getGitInstallPath(oldParsed, "temporary"); + mkdirSync(join(oldPath, "extensions"), { recursive: true }); + writeFileSync(join(oldPath, "extensions", "old.ts"), "export default function() {};"); + + const installParsedSourceSpy = vi + .spyOn(packageManager as any, "installParsedSource") + .mockImplementation(async () => { + const newPath = managerWithInternals.getGitInstallPath(newParsed, "temporary"); + mkdirSync(join(newPath, "extensions"), { recursive: true }); + writeFileSync(join(newPath, "extensions", "new.ts"), "export default function() {};"); + }); + + const result = await packageManager.resolveExtensionSources([newSource], { temporary: true }); + expect(installParsedSourceSpy).toHaveBeenCalledTimes(1); + expect(result.extensions.some((r) => pathEndsWith(r.path, "extensions/new.ts") && r.enabled)).toBe(true); + expect(result.extensions.some((r) => pathEndsWith(r.path, "extensions/old.ts"))).toBe(false); + }); + it("should not run npm view during resolve for installed unpinned packages", async () => { process.env.PI_OFFLINE = "1"; const installedPath = join(tempDir, ".pi", "npm", "node_modules", "example");