diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 8d40da4aa8b2..56f6afde3363 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -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, diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 94c92fd0b051..51255f2ec713 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -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")), diff --git a/packages/core/test/plugin/session-list.test.ts b/packages/core/test/plugin/session-list.test.ts new file mode 100644 index 000000000000..cbb5ce1d45ef --- /dev/null +++ b/packages/core/test/plugin/session-list.test.ts @@ -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) + }), +) diff --git a/packages/plugin/src/effect/session.ts b/packages/plugin/src/effect/session.ts index b9f19a6b7249..80fda0ef2765 100644 --- a/packages/plugin/src/effect/session.ts +++ b/packages/plugin/src/effect/session.ts @@ -152,6 +152,7 @@ export interface SessionHooks { export type SessionDomain = Pick< SessionApi, + | "list" | "create" | "get" | "remove" diff --git a/packages/plugin/src/promise/adapter.ts b/packages/plugin/src/promise/adapter.ts index aacf38893675..f7088b4cfdc7 100644 --- a/packages/plugin/src/promise/adapter.ts +++ b/packages/plugin/src/promise/adapter.ts @@ -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), diff --git a/packages/plugin/src/promise/session.ts b/packages/plugin/src/promise/session.ts index 9794147461d6..89c31fecf73c 100644 --- a/packages/plugin/src/promise/session.ts +++ b/packages/plugin/src/promise/session.ts @@ -152,6 +152,7 @@ export interface SessionHooks { export type SessionDomain = Pick< SessionApi, + | "list" | "create" | "get" | "remove" diff --git a/services/www/src/docs/content/build/plugins/effect.mdx b/services/www/src/docs/content/build/plugins/effect.mdx index b8fd93823c0d..1b4654b61ea1 100644 --- a/services/www/src/docs/content/build/plugins/effect.mdx +++ b/services/www/src/docs/content/build/plugins/effect.mdx @@ -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, + | "list" | "create" | "get" | "remove" diff --git a/services/www/src/docs/content/build/plugins/index.mdx b/services/www/src/docs/content/build/plugins/index.mdx index 80705a86f947..cb22b0a0f308 100644 --- a/services/www/src/docs/content/build/plugins/index.mdx +++ b/services/www/src/docs/content/build/plugins/index.mdx @@ -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