Compare commits

..
15 changed files with 9 additions and 168 deletions
@@ -485,7 +485,6 @@ export function stepStarted(message: SessionMessageAssistant) {
assistantMessageID: message.id,
agent: message.agent,
model: message.model,
started: 1700000002000,
})
}
-1
View File
@@ -11,7 +11,6 @@ export type EventApi = Client["event"]
export type GenerateApi = Client["generate"]
export type IntegrationApi = Client["integration"]
export type McpApi = Client["mcp"]
export type MessageApi = Client["message"]
export type ModelApi = Client["model"]
export type PluginApi = Client["plugin"]
export type PermissionApi = Client["permission"]
+1 -6
View File
@@ -223,17 +223,12 @@ describe("OpenAPI.fromSpec", () => {
const spec = await opencodeSpec()
const result = OpenAPI.fromSpec({ spec, baseUrl })
expect(result.skipped).toHaveLength(6)
expect(result.skipped).toHaveLength(5)
expect(result.skipped).toContainEqual({
method: "GET",
path: "/api/pty/{ptyID}/connect",
reason: "WebSocket operations are not supported",
})
expect(result.skipped).toContainEqual({
method: "POST",
path: "/api/experimental/fs/write",
reason: "request body has no JSON content (declared: application/octet-stream)",
})
expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(2)
expect(result.skipped).toContainEqual({
method: "GET",
-59
View File
@@ -19,7 +19,6 @@ import { LocationServiceMap } from "../location-service-map.js"
import { Model } from "../model.js"
import { Mcp } from "../mcp/index.js"
import { Session } from "../session.js"
import { SessionMessage } from "../session/message.js"
import { PersistentPty } from "../persistent-pty.js"
import { Provider } from "../provider.js"
import { Reference } from "../reference.js"
@@ -393,30 +392,6 @@ export const make = Effect.fn("PluginHost.make")(function* (
})
}),
},
message: {
list: Effect.fn("PluginHost.messages")(function* (input) {
if (input.cursor !== undefined && input.order !== undefined)
return yield* Effect.fail(new Error("Cursor cannot be combined with order"))
const decoded = input.cursor === undefined ? undefined : yield* decodeMessageCursor(input.cursor)
const order = decoded?.order ?? input.order ?? "desc"
const messages = yield* sessions.messages({
sessionID: input.sessionID,
limit: input.limit ?? DefaultMessagesLimit,
order,
type: input.type,
cursor: decoded ? { id: decoded.id, direction: decoded.direction } : undefined,
})
const first = messages[0]
const last = messages.at(-1)
return {
data: messages,
cursor: {
previous: first ? encodeMessageCursor(first, order, "previous") : undefined,
next: last ? encodeMessageCursor(last, order, "next") : undefined,
},
}
}),
},
permission: {
hook: (name, callback) => hooks.register("permission", name, callback),
list: (input) => permission.forSession(input.sessionID),
@@ -572,14 +547,6 @@ export const make = Effect.fn("PluginHost.make")(function* (
.pipe(Effect.map((interrupted) => ({ interrupted }))),
wait: (input) => sessions.wait(input.sessionID),
context: (input) => sessions.context(input.sessionID),
message: {
get: Effect.fn(function* (input) {
yield* sessions.get(input.sessionID)
const message = yield* sessions.message(input)
if (!message) return yield* Effect.fail(new Error(`Message not found: ${input.messageID}`))
return message
}),
},
},
}
return context
@@ -688,29 +655,3 @@ function methodImplementation(input: IntegrationMethodRegistration): Integration
function credential(value: Credential.OAuth) {
return Credential.OAuth.make({ ...value, methodID: Integration.MethodID.make(value.methodID) })
}
const DefaultMessagesLimit = 50
const MessageCursor = Schema.Struct({
id: SessionMessage.ID,
order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]),
direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]),
})
function encodeMessageCursor(
message: SessionMessage.Info,
order: "asc" | "desc",
direction: "previous" | "next",
) {
return Buffer.from(JSON.stringify({ id: message.id, order, direction })).toString("base64url")
}
const decodeMessageCursor = Effect.fn("PluginHost.decodeMessageCursor")(function* (cursor: string) {
const parsed = yield* Effect.try({
try: () => JSON.parse(Buffer.from(cursor, "base64url").toString("utf8")),
catch: () => new Error("Invalid cursor"),
})
return yield* Schema.decodeUnknownEffect(MessageCursor)(parsed).pipe(
Effect.mapError(() => new Error("Invalid cursor")),
)
})
-75
View File
@@ -1,75 +0,0 @@
import { expect } from "bun:test"
import { Bus } from "@opencode/core/bus"
import { Plugin } from "@opencode/core/plugin"
import { PluginHost } from "@opencode/core/plugin/host"
import { SessionEvent } from "@opencode/core/session/event"
import { fromPromise } from "@opencode/plugin/promise/adapter"
import { Effect } from "effect"
import { testEffect } from "./lib/effect"
import { PluginTestLayer } from "./plugin/fixture"
const it = testEffect(PluginTestLayer)
it.effect("exposes session messages through the plugin host", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const bus = yield* Bus.Service
const host = yield* PluginHost.make(plugins)
const session = yield* host.session.create({})
const empty = yield* host.message.list({ sessionID: session.id })
expect(empty.data).toEqual([])
yield* bus.publish(SessionEvent.Synthetic, { sessionID: session.id, text: "hello" })
const listed = yield* host.message.list({ sessionID: session.id })
expect(listed.data.length).toBe(1)
const first = listed.data[0]
if (!first || first.type !== "synthetic") return yield* Effect.die("expected synthetic message")
expect(first.text).toBe("hello")
expect(listed.cursor.previous).toBeDefined()
expect(listed.cursor.next).toBeDefined()
const fetched = yield* host.session.message.get({ sessionID: session.id, messageID: first.id })
expect(fetched.id).toBe(first.id)
const missing = yield* host.session.message
.get({ sessionID: session.id, messageID: first.id.replace(/^msg_/, "msg_missing_") as typeof first.id })
.pipe(Effect.flip)
expect(String(missing)).toContain("Message not found")
const invalid = yield* host.message.list({ sessionID: session.id, cursor: "invalid" }).pipe(Effect.flip)
expect(String(invalid)).toContain("Invalid cursor")
const combined = yield* host.message
.list({ sessionID: session.id, cursor: listed.cursor.next, order: "asc" })
.pipe(Effect.flip)
expect(String(combined)).toContain("Cursor cannot be combined")
}),
)
it.effect("exposes session messages to promise plugins", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const bus = yield* Bus.Service
const host = yield* PluginHost.make(plugins)
const session = yield* host.session.create({})
yield* bus.publish(SessionEvent.Synthetic, { sessionID: session.id, text: "hello promise" })
const seen: string[] = []
const definition = fromPromise({
id: "message-list",
async setup(ctx) {
const listed = await ctx.message.list({ sessionID: session.id })
const first = listed.data[0]
if (first?.type === "synthetic") seen.push(first.text)
if (first) {
const fetched = await ctx.session.message.get({ sessionID: session.id, messageID: first.id })
if (fetched.type === "synthetic") seen.push(fetched.text)
}
},
})
yield* definition.effect(host)
expect(seen).toEqual(["hello promise", "hello promise"])
}),
)
-6
View File
@@ -102,9 +102,6 @@ export function host(overrides: Overrides = {}): Plugin.Context {
transform: () => Effect.die("unused mcp.transform"),
reload: () => Effect.die("unused mcp.reload"),
},
message: overrides.message ?? {
list: () => Effect.die("unused message.list"),
},
permission: overrides.permission ?? {
hook: () => Effect.die("unused permission.hook"),
list: () => Effect.die("unused permission.list"),
@@ -178,9 +175,6 @@ export function host(overrides: Overrides = {}): Plugin.Context {
interrupt: overrides.session?.interrupt ?? (() => Effect.die("unused session.interrupt")),
wait: overrides.session?.wait ?? (() => Effect.die("unused session.wait")),
context: overrides.session?.context ?? (() => Effect.die("unused session.context")),
message: overrides.session?.message ?? {
get: () => Effect.die("unused session.message.get"),
},
},
}
}
-3
View File
@@ -1,3 +0,0 @@
import type { MessageApi } from "@opencode/client/effect/api"
export interface MessageDomain extends MessageApi<unknown> {}
-2
View File
@@ -9,7 +9,6 @@ import type { CommandDomain } from "./command.js"
import type { EventDomain } from "./event.js"
import type { IntegrationDomain } from "./integration.js"
import type { MCPDomain } from "./mcp.js"
import type { MessageDomain } from "./message.js"
import type { ModelDomain } from "./model.js"
import type { PermissionDomain } from "./permission.js"
import type { ProviderDomain } from "./provider.js"
@@ -37,7 +36,6 @@ export interface Context {
}
readonly integration: IntegrationDomain
readonly mcp: MCPDomain
readonly message: MessageDomain
readonly model: ModelDomain
readonly generate: GenerateApi<unknown>
readonly permission: PermissionDomain
-1
View File
@@ -165,7 +165,6 @@ export type SessionDomain = Pick<
| "move"
| "wait"
| "context"
| "message"
> & {
readonly hook: ModelHooks<SessionHooks>
}
-7
View File
@@ -227,7 +227,6 @@ export function fromPromise(plugin: Plugin) {
const GenerateEndpoints = ClientApi.groups["server.generate"].endpoints
const IntegrationEndpoints = ClientApi.groups["server.integration"].endpoints
const McpEndpoints = ClientApi.groups["server.mcp"].endpoints
const MessageEndpoints = ClientApi.groups["server.message"].endpoints
const ModelEndpoints = ClientApi.groups["server.model"].endpoints
const PluginEndpoints = ClientApi.groups["server.plugin"].endpoints
const PermissionEndpoints = ClientApi.groups["server.permission"].endpoints
@@ -434,9 +433,6 @@ export function fromPromise(plugin: Plugin) {
transform: transform(host.mcp),
reload: () => run(host.mcp.reload()),
},
message: {
list: adaptApiMethod(MessageEndpoints["session.messages"], host.message.list),
},
permission: {
hook: (name, callback) =>
register(host.permission.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
@@ -590,9 +586,6 @@ export function fromPromise(plugin: Plugin) {
move: adaptApiMethod(SessionEndpoints["session.move"], host.session.move),
wait: adaptApiMethod(SessionEndpoints["session.wait"], host.session.wait),
context: adaptApiMethod(SessionEndpoints["session.context"], host.session.context),
message: {
get: adaptApiMethod(SessionEndpoints["session.message"], host.session.message.get),
},
},
shell: {
hook: (name, callback) =>
-3
View File
@@ -1,3 +0,0 @@
import type { MessageApi } from "@opencode/client/promise/api"
export interface MessageDomain extends MessageApi {}
-2
View File
@@ -9,7 +9,6 @@ import type { CommandDomain } from "./command.js"
import type { EventDomain } from "./event.js"
import type { IntegrationDomain } from "./integration.js"
import type { MCPDomain } from "./mcp.js"
import type { MessageDomain } from "./message.js"
import type { ModelDomain } from "./model.js"
import type { PermissionDomain } from "./permission.js"
import type { ProviderDomain } from "./provider.js"
@@ -37,7 +36,6 @@ export interface Context {
}
readonly integration: IntegrationDomain
readonly mcp: MCPDomain
readonly message: MessageDomain
readonly model: ModelDomain
readonly generate: GenerateApi
readonly permission: PermissionDomain
-1
View File
@@ -165,7 +165,6 @@ export type SessionDomain = Pick<
| "move"
| "wait"
| "context"
| "message"
> & {
readonly hook: ModelHooks<SessionHooks>
}
+2 -1
View File
@@ -968,7 +968,8 @@ function App(props: { pair?: DialogPairCredentials }) {
{
name: "opencode.update",
title: "Update OpenCode",
slash: { name: "update", aliases: ["upgrade"] },
description: "Update OpenCode (upgrade)",
slash: { name: "update" },
run: () => updater.open?.("manual"),
category: "System",
},
+6
View File
@@ -10,6 +10,12 @@ const dir = fileURLToPath(new URL("..", import.meta.url))
process.chdir(dir)
const tag = `v${Script.version}`
if (Script.channel === "beta" && Script.release) {
console.log("\n=== desktop beta release ===\n")
await $`bun ./packages/desktop/scripts/publish.ts`
process.exit(0)
}
const pkgjsons = await Array.fromAsync(
new Bun.Glob("**/package.json").scan({
absolute: true,