Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { WorkspaceID } from "@/control-plane/schema"
import { sameDirectory } from "@/session/directory"
import { SessionV2 } from "@/v2/session"
import { Effect, Schema } from "effect"
import { HttpApiBuilder, HttpApiError, HttpApiSchema } from "effect/unstable/httpapi"
Expand Down Expand Up @@ -43,7 +44,7 @@ function hasCursorRoutingMismatch(
decoded: SessionCursor | undefined,
) {
if (!decoded) return false
if (query.directory !== undefined && query.directory !== decoded.directory) return true
if (query.directory !== undefined && !sameDirectory(query.directory, decoded.directory)) return true
return query.workspace !== undefined && query.workspace !== decoded.workspaceID
}

Expand Down
31 changes: 31 additions & 0 deletions packages/opencode/src/session/directory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { AppFileSystem } from "@opencode-ai/core/filesystem"

export function directoryVariants(input: string | undefined) {
if (!input) return []
const values = new Set<string>()
values.add(input)

const resolved = resolveDirectory(input)
values.add(resolved)

if (process.platform !== "win32") return [...values]

values.add(input.replaceAll("\\", "/"))
values.add(resolved.replaceAll("\\", "/"))
return [...values]
}

export function sameDirectory(a: string | undefined, b: string | undefined) {
if (a === b) return true
if (!a || !b) return false
const values = new Set(directoryVariants(a))
return directoryVariants(b).some((value) => values.has(value))
}

