From 3cac6d60ed09876544f3a75cb5abd734d3191ec1 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 31 Jul 2026 16:50:55 -0400 Subject: [PATCH] feat(plugin): expose session list and messages with cursor pagination --- packages/core/src/plugin/host.ts | 36 +++++++++++++++++++++ packages/core/src/plugin/runtime.ts | 2 ++ packages/core/test/plugin/host.ts | 2 ++ packages/plugin/src/effect/session.ts | 6 ++-- packages/www/content/docs/build/plugins.mdx | 2 +- 5 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 9f525cab99..1eaa1b9d33 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -4,6 +4,8 @@ import { Plugin } from "@opencode-ai/plugin/effect" import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/integration" import type { CredentialOAuth } from "@opencode-ai/sdk/v2/types" import { EventManifest } from "@opencode-ai/schema/event-manifest" +import { SessionMessagesCursor } from "@opencode-ai/schema/session-messages-cursor" +import { SessionsQuery } from "@opencode-ai/schema/sessions-query" import { App } from "../app" import { Effect, Schema, Stream } from "effect" import { Agent } from "../agent" @@ -348,6 +350,40 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: import("../p input?.location ?? Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }), }), get: (input) => runtime.session.get(input.sessionID), + list: (input) => + Effect.gen(function* () { + const query = + input?.cursor !== undefined + ? yield* SessionsQuery.Cursor.parse(input.cursor).pipe( + Effect.mapError(() => new Error("Invalid cursor")), + ) + : (input ?? {}) + const page = yield* runtime.session.list({ + ...query, + workspaceID: query.workspace, + limit: input?.limit ?? SessionsQuery.DefaultLimit, + }) + return { data: page.data, cursor: SessionsQuery.Cursor.page(query, page.data) } + }), + messages: (input) => + Effect.gen(function* () { + if (input.cursor !== undefined && input.order !== undefined) + return yield* Effect.fail(new Error("Cursor cannot be combined with order")) + const decoded = + input.cursor !== undefined + ? yield* SessionMessagesCursor.parse(input.cursor).pipe( + Effect.mapError(() => new Error("Invalid cursor")), + ) + : undefined + const order = decoded?.order ?? input.order ?? "desc" + const data = yield* runtime.session.messages({ + sessionID: input.sessionID, + limit: input.limit ?? SessionMessagesCursor.DefaultLimit, + order, + cursor: decoded ? { id: decoded.id, direction: decoded.direction } : undefined, + }) + return { data, cursor: SessionMessagesCursor.page(data, order) } + }), prompt: runtime.session.prompt, generate: (input) => runtime.session.generate(input).pipe(Effect.map((text) => ({ text }))), command: runtime.session.command, diff --git a/packages/core/src/plugin/runtime.ts b/packages/core/src/plugin/runtime.ts index f5ee1eb899..8c53ae7fd8 100644 --- a/packages/core/src/plugin/runtime.ts +++ b/packages/core/src/plugin/runtime.ts @@ -13,6 +13,7 @@ export interface Interface { Session.Interface, | "get" | "create" + | "list" | "messages" | "prompt" | "generate" @@ -58,6 +59,7 @@ export const layerWithCell = (cell: Cell) => session: { get: (sessionID) => require(cell, (runtime) => runtime.session.get(sessionID)), create: (input) => require(cell, (runtime) => runtime.session.create(input)), + list: (input) => require(cell, (runtime) => runtime.session.list(input)), messages: (input) => require(cell, (runtime) => runtime.session.messages(input)), prompt: (input) => require(cell, (runtime) => runtime.session.prompt(input)), generate: (input) => require(cell, (runtime) => runtime.session.generate(input)), diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index f45364ebdf..2646080012 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -103,6 +103,8 @@ export function host(overrides: Overrides = {}): Plugin.Context { hook: overrides.session?.hook ?? (() => Effect.die("unused session.hook")), create: overrides.session?.create ?? (() => Effect.die("unused session.create")), get: overrides.session?.get ?? (() => Effect.die("unused session.get")), + list: overrides.session?.list ?? (() => Effect.die("unused session.list")), + messages: overrides.session?.messages ?? (() => Effect.die("unused session.messages")), prompt: overrides.session?.prompt ?? (() => Effect.die("unused session.prompt")), generate: overrides.session?.generate ?? (() => Effect.die("unused session.generate")), command: overrides.session?.command ?? (() => Effect.die("unused session.command")), diff --git a/packages/plugin/src/effect/session.ts b/packages/plugin/src/effect/session.ts index e4206b6df2..e386565ae5 100644 --- a/packages/plugin/src/effect/session.ts +++ b/packages/plugin/src/effect/session.ts @@ -1,4 +1,4 @@ -import type { SessionApi } from "@opencode-ai/client/effect/api" +import type { MessageApi, SessionApi } from "@opencode-ai/client/effect/api" import type { Message, SystemPart } from "@opencode-ai/ai" import type { HttpRequest } from "@opencode-ai/ai/route" import type { Agent } from "@opencode-ai/schema/agent" @@ -29,7 +29,9 @@ export interface SessionHooks { export type SessionDomain = Pick< SessionApi, - "create" | "get" | "prompt" | "generate" | "command" | "synthetic" | "interrupt" | "rename" | "wait" + "create" | "get" | "list" | "prompt" | "generate" | "command" | "synthetic" | "interrupt" | "rename" | "wait" > & { readonly hook: Hooks + /** Read a session's projected message history, paginated like the HTTP message list endpoint. */ + readonly messages: MessageApi["list"] } diff --git a/packages/www/content/docs/build/plugins.mdx b/packages/www/content/docs/build/plugins.mdx index fd9331c880..53d4287ad9 100644 --- a/packages/www/content/docs/build/plugins.mdx +++ b/packages/www/content/docs/build/plugins.mdx @@ -177,7 +177,7 @@ and plugin options. | `ctx.integration` | `list`, `get`, `connect`, `attempt`, `transform`, `reload`, and connection lookup/resolution | | `ctx.plugin` | `list` currently active plugin IDs | | `ctx.reference` | `list`, `transform`, `reload` | -| `ctx.session` | `create`, `get`, `prompt`, `command`, `rename`, `synthetic`, `interrupt`, `wait`, and `hook` | +| `ctx.session` | `create`, `get`, `list`, `messages`, `prompt`, `command`, `rename`, `synthetic`, `interrupt`, `wait`, and `hook` | | `ctx.skill` | `list`, `transform`, `reload` | | `ctx.tool` | `transform` and `hook` | | `ctx.aisdk` | `hook` |