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: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ src/
mcp/register.ts Native MCP server definition provider (VS Code 1.100+ API)
status/details.ts Status presentation: buildStatusDetails, preferredStatusAction
status/statusBar.ts Status bar item: create, refresh, dispose
status/refreshAfterInputChange.ts Clear inflight status then refresh bar and MCP
workspace/readiness.ts Workspace readiness: environment detection, folder selection
test/
unit/ Unit tests (node:test, dependency-injected, no VS Code API)
Expand All @@ -55,6 +56,7 @@ test/
managedLifecycle.test.ts Managed install with real file I/O (22 tests)
mcpConfig.test.ts MCP config with real temp directories (12 tests)
mcpRegister.test.ts Native MCP definition helper for binary path (2 tests)
statusRefresh.test.ts Status and MCP refresh order after input change (1 test)
outputChannel.test.ts Output channel logging wrapper (22 tests)
patchloomCli.test.ts Patchloom CLI integration with real binary + managed install e2e MCP (46 tests incl. e2e)
propertyBased.test.ts Property-based tests with fast-check (13 tests)
Expand Down
24 changes: 17 additions & 7 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ 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";
import { refreshMcpServerBinary, registerMcpServerProviderWithBinary } from "./mcp/register.js";
import { refreshAfterPatchloomInputChange } from "./status/refreshAfterInputChange.js";
import { disposeStatusBar, refreshStatusBar } from "./status/statusBar.js";

export function activate(context: vscode.ExtensionContext): void {
Expand All @@ -38,17 +39,26 @@ export function activate(context: vscode.ExtensionContext): void {
new vscode.Disposable(disposeStatusBar),
vscode.workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration("patchloom")) {
clearPatchloomStatusInflight();
void refreshStatusBar();
void refreshAfterPatchloomInputChange(
clearPatchloomStatusInflight,
refreshStatusBar,
refreshMcpServerBinary
);
}
}),
vscode.workspace.onDidChangeWorkspaceFolders(() => {
clearPatchloomStatusInflight();
void refreshStatusBar();
void refreshAfterPatchloomInputChange(
clearPatchloomStatusInflight,
refreshStatusBar,
refreshMcpServerBinary
);
}),
vscode.workspace.onDidGrantWorkspaceTrust(() => {
clearPatchloomStatusInflight();
void refreshStatusBar();
void refreshAfterPatchloomInputChange(
clearPatchloomStatusInflight,
refreshStatusBar,
refreshMcpServerBinary
);
})
);

Expand Down
13 changes: 13 additions & 0 deletions src/status/refreshAfterInputChange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Drop a shared status probe and refresh surfaces that cache the CLI path.
* Callers pass the concrete refresh functions so unit tests can assert order.
*/
export async function refreshAfterPatchloomInputChange(
clearInflight: () => void,
refreshStatus: () => Promise<void>,
refreshMcp: () => Promise<void>
): Promise<void> {
clearInflight();
await refreshStatus();
await refreshMcp();
}
13 changes: 12 additions & 1 deletion test/unit/binary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1141,15 +1141,22 @@ test("clearPatchloomStatusInflight starts a new probe while one is pending", asy
clearPatchloomStatusInflight();
let calls = 0;
let releaseFirst: (() => void) | undefined;
let releaseSecond: (() => void) | undefined;
const firstGate = new Promise<void>((resolve) => {
releaseFirst = resolve;
});
const secondGate = new Promise<void>((resolve) => {
releaseSecond = resolve;
});
const resolve = async () => {
calls += 1;
const n = calls;
if (n === 1) {
await firstGate;
}
if (n === 2) {
await secondGate;
}
return {
ready: true,
source: "path" as const,
Expand All @@ -1162,10 +1169,14 @@ test("clearPatchloomStatusInflight starts a new probe while one is pending", asy
clearPatchloomStatusInflight();
const second = resolvePatchloomStatusWithSharedInflight(resolve);
releaseFirst?.();
const [a, b] = await Promise.all([first, second]);
const third = resolvePatchloomStatusWithSharedInflight(resolve);
assert.equal(calls, 2);
releaseSecond?.();
const [a, b, c] = await Promise.all([first, second, third]);
assert.equal(calls, 2);
assert.equal(a.message, "call-1");
assert.equal(b.message, "call-2");
assert.equal(c.message, "call-2");
clearPatchloomStatusInflight();
});

Expand Down
19 changes: 19 additions & 0 deletions test/unit/statusRefresh.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import assert from "node:assert/strict";
import test from "node:test";
import { refreshAfterPatchloomInputChange } from "../../src/status/refreshAfterInputChange.js";

test("refreshAfterPatchloomInputChange clears inflight before status and MCP refresh", async () => {
const order: string[] = [];
await refreshAfterPatchloomInputChange(
() => {
order.push("clear");
},
async () => {
order.push("status");
},
async () => {
order.push("mcp");
}
);
assert.deepEqual(order, ["clear", "status", "mcp"]);
});
Loading