diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index f4b8b940d20..f36881d4a3a 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -14,6 +14,7 @@ import { NodeFileSystem, NodePath } from "@effect/platform-node" import { makeRuntime } from "@/effect/run-service" import { AppFileSystem } from "@/filesystem" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { Filesystem } from "@/util/filesystem" export namespace Project { const log = Log.create({ service: "project" }) @@ -315,7 +316,7 @@ export namespace Project { d .update(SessionTable) .set({ project_id: data.id }) - .where(and(eq(SessionTable.project_id, ProjectID.global), eq(SessionTable.directory, data.worktree))) + .where(and(eq(SessionTable.project_id, ProjectID.global), eq(SessionTable.directory, Filesystem.normalizePathSeparators(data.worktree)))) .run(), ) } diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index eb01739c156..5deeb84a460 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -17,6 +17,7 @@ import { ProjectTable } from "../project/project.sql" import { Storage } from "@/storage/storage" import { Log } from "../util/log" import { updateSchema } from "../util/update-schema" +import { Filesystem } from "@/util/filesystem" import { MessageV2 } from "./message-v2" import { Instance } from "../project/instance" import { SessionPrompt } from "./prompt" @@ -400,7 +401,7 @@ export namespace Session { slug: Slug.create(), version: Installation.VERSION, projectID: Instance.project.id, - directory: input.directory, + directory: Filesystem.normalizePathSeparators(input.directory), workspaceID: input.workspaceID, parentID: input.parentID, title: input.title ?? createDefaultTitle(!!input.parentID), @@ -768,7 +769,7 @@ export namespace Session { conditions.push(eq(SessionTable.workspace_id, input.workspaceID)) } if (input?.directory) { - conditions.push(eq(SessionTable.directory, input.directory)) + conditions.push(eq(SessionTable.directory, Filesystem.normalizePathSeparators(input.directory))) } if (input?.roots) { conditions.push(isNull(SessionTable.parent_id)) @@ -808,7 +809,7 @@ export namespace Session { const conditions: SQL[] = [] if (input?.directory) { - conditions.push(eq(SessionTable.directory, input.directory)) + conditions.push(eq(SessionTable.directory, Filesystem.normalizePathSeparators(input.directory))) } if (input?.roots) { conditions.push(isNull(SessionTable.parent_id)) diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 37f00c6b9c8..fdccb5b498b 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -113,6 +113,23 @@ export namespace Filesystem { } } + /** + * Normalize path separators to forward slashes for consistent database queries. + * This fixes Windows path compatibility issues where paths might use backslashes + * in some cases and forward slashes in others, and ensures consistent drive letter casing. + */ + export function normalizePathSeparators(p: string): string { + // Convert backslashes to forward slashes + let normalized = p.replace(/\\/g, "/") + + // Normalize Windows drive letters to uppercase (e.g., c:/ -> C:/) + if (/^[a-z]:\//.test(normalized)) { + normalized = normalized[0].toUpperCase() + normalized.slice(1) + } + + return normalized + } + // We cannot rely on path.resolve() here because git.exe may come from Git Bash, Cygwin, or MSYS2, so we need to translate these paths at the boundary. // Also resolves symlinks so that callers using the result as a cache key // always get the same canonical path for a given physical directory.