function resolveDirectory(input: string) {
try {
return AppFileSystem.resolve(input)
} catch {
return input
}
}
20 changes: 14 additions & 6 deletions packages/opencode/src/session/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { MessageV2 } from "./message-v2"
import type { InstanceContext } from "../project/instance-context"
import { InstanceState } from "@/effect/instance-state"
import { Snapshot } from "@/snapshot"
import { directoryVariants } from "./directory"
import { ProjectID } from "../project/schema"
import { WorkspaceID } from "../control-plane/schema"
import { SessionID, MessageID, PartID } from "./schema"
Expand Down Expand Up @@ -894,6 +895,7 @@ function* listByProject(
},
) {
const conditions = [eq(SessionTable.project_id, input.projectID)]
const directories = directoryVariants(input.directory)

if (input.workspaceID) {
conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
Expand All @@ -903,14 +905,14 @@ function* listByProject(
const conds = [eq(SessionTable.path, input.path), like(SessionTable.path, `${input.path}/%`)]

conditions.push(
input.directory
? or(...conds, and(isNull(SessionTable.path), eq(SessionTable.directory, input.directory))!)!
directories.length > 0
? or(...conds, and(isNull(SessionTable.path), matchDirectory(SessionTable.directory, directories))!)!
: or(...conds)!,
)
}
} else if (input.scope !== "project" && !input.experimentalWorkspaces) {
if (input.directory) {
conditions.push(eq(SessionTable.directory, input.directory))
if (directories.length > 0) {
conditions.push(matchDirectory(SessionTable.directory, directories))
}
}
if (input.roots) {
Expand Down Expand Up @@ -949,9 +951,10 @@ export function* listGlobal(input?: {
archived?: boolean
}) {
const conditions: SQL[] = []
const directories = directoryVariants(input?.directory)

if (input?.directory) {
conditions.push(eq(SessionTable.directory, input.directory))
if (directories.length > 0) {
conditions.push(matchDirectory(SessionTable.directory, directories))
}
if (input?.roots) {
conditions.push(isNull(SessionTable.parent_id))
Expand Down Expand Up @@ -1008,4 +1011,9 @@ export function* listGlobal(input?: {
}
}

function matchDirectory(column: typeof SessionTable.directory, directories: string[]) {
if (directories.length === 1) return eq(column, directories[0]!)
return or(...directories.map((directory) => eq(column, directory)))!
}

export * as Session from "./session"
9 changes: 8 additions & 1 deletion packages/opencode/src/v2/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { EventV2 } from "@opencode-ai/core/event"
import { EventV2Bridge } from "@/event-v2-bridge"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { directoryVariants } from "@/session/directory"

export const Delivery = Schema.Literals(["immediate", "deferred"]).annotate({
identifier: "Session.Delivery",
Expand Down Expand Up @@ -181,7 +182,8 @@ export const layer = Layer.effect(
if (direction === "previous" && order === "asc") order = "desc"
if (direction === "previous" && order === "desc") order = "asc"
const conditions: SQL[] = []
if (input.directory) conditions.push(eq(SessionTable.directory, input.directory))
const directories = directoryVariants(input.directory)
if (directories.length > 0) conditions.push(matchDirectory(directories))
if (input.path)
conditions.push(or(eq(SessionTable.path, input.path), like(SessionTable.path, `${input.path}/%`))!)
if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
Expand Down Expand Up @@ -334,6 +336,11 @@ export const layer = Layer.effect(
}),
)

function matchDirectory(directories: string[]) {
if (directories.length === 1) return eq(SessionTable.directory, directories[0]!)
return or(...directories.map((directory) => eq(SessionTable.directory, directory)))!
}

export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))

export * as SessionV2 from "./session"
57 changes: 57 additions & 0 deletions packages/opencode/test/server/session-list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Storage } from "@/storage/storage"
import { SyncEvent } from "@/sync"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { BackgroundJob } from "@/background/job"
import { sameDirectory } from "@/session/directory"

void Log.init({ print: false })
const it = testEffect(
Expand All @@ -37,6 +38,13 @@ afterEach(async () => {
})

describe("session.list", () => {
it.effect("treats slash-equivalent Windows directories as equal", () => {
if (process.platform !== "win32") return Effect.void
expect(sameDirectory("C:/repo/demo", "C:\\repo\\demo")).toBe(true)
expect(sameDirectory("C:/repo/demo", "C:/repo/other")).toBe(false)
return Effect.void
})

it.instance(
"does not filter by directory when directory is omitted",
() =>
Expand Down Expand Up @@ -65,6 +73,26 @@ describe("session.list", () => {
{ git: true },
)

it.instance(
"matches slash-equivalent directory filters on Windows",
() =>
Effect.gen(function* () {
if (process.platform !== "win32") return
const test = yield* TestInstance
yield* Effect.promise(() => mkdir(path.join(test.directory, "packages", "opencode"), { recursive: true }))

const current = yield* withSession({ title: "current" }).pipe(
provideInstance(path.join(test.directory, "packages", "opencode")),
)

const ids = (yield* SessionNs.Service.use((session) =>
session.list({ directory: path.join(test.directory, "packages", "opencode").replaceAll("\\", "/") }),
)).map((session) => session.id)
expect(ids).toContain(current.id)
}),
{ git: true },
)

it.instance(
"filters by directory when directory is provided",
() =>
Expand Down Expand Up @@ -95,6 +123,35 @@ describe("session.list", () => {
{ git: true },
)

it.instance(
"matches legacy slash-stored directory rows on Windows",
() =>
Effect.gen(function* () {
if (process.platform !== "win32") return
const test = yield* TestInstance
yield* Effect.promise(() => mkdir(path.join(test.directory, "packages", "opencode", "src"), { recursive: true }))

const currentDir = path.join(test.directory, "packages", "opencode", "src")
const current = yield* withSession({ title: "legacy-current" }).pipe(provideInstance(currentDir))

yield* Effect.sync(() =>
Database.use((db) =>
db
.update(SessionTable)
.set({ path: null, directory: currentDir.replaceAll("\\", "/") })
.where(eq(SessionTable.id, current.id))
.run(),
),
)

const ids = (yield* SessionNs.Service.use((session) =>
session.list({ directory: currentDir, path: "packages/opencode/src" }),
)).map((session) => session.id)
expect(ids).toContain(current.id)
}),
{ git: true },
)

it.instance(
"filters by path and ignores directory when path is provided",
() =>
Expand Down
Loading