refactor(session): pass project to list (#25215)

This commit is contained in:
Kit Langton
2026-04-30 23:00:59 -04:00
committed by GitHub
parent 3c24d22d42
commit 8b56d1712f
6 changed files with 79 additions and 64 deletions
+1 -4
View File
@@ -245,10 +245,7 @@ export const ExportCommand = cmd({
output: process.stderr, output: process.stderr,
}) })
const sessions = [] const sessions = await AppRuntime.runPromise(Session.Service.use((svc) => svc.list()))
for await (const session of Session.list()) {
sessions.push(session)
}
if (sessions.length === 0) { if (sessions.length === 0) {
prompts.log.error("No sessions found", { prompts.log.error("No sessions found", {
+3 -1
View File
@@ -91,7 +91,9 @@ export const SessionListCommand = cmd({
}, },
handler: async (args) => { handler: async (args) => {
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
const sessions = [...Session.list({ roots: true, limit: args.maxCount })] const sessions = await AppRuntime.runPromise(
Session.Service.use((svc) => svc.list({ roots: true, limit: args.maxCount })),
)
if (sessions.length === 0) { if (sessions.length === 0) {
return return
@@ -5,7 +5,6 @@ import { Bus } from "@/bus"
import { Command } from "@/command" import { Command } from "@/command"
import { Permission } from "@/permission" import { Permission } from "@/permission"
import { PermissionID } from "@/permission/schema" import { PermissionID } from "@/permission/schema"
import { Instance } from "@/project/instance"
import { SessionShare } from "@/share/session" import { SessionShare } from "@/share/session"
import { Session } from "@/session/session" import { Session } from "@/session/session"
import { SessionCompaction } from "@/session/compaction" import { SessionCompaction } from "@/session/compaction"
@@ -64,20 +63,15 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
const list = Effect.fn("SessionHttpApi.list")(function* (ctx: { query: typeof ListQuery.Type }) { const list = Effect.fn("SessionHttpApi.list")(function* (ctx: { query: typeof ListQuery.Type }) {
const instance = yield* InstanceState.context return yield* session.list({
return Instance.restore(instance, () => directory: ctx.query.scope === "project" ? undefined : ctx.query.directory,
Array.from( scope: ctx.query.scope,
Session.list({ path: ctx.query.path,
directory: ctx.query.scope === "project" ? undefined : ctx.query.directory, roots: ctx.query.roots,
scope: ctx.query.scope, start: ctx.query.start,
path: ctx.query.path, search: ctx.query.search,
roots: ctx.query.roots, limit: ctx.query.limit,
start: ctx.query.start, })
search: ctx.query.search,
limit: ctx.query.limit,
}),
),
)
}) })
const status = Effect.fn("SessionHttpApi.status")(function* () { const status = Effect.fn("SessionHttpApi.status")(function* () {
@@ -78,18 +78,22 @@ export const SessionRoutes = lazy(() =>
), ),
async (c) => { async (c) => {
const query = c.req.valid("query") const query = c.req.valid("query")
const sessions: Session.Info[] = [] return c.json(
for await (const session of Session.list({ await runRequest(
directory: query.scope === "project" ? undefined : query.directory, "SessionRoutes.list",
path: query.path, c,
roots: queryBoolean(query.roots), Session.Service.use((svc) =>
start: query.start, svc.list({
search: query.search, directory: query.scope === "project" ? undefined : query.directory,
limit: query.limit, path: query.path,
})) { roots: queryBoolean(query.roots),
sessions.push(session) start: query.start,
} search: query.search,
return c.json(sessions) limit: query.limit,
}),
),
),
)
}, },
) )
.get( .get(
+29 -20
View File
@@ -26,7 +26,7 @@ import { ProjectTable } from "../project/project.sql"
import { Storage } from "@/storage/storage" import { Storage } from "@/storage/storage"
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import { MessageV2 } from "./message-v2" import { MessageV2 } from "./message-v2"
import { Instance, type InstanceContext } from "../project/instance" import type { InstanceContext } from "../project/instance"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { ProjectID } from "../project/schema" import { ProjectID } from "../project/schema"
@@ -234,6 +234,16 @@ export const MessagesInput = Schema.Struct({
sessionID: SessionID, sessionID: SessionID,
limit: Schema.optional(NonNegativeInt), limit: Schema.optional(NonNegativeInt),
}).pipe(withStatics((s) => ({ zod: zod(s) }))) }).pipe(withStatics((s) => ({ zod: zod(s) })))
export type ListInput = {
directory?: string
scope?: "project"
path?: string
workspaceID?: WorkspaceID
roots?: boolean
start?: number
search?: string
limit?: number
}
const CreatedEventSchema = Schema.Struct({ const CreatedEventSchema = Schema.Struct({
sessionID: SessionID, sessionID: SessionID,
@@ -390,6 +400,7 @@ export class BusyError extends Error {
} }
export interface Interface { export interface Interface {
readonly list: (input?: ListInput) => Effect.Effect<Info[]>
readonly create: (input?: { readonly create: (input?: {
parentID?: SessionID parentID?: SessionID
title?: string title?: string
@@ -498,6 +509,11 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service |
return fromRow(row) return fromRow(row)
}) })
const list = Effect.fn("Session.list")(function* (input?: ListInput) {
const ctx = yield* InstanceState.context
return Array.from(listByProject({ projectID: ctx.project.id, ...(input ?? {}) }))
})
const children = Effect.fn("Session.children")(function* (parentID: SessionID) { const children = Effect.fn("Session.children")(function* (parentID: SessionID) {
const rows = yield* db((d) => const rows = yield* db((d) =>
d d
@@ -731,6 +747,7 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service |
}) })
return Service.of({ return Service.of({
list,
create, create,
fork, fork,
touch, touch,
@@ -762,23 +779,15 @@ export const defaultLayer = layer.pipe(
Layer.provide(SyncEvent.defaultLayer), Layer.provide(SyncEvent.defaultLayer),
) )
export function* list(input?: { function* listByProject(input: ListInput & {
directory?: string projectID: ProjectID
scope?: "project"
path?: string
workspaceID?: WorkspaceID
roots?: boolean
start?: number
search?: string
limit?: number
}) { }) {
const project = Instance.project const conditions = [eq(SessionTable.project_id, input.projectID)]
const conditions = [eq(SessionTable.project_id, project.id)]
if (input?.workspaceID) { if (input.workspaceID) {
conditions.push(eq(SessionTable.workspace_id, input.workspaceID)) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
} }
if (input?.path !== undefined) { if (input.path !== undefined) {
if (input.path) { if (input.path) {
const conds = [eq(SessionTable.path, input.path), like(SessionTable.path, `${input.path}/%`)] const conds = [eq(SessionTable.path, input.path), like(SessionTable.path, `${input.path}/%`)]
@@ -788,22 +797,22 @@ export function* list(input?: {
: or(...conds)!, : or(...conds)!,
) )
} }
} else if (input?.scope !== "project" && !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { } else if (input.scope !== "project" && !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
if (input?.directory) { if (input.directory) {
conditions.push(eq(SessionTable.directory, input.directory)) conditions.push(eq(SessionTable.directory, input.directory))
} }
} }
if (input?.roots) { if (input.roots) {
conditions.push(isNull(SessionTable.parent_id)) conditions.push(isNull(SessionTable.parent_id))
} }
if (input?.start) { if (input.start) {
conditions.push(gte(SessionTable.time_updated, input.start)) conditions.push(gte(SessionTable.time_updated, input.start))
} }
if (input?.search) { if (input.search) {
conditions.push(like(SessionTable.title, `%${input.search}%`)) conditions.push(like(SessionTable.title, `%${input.search}%`))
} }
const limit = input?.limit ?? 100 const limit = input.limit ?? 100
const rows = Database.use((db) => const rows = Database.use((db) =>
db db
@@ -23,6 +23,9 @@ const svc = {
create(input?: SessionNs.CreateInput) { create(input?: SessionNs.CreateInput) {
return run(SessionNs.Service.use((svc) => svc.create(input))) return run(SessionNs.Service.use((svc) => svc.create(input)))
}, },
list(input?: SessionNs.ListInput) {
return run(SessionNs.Service.use((svc) => svc.list(input)))
},
} }
afterEach(async () => { afterEach(async () => {
@@ -55,7 +58,7 @@ describe("session.list", () => {
fn: async () => svc.create({ title: "sibling" }), fn: async () => svc.create({ title: "sibling" }),
}) })
const ids = [...svc.list()].map((s) => s.id) const ids = (await svc.list()).map((s) => s.id)
expect(ids).toContain(root.id) expect(ids).toContain(root.id)
expect(ids).toContain(parent.id) expect(ids).toContain(parent.id)
expect(ids).toContain(current.id) expect(ids).toContain(current.id)
@@ -88,7 +91,7 @@ describe("session.list", () => {
fn: async () => svc.create({ title: "sibling" }), fn: async () => svc.create({ title: "sibling" }),
}) })
const ids = [...svc.list({ directory: path.join(tmp.path, "packages", "opencode") })].map((s) => s.id) const ids = (await svc.list({ directory: path.join(tmp.path, "packages", "opencode") })).map((s) => s.id)
expect(ids).not.toContain(root.id) expect(ids).not.toContain(root.id)
expect(ids).not.toContain(parent.id) expect(ids).not.toContain(parent.id)
expect(ids).toContain(current.id) expect(ids).toContain(current.id)
@@ -123,9 +126,12 @@ describe("session.list", () => {
fn: async () => svc.create({ title: "sibling" }), fn: async () => svc.create({ title: "sibling" }),
}) })
const pathIDs = [ const pathIDs = (
...svc.list({ directory: path.join(tmp.path, "packages", "app"), path: "packages/opencode/src" }), await svc.list({
].map((s) => s.id) directory: path.join(tmp.path, "packages", "app"),
path: "packages/opencode/src",
})
).map((s) => s.id)
expect(pathIDs).not.toContain(parent.id) expect(pathIDs).not.toContain(parent.id)
expect(pathIDs).toContain(current.id) expect(pathIDs).toContain(current.id)
expect(pathIDs).toContain(deeper.id) expect(pathIDs).toContain(deeper.id)
@@ -155,9 +161,12 @@ describe("session.list", () => {
Database.use((db) => db.update(SessionTable).set({ path: null }).where(eq(SessionTable.id, current.id)).run()) Database.use((db) => db.update(SessionTable).set({ path: null }).where(eq(SessionTable.id, current.id)).run())
Database.use((db) => db.update(SessionTable).set({ path: null }).where(eq(SessionTable.id, sibling.id)).run()) Database.use((db) => db.update(SessionTable).set({ path: null }).where(eq(SessionTable.id, sibling.id)).run())
const pathIDs = [ const pathIDs = (
...svc.list({ directory: path.join(tmp.path, "packages", "opencode", "src"), path: "packages/opencode/src" }), await svc.list({
].map((s) => s.id) directory: path.join(tmp.path, "packages", "opencode", "src"),
path: "packages/opencode/src",
})
).map((s) => s.id)
expect(pathIDs).toContain(current.id) expect(pathIDs).toContain(current.id)
expect(pathIDs).not.toContain(sibling.id) expect(pathIDs).not.toContain(sibling.id)
}, },
@@ -172,7 +181,7 @@ describe("session.list", () => {
const root = await svc.create({ title: "root-session" }) const root = await svc.create({ title: "root-session" })
const child = await svc.create({ title: "child-session", parentID: root.id }) const child = await svc.create({ title: "child-session", parentID: root.id })
const sessions = [...svc.list({ roots: true })] const sessions = await svc.list({ roots: true })
const ids = sessions.map((s) => s.id) const ids = sessions.map((s) => s.id)
expect(ids).toContain(root.id) expect(ids).toContain(root.id)
@@ -189,7 +198,7 @@ describe("session.list", () => {
await svc.create({ title: "new-session" }) await svc.create({ title: "new-session" })
const futureStart = Date.now() + 86400000 const futureStart = Date.now() + 86400000
const sessions = [...svc.list({ start: futureStart })] const sessions = await svc.list({ start: futureStart })
expect(sessions.length).toBe(0) expect(sessions.length).toBe(0)
}, },
}) })
@@ -203,7 +212,7 @@ describe("session.list", () => {
await svc.create({ title: "unique-search-term-abc" }) await svc.create({ title: "unique-search-term-abc" })
await svc.create({ title: "other-session-xyz" }) await svc.create({ title: "other-session-xyz" })
const sessions = [...svc.list({ search: "unique-search" })] const sessions = await svc.list({ search: "unique-search" })
const titles = sessions.map((s) => s.title) const titles = sessions.map((s) => s.title)
expect(titles).toContain("unique-search-term-abc") expect(titles).toContain("unique-search-term-abc")
@@ -221,7 +230,7 @@ describe("session.list", () => {
await svc.create({ title: "session-2" }) await svc.create({ title: "session-2" })
await svc.create({ title: "session-3" }) await svc.create({ title: "session-3" })
const sessions = [...svc.list({ limit: 2 })] const sessions = await svc.list({ limit: 2 })
expect(sessions.length).toBe(2) expect(sessions.length).toBe(2)
}, },
}) })