Fix Windows workspace provider loading (#1329)

* test: cover Windows provider snapshot cwd updates

* fix: normalize Windows provider snapshot paths

* fix: preserve Claude history path candidates

* fix: match Claude project paths on Windows
This commit is contained in:
Mohamed Boudra
2026-06-05 02:29:34 +08:00
committed by GitHub
parent b5192e577a
commit c3515d74b9
7 changed files with 63 additions and 18 deletions
@@ -39,4 +39,13 @@ describe("providers snapshot query scope", () => {
providers: ["codex"],
});
});
it("uses one query scope for Windows cwd values with either separator", () => {
expect(normalizeProvidersSnapshotCwd("C:\\Users\\Ezekiel Bulver\\project")).toBe(
"C:/Users/Ezekiel Bulver/project",
);
expect(providersSnapshotQueryKey("server-1", "C:\\Users\\Ezekiel Bulver\\project")).toEqual(
providersSnapshotQueryKey("server-1", "C:/Users/Ezekiel Bulver/project"),
);
});
});
@@ -1,10 +1,10 @@
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
import { normalizeWorkspacePath } from "@/utils/workspace-identity";
export const PROVIDERS_SNAPSHOT_QUERY_ROOT = "providersSnapshot";
export function normalizeProvidersSnapshotCwd(cwd?: string | null): string | null {
const trimmed = cwd?.trim();
return trimmed ? trimmed : null;
return normalizeWorkspacePath(cwd);
}
export function providersSnapshotQueryRoot(serverId: string | null) {
@@ -250,6 +250,27 @@ describe("applyProvidersSnapshotUpdate", () => {
providersSnapshot([]),
);
});
it("applies Windows daemon updates to app-normalized workspace paths", () => {
const workspaceCwd = "C:/Users/Ezekiel Bulver/project";
const daemonCwd = "C:\\Users\\Ezekiel Bulver\\project";
queryClient.setQueryData(
providersSnapshotQueryKey(serverId, workspaceCwd),
providersSnapshot([codexEntry("loading")]),
);
applyProvidersSnapshotUpdate({
serverId,
queryClient,
message: updateMessage([codexEntry("ready", [readyCodexModel])], daemonCwd),
});
expect(queryClient.getQueryData(providersSnapshotQueryKey(serverId, workspaceCwd))).toEqual({
entries: [codexEntry("ready", [readyCodexModel])],
generatedAt: "2026-01-01T00:00:01.000Z",
requestId: "providers_snapshot_update",
});
});
});
describe("selectorOpenRefetchDecision", () => {
@@ -45,6 +45,7 @@ import { renderPromptAttachmentAsText } from "../../prompt-attachments.js";
import { claudeQuery, type ClaudeOptions, type ClaudeQueryFactory } from "./query.js";
import { realClaudeRewindSdk, revertClaudeConversation, revertClaudeFiles } from "./rewind.js";
import { normalizeProviderReplayTimestamp } from "../../provider-history-timestamps.js";
import { claudeProjectDirSync } from "./project-dir.js";
import {
getAgentStreamEventTurnId,
@@ -339,10 +340,6 @@ function isClaudeThinkingEffort(value: string | null | undefined): value is Clau
);
}
function sanitizeClaudeProjectPath(cwd: string): string {
return cwd.replace(/[\\/._:]/g, "-");
}
interface ClaudeOptionsLogSummary {
cwd: string | null;
permissionMode: string | null;
@@ -3985,14 +3982,15 @@ class ClaudeAgentSession implements AgentSession {
// Fall back to the configured cwd when the path has already disappeared.
}
for (const candidate of candidates) {
const sanitized = sanitizeClaudeProjectPath(candidate);
const historyPath = path.join(configDir, "projects", sanitized, `${sessionId}.jsonl`);
const historyPath = path.join(
claudeProjectDirSync(candidate, { configDir }),
`${sessionId}.jsonl`,
);
if (fs.existsSync(historyPath)) {
return historyPath;
}
}
const sanitized = sanitizeClaudeProjectPath(cwd);
return path.join(configDir, "projects", sanitized, `${sessionId}.jsonl`);
return path.join(claudeProjectDirSync(cwd, { configDir }), `${sessionId}.jsonl`);
}
private convertHistoryEntry(entry: ClaudeHistoryEntry): AgentTimelineItem[] {
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { createTestLogger } from "../../../../test-utils/test-logger.js";
import { ClaudeAgentClient } from "./agent.js";
import { claudeProjectDirSync } from "./project-dir.js";
import { streamSession } from "../test-utils/session-stream-adapter.js";
import type { AgentPersistenceHandle, AgentStreamEvent } from "../../agent-sdk-types.js";
@@ -15,10 +16,6 @@ const LIVE_REPLY_MARKER = "LIVE_ONLY_REPLY_MARKER";
const HISTORY_USER_MARKER = "HISTORY_ONLY_USER_MARKER";
const HISTORY_ASSISTANT_MARKER = "HISTORY_ONLY_ASSISTANT_MARKER";
function sanitizeClaudeProjectPath(cwd: string): string {
return cwd.replace(/[\\/._:]/g, "-");
}
function buildSdkQueryMock() {
const events = [
{
@@ -100,12 +97,11 @@ describe("ClaudeAgentSession history replay regression", () => {
});
tempRoot = mkdtempSync(path.join(os.tmpdir(), "claude-history-regression-"));
cwd = path.join(tempRoot, "repo");
cwd = path.join(tempRoot, "Michael Depies", "repo");
configDir = path.join(tempRoot, "claude-config");
mkdirSync(cwd, { recursive: true });
const sanitized = sanitizeClaudeProjectPath(cwd);
const historyDir = path.join(configDir, "projects", sanitized);
const historyDir = claudeProjectDirSync(cwd, { configDir });
mkdirSync(historyDir, { recursive: true });
const historyPath = path.join(historyDir, "history-session.jsonl");
writeFileSync(
@@ -7,7 +7,7 @@ import { join } from "node:path";
import { getSessionInfo } from "@anthropic-ai/claude-agent-sdk";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { claudeProjectDir } from "./project-dir.js";
import { claudeProjectDir, claudeProjectDirSync } from "./project-dir.js";
// Parity oracle: the Claude SDK's getSessionInfo({ dir }) canonicalizes the
// given dir with the SDK's own encoder and looks for `<sessionId>.jsonl` under
@@ -101,6 +101,12 @@ describe("claudeProjectDir parity with Claude Agent SDK", () => {
expect(info?.sessionId).toBe(sessionId);
});
}
test("sync helper uses the same directory as the SDK-parity helper", async () => {
const cwd = await ensureDir(join(workspaceRoot, "sync path with spaces"));
await expect(claudeProjectDir(cwd)).resolves.toBe(claudeProjectDirSync(cwd));
});
});
async function ensureDir(path: string): Promise<string> {
@@ -1,3 +1,4 @@
import { realpathSync } from "node:fs";
import { realpath } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
@@ -23,6 +24,12 @@ export async function claudeProjectDir(
return join(projectsRoot, encode(canonical));
}
export function claudeProjectDirSync(cwd: string, options?: ClaudeProjectDirOptions): string {
const canonical = canonicalizeSync(cwd);
const projectsRoot = join(resolveConfigDir(options), "projects");
return join(projectsRoot, encode(canonical));
}
async function canonicalize(input: string): Promise<string> {
try {
return (await realpath(input)).normalize("NFC");
@@ -31,6 +38,14 @@ async function canonicalize(input: string): Promise<string> {
}
}
function canonicalizeSync(input: string): string {
try {
return realpathSync.native(input).normalize("NFC");
} catch {
return input.normalize("NFC");
}
}
function encode(input: string): string {
const replaced = input.replace(/[^a-zA-Z0-9]/g, "-");
if (replaced.length <= PROJECT_DIR_LENGTH_CAP) {