From b2d9ef8328db60beda5c8603a8d24f2596509e3d Mon Sep 17 00:00:00 2001 From: Arman Charan Date: Fri, 17 Jul 2026 20:32:39 +1000 Subject: [PATCH 1/5] fix: don't boot a full instance for session list Co-authored-by: Cursor --- packages/opencode/src/cli/cmd/session.ts | 7 +- packages/opencode/src/session/session.ts | 5 +- .../test/cli/session-list-bootstrap.test.ts | 77 +++++++++++++++++++ 3 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 packages/opencode/test/cli/session-list-bootstrap.test.ts diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 9e6ddda9d2d8..5e9c00aae947 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,10 @@ 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 { project } = yield* Project.Service.use((svc) => svc.fromDirectory(process.cwd())) + 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..a5e3bb99dd5e --- /dev/null +++ b/packages/opencode/test/cli/session-list-bootstrap.test.ts @@ -0,0 +1,77 @@ +import { afterEach, expect } from "bun:test" +import { existsSync } from "node:fs" +import path from "node:path" +import { pathToFileURL } from "node:url" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Effect } from "effect" +import { fileURLToPath } from "url" +import { disposeAllInstances } from "../fixture/fixture" +import { testEffect } from "../lib/effect" +import { cliIt } from "../lib/cli-process" + +const it = testEffect(LayerNode.compile(FSUtil.node)) + +afterEach(async () => { + await disposeAllInstances() +}) + +it.live("session list skips InstanceBootstrap in source", () => + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const source = yield* fs.readFileString(fileURLToPath(new URL("../../src/cli/cmd/session.ts", import.meta.url))) + const list = source.slice(source.indexOf("SessionListCommand"), source.indexOf("formatSessionTable")) + expect(list).toContain("instance: false") + expect(list).toContain("fromDirectory") + expect(list).toContain("projectID") + }), +) + +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, +) From e72a552749d84ac74734bbbe41bf68cfd48270d1 Mon Sep 17 00:00:00 2001 From: Arman Charan Date: Fri, 17 Jul 2026 21:30:30 +1000 Subject: [PATCH 2/5] fix: name cwd before resolving project Co-authored-by: Cursor --- packages/opencode/src/cli/cmd/session.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 5e9c00aae947..04bd00610c3a 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -86,7 +86,8 @@ export const SessionListCommand = effectCmd({ default: "table", }), handler: Effect.fn("Cli.session.list")(function* (args) { - const { project } = yield* Project.Service.use((svc) => svc.fromDirectory(process.cwd())) + 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 }), ) From addff1c5cf4fe91d6eb80afe6a03a483d13d50e2 Mon Sep 17 00:00:00 2001 From: Arman Charan Date: Sat, 18 Jul 2026 08:28:16 +1000 Subject: [PATCH 3/5] test: prove session list filters by projectID Co-authored-by: Cursor --- .../test/cli/session-list-bootstrap.test.ts | 17 -------------- .../opencode/test/server/session-list.test.ts | 23 ++++++++++++++++++- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/opencode/test/cli/session-list-bootstrap.test.ts b/packages/opencode/test/cli/session-list-bootstrap.test.ts index a5e3bb99dd5e..16f070a31ae3 100644 --- a/packages/opencode/test/cli/session-list-bootstrap.test.ts +++ b/packages/opencode/test/cli/session-list-bootstrap.test.ts @@ -2,31 +2,14 @@ import { afterEach, expect } from "bun:test" import { existsSync } from "node:fs" import path from "node:path" import { pathToFileURL } from "node:url" -import { LayerNode } from "@opencode-ai/core/effect/layer-node" -import { FSUtil } from "@opencode-ai/core/fs-util" import { Effect } from "effect" -import { fileURLToPath } from "url" import { disposeAllInstances } from "../fixture/fixture" -import { testEffect } from "../lib/effect" import { cliIt } from "../lib/cli-process" -const it = testEffect(LayerNode.compile(FSUtil.node)) - afterEach(async () => { await disposeAllInstances() }) -it.live("session list skips InstanceBootstrap in source", () => - Effect.gen(function* () { - const fs = yield* FSUtil.Service - const source = yield* fs.readFileString(fileURLToPath(new URL("../../src/cli/cmd/session.ts", import.meta.url))) - const list = source.slice(source.indexOf("SessionListCommand"), source.indexOf("formatSessionTable")) - expect(list).toContain("instance: false") - expect(list).toContain("fromDirectory") - expect(list).toContain("projectID") - }), -) - cliIt.live( "session list does not run InstanceBootstrap plugins", ({ opencode, home }) => diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index 354a578b23c9..4c35d952eabb 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -5,7 +5,7 @@ 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" @@ -243,6 +243,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", () => From d118b2d3088f29280098ffa023ca0da81920f469 Mon Sep 17 00:00:00 2001 From: Arman Charan Date: Sat, 18 Jul 2026 08:28:47 +1000 Subject: [PATCH 4/5] test: provide CrossSpawnSpawner for projectID list fixture Co-authored-by: Cursor --- packages/opencode/test/server/session-list.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index 4c35d952eabb..7a38770ee627 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -1,6 +1,7 @@ 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" @@ -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([Database.node, SessionNs.node, SessionProjector.node, CrossSpawnSpawner.node]), + [[RuntimeFlags.node, RuntimeFlags.layer({ experimentalWorkspaces })]], + ) const it = testEffect(layer(false)) const itWorkspaces = testEffect(layer(true)) From aa08a8b82c6c5273cc9f3c5b92a3f248420b49be Mon Sep 17 00:00:00 2001 From: Arman Charan Date: Sat, 18 Jul 2026 10:53:43 +1000 Subject: [PATCH 5/5] test: alphabetize session list layer group Co-authored-by: Cursor --- packages/opencode/test/server/session-list.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index 7a38770ee627..c07724fe091c 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -16,7 +16,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags" const layer = (experimentalWorkspaces: boolean) => AppNodeBuilder.build( - LayerNode.group([Database.node, SessionNs.node, SessionProjector.node, CrossSpawnSpawner.node]), + LayerNode.group([CrossSpawnSpawner.node, Database.node, SessionNs.node, SessionProjector.node]), [[RuntimeFlags.node, RuntimeFlags.layer({ experimentalWorkspaces })]], ) const it = testEffect(layer(false))