diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 797a635eeef8..7e3abe75a2e0 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -18,6 +18,7 @@ import { like } from "drizzle-orm" import { inArray } from "drizzle-orm" import { lt } from "drizzle-orm" import { or } from "drizzle-orm" +import { sql } from "drizzle-orm" import { SyncEvent } from "../sync" import type { SQL } from "drizzle-orm" import { PartTable, SessionTable } from "./session.sql" @@ -49,6 +50,22 @@ function createDefaultTitle(isChild = false) { return (isChild ? childTitlePrefix : parentTitlePrefix) + new Date().toISOString() } +function isWindowsDirectory(directory: string) { + return directory.length > 1 && (directory[1] === ":" || directory.startsWith("\\\\")) +} + +function normalizeDirectory(directory: string) { + if (isWindowsDirectory(directory)) { + return directory.replaceAll("\\", "/") + } + return directory +} + +function directoryMatches(directory: string) { + if (!isWindowsDirectory(directory)) return eq(SessionTable.directory, directory) + return sql`replace(${SessionTable.directory}, ${"\\"}, ${"/"}) = ${normalizeDirectory(directory)}` +} + export function isDefaultTitle(title: string) { return new RegExp( `^(${parentTitlePrefix}|${childTitlePrefix})\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$`, @@ -536,7 +553,7 @@ export const layer: Layer.Layer< slug: Slug.create(), version: InstallationVersion, projectID: ctx.project.id, - directory: input.directory, + directory: normalizeDirectory(input.directory), path: input.path, workspaceID: input.workspaceID, parentID: input.parentID, @@ -904,13 +921,13 @@ function* listByProject( conditions.push( input.directory - ? or(...conds, and(isNull(SessionTable.path), eq(SessionTable.directory, input.directory))!)! + ? or(...conds, and(isNull(SessionTable.path), directoryMatches(input.directory))!)! : or(...conds)!, ) } } else if (input.scope !== "project" && !input.experimentalWorkspaces) { if (input.directory) { - conditions.push(eq(SessionTable.directory, input.directory)) + conditions.push(directoryMatches(input.directory)) } } if (input.roots) { @@ -951,7 +968,7 @@ export function* listGlobal(input?: { const conditions: SQL[] = [] if (input?.directory) { - conditions.push(eq(SessionTable.directory, input.directory)) + conditions.push(directoryMatches(input.directory)) } if (input?.roots) { conditions.push(isNull(SessionTable.parent_id)) diff --git a/packages/opencode/test/server/global-session-list.test.ts b/packages/opencode/test/server/global-session-list.test.ts index 0fdba1f66397..f951ad0c89fd 100644 --- a/packages/opencode/test/server/global-session-list.test.ts +++ b/packages/opencode/test/server/global-session-list.test.ts @@ -6,6 +6,9 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import * as Log from "@opencode-ai/core/util/log" import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" +import { Database } from "@/storage/db" +import { SessionTable } from "@/session/session.sql" +import { eq } from "drizzle-orm" void Log.init({ print: false }) @@ -101,4 +104,30 @@ describe("session.listGlobal", () => { }), { git: true }, ) + + it.instance( + "matches Windows directories across separator styles", + () => + Effect.gen(function* () { + const created = yield* withSession({ title: "windows-global-directory" }) + const storedDirectory = String.raw`C:\Users\demo\project` + const requestedDirectory = "C:/Users/demo/project" + + yield* Effect.sync(() => + Database.use((db) => + db + .update(SessionTable) + .set({ directory: storedDirectory, path: null }) + .where(eq(SessionTable.id, created.id)) + .run(), + ), + ) + + const sessions = yield* Effect.sync(() => [...SessionNs.listGlobal({ directory: requestedDirectory })]) + const ids = sessions.map((session) => session.id) + + expect(ids).toContain(created.id) + }), + { git: true }, + ) }) diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index 4aab0c4b1f37..302d82d92146 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -95,6 +95,33 @@ describe("session.list", () => { { git: true }, ) + it.instance( + "matches Windows directories across separator styles", + () => + Effect.gen(function* () { + const created = yield* withSession({ title: "windows-directory" }) + const storedDirectory = String.raw`C:\Users\demo\project` + const requestedDirectory = "C:/Users/demo/project" + + yield* Effect.sync(() => + Database.use((db) => + db + .update(SessionTable) + .set({ directory: storedDirectory, path: null }) + .where(eq(SessionTable.id, created.id)) + .run(), + ), + ) + + const ids = (yield* SessionNs.Service.use((session) => session.list({ directory: requestedDirectory }))).map( + (session) => session.id, + ) + + expect(ids).toContain(created.id) + }), + { git: true }, + ) + it.instance( "filters by path and ignores directory when path is provided", () =>