Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ src/
test/
unit/ Unit tests (node:test, dependency-injected, no VS Code API)
batchApply.test.ts Batch template and operation count parsing (18 tests)
binary.test.ts Binary discovery, managed install, compatibility, workspace env (66 tests)
binary.test.ts Binary discovery, managed install, compatibility, workspace env (74 tests)
binaryDiscovery.test.ts Real executable discovery on PATH (13 tests)
initializeProject.test.ts Status display, agents file classification, formatError (61 tests)
managedLifecycle.test.ts Managed install with real file I/O (22 tests)
Expand Down
48 changes: 39 additions & 9 deletions src/binary/patchloom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,46 @@ export interface PatchloomStatusInputs {
readonly isTrusted?: boolean;
}

let inflightStatus: Promise<PatchloomStatus> | undefined;

/**
* Share one in-flight status probe. Activate runs status bar, auto-update,
* and MCP register together; each would otherwise exec `--version`.
* The first `resolve` wins for joiners. No TTL cache: after the probe
* finishes (or after `clearPatchloomStatusInflight`) the next call
* starts a new probe so settings, trust, and install stay fresh.
*/
export async function resolvePatchloomStatusWithSharedInflight(
resolve: () => Promise<PatchloomStatus>
): Promise<PatchloomStatus> {
if (inflightStatus !== undefined) {
return inflightStatus;
}
const pending = resolve().finally(() => {
if (inflightStatus === pending) {
inflightStatus = undefined;
}
});
inflightStatus = pending;
return pending;
}

export function clearPatchloomStatusInflight(): void {
inflightStatus = undefined;
}

