diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 5dae699733f8..d5fda7005c3a 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -2,7 +2,7 @@ export * as SessionV2 from "./session" export * from "./session/schema" import { DateTime, Effect, Layer, Schema, Context, Stream } from "effect" -import { and, asc, desc, eq, gt, like, lt, or, type SQL } from "drizzle-orm" +import { and, asc, desc, eq, gt, gte, like, lt, or, type SQL } from "drizzle-orm" import { ProjectV2 } from "./project" import { WorkspaceV2 } from "./workspace" import { ModelV2 } from "./model" @@ -248,6 +248,13 @@ export const layer = Layer.effect( if ("directory" in input) conditions.push(eq(SessionTable.directory, input.directory)) if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID)) if ("project" in input) conditions.push(eq(SessionTable.project_id, input.project)) + if ("subpath" in input && input.subpath) + conditions.push( + or( + eq(SessionTable.path, input.subpath), + and(gte(SessionTable.path, `${input.subpath}/`), lt(SessionTable.path, `${input.subpath}0`)), + )!, + ) if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`)) if (input.anchor) { conditions.push( diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index 6fd80c60da1d..3020ae1e834a 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -11,7 +11,7 @@ import { ModelV2 } from "@opencode-ai/core/model" import { ProjectV2 } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { ProviderV2 } from "@opencode-ai/core/provider" -import { AbsolutePath } from "@opencode-ai/core/schema" +import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema" import { SessionV2 } from "@opencode-ai/core/session" import { SessionV1 } from "@opencode-ai/core/v1/session" import { Prompt } from "@opencode-ai/core/session/prompt" @@ -426,3 +426,54 @@ describe("SessionV2.create", () => { }), ) }) + +describe("SessionV2.list", () => { + it.effect("limits project sessions to the requested subpath", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const { db } = yield* Database.Service + const core = yield* session.create({ location }) + const nested = yield* session.create({ location }) + const web = yield* session.create({ location }) + + yield* Effect.all([ + db.update(SessionTable).set({ path: "packages/core" }).where(eq(SessionTable.id, core.id)).run(), + db.update(SessionTable).set({ path: "packages/core/test" }).where(eq(SessionTable.id, nested.id)).run(), + db.update(SessionTable).set({ path: "packages/web" }).where(eq(SessionTable.id, web.id)).run(), + ]).pipe(Effect.orDie) + + const listed = yield* session.list({ + project: ProjectV2.ID.global, + subpath: RelativePath.make("packages/core"), + }) + + expect(new Set(listed.map((item) => item.id))).toEqual(new Set([core.id, nested.id])) + }), + ) + + it.effect("treats wildcard characters in subpaths literally", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const { db } = yield* Database.Service + const exact = yield* session.create({ location }) + const nested = yield* session.create({ location }) + const sibling = yield* session.create({ location }) + + yield* Effect.all([ + db.update(SessionTable).set({ path: "packages/core_test" }).where(eq(SessionTable.id, exact.id)).run(), + db.update(SessionTable).set({ path: "packages/core_test/unit" }).where(eq(SessionTable.id, nested.id)).run(), + db.update(SessionTable).set({ path: "packages/coreXtest/unit" }).where(eq(SessionTable.id, sibling.id)).run(), + ]).pipe(Effect.orDie) + + const listed = yield* session.list({ + project: ProjectV2.ID.global, + subpath: RelativePath.make("packages/core_test"), + }) + + const ids = listed.map((item) => item.id) + expect(ids).toContain(exact.id) + expect(ids).toContain(nested.id) + expect(ids).not.toContain(sibling.id) + }), + ) +})