diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 9e6ddda9d2d8..04bd00610c3a 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -4,6 +4,7 @@ import { cmd } from "./cmd" import { effectCmd, fail } from "../effect-cmd" import { Session } from "@/session/session" import { SessionID } from "../../session/schema" +import { Project } from "@/project/project" import { UI } from "../ui" import { Locale } from "@/util/locale" import { Flag } from "@opencode-ai/core/flag/flag" @@ -70,6 +71,7 @@ export const SessionDeleteCommand = effectCmd({ export const SessionListCommand = effectCmd({ command: "list", describe: "list sessions", + instance: false, builder: (yargs) => yargs .option("max-count", { @@ -84,7 +86,11 @@ export const SessionListCommand = effectCmd({ default: "table", }), handler: Effect.fn("Cli.session.list")(function* (args) { - const sessions = yield* Session.Service.use((svc) => svc.list({ roots: true, limit: args.maxCount })) + const currentWorkingDirectory = process.cwd() + const { project } = yield* Project.Service.use((svc) => svc.fromDirectory(currentWorkingDirectory)) + const sessions = yield* Session.Service.use((svc) => + svc.list({ projectID: project.id, roots: true, limit: args.maxCount }), + ) if (sessions.length === 0) return diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index de8c3dc4cbd1..bd008cbd5039 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -301,6 +301,7 @@ export const MessagesInput = Schema.Struct({ }) export type ListInput = { directory?: string + projectID?: ProjectV2.ID scope?: "project" path?: string workspaceID?: WorkspaceV2.ID @@ -546,11 +547,11 @@ const layer: Layer.Layer< }) const list = Effect.fn("Session.list")(function* (input?: ListInput) { - const ctx = yield* InstanceState.context + const projectID = input?.projectID ?? (yield* InstanceState.context).project.id return yield* listByProject(db, { - projectID: ctx.project.id, experimentalWorkspaces: flags.experimentalWorkspaces, ...input, + projectID, }) }) diff --git a/packages/opencode/test/cli/session-list-bootstrap.test.ts b/packages/opencode/test/cli/session-list-bootstrap.test.ts new file mode 100644 index 000000000000..16f070a31ae3 --- /dev/null +++ b/packages/opencode/test/cli/session-list-bootstrap.test.ts @@ -0,0 +1,60 @@ +import { afterEach, expect } from "bun:test" +import { existsSync } from "node:fs" +import path from "node:path" +import { pathToFileURL } from "node:url" +import { Effect } from "effect" +import { disposeAllInstances } from "../fixture/fixture" +import { cliIt } from "../lib/cli-process" + +afterEach(async () => { + await disposeAllInstances() +}) + +cliIt.live( + "session list does not run InstanceBootstrap plugins", + ({ opencode, home }) => + Effect.gen(function* () { + const marker = path.join(home, "bootstrap-marker") + const plugin = path.join(home, "bootstrap-plugin.ts") + yield* Effect.promise(() => + Bun.write( + plugin, + [ + `const MARKER = ${JSON.stringify(marker)}`, + "export default async () => ({", + " config: async () => {", + ' await Bun.write(MARKER, "ran")', + " },", + "})", + "", + ].join("\n"), + ), + ) + + const config = { + $schema: "https://opencode.ai/config.json", + plugin: [pathToFileURL(plugin).href], + } + + const listed = yield* opencode.spawn(["session", "list"], { + env: { + OPENCODE_CONFIG_CONTENT: JSON.stringify(config), + OPENCODE_PURE: "0", + }, + timeoutMs: 15_000, + }) + opencode.expectExit(listed, 0, "session list") + expect(existsSync(marker)).toBe(false) + + const agents = yield* opencode.spawn(["agent", "list"], { + env: { + OPENCODE_CONFIG_CONTENT: JSON.stringify(config), + OPENCODE_PURE: "0", + }, + timeoutMs: 60_000, + }) + opencode.expectExit(agents, 0, "agent list") + expect(existsSync(marker)).toBe(true) + }), + 90_000, +) diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index 354a578b23c9..c07724fe091c 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -1,11 +1,12 @@ import { afterEach, describe, expect } from "bun:test" import { Effect } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Database } from "@opencode-ai/core/database/database" import { SessionProjector } from "@opencode-ai/core/session/projector" import { Session as SessionNs } from "@/session/session" -import { disposeAllInstances, provideInstance, TestInstance } from "../fixture/fixture" +import { disposeAllInstances, provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture" import { mkdir } from "fs/promises" import path from "path" import { SessionTable } from "@opencode-ai/core/session/sql" @@ -14,9 +15,10 @@ import { testEffect } from "../lib/effect" import { RuntimeFlags } from "@/effect/runtime-flags" const layer = (experimentalWorkspaces: boolean) => - AppNodeBuilder.build(LayerNode.group([Database.node, SessionNs.node, SessionProjector.node]), [ - [RuntimeFlags.node, RuntimeFlags.layer({ experimentalWorkspaces })], - ]) + AppNodeBuilder.build( + LayerNode.group([CrossSpawnSpawner.node, Database.node, SessionNs.node, SessionProjector.node]), + [[RuntimeFlags.node, RuntimeFlags.layer({ experimentalWorkspaces })]], + ) const it = testEffect(layer(false)) const itWorkspaces = testEffect(layer(true)) @@ -243,6 +245,27 @@ describe("session.list", () => { { git: true }, ) + it.instance( + "filters by projectID when projectID is provided", + () => + Effect.gen(function* () { + const second = yield* tmpdirScoped({ git: true }) + + const first = yield* withSession({ title: "first-project-root" }) + const firstChild = yield* withSession({ title: "first-project-child", parentID: first.id }) + const other = yield* withSession({ title: "other-project-root" }).pipe(provideInstance(second)) + + const ids = (yield* SessionNs.Service.use((session) => + session.list({ projectID: first.projectID, roots: true }), + )).map((session) => session.id) + + expect(ids).toContain(first.id) + expect(ids).not.toContain(firstChild.id) + expect(ids).not.toContain(other.id) + }), + { git: true }, + ) + it.instance( "filters by start time", () =>