export async function resolvePatchloomStatus(): Promise<PatchloomStatus> {
const vscode = await import("vscode");
const managedInstallRoot = getManagedInstallRoot();
return resolvePatchloomStatusWithInputs({
configuredPath: vscode.workspace.getConfiguration("patchloom").get<string>("path", ""),
pathValue: process.env.PATH,
platform: process.platform,
arch: process.arch,
managedInstallRoot,
isTrusted: vscode.workspace.isTrusted
return resolvePatchloomStatusWithSharedInflight(async () => {
const vscode = await import("vscode");
const managedInstallRoot = getManagedInstallRoot();
return resolvePatchloomStatusWithInputs({
configuredPath: vscode.workspace.getConfiguration("patchloom").get<string>("path", ""),
pathValue: process.env.PATH,
platform: process.platform,
arch: process.arch,
managedInstallRoot,
isTrusted: vscode.workspace.isTrusted
});
});
}

Expand Down
5 changes: 4 additions & 1 deletion src/commands/managedInstall.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as vscode from "vscode";
import { resolvePatchloomStatus, comparePatchloomVersions, PATCHLOOM_RELEASES_URL } from "../binary/patchloom.js";
import { resolvePatchloomStatus, comparePatchloomVersions, PATCHLOOM_RELEASES_URL, clearPatchloomStatusInflight } from "../binary/patchloom.js";
import {
detectManagedInstallTarget,
fetchLatestReleaseVersion,
Expand Down Expand Up @@ -67,6 +67,7 @@ export async function installPatchloom(): Promise<void> {
});

log?.log(`Managed install complete: Patchloom ${result.version} at ${result.binaryPath}`);
clearPatchloomStatusInflight();
await refreshStatusBar();
await refreshMcpServerBinary();
await vscode.window.showInformationMessage(
Expand Down Expand Up @@ -143,6 +144,7 @@ export async function updatePatchloom(): Promise<void> {
});

log?.log(`Managed update complete: Patchloom ${result.version} at ${result.binaryPath}`);
clearPatchloomStatusInflight();
await refreshStatusBar();
await refreshMcpServerBinary();
await vscode.window.showInformationMessage(
Expand Down Expand Up @@ -196,6 +198,7 @@ export async function reinstallPatchloom(): Promise<void> {
});

log?.log(`Managed reinstall complete: Patchloom ${result.version} at ${result.binaryPath}`);
clearPatchloomStatusInflight();
await refreshStatusBar();
await refreshMcpServerBinary();
await vscode.window.showInformationMessage(
Expand Down
5 changes: 5 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { setupWorkspace, openPatchloomReleases, openPatchloomSettings, openDocum
import { showStatus } from "./commands/showStatus.js";
import { verifyMcp } from "./commands/verifyMcp.js";
import { checkForUpdates } from "./commands/autoUpdate.js";
import { clearPatchloomStatusInflight } from "./binary/patchloom.js";
import { setManagedInstallRoot } from "./install/managed.js";
import { createPatchloomLog, getPatchloomLog, setPatchloomLog } from "./logging/outputChannel.js";
import { registerMcpServerProviderWithBinary } from "./mcp/register.js";
Expand Down Expand Up @@ -37,13 +38,16 @@ export function activate(context: vscode.ExtensionContext): void {
new vscode.Disposable(disposeStatusBar),
vscode.workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration("patchloom")) {
clearPatchloomStatusInflight();
void refreshStatusBar();
}
}),
vscode.workspace.onDidChangeWorkspaceFolders(() => {
clearPatchloomStatusInflight();
void refreshStatusBar();
}),
vscode.workspace.onDidGrantWorkspaceTrust(() => {
clearPatchloomStatusInflight();
void refreshStatusBar();
})
);
Expand All @@ -54,6 +58,7 @@ export function activate(context: vscode.ExtensionContext): void {
}

export function deactivate(): void {
clearPatchloomStatusInflight();
setManagedInstallRoot(undefined);
const log = getPatchloomLog();
log?.dispose();
Expand Down
110 changes: 109 additions & 1 deletion test/unit/binary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
parsePatchloomVersion,
ensurePatchloomReadyOrNotify,
preferredBinaryRemediationAction,
resolvePatchloomStatusWithInputs
resolvePatchloomStatusWithInputs,
resolvePatchloomStatusWithSharedInflight,
clearPatchloomStatusInflight,
type PatchloomStatus
} from "../../src/binary/patchloom.js";
import {
assertTrustedManagedInstallDownloadUrl,
Expand Down Expand Up @@ -1084,6 +1087,111 @@ test("resolvePatchloomStatusWithInputs reports restricted message when untrusted
assert.ok(status.message.includes("untrusted"));
});

test("resolvePatchloomStatusWithSharedInflight coalesces concurrent probes", async () => {
clearPatchloomStatusInflight();
let calls = 0;
const status: PatchloomStatus = {
ready: true,
source: "path",
message: "ok",
binaryPath: "/bin/patchloom"
};
const resolve = async () => {
calls += 1;
await new Promise((r) => setTimeout(r, 20));
return status;
};

const [a, b, c] = await Promise.all([
resolvePatchloomStatusWithSharedInflight(resolve),
resolvePatchloomStatusWithSharedInflight(resolve),
resolvePatchloomStatusWithSharedInflight(resolve)
]);

assert.equal(calls, 1);
assert.equal(a, status);
assert.equal(b, status);
assert.equal(c, status);
clearPatchloomStatusInflight();
});

test("resolvePatchloomStatusWithSharedInflight starts a new probe after the first finishes", async () => {
clearPatchloomStatusInflight();
let calls = 0;
const resolve = async () => {
calls += 1;
return {
ready: true,
source: "path" as const,
message: `call-${calls}`,
binaryPath: "/bin/patchloom"
};
};

const first = await resolvePatchloomStatusWithSharedInflight(resolve);
const second = await resolvePatchloomStatusWithSharedInflight(resolve);

assert.equal(calls, 2);
assert.equal(first.message, "call-1");
assert.equal(second.message, "call-2");
clearPatchloomStatusInflight();
});

test("clearPatchloomStatusInflight starts a new probe while one is pending", async () => {
clearPatchloomStatusInflight();
let calls = 0;
let releaseFirst: (() => void) | undefined;
const firstGate = new Promise<void>((resolve) => {
releaseFirst = resolve;
});
const resolve = async () => {
calls += 1;
const n = calls;
if (n === 1) {
await firstGate;
}
return {
ready: true,
source: "path" as const,
message: `call-${n}`,
binaryPath: "/bin/patchloom"
};
};

const first = resolvePatchloomStatusWithSharedInflight(resolve);
clearPatchloomStatusInflight();
const second = resolvePatchloomStatusWithSharedInflight(resolve);
releaseFirst?.();
const [a, b] = await Promise.all([first, second]);
assert.equal(calls, 2);
assert.equal(a.message, "call-1");
assert.equal(b.message, "call-2");
clearPatchloomStatusInflight();
});

test("resolvePatchloomStatusWithSharedInflight retries after a failed probe", async () => {
clearPatchloomStatusInflight();
let calls = 0;
const resolve = async () => {
calls += 1;
if (calls === 1) {
throw new Error("version failed");
}
return {
ready: true,
source: "path" as const,
message: "ok",
binaryPath: "/bin/patchloom"
};
};

await assert.rejects(() => resolvePatchloomStatusWithSharedInflight(resolve), /version failed/);
const recovered = await resolvePatchloomStatusWithSharedInflight(resolve);
assert.equal(calls, 2);
assert.equal(recovered.ready, true);
clearPatchloomStatusInflight();
});

test("resolvePatchloomStatusWithInputs allows setting and PATH in trusted workspaces", async () => {
const status = await resolvePatchloomStatusWithInputs({
configuredPath: "/custom/patchloom",
Expand Down
Loading