feat(plugin): expose session list (#56264)

This commit is contained in:
Aiden Cline 2026-10-17 00:07:26 -05:00 • committed by GitHub
parent 4a5302817b
commit bc5fa1ccf3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 79 additions and 0 deletions

View file

@ -546,6 +546,7 @@ export const make = Effect.fn("PluginHost.make")(function* (
},
session: {
hook: (name, callback, options) => hooks.register("session", name, callback, options),
list: sessions.list,
create: (input) =>
sessions.create({
id: input?.id,

View file

@ -169,6 +169,7 @@ export function host(overrides: Overrides = {}): Plugin.Context {
},
session: {
hook: overrides.session?.hook ?? (() => Effect.die("unused session.hook")),
list: overrides.session?.list ?? (() => Effect.die("unused session.list")),
create: overrides.session?.create ?? (() => Effect.die("unused session.create")),
get: overrides.session?.get ?? (() => Effect.die("unused session.get")),
remove: overrides.session?.remove ?? (() => Effect.die("unused session.remove")),

View file

@ -0,0 +1,52 @@
import { expect } from "bun:test"
import { Plugin } from "@opencode/core/plugin"
import { PluginHost } from "@opencode/core/plugin/host"
import { PluginPromise } from "@opencode/core/plugin/promise"
import { Session } from "@opencode/core/session"
import { Effect } from "effect"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
it.live("Effect plugins list sessions with the same pages as Core", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const sessions = yield* Session.Service
const context = yield* PluginHost.make(plugins)
const parent = yield* context.session.create({ title: "match parent" })
yield* sessions.create({ parentID: parent.id, title: "match child" })
yield* context.session.create({ title: "match root" })
yield* context.session.create({ title: "different" })
const filter = { directory: context.location.directory, parentID: null, search: "match", limit: 1 }
const first = yield* context.session.list(filter)
expect(first).toEqual(yield* sessions.list(filter))
expect(yield* context.session.list({ cursor: first.cursor.next })).toEqual(
yield* sessions.list({ cursor: first.cursor.next }),
)
}),
)
it.live("Promise plugins list sessions and follow cursors", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const sessions = yield* Session.Service
const context = yield* PluginHost.make(plugins)
const parent = yield* context.session.create({ title: "match parent" })
yield* sessions.create({ parentID: parent.id, title: "match child" })
yield* context.session.create({ title: "match root" })
const roots = (yield* sessions.list({ parentID: null, search: "match" })).data.map((session) => session.id)
yield* PluginPromise.fromPromise({
id: "test.session-list",
async setup(ctx) {
const first = await ctx.session.list({ parentID: null, search: "match", limit: 1 })
expect(first.cursor.next).toBeDefined()
const second = await ctx.session.list({ cursor: first.cursor.next ?? undefined, limit: 1 })
expect([...first.data, ...second.data].map((session) => session.id)).toEqual(roots)
await expect(ctx.session.list({ cursor: "invalid" })).rejects.toThrow()
},
}).effect(context)
}),
)

View file

@ -152,6 +152,7 @@ export interface SessionHooks {
export type SessionDomain = Pick<
SessionApi<unknown>,
| "list"
| "create"
| "get"
| "remove"

View file

@ -593,6 +593,7 @@ export function fromPromise(plugin: Plugin) {
register(
host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))), options),
),
list: adaptApiMethod(SessionEndpoints["session.list"], host.session.list),
create: adaptApiMethod(SessionEndpoints["session.create"], host.session.create),
get: adaptApiMethod(SessionEndpoints["session.get"], host.session.get),
remove: adaptApiMethod(SessionEndpoints["session.remove"], host.session.remove),

View file

@ -152,6 +152,7 @@ export interface SessionHooks {
export type SessionDomain = Pick<
SessionApi,
| "list"
| "create"
| "get"
| "remove"

View file

@ -824,6 +824,19 @@ effect: (ctx) =>
}),
```
List sessions a page at a time. To get the next page, pass `cursor.next` back as `cursor`. The cursor remembers your
filters, so you don't need to repeat them.
```ts
effect: (ctx) =>
Effect.gen(function* () {
const page = yield* ctx.session.list({ directory: ctx.location.directory, limit: 20 }).pipe(Effect.orDie)
const next = page.cursor.next
? yield* ctx.session.list({ cursor: page.cursor.next, limit: 20 }).pipe(Effect.orDie)
: undefined
}),
```
Remove a temporary session with `remove`. Removal stops active execution and recursively deletes child sessions.
```ts
@ -852,6 +865,7 @@ Schemas: [`Session.Info`](/api#schema-Session.Info), [`Model.Ref`](/api#schema-M
```ts
type SessionDomain = Pick<
SessionApi<unknown>,
| "list"
| "create"
| "get"
| "remove"

View file

@ -792,6 +792,14 @@ const session = await ctx.session.get({ sessionID })
const messages = await ctx.session.context({ sessionID })
```
List sessions a page at a time. To get the next page, pass `cursor.next` back as `cursor`. The cursor remembers your
filters, so you don't need to repeat them.
```ts
const page = await ctx.session.list({ directory: ctx.location.directory, limit: 20 })
const next = page.cursor.next ? await ctx.session.list({ cursor: page.cursor.next, limit: 20 }) : undefined
```
Change the agent or model used by subsequent requests.
```ts