Преглед на файлове

feat(plugin): expose session list and messages with cursor pagination

Kit Langton преди 2 седмици
родител
ревизия
3cac6d60ed

+ 36 - 0
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,

+ 2 - 0
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)),

+ 2 - 0
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")),

+ 4 - 2
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<unknown>,
-  "create" | "get" | "prompt" | "generate" | "command" | "synthetic" | "interrupt" | "rename" | "wait"
+  "create" | "get" | "list" | "prompt" | "generate" | "command" | "synthetic" | "interrupt" | "rename" | "wait"
 > & {
   readonly hook: Hooks<SessionHooks>
+  /** Read a session's projected message history, paginated like the HTTP message list endpoint. */
+  readonly messages: MessageApi<unknown>["list"]
 }

+ 1 - 1
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`                                                                                       |