From 98d51dde6a194a9e2399679313fcfcb848366f0d Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 25 Oct 2025 01:32:46 -0500 Subject: [PATCH 01/56] acp: slash commands, agents, permissions, @ references, code cleanup (#3403) Co-authored-by: yetone --- opencode.json | 7 +- packages/opencode/src/acp/agent.ts | 699 +++++++++++++++----- packages/opencode/src/acp/client.ts | 85 --- packages/opencode/src/acp/server.ts | 53 -- packages/opencode/src/acp/session.ts | 59 +- packages/opencode/src/acp/types.ts | 2 +- packages/opencode/src/cli/cmd/acp.ts | 48 +- packages/opencode/src/session/index.ts | 17 +- packages/opencode/src/session/message-v2.ts | 1 + packages/opencode/src/session/prompt.ts | 210 +----- 10 files changed, 654 insertions(+), 527 deletions(-) delete mode 100644 packages/opencode/src/acp/client.ts delete mode 100644 packages/opencode/src/acp/server.ts diff --git a/opencode.json b/opencode.json index 720ece5c1..a8819ebdc 100644 --- a/opencode.json +++ b/opencode.json @@ -1,3 +1,8 @@ { - "$schema": "https://opencode.ai/config.json" + "$schema": "https://opencode.ai/config.json", + "permission": { + "bash": { + "cat*": "ask" + } + } } diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 4f2f6dc46..03bc4d5dd 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -1,204 +1,587 @@ import type { - Agent, + Agent as ACPAgent, AgentSideConnection, AuthenticateRequest, - AuthenticateResponse, CancelNotification, InitializeRequest, - InitializeResponse, LoadSessionRequest, - LoadSessionResponse, NewSessionRequest, - NewSessionResponse, + PermissionOption, PromptRequest, - PromptResponse, SetSessionModelRequest, - SetSessionModelResponse, + SetSessionModeRequest, + SetSessionModeResponse, } from "@agentclientprotocol/sdk" import { Log } from "../util/log" import { ACPSessionManager } from "./session" import type { ACPConfig } from "./types" import { Provider } from "../provider/provider" import { SessionPrompt } from "../session/prompt" -import { Identifier } from "../id/id" +import { Installation } from "@/installation" +import { SessionLock } from "@/session/lock" +import { Bus } from "@/bus" +import { MessageV2 } from "@/session/message-v2" +import { Storage } from "@/storage/storage" +import { Command } from "@/command" +import { Agent as Agents } from "@/agent/agent" +import { Permission } from "@/permission" -export class OpenCodeAgent implements Agent { - private log = Log.create({ service: "acp-agent" }) - private sessionManager = new ACPSessionManager() - private connection: AgentSideConnection - private config: ACPConfig +export namespace ACP { + const log = Log.create({ service: "acp-agent" }) - constructor(connection: AgentSideConnection, config: ACPConfig = {}) { - this.connection = connection - this.config = config - } + // TODO: mcp servers? - async initialize(params: InitializeRequest): Promise { - this.log.info("initialize", { protocolVersion: params.protocolVersion }) + type ToolKind = + | "read" + | "edit" + | "delete" + | "move" + | "search" + | "execute" + | "think" + | "fetch" + | "switch_mode" + | "other" - return { - protocolVersion: 1, - agentCapabilities: { - loadSession: false, - }, - _meta: { - opencode: { - version: await import("../installation").then((m) => m.Installation.VERSION), + export class Agent implements ACPAgent { + private sessionManager = new ACPSessionManager() + private connection: AgentSideConnection + private config: ACPConfig + + constructor(connection: AgentSideConnection, config: ACPConfig = {}) { + this.connection = connection + this.config = config + this.setupEventSubscriptions() + } + + private setupEventSubscriptions() { + const options: PermissionOption[] = [ + { optionId: "once", kind: "allow_once", name: "Allow once" }, + { optionId: "always", kind: "allow_always", name: "Always allow" }, + { optionId: "reject", kind: "reject_once", name: "Reject" }, + ] + Bus.subscribe(Permission.Event.Updated, async (event) => { + const acpSession = this.sessionManager.get(event.properties.sessionID) + if (!acpSession) return + try { + const permission = event.properties + const res = await this.connection + .requestPermission({ + sessionId: acpSession.id, + toolCall: { + toolCallId: permission.callID ?? permission.id, + status: "pending", + title: permission.title, + rawInput: permission.metadata, + kind: toToolKind(permission.type), + locations: toLocations(permission.type, permission.metadata), + }, + options, + }) + .catch((error) => { + log.error("failed to request permission from ACP", { + error, + permissionID: permission.id, + sessionID: permission.sessionID, + }) + Permission.respond({ + sessionID: permission.sessionID, + permissionID: permission.id, + response: "reject", + }) + return + }) + if (!res) return + if (res.outcome.outcome !== "selected") { + Permission.respond({ sessionID: permission.sessionID, permissionID: permission.id, response: "reject" }) + return + } + Permission.respond({ + sessionID: permission.sessionID, + permissionID: permission.id, + response: res.outcome.optionId as "once" | "always" | "reject", + }) + } catch (err) { + if (!(err instanceof Permission.RejectedError)) { + log.error("unexpected error when handling permission", { error: err }) + throw err + } + } + }) + + Bus.subscribe(MessageV2.Event.PartUpdated, async (event) => { + const props = event.properties + const { part } = props + const acpSession = this.sessionManager.get(part.sessionID) + if (!acpSession) return + + const message = await Storage.read(["message", part.sessionID, part.messageID]).catch( + () => undefined, + ) + if (!message || message.role !== "assistant") return + + if (part.type === "tool") { + switch (part.state.status) { + case "pending": + await this.connection + .sessionUpdate({ + sessionId: acpSession.id, + update: { + sessionUpdate: "tool_call", + toolCallId: part.callID, + title: part.tool, + kind: toToolKind(part.tool), + status: "pending", + locations: [], + rawInput: {}, + }, + }) + .catch((err) => { + log.error("failed to send tool pending to ACP", { error: err }) + }) + break + case "running": + await this.connection + .sessionUpdate({ + sessionId: acpSession.id, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "in_progress", + locations: toLocations(part.tool, part.state.input), + rawInput: part.state.input, + }, + }) + .catch((err) => { + log.error("failed to send tool in_progress to ACP", { error: err }) + }) + break + case "completed": + await this.connection + .sessionUpdate({ + sessionId: acpSession.id, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "completed", + content: [ + { + type: "content", + content: { + type: "text", + text: part.state.output, + }, + }, + ], + title: part.state.title, + rawOutput: { + output: part.state.output, + metadata: part.state.metadata, + }, + }, + }) + .catch((err) => { + log.error("failed to send tool completed to ACP", { error: err }) + }) + break + case "error": + await this.connection + .sessionUpdate({ + sessionId: acpSession.id, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "failed", + content: [ + { + type: "content", + content: { + type: "text", + text: part.state.error, + }, + }, + ], + rawOutput: { + error: part.state.error, + }, + }, + }) + .catch((err) => { + log.error("failed to send tool error to ACP", { error: err }) + }) + break + } + } else if (part.type === "text") { + const delta = props.delta + if (delta && part.synthetic !== true) { + await this.connection + .sessionUpdate({ + sessionId: acpSession.id, + update: { + sessionUpdate: "agent_message_chunk", + content: { + type: "text", + text: delta, + }, + }, + }) + .catch((err) => { + log.error("failed to send text to ACP", { error: err }) + }) + } + } else if (part.type === "reasoning") { + const delta = props.delta + if (delta) { + await this.connection + .sessionUpdate({ + sessionId: acpSession.id, + update: { + sessionUpdate: "agent_thought_chunk", + content: { + type: "text", + text: delta, + }, + }, + }) + .catch((err) => { + log.error("failed to send reasoning to ACP", { error: err }) + }) + } + } + }) + } + + async initialize(params: InitializeRequest) { + log.info("initialize", { protocolVersion: params.protocolVersion }) + + return { + protocolVersion: 1, + agentCapabilities: { + loadSession: true, + // TODO: map acp mcp + // mcpCapabilities: { + // http: true, + // sse: true, + // }, }, - }, + authMethods: [ + { + description: "Run `opencode auth login` in the terminal", + name: "Login with opencode", + id: "opencode-login", + }, + ], + _meta: { + opencode: { + version: Installation.VERSION, + }, + }, + } + } + + async authenticate(_params: AuthenticateRequest) { + throw new Error("Authentication not implemented") + } + + async newSession(params: NewSessionRequest) { + const model = await defaultModel(this.config) + const session = await this.sessionManager.create(params.cwd, params.mcpServers, model) + + const load = await this.loadSession({ + cwd: params.cwd, + mcpServers: params.mcpServers, + sessionId: session.id, + }) + + return { + sessionId: session.id, + models: load.models, + modes: load.modes, + _meta: {}, + } + } + + async loadSession(params: LoadSessionRequest) { + const model = await defaultModel(this.config) + const sessionId = params.sessionId + + const providers = await Provider.list() + const entries = Object.entries(providers).sort((a, b) => { + const nameA = a[1].info.name.toLowerCase() + const nameB = b[1].info.name.toLowerCase() + if (nameA < nameB) return -1 + if (nameA > nameB) return 1 + return 0 + }) + const availableModels = entries.flatMap(([providerID, provider]) => { + const models = Provider.sort(Object.values(provider.info.models)) + return models.map((model) => ({ + modelId: `${providerID}/${model.id}`, + name: `${provider.info.name}/${model.name}`, + })) + }) + + const availableCommands = (await Command.list()).map((command) => ({ + name: command.name, + description: command.description ?? "", + })) + + setTimeout(() => { + this.connection.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "available_commands_update", + availableCommands, + }, + }) + }, 0) + + const availableModes = (await Agents.list()) + .filter((agent) => agent.mode !== "subagent") + .map((agent) => ({ + id: agent.name, + name: agent.name, + description: agent.description, + })) + + const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id + + return { + sessionId, + models: { + currentModelId: `${model.providerID}/${model.modelID}`, + availableModels, + }, + modes: { + availableModes, + currentModeId, + }, + _meta: {}, + } + } + + async setSessionModel(params: SetSessionModelRequest) { + const session = this.sessionManager.get(params.sessionId) + if (!session) { + throw new Error(`Session not found: ${params.sessionId}`) + } + + const parsed = Provider.parseModel(params.modelId) + const model = await Provider.getModel(parsed.providerID, parsed.modelID) + + this.sessionManager.setModel(session.id, { + providerID: model.providerID, + modelID: model.modelID, + }) + + return { + _meta: {}, + } + } + + async setSessionMode(params: SetSessionModeRequest): Promise { + const session = this.sessionManager.get(params.sessionId) + if (!session) { + throw new Error(`Session not found: ${params.sessionId}`) + } + await Agents.get(params.modeId).then((agent) => { + if (!agent) throw new Error(`Agent not found: ${params.modeId}`) + }) + this.sessionManager.setMode(params.sessionId, params.modeId) + } + + async prompt(params: PromptRequest) { + const sessionID = params.sessionId + const acpSession = this.sessionManager.get(sessionID) + if (!acpSession) { + throw new Error(`Session not found: ${sessionID}`) + } + + const current = acpSession.model + const model = current ?? (await defaultModel(this.config)) + if (!current) { + this.sessionManager.setModel(acpSession.id, model) + } + const agent = acpSession.modeId ?? "build" + + const parts: SessionPrompt.PromptInput["parts"] = [] + for (const part of params.prompt) { + switch (part.type) { + case "text": + parts.push({ + type: "text" as const, + text: part.text, + }) + break + case "image": + if (part.data) { + parts.push({ + type: "file", + url: `data:${part.mimeType};base64,${part.data}`, + mime: part.mimeType, + }) + } else if (part.uri && part.uri.startsWith("http:")) { + parts.push({ + type: "file", + url: part.uri, + mime: part.mimeType, + }) + } + break + + case "resource_link": + const parsed = parseUri(part.uri) + parts.push(parsed) + + break + + case "resource": + const resource = part.resource + if ("text" in resource) { + parts.push({ + type: "text", + text: resource.text, + }) + } + break + + default: + break + } + } + + log.info("parts", { parts }) + + const cmd = await (async () => { + const text = parts.filter((part) => part.type === "text").join("") + const match = text.match(/^\/(\w+)\s*(.*)$/) + if (!match) return + + const [c, args] = match.slice(1) + const command = await Command.get(c) + if (!command) return + return { command, args } + })() + + if (cmd) { + await SessionPrompt.command({ + sessionID, + command: cmd.command.name, + arguments: cmd.args, + agent, + }) + } else { + await SessionPrompt.prompt({ + sessionID, + model: { + providerID: model.providerID, + modelID: model.modelID, + }, + parts, + agent, + }) + } + + return { + stopReason: "end_turn" as const, + _meta: {}, + } + } + + async cancel(params: CancelNotification) { + SessionLock.abort(params.sessionId) } } - async authenticate(params: AuthenticateRequest): Promise { - this.log.info("authenticate", { methodId: params.methodId }) - throw new Error("Authentication not yet implemented") - } + function toToolKind(toolName: string): ToolKind { + const tool = toolName.toLocaleLowerCase() + switch (tool) { + case "bash": + return "execute" + case "webfetch": + return "fetch" - async newSession(params: NewSessionRequest): Promise { - this.log.info("newSession", { cwd: params.cwd, mcpServers: params.mcpServers.length }) + case "edit": + case "patch": + case "write": + return "edit" - const model = await this.defaultModel() - const session = await this.sessionManager.create(params.cwd, params.mcpServers, model) - const availableModels = await this.availableModels() + case "grep": + case "glob": + case "context7_resolve_library_id": + case "context7_get_library_docs": + return "search" - return { - sessionId: session.id, - models: { - currentModelId: `${model.providerID}/${model.modelID}`, - availableModels, - }, - _meta: {}, + case "list": + case "read": + return "read" + + default: + return "other" } } - async loadSession(params: LoadSessionRequest): Promise { - this.log.info("loadSession", { sessionId: params.sessionId, cwd: params.cwd }) - - const defaultModel = await this.defaultModel() - const session = await this.sessionManager.load(params.sessionId, params.cwd, params.mcpServers, defaultModel) - const availableModels = await this.availableModels() - - return { - models: { - currentModelId: `${session.model.providerID}/${session.model.modelID}`, - availableModels, - }, - _meta: {}, + function toLocations(toolName: string, input: Record): { path: string }[] { + const tool = toolName.toLocaleLowerCase() + switch (tool) { + case "read": + case "edit": + case "write": + return input["filePath"] ? [{ path: input["filePath"] }] : [] + case "glob": + case "grep": + return input["path"] ? [{ path: input["path"] }] : [] + case "bash": + return [] + case "list": + return input["path"] ? [{ path: input["path"] }] : [] + default: + return [] } } - async setSessionModel(params: SetSessionModelRequest): Promise { - this.log.info("setSessionModel", { sessionId: params.sessionId, modelId: params.modelId }) - - const session = this.sessionManager.get(params.sessionId) - if (!session) { - throw new Error(`Session not found: ${params.sessionId}`) - } - - const parsed = Provider.parseModel(params.modelId) - const model = await Provider.getModel(parsed.providerID, parsed.modelID) - - this.sessionManager.setModel(session.id, { - providerID: model.providerID, - modelID: model.modelID, - }) - - return { - _meta: {}, - } - } - - private async defaultModel() { - const configured = this.config.defaultModel + async function defaultModel(config: ACPConfig) { + const configured = config.defaultModel if (configured) return configured return Provider.defaultModel() } - private async availableModels() { - const providers = await Provider.list() - const entries = Object.entries(providers).sort((a, b) => { - const nameA = a[1].info.name.toLowerCase() - const nameB = b[1].info.name.toLowerCase() - if (nameA < nameB) return -1 - if (nameA > nameB) return 1 - return 0 - }) - return entries.flatMap(([providerID, provider]) => { - const models = Provider.sort(Object.values(provider.info.models)) - return models.map((model) => ({ - modelId: `${providerID}/${model.id}`, - name: `${provider.info.name}/${model.name}`, - })) - }) - } - - async prompt(params: PromptRequest): Promise { - this.log.info("prompt", { - sessionId: params.sessionId, - promptLength: params.prompt.length, - }) - - const acpSession = this.sessionManager.get(params.sessionId) - if (!acpSession) { - throw new Error(`Session not found: ${params.sessionId}`) - } - - const current = acpSession.model - const model = current ?? (await this.defaultModel()) - if (!current) { - this.sessionManager.setModel(acpSession.id, model) - } - - const parts = params.prompt.map((content) => { - if (content.type === "text") { + function parseUri( + uri: string, + ): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } { + try { + if (uri.startsWith("file://")) { + const path = uri.slice(7) + const name = path.split("/").pop() || path return { - type: "text" as const, - text: content.text, + type: "file", + url: uri, + filename: name, + mime: "text/plain", } } - if (content.type === "resource") { - const resource = content.resource - let text = "" - if ("text" in resource && typeof resource.text === "string") { - text = resource.text - } - return { - type: "text" as const, - text, + if (uri.startsWith("zed://")) { + const url = new URL(uri) + const path = url.searchParams.get("path") + if (path) { + const name = path.split("/").pop() || path + return { + type: "file", + url: `file://${path}`, + filename: name, + mime: "text/plain", + } } } return { - type: "text" as const, - text: JSON.stringify(content), + type: "text", + text: uri, + } + } catch { + return { + type: "text", + text: uri, } - }) - - await SessionPrompt.prompt({ - sessionID: acpSession.openCodeSessionId, - messageID: Identifier.ascending("message"), - model: { - providerID: model.providerID, - modelID: model.modelID, - }, - parts, - acpConnection: { - connection: this.connection, - sessionId: params.sessionId, - }, - }) - - this.log.debug("prompt response completed") - - // Streaming notifications are now handled during prompt execution - // No need to send final text chunk here - - return { - stopReason: "end_turn", - _meta: {}, } } - - async cancel(params: CancelNotification): Promise { - this.log.info("cancel", { sessionId: params.sessionId }) - } } diff --git a/packages/opencode/src/acp/client.ts b/packages/opencode/src/acp/client.ts deleted file mode 100644 index 24119eabe..000000000 --- a/packages/opencode/src/acp/client.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { - Client, - CreateTerminalRequest, - CreateTerminalResponse, - KillTerminalCommandRequest, - KillTerminalResponse, - ReadTextFileRequest, - ReadTextFileResponse, - ReleaseTerminalRequest, - ReleaseTerminalResponse, - RequestPermissionRequest, - RequestPermissionResponse, - SessionNotification, - TerminalOutputRequest, - TerminalOutputResponse, - WaitForTerminalExitRequest, - WaitForTerminalExitResponse, - WriteTextFileRequest, - WriteTextFileResponse, -} from "@agentclientprotocol/sdk" -import { Log } from "../util/log" - -export class ACPClient implements Client { - private log = Log.create({ service: "acp-client" }) - - async requestPermission(params: RequestPermissionRequest): Promise { - this.log.debug("requestPermission", params) - const firstOption = params.options[0] - if (!firstOption) { - return { outcome: { outcome: "cancelled" } } - } - return { - outcome: { - outcome: "selected", - optionId: firstOption.optionId, - }, - } - } - - async sessionUpdate(params: SessionNotification): Promise { - this.log.debug("sessionUpdate", { sessionId: params.sessionId }) - } - - async writeTextFile(params: WriteTextFileRequest): Promise { - this.log.debug("writeTextFile", { path: params.path }) - await Bun.write(params.path, params.content) - return { _meta: {} } - } - - async readTextFile(params: ReadTextFileRequest): Promise { - this.log.debug("readTextFile", { path: params.path }) - const file = Bun.file(params.path) - const exists = await file.exists() - if (!exists) { - throw new Error(`File not found: ${params.path}`) - } - const content = await file.text() - return { content, _meta: {} } - } - - async createTerminal(params: CreateTerminalRequest): Promise { - this.log.debug("createTerminal", params) - throw new Error("Terminal support not yet implemented") - } - - async terminalOutput(params: TerminalOutputRequest): Promise { - this.log.debug("terminalOutput", params) - throw new Error("Terminal support not yet implemented") - } - - async releaseTerminal(params: ReleaseTerminalRequest): Promise { - this.log.debug("releaseTerminal", params) - throw new Error("Terminal support not yet implemented") - } - - async waitForTerminalExit(params: WaitForTerminalExitRequest): Promise { - this.log.debug("waitForTerminalExit", params) - throw new Error("Terminal support not yet implemented") - } - - async killTerminal(params: KillTerminalCommandRequest): Promise { - this.log.debug("killTerminal", params) - throw new Error("Terminal support not yet implemented") - } -} diff --git a/packages/opencode/src/acp/server.ts b/packages/opencode/src/acp/server.ts deleted file mode 100644 index 0e5306dcd..000000000 --- a/packages/opencode/src/acp/server.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk" -import { Log } from "../util/log" -import { Instance } from "../project/instance" -import { OpenCodeAgent } from "./agent" - -export namespace ACPServer { - const log = Log.create({ service: "acp-server" }) - - export async function start() { - await Instance.provide({ - directory: process.cwd(), - fn: async () => { - log.info("starting ACP server", { cwd: process.cwd() }) - - const stdout = new WritableStream({ - write(chunk) { - process.stdout.write(chunk) - }, - }) - - const stdin = new ReadableStream({ - start(controller) { - process.stdin.on("data", (chunk) => { - controller.enqueue(new Uint8Array(chunk)) - }) - process.stdin.on("end", () => { - controller.close() - }) - }, - }) - - const stream = ndJsonStream(stdout, stdin) - - new AgentSideConnection((conn) => { - return new OpenCodeAgent(conn) - }, stream) - - await new Promise((resolve) => { - process.on("SIGTERM", () => { - log.info("received SIGTERM") - resolve() - }) - process.on("SIGINT", () => { - log.info("received SIGINT") - resolve() - }) - }) - - log.info("ACP server stopped") - }, - }) - } -} diff --git a/packages/opencode/src/acp/session.ts b/packages/opencode/src/acp/session.ts index 3a7972590..652e8cfdd 100644 --- a/packages/opencode/src/acp/session.ts +++ b/packages/opencode/src/acp/session.ts @@ -7,20 +7,15 @@ import type { ACPSessionState } from "./types" export class ACPSessionManager { private sessions = new Map() - async create( - cwd: string, - mcpServers: McpServer[], - model?: ACPSessionState["model"], - ): Promise { - const sessionId = `acp_${Identifier.ascending("session")}` - const openCodeSession = await Session.create({ title: `ACP Session ${sessionId}` }) + async create(cwd: string, mcpServers: McpServer[], model?: ACPSessionState["model"]): Promise { + const session = await Session.create({ title: `ACP Session ${crypto.randomUUID()}` }) + const sessionId = session.id const resolvedModel = model ?? (await Provider.defaultModel()) const state: ACPSessionState = { id: sessionId, cwd, mcpServers, - openCodeSessionId: openCodeSession.id, createdAt: new Date(), model: resolvedModel, } @@ -29,54 +24,22 @@ export class ACPSessionManager { return state } - get(sessionId: string): ACPSessionState | undefined { + get(sessionId: string) { return this.sessions.get(sessionId) } - async remove(sessionId: string): Promise { + async remove(sessionId: string) { const state = this.sessions.get(sessionId) if (!state) return - await Session.remove(state.openCodeSessionId).catch(() => {}) + await Session.remove(sessionId).catch(() => {}) this.sessions.delete(sessionId) } - has(sessionId: string): boolean { + has(sessionId: string) { return this.sessions.has(sessionId) } - async load( - sessionId: string, - cwd: string, - mcpServers: McpServer[], - model?: ACPSessionState["model"], - ): Promise { - const existing = this.sessions.get(sessionId) - if (existing) { - if (!existing.model) { - const resolved = model ?? (await Provider.defaultModel()) - existing.model = resolved - this.sessions.set(sessionId, existing) - } - return existing - } - - const openCodeSession = await Session.create({ title: `ACP Session ${sessionId} (loaded)` }) - const resolvedModel = model ?? (await Provider.defaultModel()) - - const state: ACPSessionState = { - id: sessionId, - cwd, - mcpServers, - openCodeSessionId: openCodeSession.id, - createdAt: new Date(), - model: resolvedModel, - } - - this.sessions.set(sessionId, state) - return state - } - getModel(sessionId: string) { const session = this.sessions.get(sessionId) if (!session) return @@ -90,4 +53,12 @@ export class ACPSessionManager { this.sessions.set(sessionId, session) return session } + + setMode(sessionId: string, modeId: string) { + const session = this.sessions.get(sessionId) + if (!session) return + session.modeId = modeId + this.sessions.set(sessionId, session) + return session + } } diff --git a/packages/opencode/src/acp/types.ts b/packages/opencode/src/acp/types.ts index 1bffa0197..56308cb76 100644 --- a/packages/opencode/src/acp/types.ts +++ b/packages/opencode/src/acp/types.ts @@ -4,12 +4,12 @@ export interface ACPSessionState { id: string cwd: string mcpServers: McpServer[] - openCodeSessionId: string createdAt: Date model: { providerID: string modelID: string } + modeId?: string } export interface ACPConfig { diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts index f415cd6ad..6628137f4 100644 --- a/packages/opencode/src/cli/cmd/acp.ts +++ b/packages/opencode/src/cli/cmd/acp.ts @@ -1,5 +1,17 @@ -import { ACPServer } from "../../acp/server" +import { Log } from "@/util/log" +import { bootstrap } from "../bootstrap" import { cmd } from "./cmd" +import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk" +import { ACP } from "@/acp/agent" + +const log = Log.create({ service: "acp-command" }) + +process.on("unhandledRejection", (reason, promise) => { + log.error("Unhandled rejection", { + promise, + reason, + }) +}) export const AcpCommand = cmd({ command: "acp", @@ -13,6 +25,38 @@ export const AcpCommand = cmd({ }, handler: async (opts) => { if (opts.cwd) process.chdir(opts["cwd"]) - await ACPServer.start() + await bootstrap(process.cwd(), async () => { + const input = new WritableStream({ + write(chunk) { + return new Promise((resolve, reject) => { + process.stdout.write(Buffer.from(chunk), (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + }, + }) + const output = new ReadableStream({ + start(controller) { + process.stdin.on("data", (chunk: Buffer) => { + controller.enqueue(new Uint8Array(chunk)) + }) + process.stdin.on("end", () => controller.close()) + process.stdin.on("error", (err) => controller.error(err)) + }, + }) + + const stream = ndJsonStream(input, output) + + new AgentSideConnection((conn) => { + return new ACP.Agent(conn) + }, stream) + + log.info("setup connection") + }) + process.stdin.resume() }, }) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 64f64082e..640dd55c2 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -340,10 +340,25 @@ export namespace Session { }, ) - export const updatePart = fn(MessageV2.Part, async (part) => { + const UpdatePartInput = z.union([ + MessageV2.Part, + z.object({ + part: MessageV2.TextPart, + delta: z.string(), + }), + z.object({ + part: MessageV2.ReasoningPart, + delta: z.string(), + }), + ]) + + export const updatePart = fn(UpdatePartInput, async (input) => { + const part = "delta" in input ? input.part : input + const delta = "delta" in input ? input.delta : undefined await Storage.write(["part", part.messageID, part.id], part) Bus.publish(MessageV2.Event.PartUpdated, { part, + delta, }) return part }) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index b3efefcc9..c37773b74 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -359,6 +359,7 @@ export namespace MessageV2 { "message.part.updated", z.object({ part: Part, + delta: z.string().optional(), }), ), PartRemoved: Bus.event( diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b6ec891bc..6adeb6f7e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -96,16 +96,6 @@ export namespace SessionPrompt { agent: z.string().optional(), system: z.string().optional(), tools: z.record(z.string(), z.boolean()).optional(), - /** - * ACP (Agent Client Protocol) connection details for streaming responses. - * When provided, enables real-time streaming and tool execution visibility. - */ - acpConnection: z - .object({ - connection: z.any(), // AgentSideConnection - using any to avoid circular deps - sessionId: z.string(), // ACP session ID (different from opencode sessionID) - }) - .optional(), parts: z.array( z.discriminatedUnion("type", [ MessageV2.TextPart.omit({ @@ -184,7 +174,6 @@ export namespace SessionPrompt { agent: agent.name, system, abort: abort.signal, - acpConnection: input.acpConnection, }) const tools = await resolveTools({ @@ -196,6 +185,28 @@ export namespace SessionPrompt { processor, }) + // const permUnsub = (() => { + // const handled = new Set() + // const options = [ + // { optionId: "allow_once", kind: "allow_once", name: "Allow once" }, + // { optionId: "allow_always", kind: "allow_always", name: "Always allow" }, + // { optionId: "reject_once", kind: "reject_once", name: "Reject" }, + // ] + // return Bus.subscribe(Permission.Event.Updated, async (event) => { + // const info = event.properties + // if (info.sessionID !== input.sessionID) return + // if (handled.has(info.id)) return + // handled.add(info.id) + // const toolCallId = info.callID ?? info.id + // const metadata = info.metadata ?? {} + // // TODO: emit permission event to bus for ACP to handle + // Permission.respond({ sessionID: info.sessionID, permissionID: info.id, response: "reject" }) + // }) + // })() + // await using _permSub = defer(() => { + // permUnsub?.() + // }) + const params = await Plugin.trigger( "chat.params", { @@ -889,60 +900,6 @@ export namespace SessionPrompt { return input.messages } - /** - * Maps tool names to ACP tool kinds for consistent categorization. - * - read: Tools that read data (read, glob, grep, list, webfetch, docs) - * - edit: Tools that modify state (edit, write, bash) - * - other: All other tools (MCP tools, task, todowrite, etc.) - */ - function determineToolKind(toolName: string): "read" | "edit" | "other" { - const readTools = [ - "read", - "glob", - "grep", - "list", - "webfetch", - "context7_resolve_library_id", - "context7_get_library_docs", - ] - const editTools = ["edit", "write", "bash"] - - if (readTools.includes(toolName.toLowerCase())) return "read" - if (editTools.includes(toolName.toLowerCase())) return "edit" - return "other" - } - - /** - * Extracts file/directory locations from tool inputs for ACP notifications. - * Returns array of {path} objects that ACP clients can use for navigation. - * - * Examples: - * - read({filePath: "/foo/bar.ts"}) -> [{path: "/foo/bar.ts"}] - * - glob({pattern: "*.ts", path: "/src"}) -> [{path: "/src"}] - * - bash({command: "ls"}) -> [] (no file references) - */ - function extractLocations(toolName: string, input: Record): { path: string }[] { - try { - switch (toolName.toLowerCase()) { - case "read": - case "edit": - case "write": - return input["filePath"] ? [{ path: input["filePath"] }] : [] - case "glob": - case "grep": - return input["path"] ? [{ path: input["path"] }] : [] - case "bash": - return [] - case "list": - return input["path"] ? [{ path: input["path"] }] : [] - default: - return [] - } - } catch { - return [] - } - } - export type Processor = Awaited> async function createProcessor(input: { sessionID: string @@ -951,10 +908,6 @@ export namespace SessionPrompt { system: string[] agent: string abort: AbortSignal - acpConnection?: { - connection: any - sessionId: string - } }) { const toolcalls: Record = {} let snapshot: string | undefined @@ -1052,7 +1005,7 @@ export namespace SessionPrompt { const part = reasoningMap[value.id] part.text += value.text if (value.providerMetadata) part.metadata = value.providerMetadata - if (part.text) await Session.updatePart(part) + if (part.text) await Session.updatePart({ part, delta: value.text }) } break @@ -1084,26 +1037,6 @@ export namespace SessionPrompt { }, }) toolcalls[value.id] = part as MessageV2.ToolPart - - // Notify ACP client of pending tool call - if (input.acpConnection) { - await input.acpConnection.connection - .sessionUpdate({ - sessionId: input.acpConnection.sessionId, - update: { - sessionUpdate: "tool_call", - toolCallId: value.id, - title: value.toolName, - kind: determineToolKind(value.toolName), - status: "pending", - locations: [], // Will be populated when we have input - rawInput: {}, - }, - }) - .catch((err: Error) => { - log.error("failed to send tool pending to ACP", { error: err }) - }) - } break case "tool-input-delta": @@ -1128,24 +1061,6 @@ export namespace SessionPrompt { metadata: value.providerMetadata, }) toolcalls[value.toolCallId] = part as MessageV2.ToolPart - - // Notify ACP client that tool is running - if (input.acpConnection) { - await input.acpConnection.connection - .sessionUpdate({ - sessionId: input.acpConnection.sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: value.toolCallId, - status: "in_progress", - locations: extractLocations(value.toolName, value.input), - rawInput: value.input, - }, - }) - .catch((err: Error) => { - log.error("failed to send tool in_progress to ACP", { error: err }) - }) - } } break } @@ -1168,32 +1083,6 @@ export namespace SessionPrompt { }, }) - // Notify ACP client that tool completed - if (input.acpConnection) { - await input.acpConnection.connection - .sessionUpdate({ - sessionId: input.acpConnection.sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: value.toolCallId, - status: "completed", - content: [ - { - type: "content", - content: { - type: "text", - text: value.output.output, - }, - }, - ], - rawOutput: value.output, - }, - }) - .catch((err: Error) => { - log.error("failed to send tool completed to ACP", { error: err }) - }) - } - delete toolcalls[value.toolCallId] } break @@ -1216,34 +1105,6 @@ export namespace SessionPrompt { }, }) - // Notify ACP client of tool error - if (input.acpConnection) { - await input.acpConnection.connection - .sessionUpdate({ - sessionId: input.acpConnection.sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: value.toolCallId, - status: "failed", - content: [ - { - type: "content", - content: { - type: "text", - text: `Error: ${(value.error as any).toString()}`, - }, - }, - ], - rawOutput: { - error: (value.error as any).toString(), - }, - }, - }) - .catch((err: Error) => { - log.error("failed to send tool error to ACP", { error: err }) - }) - } - if (value.error instanceof Permission.RejectedError) { blocked = true } @@ -1322,26 +1183,11 @@ export namespace SessionPrompt { if (currentText) { currentText.text += value.text if (value.providerMetadata) currentText.metadata = value.providerMetadata - if (currentText.text) await Session.updatePart(currentText) - - // Send streaming chunk to ACP client - if (input.acpConnection && value.text) { - await input.acpConnection.connection - .sessionUpdate({ - sessionId: input.acpConnection.sessionId, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: value.text, - }, - }, - }) - .catch((err: Error) => { - log.error("failed to send text delta to ACP", { error: err }) - // Don't fail the whole request if ACP notification fails - }) - } + if (currentText.text) + await Session.updatePart({ + part: currentText, + delta: value.text, + }) } break From fe5e7cfd1b70f8a04a0e21a603822d7134955062 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 25 Oct 2025 01:35:43 -0500 Subject: [PATCH 02/56] ignore: rm change --- opencode.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/opencode.json b/opencode.json index a8819ebdc..720ece5c1 100644 --- a/opencode.json +++ b/opencode.json @@ -1,8 +1,3 @@ { - "$schema": "https://opencode.ai/config.json", - "permission": { - "bash": { - "cat*": "ask" - } - } + "$schema": "https://opencode.ai/config.json" } From fc2afdc92f55dba859a3c28fdf6e187a64ee0e0f Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 25 Oct 2025 06:45:23 +0000 Subject: [PATCH 03/56] release: v0.15.17 --- bun.lock | 22 +++++++++++----------- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/sdk/js/src/gen/types.gen.ts | 6 ++---- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 15 files changed, 26 insertions(+), 28 deletions(-) diff --git a/bun.lock b/bun.lock index e964ec6a4..e1d1ea2a0 100644 --- a/bun.lock +++ b/bun.lock @@ -37,7 +37,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "0.15.16", + "version": "0.15.17", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -64,7 +64,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "0.15.16", + "version": "0.15.17", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -88,7 +88,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "0.15.16", + "version": "0.15.17", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -109,7 +109,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "0.15.16", + "version": "0.15.17", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -149,7 +149,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "0.15.16", + "version": "0.15.17", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -165,7 +165,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "0.15.16", + "version": "0.15.17", "bin": { "opencode": "./bin/opencode", }, @@ -229,7 +229,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "0.15.16", + "version": "0.15.17", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -249,7 +249,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "0.15.16", + "version": "0.15.17", "devDependencies": { "@hey-api/openapi-ts": "0.81.0", "@tsconfig/node22": "catalog:", @@ -260,7 +260,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "0.15.16", + "version": "0.15.17", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -273,7 +273,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "0.15.16", + "version": "0.15.17", "dependencies": { "@kobalte/core": "catalog:", "@pierre/precision-diffs": "0.0.2-alpha.1-1", @@ -296,7 +296,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "0.15.16", + "version": "0.15.17", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 46e6aef5c..9fea56f7f 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -7,7 +7,7 @@ "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json", "start": "vinxi start", - "version": "0.15.16" + "version": "0.15.17" }, "dependencies": { "@ibm/plex": "6.4.1", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 6251e75e0..9e53aaf9b 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "0.15.16", + "version": "0.15.17", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index f9d9ee4b0..9df0e70e4 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "0.15.16", + "version": "0.15.17", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 05048f6b0..9f1140ecf 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "0.15.16", + "version": "0.15.17", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 40ad91780..81b977ec0 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "0.15.16", + "version": "0.15.17", "description": "", "type": "module", "scripts": { diff --git a/packages/function/package.json b/packages/function/package.json index 1d7da00ff..b24a967c2 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "0.15.16", + "version": "0.15.17", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 20703af31..42583d1fd 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "0.15.16", + "version": "0.15.17", "name": "opencode", "type": "module", "private": true, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 512869a1f..a368175b3 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "0.15.16", + "version": "0.15.17", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index fc73d199e..3c1e1fa5c 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "0.15.16", + "version": "0.15.17", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 3c53b4ac1..3cf136918 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -836,6 +836,7 @@ export type StepFinishPart = { sessionID: string messageID: string type: "step-finish" + reason: string snapshot?: string cost: number tokens: { @@ -1116,6 +1117,7 @@ export type EventMessagePartUpdated = { type: "message.part.updated" properties: { part: Part + delta?: string } } @@ -1896,10 +1898,6 @@ export type SessionPromptData = { tools?: { [key: string]: boolean } - acpConnection?: { - connection: unknown - sessionId: string - } parts: Array } path: { diff --git a/packages/slack/package.json b/packages/slack/package.json index 26a3902e5..3b6e5a5a6 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "0.15.16", + "version": "0.15.17", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 2ccd1f2d7..be500ea94 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "0.15.16", + "version": "0.15.17", "type": "module", "exports": { ".": "./src/components/index.ts", diff --git a/packages/web/package.json b/packages/web/package.json index 761450f45..a123e64b4 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "0.15.16", + "version": "0.15.17", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 8eae02660..7a6ecf75d 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "0.15.16", + "version": "0.15.17", "publisher": "sst-dev", "repository": { "type": "git", From 187a5fe301657ad6823fb39fce09fef468ef5dbe Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 25 Oct 2025 12:04:18 +0000 Subject: [PATCH 04/56] ignore: update download stats 2025-10-25 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index 39b4056b6..9651a2b02 100644 --- a/STATS.md +++ b/STATS.md @@ -118,3 +118,4 @@ | 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) | | 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) | | 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) | +| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) | From ae62bc8b1fce194ae99a05251ed4bf85e38acc32 Mon Sep 17 00:00:00 2001 From: Paulo Edgar Castro Date: Sat, 25 Oct 2025 18:08:27 +0100 Subject: [PATCH 05/56] fix: timeout param that allows user to disable provider timeout (#3443) --- packages/opencode/src/provider/provider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 7c40955dc..b2f2e5d40 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -422,14 +422,14 @@ export namespace Provider { const modPath = provider.id === "google-vertex-anthropic" ? `${installedPath}/dist/anthropic/index.mjs` : installedPath const mod = await import(modPath) - if (options["timeout"] !== undefined) { + if (options["timeout"] !== undefined && options["timeout"] !== null) { // Only override fetch if user explicitly sets timeout options["fetch"] = async (input: any, init?: BunFetchRequestInit) => { const { signal, ...rest } = init ?? {} const signals: AbortSignal[] = [] if (signal) signals.push(signal) - signals.push(AbortSignal.timeout(options["timeout"])) + if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"])) const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0] From 2e434a459aa83766871075f3341843cfcb4aa72d Mon Sep 17 00:00:00 2001 From: Mohammad Alhashemi <64827602+malhashemi@users.noreply.github.com> Date: Sun, 26 Oct 2025 03:56:54 +0800 Subject: [PATCH 06/56] feat: add noReply parameter (#3433) Co-authored-by: opencode-agent[bot] Co-authored-by: rekram1-node --- packages/opencode/src/session/prompt.ts | 6 +++ packages/sdk/js/src/gen/types.gen.ts | 1 + packages/web/src/content/docs/sdk.mdx | 51 ++++++++++++++---------- packages/web/src/content/docs/server.mdx | 40 +++++++++---------- 4 files changed, 57 insertions(+), 41 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 6adeb6f7e..ec1375891 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -94,6 +94,7 @@ export namespace SessionPrompt { }) .optional(), agent: z.string().optional(), + noReply: z.boolean().optional(), system: z.string().optional(), tools: z.record(z.string(), z.boolean()).optional(), parts: z.array( @@ -142,6 +143,11 @@ export namespace SessionPrompt { const userMsg = await createUserMessage(input) await Session.touch(input.sessionID) + // Early return for context-only messages (no AI inference) + if (input.noReply) { + return userMsg + } + if (isBusy(input.sessionID)) { return new Promise((resolve) => { const queue = state().queued.get(input.sessionID) ?? [] diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 3cf136918..47bb20972 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1894,6 +1894,7 @@ export type SessionPromptData = { modelID: string } agent?: string + noReply?: boolean system?: string tools?: { [key: string]: boolean diff --git a/packages/web/src/content/docs/sdk.mdx b/packages/web/src/content/docs/sdk.mdx index 6d66cebfd..07166165a 100644 --- a/packages/web/src/content/docs/sdk.mdx +++ b/packages/web/src/content/docs/sdk.mdx @@ -209,27 +209,27 @@ const { providers, default: defaults } = await client.config.providers() ### Sessions -| Method | Description | Notes | -| ---------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| `session.list()` | List sessions | Returns Session[] | -| `session.get({ path })` | Get session | Returns Session | -| `session.children({ path })` | List child sessions | Returns Session[] | -| `session.create({ body })` | Create session | Returns Session | -| `session.delete({ path })` | Delete session | Returns `boolean` | -| `session.update({ path, body })` | Update session properties | Returns Session | -| `session.init({ path, body })` | Analyze app and create `AGENTS.md` | Returns `boolean` | -| `session.abort({ path })` | Abort a running session | Returns `boolean` | -| `session.share({ path })` | Share session | Returns Session | -| `session.unshare({ path })` | Unshare session | Returns Session | -| `session.summarize({ path, body })` | Summarize session | Returns `boolean` | -| `session.messages({ path })` | List messages in a session | Returns `{ info: `Message`, parts: `Part[]`}[]` | -| `session.message({ path })` | Get message details | Returns `{ info: `Message`, parts: `Part[]`}` | -| `session.prompt({ path, body })` | Send prompt message | Returns `{ info: `AssistantMessage`, parts: `Part[]`}` | -| `session.command({ path, body })` | Send command to session | Returns `{ info: `AssistantMessage`, parts: `Part[]`}` | -| `session.shell({ path, body })` | Run a shell command | Returns AssistantMessage | -| `session.revert({ path, body })` | Revert a message | Returns Session | -| `session.unrevert({ path })` | Restore reverted messages | Returns Session | -| `postSessionByIdPermissionsByPermissionId({ path, body })` | Respond to a permission request | Returns `boolean` | +| Method | Description | Notes | +| ---------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `session.list()` | List sessions | Returns Session[] | +| `session.get({ path })` | Get session | Returns Session | +| `session.children({ path })` | List child sessions | Returns Session[] | +| `session.create({ body })` | Create session | Returns Session | +| `session.delete({ path })` | Delete session | Returns `boolean` | +| `session.update({ path, body })` | Update session properties | Returns Session | +| `session.init({ path, body })` | Analyze app and create `AGENTS.md` | Returns `boolean` | +| `session.abort({ path })` | Abort a running session | Returns `boolean` | +| `session.share({ path })` | Share session | Returns Session | +| `session.unshare({ path })` | Unshare session | Returns Session | +| `session.summarize({ path, body })` | Summarize session | Returns `boolean` | +| `session.messages({ path })` | List messages in a session | Returns `{ info: `Message`, parts: `Part[]`}[]` | +| `session.message({ path })` | Get message details | Returns `{ info: `Message`, parts: `Part[]`}` | +| `session.prompt({ path, body })` | Send prompt message | `body.noReply: true` returns UserMessage (context only). Default returns AssistantMessage with AI response | +| `session.command({ path, body })` | Send command to session | Returns `{ info: `AssistantMessage`, parts: `Part[]`}` | +| `session.shell({ path, body })` | Run a shell command | Returns AssistantMessage | +| `session.revert({ path, body })` | Revert a message | Returns Session | +| `session.unrevert({ path })` | Restore reverted messages | Returns Session | +| `postSessionByIdPermissionsByPermissionId({ path, body })` | Respond to a permission request | Returns `boolean` | --- @@ -251,6 +251,15 @@ const result = await client.session.prompt({ parts: [{ type: "text", text: "Hello!" }], }, }) + +// Inject context without triggering AI response (useful for plugins) +await client.session.prompt({ + path: { id: session.id }, + body: { + noReply: true, + parts: [{ type: "text", text: "You are a helpful assistant." }], + }, +}) ``` --- diff --git a/packages/web/src/content/docs/server.mdx b/packages/web/src/content/docs/server.mdx index e1d026221..3d880fb7c 100644 --- a/packages/web/src/content/docs/server.mdx +++ b/packages/web/src/content/docs/server.mdx @@ -88,26 +88,26 @@ The opencode server exposes the following APIs. ### Sessions -| Method | Path | Description | Notes | -| -------- | ---------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `GET` | `/session` | List sessions | Returns Session[] | -| `GET` | `/session/:id` | Get session | Returns Session | -| `GET` | `/session/:id/children` | List child sessions | Returns Session[] | -| `POST` | `/session` | Create session | body: `{ parentID?, title? }`, returns Session | -| `DELETE` | `/session/:id` | Delete session | | -| `PATCH` | `/session/:id` | Update session properties | body: `{ title? }`, returns Session | -| `POST` | `/session/:id/init` | Analyze app and create `AGENTS.md` | body: `{ messageID, providerID, modelID }` | -| `POST` | `/session/:id/abort` | Abort a running session | | -| `POST` | `/session/:id/share` | Share session | Returns Session | -| `DELETE` | `/session/:id/share` | Unshare session | Returns Session | -| `POST` | `/session/:id/summarize` | Summarize session | | -| `GET` | `/session/:id/message` | List messages in a session | Returns `{ info: `Message`, parts: `Part[]`}[]` | -| `GET` | `/session/:id/message/:messageID` | Get message details | Returns `{ info: `Message`, parts: `Part[]`}` | -| `POST` | `/session/:id/message` | Send chat message | body matches [`ChatInput`](https://github.com/sst/opencode/blob/main/packages/opencode/src/session/index.ts#L358), returns Message | -| `POST` | `/session/:id/shell` | Run a shell command | body matches [`CommandInput`](https://github.com/sst/opencode/blob/main/packages/opencode/src/session/index.ts#L1007), returns Message | -| `POST` | `/session/:id/revert` | Revert a message | body: `{ messageID }` | -| `POST` | `/session/:id/unrevert` | Restore reverted messages | | -| `POST` | `/session/:id/permissions/:permissionID` | Respond to a permission request | body: `{ response }` | +| Method | Path | Description | Notes | +| -------- | ---------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GET` | `/session` | List sessions | Returns Session[] | +| `GET` | `/session/:id` | Get session | Returns Session | +| `GET` | `/session/:id/children` | List child sessions | Returns Session[] | +| `POST` | `/session` | Create session | body: `{ parentID?, title? }`, returns Session | +| `DELETE` | `/session/:id` | Delete session | | +| `PATCH` | `/session/:id` | Update session properties | body: `{ title? }`, returns Session | +| `POST` | `/session/:id/init` | Analyze app and create `AGENTS.md` | body: `{ messageID, providerID, modelID }` | +| `POST` | `/session/:id/abort` | Abort a running session | | +| `POST` | `/session/:id/share` | Share session | Returns Session | +| `DELETE` | `/session/:id/share` | Unshare session | Returns Session | +| `POST` | `/session/:id/summarize` | Summarize session | | +| `GET` | `/session/:id/message` | List messages in a session | Returns `{ info: `Message`, parts: `Part[]`}[]` | +| `GET` | `/session/:id/message/:messageID` | Get message details | Returns `{ info: `Message`, parts: `Part[]`}` | +| `POST` | `/session/:id/message` | Send chat message | body matches [`ChatInput`](https://github.com/sst/opencode/blob/main/packages/opencode/src/session/index.ts#L358). Optional `noReply: true` skips AI inference and returns UserMessage. Returns Message | +| `POST` | `/session/:id/shell` | Run a shell command | body matches [`CommandInput`](https://github.com/sst/opencode/blob/main/packages/opencode/src/session/index.ts#L1007), returns Message | +| `POST` | `/session/:id/revert` | Revert a message | body: `{ messageID }` | +| `POST` | `/session/:id/unrevert` | Restore reverted messages | | +| `POST` | `/session/:id/permissions/:permissionID` | Respond to a permission request | body: `{ response }` | --- From 795b845782c1921b1684f4ab5783439b92e40c13 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 25 Oct 2025 17:26:27 -0400 Subject: [PATCH 07/56] update anthropic prompt --- .../opencode/src/session/prompt/anthropic.txt | 111 +++++------------- 1 file changed, 30 insertions(+), 81 deletions(-) diff --git a/packages/opencode/src/session/prompt/anthropic.txt b/packages/opencode/src/session/prompt/anthropic.txt index 6e623fdad..4f377beb9 100644 --- a/packages/opencode/src/session/prompt/anthropic.txt +++ b/packages/opencode/src/session/prompt/anthropic.txt @@ -1,81 +1,24 @@ +You are OpenCode, the best coding agent on the planet. + You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user. IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files. -If the user asks for help or wants to give feedback inform them of the following: -- /help: Get help with using opencode -- To give feedback, users should report the issue at https://github.com/sst/opencode/issues +If the user asks for help or wants to give feedback inform them of the following: +- ctrl+p to list available actions +- To give feedback, users should report the issue at + https://github.com/sst/opencode + +When the user directly asks about OpenCode (eg. "can OpenCode do...", "does OpenCode have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific OpenCode feature (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from OpenCode docs. The list of available docs is available at https://opencode.ai/docs # Tone and style -You should be concise, direct, and to the point. -You should be concise, direct, and to the point, while providing complete information and matching the level of detail you provide in your response with the level of complexity of the user's query or the work you have completed. -IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do. -IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to. -Do not add additional code explanation summary unless requested by the user. After working on a file, just stop, rather than providing an explanation of what you did. -Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is .", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". Here are some examples to demonstrate appropriate verbosity: - -user: 2 + 2 -assistant: 4 - - - -user: what is 2+2? -assistant: 4 - - - -user: is 11 a prime number? -assistant: Yes - - - -user: what command should I run to list files in the current directory? -assistant: ls - - - -user: what command should I run to watch files in the current directory? -assistant: [use the ls tool to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files] -npm run dev - - - -user: How many golf balls fit inside a jetta? -assistant: 150000 - - - -user: what files are in the directory src/? -assistant: [runs ls and sees foo.c, bar.c, baz.c] -user: which file contains the implementation of foo? -assistant: src/foo.c - -When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system). -Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification. -Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session. -If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences. -Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked. -IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. - -# Proactiveness -You are allowed to be proactive, but only when the user asks you to do something. You should strive to strike a balance between: -- Doing the right thing when asked, including taking actions and follow-up actions -- Not surprising the user with actions you take without asking -For example, if the user asks you how to approach something, you should do your best to answer their question first, and not immediately jump into taking actions. +- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked. +- Your output will be displayed on a command line interface. Your responses should be short and concise. You can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification. +- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session. +- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files. # Professional objectivity -Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if Claude honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. - -# Following conventions -When making changes to files, first understand the file's code conventions. Mimic code style, use existing libraries and utilities, and follow existing patterns. -- NEVER assume that a given library is available, even if it is well known. Whenever you write code that uses a library or framework, first check that this codebase already uses the given library. For example, you might look at neighboring files, or check the package.json (or cargo.toml, and so on depending on the language). -- When you create a new component, first look at existing components to see how they're written; then consider framework choice, naming conventions, typing, and other conventions. -- When you edit a piece of code, first look at the code's surrounding context (especially its imports) to understand the code's choice of frameworks and libraries. Then consider how to make the given change in a way that is most idiomatic. -- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository. - -# Code style -- IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked - +Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if OpenCode honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. # Task Management You have access to the TodoWrite tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress. @@ -87,7 +30,7 @@ Examples: user: Run the build and fix any type errors -assistant: I'm going to use the TodoWrite tool to write the following items to the todo list: +assistant: I'm going to use the TodoWrite tool to write the following items to the todo list: - Run the build - Fix any type errors @@ -107,7 +50,6 @@ In the above example, the assistant completes all the tasks, including the 10 er user: Help me write a new feature that allows users to track their usage metrics and export them to various formats - assistant: I'll help you implement a usage metrics tracking and export feature. Let me first use the TodoWrite tool to plan this task. Adding the following todos to the todo list: 1. Research existing metrics tracking in the codebase @@ -124,25 +66,32 @@ I've found some existing telemetry code. Let me mark the first todo as in_progre [Assistant continues implementing the feature step by step, marking todos as in_progress and completed as they go] + # Doing tasks The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended: +- - Use the TodoWrite tool to plan the task if required -- Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially. -- Implement the solution using all tools available to you -- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach. -- VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to CLAUDE.md so that you will know to run it next time. -NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive. -- Tool results and user messages may include tags. tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result. +- Tool results and user messages may include tags. tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear. + # Tool usage policy - When doing file search, prefer to use the Task tool in order to reduce context usage. - You should proactively use the Task tool with specialized agents when the task at hand matches the agent's description. - When WebFetch returns a message about a redirect to a different host, you should immediately make a new WebFetch request with the redirect URL provided in the response. -- You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. When making multiple bash tool calls, you MUST send a single message with multiple tools calls to run the calls in parallel. For example, if you need to run "git status" and "git diff", send a single message with two tool calls to run the calls in parallel. - -IMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation. +- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls. +- If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple Task tool calls. +- Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: Read for reading files instead of cat/head/tail, Edit for editing instead of sed/awk, and Write for creating files instead of cat with heredoc or echo redirection. Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead. +- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the Task tool instead of running search commands directly. + +user: Where are errors from the client handled? +assistant: [Uses the Task tool to find the files that handle client errors instead of using Glob or Grep directly] + + +user: What is the codebase structure? +assistant: [Uses the Task tool] + # Code References From 42c1e61bf4e89e33ae3ff99a58ee56445be99ca2 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 25 Oct 2025 16:27:09 -0500 Subject: [PATCH 08/56] fix: $ invocation not .quiet() (#3449) --- packages/opencode/src/lsp/server.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 3969a64e8..ee0f73fc4 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -931,9 +931,15 @@ export namespace LSPServer { await fs.mkdir(installDir, { recursive: true }) if (ext === "zip") { - await $`unzip -o -q ${tempPath} -d ${installDir}`.quiet().nothrow() + const ok = await $`unzip -o -q ${tempPath} -d ${installDir}`.quiet().catch((error) => { + log.error("Failed to extract lua-language-server archive", { error }) + }) + if (!ok) return } else { - await $`tar -xzf ${tempPath} -C ${installDir}`.nothrow() + const ok = await $`tar -xzf ${tempPath} -C ${installDir}`.quiet().catch((error) => { + log.error("Failed to extract lua-language-server archive", { error }) + }) + if (!ok) return } await fs.rm(tempPath, { force: true }) @@ -947,7 +953,10 @@ export namespace LSPServer { } if (platform !== "win32") { - await $`chmod +x ${bin}`.nothrow() + const ok = await $`chmod +x ${bin}`.quiet().catch((error) => { + log.error("Failed to set executable permission for lua-language-server binary", { error }) + }) + if (!ok) return } log.info(`installed lua-language-server`, { bin }) From 0a778a2789b4e3400ef1e6bd8808671595290806 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 25 Oct 2025 22:14:29 -0500 Subject: [PATCH 09/56] make title gen more reliable --- packages/opencode/src/session/prompt.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index ec1375891..b769d6052 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1759,9 +1759,15 @@ export namespace SessionPrompt { .then((result) => { if (result.text) return Session.update(input.session.id, (draft) => { - const cleaned = result.text.replace(/[\s\S]*?<\/think>\s*/g, "").split("\n")[0] + const cleaned = result.text + .replace(/[\s\S]*?<\/think>\s*/g, "") + .split("\n") + .map((line) => line.trim()) + .find((line) => line.length > 0) + if (!cleaned) return + const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned - draft.title = title.trim() + draft.title = title }) }) .catch((error) => { From 20963c4186a659c38716142fc3922d96ab9e4016 Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 26 Oct 2025 03:49:21 +0000 Subject: [PATCH 10/56] release: v0.15.18 --- bun.lock | 22 +++++++++++----------- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 14 files changed, 24 insertions(+), 24 deletions(-) diff --git a/bun.lock b/bun.lock index e1d1ea2a0..8dcbb2b27 100644 --- a/bun.lock +++ b/bun.lock @@ -37,7 +37,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "0.15.17", + "version": "0.15.18", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -64,7 +64,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "0.15.17", + "version": "0.15.18", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -88,7 +88,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "0.15.17", + "version": "0.15.18", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -109,7 +109,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "0.15.17", + "version": "0.15.18", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -149,7 +149,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "0.15.17", + "version": "0.15.18", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -165,7 +165,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "0.15.17", + "version": "0.15.18", "bin": { "opencode": "./bin/opencode", }, @@ -229,7 +229,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "0.15.17", + "version": "0.15.18", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -249,7 +249,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "0.15.17", + "version": "0.15.18", "devDependencies": { "@hey-api/openapi-ts": "0.81.0", "@tsconfig/node22": "catalog:", @@ -260,7 +260,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "0.15.17", + "version": "0.15.18", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -273,7 +273,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "0.15.17", + "version": "0.15.18", "dependencies": { "@kobalte/core": "catalog:", "@pierre/precision-diffs": "0.0.2-alpha.1-1", @@ -296,7 +296,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "0.15.17", + "version": "0.15.18", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 9fea56f7f..b8dea62bc 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -7,7 +7,7 @@ "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json", "start": "vinxi start", - "version": "0.15.17" + "version": "0.15.18" }, "dependencies": { "@ibm/plex": "6.4.1", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 9e53aaf9b..f9a4909ff 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "0.15.17", + "version": "0.15.18", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 9df0e70e4..2aa560815 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "0.15.17", + "version": "0.15.18", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 9f1140ecf..accc76b42 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "0.15.17", + "version": "0.15.18", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 81b977ec0..135ee9bb1 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "0.15.17", + "version": "0.15.18", "description": "", "type": "module", "scripts": { diff --git a/packages/function/package.json b/packages/function/package.json index b24a967c2..1557eecfc 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "0.15.17", + "version": "0.15.18", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 42583d1fd..ca588b485 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "0.15.17", + "version": "0.15.18", "name": "opencode", "type": "module", "private": true, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index a368175b3..52efc9381 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "0.15.17", + "version": "0.15.18", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 3c1e1fa5c..15a411118 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "0.15.17", + "version": "0.15.18", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/slack/package.json b/packages/slack/package.json index 3b6e5a5a6..2f9e4cb55 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "0.15.17", + "version": "0.15.18", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index be500ea94..bf75293aa 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "0.15.17", + "version": "0.15.18", "type": "module", "exports": { ".": "./src/components/index.ts", diff --git a/packages/web/package.json b/packages/web/package.json index a123e64b4..e24eeb042 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "0.15.17", + "version": "0.15.18", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 7a6ecf75d..b3220f817 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "0.15.17", + "version": "0.15.18", "publisher": "sst-dev", "repository": { "type": "git", From c70e393c8156a441a519e1ea93578c82c9c7bcef Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 26 Oct 2025 01:21:34 -0500 Subject: [PATCH 11/56] Remove claude-haiku-4.5 from default priority for GitHub Copilot session title generation --- packages/opencode/src/provider/provider.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index b2f2e5d40..bc95e543b 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -101,7 +101,7 @@ export namespace Provider { "nova-pro", "nova-premier", "claude", - "deepseek" + "deepseek", ].some((m) => modelID.includes(m)) const isGovCloud = region.startsWith("us-gov") if (modelRequiresPrefix && !isGovCloud) { @@ -517,14 +517,11 @@ export namespace Provider { const provider = await state().then((state) => state.providers[providerID]) if (!provider) return - const priority = [ - "claude-haiku-4-5", - "claude-haiku-4.5", - "3-5-haiku", - "3.5-haiku", - "gemini-2.5-flash", - "gpt-5-nano", - ] + let priority = ["claude-haiku-4-5", "claude-haiku-4.5", "3-5-haiku", "3.5-haiku", "gemini-2.5-flash", "gpt-5-nano"] + // claude-haiku-4.5 is considered a premium model in github copilot, we shouldn't use premium requests for title gen + if (providerID === "github-copilot") { + priority = priority.filter((m) => m !== "claude-haiku-4.5") + } for (const item of priority) { for (const model of Object.keys(provider.info.models)) { if (model.includes(item)) return getModel(providerID, model) From 7d0c6860cd5215663b385e436473b3f9759601ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joscha=20G=C3=B6tzer?= Date: Sun, 26 Oct 2025 07:40:17 +0100 Subject: [PATCH 12/56] fix: make build script work cross-platform (#3430) Co-authored-by: JosXa --- packages/opencode/script/build.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index feb91e09b..b3d6d57dd 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -1,6 +1,11 @@ #!/usr/bin/env bun import path from "path" -const dir = new URL("..", import.meta.url).pathname +import { fileURLToPath } from "url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const dir = path.resolve(__dirname, "..") + process.chdir(dir) import { $ } from "bun" From 2c792f17e66f3f3f5fbd77df92319b88d2dd3f01 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 26 Oct 2025 12:04:22 +0000 Subject: [PATCH 13/56] ignore: update download stats 2025-10-26 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index 9651a2b02..fd1a15446 100644 --- a/STATS.md +++ b/STATS.md @@ -119,3 +119,4 @@ | 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) | | 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) | | 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) | +| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) | From 3241f6b8bb39cf6e9ce43b7d1b68e470911848f7 Mon Sep 17 00:00:00 2001 From: Dan McGuirk Date: Sun, 26 Oct 2025 12:37:25 -0700 Subject: [PATCH 14/56] docs: fix typos (#3454) --- github/README.md | 4 ++-- packages/web/src/content/docs/github.mdx | 2 +- packages/web/src/content/docs/gitlab.mdx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/github/README.md b/github/README.md index 7601f5133..8e5b6d813 100644 --- a/github/README.md +++ b/github/README.md @@ -104,7 +104,7 @@ To test locally: - `MODEL`: The model used by opencode. Same as the `MODEL` defined in the GitHub workflow. - `ANTHROPIC_API_KEY`: Your model provider API key. Same as the keys defined in the GitHub workflow. - `GITHUB_RUN_ID`: Dummy value to emulate GitHub action environment. - - `MOCK_TOKEN`: A GitHub persontal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens). + - `MOCK_TOKEN`: A GitHub personal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens). - `MOCK_EVENT`: Mock GitHub event payload (see templates below). - `/path/to/opencode`: Path to your cloned opencode repo. `bun /path/to/opencode/github/index.ts` runs your local version of `opencode`. @@ -118,7 +118,7 @@ Replace: - `"owner":"sst"` with repo owner - `"repo":"hello-world"` with repo name -- `"actor":"fwang"` with the GitHub username of commentor +- `"actor":"fwang"` with the GitHub username of commenter - `"number":4` with the GitHub issue id - `"body":"hey opencode, summarize thread"` with comment body diff --git a/packages/web/src/content/docs/github.mdx b/packages/web/src/content/docs/github.mdx index d592fc84f..359f696fc 100644 --- a/packages/web/src/content/docs/github.mdx +++ b/packages/web/src/content/docs/github.mdx @@ -80,7 +80,7 @@ Or you can set it up manually. - `model`: The model to use with opencode. Takes the format of `provider/model`. This is **required**. - `share`: Whether to share the opencode session. Defaults to **true** for public repositories. -- `token`: Optional GitHub access token for performing operations such as creating comments, commiting changes, and opening pull requests. By default, opencode uses the installation access token from the opencode GitHub App, so commits, comments, and pull requests appear as coming from the app. +- `token`: Optional GitHub access token for performing operations such as creating comments, committing changes, and opening pull requests. By default, opencode uses the installation access token from the opencode GitHub App, so commits, comments, and pull requests appear as coming from the app. Alternatively, you can use the GitHub Action runner's [built-in `GITHUB_TOKEN`](https://docs.github.com/en/actions/tutorials/authenticate-with-github_token) without installing the opencode GitHub App. Just make sure to grant the required permissions in your workflow: diff --git a/packages/web/src/content/docs/gitlab.mdx b/packages/web/src/content/docs/gitlab.mdx index 335529540..2490e5963 100644 --- a/packages/web/src/content/docs/gitlab.mdx +++ b/packages/web/src/content/docs/gitlab.mdx @@ -79,7 +79,7 @@ Check out the [**GitLab docs**](https://docs.gitlab.com/user/duo_agent_platform/ Please use the glab CLI to access data from GitLab. The glab CLI has already been authenticated. You can run the corresponding commands. - If you are asked to summarise an MR or issue or asked to provide more information then please post back a note to the MR/Issue so that the user can see it. + If you are asked to summarize an MR or issue or asked to provide more information then please post back a note to the MR/Issue so that the user can see it. You don't need to commit or push up changes, those will be done automatically based on the file changes you make. " From 0eb899a9500a6cff6ed2e29bfcea79f3217a6d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sun, 26 Oct 2025 20:50:41 +0100 Subject: [PATCH 15/56] chore: cleanup versioned zod imports (#3460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit Co-authored-by: opencode-agent[bot] Co-authored-by: rekram1-node --- packages/opencode/script/schema.ts | 2 +- packages/opencode/src/agent/agent.ts | 2 +- packages/opencode/src/auth/github-copilot.ts | 2 +- packages/opencode/src/auth/index.ts | 2 +- packages/opencode/src/bun/index.ts | 2 +- packages/opencode/src/bus/index.ts | 4 +- packages/opencode/src/cli/ui.ts | 2 +- packages/opencode/src/command/index.ts | 2 +- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/file/fzf.ts | 2 +- packages/opencode/src/file/index.ts | 2 +- packages/opencode/src/file/ripgrep.ts | 4 +- packages/opencode/src/file/watcher.ts | 2 +- packages/opencode/src/id/id.ts | 2 +- packages/opencode/src/ide/index.ts | 2 +- packages/opencode/src/installation/index.ts | 2 +- packages/opencode/src/lsp/client.ts | 2 +- packages/opencode/src/lsp/index.ts | 2 +- packages/opencode/src/mcp/index.ts | 2 +- packages/opencode/src/permission/index.ts | 2 +- packages/opencode/src/project/project.ts | 2 +- packages/opencode/src/provider/models.ts | 2 +- packages/opencode/src/provider/provider.ts | 2 +- packages/opencode/src/server/server.ts | 2 +- packages/opencode/src/session/compaction.ts | 2 +- packages/opencode/src/session/index.ts | 2 +- packages/opencode/src/session/lock.ts | 2 +- packages/opencode/src/session/message-v2.ts | 2 +- packages/opencode/src/session/message.ts | 2 +- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/src/session/revert.ts | 2 +- packages/opencode/src/session/todo.ts | 2 +- packages/opencode/src/snapshot/index.ts | 2 +- packages/opencode/src/tool/bash.ts | 2 +- packages/opencode/src/tool/edit.ts | 2 +- packages/opencode/src/tool/glob.ts | 2 +- packages/opencode/src/tool/grep.ts | 2 +- packages/opencode/src/tool/invalid.ts | 2 +- packages/opencode/src/tool/ls.ts | 2 +- packages/opencode/src/tool/lsp-diagnostics.ts | 2 +- packages/opencode/src/tool/lsp-hover.ts | 2 +- packages/opencode/src/tool/multiedit.ts | 2 +- packages/opencode/src/tool/patch.ts | 47 ++++++++++--------- packages/opencode/src/tool/read.ts | 2 +- packages/opencode/src/tool/registry.ts | 2 +- packages/opencode/src/tool/task.ts | 2 +- packages/opencode/src/tool/todo.ts | 2 +- packages/opencode/src/tool/tool.ts | 2 +- packages/opencode/src/tool/webfetch.ts | 2 +- packages/opencode/src/tool/write.ts | 2 +- packages/opencode/src/util/error.ts | 2 +- packages/opencode/src/util/log.ts | 2 +- packages/plugin/src/tool.ts | 2 +- 53 files changed, 78 insertions(+), 77 deletions(-) diff --git a/packages/opencode/script/schema.ts b/packages/opencode/script/schema.ts index 9721b557a..585701c95 100755 --- a/packages/opencode/script/schema.ts +++ b/packages/opencode/script/schema.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun -import { z } from "zod/v4" +import { z } from "zod" import { Config } from "../src/config/config" const file = process.argv[2] diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 3577a9176..a6933708b 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -1,5 +1,5 @@ import { Config } from "../config/config" -import z from "zod/v4" +import z from "zod" import { Provider } from "../provider/provider" import { generateObject, type ModelMessage } from "ai" import PROMPT_GENERATE from "./generate.txt" diff --git a/packages/opencode/src/auth/github-copilot.ts b/packages/opencode/src/auth/github-copilot.ts index c9d90cd56..bd5740c9b 100644 --- a/packages/opencode/src/auth/github-copilot.ts +++ b/packages/opencode/src/auth/github-copilot.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Auth } from "./index" import { NamedError } from "../util/error" diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index ef9846a37..6d90c9325 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -1,7 +1,7 @@ import path from "path" import { Global } from "../global" import fs from "fs/promises" -import z from "zod/v4" +import z from "zod" export namespace Auth { export const Oauth = z diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts index d6fb29d62..8874a27ca 100644 --- a/packages/opencode/src/bun/index.ts +++ b/packages/opencode/src/bun/index.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Global } from "../global" import { Log } from "../util/log" import path from "path" diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index 7fbefba44..f4dd3ed2c 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -1,5 +1,5 @@ -import z from "zod/v4" -import type { ZodType } from "zod/v4" +import z from "zod" +import type { ZodType } from "zod" import { Log } from "../util/log" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts index bdbaed911..361e45255 100644 --- a/packages/opencode/src/cli/ui.ts +++ b/packages/opencode/src/cli/ui.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { EOL } from "os" import { NamedError } from "../util/error" diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index f879e627a..c6b24c75b 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Config } from "../config/config" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 237240418..4c6003b9e 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1,7 +1,7 @@ import { Log } from "../util/log" import path from "path" import os from "os" -import z from "zod/v4" +import z from "zod" import { Filesystem } from "../util/filesystem" import { ModelsDev } from "../provider/models" import { mergeDeep, pipe } from "remeda" diff --git a/packages/opencode/src/file/fzf.ts b/packages/opencode/src/file/fzf.ts index 7a35351fa..cd0aa4fc8 100644 --- a/packages/opencode/src/file/fzf.ts +++ b/packages/opencode/src/file/fzf.ts @@ -1,7 +1,7 @@ import path from "path" import { Global } from "../global" import fs from "fs/promises" -import z from "zod/v4" +import z from "zod" import { NamedError } from "../util/error" import { lazy } from "../util/lazy" import { Log } from "../util/log" diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 1478de86c..49eac54ac 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Bus } from "../bus" import { $ } from "bun" import type { BunFile } from "bun" diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index f575154e0..6e7f549a7 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -2,7 +2,7 @@ import path from "path" import { Global } from "../global" import fs from "fs/promises" -import z from "zod/v4" +import z from "zod" import { NamedError } from "../util/error" import { lazy } from "../util/lazy" import { $ } from "bun" @@ -218,7 +218,7 @@ export namespace Ripgrep { code: "ENOENT", errno: -2, path: input.cwd, - }); + }) } const proc = Bun.spawn(args, { diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 1bae71cfb..7d190c60b 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Bus } from "../bus" import { Flag } from "../flag/flag" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/id/id.ts b/packages/opencode/src/id/id.ts index ec24f30d9..99eb6c9ff 100644 --- a/packages/opencode/src/id/id.ts +++ b/packages/opencode/src/id/id.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { randomBytes } from "crypto" export namespace Identifier { diff --git a/packages/opencode/src/ide/index.ts b/packages/opencode/src/ide/index.ts index 7ab2e7ff4..ac80dac3e 100644 --- a/packages/opencode/src/ide/index.ts +++ b/packages/opencode/src/ide/index.ts @@ -1,5 +1,5 @@ import { spawn } from "bun" -import z from "zod/v4" +import z from "zod" import { NamedError } from "../util/error" import { Log } from "../util/log" import { Bus } from "../bus" diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 5e2a3cd33..19c6674cc 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -1,6 +1,6 @@ import path from "path" import { $ } from "bun" -import z from "zod/v4" +import z from "zod" import { NamedError } from "../util/error" import { Bus } from "../bus" import { Log } from "../util/log" diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index 8a6589680..2d36f454b 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -4,7 +4,7 @@ import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types import { Log } from "../util/log" import { LANGUAGE_EXTENSIONS } from "./language" import { Bus } from "../bus" -import z from "zod/v4" +import z from "zod" import type { LSPServer } from "./server" import { NamedError } from "../util/error" import { withTimeout } from "../util/timeout" diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index d811b2378..d533815fe 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -2,7 +2,7 @@ import { Log } from "../util/log" import { LSPClient } from "./client" import path from "path" import { LSPServer } from "./server" -import z from "zod/v4" +import z from "zod" import { Config } from "../config/config" import { spawn } from "child_process" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index dc5bb8b86..fa3513bb7 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -5,7 +5,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" import { Config } from "../config/config" import { Log } from "../util/log" import { NamedError } from "../util/error" -import z from "zod/v4" +import z from "zod" import { Session } from "../session" import { Bus } from "../bus" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index f4b178234..a36da6e81 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Bus } from "../bus" import { Log } from "../util/log" import { Identifier } from "../id/id" diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 88134483e..34bd4aea7 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Filesystem } from "../util/filesystem" import path from "path" import { $ } from "bun" diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 6003c701e..4981d38b3 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -1,7 +1,7 @@ import { Global } from "../global" import { Log } from "../util/log" import path from "path" -import z from "zod/v4" +import z from "zod" import { data } from "./models-macro" with { type: "macro" } import { Installation } from "../installation" diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index bc95e543b..c42b361b1 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import path from "path" import { Config } from "../config/config" import { mergeDeep, sortBy } from "remeda" diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 00c9475b7..550312c44 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -5,7 +5,7 @@ import { Hono } from "hono" import { cors } from "hono/cors" import { stream, streamSSE } from "hono/streaming" import { Session } from "../session" -import z from "zod/v4" +import z from "zod" import { Provider } from "../provider/provider" import { mapValues } from "remeda" import { NamedError } from "../util/error" diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index b23fa7cdd..d9ead5791 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -7,7 +7,7 @@ import { defer } from "../util/defer" import { MessageV2 } from "./message-v2" import { SystemPrompt } from "./system" import { Bus } from "../bus" -import z from "zod/v4" +import z from "zod" import type { ModelsDev } from "../provider/models" import { SessionPrompt } from "./prompt" import { Flag } from "../flag/flag" diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 640dd55c2..ee4cc704f 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -1,5 +1,5 @@ import { Decimal } from "decimal.js" -import z from "zod/v4" +import z from "zod" import { type LanguageModelUsage, type ProviderMetadata } from "ai" import PROMPT_INITIALIZE from "../session/prompt/initialize.txt" diff --git a/packages/opencode/src/session/lock.ts b/packages/opencode/src/session/lock.ts index 4b510dc97..ed024edab 100644 --- a/packages/opencode/src/session/lock.ts +++ b/packages/opencode/src/session/lock.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Instance } from "../project/instance" import { Log } from "../util/log" import { NamedError } from "../util/error" diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index c37773b74..e1a5c844f 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Bus } from "../bus" import { NamedError } from "../util/error" import { Message } from "./message" diff --git a/packages/opencode/src/session/message.ts b/packages/opencode/src/session/message.ts index f8b5115fb..4471f9235 100644 --- a/packages/opencode/src/session/message.ts +++ b/packages/opencode/src/session/message.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { NamedError } from "../util/error" export namespace Message { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b769d6052..7018978e2 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1,7 +1,7 @@ import path from "path" import os from "os" import fs from "fs/promises" -import z from "zod/v4" +import z from "zod" import { Identifier } from "../id/id" import { MessageV2 } from "./message-v2" import { Log } from "../util/log" diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 0b0f4294f..a88b5f08f 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Identifier } from "../id/id" import { Snapshot } from "../snapshot" import { MessageV2 } from "./message-v2" diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index d0b454f75..d52087739 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Bus } from "../bus" import { Storage } from "../storage/storage" diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 4301694a8..98b316804 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -3,7 +3,7 @@ import path from "path" import fs from "fs/promises" import { Log } from "../util/log" import { Global } from "../global" -import z from "zod/v4" +import z from "zod" import { Config } from "../config/config" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index d56b4690b..6a7af3811 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { spawn } from "child_process" import { Tool } from "./tool" import DESCRIPTION from "./bash.txt" diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index c993c5fbf..a8e6fc3b6 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -3,7 +3,7 @@ // https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts // https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts -import z from "zod/v4" +import z from "zod" import * as path from "path" import { Tool } from "./tool" import { LSP } from "../lsp" diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index 7553a5aa5..11c12f19a 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import path from "path" import { Tool } from "./tool" import DESCRIPTION from "./glob.txt" diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index cc654e339..a4d57b3d6 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Tool } from "./tool" import { Ripgrep } from "../file/ripgrep" diff --git a/packages/opencode/src/tool/invalid.ts b/packages/opencode/src/tool/invalid.ts index 318c4b134..728e9c89f 100644 --- a/packages/opencode/src/tool/invalid.ts +++ b/packages/opencode/src/tool/invalid.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Tool } from "./tool" export const InvalidTool = Tool.define("invalid", { diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts index b80f668a5..95c36e745 100644 --- a/packages/opencode/src/tool/ls.ts +++ b/packages/opencode/src/tool/ls.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Tool } from "./tool" import * as path from "path" import DESCRIPTION from "./ls.txt" diff --git a/packages/opencode/src/tool/lsp-diagnostics.ts b/packages/opencode/src/tool/lsp-diagnostics.ts index 6ea1b0593..18a6868b6 100644 --- a/packages/opencode/src/tool/lsp-diagnostics.ts +++ b/packages/opencode/src/tool/lsp-diagnostics.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Tool } from "./tool" import path from "path" import { LSP } from "../lsp" diff --git a/packages/opencode/src/tool/lsp-hover.ts b/packages/opencode/src/tool/lsp-hover.ts index 2999d17ae..7ef856cc5 100644 --- a/packages/opencode/src/tool/lsp-hover.ts +++ b/packages/opencode/src/tool/lsp-hover.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Tool } from "./tool" import path from "path" import { LSP } from "../lsp" diff --git a/packages/opencode/src/tool/multiedit.ts b/packages/opencode/src/tool/multiedit.ts index 2a1b2fbbb..7f562f473 100644 --- a/packages/opencode/src/tool/multiedit.ts +++ b/packages/opencode/src/tool/multiedit.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Tool } from "./tool" import { EditTool } from "./edit" import DESCRIPTION from "./multiedit.txt" diff --git a/packages/opencode/src/tool/patch.ts b/packages/opencode/src/tool/patch.ts index 8f3033080..118e0840c 100644 --- a/packages/opencode/src/tool/patch.ts +++ b/packages/opencode/src/tool/patch.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import * as path from "path" import * as fs from "fs/promises" import { Tool } from "./tool" @@ -17,7 +17,8 @@ const PatchParams = z.object({ }) export const PatchTool = Tool.define("patch", { - description: "Apply a patch to modify multiple files. Supports adding, updating, and deleting files with context-aware changes.", + description: + "Apply a patch to modify multiple files. Supports adding, updating, and deleting files with context-aware changes.", parameters: PatchParams, async execute(params, ctx) { if (!params.patchText) { @@ -46,12 +47,12 @@ export const PatchTool = Tool.define("patch", { type: "add" | "update" | "delete" | "move" movePath?: string }> = [] - + let totalDiff = "" for (const hunk of hunks) { const filePath = path.resolve(Instance.directory, hunk.path) - + if (!Filesystem.contains(Instance.directory, filePath)) { throw new Error(`File ${filePath} is not in the current working directory`) } @@ -62,30 +63,30 @@ export const PatchTool = Tool.define("patch", { const oldContent = "" const newContent = hunk.contents const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent) - + fileChanges.push({ filePath, oldContent, newContent, type: "add", }) - + totalDiff += diff + "\n" } break - + case "update": // Check if file exists for update const stats = await fs.stat(filePath).catch(() => null) if (!stats || stats.isDirectory()) { throw new Error(`File not found or is directory: ${filePath}`) } - + // Read file and update time tracking (like edit tool does) await FileTime.assert(ctx.sessionID, filePath) const oldContent = await fs.readFile(filePath, "utf-8") let newContent = oldContent - + // Apply the update chunks to get new content try { const fileUpdate = Patch.deriveNewContentsFromChunks(filePath, hunk.chunks) @@ -93,9 +94,9 @@ export const PatchTool = Tool.define("patch", { } catch (error) { throw new Error(`Failed to apply update to ${filePath}: ${error}`) } - + const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent) - + fileChanges.push({ filePath, oldContent, @@ -103,23 +104,23 @@ export const PatchTool = Tool.define("patch", { type: hunk.move_path ? "move" : "update", movePath: hunk.move_path ? path.resolve(Instance.directory, hunk.move_path) : undefined, }) - + totalDiff += diff + "\n" break - + case "delete": // Check if file exists for deletion await FileTime.assert(ctx.sessionID, filePath) const contentToDelete = await fs.readFile(filePath, "utf-8") const deleteDiff = createTwoFilesPatch(filePath, filePath, contentToDelete, "") - + fileChanges.push({ filePath, oldContent: contentToDelete, newContent: "", type: "delete", }) - + totalDiff += deleteDiff + "\n" break } @@ -141,7 +142,7 @@ export const PatchTool = Tool.define("patch", { // Apply the changes const changedFiles: string[] = [] - + for (const change of fileChanges) { switch (change.type) { case "add": @@ -153,12 +154,12 @@ export const PatchTool = Tool.define("patch", { await fs.writeFile(change.filePath, change.newContent, "utf-8") changedFiles.push(change.filePath) break - + case "update": await fs.writeFile(change.filePath, change.newContent, "utf-8") changedFiles.push(change.filePath) break - + case "move": if (change.movePath) { // Create parent directories for destination @@ -173,13 +174,13 @@ export const PatchTool = Tool.define("patch", { changedFiles.push(change.movePath) } break - + case "delete": await fs.unlink(change.filePath) changedFiles.push(change.filePath) break } - + // Update file time tracking FileTime.read(ctx.sessionID, change.filePath) if (change.movePath) { @@ -193,7 +194,7 @@ export const PatchTool = Tool.define("patch", { } // Generate output summary - const relativePaths = changedFiles.map(filePath => path.relative(Instance.worktree, filePath)) + const relativePaths = changedFiles.map((filePath) => path.relative(Instance.worktree, filePath)) const summary = `${fileChanges.length} files changed` return { @@ -201,7 +202,7 @@ export const PatchTool = Tool.define("patch", { metadata: { diff: totalDiff, }, - output: `Patch applied successfully. ${summary}:\n${relativePaths.map(p => ` ${p}`).join("\n")}`, + output: `Patch applied successfully. ${summary}:\n${relativePaths.map((p) => ` ${p}`).join("\n")}`, } }, -}) \ No newline at end of file +}) diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 5e8cecaf2..bc89dae2c 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import * as fs from "fs" import * as path from "path" import { Tool } from "./tool" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 1d6372090..4ea70f289 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -16,7 +16,7 @@ import { Instance } from "../project/instance" import { Config } from "../config/config" import path from "path" import { type ToolDefinition } from "@opencode-ai/plugin" -import z from "zod/v4" +import z from "zod" import { Plugin } from "../plugin" export namespace ToolRegistry { diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 95f650e01..830c298a5 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -1,6 +1,6 @@ import { Tool } from "./tool" import DESCRIPTION from "./task.txt" -import z from "zod/v4" +import z from "zod" import { Session } from "../session" import { Bus } from "../bus" import { MessageV2 } from "../session/message-v2" diff --git a/packages/opencode/src/tool/todo.ts b/packages/opencode/src/tool/todo.ts index 63180eb6e..fffe9d107 100644 --- a/packages/opencode/src/tool/todo.ts +++ b/packages/opencode/src/tool/todo.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Tool } from "./tool" import DESCRIPTION_WRITE from "./todowrite.txt" import { Todo } from "../session/todo" diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 2fc72274d..978c9c072 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import type { MessageV2 } from "../session/message-v2" export namespace Tool { diff --git a/packages/opencode/src/tool/webfetch.ts b/packages/opencode/src/tool/webfetch.ts index 71b09cd95..0333bb018 100644 --- a/packages/opencode/src/tool/webfetch.ts +++ b/packages/opencode/src/tool/webfetch.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import { Tool } from "./tool" import TurndownService from "turndown" import DESCRIPTION from "./webfetch.txt" diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index aa79c9bfb..fb7e2fe03 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" import * as path from "path" import { Tool } from "./tool" import { LSP } from "../lsp" diff --git a/packages/opencode/src/util/error.ts b/packages/opencode/src/util/error.ts index 6e5414f46..d488abb6c 100644 --- a/packages/opencode/src/util/error.ts +++ b/packages/opencode/src/util/error.ts @@ -1,4 +1,4 @@ -import z from "zod/v4" +import z from "zod" export abstract class NamedError extends Error { abstract schema(): z.core.$ZodType diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 5844a114f..463069562 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -1,7 +1,7 @@ import path from "path" import fs from "fs/promises" import { Global } from "../global" -import z from "zod/v4" +import z from "zod" export namespace Log { export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) diff --git a/packages/plugin/src/tool.ts b/packages/plugin/src/tool.ts index 2998a1e72..37e802ac4 100644 --- a/packages/plugin/src/tool.ts +++ b/packages/plugin/src/tool.ts @@ -1,4 +1,4 @@ -import { z } from "zod/v4" +import { z } from "zod" export type ToolContext = { sessionID: string From 5162268f9dc536293445bdfbf5af97a8eba1c884 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 26 Oct 2025 15:04:48 -0500 Subject: [PATCH 16/56] docs: update agent frontmatter permission example --- packages/web/src/content/docs/agents.mdx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index e15ba6cb1..c99988ad8 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -389,7 +389,10 @@ description: Code review without edits mode: subagent permission: edit: deny - bash: ask + bash: + "git diff": allow + "git log*": allow + "*": ask webfetch: deny --- From 5e886c35d59a31da5268a8442ff335acef3604e0 Mon Sep 17 00:00:00 2001 From: Denys Rybalka Date: Mon, 27 Oct 2025 01:50:45 +0100 Subject: [PATCH 17/56] chore: use stable URLs in PKGBUILD (#3448) --- packages/opencode/script/publish.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index 3bdf6a49d..ef35b8514 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -51,13 +51,16 @@ if (!Script.preview) { const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim()) const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim()) + const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2) + // arch const binaryPkgbuild = [ "# Maintainer: dax", "# Maintainer: adam", "", "pkgname='opencode-bin'", - `pkgver=${Script.version.split("-")[0]}`, + `pkgver=${pkgver}`, + `_subver=${_subver}`, "options=('!debug' '!strip')", "pkgrel=1", "pkgdesc='The AI coding agent built for the terminal.'", @@ -68,10 +71,10 @@ if (!Script.preview) { "conflicts=('opencode')", "depends=('fzf' 'ripgrep')", "", - `source_aarch64=("\${pkgname}_\${pkgver}_aarch64.zip::https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-arm64.zip")`, + `source_aarch64=("\${pkgname}_\${pkgver}_aarch64.zip::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.zip")`, `sha256sums_aarch64=('${arm64Sha}')`, "", - `source_x86_64=("\${pkgname}_\${pkgver}_x86_64.zip::https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-x64.zip")`, + `source_x86_64=("\${pkgname}_\${pkgver}_x86_64.zip::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.zip")`, `sha256sums_x86_64=('${x64Sha}')`, "", "package() {", @@ -86,7 +89,8 @@ if (!Script.preview) { "# Maintainer: adam", "", "pkgname='opencode'", - `pkgver=${Script.version.split("-")[0]}`, + `pkgver=${pkgver}`, + `_subver=${_subver}`, "options=('!debug' '!strip')", "pkgrel=1", "pkgdesc='The AI coding agent built for the terminal.'", @@ -98,7 +102,7 @@ if (!Script.preview) { "depends=('fzf' 'ripgrep')", "makedepends=('git' 'bun-bin' 'go')", "", - `source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v${Script.version}.tar.gz")`, + `source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`, `sha256sums=('SKIP')`, "", "build() {", From 316d4c9197417c83d4076037d09905a5390da06e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 25 Oct 2025 22:04:49 -0500 Subject: [PATCH 18/56] wip --- packages/web/src/content/docs/acp.mdx | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/web/src/content/docs/acp.mdx diff --git a/packages/web/src/content/docs/acp.mdx b/packages/web/src/content/docs/acp.mdx new file mode 100644 index 000000000..379a6d84b --- /dev/null +++ b/packages/web/src/content/docs/acp.mdx @@ -0,0 +1,4 @@ +--- +title: Agent Client Protocol (ACP) +description: use +--- From a9624c0fffcab843e05817aac92ae1d28f571d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haris=20Gu=C5=A1i=C4=87?= Date: Mon, 27 Oct 2025 06:03:10 +0100 Subject: [PATCH 19/56] fix: Explicitly exit CLI to prevent hanging subprocesses (#3083) Co-authored-by: Aiden Cline --- packages/opencode/src/file/watcher.ts | 3 +- packages/opencode/src/index.ts | 10 +++-- packages/opencode/src/lsp/index.ts | 4 +- packages/opencode/src/mcp/index.ts | 4 +- packages/opencode/src/project/bootstrap.ts | 2 +- packages/opencode/src/project/state.ts | 50 ++++++++++++++++++---- packages/opencode/src/session/prompt.ts | 49 ++++++++------------- 7 files changed, 71 insertions(+), 51 deletions(-) diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 7d190c60b..d5985b582 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -63,7 +63,8 @@ export namespace FileWatcher { return { sub } }, async (state) => { - state.sub?.unsubscribe() + if (!state.sub) return + await state.sub?.unsubscribe() }, ) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index a0cce76ad..45ccd3cad 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -22,8 +22,6 @@ import { AttachCommand } from "./cli/cmd/attach" import { AcpCommand } from "./cli/cmd/acp" import { EOL } from "os" -const cancel = new AbortController() - process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { e: e instanceof Error ? e.message : e, @@ -135,6 +133,10 @@ try { console.error(e) } process.exitCode = 1 +} finally { + // Some subprocesses don't react properly to SIGTERM and similar signals. + // Most notably, some docker-container-based MCP servers don't handle such signals unless + // run using `docker run --init`. + // Explicitly exit to avoid any hanging subprocesses. + process.exit(); } - -cancel.abort() diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index d533815fe..72a9cae21 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -101,9 +101,7 @@ export namespace LSP { } }, async (state) => { - for (const client of state.clients) { - await client.shutdown() - } + await Promise.all(state.clients.map((client) => client.shutdown())) }, ) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index fa3513bb7..b0e72b53a 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -145,9 +145,7 @@ export namespace MCP { } }, async (state) => { - for (const client of Object.values(state.clients)) { - client.close() - } + await Promise.all(Object.values(state.clients).map((client) => client.close())) }, ) diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index c7805aa7a..45e85fd24 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -11,7 +11,7 @@ export async function InstanceBootstrap() { await Plugin.init() Share.init() Format.init() - LSP.init() + await LSP.init() FileWatcher.init() File.init() } diff --git a/packages/opencode/src/project/state.ts b/packages/opencode/src/project/state.ts index 2ffef3b39..6377833eb 100644 --- a/packages/opencode/src/project/state.ts +++ b/packages/opencode/src/project/state.ts @@ -1,23 +1,26 @@ +import { Log } from "@/util/log" + export namespace State { interface Entry { state: any dispose?: (state: any) => Promise } - const entries = new Map>() + const log = Log.create({ service: "state" }) + const recordsByKey = new Map>() export function create(root: () => string, init: () => S, dispose?: (state: Awaited) => Promise) { return () => { const key = root() - let collection = entries.get(key) - if (!collection) { - collection = new Map() - entries.set(key, collection) + let entries = recordsByKey.get(key) + if (!entries) { + entries = new Map() + recordsByKey.set(key, entries) } - const exists = collection.get(init) + const exists = entries.get(init) if (exists) return exists.state as S const state = init() - collection.set(init, { + entries.set(init, { state, dispose, }) @@ -26,9 +29,38 @@ export namespace State { } export async function dispose(key: string) { - for (const [_, entry] of entries.get(key)?.entries() ?? []) { + const entries = recordsByKey.get(key) + if (!entries) return + + log.info("waiting for state disposal to complete", { key }) + + let disposalFinished = false + + setTimeout(() => { + if (!disposalFinished) { + log.warn( + "state disposal is taking an unusually long time - if it does not complete in a reasonable time, please report this as a bug", + { key }, + ) + } + }, 10000).unref() + + const tasks: Promise[] = [] + for (const entry of entries.values()) { if (!entry.dispose) continue - await entry.dispose(await entry.state) + + const task = Promise.resolve(entry.state) + .then((state) => entry.dispose!(state)) + .catch((error) => { + log.error("Error while disposing state:", { error, key }) + }) + + tasks.push(task) } + + await Promise.all(tasks) + + disposalFinished = true + log.info("state disposal completed", { key }) } } diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 7018978e2..184f4af85 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -74,13 +74,22 @@ export namespace SessionPrompt { callback: (input: MessageV2.WithParts) => void }[] >() + const pending = new Set>() + + const track = (promise: Promise) => { + pending.add(promise) + promise.finally(() => pending.delete(promise)) + } return { queued, + pending, + track, } }, async (current) => { current.queued.clear() + await Promise.allSettled([...current.pending]) }, ) @@ -191,28 +200,6 @@ export namespace SessionPrompt { processor, }) - // const permUnsub = (() => { - // const handled = new Set() - // const options = [ - // { optionId: "allow_once", kind: "allow_once", name: "Allow once" }, - // { optionId: "allow_always", kind: "allow_always", name: "Always allow" }, - // { optionId: "reject_once", kind: "reject_once", name: "Reject" }, - // ] - // return Bus.subscribe(Permission.Event.Updated, async (event) => { - // const info = event.properties - // if (info.sessionID !== input.sessionID) return - // if (handled.has(info.id)) return - // handled.add(info.id) - // const toolCallId = info.callID ?? info.id - // const metadata = info.metadata ?? {} - // // TODO: emit permission event to bus for ACP to handle - // Permission.respond({ sessionID: info.sessionID, permissionID: info.id, response: "reject" }) - // }) - // })() - // await using _permSub = defer(() => { - // permUnsub?.() - // }) - const params = await Plugin.trigger( "chat.params", { @@ -247,13 +234,15 @@ export namespace SessionPrompt { step++ await processor.next(msgs.findLast((m) => m.info.role === "user")?.info.id!) if (step === 1) { - ensureTitle({ - session, - history: msgs, - message: userMsg, - providerID: model.providerID, - modelID: model.info.id, - }) + state().track( + ensureTitle({ + session, + history: msgs, + message: userMsg, + providerID: model.providerID, + modelID: model.info.id, + }), + ) SessionSummary.summarize({ sessionID: input.sessionID, messageID: userMsg.info.id, @@ -1730,7 +1719,7 @@ export namespace SessionPrompt { thinkingBudget: 0, } } - generateText({ + await generateText({ maxOutputTokens: small.info.reasoning ? 1500 : 20, providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options), messages: [ From fdb5bae3c6578c41ffb5bf3140879f79a586d164 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 27 Oct 2025 00:56:00 -0500 Subject: [PATCH 20/56] docs: acp --- packages/web/astro.config.mjs | 1 + packages/web/src/content/docs/acp.mdx | 103 +++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index 3bcd98cb7..484807497 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -86,6 +86,7 @@ export default defineConfig({ "lsp", "mcp-servers", "custom-tools", + "acp", ], }, diff --git a/packages/web/src/content/docs/acp.mdx b/packages/web/src/content/docs/acp.mdx index 379a6d84b..ea741faca 100644 --- a/packages/web/src/content/docs/acp.mdx +++ b/packages/web/src/content/docs/acp.mdx @@ -1,4 +1,103 @@ --- -title: Agent Client Protocol (ACP) -description: use +title: Agent Client Protocol +description: Use OpenCode in any ACP-compatible editor. --- + +OpenCode supports the [Agent Client Protocol (ACP)](https://agentclientprotocol.com), allowing you to use it directly in compatible editors and IDEs. + +ACP is an open protocol that standardizes communication between code editors and AI coding agents. Similar to LSP for language servers, ACP allows agents like OpenCode to work seamlessly across different development environments. + +:::tip +For a list of editors and tools that support ACP, see the [ACP progress report](https://zed.dev/blog/acp-progress-report#available-now). +::: + +--- + +## Configure + +To use OpenCode via ACP, configure your editor to run the `opencode acp` command. + +The command starts OpenCode as an ACP-compatible subprocess that communicates with your editor over JSON-RPC via stdio. + +Below are examples for popular editors that support ACP. + +--- + +### Zed + +Add to your Zed configuration (`~/.config/zed/settings.json`): + +```json +{ + "agent_servers": { + "OpenCode": { + "command": "opencode", + "args": ["acp"] + } + } +} +``` + +To open it, use the `agent: new thread` action in the Command Palette + +You can also bind a keyboard shortcut by editing your `keymap.json`: + +```json +[ + { + "bindings": { + "cmd-alt-o": ["agent::NewExternalAgentThread", { "agent": "OpenCode" }] + } + } +] +``` + +--- + +### Avante.nvim + +Add to your Avante configuration: + +```lua +{ + acp_providers = { + ["opencode"] = { + command = "opencode", + args = { "acp" } + } + } +} +``` + +If you need to pass environment variables: + +```lua +{ + acp_providers = { + ["opencode"] = { + command = "opencode", + args = { "acp" }, + env = { + OPENCODE_API_KEY = os.getenv("OPENCODE_API_KEY") + } + } + } +} +``` + +--- + +## Capabilities + +OpenCode works the same via ACP as it does in the terminal. All features are supported: + +- Built-in tools (file operations, terminal commands, etc.) +- Custom tools and slash commands +- MCP servers configured in your OpenCode config +- Project-specific rules from `AGENTS.md` +- Custom formatters and linters +- Agents and permissions system + +:::note +Some built-in slash commands like `/undo` and `/redo` are currently unsupported in ACP mode. +::: From 1a6fd018f6d6b06c9d8f5fba4fe31b5d1c7b0de2 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 27 Oct 2025 01:30:13 -0500 Subject: [PATCH 21/56] Revert "fix: Explicitly exit CLI to prevent hanging subprocesses (#3083)" This reverts commit a9624c0fffcab843e05817aac92ae1d28f571d0d. --- packages/opencode/src/file/watcher.ts | 3 +- packages/opencode/src/index.ts | 10 ++--- packages/opencode/src/lsp/index.ts | 4 +- packages/opencode/src/mcp/index.ts | 4 +- packages/opencode/src/project/bootstrap.ts | 2 +- packages/opencode/src/project/state.ts | 50 ++++------------------ packages/opencode/src/session/prompt.ts | 49 +++++++++++++-------- 7 files changed, 51 insertions(+), 71 deletions(-) diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index d5985b582..7d190c60b 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -63,8 +63,7 @@ export namespace FileWatcher { return { sub } }, async (state) => { - if (!state.sub) return - await state.sub?.unsubscribe() + state.sub?.unsubscribe() }, ) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 45ccd3cad..a0cce76ad 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -22,6 +22,8 @@ import { AttachCommand } from "./cli/cmd/attach" import { AcpCommand } from "./cli/cmd/acp" import { EOL } from "os" +const cancel = new AbortController() + process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { e: e instanceof Error ? e.message : e, @@ -133,10 +135,6 @@ try { console.error(e) } process.exitCode = 1 -} finally { - // Some subprocesses don't react properly to SIGTERM and similar signals. - // Most notably, some docker-container-based MCP servers don't handle such signals unless - // run using `docker run --init`. - // Explicitly exit to avoid any hanging subprocesses. - process.exit(); } + +cancel.abort() diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 72a9cae21..d533815fe 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -101,7 +101,9 @@ export namespace LSP { } }, async (state) => { - await Promise.all(state.clients.map((client) => client.shutdown())) + for (const client of state.clients) { + await client.shutdown() + } }, ) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index b0e72b53a..fa3513bb7 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -145,7 +145,9 @@ export namespace MCP { } }, async (state) => { - await Promise.all(Object.values(state.clients).map((client) => client.close())) + for (const client of Object.values(state.clients)) { + client.close() + } }, ) diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 45e85fd24..c7805aa7a 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -11,7 +11,7 @@ export async function InstanceBootstrap() { await Plugin.init() Share.init() Format.init() - await LSP.init() + LSP.init() FileWatcher.init() File.init() } diff --git a/packages/opencode/src/project/state.ts b/packages/opencode/src/project/state.ts index 6377833eb..2ffef3b39 100644 --- a/packages/opencode/src/project/state.ts +++ b/packages/opencode/src/project/state.ts @@ -1,26 +1,23 @@ -import { Log } from "@/util/log" - export namespace State { interface Entry { state: any dispose?: (state: any) => Promise } - const log = Log.create({ service: "state" }) - const recordsByKey = new Map>() + const entries = new Map>() export function create(root: () => string, init: () => S, dispose?: (state: Awaited) => Promise) { return () => { const key = root() - let entries = recordsByKey.get(key) - if (!entries) { - entries = new Map() - recordsByKey.set(key, entries) + let collection = entries.get(key) + if (!collection) { + collection = new Map() + entries.set(key, collection) } - const exists = entries.get(init) + const exists = collection.get(init) if (exists) return exists.state as S const state = init() - entries.set(init, { + collection.set(init, { state, dispose, }) @@ -29,38 +26,9 @@ export namespace State { } export async function dispose(key: string) { - const entries = recordsByKey.get(key) - if (!entries) return - - log.info("waiting for state disposal to complete", { key }) - - let disposalFinished = false - - setTimeout(() => { - if (!disposalFinished) { - log.warn( - "state disposal is taking an unusually long time - if it does not complete in a reasonable time, please report this as a bug", - { key }, - ) - } - }, 10000).unref() - - const tasks: Promise[] = [] - for (const entry of entries.values()) { + for (const [_, entry] of entries.get(key)?.entries() ?? []) { if (!entry.dispose) continue - - const task = Promise.resolve(entry.state) - .then((state) => entry.dispose!(state)) - .catch((error) => { - log.error("Error while disposing state:", { error, key }) - }) - - tasks.push(task) + await entry.dispose(await entry.state) } - - await Promise.all(tasks) - - disposalFinished = true - log.info("state disposal completed", { key }) } } diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 184f4af85..7018978e2 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -74,22 +74,13 @@ export namespace SessionPrompt { callback: (input: MessageV2.WithParts) => void }[] >() - const pending = new Set>() - - const track = (promise: Promise) => { - pending.add(promise) - promise.finally(() => pending.delete(promise)) - } return { queued, - pending, - track, } }, async (current) => { current.queued.clear() - await Promise.allSettled([...current.pending]) }, ) @@ -200,6 +191,28 @@ export namespace SessionPrompt { processor, }) + // const permUnsub = (() => { + // const handled = new Set() + // const options = [ + // { optionId: "allow_once", kind: "allow_once", name: "Allow once" }, + // { optionId: "allow_always", kind: "allow_always", name: "Always allow" }, + // { optionId: "reject_once", kind: "reject_once", name: "Reject" }, + // ] + // return Bus.subscribe(Permission.Event.Updated, async (event) => { + // const info = event.properties + // if (info.sessionID !== input.sessionID) return + // if (handled.has(info.id)) return + // handled.add(info.id) + // const toolCallId = info.callID ?? info.id + // const metadata = info.metadata ?? {} + // // TODO: emit permission event to bus for ACP to handle + // Permission.respond({ sessionID: info.sessionID, permissionID: info.id, response: "reject" }) + // }) + // })() + // await using _permSub = defer(() => { + // permUnsub?.() + // }) + const params = await Plugin.trigger( "chat.params", { @@ -234,15 +247,13 @@ export namespace SessionPrompt { step++ await processor.next(msgs.findLast((m) => m.info.role === "user")?.info.id!) if (step === 1) { - state().track( - ensureTitle({ - session, - history: msgs, - message: userMsg, - providerID: model.providerID, - modelID: model.info.id, - }), - ) + ensureTitle({ + session, + history: msgs, + message: userMsg, + providerID: model.providerID, + modelID: model.info.id, + }) SessionSummary.summarize({ sessionID: input.sessionID, messageID: userMsg.info.id, @@ -1719,7 +1730,7 @@ export namespace SessionPrompt { thinkingBudget: 0, } } - await generateText({ + generateText({ maxOutputTokens: small.info.reasoning ? 1500 : 20, providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options), messages: [ From db85f01effc3b395b734e8b36c9a8f4c912c4edd Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 27 Oct 2025 12:04:50 +0000 Subject: [PATCH 22/56] ignore: update download stats 2025-10-27 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index fd1a15446..cd00e57e0 100644 --- a/STATS.md +++ b/STATS.md @@ -120,3 +120,4 @@ | 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) | | 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) | | 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) | +| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) | From b562863fcc7ff760130fa487d2f00054a9e8d93b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernat=20Peric=C3=A0s?= Date: Mon, 27 Oct 2025 13:18:23 +0100 Subject: [PATCH 23/56] feat: add `session.started` event that triggers when a new session is created (#3413) --- packages/opencode/src/session/index.ts | 9 +++ .../opencode/test/session/session.test.ts | 71 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 packages/opencode/test/session/session.test.ts diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index ee4cc704f..b4785c536 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -78,6 +78,12 @@ export namespace Session { export type ShareInfo = z.output export const Event = { + Started: Bus.event( + "session.started", + z.object({ + info: Info, + }), + ), Updated: Bus.event( "session.updated", z.object({ @@ -167,6 +173,9 @@ export namespace Session { } log.info("created", result) await Storage.write(["session", Instance.project.id, result.id], result) + Bus.publish(Event.Started, { + info: result, + }) const cfg = await Config.get() if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto")) share(result.id) diff --git a/packages/opencode/test/session/session.test.ts b/packages/opencode/test/session/session.test.ts new file mode 100644 index 000000000..573c9e59e --- /dev/null +++ b/packages/opencode/test/session/session.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from "bun:test" +import path from "path" +import { Session } from "../../src/session" +import { Bus } from "../../src/bus" +import { Log } from "../../src/util/log" +import { Instance } from "../../src/project/instance" + +const projectRoot = path.join(__dirname, "../..") +Log.init({ print: false }) + +describe("session.started event", () => { + test("should emit session.started event when session is created", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + let eventReceived = false + let receivedInfo: Session.Info | undefined + + const unsub = Bus.subscribe(Session.Event.Started, (event) => { + eventReceived = true + receivedInfo = event.properties.info as Session.Info + }) + + const session = await Session.create({}) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + unsub() + + expect(eventReceived).toBe(true) + expect(receivedInfo).toBeDefined() + expect(receivedInfo?.id).toBe(session.id) + expect(receivedInfo?.projectID).toBe(session.projectID) + expect(receivedInfo?.directory).toBe(session.directory) + expect(receivedInfo?.title).toBe(session.title) + + await Session.remove(session.id) + }, + }) + }) + + test("session.started event should be emitted before session.updated", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const events: string[] = [] + + const unsubStarted = Bus.subscribe(Session.Event.Started, () => { + events.push("started") + }) + + const unsubUpdated = Bus.subscribe(Session.Event.Updated, () => { + events.push("updated") + }) + + const session = await Session.create({}) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + unsubStarted() + unsubUpdated() + + expect(events).toContain("started") + expect(events).toContain("updated") + expect(events.indexOf("started")).toBeLessThan(events.indexOf("updated")) + + await Session.remove(session.id) + }, + }) + }) +}) From e6301ca5d514d81010564a0c46f00b1ce441bbcb Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 27 Oct 2025 10:42:47 -0500 Subject: [PATCH 24/56] tweak: rename event --- packages/opencode/src/session/index.ts | 6 +++--- packages/opencode/test/session/session.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index b4785c536..ff9f436e2 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -78,8 +78,8 @@ export namespace Session { export type ShareInfo = z.output export const Event = { - Started: Bus.event( - "session.started", + Created: Bus.event( + "session.created", z.object({ info: Info, }), @@ -173,7 +173,7 @@ export namespace Session { } log.info("created", result) await Storage.write(["session", Instance.project.id, result.id], result) - Bus.publish(Event.Started, { + Bus.publish(Event.Created, { info: result, }) const cfg = await Config.get() diff --git a/packages/opencode/test/session/session.test.ts b/packages/opencode/test/session/session.test.ts index 573c9e59e..219cef127 100644 --- a/packages/opencode/test/session/session.test.ts +++ b/packages/opencode/test/session/session.test.ts @@ -16,7 +16,7 @@ describe("session.started event", () => { let eventReceived = false let receivedInfo: Session.Info | undefined - const unsub = Bus.subscribe(Session.Event.Started, (event) => { + const unsub = Bus.subscribe(Session.Event.Created, (event) => { eventReceived = true receivedInfo = event.properties.info as Session.Info }) @@ -45,7 +45,7 @@ describe("session.started event", () => { fn: async () => { const events: string[] = [] - const unsubStarted = Bus.subscribe(Session.Event.Started, () => { + const unsubStarted = Bus.subscribe(Session.Event.Created, () => { events.push("started") }) From 0e65700183c6ed6b008112ee9638ce7621d6347a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 27 Oct 2025 10:47:04 -0500 Subject: [PATCH 25/56] update sdk --- packages/sdk/js/src/gen/types.gen.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 47bb20972..0b55cc5b4 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1197,6 +1197,13 @@ export type EventSessionIdle = { } } +export type EventSessionCreated = { + type: "session.created" + properties: { + info: Session + } +} + export type EventSessionUpdated = { type: "session.updated" properties: { @@ -1247,6 +1254,7 @@ export type Event = | EventFileWatcherUpdated | EventTodoUpdated | EventSessionIdle + | EventSessionCreated | EventSessionUpdated | EventSessionDeleted | EventSessionError From a606e1d2ec9d634f9b8468e2f6c0ce4259d5afa0 Mon Sep 17 00:00:00 2001 From: Aurelien Ribon Date: Mon, 27 Oct 2025 16:50:57 +0100 Subject: [PATCH 26/56] fix: dont set reasoning effort to `medium` for `gpt-5-pro` (#3474) --- packages/opencode/src/provider/transform.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 73ff442ab..dda02cc4e 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -92,7 +92,9 @@ export namespace ProviderTransform { } if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) { - if (!modelID.includes("codex")) result["reasoningEffort"] = "medium" + if (!modelID.includes("codex") && !modelID.includes("gpt-5-pro")) { + result["reasoningEffort"] = "medium" + } if (providerID !== "azure") { result["textVerbosity"] = modelID.includes("codex") ? "medium" : "low" From 0af450575647fc906f017b0065fe3aca227c369f Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 27 Oct 2025 14:03:30 -0500 Subject: [PATCH 27/56] fix: litellm error tool= param must be specified --- packages/opencode/src/session/compaction.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index d9ead5791..76313453f 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -155,6 +155,7 @@ export namespace SessionCompaction { error, }) }, + tools: model.info.tool_call ? {} : undefined, messages: [ ...system.map( (x): ModelMessage => ({ From 0acae8211afb1194035a1801a7cd9d9ed1d66eae Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 27 Oct 2025 06:48:03 -0500 Subject: [PATCH 28/56] wip: desktop work --- bun.lock | 6 +++--- package.json | 6 +++--- packages/desktop/src/components/diff.tsx | 27 +++++++++++++++++++----- packages/ui/package.json | 2 +- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/bun.lock b/bun.lock index 8dcbb2b27..d2959d4c5 100644 --- a/bun.lock +++ b/bun.lock @@ -276,7 +276,7 @@ "version": "0.15.18", "dependencies": { "@kobalte/core": "catalog:", - "@pierre/precision-diffs": "0.0.2-alpha.1-1", + "@pierre/precision-diffs": "catalog:", "@solidjs/meta": "catalog:", "fuzzysort": "catalog:", "luxon": "catalog:", @@ -343,7 +343,7 @@ "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/precision-diffs": "0.0.2-alpha.1-1", + "@pierre/precision-diffs": "0.3.2", "@solidjs/meta": "0.29.4", "@tailwindcss/vite": "4.1.11", "@tsconfig/bun": "1.0.9", @@ -937,7 +937,7 @@ "@petamoriken/float16": ["@petamoriken/float16@3.9.2", "", {}, "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog=="], - "@pierre/precision-diffs": ["@pierre/precision-diffs@0.0.2-alpha.1-1", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/transformers": "3.13.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.13.0" } }, "sha512-T43cwB7gMnbM+tp9p73NptUm4uUOfmrP5ihMOAHWQPpzBa/oeTjqZlmEmSQLpT8WKKnWG0lbKZPtlw7l0gW0Vw=="], + "@pierre/precision-diffs": ["@pierre/precision-diffs@0.3.2", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/transformers": "3.13.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.13.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-HE+wFB0TV+wmjur/J+qI5PsRQl5RN6tCEFTusW0S5FDfZJUIpkxJCacqUxyEI0DriXMKhgGQ+oCQShfaFELdrQ=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], diff --git a/package.json b/package.json index 48afdc7c0..a5e521797 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@tsconfig/bun": "1.0.9", "@cloudflare/workers-types": "4.20251008.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/precision-diffs": "0.0.2-alpha.1-1", + "@pierre/precision-diffs": "0.3.2", "@solidjs/meta": "0.29.4", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", @@ -53,8 +53,8 @@ "turbo": "2.5.6" }, "dependencies": { - "@opencode-ai/sdk": "workspace:*", - "@opencode-ai/script": "workspace:*" + "@opencode-ai/script": "workspace:*", + "@opencode-ai/sdk": "workspace:*" }, "repository": { "type": "git", diff --git a/packages/desktop/src/components/diff.tsx b/packages/desktop/src/components/diff.tsx index d8457b528..b3d6d7e25 100644 --- a/packages/desktop/src/components/diff.tsx +++ b/packages/desktop/src/components/diff.tsx @@ -2,8 +2,9 @@ import { type FileContents, FileDiff, type DiffLineAnnotation, + type HunkData, DiffFileRendererOptions, - registerCustomTheme, + // registerCustomTheme, } from "@pierre/precision-diffs" import { ComponentProps, createEffect, splitProps } from "solid-js" @@ -15,8 +16,7 @@ export type DiffProps = Omit, "themes"> & { classList?: ComponentProps<"div">["classList"] } -// @ts-expect-error -registerCustomTheme("opencode", () => import("./theme.json")) +// registerCustomTheme("opencode", () => import("./theme.json")) // interface ThreadMetadata { // threadId: string @@ -49,7 +49,7 @@ export function Diff(props: DiffProps) { // annotations and a container element to hold the diff createEffect(() => { const instance = new FileDiff({ - theme: "opencode", + theme: "pierre-light", // Or can also provide a 'themes' prop, which allows the code to adapt // to your OS light or dark theme // themes: { dark: 'pierre-night', light: 'pierre-light' }, @@ -97,7 +97,24 @@ export function Diff(props: DiffProps) { // // 'simple': // Just a subtle bar separator between each hunk - hunkSeparators: "line-info", + // hunkSeparators: "line-info", + hunkSeparators(hunkData: HunkData) { + const fragment = document.createDocumentFragment() + const numCol = document.createElement("div") + numCol.textContent = `${hunkData.lines}` + numCol.style.position = "sticky" + numCol.style.left = "0" + numCol.style.backgroundColor = "var(--pjs-bg)" + numCol.style.zIndex = "2" + fragment.appendChild(numCol) + const contentCol = document.createElement("div") + contentCol.textContent = "unmodified lines" + contentCol.style.position = "sticky" + contentCol.style.width = "var(--pjs-column-content-width)" + contentCol.style.left = "var(--pjs-column-number-width)" + fragment.appendChild(contentCol) + return fragment + }, // On lines that have both additions and deletions, we can run a // separate diff check to mark parts of the lines that change. // 'none': diff --git a/packages/ui/package.json b/packages/ui/package.json index bf75293aa..cdb9eee1c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@kobalte/core": "catalog:", - "@pierre/precision-diffs": "0.0.2-alpha.1-1", + "@pierre/precision-diffs": "catalog:", "@solidjs/meta": "catalog:", "fuzzysort": "catalog:", "luxon": "catalog:", From d03b79e61eef0be1cca669e5e6a13df78cc4be85 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 27 Oct 2025 06:53:09 -0500 Subject: [PATCH 29/56] wip: desktop work --- packages/desktop/src/components/diff.tsx | 36 ++++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/desktop/src/components/diff.tsx b/packages/desktop/src/components/diff.tsx index b3d6d7e25..c39ad852e 100644 --- a/packages/desktop/src/components/diff.tsx +++ b/packages/desktop/src/components/diff.tsx @@ -97,24 +97,24 @@ export function Diff(props: DiffProps) { // // 'simple': // Just a subtle bar separator between each hunk - // hunkSeparators: "line-info", - hunkSeparators(hunkData: HunkData) { - const fragment = document.createDocumentFragment() - const numCol = document.createElement("div") - numCol.textContent = `${hunkData.lines}` - numCol.style.position = "sticky" - numCol.style.left = "0" - numCol.style.backgroundColor = "var(--pjs-bg)" - numCol.style.zIndex = "2" - fragment.appendChild(numCol) - const contentCol = document.createElement("div") - contentCol.textContent = "unmodified lines" - contentCol.style.position = "sticky" - contentCol.style.width = "var(--pjs-column-content-width)" - contentCol.style.left = "var(--pjs-column-number-width)" - fragment.appendChild(contentCol) - return fragment - }, + hunkSeparators: "line-info", + // hunkSeparators(hunkData: HunkData) { + // const fragment = document.createDocumentFragment() + // const numCol = document.createElement("div") + // numCol.textContent = `${hunkData.lines}` + // numCol.style.position = "sticky" + // numCol.style.left = "0" + // numCol.style.backgroundColor = "var(--pjs-bg)" + // numCol.style.zIndex = "2" + // fragment.appendChild(numCol) + // const contentCol = document.createElement("div") + // contentCol.textContent = "unmodified lines" + // contentCol.style.position = "sticky" + // contentCol.style.width = "var(--pjs-column-content-width)" + // contentCol.style.left = "var(--pjs-column-number-width)" + // fragment.appendChild(contentCol) + // return fragment + // }, // On lines that have both additions and deletions, we can run a // separate diff check to mark parts of the lines that change. // 'none': From fc115ea367dd034c7b989819d4f547c5d7519253 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:35:47 -0500 Subject: [PATCH 30/56] wip: desktop work --- bun.lock | 1 + packages/desktop/package.json | 3 +- .../src/components/assistant-message.tsx | 362 +++++++++++++++ .../desktop/src/components/diff-changes.tsx | 20 + packages/desktop/src/context/local.tsx | 8 - packages/desktop/src/pages/index.tsx | 180 +++++-- packages/opencode/src/tool/tool.ts | 3 + packages/ui/script/colors.txt | 438 +++++++++--------- packages/ui/src/components/collapsible.css | 46 +- packages/ui/src/components/collapsible.tsx | 14 +- packages/ui/src/components/icon.tsx | 10 + packages/ui/src/styles/tailwind/colors.css | 16 +- packages/ui/src/styles/theme.css | 50 +- 13 files changed, 854 insertions(+), 297 deletions(-) create mode 100644 packages/desktop/src/components/assistant-message.tsx create mode 100644 packages/desktop/src/components/diff-changes.tsx diff --git a/bun.lock b/bun.lock index d2959d4c5..42d088109 100644 --- a/bun.lock +++ b/bun.lock @@ -141,6 +141,7 @@ "@types/luxon": "3.7.1", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", + "opencode": "workspace:*", "typescript": "catalog:", "vite": "catalog:", "vite-plugin-icons-spritesheet": "3.0.1", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 135ee9bb1..c4af384f4 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -12,12 +12,13 @@ }, "license": "MIT", "devDependencies": { + "opencode": "workspace:*", "@tailwindcss/vite": "catalog:", "@tsconfig/bun": "1.0.9", "@types/luxon": "3.7.1", "@types/node": "catalog:", - "typescript": "catalog:", "@typescript/native-preview": "catalog:", + "typescript": "catalog:", "vite": "catalog:", "vite-plugin-icons-spritesheet": "3.0.1", "vite-plugin-solid": "catalog:" diff --git a/packages/desktop/src/components/assistant-message.tsx b/packages/desktop/src/components/assistant-message.tsx new file mode 100644 index 000000000..2e3d659aa --- /dev/null +++ b/packages/desktop/src/components/assistant-message.tsx @@ -0,0 +1,362 @@ +import type { Part, AssistantMessage, ReasoningPart, TextPart, ToolPart } from "@opencode-ai/sdk" +import type { Tool } from "opencode/tool/tool" +import type { ReadTool } from "opencode/tool/read" +import { children, Component, createMemo, For, Match, Show, Switch, type JSX } from "solid-js" +import { Dynamic } from "solid-js/web" +import { Markdown } from "./markdown" +import { Collapsible, Icon, IconProps } from "@opencode-ai/ui" +import { getDirectory, getFilename } from "@/utils" +import { ListTool } from "opencode/tool/ls" +import { GlobTool } from "opencode/tool/glob" +import { GrepTool } from "opencode/tool/grep" +import { WebFetchTool } from "opencode/tool/webfetch" +import { TaskTool } from "opencode/tool/task" +import { BashTool } from "opencode/tool/bash" +import { EditTool } from "opencode/tool/edit" +import { DiffChanges } from "./diff-changes" +import { WriteTool } from "opencode/tool/write" + +export function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) { + return ( +
+ + {(part) => { + const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING]) + return ( + + + + ) + }} + +
+ ) +} + +const PART_MAPPING = { + text: TextPart, + tool: ToolPart, + reasoning: ReasoningPart, +} + +function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }) { + return null + // return ( + // + //
{props.part.text}
+ //
+ // ) +} + +function TextPart(props: { part: TextPart; message: AssistantMessage }) { + return ( + + + + ) +} + +function ToolPart(props: { part: ToolPart; message: AssistantMessage }) { + // const sync = useSync() + + const component = createMemo(() => { + const render = ToolRegistry.render(props.part.tool) ?? GenericTool + + const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {}) + const input = props.part.state.status === "completed" ? props.part.state.input : {} + // const permissions = sync.data.permission[props.message.sessionID] ?? [] + // const permissionIndex = permissions.findIndex((x) => x.callID === props.part.callID) + // const permission = permissions[permissionIndex] + + return ( + <> + + {/* {props.part.state.error.replace("Error: ", "")} */} + + ) + }) + + return {component()} +} + +type TriggerTitle = { + title: string + subtitle?: string + args?: string[] + action?: JSX.Element +} + +const isTriggerTitle = (val: any): val is TriggerTitle => { + return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node) +} + +function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX.Element; children?: JSX.Element }) { + const resolved = children(() => props.children) + + return ( + + +
+
+ + + +
+
+ + {(props.trigger as TriggerTitle).title} + + + {(props.trigger as TriggerTitle).subtitle} + + + + {(arg) => {arg}} + + +
+ {(props.trigger as TriggerTitle).action} +
+
+ {props.trigger as JSX.Element} +
+
+ + + +
+
+ + {props.children} + +
+ ) +} + +function GenericTool(props: ToolProps) { + return +} + +type ToolProps = { + input: Partial> + metadata: Partial> + // permission: Record + tool: string + output?: string +} + +const ToolRegistry = (() => { + const state: Record< + string, + { + name: string + render?: Component> + } + > = {} + function register(input: { name: string; render?: Component> }) { + state[input.name] = input + return input + } + return { + register, + render(name: string) { + return state[name]?.render + }, + } +})() + +ToolRegistry.register({ + name: "read", + render(props) { + return ( + + ) + }, +}) + +ToolRegistry.register({ + name: "list", + render(props) { + return ( + + +
{props.output}
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "glob", + render(props) { + return ( + {props.output} + + + ) + }, +}) + +ToolRegistry.register({ + name: "grep", + render(props) { + const args = [] + if (props.input.pattern) args.push("pattern=" + props.input.pattern) + if (props.input.include) args.push("include=" + props.input.include) + return ( + + +
{props.output}
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "webfetch", + render(props) { + return ( + + + + ), + }} + > + +
{props.output}
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "task", + render(props) { + return ( + + +
{props.output}
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "bash", + render(props) { + return ( + + +
{props.output}
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "edit", + render(props) { + return ( + +
+
Edit
+
+ + {getDirectory(props.input.filePath!)}/ + + {getFilename(props.input.filePath ?? "")} +
+
+
{/* */}
+ + } + > + +
{props.output}
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "write", + render(props) { + return ( + +
+
Write
+
+ + {getDirectory(props.input.filePath!)}/ + + {getFilename(props.input.filePath ?? "")} +
+
+
{/* */}
+ + } + > + +
{props.output}
+
+
+ ) + }, +}) diff --git a/packages/desktop/src/components/diff-changes.tsx b/packages/desktop/src/components/diff-changes.tsx new file mode 100644 index 000000000..3b633f70f --- /dev/null +++ b/packages/desktop/src/components/diff-changes.tsx @@ -0,0 +1,20 @@ +import { FileDiff } from "@opencode-ai/sdk" +import { createMemo, Show } from "solid-js" + +export function DiffChanges(props: { diff: FileDiff | FileDiff[] }) { + const additions = createMemo(() => + Array.isArray(props.diff) ? props.diff.reduce((acc, diff) => acc + (diff.additions ?? 0), 0) : props.diff.additions, + ) + const deletions = createMemo(() => + Array.isArray(props.diff) ? props.diff.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0) : props.diff.deletions, + ) + const total = createMemo(() => additions() + deletions()) + return ( + 0}> +
+ {`+${additions()}`} + {`-${deletions()}`} +
+
+ ) +} diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index 6ed8ec17b..978dbfbc6 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -460,13 +460,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ return sync.data.message[store.active]?.find((m) => m.id === store.activeMessage) }) - const activeAssistantMessages = createMemo(() => { - if (!store.active || !activeMessage()) return [] - return sync.data.message[store.active]?.filter( - (m) => m.role === "assistant" && m.parentID == activeMessage()?.id, - ) - }) - const model = createMemo(() => { if (!last()) return const model = sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID] @@ -504,7 +497,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ return { active, activeMessage, - activeAssistantMessages, lastUserMessage, cost, last, diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx index 6702284b2..15da87bd6 100644 --- a/packages/desktop/src/pages/index.tsx +++ b/packages/desktop/src/pages/index.tsx @@ -22,6 +22,10 @@ import { Code } from "@/components/code" import { useSync } from "@/context/sync" import { useSDK } from "@/context/sdk" import { Diff } from "@/components/diff" +import { ProgressCircle } from "@/components/progress-circle" +import { AssistantMessage } from "@/components/assistant-message" +import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk" +import { DiffChanges } from "@/components/diff-changes" export default function Page() { const local = useLocal() @@ -92,7 +96,7 @@ export default function Page() { } } - if (event.key.length === 1 && event.key !== "Unidentified") { + if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) { inputRef?.focus() } } @@ -392,9 +396,6 @@ export default function Page() { {(session) => { const diffs = createMemo(() => session.summary?.diffs ?? []) const filesChanged = createMemo(() => diffs().length) - const additions = createMemo(() => diffs().reduce((acc, diff) => (acc ?? 0) + (diff.additions ?? 0), 0)) - const deletions = createMemo(() => diffs().reduce((acc, diff) => (acc ?? 0) + (diff.deletions ?? 0), 0)) - return (
@@ -408,12 +409,7 @@ export default function Page() {
{`${filesChanged() || "No"} file${filesChanged() !== 1 ? "s" : ""} changed`} - -
- {`+${additions()}`} - {`-${deletions()}`} -
-
+
@@ -434,13 +430,12 @@ export default function Page() {
- +
Chat
- -
- {local.session.context()}% -
-
+ + +
{local.session.context() ?? 0}%
+
{/* Review */} file.path)}> @@ -548,33 +543,114 @@ export default function Page() { 1}>
    - {(message) => ( -
  • local.session.setActiveMessage(message.id)} - > -
    - - - - - - - - - -
    -
    { + const countLines = (text: string) => { + if (!text) return 0 + return text.split("\n").length + } + + const additions = createMemo( + () => + message.summary?.diffs.reduce((acc, diff) => acc + (diff.additions ?? 0), 0) ?? 0, + ) + + const deletions = createMemo( + () => + message.summary?.diffs.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0) ?? 0, + ) + + const totalBeforeLines = createMemo( + () => + message.summary?.diffs.reduce((acc, diff) => acc + countLines(diff.before), 0) ?? + 0, + ) + + const blockCounts = createMemo(() => { + const TOTAL_BLOCKS = 5 + + const adds = additions() + const dels = deletions() + const unchanged = Math.max(0, totalBeforeLines() - dels) + + const totalActivity = unchanged + adds + dels + + if (totalActivity === 0) { + return { added: 0, deleted: 0, neutral: TOTAL_BLOCKS } + } + + const percentAdded = adds / totalActivity + const percentDeleted = dels / totalActivity + const added_raw = percentAdded * TOTAL_BLOCKS + const deleted_raw = percentDeleted * TOTAL_BLOCKS + + let added = adds > 0 ? Math.ceil(added_raw) : 0 + let deleted = dels > 0 ? Math.ceil(deleted_raw) : 0 + + let total_allocated = added + deleted + if (total_allocated > TOTAL_BLOCKS) { + if (added_raw < deleted_raw) { + added = Math.floor(added_raw) + } else { + deleted = Math.floor(deleted_raw) + } + + total_allocated = added + deleted + if (total_allocated > TOTAL_BLOCKS) { + if (added_raw < deleted_raw) { + deleted = Math.floor(deleted_raw) + } else { + added = Math.floor(added_raw) + } + } + } + + const neutral = Math.max(0, TOTAL_BLOCKS - added - deleted) + + return { added, deleted, neutral } + }) + + const ADD_COLOR = "var(--icon-diff-add-base)" + const DELETE_COLOR = "var(--icon-diff-delete-base)" + const NEUTRAL_COLOR = "var(--icon-weak-base)" + + const visibleBlocks = createMemo(() => { + const counts = blockCounts() + const blocks = [ + ...Array(counts.added).fill(ADD_COLOR), + ...Array(counts.deleted).fill(DELETE_COLOR), + ...Array(counts.neutral).fill(NEUTRAL_COLOR), + ] + return blocks.slice(0, 5) + }) + + return ( +
  • local.session.setActiveMessage(message.id)} > - {message.summary?.title ?? local.session.getMessageText(message)} -
- - )} +
+ + + + {(color, i) => ( + + )} + + + +
+
+ {message.summary?.title ?? local.session.getMessageText(message)} +
+ + ) + }} @@ -585,6 +661,11 @@ export default function Page() { const title = createMemo(() => message.summary?.title) const prompt = createMemo(() => local.session.getMessageText(message)) const summary = createMemo(() => message.summary?.body) + const assistantMessages = createMemo(() => { + return sync.data.message[activeSession().id]?.filter( + (m) => m.role === "assistant" && m.parentID == message.id, + ) as AssistantMessageType[] + }) return (
-
- {`+${diff.additions}`} - {`-${diff.deletions}`} -
+
@@ -661,10 +739,18 @@ export default function Page() { {/* Response */} -
+

Response

+
+ + {(assistantMessage) => { + const parts = createMemo(() => sync.data.part[assistantMessage.id]) + return + }} + +
) diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 978c9c072..c7a28c516 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -32,6 +32,9 @@ export namespace Tool { }> } + export type InferParameters = T extends Info ? z.infer

: never + export type InferMetadata = T extends Info ? M : never + export function define( id: string, init: Info["init"] | Awaited["init"]>>, diff --git a/packages/ui/script/colors.txt b/packages/ui/script/colors.txt index 15f8bb3d1..b022e8a11 100644 --- a/packages/ui/script/colors.txt +++ b/packages/ui/script/colors.txt @@ -1,214 +1,224 @@ - --background-base: #f8f7f7; - --background-weak: var(--smoke-light-3); - --background-strong: var(--smoke-light-1); - --background-stronger: #fcfcfc; - --base: var(--smoke-light-alpha-2); - --surface-base: var(--smoke-light-alpha-2); - --base2: var(--smoke-light-alpha-2); - --base3: var(--smoke-light-alpha-2); - --surface-inset-base: var(--smoke-light-alpha-3); - --surface-inset-base-hover: var(--smoke-light-alpha-3); - --surface-inset-strong: #1f000017; - --surface-inset-strong-hover: #1f000017; - --surface-raised-base: var(--smoke-light-alpha-1); - --surface-float-base: var(--smoke-dark-1); - --surface-float-base-hover: var(--smoke-dark-2); - --surface-raised-base-hover: var(--smoke-light-alpha-2); - --surface-raised-strong: var(--smoke-light-1); - --surface-raised-strong-hover: var(--white); - --surface-raised-stronger: var(--white); - --surface-raised-stronger-hover: var(--white); - --surface-weak: var(--smoke-light-alpha-3); - --surface-weaker: var(--smoke-light-alpha-4); - --surface-strong: #ffffff; - --surface-raised-stronger-non-alpha: var(--white); - --surface-brand-base: var(--yuzu-light-9); - --surface-brand-hover: var(--yuzu-light-10); - --surface-interactive-base: var(--cobalt-light-3); - --surface-interactive-hover: var(--cobalt-light-4); - --surface-interactive-weak: var(--cobalt-light-2); - --surface-interactive-weak-hover: var(--cobalt-light-3); - --surface-success-base: var(--apple-light-3); - --surface-success-weak: var(--apple-light-2); - --surface-success-strong: var(--apple-light-9); - --surface-warning-base: var(--solaris-light-3); - --surface-warning-weak: var(--solaris-light-2); - --surface-warning-strong: var(--solaris-light-9); - --surface-critical-base: var(--ember-light-3); - --surface-critical-weak: var(--ember-light-2); - --surface-critical-strong: var(--ember-light-9); - --surface-info-base: var(--lilac-light-3); - --surface-info-weak: var(--lilac-light-2); - --surface-info-strong: var(--lilac-light-9); - --surface-diff-skip-base: var(--smoke-light-3); - --surface-diff-unchanged-base: #ffffff00; - --surface-diff-hidden-base: var(--blue-light-3); - --surface-diff-hidden-weak: var(--blue-light-2); - --surface-diff-hidden-weaker: var(--blue-light-1); - --surface-diff-hidden-strong: var(--blue-light-5); - --surface-diff-hidden-stronger: var(--blue-light-9); - --surface-diff-add-base: var(--mint-light-3); - --surface-diff-add-weak: var(--mint-light-2); - --surface-diff-add-weaker: var(--mint-light-1); - --surface-diff-add-strong: var(--mint-light-5); - --surface-diff-add-stronger: var(--mint-light-9); - --surface-diff-delete-base: var(--ember-light-3); - --surface-diff-delete-weak: var(--ember-light-2); - --surface-diff-delete-weaker: var(--ember-light-1); - --surface-diff-delete-strong: var(--ember-light-6); - --surface-diff-delete-stronger: var(--ember-light-9); - --text-base: var(--smoke-light-11); - --input-base: var(--smoke-light-1); - --input-hover: var(--smoke-light-2); - --input-active: var(--cobalt-light-1); - --input-selected: var(--cobalt-light-4); - --input-focus: var(--cobalt-light-1); - --input-disabled: var(--smoke-light-4); - --text-weak: var(--smoke-light-9); - --text-weaker: var(--smoke-light-8); - --text-strong: var(--smoke-light-12); - --text-on-brand-base: var(--smoke-light-alpha-11); - --text-on-interactive-base: var(--smoke-light-1); - --text-on-success-base: var(--smoke-dark-alpha-11); - --text-on-warning-base: var(--smoke-dark-alpha-11); - --text-on-info-base: var(--smoke-dark-alpha-11); - --text-diff-add-base: var(--mint-light-11); - --text-diff-delete-base: var(--ember-light-11); - --text-diff-delete-strong: var(--ember-light-12); - --text-diff-add-strong: var(--mint-light-12); - --text-on-info-weak: var(--smoke-dark-alpha-9); - --text-on-info-strong: var(--smoke-dark-alpha-12); - --text-on-warning-weak: var(--smoke-dark-alpha-9); - --text-on-warning-strong: var(--smoke-dark-alpha-12); - --text-on-success-weak: var(--smoke-dark-alpha-9); - --text-on-success-strong: var(--smoke-dark-alpha-12); - --text-on-brand-weak: var(--smoke-light-alpha-9); - --text-on-brand-weaker: var(--smoke-light-alpha-8); - --text-on-brand-strong: var(--smoke-light-alpha-12); - --button-secondary-base: #fdfcfc; - --border-base: var(--smoke-light-alpha-7); - --border-hover: var(--smoke-light-alpha-8); - --border-active: var(--smoke-light-alpha-9); - --border-selected: var(--cobalt-light-alpha-9); - --border-disabled: var(--smoke-light-alpha-8); - --border-focus: var(--smoke-light-alpha-9); - --border-weak-base: var(--smoke-light-alpha-5); - --border-strong-base: var(--smoke-light-alpha-7); - --border-strong-hover: var(--smoke-light-alpha-8); - --border-strong-active: var(--smoke-light-alpha-7); - --border-strong-selected: var(--cobalt-light-alpha-6); - --border-strong-disabled: var(--smoke-light-alpha-6); - --border-strong-focus: var(--smoke-light-alpha-7); - --border-weak-hover: var(--smoke-light-alpha-6); - --border-weak-active: var(--smoke-light-alpha-7); - --border-weak-selected: var(--cobalt-light-alpha-6); - --border-weak-disabled: var(--smoke-light-alpha-6); - --border-weak-focus: var(--smoke-light-alpha-7); - --border-interactive-base: var(--cobalt-light-7); - --border-interactive-hover: var(--cobalt-light-8); - --border-interactive-active: var(--cobalt-light-9); - --border-interactive-selected: var(--cobalt-light-9); - --border-interactive-disabled: var(--smoke-light-8); - --border-interactive-focus: var(--cobalt-light-9); - --border-success-base: var(--apple-light-6); - --border-success-hover: var(--apple-light-7); - --border-success-selected: var(--apple-light-9); - --border-warning-base: var(--solaris-light-6); - --border-warning-hover: var(--solaris-light-7); - --border-warning-selected: var(--solaris-light-9); - --border-critical-base: var(--ember-light-6); - --border-critical-hover: var(--ember-light-7); - --border-critical-selected: var(--ember-light-9); - --border-info-base: var(--lilac-light-6); - --border-info-hover: var(--lilac-light-7); - --border-info-selected: var(--lilac-light-9); - --icon-base: var(--smoke-light-9); - --icon-hover: var(--smoke-light-11); - --icon-active: var(--smoke-light-12); - --icon-selected: var(--smoke-light-12); - --icon-disabled: var(--smoke-light-8); - --icon-focus: var(--smoke-light-12); - --icon-weak-base: var(--smoke-light-7); - --icon-invert-base: #ffffff; - --icon-weak-hover: var(--smoke-light-8); - --icon-weak-active: var(--smoke-light-9); - --icon-weak-selected: var(--smoke-light-10); - --icon-weak-disabled: var(--smoke-light-6); - --icon-weak-focus: var(--smoke-light-9); - --icon-strong-base: var(--smoke-light-12); - --icon-strong-hover: var(--smoke-light-12); - --icon-strong-active: var(--smoke-light-12); - --icon-strong-selected: var(--smoke-light-12); - --icon-strong-disabled: var(--smoke-light-8); - --icon-strong-focus: var(--smoke-light-12); - --icon-brand-base: var(--smoke-light-12); - --icon-interactive-base: var(--cobalt-light-9); - --icon-success-base: var(--apple-light-7); - --icon-success-hover: var(--apple-light-8); - --icon-success-active: var(--apple-light-11); - --icon-warning-base: var(--amber-light-7); - --icon-warning-hover: var(--amber-light-8); - --icon-warning-active: var(--amber-light-11); - --icon-critical-base: var(--ember-light-7); - --icon-critical-hover: var(--ember-light-8); - --icon-critical-active: var(--ember-light-11); - --icon-info-base: var(--lilac-light-7); - --icon-info-hover: var(--lilac-light-8); - --icon-info-active: var(--lilac-light-11); - --icon-on-brand-base: var(--smoke-light-alpha-11); - --icon-on-brand-hover: var(--smoke-light-alpha-12); - --icon-on-brand-selected: var(--smoke-light-alpha-12); - --icon-on-interactive-base: var(--smoke-light-alpha-9); - --icon-on-interactive-hover: var(--smoke-light-alpha-10); - --icon-on-interactive-selected: var(--smoke-light-alpha-11); - --icon-agent-plan-base: var(--purple-light-9); - --icon-agent-docs-base: var(--amber-light-9); - --icon-agent-ask-base: var(--cyan-light-9); - --icon-agent-build-base: var(--blue-light-9); - --icon-on-success-base: var(--apple-light-alpha-9); - --icon-on-success-hover: var(--apple-light-alpha-10); - --icon-on-success-selected: var(--apple-light-alpha-11); - --icon-on-warning-base: var(--amber-lightalpha-9); - --icon-on-warning-hover: var(--amber-lightalpha-10); - --icon-on-warning-selected: var(--amber-lightalpha-11); - --icon-on-critical-base: var(--ember-light-alpha-9); - --icon-on-critical-hover: var(--ember-light-alpha-10); - --icon-on-critical-selected: var(--ember-light-alpha-11); - --icon-on-info-base: var(--lilac-light-9); - --icon-on-info-hover: var(--lilac-light-alpha-10); - --icon-on-info-selected: var(--lilac-light-alpha-11); - --icon-diff-add-base: var(--mint-light-11); - --icon-diff-add-hover: var(--mint-light-12); - --icon-diff-add-active: var(--mint-light-12); - --icon-diff-delete-base: var(--ember-light-9); - --icon-diff-delete-hover: var(--ember-light-10); - --icon-diff-delete-active: var(--ember-light-11); - --syntax-comment: #ffffff; - --syntax-string: #ffffff; - --syntax-keyword: #ffffff; - --syntax-function: #ffffff; - --syntax-number: #ffffff; - --syntax-operator: #ffffff; - --syntax-variable: #ffffff; - --syntax-type: #ffffff; - --syntax-constant: #ffffff; - --syntax-punctuation: #ffffff; - --syntax-success: #ffffff; - --syntax-warning: #ffffff; - --syntax-critical: #ffffff; - --syntax-info: #ffffff; - --markdown-heading: #ffffff; - --markdown-text: #ffffff; - --markdown-link: #ffffff; - --markdown-link-text: #ffffff; - --markdown-code: #ffffff; - --markdown-block-quote: #ffffff; - --markdown-emph: #ffffff; - --markdown-strong: #ffffff; - --markdown-horizontal-rule: #ffffff; - --markdown-list-item: #ffffff; - --markdown-list-enumeration: #ffffff; - --markdown-image: #ffffff; - --markdown-image-text: #ffffff; - --markdown-code-block: #ffffff; - --border-color: #ffffff; +--background-base: #F8F7F7; +--background-weak: var(--smoke-light-3); +--background-strong: var(--smoke-light-1); +--background-stronger: #FCFCFC; +--base: var(--smoke-light-alpha-2); +--surface-base: var(--smoke-light-alpha-2); +--surface-base-hover: #0500000F; +--surface-base-active: var(--smoke-light-alpha-3); +--surface-base-interactive-active: var(--cobalt-light-alpha-3); +--base2: var(--smoke-light-alpha-2); +--base3: var(--smoke-light-alpha-2); +--surface-inset-base: var(--smoke-light-alpha-2); +--surface-inset-base-hover: var(--smoke-light-alpha-3); +--surface-inset-strong: #1F000017; +--surface-inset-strong-hover: #1F000017; +--surface-raised-base: var(--smoke-light-alpha-1); +--surface-float-base: var(--smoke-dark-1); +--surface-float-base-hover: var(--smoke-dark-2); +--surface-raised-base-hover: var(--smoke-light-alpha-2); +--surface-raised-strong: var(--smoke-light-1); +--surface-raised-strong-hover: var(--white); +--surface-raised-stronger: var(--white); +--surface-raised-stronger-hover: var(--white); +--surface-weak: var(--smoke-light-alpha-3); +--surface-weaker: var(--smoke-light-alpha-4); +--surface-strong: #FFFFFF; +--surface-raised-stronger-non-alpha: var(--white); +--surface-brand-base: var(--yuzu-light-9); +--surface-brand-hover: var(--yuzu-light-10); +--surface-interactive-base: var(--cobalt-light-3); +--surface-interactive-hover: var(--cobalt-light-4); +--surface-interactive-weak: var(--cobalt-light-2); +--surface-interactive-weak-hover: var(--cobalt-light-3); +--surface-success-base: var(--apple-light-3); +--surface-success-weak: var(--apple-light-2); +--surface-success-strong: var(--apple-light-9); +--surface-warning-base: var(--solaris-light-3); +--surface-warning-weak: var(--solaris-light-2); +--surface-warning-strong: var(--solaris-light-9); +--surface-critical-base: var(--ember-light-3); +--surface-critical-weak: var(--ember-light-2); +--surface-critical-strong: var(--ember-light-9); +--surface-info-base: var(--lilac-light-3); +--surface-info-weak: var(--lilac-light-2); +--surface-info-strong: var(--lilac-light-9); +--surface-diff-hidden-base: var(--blue-light-3); +--surface-diff-skip-base: var(--smoke-light-2); +--surface-diff-unchanged-base: #FFFFFF00; +--surface-diff-hidden-weak: var(--blue-light-2); +--surface-diff-hidden-weaker: var(--blue-light-1); +--surface-diff-hidden-strong: var(--blue-light-5); +--surface-diff-hidden-stronger: var(--blue-light-9); +--surface-diff-add-base: var(--mint-light-3); +--surface-diff-add-weak: var(--mint-light-2); +--surface-diff-add-weaker: var(--mint-light-1); +--surface-diff-add-strong: var(--mint-light-5); +--surface-diff-add-stronger: var(--mint-light-9); +--surface-diff-delete-base: var(--ember-light-3); +--surface-diff-delete-weak: var(--ember-light-2); +--surface-diff-delete-weaker: var(--ember-light-1); +--surface-diff-delete-strong: var(--ember-light-6); +--surface-diff-delete-stronger: var(--ember-light-9); +--text-base: var(--smoke-light-11); +--input-base: var(--smoke-light-1); +--input-hover: var(--smoke-light-2); +--input-active: var(--cobalt-light-1); +--input-selected: var(--cobalt-light-4); +--input-focus: var(--cobalt-light-1); +--input-disabled: var(--smoke-light-4); +--text-weak: var(--smoke-light-9); +--text-weaker: var(--smoke-light-8); +--text-strong: var(--smoke-light-12); +--text-interactive-base: var(--cobalt-light-9); +--text-on-brand-base: var(--smoke-light-alpha-11); +--text-on-interactive-base: var(--smoke-light-1); +--text-on-interactive-weak: var(--smoke-dark-alpha-11); +--text-on-success-base: var(--smoke-dark-alpha-11); +--text-on-warning-base: var(--smoke-dark-alpha-11); +--text-on-info-base: var(--smoke-dark-alpha-11); +--text-diff-add-base: var(--mint-light-11); +--text-diff-delete-base: var(--ember-light-11); +--text-diff-delete-strong: var(--ember-light-12); +--text-diff-add-strong: var(--mint-light-12); +--text-on-info-weak: var(--smoke-dark-alpha-9); +--text-on-info-strong: var(--smoke-dark-alpha-12); +--text-on-warning-weak: var(--smoke-dark-alpha-9); +--text-on-warning-strong: var(--smoke-dark-alpha-12); +--text-on-success-weak: var(--smoke-dark-alpha-9); +--text-on-success-strong: var(--smoke-dark-alpha-12); +--text-on-brand-weak: var(--smoke-light-alpha-9); +--text-on-brand-weaker: var(--smoke-light-alpha-8); +--text-on-brand-strong: var(--smoke-light-alpha-12); +--button-secondary-base: #FDFCFC; +--button-secondary-base-hover: #FAF9F9; +--border-base: var(--smoke-light-alpha-7); +--border-hover: var(--smoke-light-alpha-8); +--border-active: var(--smoke-light-alpha-9); +--border-selected: var(--cobalt-light-alpha-9); +--border-disabled: var(--smoke-light-alpha-8); +--border-focus: var(--smoke-light-alpha-9); +--border-weak-base: var(--smoke-light-alpha-5); +--border-strong-base: var(--smoke-light-alpha-7); +--border-strong-hover: var(--smoke-light-alpha-8); +--border-strong-active: var(--smoke-light-alpha-7); +--border-strong-selected: var(--cobalt-light-alpha-6); +--border-strong-disabled: var(--smoke-light-alpha-6); +--border-strong-focus: var(--smoke-light-alpha-7); +--border-weak-hover: var(--smoke-light-alpha-6); +--border-weak-active: var(--smoke-light-alpha-7); +--border-weak-selected: var(--cobalt-light-alpha-5); +--border-weak-disabled: var(--smoke-light-alpha-6); +--border-weak-focus: var(--smoke-light-alpha-7); +--border-interactive-base: var(--cobalt-light-7); +--border-interactive-hover: var(--cobalt-light-8); +--border-interactive-active: var(--cobalt-light-9); +--border-interactive-selected: var(--cobalt-light-9); +--border-interactive-disabled: var(--smoke-light-8); +--border-interactive-focus: var(--cobalt-light-9); +--border-success-base: var(--apple-light-6); +--border-success-hover: var(--apple-light-7); +--border-success-selected: var(--apple-light-9); +--border-warning-base: var(--solaris-light-6); +--border-warning-hover: var(--solaris-light-7); +--border-warning-selected: var(--solaris-light-9); +--border-critical-base: var(--ember-light-6); +--border-critical-hover: var(--ember-light-7); +--border-critical-selected: var(--ember-light-9); +--border-info-base: var(--lilac-light-6); +--border-info-hover: var(--lilac-light-7); +--border-info-selected: var(--lilac-light-9); +--icon-base: var(--smoke-light-9); +--icon-hover: var(--smoke-light-11); +--icon-active: var(--smoke-light-12); +--icon-selected: var(--smoke-light-12); +--icon-disabled: var(--smoke-light-8); +--icon-focus: var(--smoke-light-12); +--icon-weak-base: var(--smoke-light-7); +--icon-invert-base: #FFFFFF; +--icon-weak-hover: var(--smoke-light-8); +--icon-weak-active: var(--smoke-light-9); +--icon-weak-selected: var(--smoke-light-10); +--icon-weak-disabled: var(--smoke-light-6); +--icon-weak-focus: var(--smoke-light-9); +--icon-strong-base: var(--smoke-light-12); +--icon-strong-hover: var(--smoke-light-12); +--icon-strong-active: var(--smoke-light-12); +--icon-strong-selected: var(--smoke-light-12); +--icon-strong-disabled: var(--smoke-light-8); +--icon-strong-focus: var(--smoke-light-12); +--icon-brand-base: var(--smoke-light-12); +--icon-interactive-base: var(--cobalt-light-9); +--icon-success-base: var(--apple-light-7); +--icon-success-hover: var(--apple-light-8); +--icon-success-active: var(--apple-light-11); +--icon-warning-base: var(--amber-light-7); +--icon-warning-hover: var(--amber-light-8); +--icon-warning-active: var(--amber-light-11); +--icon-critical-base: var(--ember-light-7); +--icon-critical-hover: var(--ember-light-8); +--icon-critical-active: var(--ember-light-11); +--icon-info-base: var(--lilac-light-7); +--icon-info-hover: var(--lilac-light-8); +--icon-info-active: var(--lilac-light-11); +--icon-on-brand-base: var(--smoke-light-alpha-11); +--icon-on-brand-hover: var(--smoke-light-alpha-12); +--icon-on-brand-selected: var(--smoke-light-alpha-12); +--icon-on-interactive-base: var(--smoke-light-1); +--icon-agent-plan-base: var(--purple-light-9); +--icon-agent-docs-base: var(--amber-light-9); +--icon-agent-ask-base: var(--cyan-light-9); +--icon-agent-build-base: var(--cobalt-light-9); +--icon-on-success-base: var(--apple-light-alpha-9); +--icon-on-success-hover: var(--apple-light-alpha-10); +--icon-on-success-selected: var(--apple-light-alpha-11); +--icon-on-warning-base: var(--amber-lightalpha-9); +--icon-on-warning-hover: var(--amber-lightalpha-10); +--icon-on-warning-selected: var(--amber-lightalpha-11); +--icon-on-critical-base: var(--ember-light-alpha-9); +--icon-on-critical-hover: var(--ember-light-alpha-10); +--icon-on-critical-selected: var(--ember-light-alpha-11); +--icon-on-info-base: var(--lilac-light-9); +--icon-on-info-hover: var(--lilac-light-alpha-10); +--icon-on-info-selected: var(--lilac-light-alpha-11); +--icon-diff-add-base: var(--mint-light-11); +--icon-diff-add-hover: var(--mint-light-12); +--icon-diff-add-active: var(--mint-light-12); +--icon-diff-delete-base: var(--ember-light-9); +--icon-diff-delete-hover: var(--ember-light-10); +--icon-diff-delete-active: var(--ember-light-11); +--syntax-comment: #8A8A8A; +--syntax-string: #D68C27; +--syntax-keyword: #3B7DD8; +--syntax-function: #D1383D; +--syntax-number: #3D9A57; +--syntax-operator: #D68C27; +--syntax-variable: #B0851F; +--syntax-type: #318795; +--syntax-constant: #953170; +--syntax-punctuation: #1A1A1A; +--syntax-success: var(--apple-dark-10); +--syntax-warning: var(--amber-light-10); +--syntax-critical: var(--ember-dark-9); +--syntax-info: var(--lilac-dark-11); +--markdown-heading: #D68C27; +--markdown-text: #1A1A1A; +--markdown-link: #3B7DD8; +--markdown-link-text: #318795; +--markdown-code: #3D9A57; +--markdown-block-quote: #B0851F; +--markdown-emph: #B0851F; +--markdown-strong: #D68C27; +--markdown-horizontal-rule: #8A8A8A; +--markdown-list-item: #3B7DD8; +--markdown-list-enumeration: #318795; +--markdown-image: #3B7DD8; +--markdown-image-text: #318795; +--markdown-code-block: #1A1A1A; +--border-color: #FFFFFF; +--border-weaker-base: var(--smoke-light-alpha-3); +--border-weaker-hover: var(--smoke-light-alpha-4); +--border-weaker-active: var(--smoke-light-alpha-6); +--border-weaker-selected: var(--cobalt-light-alpha-4); +--border-weaker-disabled: var(--smoke-light-alpha-2); +--border-weaker-focus: var(--smoke-light-alpha-6); diff --git a/packages/ui/src/components/collapsible.css b/packages/ui/src/components/collapsible.css index 441d0083f..34699fc20 100644 --- a/packages/ui/src/components/collapsible.css +++ b/packages/ui/src/components/collapsible.css @@ -1,23 +1,55 @@ [data-component="collapsible"] { + width: 100%; display: flex; flex-direction: column; + background-color: var(--surface-inset-base); + border: 1px solid var(--border-weaker-base); + transition: background-color 0.15s ease; + border-radius: 8px; + overflow: clip; - [data-slot="trigger"] { - cursor: pointer; + [data-slot="collapsible-trigger"] { + width: 100%; + display: flex; + height: 40px; + padding: 6px 8px 6px 12px; + align-items: center; + align-self: stretch; + cursor: default; user-select: none; + color: var(--text-base); + /* text-12-medium */ + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); /* 166.667% */ + letter-spacing: var(--letter-spacing-normal); + + /* &:hover { */ + /* background-color: var(--surface-base); */ + /* } */ &:focus-visible { - outline: 2px solid var(--border-focus); - outline-offset: 2px; + outline: none; } - &[data-disabled] { cursor: not-allowed; - opacity: 0.5; + } + + [data-slot="collapsible-arrow"] { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + + /* [data-slot="collapsible-arrow-icon"] { */ + /* } */ } } - [data-slot="content"] { + [data-slot="collapsible-content"] { overflow: hidden; /* animation: slideUp 250ms ease-out; */ diff --git a/packages/ui/src/components/collapsible.tsx b/packages/ui/src/components/collapsible.tsx index f926192e8..d2e4a139b 100644 --- a/packages/ui/src/components/collapsible.tsx +++ b/packages/ui/src/components/collapsible.tsx @@ -1,5 +1,6 @@ import { Collapsible as Kobalte, CollapsibleRootProps } from "@kobalte/core/collapsible" import { ComponentProps, ParentProps, splitProps } from "solid-js" +import { Icon } from "./icon" export interface CollapsibleProps extends ParentProps { class?: string @@ -21,14 +22,23 @@ function CollapsibleRoot(props: CollapsibleProps) { } function CollapsibleTrigger(props: ComponentProps) { - return + return } function CollapsibleContent(props: ComponentProps) { - return + return +} + +function CollapsibleArrow(props?: ComponentProps<"div">) { + return ( +

+ +
+ ) } export const Collapsible = Object.assign(CollapsibleRoot, { + Arrow: CollapsibleArrow, Trigger: CollapsibleTrigger, Content: CollapsibleContent, }) diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 0011a9676..5736146e5 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -139,6 +139,16 @@ const newIcons = { folder: ``, "pencil-line": ``, "chevron-grabber-vertical": ``, + mcp: ``, + glasses: ``, + "bullet-list": ``, + "magnifying-glass-menu": ``, + "window-cursor": ``, + task: ``, + checklist: ``, + console: ``, + "code-lines": ``, + "square-arrow-top-right": ``, } export interface IconProps extends ComponentProps<"svg"> { diff --git a/packages/ui/src/styles/tailwind/colors.css b/packages/ui/src/styles/tailwind/colors.css index 2bf3fd772..e2f6788ab 100644 --- a/packages/ui/src/styles/tailwind/colors.css +++ b/packages/ui/src/styles/tailwind/colors.css @@ -9,6 +9,9 @@ --color-background-stronger: var(--background-stronger); --color-base: var(--base); --color-surface-base: var(--surface-base); + --color-surface-base-hover: var(--surface-base-hover); + --color-surface-base-active: var(--surface-base-active); + --color-surface-base-interactive-active: var(--surface-base-interactive-active); --color-base2: var(--base2); --color-base3: var(--base3); --color-surface-inset-base: var(--surface-inset-base); @@ -45,9 +48,9 @@ --color-surface-info-base: var(--surface-info-base); --color-surface-info-weak: var(--surface-info-weak); --color-surface-info-strong: var(--surface-info-strong); + --color-surface-diff-hidden-base: var(--surface-diff-hidden-base); --color-surface-diff-skip-base: var(--surface-diff-skip-base); --color-surface-diff-unchanged-base: var(--surface-diff-unchanged-base); - --color-surface-diff-hidden-base: var(--surface-diff-hidden-base); --color-surface-diff-hidden-weak: var(--surface-diff-hidden-weak); --color-surface-diff-hidden-weaker: var(--surface-diff-hidden-weaker); --color-surface-diff-hidden-strong: var(--surface-diff-hidden-strong); @@ -72,8 +75,10 @@ --color-text-weak: var(--text-weak); --color-text-weaker: var(--text-weaker); --color-text-strong: var(--text-strong); + --color-text-interactive-base: var(--text-interactive-base); --color-text-on-brand-base: var(--text-on-brand-base); --color-text-on-interactive-base: var(--text-on-interactive-base); + --color-text-on-interactive-weak: var(--text-on-interactive-weak); --color-text-on-success-base: var(--text-on-success-base); --color-text-on-warning-base: var(--text-on-warning-base); --color-text-on-info-base: var(--text-on-info-base); @@ -91,6 +96,7 @@ --color-text-on-brand-weaker: var(--text-on-brand-weaker); --color-text-on-brand-strong: var(--text-on-brand-strong); --color-button-secondary-base: var(--button-secondary-base); + --color-button-secondary-base-hover: var(--button-secondary-base-hover); --color-border-base: var(--border-base); --color-border-hover: var(--border-hover); --color-border-active: var(--border-active); @@ -164,8 +170,6 @@ --color-icon-on-brand-hover: var(--icon-on-brand-hover); --color-icon-on-brand-selected: var(--icon-on-brand-selected); --color-icon-on-interactive-base: var(--icon-on-interactive-base); - --color-icon-on-interactive-hover: var(--icon-on-interactive-hover); - --color-icon-on-interactive-selected: var(--icon-on-interactive-selected); --color-icon-agent-plan-base: var(--icon-agent-plan-base); --color-icon-agent-docs-base: var(--icon-agent-docs-base); --color-icon-agent-ask-base: var(--icon-agent-ask-base); @@ -217,4 +221,10 @@ --color-markdown-image-text: var(--markdown-image-text); --color-markdown-code-block: var(--markdown-code-block); --color-border-color: var(--border-color); + --color-border-weaker-base: var(--border-weaker-base); + --color-border-weaker-hover: var(--border-weaker-hover); + --color-border-weaker-active: var(--border-weaker-active); + --color-border-weaker-selected: var(--border-weaker-selected); + --color-border-weaker-disabled: var(--border-weaker-disabled); + --color-border-weaker-focus: var(--border-weaker-focus); } \ No newline at end of file diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css index 5358f380d..0c22bae5a 100644 --- a/packages/ui/src/styles/theme.css +++ b/packages/ui/src/styles/theme.css @@ -66,11 +66,14 @@ --background-weak: var(--smoke-light-3); --background-strong: var(--smoke-light-1); --background-stronger: #fcfcfc; - --surface-base: var(--smoke-light-alpha-2); --base: var(--smoke-light-alpha-2); + --surface-base: var(--smoke-light-alpha-2); + --surface-base-hover: #0500000f; + --surface-base-active: var(--smoke-light-alpha-3); + --surface-base-interactive-active: var(--cobalt-light-alpha-3); --base2: var(--smoke-light-alpha-2); --base3: var(--smoke-light-alpha-2); - --surface-inset-base: var(--smoke-light-alpha-3); + --surface-inset-base: var(--smoke-light-alpha-2); --surface-inset-base-hover: var(--smoke-light-alpha-3); --surface-inset-strong: #1f000017; --surface-inset-strong-hover: #1f000017; @@ -105,7 +108,7 @@ --surface-info-weak: var(--lilac-light-2); --surface-info-strong: var(--lilac-light-9); --surface-diff-hidden-base: var(--blue-light-3); - --surface-diff-skip-base: var(--smoke-light-3); + --surface-diff-skip-base: var(--smoke-light-2); --surface-diff-unchanged-base: #ffffff00; --surface-diff-hidden-weak: var(--blue-light-2); --surface-diff-hidden-weaker: var(--blue-light-1); @@ -131,6 +134,7 @@ --text-weak: var(--smoke-light-9); --text-weaker: var(--smoke-light-8); --text-strong: var(--smoke-light-12); + --text-interactive-base: var(--cobalt-light-9); --text-on-brand-base: var(--smoke-light-alpha-11); --text-on-interactive-base: var(--smoke-light-1); --text-on-interactive-weak: var(--smoke-dark-alpha-11); @@ -151,7 +155,7 @@ --text-on-brand-weaker: var(--smoke-light-alpha-8); --text-on-brand-strong: var(--smoke-light-alpha-12); --button-secondary-base: #fdfcfc; - --button-secondary-base-hover: var(--smoke-light-2); + --button-secondary-base-hover: #faf9f9; --border-base: var(--smoke-light-alpha-7); --border-hover: var(--smoke-light-alpha-8); --border-active: var(--smoke-light-alpha-9); @@ -167,7 +171,7 @@ --border-strong-focus: var(--smoke-light-alpha-7); --border-weak-hover: var(--smoke-light-alpha-6); --border-weak-active: var(--smoke-light-alpha-7); - --border-weak-selected: var(--cobalt-light-alpha-4); + --border-weak-selected: var(--cobalt-light-alpha-5); --border-weak-disabled: var(--smoke-light-alpha-6); --border-weak-focus: var(--smoke-light-alpha-7); --border-interactive-base: var(--cobalt-light-7); @@ -228,7 +232,7 @@ --icon-agent-plan-base: var(--purple-light-9); --icon-agent-docs-base: var(--amber-light-9); --icon-agent-ask-base: var(--cyan-light-9); - --icon-agent-build-base: var(--blue-light-9); + --icon-agent-build-base: var(--cobalt-light-9); --icon-on-success-base: var(--apple-light-alpha-9); --icon-on-success-hover: var(--apple-light-alpha-10); --icon-on-success-selected: var(--apple-light-alpha-11); @@ -276,6 +280,12 @@ --markdown-image-text: #318795; --markdown-code-block: #1a1a1a; --border-color: #ffffff; + --border-weaker-base: var(--smoke-light-alpha-3); + --border-weaker-hover: var(--smoke-light-alpha-4); + --border-weaker-active: var(--smoke-light-alpha-6); + --border-weaker-selected: var(--cobalt-light-alpha-4); + --border-weaker-disabled: var(--smoke-light-alpha-2); + --border-weaker-focus: var(--smoke-light-alpha-6); @media (prefers-color-scheme: dark) { /* OC-1-Dark */ @@ -284,8 +294,11 @@ --background-weak: #201d1d; --background-strong: #151313; --background-stronger: #201c1c; - --surface-base: var(--smoke-dark-alpha-3); --base: var(--smoke-dark-alpha-2); + --surface-base: var(--smoke-dark-alpha-2); + --surface-base-hover: #e0b7b716; + --surface-base-active: var(--smoke-dark-alpha-3); + --surface-base-interactive-active: var(--cobalt-dark-alpha-2); --base2: var(--smoke-dark-alpha-2); --base3: var(--smoke-dark-alpha-2); --surface-inset-base: #0e0b0b7f; @@ -300,8 +313,8 @@ --surface-raised-strong-hover: var(--smoke-dark-alpha-6); --surface-raised-stronger: var(--smoke-dark-alpha-6); --surface-raised-stronger-hover: var(--smoke-dark-alpha-7); - --surface-weak: var(--smoke-dark-alpha-5); - --surface-weaker: var(--smoke-dark-alpha-6); + --surface-weak: var(--smoke-dark-alpha-4); + --surface-weaker: var(--smoke-dark-alpha-5); --surface-strong: var(--smoke-dark-alpha-7); --surface-raised-stronger-non-alpha: var(--smoke-dark-4); --surface-brand-base: var(--yuzu-light-9); @@ -323,7 +336,7 @@ --surface-info-weak: var(--lilac-light-2); --surface-info-strong: var(--lilac-light-9); --surface-diff-hidden-base: var(--blue-dark-2); - --surface-diff-skip-base: var(--smoke-dark-alpha-2); + --surface-diff-skip-base: var(--smoke-dark-alpha-1); --surface-diff-unchanged-base: var(--smoke-dark-1); --surface-diff-hidden-weak: var(--blue-dark-1); --surface-diff-hidden-weaker: var(--blue-dark-3); @@ -349,6 +362,7 @@ --text-weak: var(--smoke-dark-alpha-9); --text-weaker: var(--smoke-dark-alpha-8); --text-strong: var(--smoke-dark-alpha-12); + --text-interactive-base: var(--cobalt-dark-11); --text-on-brand-base: var(--smoke-dark-alpha-11); --text-on-interactive-base: var(--smoke-dark-12); --text-on-interactive-weak: var(--smoke-dark-alpha-11); @@ -368,12 +382,12 @@ --text-on-brand-weak: var(--smoke-dark-alpha-9); --text-on-brand-weaker: var(--smoke-dark-alpha-8); --text-on-brand-strong: var(--smoke-dark-alpha-12); - --button-secondary-base: var(--smoke-dark-6); - --button-secondary-base-hover: var(--smoke-dark-5); + --button-secondary-base: var(--smoke-dark-4); + --button-secondary-base-hover: #2a2727; --border-base: var(--smoke-dark-alpha-7); --border-hover: var(--smoke-dark-alpha-8); --border-active: var(--smoke-dark-alpha-9); - --border-selected: var(--cobalt-dark-alpha-9); + --border-selected: var(--cobalt-dark-alpha-11); --border-disabled: var(--smoke-dark-alpha-8); --border-focus: var(--smoke-dark-alpha-9); --border-weak-base: var(--smoke-dark-alpha-6); @@ -385,7 +399,7 @@ --border-strong-focus: var(--smoke-dark-alpha-8); --border-weak-hover: var(--smoke-dark-alpha-7); --border-weak-active: var(--smoke-dark-alpha-8); - --border-weak-selected: var(--cobalt-dark-alpha-3); + --border-weak-selected: var(--cobalt-dark-alpha-6); --border-weak-disabled: var(--smoke-dark-alpha-6); --border-weak-focus: var(--smoke-dark-alpha-8); --border-interactive-base: var(--cobalt-light-7); @@ -446,7 +460,7 @@ --icon-agent-plan-base: var(--purple-dark-9); --icon-agent-docs-base: var(--amber-dark-9); --icon-agent-ask-base: var(--cyan-dark-9); - --icon-agent-build-base: var(--blue-dark-9); + --icon-agent-build-base: var(--cobalt-dark-11); --icon-on-success-base: var(--apple-dark-alpha-9); --icon-on-success-hover: var(--apple-dark-alpha-10); --icon-on-success-selected: var(--apple-dark-alpha-11); @@ -494,5 +508,11 @@ --markdown-image-text: #56b6c2; --markdown-code-block: #eeeeee; --border-color: #ffffff; + --border-weaker-base: var(--smoke-dark-alpha-3); + --border-weaker-hover: var(--smoke-dark-alpha-4); + --border-weaker-active: var(--smoke-dark-alpha-6); + --border-weaker-selected: var(--cobalt-dark-alpha-3); + --border-weaker-disabled: var(--smoke-dark-alpha-2); + --border-weaker-focus: var(--smoke-dark-alpha-6); } } From 485e4520e7a1292176373068f37fb91356b68948 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:37:03 -0500 Subject: [PATCH 31/56] wip: desktop work --- .../desktop/src/components/assistant-message.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/desktop/src/components/assistant-message.tsx b/packages/desktop/src/components/assistant-message.tsx index 2e3d659aa..38c06bbe5 100644 --- a/packages/desktop/src/components/assistant-message.tsx +++ b/packages/desktop/src/components/assistant-message.tsx @@ -6,15 +6,15 @@ import { Dynamic } from "solid-js/web" import { Markdown } from "./markdown" import { Collapsible, Icon, IconProps } from "@opencode-ai/ui" import { getDirectory, getFilename } from "@/utils" -import { ListTool } from "opencode/tool/ls" -import { GlobTool } from "opencode/tool/glob" -import { GrepTool } from "opencode/tool/grep" -import { WebFetchTool } from "opencode/tool/webfetch" -import { TaskTool } from "opencode/tool/task" -import { BashTool } from "opencode/tool/bash" -import { EditTool } from "opencode/tool/edit" +import type { ListTool } from "opencode/tool/ls" +import type { GlobTool } from "opencode/tool/glob" +import type { GrepTool } from "opencode/tool/grep" +import type { WebFetchTool } from "opencode/tool/webfetch" +import type { TaskTool } from "opencode/tool/task" +import type { BashTool } from "opencode/tool/bash" +import type { EditTool } from "opencode/tool/edit" +import type { WriteTool } from "opencode/tool/write" import { DiffChanges } from "./diff-changes" -import { WriteTool } from "opencode/tool/write" export function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) { return ( From ee07ed2dc41d6f2e19b11b71bfcaa9892df780d1 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 27 Oct 2025 15:44:12 -0500 Subject: [PATCH 32/56] chore: delete unused file --- packages/opencode/src/auth/github-copilot.ts | 147 ------------------- 1 file changed, 147 deletions(-) delete mode 100644 packages/opencode/src/auth/github-copilot.ts diff --git a/packages/opencode/src/auth/github-copilot.ts b/packages/opencode/src/auth/github-copilot.ts deleted file mode 100644 index bd5740c9b..000000000 --- a/packages/opencode/src/auth/github-copilot.ts +++ /dev/null @@ -1,147 +0,0 @@ -import z from "zod" -import { Auth } from "./index" -import { NamedError } from "../util/error" - -export namespace AuthGithubCopilot { - const CLIENT_ID = "Iv1.b507a08c87ecfe98" - const DEVICE_CODE_URL = "https://github.com/login/device/code" - const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token" - const COPILOT_API_KEY_URL = "https://api.github.com/copilot_internal/v2/token" - - interface DeviceCodeResponse { - device_code: string - user_code: string - verification_uri: string - expires_in: number - interval: number - } - - interface AccessTokenResponse { - access_token?: string - error?: string - error_description?: string - } - - interface CopilotTokenResponse { - token: string - expires_at: number - refresh_in: number - endpoints: { - api: string - } - } - - export async function authorize() { - const deviceResponse = await fetch(DEVICE_CODE_URL, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - "User-Agent": "GitHubCopilotChat/0.26.7", - }, - body: JSON.stringify({ - client_id: CLIENT_ID, - scope: "read:user", - }), - }) - const deviceData: DeviceCodeResponse = await deviceResponse.json() - return { - device: deviceData.device_code, - user: deviceData.user_code, - verification: deviceData.verification_uri, - interval: deviceData.interval || 5, - expiry: deviceData.expires_in, - } - } - - export async function poll(device_code: string) { - const response = await fetch(ACCESS_TOKEN_URL, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - "User-Agent": "GitHubCopilotChat/0.26.7", - }, - body: JSON.stringify({ - client_id: CLIENT_ID, - device_code, - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - }), - }) - - if (!response.ok) return "failed" - - const data: AccessTokenResponse = await response.json() - - if (data.access_token) { - // Store the GitHub OAuth token - await Auth.set("github-copilot", { - type: "oauth", - refresh: data.access_token, - access: "", - expires: 0, - }) - return "complete" - } - - if (data.error === "authorization_pending") return "pending" - - if (data.error) return "failed" - - return "pending" - } - - export async function access() { - const info = await Auth.get("github-copilot") - if (!info || info.type !== "oauth") return - if (info.access && info.expires > Date.now()) return info.access - - // Get new Copilot API token - const response = await fetch(COPILOT_API_KEY_URL, { - headers: { - Accept: "application/json", - Authorization: `Bearer ${info.refresh}`, - "User-Agent": "GitHubCopilotChat/0.26.7", - "Editor-Version": "vscode/1.99.3", - "Editor-Plugin-Version": "copilot-chat/0.26.7", - }, - }) - - if (!response.ok) return - - const tokenData: CopilotTokenResponse = await response.json() - - // Store the Copilot API token - await Auth.set("github-copilot", { - type: "oauth", - refresh: info.refresh, - access: tokenData.token, - expires: tokenData.expires_at * 1000, - }) - - return tokenData.token - } - - export const DeviceCodeError = NamedError.create("DeviceCodeError", z.object({})) - - export const TokenExchangeError = NamedError.create( - "TokenExchangeError", - z.object({ - message: z.string(), - }), - ) - - export const AuthenticationError = NamedError.create( - "AuthenticationError", - z.object({ - message: z.string(), - }), - ) - - export const CopilotTokenError = NamedError.create( - "CopilotTokenError", - z.object({ - message: z.string(), - }), - ) -} From 3c56dbcf5840e540eb8970805a6c725cce8e4f85 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 27 Oct 2025 16:15:13 -0500 Subject: [PATCH 33/56] chore: rm comment --- packages/opencode/src/session/prompt.ts | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 7018978e2..26a04cb8e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -191,28 +191,6 @@ export namespace SessionPrompt { processor, }) - // const permUnsub = (() => { - // const handled = new Set() - // const options = [ - // { optionId: "allow_once", kind: "allow_once", name: "Allow once" }, - // { optionId: "allow_always", kind: "allow_always", name: "Always allow" }, - // { optionId: "reject_once", kind: "reject_once", name: "Reject" }, - // ] - // return Bus.subscribe(Permission.Event.Updated, async (event) => { - // const info = event.properties - // if (info.sessionID !== input.sessionID) return - // if (handled.has(info.id)) return - // handled.add(info.id) - // const toolCallId = info.callID ?? info.id - // const metadata = info.metadata ?? {} - // // TODO: emit permission event to bus for ACP to handle - // Permission.respond({ sessionID: info.sessionID, permissionID: info.id, response: "reject" }) - // }) - // })() - // await using _permSub = defer(() => { - // permUnsub?.() - // }) - const params = await Plugin.trigger( "chat.params", { From e3e9fd7aa8fbf5a7d7b21d721d014b15e16c41bb Mon Sep 17 00:00:00 2001 From: Jay V Date: Mon, 27 Oct 2025 17:48:17 -0400 Subject: [PATCH 34/56] docs: edit --- packages/web/astro.config.mjs | 2 +- packages/web/src/content/docs/acp.mdx | 32 +++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index 484807497..7d509cabc 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -85,8 +85,8 @@ export default defineConfig({ "permissions", "lsp", "mcp-servers", - "custom-tools", "acp", + "custom-tools", ], }, diff --git a/packages/web/src/content/docs/acp.mdx b/packages/web/src/content/docs/acp.mdx index ea741faca..15ec1a1f0 100644 --- a/packages/web/src/content/docs/acp.mdx +++ b/packages/web/src/content/docs/acp.mdx @@ -1,16 +1,16 @@ --- -title: Agent Client Protocol +title: ACP Support description: Use OpenCode in any ACP-compatible editor. --- -OpenCode supports the [Agent Client Protocol (ACP)](https://agentclientprotocol.com), allowing you to use it directly in compatible editors and IDEs. - -ACP is an open protocol that standardizes communication between code editors and AI coding agents. Similar to LSP for language servers, ACP allows agents like OpenCode to work seamlessly across different development environments. +OpenCode supports the [Agent Client Protocol](https://agentclientprotocol.com) or (ACP), allowing you to use it directly in compatible editors and IDEs. :::tip -For a list of editors and tools that support ACP, see the [ACP progress report](https://zed.dev/blog/acp-progress-report#available-now). +For a list of editors and tools that support ACP, check out the [ACP progress report](https://zed.dev/blog/acp-progress-report#available-now). ::: +ACP is an open protocol that standardizes communication between code editors and AI coding agents. + --- ## Configure @@ -25,9 +25,9 @@ Below are examples for popular editors that support ACP. ### Zed -Add to your Zed configuration (`~/.config/zed/settings.json`): +Add to your [Zed](https://zed.dev) configuration (`~/.config/zed/settings.json`): -```json +```json title="~/.config/zed/settings.json" { "agent_servers": { "OpenCode": { @@ -38,11 +38,11 @@ Add to your Zed configuration (`~/.config/zed/settings.json`): } ``` -To open it, use the `agent: new thread` action in the Command Palette +To open it, use the `agent: new thread` action in the **Command Palette**. You can also bind a keyboard shortcut by editing your `keymap.json`: -```json +```json title="keymap.json" [ { "bindings": { @@ -56,7 +56,7 @@ You can also bind a keyboard shortcut by editing your `keymap.json`: ### Avante.nvim -Add to your Avante configuration: +Add to your [Avante.nvim](https://github.com/yetone/avante.nvim) configuration: ```lua { @@ -71,7 +71,7 @@ Add to your Avante configuration: If you need to pass environment variables: -```lua +```lua {6-8} { acp_providers = { ["opencode"] = { @@ -87,17 +87,17 @@ If you need to pass environment variables: --- -## Capabilities +## Support OpenCode works the same via ACP as it does in the terminal. All features are supported: +:::note +Some built-in slash commands like `/undo` and `/redo` are currently unsupported. +::: + - Built-in tools (file operations, terminal commands, etc.) - Custom tools and slash commands - MCP servers configured in your OpenCode config - Project-specific rules from `AGENTS.md` - Custom formatters and linters - Agents and permissions system - -:::note -Some built-in slash commands like `/undo` and `/redo` are currently unsupported in ACP mode. -::: From 55453dc606d4225a5b4908b082c6e1fb08df69ff Mon Sep 17 00:00:00 2001 From: Jay V Date: Mon, 27 Oct 2025 17:49:31 -0400 Subject: [PATCH 35/56] Add missing dependencies for desktop package --- bun.lock | 40 +++++++++++++++++++++++++++++++++-- packages/desktop/package.json | 2 +- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 42d088109..012afac4e 100644 --- a/bun.lock +++ b/bun.lock @@ -114,7 +114,7 @@ "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", - "@pierre/precision-diffs": "catalog:", + "@pierre/precision-diffs": "0.3.5", "@shikijs/transformers": "3.9.2", "@solid-primitives/active-element": "2.1.3", "@solid-primitives/event-bus": "1.1.2", @@ -938,7 +938,7 @@ "@petamoriken/float16": ["@petamoriken/float16@3.9.2", "", {}, "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog=="], - "@pierre/precision-diffs": ["@pierre/precision-diffs@0.3.2", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/transformers": "3.13.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.13.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-HE+wFB0TV+wmjur/J+qI5PsRQl5RN6tCEFTusW0S5FDfZJUIpkxJCacqUxyEI0DriXMKhgGQ+oCQShfaFELdrQ=="], + "@pierre/precision-diffs": ["@pierre/precision-diffs@0.3.5", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/transformers": "3.13.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.13.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-qbotIS8CahO/7guljDzU3RVpDfg6WViWe0EB0/SZQi3xHD+nzxxlC+pGoyIFSn+47GG0EKxTnvkfaYANm19FCA=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], @@ -3498,6 +3498,8 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], + "@opencode-ai/ui/@pierre/precision-diffs": ["@pierre/precision-diffs@0.3.2", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/transformers": "3.13.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.13.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-HE+wFB0TV+wmjur/J+qI5PsRQl5RN6tCEFTusW0S5FDfZJUIpkxJCacqUxyEI0DriXMKhgGQ+oCQShfaFELdrQ=="], + "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="], "@opencode-ai/web/@types/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="], @@ -3734,6 +3736,8 @@ "nypm/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "opencode/@pierre/precision-diffs": ["@pierre/precision-diffs@0.3.2", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/transformers": "3.13.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.13.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-HE+wFB0TV+wmjur/J+qI5PsRQl5RN6tCEFTusW0S5FDfZJUIpkxJCacqUxyEI0DriXMKhgGQ+oCQShfaFELdrQ=="], + "opencode/ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="], "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], @@ -4068,6 +4072,10 @@ "@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], + "@opencode-ai/ui/@pierre/precision-diffs/@shikijs/transformers": ["@shikijs/transformers@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0" } }, "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA=="], + + "@opencode-ai/ui/@pierre/precision-diffs/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="], + "@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="], "@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="], @@ -4234,6 +4242,10 @@ "nypm/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + "opencode/@pierre/precision-diffs/@shikijs/transformers": ["@shikijs/transformers@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0" } }, "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA=="], + + "opencode/@pierre/precision-diffs/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="], + "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], @@ -4414,6 +4426,18 @@ "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "@opencode-ai/ui/@pierre/precision-diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="], + + "@opencode-ai/ui/@pierre/precision-diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="], + + "@opencode-ai/ui/@pierre/precision-diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="], + + "@opencode-ai/ui/@pierre/precision-diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="], + + "@opencode-ai/ui/@pierre/precision-diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="], + + "@opencode-ai/ui/@pierre/precision-diffs/shiki/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="], + "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], "@vercel/nft/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -4436,6 +4460,18 @@ "nitropack/serve-static/send/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "opencode/@pierre/precision-diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="], + + "opencode/@pierre/precision-diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="], + + "opencode/@pierre/precision-diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="], + + "opencode/@pierre/precision-diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="], + + "opencode/@pierre/precision-diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="], + + "opencode/@pierre/precision-diffs/shiki/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="], + "opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], diff --git a/packages/desktop/package.json b/packages/desktop/package.json index c4af384f4..77ace500d 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -27,7 +27,7 @@ "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", - "@pierre/precision-diffs": "catalog:", + "@pierre/precision-diffs": "0.3.5", "@shikijs/transformers": "3.9.2", "@solid-primitives/active-element": "2.1.3", "@solid-primitives/event-bus": "1.1.2", From a2951a2702d2f9d71f26a15d59be748038409bad Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 27 Oct 2025 18:03:32 -0400 Subject: [PATCH 36/56] Remove typecheck script from desktop package --- packages/desktop/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 77ace500d..a14b01943 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -7,8 +7,7 @@ "start": "vite", "dev": "vite", "build": "vite build", - "serve": "vite preview", - "typecheck": "tsgo --noEmit" + "serve": "vite preview" }, "license": "MIT", "devDependencies": { From e3e16e58c580a57dd109bf4d4d416af789a21874 Mon Sep 17 00:00:00 2001 From: Jay V Date: Mon, 27 Oct 2025 18:16:25 -0400 Subject: [PATCH 37/56] docs: edit --- packages/web/src/content/docs/providers.mdx | 72 ++++++++++----------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index dbf4b62de..9a5818f70 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -374,42 +374,6 @@ Some models need to be manually enabled in your [GitHub Copilot settings](https: --- -### Groq - -1. Head over to the [Groq console](https://console.groq.com/), click **Create API Key**, and copy the key. - -2. Run `opencode auth login` and select Groq. - - ```bash - $ opencode auth login - - ┌ Add credential - │ - ◆ Select provider - │ ● Groq - │ ... - └ - ``` - -3. Enter the API key for the provider. - - ```bash - $ opencode auth login - - ┌ Add credential - │ - ◇ Select provider - │ Groq - │ - ◇ Enter your API key - │ _ - └ - ``` - -4. Run the `/models` command to select the one you want. - ---- - ### Google Vertex AI To use Google Vertex AI with OpenCode: @@ -446,6 +410,42 @@ To use Google Vertex AI with OpenCode: --- +### Groq + +1. Head over to the [Groq console](https://console.groq.com/), click **Create API Key**, and copy the key. + +2. Run `opencode auth login` and select Groq. + + ```bash + $ opencode auth login + + ┌ Add credential + │ + ◆ Select provider + │ ● Groq + │ ... + └ + ``` + +3. Enter the API key for the provider. + + ```bash + $ opencode auth login + + ┌ Add credential + │ + ◇ Select provider + │ Groq + │ + ◇ Enter your API key + │ _ + └ + ``` + +4. Run the `/models` command to select the one you want. + +--- + ### LM Studio You can configure opencode to use local models through LM Studio. From 7216a8c86d964207083784f2d3d595a5affb9518 Mon Sep 17 00:00:00 2001 From: kcrommett <523952+kcrommett@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:51:33 -0700 Subject: [PATCH 38/56] fix: editor paste functionality for text attachments (#3489) --- packages/tui/internal/components/chat/editor.go | 7 +++++-- packages/tui/internal/tui/tui.go | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index 2841e2cc8..d3c813840 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -48,6 +48,7 @@ type EditorComponent interface { SetInterruptKeyInDebounce(inDebounce bool) SetExitKeyInDebounce(inDebounce bool) RestoreFromHistory(index int) + GetAttachments() []*attachment.Attachment } type editorComponent struct { @@ -471,6 +472,10 @@ func (m *editorComponent) Length() int { return m.textarea.Length() } +func (m *editorComponent) GetAttachments() []*attachment.Attachment { + return m.textarea.GetAttachments() +} + func (m *editorComponent) Submit() (tea.Model, tea.Cmd) { value := strings.TrimSpace(m.Value()) if value == "" { @@ -628,9 +633,7 @@ func (m *editorComponent) SetValueWithAttachments(value string) { } if end > start { filePath := value[start:end] - slog.Debug("test", "filePath", filePath) if _, err := os.Stat(filepath.Join(util.CwdPath, filePath)); err == nil { - slog.Debug("test", "found", true) attachment := m.createAttachmentFromFile(filePath) if attachment != nil { m.textarea.InsertAttachment(attachment) diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 69fa7bdb8..279443674 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -1164,6 +1164,14 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) { } value := a.editor.Value() + + // Expand text attachments before opening editor + for _, att := range a.editor.GetAttachments() { + if textSource, ok := att.GetTextSource(); ok { + value = strings.Replace(value, att.Display, textSource.Value, 1) + } + } + updated, cmd := a.editor.Clear() a.editor = updated.(chat.EditorComponent) cmds = append(cmds, cmd) From 71abca9571b74830908bf5d2aff0c9864b1c5191 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 27 Oct 2025 17:00:49 -0400 Subject: [PATCH 39/56] wip: zen --- .../console/app/src/routes/zen/util/error.ts | 5 + .../console/app/src/routes/zen/util/format.ts | 1 + .../app/src/routes/zen/{ => util}/handler.ts | 115 ++-- .../console/app/src/routes/zen/util/logger.ts | 12 + .../src/routes/zen/util/provider/anthropic.ts | 618 ++++++++++++++++++ .../zen/util/provider/openai-compatible.ts | 541 +++++++++++++++ .../src/routes/zen/util/provider/openai.ts | 600 +++++++++++++++++ .../src/routes/zen/util/provider/provider.ts | 207 ++++++ .../app/src/routes/zen/v1/chat/completions.ts | 58 +- .../console/app/src/routes/zen/v1/messages.ts | 59 +- .../console/app/src/routes/zen/v1/models.ts | 60 ++ .../app/src/routes/zen/v1/responses.ts | 47 +- 12 files changed, 2108 insertions(+), 215 deletions(-) create mode 100644 packages/console/app/src/routes/zen/util/error.ts create mode 100644 packages/console/app/src/routes/zen/util/format.ts rename packages/console/app/src/routes/zen/{ => util}/handler.ts (86%) create mode 100644 packages/console/app/src/routes/zen/util/logger.ts create mode 100644 packages/console/app/src/routes/zen/util/provider/anthropic.ts create mode 100644 packages/console/app/src/routes/zen/util/provider/openai-compatible.ts create mode 100644 packages/console/app/src/routes/zen/util/provider/openai.ts create mode 100644 packages/console/app/src/routes/zen/util/provider/provider.ts create mode 100644 packages/console/app/src/routes/zen/v1/models.ts diff --git a/packages/console/app/src/routes/zen/util/error.ts b/packages/console/app/src/routes/zen/util/error.ts new file mode 100644 index 000000000..dfc7e9fcd --- /dev/null +++ b/packages/console/app/src/routes/zen/util/error.ts @@ -0,0 +1,5 @@ +export class AuthError extends Error {} +export class CreditsError extends Error {} +export class MonthlyLimitError extends Error {} +export class UserLimitError extends Error {} +export class ModelError extends Error {} diff --git a/packages/console/app/src/routes/zen/util/format.ts b/packages/console/app/src/routes/zen/util/format.ts new file mode 100644 index 000000000..53a074969 --- /dev/null +++ b/packages/console/app/src/routes/zen/util/format.ts @@ -0,0 +1 @@ +export type Format = "anthropic" | "openai" | "oa-compat" diff --git a/packages/console/app/src/routes/zen/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts similarity index 86% rename from packages/console/app/src/routes/zen/handler.ts rename to packages/console/app/src/routes/zen/util/handler.ts index 67b03ab00..7fbb518a0 100644 --- a/packages/console/app/src/routes/zen/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -1,67 +1,41 @@ -import { z } from "zod" import type { APIEvent } from "@solidjs/start/server" -import path from "node:path" import { and, Database, eq, isNull, lt, or, sql } from "@opencode-ai/console-core/drizzle/index.js" import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" import { BillingTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js" import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js" import { Identifier } from "@opencode-ai/console-core/identifier.js" -import { Resource } from "@opencode-ai/console-resource" -import { Billing } from "../../../../core/src/billing" +import { Billing } from "@opencode-ai/console-core/billing.js" import { Actor } from "@opencode-ai/console-core/actor.js" import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" import { ZenData } from "@opencode-ai/console-core/model.js" import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js" import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js" +import { logger } from "./logger" +import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError } from "./error" +import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider" +import { Format } from "./format" +import { anthropicHelper } from "./provider/anthropic" +import { openaiHelper } from "./provider/openai" +import { oaCompatHelper } from "./provider/openai-compatible" + +type ZenData = Awaited> +type Model = ZenData["models"][string] export async function handler( input: APIEvent, opts: { - modifyBody?: (body: any) => any - setAuthHeader: (headers: Headers, apiKey: string) => void + format: Format parseApiKey: (headers: Headers) => string | undefined - onStreamPart: (chunk: string) => void - getStreamUsage: () => any - normalizeUsage: (body: any) => { - inputTokens: number - outputTokens: number - reasoningTokens?: number - cacheReadTokens?: number - cacheWrite5mTokens?: number - cacheWrite1hTokens?: number - } }, ) { - class AuthError extends Error {} - class CreditsError extends Error {} - class MonthlyLimitError extends Error {} - class UserLimitError extends Error {} - class ModelError extends Error {} - - type ZenData = Awaited> - type Model = ZenData["models"][string] - const FREE_WORKSPACES = [ "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank "wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench ] - const logger = { - metric: (values: Record) => { - console.log(`_metric:${JSON.stringify(values)}`) - }, - log: console.log, - debug: (message: string) => { - if (Resource.App.stage === "production") return - console.debug(message) - }, - } - try { - const url = new URL(input.request.url) const body = await input.request.json() - logger.debug(JSON.stringify(body)) logger.metric({ is_tream: !!body.stream, session: input.request.headers.get("x-opencode-session"), @@ -78,22 +52,28 @@ export async function handler( // Request to model provider const startTimestamp = Date.now() - const res = await fetch(path.posix.join(providerInfo.api, url.pathname.replace(/^\/zen\/v1/, "") + url.search), { + const reqUrl = providerInfo.modifyUrl(providerInfo.api) + const reqBody = JSON.stringify( + providerInfo.modifyBody({ + ...createBodyConverter(opts.format, providerInfo.format)(body), + model: providerInfo.model, + }), + ) + logger.debug("REQUEST URL: " + reqUrl) + logger.debug("REQUEST: " + reqBody) + const res = await fetch(reqUrl, { method: "POST", headers: (() => { const headers = input.request.headers headers.delete("host") headers.delete("content-length") - opts.setAuthHeader(headers, providerInfo.apiKey) + providerInfo.modifyHeaders(headers, providerInfo.apiKey) Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => { headers.set(k, headers.get(v)!) }) return headers })(), - body: JSON.stringify({ - ...(opts.modifyBody?.(body) ?? body), - model: providerInfo.model, - }), + body: reqBody, }) // Scrub response headers @@ -104,14 +84,19 @@ export async function handler( resHeaders.set(k, v) } } + logger.debug("STATUS: " + res.status + " " + res.statusText) + if (res.status === 400 || res.status === 503) { + logger.debug("RESPONSE: " + (await res.text())) + } // Handle non-streaming response if (!body.stream) { + const responseConverter = createResponseConverter(providerInfo.format, opts.format) const json = await res.json() - const body = JSON.stringify(json) + const body = JSON.stringify(responseConverter(json)) logger.metric({ response_length: body.length }) - logger.debug(body) - await trackUsage(authInfo, modelInfo, providerInfo.id, json.usage) + logger.debug("RESPONSE: " + body) + await trackUsage(authInfo, modelInfo, providerInfo, json.usage) await reload(authInfo) return new Response(body, { status: res.status, @@ -121,10 +106,13 @@ export async function handler( } // Handle streaming response + const streamConverter = createStreamPartConverter(providerInfo.format, opts.format) + const usageParser = providerInfo.createUsageParser() const stream = new ReadableStream({ start(c) { const reader = res.body?.getReader() const decoder = new TextDecoder() + const encoder = new TextEncoder() let buffer = "" let responseLength = 0 @@ -136,9 +124,9 @@ export async function handler( response_length: responseLength, "timestamp.last_byte": Date.now(), }) - const usage = opts.getStreamUsage() + const usage = usageParser.retrieve() if (usage) { - await trackUsage(authInfo, modelInfo, providerInfo.id, usage) + await trackUsage(authInfo, modelInfo, providerInfo, usage) await reload(authInfo) } c.close() @@ -158,12 +146,21 @@ export async function handler( const parts = buffer.split("\n\n") buffer = parts.pop() ?? "" - for (const part of parts) { - logger.debug(part) - opts.onStreamPart(part.trim()) + for (let part of parts) { + logger.debug("PART: " + part) + + part = part.trim() + usageParser.parse(part) + + if (providerInfo.format !== opts.format) { + part = streamConverter(part) + c.enqueue(encoder.encode(part + "\n\n")) + } } - c.enqueue(value) + if (providerInfo.format === opts.format) { + c.enqueue(value) + } return pump() }) || Promise.resolve() @@ -235,7 +232,11 @@ export async function handler( throw new ModelError(`Provider ${provider.id} not supported`) } - return { ...provider, ...zenData.providers[provider.id] } + return { + ...provider, + ...zenData.providers[provider.id], + ...(provider.id === "anthropic" ? anthropicHelper : provider.id === "openai" ? openaiHelper : oaCompatHelper), + } } async function authenticate( @@ -356,11 +357,11 @@ export async function handler( async function trackUsage( authInfo: Awaited>, modelInfo: ReturnType, - providerId: string, + providerInfo: Awaited>, usage: any, ) { const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } = - opts.normalizeUsage(usage) + providerInfo.normalizeUsage(usage) const modelCost = modelInfo.cost200K && @@ -421,7 +422,7 @@ export async function handler( workspaceID: authInfo.workspaceID, id: Identifier.create("usage"), model: modelInfo.id, - provider: providerId, + provider: providerInfo.id, inputTokens, outputTokens, reasoningTokens, diff --git a/packages/console/app/src/routes/zen/util/logger.ts b/packages/console/app/src/routes/zen/util/logger.ts new file mode 100644 index 000000000..aef46ddd0 --- /dev/null +++ b/packages/console/app/src/routes/zen/util/logger.ts @@ -0,0 +1,12 @@ +import { Resource } from "@opencode-ai/console-resource" + +export const logger = { + metric: (values: Record) => { + console.log(`_metric:${JSON.stringify(values)}`) + }, + log: console.log, + debug: (message: string) => { + if (Resource.App.stage === "production") return + console.debug(message) + }, +} diff --git a/packages/console/app/src/routes/zen/util/provider/anthropic.ts b/packages/console/app/src/routes/zen/util/provider/anthropic.ts new file mode 100644 index 000000000..64b040a53 --- /dev/null +++ b/packages/console/app/src/routes/zen/util/provider/anthropic.ts @@ -0,0 +1,618 @@ +import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider" + +type Usage = { + cache_creation?: { + ephemeral_5m_input_tokens?: number + ephemeral_1h_input_tokens?: number + } + cache_creation_input_tokens?: number + cache_read_input_tokens?: number + input_tokens?: number + output_tokens?: number + server_tool_use?: { + web_search_requests?: number + } +} + +export const anthropicHelper = { + format: "anthropic", + modifyUrl: (providerApi: string) => providerApi + "/messages", + modifyHeaders: (headers: Headers, apiKey: string) => { + headers.set("x-api-key", apiKey) + headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01") + }, + modifyBody: (body: Record) => { + return { + ...body, + service_tier: "standard_only", + } + }, + createUsageParser: () => { + let usage: Usage + + return { + parse: (chunk: string) => { + const data = chunk.split("\n")[1] + if (!data.startsWith("data: ")) return + + let json + try { + json = JSON.parse(data.slice(6)) + } catch (e) { + return + } + + const usageUpdate = json.usage ?? json.message?.usage + if (!usageUpdate) return + usage = { + ...usage, + ...usageUpdate, + cache_creation: { + ...usage?.cache_creation, + ...usageUpdate.cache_creation, + }, + server_tool_use: { + ...usage?.server_tool_use, + ...usageUpdate.server_tool_use, + }, + } + }, + retrieve: () => usage, + } + }, + normalizeUsage: (usage: Usage) => ({ + inputTokens: usage.input_tokens ?? 0, + outputTokens: usage.output_tokens ?? 0, + reasoningTokens: undefined, + cacheReadTokens: usage.cache_read_input_tokens ?? undefined, + cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined, + cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined, + }), +} satisfies ProviderHelper + +export function fromAnthropicRequest(body: any): CommonRequest { + if (!body || typeof body !== "object") return body + + const msgs: any[] = [] + + const sys = Array.isArray(body.system) ? body.system : undefined + if (sys && sys.length > 0) { + for (const s of sys) { + if (!s) continue + if ((s as any).type !== "text") continue + if (typeof (s as any).text !== "string") continue + if ((s as any).text.length === 0) continue + msgs.push({ role: "system", content: (s as any).text }) + } + } + + const toImg = (src: any) => { + if (!src || typeof src !== "object") return undefined + if ((src as any).type === "url" && typeof (src as any).url === "string") + return { type: "image_url", image_url: { url: (src as any).url } } + if ( + (src as any).type === "base64" && + typeof (src as any).media_type === "string" && + typeof (src as any).data === "string" + ) + return { type: "image_url", image_url: { url: `data:${(src as any).media_type};base64,${(src as any).data}` } } + return undefined + } + + const inMsgs = Array.isArray(body.messages) ? body.messages : [] + for (const m of inMsgs) { + if (!m || !(m as any).role) continue + + if ((m as any).role === "user") { + const partsIn = Array.isArray((m as any).content) ? (m as any).content : [] + const partsOut: any[] = [] + for (const p of partsIn) { + if (!p || !(p as any).type) continue + if ((p as any).type === "text" && typeof (p as any).text === "string") + partsOut.push({ type: "text", text: (p as any).text }) + if ((p as any).type === "image") { + const ip = toImg((p as any).source) + if (ip) partsOut.push(ip) + } + if ((p as any).type === "tool_result") { + const id = (p as any).tool_use_id + const content = + typeof (p as any).content === "string" ? (p as any).content : JSON.stringify((p as any).content) + msgs.push({ role: "tool", tool_call_id: id, content }) + } + } + if (partsOut.length > 0) { + if (partsOut.length === 1 && partsOut[0].type === "text") msgs.push({ role: "user", content: partsOut[0].text }) + else msgs.push({ role: "user", content: partsOut }) + } + continue + } + + if ((m as any).role === "assistant") { + const partsIn = Array.isArray((m as any).content) ? (m as any).content : [] + const texts: string[] = [] + const tcs: any[] = [] + for (const p of partsIn) { + if (!p || !(p as any).type) continue + if ((p as any).type === "text" && typeof (p as any).text === "string") texts.push((p as any).text) + if ((p as any).type === "tool_use") { + const name = (p as any).name + const id = (p as any).id + const inp = (p as any).input + const input = (() => { + if (typeof inp === "string") return inp + try { + return JSON.stringify(inp ?? {}) + } catch { + return String(inp ?? "") + } + })() + tcs.push({ id, type: "function", function: { name, arguments: input } }) + } + } + const out: any = { role: "assistant", content: texts.join("") } + if (tcs.length > 0) out.tool_calls = tcs + msgs.push(out) + continue + } + } + + const tools = Array.isArray(body.tools) + ? body.tools + .filter((t: any) => t && typeof t === "object" && "input_schema" in t) + .map((t: any) => ({ + type: "function", + function: { name: (t as any).name, description: (t as any).description, parameters: (t as any).input_schema }, + })) + : undefined + + const tcin = body.tool_choice + const tc = (() => { + if (!tcin) return undefined + if ((tcin as any).type === "auto") return "auto" + if ((tcin as any).type === "any") return "required" + if ((tcin as any).type === "tool" && typeof (tcin as any).name === "string") + return { type: "function" as const, function: { name: (tcin as any).name } } + return undefined + })() + + const stop = (() => { + const v = body.stop_sequences + if (!v) return undefined + if (Array.isArray(v)) return v.length === 1 ? v[0] : v + if (typeof v === "string") return v + return undefined + })() + + return { + max_tokens: body.max_tokens, + temperature: body.temperature, + top_p: body.top_p, + stop, + messages: msgs, + stream: !!body.stream, + tools, + tool_choice: tc, + } +} + +export function toAnthropicRequest(body: CommonRequest) { + if (!body || typeof body !== "object") return body + + const sysIn = Array.isArray(body.messages) ? body.messages.filter((m: any) => m && m.role === "system") : [] + let ccCount = 0 + const cc = () => { + ccCount++ + return ccCount <= 4 ? { cache_control: { type: "ephemeral" } } : {} + } + const system = sysIn + .filter((m: any) => typeof m.content === "string" && m.content.length > 0) + .map((m: any) => ({ type: "text", text: m.content, ...cc() })) + + const msgsIn = Array.isArray(body.messages) ? body.messages : [] + const msgsOut: any[] = [] + + const toSrc = (p: any) => { + if (!p || typeof p !== "object") return undefined + if ((p as any).type === "image_url" && (p as any).image_url) { + const u = (p as any).image_url.url ?? (p as any).image_url + if (typeof u === "string" && u.startsWith("data:")) { + const m = u.match(/^data:([^;]+);base64,(.*)$/) + if (m) return { type: "base64", media_type: m[1], data: m[2] } + } + if (typeof u === "string") return { type: "url", url: u } + } + return undefined + } + + for (const m of msgsIn) { + if (!m || !(m as any).role) continue + + if ((m as any).role === "user") { + if (typeof (m as any).content === "string") { + msgsOut.push({ + role: "user", + content: [{ type: "text", text: (m as any).content, ...cc() }], + }) + } else if (Array.isArray((m as any).content)) { + const parts: any[] = [] + for (const p of (m as any).content) { + if (!p || !(p as any).type) continue + if ((p as any).type === "text" && typeof (p as any).text === "string") + parts.push({ type: "text", text: (p as any).text, ...cc() }) + if ((p as any).type === "image_url") { + const s = toSrc(p) + if (s) parts.push({ type: "image", source: s, ...cc() }) + } + } + if (parts.length > 0) msgsOut.push({ role: "user", content: parts }) + } + continue + } + + if ((m as any).role === "assistant") { + const out: any = { role: "assistant", content: [] as any[] } + if (typeof (m as any).content === "string" && (m as any).content.length > 0) { + ;(out.content as any[]).push({ type: "text", text: (m as any).content, ...cc() }) + } + if (Array.isArray((m as any).tool_calls)) { + for (const tc of (m as any).tool_calls) { + if ((tc as any).type === "function" && (tc as any).function) { + let input: any + const a = (tc as any).function.arguments + if (typeof a === "string") { + try { + input = JSON.parse(a) + } catch { + input = a + } + } else input = a + const id = (tc as any).id || `toolu_${Math.random().toString(36).slice(2)}` + ;(out.content as any[]).push({ + type: "tool_use", + id, + name: (tc as any).function.name, + input, + ...cc(), + }) + } + } + } + if ((out.content as any[]).length > 0) msgsOut.push(out) + continue + } + + if ((m as any).role === "tool") { + msgsOut.push({ + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: (m as any).tool_call_id, + content: (m as any).content, + ...cc(), + }, + ], + }) + continue + } + } + + const tools = Array.isArray(body.tools) + ? body.tools + .filter((t: any) => t && typeof t === "object" && (t as any).type === "function") + .map((t: any) => ({ + name: (t as any).function.name, + description: (t as any).function.description, + input_schema: (t as any).function.parameters, + ...cc(), + })) + : undefined + + const tcIn = body.tool_choice + const tool_choice = (() => { + if (!tcIn) return undefined + if (tcIn === "auto") return { type: "auto" } + if (tcIn === "required") return { type: "any" } + if ((tcIn as any).type === "function" && (tcIn as any).function?.name) + return { type: "tool", name: (tcIn as any).function.name } + return undefined + })() + + const stop_sequences = (() => { + const v = body.stop + if (!v) return undefined + if (Array.isArray(v)) return v + if (typeof v === "string") return [v] + return undefined + })() + + return { + max_tokens: body.max_tokens ?? 32_000, + temperature: body.temperature, + top_p: body.top_p, + system: system.length > 0 ? system : undefined, + messages: msgsOut, + stream: !!body.stream, + tools, + tool_choice, + stop_sequences, + } +} + +export function fromAnthropicResponse(resp: any): CommonResponse { + if (!resp || typeof resp !== "object") return resp + + if (Array.isArray((resp as any).choices)) return resp + + const isAnthropic = typeof (resp as any).type === "string" && (resp as any).type === "message" + if (!isAnthropic) return resp + + const idIn = (resp as any).id + const id = + typeof idIn === "string" ? idIn.replace(/^msg_/, "chatcmpl_") : `chatcmpl_${Math.random().toString(36).slice(2)}` + const model = (resp as any).model + + const blocks: any[] = Array.isArray((resp as any).content) ? (resp as any).content : [] + const text = blocks + .filter((b) => b && b.type === "text" && typeof (b as any).text === "string") + .map((b: any) => b.text) + .join("") + const tcs = blocks + .filter((b) => b && b.type === "tool_use") + .map((b: any) => { + const name = (b as any).name + const args = (() => { + const inp = (b as any).input + if (typeof inp === "string") return inp + try { + return JSON.stringify(inp ?? {}) + } catch { + return String(inp ?? "") + } + })() + const tid = + typeof (b as any).id === "string" && (b as any).id.length > 0 + ? (b as any).id + : `toolu_${Math.random().toString(36).slice(2)}` + return { id: tid, type: "function" as const, function: { name, arguments: args } } + }) + + const finish = (r: string | null) => { + if (r === "end_turn") return "stop" + if (r === "tool_use") return "tool_calls" + if (r === "max_tokens") return "length" + if (r === "content_filter") return "content_filter" + return null + } + + const u = (resp as any).usage + const usage = (() => { + if (!u) return undefined as any + const pt = typeof (u as any).input_tokens === "number" ? (u as any).input_tokens : undefined + const ct = typeof (u as any).output_tokens === "number" ? (u as any).output_tokens : undefined + const total = pt != null && ct != null ? pt + ct : undefined + const cached = + typeof (u as any).cache_read_input_tokens === "number" ? (u as any).cache_read_input_tokens : undefined + const details = cached != null ? { cached_tokens: cached } : undefined + return { + prompt_tokens: pt, + completion_tokens: ct, + total_tokens: total, + ...(details ? { prompt_tokens_details: details } : {}), + } + })() + + return { + id, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model, + choices: [ + { + index: 0, + message: { + role: "assistant", + ...(text && text.length > 0 ? { content: text } : {}), + ...(tcs.length > 0 ? { tool_calls: tcs } : {}), + }, + finish_reason: finish((resp as any).stop_reason ?? null), + }, + ], + ...(usage ? { usage } : {}), + } +} + +export function toAnthropicResponse(resp: CommonResponse) { + if (!resp || typeof resp !== "object") return resp + + if (!Array.isArray((resp as any).choices)) return resp + + const choice = (resp as any).choices[0] + if (!choice) return resp + + const message = choice.message + if (!message) return resp + + const content: any[] = [] + + if (typeof message.content === "string" && message.content.length > 0) + content.push({ type: "text", text: message.content }) + + if (Array.isArray(message.tool_calls)) { + for (const tc of message.tool_calls) { + if ((tc as any).type === "function" && (tc as any).function) { + let input: any + try { + input = JSON.parse((tc as any).function.arguments) + } catch { + input = (tc as any).function.arguments + } + content.push({ type: "tool_use", id: (tc as any).id, name: (tc as any).function.name, input }) + } + } + } + + const stop_reason = (() => { + const r = choice.finish_reason + if (r === "stop") return "end_turn" + if (r === "tool_calls") return "tool_use" + if (r === "length") return "max_tokens" + if (r === "content_filter") return "content_filter" + return null + })() + + const usage = (() => { + const u = (resp as any).usage + if (!u) return undefined + return { + input_tokens: u.prompt_tokens, + output_tokens: u.completion_tokens, + cache_read_input_tokens: u.prompt_tokens_details?.cached_tokens, + } + })() + + return { + id: (resp as any).id, + type: "message", + role: "assistant", + content: content.length > 0 ? content : [{ type: "text", text: "" }], + model: (resp as any).model, + stop_reason, + usage, + } +} + +export function fromAnthropicChunk(chunk: string): CommonChunk | string { + // Anthropic sends two lines per part: "event: \n" + "data: " + const lines = chunk.split("\n") + const dataLine = lines.find((l) => l.startsWith("data: ")) + if (!dataLine) return chunk + + let json + try { + json = JSON.parse(dataLine.slice(6)) + } catch { + return chunk + } + + const out: CommonChunk = { + id: json.id ?? json.message?.id ?? "", + object: "chat.completion.chunk", + created: Math.floor(Date.now() / 1000), + model: json.model ?? json.message?.model ?? "", + choices: [], + } + + if (json.type === "content_block_start") { + const cb = json.content_block + if (cb?.type === "text") { + out.choices.push({ index: json.index ?? 0, delta: { role: "assistant", content: "" }, finish_reason: null }) + } else if (cb?.type === "tool_use") { + out.choices.push({ + index: json.index ?? 0, + delta: { + tool_calls: [ + { index: json.index ?? 0, id: cb.id, type: "function", function: { name: cb.name, arguments: "" } }, + ], + }, + finish_reason: null, + }) + } + } + + if (json.type === "content_block_delta") { + const d = json.delta + if (d?.type === "text_delta") { + out.choices.push({ index: json.index ?? 0, delta: { content: d.text }, finish_reason: null }) + } else if (d?.type === "input_json_delta") { + out.choices.push({ + index: json.index ?? 0, + delta: { tool_calls: [{ index: json.index ?? 0, function: { arguments: d.partial_json } }] }, + finish_reason: null, + }) + } + } + + if (json.type === "message_delta") { + const d = json.delta + const finish_reason = (() => { + const r = d?.stop_reason + if (r === "end_turn") return "stop" + if (r === "tool_use") return "tool_calls" + if (r === "max_tokens") return "length" + if (r === "content_filter") return "content_filter" + return null + })() + + out.choices.push({ index: 0, delta: {}, finish_reason }) + } + + if (json.usage) { + const u = json.usage + out.usage = { + prompt_tokens: u.input_tokens, + completion_tokens: u.output_tokens, + total_tokens: (u.input_tokens || 0) + (u.output_tokens || 0), + ...(u.cache_read_input_tokens ? { prompt_tokens_details: { cached_tokens: u.cache_read_input_tokens } } : {}), + } + } + + return out +} + +export function toAnthropicChunk(chunk: CommonChunk): string { + if (!chunk.choices || !Array.isArray(chunk.choices) || chunk.choices.length === 0) { + return JSON.stringify({}) + } + + const choice = chunk.choices[0] + const delta = choice.delta + if (!delta) return JSON.stringify({}) + + const result: any = {} + + if (delta.content) { + result.type = "content_block_delta" + result.index = 0 + result.delta = { type: "text_delta", text: delta.content } + } + + if (delta.tool_calls) { + for (const tc of delta.tool_calls) { + if (tc.function?.name) { + result.type = "content_block_start" + result.index = tc.index ?? 0 + result.content_block = { type: "tool_use", id: tc.id, name: tc.function.name, input: {} } + } else if (tc.function?.arguments) { + result.type = "content_block_delta" + result.index = tc.index ?? 0 + result.delta = { type: "input_json_delta", partial_json: tc.function.arguments } + } + } + } + + if (choice.finish_reason) { + const stop_reason = (() => { + const r = choice.finish_reason + if (r === "stop") return "end_turn" + if (r === "tool_calls") return "tool_use" + if (r === "length") return "max_tokens" + if (r === "content_filter") return "content_filter" + return null + })() + result.type = "message_delta" + result.delta = { stop_reason, stop_sequence: null } + } + + if (chunk.usage) { + const u = chunk.usage + result.usage = { + input_tokens: u.prompt_tokens, + output_tokens: u.completion_tokens, + cache_read_input_tokens: u.prompt_tokens_details?.cached_tokens, + } + } + + return JSON.stringify(result) +} diff --git a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts new file mode 100644 index 000000000..aae6bed57 --- /dev/null +++ b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts @@ -0,0 +1,541 @@ +import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider" + +type Usage = { + prompt_tokens?: number + completion_tokens?: number + total_tokens?: number + // used by moonshot + cached_tokens?: number + // used by xai + prompt_tokens_details?: { + text_tokens?: number + audio_tokens?: number + image_tokens?: number + cached_tokens?: number + } + completion_tokens_details?: { + reasoning_tokens?: number + audio_tokens?: number + accepted_prediction_tokens?: number + rejected_prediction_tokens?: number + } +} + +export const oaCompatHelper = { + format: "oa-compat", + modifyUrl: (providerApi: string) => providerApi + "/chat/completions", + modifyHeaders: (headers: Headers, apiKey: string) => { + headers.set("authorization", `Bearer ${apiKey}`) + }, + modifyBody: (body: Record) => { + return { + ...body, + ...(body.stream ? { stream_options: { include_usage: true } } : {}), + } + }, + createUsageParser: () => { + let usage: Usage + + return { + parse: (chunk: string) => { + if (!chunk.startsWith("data: ")) return + + let json + try { + json = JSON.parse(chunk.slice(6)) as { usage?: Usage } + } catch (e) { + return + } + + if (!json.usage) return + usage = json.usage + }, + retrieve: () => usage, + } + }, + normalizeUsage: (usage: Usage) => { + const inputTokens = usage.prompt_tokens ?? 0 + const outputTokens = usage.completion_tokens ?? 0 + const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? undefined + const cacheReadTokens = usage.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? undefined + return { + inputTokens: inputTokens - (cacheReadTokens ?? 0), + outputTokens, + reasoningTokens, + cacheReadTokens, + cacheWrite5mTokens: undefined, + cacheWrite1hTokens: undefined, + } + }, +} satisfies ProviderHelper + +export function fromOaCompatibleRequest(body: any): CommonRequest { + if (!body || typeof body !== "object") return body + + const msgsIn = Array.isArray(body.messages) ? body.messages : [] + const msgsOut: any[] = [] + + for (const m of msgsIn) { + if (!m || !m.role) continue + + if (m.role === "system") { + if (typeof m.content === "string" && m.content.length > 0) msgsOut.push({ role: "system", content: m.content }) + continue + } + + if (m.role === "user") { + if (typeof m.content === "string") { + msgsOut.push({ role: "user", content: m.content }) + } else if (Array.isArray(m.content)) { + const parts: any[] = [] + for (const p of m.content) { + if (!p || !p.type) continue + if (p.type === "text" && typeof p.text === "string") parts.push({ type: "text", text: p.text }) + if (p.type === "image_url") parts.push({ type: "image_url", image_url: p.image_url }) + } + if (parts.length === 1 && parts[0].type === "text") msgsOut.push({ role: "user", content: parts[0].text }) + else if (parts.length > 0) msgsOut.push({ role: "user", content: parts }) + } + continue + } + + if (m.role === "assistant") { + const out: any = { role: "assistant" } + if (typeof m.content === "string") out.content = m.content + if (Array.isArray(m.tool_calls)) out.tool_calls = m.tool_calls + msgsOut.push(out) + continue + } + + if (m.role === "tool") { + msgsOut.push({ role: "tool", tool_call_id: m.tool_call_id, content: m.content }) + continue + } + } + + return { + max_tokens: body.max_tokens, + temperature: body.temperature, + top_p: body.top_p, + stop: body.stop, + messages: msgsOut, + stream: !!body.stream, + tools: Array.isArray(body.tools) ? body.tools : undefined, + tool_choice: body.tool_choice, + } +} + +export function toOaCompatibleRequest(body: CommonRequest) { + if (!body || typeof body !== "object") return body + + const msgsIn = Array.isArray(body.messages) ? body.messages : [] + const msgsOut: any[] = [] + + const toImg = (p: any) => { + if (!p || typeof p !== "object") return undefined + if (p.type === "image_url" && p.image_url) return { type: "image_url", image_url: p.image_url } + const s = (p as any).source + if (!s || typeof s !== "object") return undefined + if (s.type === "url" && typeof s.url === "string") return { type: "image_url", image_url: { url: s.url } } + if (s.type === "base64" && typeof s.media_type === "string" && typeof s.data === "string") + return { type: "image_url", image_url: { url: `data:${s.media_type};base64,${s.data}` } } + return undefined + } + + for (const m of msgsIn) { + if (!m || !m.role) continue + + if (m.role === "system") { + if (typeof m.content === "string" && m.content.length > 0) msgsOut.push({ role: "system", content: m.content }) + continue + } + + if (m.role === "user") { + if (typeof m.content === "string") { + msgsOut.push({ role: "user", content: m.content }) + continue + } + if (Array.isArray(m.content)) { + const parts: any[] = [] + for (const p of m.content) { + if (!p || !p.type) continue + if (p.type === "text" && typeof p.text === "string") parts.push({ type: "text", text: p.text }) + const ip = toImg(p) + if (ip) parts.push(ip) + } + if (parts.length === 1 && parts[0].type === "text") msgsOut.push({ role: "user", content: parts[0].text }) + else if (parts.length > 0) msgsOut.push({ role: "user", content: parts }) + } + continue + } + + if (m.role === "assistant") { + const out: any = { role: "assistant" } + if (typeof m.content === "string") out.content = m.content + if (Array.isArray(m.tool_calls)) out.tool_calls = m.tool_calls + msgsOut.push(out) + continue + } + + if (m.role === "tool") { + msgsOut.push({ role: "tool", tool_call_id: m.tool_call_id, content: m.content }) + continue + } + } + + const tools = Array.isArray(body.tools) + ? body.tools.map((tool: any) => ({ + type: "function", + function: { + name: tool.name, + description: tool.description, + parameters: tool.parameters, + }, + })) + : undefined + + return { + model: body.model, + max_tokens: body.max_tokens, + temperature: body.temperature, + top_p: body.top_p, + stop: body.stop, + messages: msgsOut, + stream: !!body.stream, + tools, + tool_choice: body.tool_choice, + response_format: (body as any).response_format, + } +} + +export function fromOaCompatibleResponse(resp: any): CommonResponse { + if (!resp || typeof resp !== "object") return resp + + if (!Array.isArray((resp as any).choices)) return resp + + const choice = (resp as any).choices[0] + if (!choice) return resp + + const message = choice.message + if (!message) return resp + + const content: any[] = [] + + if (typeof message.content === "string" && message.content.length > 0) { + content.push({ type: "text", text: message.content }) + } + + if (Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.type === "function" && toolCall.function) { + let input + try { + input = JSON.parse(toolCall.function.arguments) + } catch { + input = toolCall.function.arguments + } + content.push({ + type: "tool_use", + id: toolCall.id, + name: toolCall.function.name, + input, + }) + } + } + } + + const stopReason = (() => { + const reason = choice.finish_reason + if (reason === "stop") return "stop" + if (reason === "tool_calls") return "tool_calls" + if (reason === "length") return "length" + if (reason === "content_filter") return "content_filter" + return null + })() + + const usage = (() => { + const u = (resp as any).usage + if (!u) return undefined + return { + prompt_tokens: u.prompt_tokens, + completion_tokens: u.completion_tokens, + total_tokens: u.total_tokens, + ...(u.prompt_tokens_details?.cached_tokens + ? { prompt_tokens_details: { cached_tokens: u.prompt_tokens_details.cached_tokens } } + : {}), + } + })() + + return { + id: (resp as any).id, + object: "chat.completion" as const, + created: Math.floor(Date.now() / 1000), + model: (resp as any).model, + choices: [ + { + index: 0, + message: { + role: "assistant" as const, + ...(content.length > 0 && content.some((c) => c.type === "text") + ? { + content: content + .filter((c) => c.type === "text") + .map((c: any) => c.text) + .join(""), + } + : {}), + ...(content.length > 0 && content.some((c) => c.type === "tool_use") + ? { + tool_calls: content + .filter((c) => c.type === "tool_use") + .map((c: any) => ({ + id: c.id, + type: "function" as const, + function: { + name: c.name, + arguments: typeof c.input === "string" ? c.input : JSON.stringify(c.input), + }, + })), + } + : {}), + }, + finish_reason: stopReason, + }, + ], + ...(usage ? { usage } : {}), + } +} + +export function toOaCompatibleResponse(resp: CommonResponse) { + if (!resp || typeof resp !== "object") return resp + + if (Array.isArray((resp as any).choices)) return resp + + const isAnthropic = typeof (resp as any).type === "string" && (resp as any).type === "message" + if (!isAnthropic) return resp + + const idIn = (resp as any).id + const id = + typeof idIn === "string" ? idIn.replace(/^msg_/, "chatcmpl_") : `chatcmpl_${Math.random().toString(36).slice(2)}` + const model = (resp as any).model + + const blocks: any[] = Array.isArray((resp as any).content) ? (resp as any).content : [] + const text = blocks + .filter((b) => b && b.type === "text" && typeof b.text === "string") + .map((b) => b.text) + .join("") + const tcs = blocks + .filter((b) => b && b.type === "tool_use") + .map((b) => { + const name = (b as any).name + const args = (() => { + const inp = (b as any).input + if (typeof inp === "string") return inp + try { + return JSON.stringify(inp ?? {}) + } catch { + return String(inp ?? "") + } + })() + const tid = + typeof (b as any).id === "string" && (b as any).id.length > 0 + ? (b as any).id + : `toolu_${Math.random().toString(36).slice(2)}` + return { id: tid, type: "function" as const, function: { name, arguments: args } } + }) + + const finish = (r: string | null) => { + if (r === "end_turn") return "stop" + if (r === "tool_use") return "tool_calls" + if (r === "max_tokens") return "length" + if (r === "content_filter") return "content_filter" + return null + } + + const u = (resp as any).usage + const usage = (() => { + if (!u) return undefined as any + const pt = typeof u.input_tokens === "number" ? u.input_tokens : undefined + const ct = typeof u.output_tokens === "number" ? u.output_tokens : undefined + const total = pt != null && ct != null ? pt + ct : undefined + const cached = typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : undefined + const details = cached != null ? { cached_tokens: cached } : undefined + return { + prompt_tokens: pt, + completion_tokens: ct, + total_tokens: total, + ...(details ? { prompt_tokens_details: details } : {}), + } + })() + + return { + id, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model, + choices: [ + { + index: 0, + message: { + role: "assistant", + ...(text && text.length > 0 ? { content: text } : {}), + ...(tcs.length > 0 ? { tool_calls: tcs } : {}), + }, + finish_reason: finish((resp as any).stop_reason ?? null), + }, + ], + ...(usage ? { usage } : {}), + } +} + +export function fromOaCompatibleChunk(chunk: string): CommonChunk | string { + if (!chunk.startsWith("data: ")) return chunk + + let json + try { + json = JSON.parse(chunk.slice(6)) + } catch { + return chunk + } + + if (!json.choices || !Array.isArray(json.choices) || json.choices.length === 0) { + return chunk + } + + const choice = json.choices[0] + const delta = choice.delta + + if (!delta) return chunk + + const result: CommonChunk = { + id: json.id ?? "", + object: "chat.completion.chunk", + created: json.created ?? Math.floor(Date.now() / 1000), + model: json.model ?? "", + choices: [], + } + + if (delta.content) { + result.choices.push({ + index: choice.index ?? 0, + delta: { content: delta.content }, + finish_reason: null, + }) + } + + if (delta.tool_calls) { + for (const toolCall of delta.tool_calls) { + result.choices.push({ + index: choice.index ?? 0, + delta: { + tool_calls: [ + { + index: toolCall.index ?? 0, + id: toolCall.id, + type: toolCall.type ?? "function", + function: toolCall.function, + }, + ], + }, + finish_reason: null, + }) + } + } + + if (choice.finish_reason) { + result.choices.push({ + index: choice.index ?? 0, + delta: {}, + finish_reason: choice.finish_reason, + }) + } + + if (json.usage) { + const usage = json.usage + result.usage = { + prompt_tokens: usage.prompt_tokens, + completion_tokens: usage.completion_tokens, + total_tokens: usage.total_tokens, + ...(usage.prompt_tokens_details?.cached_tokens + ? { prompt_tokens_details: { cached_tokens: usage.prompt_tokens_details.cached_tokens } } + : {}), + } + } + + return result +} + +export function toOaCompatibleChunk(chunk: CommonChunk): string { + const result: any = { + id: chunk.id, + object: "chat.completion.chunk", + created: chunk.created, + model: chunk.model, + choices: [], + } + + if (!chunk.choices || chunk.choices.length === 0) { + return `data: ${JSON.stringify(result)}` + } + + const choice = chunk.choices[0] + const delta = choice.delta + + if (delta?.role) { + result.choices.push({ + index: choice.index, + delta: { role: delta.role }, + finish_reason: null, + }) + } + + if (delta?.content) { + result.choices.push({ + index: choice.index, + delta: { content: delta.content }, + finish_reason: null, + }) + } + + if (delta?.tool_calls) { + for (const tc of delta.tool_calls) { + result.choices.push({ + index: choice.index, + delta: { + tool_calls: [ + { + index: tc.index, + id: tc.id, + type: tc.type, + function: tc.function, + }, + ], + }, + finish_reason: null, + }) + } + } + + if (choice.finish_reason) { + result.choices.push({ + index: choice.index, + delta: {}, + finish_reason: choice.finish_reason, + }) + } + + if (chunk.usage) { + result.usage = { + prompt_tokens: chunk.usage.prompt_tokens, + completion_tokens: chunk.usage.completion_tokens, + total_tokens: chunk.usage.total_tokens, + ...(chunk.usage.prompt_tokens_details?.cached_tokens + ? { + prompt_tokens_details: { cached_tokens: chunk.usage.prompt_tokens_details.cached_tokens }, + } + : {}), + } + } + + return `data: ${JSON.stringify(result)}` +} diff --git a/packages/console/app/src/routes/zen/util/provider/openai.ts b/packages/console/app/src/routes/zen/util/provider/openai.ts new file mode 100644 index 000000000..9781d821d --- /dev/null +++ b/packages/console/app/src/routes/zen/util/provider/openai.ts @@ -0,0 +1,600 @@ +import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider" + +type Usage = { + input_tokens?: number + input_tokens_details?: { + cached_tokens?: number + } + output_tokens?: number + output_tokens_details?: { + reasoning_tokens?: number + } + total_tokens?: number +} + +export const openaiHelper = { + format: "openai", + modifyUrl: (providerApi: string) => providerApi + "/responses", + modifyHeaders: (headers: Headers, apiKey: string) => { + headers.set("authorization", `Bearer ${apiKey}`) + }, + modifyBody: (body: Record) => { + return body + }, + createUsageParser: () => { + let usage: Usage + + return { + parse: (chunk: string) => { + const [event, data] = chunk.split("\n") + if (event !== "event: response.completed") return + if (!data.startsWith("data: ")) return + + let json + try { + json = JSON.parse(data.slice(6)) as { response?: { usage?: Usage } } + } catch (e) { + return + } + + if (!json.response?.usage) return + usage = json.response.usage + }, + retrieve: () => usage, + } + }, + normalizeUsage: (usage: Usage) => { + const inputTokens = usage.input_tokens ?? 0 + const outputTokens = usage.output_tokens ?? 0 + const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? undefined + const cacheReadTokens = usage.input_tokens_details?.cached_tokens ?? undefined + return { + inputTokens: inputTokens - (cacheReadTokens ?? 0), + outputTokens: outputTokens - (reasoningTokens ?? 0), + reasoningTokens, + cacheReadTokens, + cacheWrite5mTokens: undefined, + cacheWrite1hTokens: undefined, + } + }, +} satisfies ProviderHelper + +export function fromOpenaiRequest(body: any): CommonRequest { + if (!body || typeof body !== "object") return body + + const toImg = (p: any) => { + if (!p || typeof p !== "object") return undefined + if ((p as any).type === "image_url" && (p as any).image_url) + return { type: "image_url", image_url: (p as any).image_url } + if ((p as any).type === "input_image" && (p as any).image_url) + return { type: "image_url", image_url: (p as any).image_url } + const s = (p as any).source + if (!s || typeof s !== "object") return undefined + if ((s as any).type === "url" && typeof (s as any).url === "string") + return { type: "image_url", image_url: { url: (s as any).url } } + if ( + (s as any).type === "base64" && + typeof (s as any).media_type === "string" && + typeof (s as any).data === "string" + ) + return { type: "image_url", image_url: { url: `data:${(s as any).media_type};base64,${(s as any).data}` } } + return undefined + } + + const msgs: any[] = [] + + const inMsgs = Array.isArray(body.input) ? body.input : Array.isArray(body.messages) ? body.messages : [] + + for (const m of inMsgs) { + if (!m) continue + + // Responses API items without role: + if (!(m as any).role && (m as any).type) { + if ((m as any).type === "function_call") { + const name = (m as any).name + const a = (m as any).arguments + const args = typeof a === "string" ? a : JSON.stringify(a ?? {}) + msgs.push({ + role: "assistant", + tool_calls: [{ id: (m as any).id, type: "function", function: { name, arguments: args } }], + }) + } + if ((m as any).type === "function_call_output") { + const id = (m as any).call_id + const out = (m as any).output + const content = typeof out === "string" ? out : JSON.stringify(out) + msgs.push({ role: "tool", tool_call_id: id, content }) + } + continue + } + + if ((m as any).role === "system" || (m as any).role === "developer") { + const c = (m as any).content + if (typeof c === "string" && c.length > 0) msgs.push({ role: "system", content: c }) + if (Array.isArray(c)) { + const t = c.find((p: any) => p && typeof p.text === "string") + if (t && typeof t.text === "string" && t.text.length > 0) msgs.push({ role: "system", content: t.text }) + } + continue + } + + if ((m as any).role === "user") { + const c = (m as any).content + if (typeof c === "string") { + msgs.push({ role: "user", content: c }) + } else if (Array.isArray(c)) { + const parts: any[] = [] + for (const p of c) { + if (!p || !(p as any).type) continue + if (((p as any).type === "text" || (p as any).type === "input_text") && typeof (p as any).text === "string") + parts.push({ type: "text", text: (p as any).text }) + const ip = toImg(p) + if (ip) parts.push(ip) + if ((p as any).type === "tool_result") { + const id = (p as any).tool_call_id + const content = + typeof (p as any).content === "string" ? (p as any).content : JSON.stringify((p as any).content) + msgs.push({ role: "tool", tool_call_id: id, content }) + } + } + if (parts.length === 1 && parts[0].type === "text") msgs.push({ role: "user", content: parts[0].text }) + else if (parts.length > 0) msgs.push({ role: "user", content: parts }) + } + continue + } + + if ((m as any).role === "assistant") { + const c = (m as any).content + const out: any = { role: "assistant" } + if (typeof c === "string" && c.length > 0) out.content = c + if (Array.isArray((m as any).tool_calls)) out.tool_calls = (m as any).tool_calls + msgs.push(out) + continue + } + + if ((m as any).role === "tool") { + msgs.push({ role: "tool", tool_call_id: (m as any).tool_call_id, content: (m as any).content }) + continue + } + } + + const tcIn = body.tool_choice + const tc = (() => { + if (!tcIn) return undefined + if (tcIn === "auto") return "auto" + if (tcIn === "required") return "required" + if ((tcIn as any).type === "function" && (tcIn as any).function?.name) + return { type: "function" as const, function: { name: (tcIn as any).function.name } } + return undefined + })() + + const stop = (() => { + const v = body.stop_sequences ?? body.stop + if (!v) return undefined + if (Array.isArray(v)) return v.length === 1 ? v[0] : v + if (typeof v === "string") return v + return undefined + })() + + return { + max_tokens: body.max_output_tokens ?? body.max_tokens, + temperature: body.temperature, + top_p: body.top_p, + stop, + messages: msgs, + stream: !!body.stream, + tools: Array.isArray(body.tools) ? body.tools : undefined, + tool_choice: tc, + } +} + +export function toOpenaiRequest(body: CommonRequest) { + if (!body || typeof body !== "object") return body + + const msgsIn = Array.isArray(body.messages) ? body.messages : [] + const input: any[] = [] + + const toPart = (p: any) => { + if (!p || typeof p !== "object") return undefined + if ((p as any).type === "text" && typeof (p as any).text === "string") + return { type: "input_text", text: (p as any).text } + if ((p as any).type === "image_url" && (p as any).image_url) + return { type: "input_image", image_url: (p as any).image_url } + const s = (p as any).source + if (!s || typeof s !== "object") return undefined + if ((s as any).type === "url" && typeof (s as any).url === "string") + return { type: "input_image", image_url: { url: (s as any).url } } + if ( + (s as any).type === "base64" && + typeof (s as any).media_type === "string" && + typeof (s as any).data === "string" + ) + return { type: "input_image", image_url: { url: `data:${(s as any).media_type};base64,${(s as any).data}` } } + return undefined + } + + for (const m of msgsIn) { + if (!m || !(m as any).role) continue + + if ((m as any).role === "system") { + const c = (m as any).content + if (typeof c === "string") input.push({ role: "system", content: c }) + continue + } + + if ((m as any).role === "user") { + const c = (m as any).content + if (typeof c === "string") { + input.push({ role: "user", content: [{ type: "input_text", text: c }] }) + } else if (Array.isArray(c)) { + const parts: any[] = [] + for (const p of c) { + const op = toPart(p) + if (op) parts.push(op) + } + if (parts.length > 0) input.push({ role: "user", content: parts }) + } + continue + } + + if ((m as any).role === "assistant") { + const c = (m as any).content + if (typeof c === "string" && c.length > 0) { + input.push({ role: "assistant", content: [{ type: "output_text", text: c }] }) + } + if (Array.isArray((m as any).tool_calls)) { + for (const tc of (m as any).tool_calls) { + if ((tc as any).type === "function" && (tc as any).function) { + const name = (tc as any).function.name + const a = (tc as any).function.arguments + const args = typeof a === "string" ? a : JSON.stringify(a) + input.push({ type: "function_call", call_id: (tc as any).id, name, arguments: args }) + } + } + } + continue + } + + if ((m as any).role === "tool") { + const out = typeof (m as any).content === "string" ? (m as any).content : JSON.stringify((m as any).content) + input.push({ type: "function_call_output", call_id: (m as any).tool_call_id, output: out }) + continue + } + } + + const stop_sequences = (() => { + const v = body.stop + if (!v) return undefined + if (Array.isArray(v)) return v + if (typeof v === "string") return [v] + return undefined + })() + + const tcIn = body.tool_choice + const tool_choice = (() => { + if (!tcIn) return undefined + if (tcIn === "auto") return "auto" + if (tcIn === "required") return "required" + if ((tcIn as any).type === "function" && (tcIn as any).function?.name) + return { type: "function", function: { name: (tcIn as any).function.name } } + return undefined + })() + + const tools = (() => { + if (!Array.isArray(body.tools)) return undefined + return body.tools.map((tool: any) => { + if (tool.type === "function") { + return { + type: "function", + name: tool.function?.name, + description: tool.function?.description, + parameters: tool.function?.parameters, + strict: tool.function?.strict, + } + } + return tool + }) + })() + + return { + model: body.model, + input, + max_output_tokens: body.max_tokens, + top_p: body.top_p, + stop_sequences, + stream: !!body.stream, + tools, + tool_choice, + include: Array.isArray((body as any).include) ? (body as any).include : undefined, + truncation: (body as any).truncation, + metadata: (body as any).metadata, + store: (body as any).store, + user: (body as any).user, + text: { verbosity: "low" }, + reasoning: { effort: "medium" }, + } +} + +export function fromOpenaiResponse(resp: any): CommonResponse { + if (!resp || typeof resp !== "object") return resp + if (Array.isArray((resp as any).choices)) return resp + + const r = (resp as any).response ?? resp + if (!r || typeof r !== "object") return resp + + const idIn = (r as any).id + const id = + typeof idIn === "string" ? idIn.replace(/^resp_/, "chatcmpl_") : `chatcmpl_${Math.random().toString(36).slice(2)}` + const model = (r as any).model ?? (resp as any).model + + const out = Array.isArray((r as any).output) ? (r as any).output : [] + const text = out + .filter((o: any) => o && o.type === "message" && Array.isArray((o as any).content)) + .flatMap((o: any) => (o as any).content) + .filter((p: any) => p && p.type === "output_text" && typeof p.text === "string") + .map((p: any) => p.text) + .join("") + + const tcs = out + .filter((o: any) => o && o.type === "function_call") + .map((o: any) => { + const name = (o as any).name + const a = (o as any).arguments + const args = typeof a === "string" ? a : JSON.stringify(a ?? {}) + const tid = + typeof (o as any).id === "string" && (o as any).id.length > 0 + ? (o as any).id + : `toolu_${Math.random().toString(36).slice(2)}` + return { id: tid, type: "function" as const, function: { name, arguments: args } } + }) + + const finish = (r: string | null) => { + if (r === "stop") return "stop" + if (r === "tool_call" || r === "tool_calls") return "tool_calls" + if (r === "length" || r === "max_output_tokens") return "length" + if (r === "content_filter") return "content_filter" + return null + } + + const u = (r as any).usage ?? (resp as any).usage + const usage = (() => { + if (!u) return undefined as any + const pt = typeof (u as any).input_tokens === "number" ? (u as any).input_tokens : undefined + const ct = typeof (u as any).output_tokens === "number" ? (u as any).output_tokens : undefined + const total = pt != null && ct != null ? pt + ct : undefined + const cached = (u as any).input_tokens_details?.cached_tokens + const details = typeof cached === "number" ? { cached_tokens: cached } : undefined + return { + prompt_tokens: pt, + completion_tokens: ct, + total_tokens: total, + ...(details ? { prompt_tokens_details: details } : {}), + } + })() + + return { + id, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model, + choices: [ + { + index: 0, + message: { + role: "assistant", + ...(text && text.length > 0 ? { content: text } : {}), + ...(tcs.length > 0 ? { tool_calls: tcs } : {}), + }, + finish_reason: finish((r as any).stop_reason ?? null), + }, + ], + ...(usage ? { usage } : {}), + } +} + +export function toOpenaiResponse(resp: CommonResponse) { + if (!resp || typeof resp !== "object") return resp + if (!Array.isArray((resp as any).choices)) return resp + + const choice = (resp as any).choices[0] + if (!choice) return resp + + const msg = choice.message + if (!msg) return resp + + const outputItems: any[] = [] + + if (typeof msg.content === "string" && msg.content.length > 0) { + outputItems.push({ + id: `msg_${Math.random().toString(36).slice(2)}`, + type: "message", + status: "completed", + role: "assistant", + content: [{ type: "output_text", text: msg.content, annotations: [], logprobs: [] }], + }) + } + + if (Array.isArray(msg.tool_calls)) { + for (const tc of msg.tool_calls) { + if ((tc as any).type === "function" && (tc as any).function) { + outputItems.push({ + id: (tc as any).id, + type: "function_call", + name: (tc as any).function.name, + call_id: (tc as any).id, + arguments: (tc as any).function.arguments, + }) + } + } + } + + const stop_reason = (() => { + const r = choice.finish_reason + if (r === "stop") return "stop" + if (r === "tool_calls") return "tool_call" + if (r === "length") return "max_output_tokens" + if (r === "content_filter") return "content_filter" + return null + })() + + const usage = (() => { + const u = (resp as any).usage + if (!u) return undefined + return { + input_tokens: u.prompt_tokens, + output_tokens: u.completion_tokens, + total_tokens: u.total_tokens, + ...(u.prompt_tokens_details?.cached_tokens + ? { input_tokens_details: { cached_tokens: u.prompt_tokens_details.cached_tokens } } + : {}), + } + })() + + return { + id: (resp as any).id?.replace(/^chatcmpl_/, "resp_") ?? `resp_${Math.random().toString(36).slice(2)}`, + object: "response", + model: (resp as any).model, + output: outputItems, + stop_reason, + usage, + } +} + +export function fromOpenaiChunk(chunk: string): CommonChunk | string { + const lines = chunk.split("\n") + const ev = lines[0] + const dl = lines[1] + if (!ev || !dl || !dl.startsWith("data: ")) return chunk + + let json: any + try { + json = JSON.parse(dl.slice(6)) + } catch { + return chunk + } + + const respObj = json.response ?? {} + + const out: CommonChunk = { + id: respObj.id ?? json.id ?? "", + object: "chat.completion.chunk", + created: Math.floor(Date.now() / 1000), + model: respObj.model ?? json.model ?? "", + choices: [], + } + + const e = ev.replace("event: ", "").trim() + + if (e === "response.output_text.delta") { + const d = (json as any).delta ?? (json as any).text ?? (json as any).output_text_delta + if (typeof d === "string" && d.length > 0) + out.choices.push({ index: 0, delta: { content: d }, finish_reason: null }) + } + + if (e === "response.output_item.added" && (json as any).item?.type === "function_call") { + const name = (json as any).item?.name + const id = (json as any).item?.id + if (typeof name === "string" && name.length > 0) { + out.choices.push({ + index: 0, + delta: { tool_calls: [{ index: 0, id, type: "function", function: { name, arguments: "" } }] }, + finish_reason: null, + }) + } + } + + if (e === "response.function_call_arguments.delta") { + const a = (json as any).delta ?? (json as any).arguments_delta + if (typeof a === "string" && a.length > 0) { + out.choices.push({ + index: 0, + delta: { tool_calls: [{ index: 0, function: { arguments: a } }] }, + finish_reason: null, + }) + } + } + + if (e === "response.completed") { + const fr = (() => { + const sr = (respObj as any).stop_reason ?? (json as any).stop_reason + if (sr === "stop") return "stop" + if (sr === "tool_call" || sr === "tool_calls") return "tool_calls" + if (sr === "length" || sr === "max_output_tokens") return "length" + if (sr === "content_filter") return "content_filter" + return null + })() + out.choices.push({ index: 0, delta: {}, finish_reason: fr }) + + const u = (respObj as any).usage ?? (json as any).response?.usage + if (u) { + out.usage = { + prompt_tokens: u.input_tokens, + completion_tokens: u.output_tokens, + total_tokens: (u.input_tokens || 0) + (u.output_tokens || 0), + ...(u.input_tokens_details?.cached_tokens + ? { prompt_tokens_details: { cached_tokens: u.input_tokens_details.cached_tokens } } + : {}), + } + } + } + + return out +} + +export function toOpenaiChunk(chunk: CommonChunk): string { + if (!chunk.choices || !Array.isArray(chunk.choices) || chunk.choices.length === 0) { + return "" + } + + const choice = chunk.choices[0] + const d = choice.delta + if (!d) return "" + + const id = chunk.id + const model = chunk.model + + if (d.content) { + const data = { id, type: "response.output_text.delta", delta: d.content, response: { id, model } } + return `event: response.output_text.delta\ndata: ${JSON.stringify(data)}` + } + + if (d.tool_calls) { + for (const tc of d.tool_calls) { + if (tc.function?.name) { + const data = { + type: "response.output_item.added", + output_index: 0, + item: { id: tc.id, type: "function_call", name: tc.function.name, call_id: tc.id, arguments: "" }, + } + return `event: response.output_item.added\ndata: ${JSON.stringify(data)}` + } + if (tc.function?.arguments) { + const data = { + type: "response.function_call_arguments.delta", + output_index: 0, + delta: tc.function.arguments, + } + return `event: response.function_call_arguments.delta\ndata: ${JSON.stringify(data)}` + } + } + } + + if (choice.finish_reason) { + const u = chunk.usage + const usage = u + ? { + input_tokens: u.prompt_tokens, + output_tokens: u.completion_tokens, + total_tokens: u.total_tokens, + ...(u.prompt_tokens_details?.cached_tokens + ? { input_tokens_details: { cached_tokens: u.prompt_tokens_details.cached_tokens } } + : {}), + } + : undefined + + const data: any = { id, type: "response.completed", response: { id, model, ...(usage ? { usage } : {}) } } + return `event: response.completed\ndata: ${JSON.stringify(data)}` + } + + return "" +} diff --git a/packages/console/app/src/routes/zen/util/provider/provider.ts b/packages/console/app/src/routes/zen/util/provider/provider.ts new file mode 100644 index 000000000..5beb460e9 --- /dev/null +++ b/packages/console/app/src/routes/zen/util/provider/provider.ts @@ -0,0 +1,207 @@ +import { Format } from "../format" + +import { + fromAnthropicChunk, + fromAnthropicRequest, + fromAnthropicResponse, + toAnthropicChunk, + toAnthropicRequest, + toAnthropicResponse, +} from "./anthropic" +import { + fromOpenaiChunk, + fromOpenaiRequest, + fromOpenaiResponse, + toOpenaiChunk, + toOpenaiRequest, + toOpenaiResponse, +} from "./openai" +import { + fromOaCompatibleChunk, + fromOaCompatibleRequest, + fromOaCompatibleResponse, + toOaCompatibleChunk, + toOaCompatibleRequest, + toOaCompatibleResponse, +} from "./openai-compatible" + +export type ProviderHelper = { + format: Format + modifyUrl: (providerApi: string) => string + modifyHeaders: (headers: Headers, apiKey: string) => void + modifyBody: (body: Record) => Record + createUsageParser: () => { + parse: (chunk: string) => void + retrieve: () => any + } + normalizeUsage: (usage: any) => { + inputTokens: number + outputTokens: number + reasoningTokens?: number + cacheReadTokens?: number + cacheWrite5mTokens?: number + cacheWrite1hTokens?: number + } +} + +export interface CommonMessage { + role: "system" | "user" | "assistant" | "tool" + content?: string | Array + tool_call_id?: string + tool_calls?: CommonToolCall[] +} + +export interface CommonContentPart { + type: "text" | "image_url" + text?: string + image_url?: { url: string } +} + +export interface CommonToolCall { + id: string + type: "function" + function: { + name: string + arguments: string + } +} + +export interface CommonTool { + type: "function" + function: { + name: string + description?: string + parameters?: Record + } +} + +export interface CommonUsage { + input_tokens?: number + output_tokens?: number + total_tokens?: number + prompt_tokens?: number + completion_tokens?: number + cache_read_input_tokens?: number + cache_creation?: { + ephemeral_5m_input_tokens?: number + ephemeral_1h_input_tokens?: number + } + input_tokens_details?: { + cached_tokens?: number + } + output_tokens_details?: { + reasoning_tokens?: number + } +} + +export interface CommonRequest { + model?: string + max_tokens?: number + temperature?: number + top_p?: number + stop?: string | string[] + messages: CommonMessage[] + stream?: boolean + tools?: CommonTool[] + tool_choice?: "auto" | "required" | { type: "function"; function: { name: string } } +} + +export interface CommonResponse { + id: string + object: "chat.completion" + created: number + model: string + choices: Array<{ + index: number + message: { + role: "assistant" + content?: string + tool_calls?: CommonToolCall[] + } + finish_reason: "stop" | "tool_calls" | "length" | "content_filter" | null + }> + usage?: { + prompt_tokens?: number + completion_tokens?: number + total_tokens?: number + prompt_tokens_details?: { cached_tokens?: number } + } +} + +export interface CommonChunk { + id: string + object: "chat.completion.chunk" + created: number + model: string + choices: Array<{ + index: number + delta: { + role?: "assistant" + content?: string + tool_calls?: Array<{ + index: number + id?: string + type?: "function" + function?: { + name?: string + arguments?: string + } + }> + } + finish_reason: "stop" | "tool_calls" | "length" | "content_filter" | null + }> + usage?: { + prompt_tokens?: number + completion_tokens?: number + total_tokens?: number + prompt_tokens_details?: { cached_tokens?: number } + } +} + +export function createBodyConverter(from: Format, to: Format) { + return (body: any): any => { + if (from === to) return body + + let raw: CommonRequest + if (from === "anthropic") raw = fromAnthropicRequest(body) + else if (from === "openai") raw = fromOpenaiRequest(body) + else raw = fromOaCompatibleRequest(body) + + if (to === "anthropic") return toAnthropicRequest(raw) + if (to === "openai") return toOpenaiRequest(raw) + if (to === "oa-compat") return toOaCompatibleRequest(raw) + } +} + +export function createStreamPartConverter(from: Format, to: Format) { + return (part: any): any => { + if (from === to) return part + + let raw: CommonChunk | string + if (from === "anthropic") raw = fromAnthropicChunk(part) + else if (from === "openai") raw = fromOpenaiChunk(part) + else raw = fromOaCompatibleChunk(part) + + // If result is a string (error case), pass it through + if (typeof raw === "string") return raw + + if (to === "anthropic") return toAnthropicChunk(raw) + if (to === "openai") return toOpenaiChunk(raw) + if (to === "oa-compat") return toOaCompatibleChunk(raw) + } +} + +export function createResponseConverter(from: Format, to: Format) { + return (response: any): any => { + if (from === to) return response + + let raw: CommonResponse + if (from === "anthropic") raw = fromAnthropicResponse(response) + else if (from === "openai") raw = fromOpenaiResponse(response) + else raw = fromOaCompatibleResponse(response) + + if (to === "anthropic") return toAnthropicResponse(raw) + if (to === "openai") return toOpenaiResponse(raw) + if (to === "oa-compat") return toOaCompatibleResponse(raw) + } +} diff --git a/packages/console/app/src/routes/zen/v1/chat/completions.ts b/packages/console/app/src/routes/zen/v1/chat/completions.ts index 33c16247e..44326e79e 100644 --- a/packages/console/app/src/routes/zen/v1/chat/completions.ts +++ b/packages/console/app/src/routes/zen/v1/chat/completions.ts @@ -1,63 +1,9 @@ import type { APIEvent } from "@solidjs/start/server" -import { handler } from "~/routes/zen/handler" - -type Usage = { - prompt_tokens?: number - completion_tokens?: number - total_tokens?: number - // used by moonshot - cached_tokens?: number - // used by xai - prompt_tokens_details?: { - text_tokens?: number - audio_tokens?: number - image_tokens?: number - cached_tokens?: number - } - completion_tokens_details?: { - reasoning_tokens?: number - audio_tokens?: number - accepted_prediction_tokens?: number - rejected_prediction_tokens?: number - } -} +import { handler } from "~/routes/zen/util/handler" export function POST(input: APIEvent) { - let usage: Usage return handler(input, { - modifyBody: (body: any) => ({ - ...body, - ...(body.stream ? { stream_options: { include_usage: true } } : {}), - }), - setAuthHeader: (headers: Headers, apiKey: string) => { - headers.set("authorization", `Bearer ${apiKey}`) - }, + format: "oa-compat", parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1], - onStreamPart: (chunk: string) => { - if (!chunk.startsWith("data: ")) return - - let json - try { - json = JSON.parse(chunk.slice(6)) as { usage?: Usage } - } catch (e) { - return - } - - if (!json.usage) return - usage = json.usage - }, - getStreamUsage: () => usage, - normalizeUsage: (usage: Usage) => { - const inputTokens = usage.prompt_tokens ?? 0 - const outputTokens = usage.completion_tokens ?? 0 - const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? undefined - const cacheReadTokens = usage.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? undefined - return { - inputTokens: inputTokens - (cacheReadTokens ?? 0), - outputTokens: outputTokens - (reasoningTokens ?? 0), - reasoningTokens, - cacheReadTokens, - } - }, }) } diff --git a/packages/console/app/src/routes/zen/v1/messages.ts b/packages/console/app/src/routes/zen/v1/messages.ts index 4a7dda5f7..4478b6444 100644 --- a/packages/console/app/src/routes/zen/v1/messages.ts +++ b/packages/console/app/src/routes/zen/v1/messages.ts @@ -1,64 +1,9 @@ import type { APIEvent } from "@solidjs/start/server" -import { handler } from "~/routes/zen/handler" - -type Usage = { - cache_creation?: { - ephemeral_5m_input_tokens?: number - ephemeral_1h_input_tokens?: number - } - cache_creation_input_tokens?: number - cache_read_input_tokens?: number - input_tokens?: number - output_tokens?: number - server_tool_use?: { - web_search_requests?: number - } -} +import { handler } from "~/routes/zen/util/handler" export function POST(input: APIEvent) { - let usage: Usage return handler(input, { - modifyBody: (body: any) => ({ - ...body, - service_tier: "standard_only", - }), - setAuthHeader: (headers: Headers, apiKey: string) => headers.set("x-api-key", apiKey), + format: "anthropic", parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined, - onStreamPart: (chunk: string) => { - const data = chunk.split("\n")[1] - if (!data.startsWith("data: ")) return - - let json - try { - json = JSON.parse(data.slice(6)) - } catch (e) { - return - } - - // ie. { type: "message_start"; message: { usage: Usage } } - // ie. { type: "message_delta"; usage: Usage } - const usageUpdate = json.usage ?? json.message?.usage - if (!usageUpdate) return - usage = { - ...usage, - ...usageUpdate, - cache_creation: { - ...usage?.cache_creation, - ...usageUpdate.cache_creation, - }, - server_tool_use: { - ...usage?.server_tool_use, - ...usageUpdate.server_tool_use, - }, - } - }, - getStreamUsage: () => usage, - normalizeUsage: (usage: Usage) => ({ - inputTokens: usage.input_tokens ?? 0, - outputTokens: usage.output_tokens ?? 0, - cacheReadTokens: usage.cache_read_input_tokens ?? undefined, - cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined, - cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined, - }), }) } diff --git a/packages/console/app/src/routes/zen/v1/models.ts b/packages/console/app/src/routes/zen/v1/models.ts new file mode 100644 index 000000000..ad5769bb6 --- /dev/null +++ b/packages/console/app/src/routes/zen/v1/models.ts @@ -0,0 +1,60 @@ +import type { APIEvent } from "@solidjs/start/server" +import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js" +import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" +import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" +import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js" +import { ZenData } from "@opencode-ai/console-core/model.js" + +export async function OPTIONS(input: APIEvent) { + return new Response(null, { + status: 200, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + }, + }) +} + +export async function GET(input: APIEvent) { + const zenData = ZenData.list() + const disabledModels = await authenticate() + + return new Response( + JSON.stringify({ + object: "list", + data: Object.entries(zenData.models) + .filter(([id]) => !disabledModels.includes(id)) + .map(([id, model]) => ({ + id: `opencode/${id}`, + object: "model", + created: Math.floor(Date.now() / 1000), + owned_by: "opencode", + })), + }), + { + headers: { + "Content-Type": "application/json", + }, + }, + ) + + async function authenticate() { + const apiKey = input.request.headers.get("authorization")?.split(" ")[1] + if (!apiKey) return [] + + const disabledModels = await Database.use((tx) => + tx + .select({ + model: ModelTable.model, + }) + .from(KeyTable) + .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID)) + .leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), isNull(ModelTable.timeDeleted))) + .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted))) + .then((rows) => rows.map((row) => row.model)), + ) + + return disabledModels + } +} diff --git a/packages/console/app/src/routes/zen/v1/responses.ts b/packages/console/app/src/routes/zen/v1/responses.ts index 486c129b9..eadc5bc8e 100644 --- a/packages/console/app/src/routes/zen/v1/responses.ts +++ b/packages/console/app/src/routes/zen/v1/responses.ts @@ -1,52 +1,9 @@ import type { APIEvent } from "@solidjs/start/server" -import { handler } from "~/routes/zen/handler" - -type Usage = { - input_tokens?: number - input_tokens_details?: { - cached_tokens?: number - } - output_tokens?: number - output_tokens_details?: { - reasoning_tokens?: number - } - total_tokens?: number -} +import { handler } from "~/routes/zen/util/handler" export function POST(input: APIEvent) { - let usage: Usage return handler(input, { - setAuthHeader: (headers: Headers, apiKey: string) => { - headers.set("authorization", `Bearer ${apiKey}`) - }, + format: "openai", parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1], - onStreamPart: (chunk: string) => { - const [event, data] = chunk.split("\n") - if (event !== "event: response.completed") return - if (!data.startsWith("data: ")) return - - let json - try { - json = JSON.parse(data.slice(6)) as { response?: { usage?: Usage } } - } catch (e) { - return - } - - if (!json.response?.usage) return - usage = json.response.usage - }, - getStreamUsage: () => usage, - normalizeUsage: (usage: Usage) => { - const inputTokens = usage.input_tokens ?? 0 - const outputTokens = usage.output_tokens ?? 0 - const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? undefined - const cacheReadTokens = usage.input_tokens_details?.cached_tokens ?? undefined - return { - inputTokens: inputTokens - (cacheReadTokens ?? 0), - outputTokens: outputTokens - (reasoningTokens ?? 0), - reasoningTokens, - cacheReadTokens, - } - }, }) } From 78169017136048fc65fb6cb9d13095a0c6c50af3 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 27 Oct 2025 21:25:56 -0400 Subject: [PATCH 40/56] wip: zen doc --- packages/web/src/content/docs/zen.mdx | 32 +++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index ab48c2c14..a53ad3131 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -71,9 +71,10 @@ You can also access our models through the following API endpoints. | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Grok Code Fast 1 | grok-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | The [model id](/docs/config/#models) in your OpenCode config uses the format `opencode/`. For example, for GPT 5 Codex, you would @@ -81,14 +82,41 @@ use `opencode/gpt-5-codex` in your config. --- +### Unified + +All models in Zen can also be accessed through a single unified endpoint: + +``` +https://opencode.ai/zen/v1/chat/completions +``` + +This endpoint is OpenAI-compatible, so it works seamlessly with the `@ai-sdk/openai-compatible` package and any OpenAI-compatible SDKs or tools. + +Use this if you want to simplify integration across multiple models without changing endpoints or SDKs. + +This feature is currently in beta. + +--- + +### Models + +You can fetch the full list of available models and their metadata from: + +``` +https://opencode.ai/zen/v1/models +``` + +--- + ## Pricing We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | Model | Input | Output | Cached Read | Cached Write | | --------------------------------- | ------ | ------ | ----------- | ------------ | -| Qwen3 Coder 480B | $0.45 | $1.50 | - | - | +| GLM 4.6 | $0.60 | $1.90 | $0.11 | - | | Kimi K2 | $0.60 | $2.50 | $0.36 | - | +| Qwen3 Coder 480B | $0.45 | $1.50 | - | - | | Grok Code Fast 1 | Free | Free | - | - | | Code Supernova | Free | Free | - | - | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | From 6fe8e3973cdcb623a39df0760a68cb49705789a0 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 27 Oct 2025 21:36:10 -0400 Subject: [PATCH 41/56] zen: support 1M claude context --- packages/console/app/src/routes/zen/util/handler.ts | 2 +- .../console/app/src/routes/zen/util/provider/anthropic.ts | 5 ++++- .../app/src/routes/zen/util/provider/openai-compatible.ts | 2 +- packages/console/app/src/routes/zen/util/provider/openai.ts | 2 +- .../console/app/src/routes/zen/util/provider/provider.ts | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 7fbb518a0..85ba5eea1 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -67,7 +67,7 @@ export async function handler( const headers = input.request.headers headers.delete("host") headers.delete("content-length") - providerInfo.modifyHeaders(headers, providerInfo.apiKey) + providerInfo.modifyHeaders(headers, body, providerInfo.apiKey) Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => { headers.set(k, headers.get(v)!) }) diff --git a/packages/console/app/src/routes/zen/util/provider/anthropic.ts b/packages/console/app/src/routes/zen/util/provider/anthropic.ts index 64b040a53..807f427af 100644 --- a/packages/console/app/src/routes/zen/util/provider/anthropic.ts +++ b/packages/console/app/src/routes/zen/util/provider/anthropic.ts @@ -17,9 +17,12 @@ type Usage = { export const anthropicHelper = { format: "anthropic", modifyUrl: (providerApi: string) => providerApi + "/messages", - modifyHeaders: (headers: Headers, apiKey: string) => { + modifyHeaders: (headers: Headers, body: Record, apiKey: string) => { headers.set("x-api-key", apiKey) headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01") + if (body.model.startsWith("claude-sonnet-")) { + headers.set("anthropic-beta", "context-1m-2025-08-07") + } }, modifyBody: (body: Record) => { return { diff --git a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts index aae6bed57..cad6bd686 100644 --- a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts +++ b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts @@ -24,7 +24,7 @@ type Usage = { export const oaCompatHelper = { format: "oa-compat", modifyUrl: (providerApi: string) => providerApi + "/chat/completions", - modifyHeaders: (headers: Headers, apiKey: string) => { + modifyHeaders: (headers: Headers, body: Record, apiKey: string) => { headers.set("authorization", `Bearer ${apiKey}`) }, modifyBody: (body: Record) => { diff --git a/packages/console/app/src/routes/zen/util/provider/openai.ts b/packages/console/app/src/routes/zen/util/provider/openai.ts index 9781d821d..21c15f355 100644 --- a/packages/console/app/src/routes/zen/util/provider/openai.ts +++ b/packages/console/app/src/routes/zen/util/provider/openai.ts @@ -15,7 +15,7 @@ type Usage = { export const openaiHelper = { format: "openai", modifyUrl: (providerApi: string) => providerApi + "/responses", - modifyHeaders: (headers: Headers, apiKey: string) => { + modifyHeaders: (headers: Headers, body: Record, apiKey: string) => { headers.set("authorization", `Bearer ${apiKey}`) }, modifyBody: (body: Record) => { diff --git a/packages/console/app/src/routes/zen/util/provider/provider.ts b/packages/console/app/src/routes/zen/util/provider/provider.ts index 5beb460e9..c8ba644ba 100644 --- a/packages/console/app/src/routes/zen/util/provider/provider.ts +++ b/packages/console/app/src/routes/zen/util/provider/provider.ts @@ -28,7 +28,7 @@ import { export type ProviderHelper = { format: Format modifyUrl: (providerApi: string) => string - modifyHeaders: (headers: Headers, apiKey: string) => void + modifyHeaders: (headers: Headers, body: Record, apiKey: string) => void modifyBody: (body: Record) => Record createUsageParser: () => { parse: (chunk: string) => void From 4caa458232797564622d96a641438a9a9ec48b82 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 27 Oct 2025 21:40:08 -0400 Subject: [PATCH 42/56] acp: fix type error --- packages/opencode/src/cli/cmd/acp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts index 6628137f4..4f119d012 100644 --- a/packages/opencode/src/cli/cmd/acp.ts +++ b/packages/opencode/src/cli/cmd/acp.ts @@ -29,7 +29,7 @@ export const AcpCommand = cmd({ const input = new WritableStream({ write(chunk) { return new Promise((resolve, reject) => { - process.stdout.write(Buffer.from(chunk), (err) => { + process.stdout.write(chunk, (err) => { if (err) { reject(err) } else { From 982954cc1b4b471be019ca012c18f9451c2fcfd8 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 28 Oct 2025 00:08:30 -0500 Subject: [PATCH 43/56] feat (acp): mcp server support, file diffs, some default slash commands (/init, /compact), show todos properly (#3490) The mcp server support does not mean acp didn't allow u to use mcp servers previously, it means that now you can connect new servers via ACP instead of relying on the opencode defined ones --- packages/opencode/src/acp/agent.ts | 238 ++++++++++++++----- packages/opencode/src/mcp/index.ts | 245 +++++++++++--------- packages/opencode/src/session/compaction.ts | 6 +- 3 files changed, 319 insertions(+), 170 deletions(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 03bc4d5dd..0fa2509d6 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -1,16 +1,20 @@ -import type { - Agent as ACPAgent, - AgentSideConnection, - AuthenticateRequest, - CancelNotification, - InitializeRequest, - LoadSessionRequest, - NewSessionRequest, - PermissionOption, - PromptRequest, - SetSessionModelRequest, - SetSessionModeRequest, - SetSessionModeResponse, +import { + sessionModeSchema, + type Agent as ACPAgent, + type AgentSideConnection, + type AuthenticateRequest, + type CancelNotification, + type InitializeRequest, + type LoadSessionRequest, + type NewSessionRequest, + type PermissionOption, + type PlanEntry, + type PromptRequest, + type SetSessionModelRequest, + type SetSessionModeRequest, + type SetSessionModeResponse, + type ToolCallContent, + type ToolKind, } from "@agentclientprotocol/sdk" import { Log } from "../util/log" import { ACPSessionManager } from "./session" @@ -25,24 +29,17 @@ import { Storage } from "@/storage/storage" import { Command } from "@/command" import { Agent as Agents } from "@/agent/agent" import { Permission } from "@/permission" +import { Session } from "@/session" +import { Identifier } from "@/id/id" +import { SessionCompaction } from "@/session/compaction" +import type { Config } from "@/config/config" +import { MCP } from "@/mcp" +import { Todo } from "@/session/todo" +import { z } from "zod" export namespace ACP { const log = Log.create({ service: "acp-agent" }) - // TODO: mcp servers? - - type ToolKind = - | "read" - | "edit" - | "delete" - | "move" - | "search" - | "execute" - | "think" - | "fetch" - | "switch_mode" - | "other" - export class Agent implements ACPAgent { private sessionManager = new ACPSessionManager() private connection: AgentSideConnection @@ -157,6 +154,62 @@ export namespace ACP { }) break case "completed": + const kind = toToolKind(part.tool) + const content: ToolCallContent[] = [ + { + type: "content", + content: { + type: "text", + text: part.state.output, + }, + }, + ] + + if (kind === "edit") { + const input = part.state.input + const filePath = typeof input["filePath"] === "string" ? input["filePath"] : "" + const oldText = typeof input["oldString"] === "string" ? input["oldString"] : "" + const newText = + typeof input["newString"] === "string" + ? input["newString"] + : typeof input["content"] === "string" + ? input["content"] + : "" + content.push({ + type: "diff", + path: filePath, + oldText, + newText, + }) + } + + if (part.tool === "todowrite") { + const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output)) + if (parsedTodos.success) { + await this.connection + .sessionUpdate({ + sessionId: acpSession.id, + update: { + sessionUpdate: "plan", + entries: parsedTodos.data.map((todo) => { + const status: PlanEntry["status"] = + todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"]) + return { + priority: "medium", + status, + content: todo.content, + } + }), + }, + }) + .catch((err) => { + log.error("failed to send session update for todo", { error: err }) + }) + } else { + log.error("failed to parse todo output", { error: parsedTodos.error }) + } + } + await this.connection .sessionUpdate({ sessionId: acpSession.id, @@ -164,15 +217,8 @@ export namespace ACP { sessionUpdate: "tool_call_update", toolCallId: part.callID, status: "completed", - content: [ - { - type: "content", - content: { - type: "text", - text: part.state.output, - }, - }, - ], + kind, + content, title: part.state.title, rawOutput: { output: part.state.output, @@ -258,11 +304,14 @@ export namespace ACP { protocolVersion: 1, agentCapabilities: { loadSession: true, - // TODO: map acp mcp - // mcpCapabilities: { - // http: true, - // sse: true, - // }, + mcpCapabilities: { + http: true, + sse: true, + }, + promptCapabilities: { + embeddedContext: true, + image: true, + }, }, authMethods: [ { @@ -287,6 +336,7 @@ export namespace ACP { const model = await defaultModel(this.config) const session = await this.sessionManager.create(params.cwd, params.mcpServers, model) + log.info("creating_session", { mcpServers: params.mcpServers.length }) const load = await this.loadSession({ cwd: params.cwd, mcpServers: params.mcpServers, @@ -325,6 +375,17 @@ export namespace ACP { name: command.name, description: command.description ?? "", })) + const names = new Set(availableCommands.map((c) => c.name)) + if (!names.has("init")) + availableCommands.push({ + name: "init", + description: "create/update a AGENTS.md", + }) + if (!names.has("compact")) + availableCommands.push({ + name: "compact", + description: "compact the session", + }) setTimeout(() => { this.connection.sessionUpdate({ @@ -346,6 +407,35 @@ export namespace ACP { const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id + const mcpServers: Record = {} + for (const server of params.mcpServers) { + if ("type" in server) { + mcpServers[server.name] = { + url: server.url, + headers: server.headers.reduce>((acc, { name, value }) => { + acc[name] = value + return acc + }, {}), + type: "remote", + } + } else { + mcpServers[server.name] = { + type: "local", + command: [server.command, ...server.args], + environment: server.env.reduce>((acc, { name, value }) => { + acc[name] = value + return acc + }, {}), + } + } + } + + await Promise.all( + Object.entries(mcpServers).map(async ([key, mcp]) => { + await MCP.add(key, mcp) + }), + ) + return { sessionId, models: { @@ -452,25 +542,25 @@ export namespace ACP { log.info("parts", { parts }) - const cmd = await (async () => { - const text = parts.filter((part) => part.type === "text").join("") - const match = text.match(/^\/(\w+)\s*(.*)$/) - if (!match) return + const cmd = (() => { + const text = parts + .filter((p) => p.type === "text") + .map((p) => p.text) + .join("") + .trim() - const [c, args] = match.slice(1) - const command = await Command.get(c) - if (!command) return - return { command, args } + if (!text.startsWith("/")) return + + const [name, ...rest] = text.slice(1).split(/\s+/) + return { name, args: rest.join(" ").trim() } })() - if (cmd) { - await SessionPrompt.command({ - sessionID, - command: cmd.command.name, - arguments: cmd.args, - agent, - }) - } else { + const done = { + stopReason: "end_turn" as const, + _meta: {}, + } + + if (!cmd) { await SessionPrompt.prompt({ sessionID, model: { @@ -480,12 +570,40 @@ export namespace ACP { parts, agent, }) + return done } - return { - stopReason: "end_turn" as const, - _meta: {}, + const command = await Command.get(cmd.name) + if (command) { + await SessionPrompt.command({ + sessionID, + command: command.name, + arguments: cmd.args, + model: model.providerID + "/" + model.modelID, + agent, + }) + return done } + + switch (cmd.name) { + case "init": + await Session.initialize({ + sessionID, + messageID: Identifier.ascending("message"), + providerID: model.providerID, + modelID: model.modelID, + }) + break + case "compact": + await SessionCompaction.run({ + sessionID, + providerID: model.providerID, + modelID: model.modelID, + }) + break + } + + return done } async cancel(params: CancelNotification) { diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index fa3513bb7..1c3b84369 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -26,122 +26,22 @@ export namespace MCP { const state = Instance.state( async () => { const cfg = await Config.get() + const config = cfg.mcp ?? {} const clients: { [name: string]: MCPClient } = {} - for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) { - if (mcp.enabled === false) { - log.info("mcp server disabled", { key }) - continue - } - log.info("found", { key, type: mcp.type }) - if (mcp.type === "remote") { - const transports = [ - { - name: "StreamableHTTP", - transport: new StreamableHTTPClientTransport(new URL(mcp.url), { - requestInit: { - headers: mcp.headers, - }, - }), - }, - { - name: "SSE", - transport: new SSEClientTransport(new URL(mcp.url), { - requestInit: { - headers: mcp.headers, - }, - }), - }, - ] - let lastError: Error | undefined - for (const { name, transport } of transports) { - const client = await experimental_createMCPClient({ - name: "opencode", - transport, - }).catch((error) => { - lastError = error instanceof Error ? error : new Error(String(error)) - log.debug("transport connection failed", { - key, - transport: name, - url: mcp.url, - error: lastError.message, - }) - return null - }) - if (client) { - log.debug("transport connection succeeded", { key, transport: name }) - clients[key] = client - break - } - } - if (!clients[key]) { - const errorMessage = lastError - ? `MCP server ${key} failed to connect: ${lastError.message}` - : `MCP server ${key} failed to connect to ${mcp.url}` - log.error("remote mcp connection failed", { key, url: mcp.url, error: lastError?.message }) - Bus.publish(Session.Event.Error, { - error: { - name: "UnknownError", - data: { - message: errorMessage, - }, - }, - }) - } - } - if (mcp.type === "local") { - const [cmd, ...args] = mcp.command - const client = await experimental_createMCPClient({ - name: "opencode", - transport: new StdioClientTransport({ - stderr: "ignore", - command: cmd, - args, - env: { - ...process.env, - ...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}), - ...mcp.environment, - }, - }), - }).catch((error) => { - const errorMessage = - error instanceof Error - ? `MCP server ${key} failed to start: ${error.message}` - : `MCP server ${key} failed to start` - log.error("local mcp startup failed", { - key, - command: mcp.command, - error: error instanceof Error ? error.message : String(error), - }) - Bus.publish(Session.Event.Error, { - error: { - name: "UnknownError", - data: { - message: errorMessage, - }, - }, - }) - return null - }) - if (client) { - clients[key] = client - } - } - } - - for (const [key, client] of Object.entries(clients)) { - const result = await withTimeout(client.tools(), 5000).catch(() => {}) - if (!result) { - log.warn("mcp client verification failed, removing client", { key }) - delete clients[key] - } - } + await Promise.all( + Object.entries(config).map(async ([key, mcp]) => { + const result = await create(key, mcp).catch(() => undefined) + if (!result) return + clients[key] = result.client + }), + ) return { clients, - config: cfg.mcp ?? {}, + config, } }, async (state) => { @@ -151,6 +51,133 @@ export namespace MCP { }, ) + export async function add(name: string, mcp: Config.Mcp) { + const s = await state() + const result = await create(name, mcp) + if (!result) return + s.clients[name] = result.client + } + + async function create(name: string, mcp: Config.Mcp) { + if (mcp.enabled === false) { + log.info("mcp server disabled", { name }) + return + } + log.info("found", { name, type: mcp.type }) + + let mcpClient: MCPClient | undefined + + if (mcp.type === "remote") { + const transports = [ + { + name: "StreamableHTTP", + transport: new StreamableHTTPClientTransport(new URL(mcp.url), { + requestInit: { + headers: mcp.headers, + }, + }), + }, + { + name: "SSE", + transport: new SSEClientTransport(new URL(mcp.url), { + requestInit: { + headers: mcp.headers, + }, + }), + }, + ] + let lastError: Error | undefined + for (const { name, transport } of transports) { + const client = await experimental_createMCPClient({ + name: "opencode", + transport, + }).catch((error) => { + lastError = error instanceof Error ? error : new Error(String(error)) + log.debug("transport connection failed", { + name, + transport: name, + url: mcp.url, + error: lastError.message, + }) + return null + }) + if (client) { + log.debug("transport connection succeeded", { name, transport: name }) + mcpClient = client + break + } + } + if (!mcpClient) { + const errorMessage = lastError + ? `MCP server ${name} failed to connect: ${lastError.message}` + : `MCP server ${name} failed to connect to ${mcp.url}` + log.error("remote mcp connection failed", { name, url: mcp.url, error: lastError?.message }) + Bus.publish(Session.Event.Error, { + error: { + name: "UnknownError", + data: { + message: errorMessage, + }, + }, + }) + } + } + + if (mcp.type === "local") { + const [cmd, ...args] = mcp.command + const client = await experimental_createMCPClient({ + name: "opencode", + transport: new StdioClientTransport({ + stderr: "ignore", + command: cmd, + args, + env: { + ...process.env, + ...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}), + ...mcp.environment, + }, + }), + }).catch((error) => { + const errorMessage = + error instanceof Error + ? `MCP server ${name} failed to start: ${error.message}` + : `MCP server ${name} failed to start` + log.error("local mcp startup failed", { + name, + command: mcp.command, + error: error instanceof Error ? error.message : String(error), + }) + Bus.publish(Session.Event.Error, { + error: { + name: "UnknownError", + data: { + message: errorMessage, + }, + }, + }) + return null + }) + if (client) { + mcpClient = client + } + } + + if (!mcpClient) { + log.warn("mcp client not initialized", { name }) + return + } + + const result = await withTimeout(mcpClient.tools(), 5000).catch(() => {}) + if (!result) { + log.warn("mcp client verification failed, dropping client", { name }) + return + } + + return { + client: mcpClient, + } + } + export async function status() { return state().then((state) => { const result: Record = {} diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 76313453f..657ac4475 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -189,7 +189,11 @@ export namespace SessionCompaction { case "text-delta": part.text += value.text if (value.providerMetadata) part.metadata = value.providerMetadata - if (part.text) await Session.updatePart(part) + if (part.text) + await Session.updatePart({ + part, + delta: value.text, + }) continue case "text-end": { part.text = part.text.trimEnd() From d8249f32a83b84d49ae0391975b305fc7de3fdcb Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 28 Oct 2025 01:14:13 -0400 Subject: [PATCH 44/56] do not set temperature for claude models --- packages/opencode/src/provider/transform.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index dda02cc4e..6212edff8 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -75,7 +75,7 @@ export namespace ProviderTransform { export function temperature(_providerID: string, modelID: string) { if (modelID.toLowerCase().includes("qwen")) return 0.55 - if (modelID.toLowerCase().includes("claude")) return 1 + if (modelID.toLowerCase().includes("claude")) return undefined return 0 } From 872c9467b2b078019ad5a5e01906a67bdc6ce953 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 28 Oct 2025 00:43:29 -0500 Subject: [PATCH 45/56] chore: rm unused import --- packages/opencode/src/acp/agent.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 0fa2509d6..ae9a74a66 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -1,5 +1,4 @@ import { - sessionModeSchema, type Agent as ACPAgent, type AgentSideConnection, type AuthenticateRequest, From 22821744ef54ea9f8a4b27f62b55e74569ef8913 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 28 Oct 2025 02:54:23 -0400 Subject: [PATCH 46/56] feat: add OPENCODE_FAKE_VCS flag for VCS testing and update todo tracking instructions --- packages/opencode/src/flag/flag.ts | 1 + packages/opencode/src/project/project.ts | 2 ++ packages/opencode/src/session/prompt/anthropic.txt | 2 ++ 3 files changed, 5 insertions(+) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index fff271cd2..879aa758a 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -9,6 +9,7 @@ export namespace Flag { export const OPENCODE_DISABLE_LSP_DOWNLOAD = truthy("OPENCODE_DISABLE_LSP_DOWNLOAD") export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS") export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT") + export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"] // Experimental export const OPENCODE_EXPERIMENTAL_WATCHER = truthy("OPENCODE_EXPERIMENTAL_WATCHER") diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 34bd4aea7..339efc2cb 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -4,6 +4,7 @@ import path from "path" import { $ } from "bun" import { Storage } from "../storage/storage" import { Log } from "../util/log" +import { Flag } from "@/flag/flag" export namespace Project { const log = Log.create({ service: "project" }) @@ -31,6 +32,7 @@ export namespace Project { const project: Info = { id: "global", worktree: "/", + vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), time: { created: Date.now(), }, diff --git a/packages/opencode/src/session/prompt/anthropic.txt b/packages/opencode/src/session/prompt/anthropic.txt index 4f377beb9..43b11250a 100644 --- a/packages/opencode/src/session/prompt/anthropic.txt +++ b/packages/opencode/src/session/prompt/anthropic.txt @@ -93,6 +93,8 @@ user: What is the codebase structure? assistant: [Uses the Task tool]
+IMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation. + # Code References When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location. From 6af6a1295f530c095a9f9699bd238993d8219a29 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 28 Oct 2025 08:12:32 +0000 Subject: [PATCH 47/56] release: v0.15.19 --- bun.lock | 22 +++++++++++----------- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 14 files changed, 24 insertions(+), 24 deletions(-) diff --git a/bun.lock b/bun.lock index 012afac4e..eec72e6c6 100644 --- a/bun.lock +++ b/bun.lock @@ -37,7 +37,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "0.15.18", + "version": "0.15.19", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -64,7 +64,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "0.15.18", + "version": "0.15.19", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -88,7 +88,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "0.15.18", + "version": "0.15.19", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -109,7 +109,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "0.15.18", + "version": "0.15.19", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -150,7 +150,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "0.15.18", + "version": "0.15.19", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -166,7 +166,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "0.15.18", + "version": "0.15.19", "bin": { "opencode": "./bin/opencode", }, @@ -230,7 +230,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "0.15.18", + "version": "0.15.19", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -250,7 +250,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "0.15.18", + "version": "0.15.19", "devDependencies": { "@hey-api/openapi-ts": "0.81.0", "@tsconfig/node22": "catalog:", @@ -261,7 +261,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "0.15.18", + "version": "0.15.19", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -274,7 +274,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "0.15.18", + "version": "0.15.19", "dependencies": { "@kobalte/core": "catalog:", "@pierre/precision-diffs": "catalog:", @@ -297,7 +297,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "0.15.18", + "version": "0.15.19", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index b8dea62bc..868aa1899 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -7,7 +7,7 @@ "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json", "start": "vinxi start", - "version": "0.15.18" + "version": "0.15.19" }, "dependencies": { "@ibm/plex": "6.4.1", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index f9a4909ff..948e4af27 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "0.15.18", + "version": "0.15.19", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 2aa560815..4be5f460a 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "0.15.18", + "version": "0.15.19", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index accc76b42..bf62733e7 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "0.15.18", + "version": "0.15.19", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index a14b01943..e401c2242 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "0.15.18", + "version": "0.15.19", "description": "", "type": "module", "scripts": { diff --git a/packages/function/package.json b/packages/function/package.json index 1557eecfc..db56da8de 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "0.15.18", + "version": "0.15.19", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ca588b485..93a69fbed 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "0.15.18", + "version": "0.15.19", "name": "opencode", "type": "module", "private": true, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 52efc9381..4395fb3e0 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "0.15.18", + "version": "0.15.19", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 15a411118..042e35080 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "0.15.18", + "version": "0.15.19", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/slack/package.json b/packages/slack/package.json index 2f9e4cb55..5fc872c12 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "0.15.18", + "version": "0.15.19", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index cdb9eee1c..a6d6a0125 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "0.15.18", + "version": "0.15.19", "type": "module", "exports": { ".": "./src/components/index.ts", diff --git a/packages/web/package.json b/packages/web/package.json index e24eeb042..5337a4446 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "0.15.18", + "version": "0.15.19", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index b3220f817..918cde136 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "0.15.18", + "version": "0.15.19", "publisher": "sst-dev", "repository": { "type": "git", From dfebf40471bc1dd9b58d062382156878cf95a17e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 28 Oct 2025 12:04:31 +0000 Subject: [PATCH 48/56] ignore: update download stats 2025-10-28 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index cd00e57e0..71fda1684 100644 --- a/STATS.md +++ b/STATS.md @@ -121,3 +121,4 @@ | 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) | | 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) | | 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) | +| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) | From ee1af0fe80662b6c7c233ae3fe70ada19008bf23 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 28 Oct 2025 10:03:53 -0500 Subject: [PATCH 49/56] fix: blank version issue --- packages/sdk/go/.release-please-manifest.json | 2 +- packages/sdk/go/.stats.yml | 4 +- packages/sdk/go/CHANGELOG.md | 16 + packages/sdk/go/README.md | 2 +- packages/sdk/go/app.go | 77 ++++ packages/sdk/go/config.go | 104 ++++- packages/sdk/go/event.go | 177 +++++++- packages/sdk/go/file.go | 42 +- packages/sdk/go/internal/version.go | 2 +- packages/sdk/go/session.go | 421 ++++++++++++++++-- packages/sdk/go/session_test.go | 3 +- 11 files changed, 776 insertions(+), 74 deletions(-) diff --git a/packages/sdk/go/.release-please-manifest.json b/packages/sdk/go/.release-please-manifest.json index 6f2b40185..4ad3fef33 100644 --- a/packages/sdk/go/.release-please-manifest.json +++ b/packages/sdk/go/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.16.2" + ".": "0.18.0" } \ No newline at end of file diff --git a/packages/sdk/go/.stats.yml b/packages/sdk/go/.stats.yml index 911073ed4..5383f794a 100644 --- a/packages/sdk/go/.stats.yml +++ b/packages/sdk/go/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 43 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-273fc9fea965af661dfed0902d00f10d6ed844f0681ca861a58821c4902eac2f.yml -openapi_spec_hash: c6144f23a1bac75f79be86edd405552b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-92f9d0f8daee2ea7458f8b9f1d7a7f941ff932442ad944bc7576254d5978b6d5.yml +openapi_spec_hash: 5b785c4ff6fb69039915f0e746abdaf9 config_hash: 026ef000d34bf2f930e7b41e77d2d3ff diff --git a/packages/sdk/go/CHANGELOG.md b/packages/sdk/go/CHANGELOG.md index 27affc4f0..498a78029 100644 --- a/packages/sdk/go/CHANGELOG.md +++ b/packages/sdk/go/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.18.0 (2025-10-10) + +Full Changelog: [v0.17.0...v0.18.0](https://github.com/sst/opencode-sdk-go/compare/v0.17.0...v0.18.0) + +### Features + +* **api:** api update ([0a7f5e7](https://github.com/sst/opencode-sdk-go/commit/0a7f5e710911506512a132ba39e0593c412beb77)) + +## 0.17.0 (2025-10-07) + +Full Changelog: [v0.16.2...v0.17.0](https://github.com/sst/opencode-sdk-go/compare/v0.16.2...v0.17.0) + +### Features + +* **api:** api update ([84a3df5](https://github.com/sst/opencode-sdk-go/commit/84a3df50a7ff3d87e5593e4f29dfb5d561f71cc3)) + ## 0.16.2 (2025-09-26) Full Changelog: [v0.16.1...v0.16.2](https://github.com/sst/opencode-sdk-go/compare/v0.16.1...v0.16.2) diff --git a/packages/sdk/go/README.md b/packages/sdk/go/README.md index 2de28f6ce..f4c02d125 100644 --- a/packages/sdk/go/README.md +++ b/packages/sdk/go/README.md @@ -24,7 +24,7 @@ Or to pin the version: ```sh -go get -u 'github.com/sst/opencode-sdk-go@v0.16.2' +go get -u 'github.com/sst/opencode-sdk-go@v0.18.0' ``` diff --git a/packages/sdk/go/app.go b/packages/sdk/go/app.go index 19662f100..4ba42332a 100644 --- a/packages/sdk/go/app.go +++ b/packages/sdk/go/app.go @@ -62,7 +62,9 @@ type Model struct { Temperature bool `json:"temperature,required"` ToolCall bool `json:"tool_call,required"` Experimental bool `json:"experimental"` + Modalities ModelModalities `json:"modalities"` Provider ModelProvider `json:"provider"` + Status ModelStatus `json:"status"` JSON modelJSON `json:"-"` } @@ -79,7 +81,9 @@ type modelJSON struct { Temperature apijson.Field ToolCall apijson.Field Experimental apijson.Field + Modalities apijson.Field Provider apijson.Field + Status apijson.Field raw string ExtraFields map[string]apijson.Field } @@ -140,6 +144,64 @@ func (r modelLimitJSON) RawJSON() string { return r.raw } +type ModelModalities struct { + Input []ModelModalitiesInput `json:"input,required"` + Output []ModelModalitiesOutput `json:"output,required"` + JSON modelModalitiesJSON `json:"-"` +} + +// modelModalitiesJSON contains the JSON metadata for the struct [ModelModalities] +type modelModalitiesJSON struct { + Input apijson.Field + Output apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ModelModalities) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r modelModalitiesJSON) RawJSON() string { + return r.raw +} + +type ModelModalitiesInput string + +const ( + ModelModalitiesInputText ModelModalitiesInput = "text" + ModelModalitiesInputAudio ModelModalitiesInput = "audio" + ModelModalitiesInputImage ModelModalitiesInput = "image" + ModelModalitiesInputVideo ModelModalitiesInput = "video" + ModelModalitiesInputPdf ModelModalitiesInput = "pdf" +) + +func (r ModelModalitiesInput) IsKnown() bool { + switch r { + case ModelModalitiesInputText, ModelModalitiesInputAudio, ModelModalitiesInputImage, ModelModalitiesInputVideo, ModelModalitiesInputPdf: + return true + } + return false +} + +type ModelModalitiesOutput string + +const ( + ModelModalitiesOutputText ModelModalitiesOutput = "text" + ModelModalitiesOutputAudio ModelModalitiesOutput = "audio" + ModelModalitiesOutputImage ModelModalitiesOutput = "image" + ModelModalitiesOutputVideo ModelModalitiesOutput = "video" + ModelModalitiesOutputPdf ModelModalitiesOutput = "pdf" +) + +func (r ModelModalitiesOutput) IsKnown() bool { + switch r { + case ModelModalitiesOutputText, ModelModalitiesOutputAudio, ModelModalitiesOutputImage, ModelModalitiesOutputVideo, ModelModalitiesOutputPdf: + return true + } + return false +} + type ModelProvider struct { Npm string `json:"npm,required"` JSON modelProviderJSON `json:"-"` @@ -160,6 +222,21 @@ func (r modelProviderJSON) RawJSON() string { return r.raw } +type ModelStatus string + +const ( + ModelStatusAlpha ModelStatus = "alpha" + ModelStatusBeta ModelStatus = "beta" +) + +func (r ModelStatus) IsKnown() bool { + switch r { + case ModelStatusAlpha, ModelStatusBeta: + return true + } + return false +} + type Provider struct { ID string `json:"id,required"` Env []string `json:"env,required"` diff --git a/packages/sdk/go/config.go b/packages/sdk/go/config.go index 561a35a0f..02460fb5d 100644 --- a/packages/sdk/go/config.go +++ b/packages/sdk/go/config.go @@ -1567,19 +1567,21 @@ func (r configProviderJSON) RawJSON() string { } type ConfigProviderModel struct { - ID string `json:"id"` - Attachment bool `json:"attachment"` - Cost ConfigProviderModelsCost `json:"cost"` - Experimental bool `json:"experimental"` - Limit ConfigProviderModelsLimit `json:"limit"` - Name string `json:"name"` - Options map[string]interface{} `json:"options"` - Provider ConfigProviderModelsProvider `json:"provider"` - Reasoning bool `json:"reasoning"` - ReleaseDate string `json:"release_date"` - Temperature bool `json:"temperature"` - ToolCall bool `json:"tool_call"` - JSON configProviderModelJSON `json:"-"` + ID string `json:"id"` + Attachment bool `json:"attachment"` + Cost ConfigProviderModelsCost `json:"cost"` + Experimental bool `json:"experimental"` + Limit ConfigProviderModelsLimit `json:"limit"` + Modalities ConfigProviderModelsModalities `json:"modalities"` + Name string `json:"name"` + Options map[string]interface{} `json:"options"` + Provider ConfigProviderModelsProvider `json:"provider"` + Reasoning bool `json:"reasoning"` + ReleaseDate string `json:"release_date"` + Status ConfigProviderModelsStatus `json:"status"` + Temperature bool `json:"temperature"` + ToolCall bool `json:"tool_call"` + JSON configProviderModelJSON `json:"-"` } // configProviderModelJSON contains the JSON metadata for the struct @@ -1590,11 +1592,13 @@ type configProviderModelJSON struct { Cost apijson.Field Experimental apijson.Field Limit apijson.Field + Modalities apijson.Field Name apijson.Field Options apijson.Field Provider apijson.Field Reasoning apijson.Field ReleaseDate apijson.Field + Status apijson.Field Temperature apijson.Field ToolCall apijson.Field raw string @@ -1659,6 +1663,65 @@ func (r configProviderModelsLimitJSON) RawJSON() string { return r.raw } +type ConfigProviderModelsModalities struct { + Input []ConfigProviderModelsModalitiesInput `json:"input,required"` + Output []ConfigProviderModelsModalitiesOutput `json:"output,required"` + JSON configProviderModelsModalitiesJSON `json:"-"` +} + +// configProviderModelsModalitiesJSON contains the JSON metadata for the struct +// [ConfigProviderModelsModalities] +type configProviderModelsModalitiesJSON struct { + Input apijson.Field + Output apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigProviderModelsModalities) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configProviderModelsModalitiesJSON) RawJSON() string { + return r.raw +} + +type ConfigProviderModelsModalitiesInput string + +const ( + ConfigProviderModelsModalitiesInputText ConfigProviderModelsModalitiesInput = "text" + ConfigProviderModelsModalitiesInputAudio ConfigProviderModelsModalitiesInput = "audio" + ConfigProviderModelsModalitiesInputImage ConfigProviderModelsModalitiesInput = "image" + ConfigProviderModelsModalitiesInputVideo ConfigProviderModelsModalitiesInput = "video" + ConfigProviderModelsModalitiesInputPdf ConfigProviderModelsModalitiesInput = "pdf" +) + +func (r ConfigProviderModelsModalitiesInput) IsKnown() bool { + switch r { + case ConfigProviderModelsModalitiesInputText, ConfigProviderModelsModalitiesInputAudio, ConfigProviderModelsModalitiesInputImage, ConfigProviderModelsModalitiesInputVideo, ConfigProviderModelsModalitiesInputPdf: + return true + } + return false +} + +type ConfigProviderModelsModalitiesOutput string + +const ( + ConfigProviderModelsModalitiesOutputText ConfigProviderModelsModalitiesOutput = "text" + ConfigProviderModelsModalitiesOutputAudio ConfigProviderModelsModalitiesOutput = "audio" + ConfigProviderModelsModalitiesOutputImage ConfigProviderModelsModalitiesOutput = "image" + ConfigProviderModelsModalitiesOutputVideo ConfigProviderModelsModalitiesOutput = "video" + ConfigProviderModelsModalitiesOutputPdf ConfigProviderModelsModalitiesOutput = "pdf" +) + +func (r ConfigProviderModelsModalitiesOutput) IsKnown() bool { + switch r { + case ConfigProviderModelsModalitiesOutputText, ConfigProviderModelsModalitiesOutputAudio, ConfigProviderModelsModalitiesOutputImage, ConfigProviderModelsModalitiesOutputVideo, ConfigProviderModelsModalitiesOutputPdf: + return true + } + return false +} + type ConfigProviderModelsProvider struct { Npm string `json:"npm,required"` JSON configProviderModelsProviderJSON `json:"-"` @@ -1680,6 +1743,21 @@ func (r configProviderModelsProviderJSON) RawJSON() string { return r.raw } +type ConfigProviderModelsStatus string + +const ( + ConfigProviderModelsStatusAlpha ConfigProviderModelsStatus = "alpha" + ConfigProviderModelsStatusBeta ConfigProviderModelsStatus = "beta" +) + +func (r ConfigProviderModelsStatus) IsKnown() bool { + switch r { + case ConfigProviderModelsStatusAlpha, ConfigProviderModelsStatusBeta: + return true + } + return false +} + type ConfigProviderOptions struct { APIKey string `json:"apiKey"` BaseURL string `json:"baseURL"` diff --git a/packages/sdk/go/event.go b/packages/sdk/go/event.go index ac5231c7f..41a37951c 100644 --- a/packages/sdk/go/event.go +++ b/packages/sdk/go/event.go @@ -65,6 +65,7 @@ type EventListResponse struct { // [EventListResponseEventFileWatcherUpdatedProperties], // [EventListResponseEventTodoUpdatedProperties], // [EventListResponseEventSessionIdleProperties], + // [EventListResponseEventSessionCreatedProperties], // [EventListResponseEventSessionUpdatedProperties], // [EventListResponseEventSessionDeletedProperties], // [EventListResponseEventSessionErrorProperties], [interface{}], @@ -110,9 +111,10 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) { // [EventListResponseEventPermissionUpdated], // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventFileWatcherUpdated], [EventListResponseEventTodoUpdated], -// [EventListResponseEventSessionIdle], [EventListResponseEventSessionUpdated], -// [EventListResponseEventSessionDeleted], [EventListResponseEventSessionError], -// [EventListResponseEventServerConnected], [EventListResponseEventIdeInstalled]. +// [EventListResponseEventSessionIdle], [EventListResponseEventSessionCreated], +// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], +// [EventListResponseEventSessionError], [EventListResponseEventServerConnected], +// [EventListResponseEventIdeInstalled]. func (r EventListResponse) AsUnion() EventListResponseUnion { return r.union } @@ -126,9 +128,10 @@ func (r EventListResponse) AsUnion() EventListResponseUnion { // [EventListResponseEventPermissionUpdated], // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventFileWatcherUpdated], [EventListResponseEventTodoUpdated], -// [EventListResponseEventSessionIdle], [EventListResponseEventSessionUpdated], -// [EventListResponseEventSessionDeleted], [EventListResponseEventSessionError], -// [EventListResponseEventServerConnected] or [EventListResponseEventIdeInstalled]. +// [EventListResponseEventSessionIdle], [EventListResponseEventSessionCreated], +// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], +// [EventListResponseEventSessionError], [EventListResponseEventServerConnected] or +// [EventListResponseEventIdeInstalled]. type EventListResponseUnion interface { implementsEventListResponse() } @@ -189,6 +192,10 @@ func init() { TypeFilter: gjson.JSON, Type: reflect.TypeOf(EventListResponseEventSessionIdle{}), }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventSessionCreated{}), + }, apijson.UnionVariant{ TypeFilter: gjson.JSON, Type: reflect.TypeOf(EventListResponseEventSessionUpdated{}), @@ -482,14 +489,16 @@ func (r eventListResponseEventMessagePartUpdatedJSON) RawJSON() string { func (r EventListResponseEventMessagePartUpdated) implementsEventListResponse() {} type EventListResponseEventMessagePartUpdatedProperties struct { - Part Part `json:"part,required"` - JSON eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"` + Part Part `json:"part,required"` + Delta string `json:"delta"` + JSON eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"` } // eventListResponseEventMessagePartUpdatedPropertiesJSON contains the JSON // metadata for the struct [EventListResponseEventMessagePartUpdatedProperties] type eventListResponseEventMessagePartUpdatedPropertiesJSON struct { Part apijson.Field + Delta apijson.Field raw string ExtraFields map[string]apijson.Field } @@ -1034,6 +1043,66 @@ func (r EventListResponseEventSessionIdleType) IsKnown() bool { return false } +type EventListResponseEventSessionCreated struct { + Properties EventListResponseEventSessionCreatedProperties `json:"properties,required"` + Type EventListResponseEventSessionCreatedType `json:"type,required"` + JSON eventListResponseEventSessionCreatedJSON `json:"-"` +} + +// eventListResponseEventSessionCreatedJSON contains the JSON metadata for the +// struct [EventListResponseEventSessionCreated] +type eventListResponseEventSessionCreatedJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionCreated) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionCreatedJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventSessionCreated) implementsEventListResponse() {} + +type EventListResponseEventSessionCreatedProperties struct { + Info Session `json:"info,required"` + JSON eventListResponseEventSessionCreatedPropertiesJSON `json:"-"` +} + +// eventListResponseEventSessionCreatedPropertiesJSON contains the JSON metadata +// for the struct [EventListResponseEventSessionCreatedProperties] +type eventListResponseEventSessionCreatedPropertiesJSON struct { + Info apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionCreatedProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionCreatedPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventSessionCreatedType string + +const ( + EventListResponseEventSessionCreatedTypeSessionCreated EventListResponseEventSessionCreatedType = "session.created" +) + +func (r EventListResponseEventSessionCreatedType) IsKnown() bool { + switch r { + case EventListResponseEventSessionCreatedTypeSessionCreated: + return true + } + return false +} + type EventListResponseEventSessionUpdated struct { Properties EventListResponseEventSessionUpdatedProperties `json:"properties,required"` Type EventListResponseEventSessionUpdatedType `json:"type,required"` @@ -1204,7 +1273,8 @@ func (r eventListResponseEventSessionErrorPropertiesJSON) RawJSON() string { type EventListResponseEventSessionErrorPropertiesError struct { // This field can have the runtime type of [shared.ProviderAuthErrorData], - // [shared.UnknownErrorData], [interface{}], [shared.MessageAbortedErrorData]. + // [shared.UnknownErrorData], [interface{}], [shared.MessageAbortedErrorData], + // [EventListResponseEventSessionErrorPropertiesErrorAPIErrorData]. Data interface{} `json:"data,required"` Name EventListResponseEventSessionErrorPropertiesErrorName `json:"name,required"` JSON eventListResponseEventSessionErrorPropertiesErrorJSON `json:"-"` @@ -1239,14 +1309,16 @@ func (r *EventListResponseEventSessionErrorPropertiesError) UnmarshalJSON(data [ // Possible runtime types of the union are [shared.ProviderAuthError], // [shared.UnknownError], // [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError], -// [shared.MessageAbortedError]. +// [shared.MessageAbortedError], +// [EventListResponseEventSessionErrorPropertiesErrorAPIError]. func (r EventListResponseEventSessionErrorPropertiesError) AsUnion() EventListResponseEventSessionErrorPropertiesErrorUnion { return r.union } // Union satisfied by [shared.ProviderAuthError], [shared.UnknownError], -// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError] or -// [shared.MessageAbortedError]. +// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError], +// [shared.MessageAbortedError] or +// [EventListResponseEventSessionErrorPropertiesErrorAPIError]. type EventListResponseEventSessionErrorPropertiesErrorUnion interface { ImplementsEventListResponseEventSessionErrorPropertiesError() } @@ -1271,6 +1343,10 @@ func init() { TypeFilter: gjson.JSON, Type: reflect.TypeOf(shared.MessageAbortedError{}), }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventSessionErrorPropertiesErrorAPIError{}), + }, ) } @@ -1315,6 +1391,77 @@ func (r EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErro return false } +type EventListResponseEventSessionErrorPropertiesErrorAPIError struct { + Data EventListResponseEventSessionErrorPropertiesErrorAPIErrorData `json:"data,required"` + Name EventListResponseEventSessionErrorPropertiesErrorAPIErrorName `json:"name,required"` + JSON eventListResponseEventSessionErrorPropertiesErrorAPIErrorJSON `json:"-"` +} + +// eventListResponseEventSessionErrorPropertiesErrorAPIErrorJSON contains the JSON +// metadata for the struct +// [EventListResponseEventSessionErrorPropertiesErrorAPIError] +type eventListResponseEventSessionErrorPropertiesErrorAPIErrorJSON struct { + Data apijson.Field + Name apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionErrorPropertiesErrorAPIError) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionErrorPropertiesErrorAPIErrorJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventSessionErrorPropertiesErrorAPIError) ImplementsEventListResponseEventSessionErrorPropertiesError() { +} + +type EventListResponseEventSessionErrorPropertiesErrorAPIErrorData struct { + IsRetryable bool `json:"isRetryable,required"` + Message string `json:"message,required"` + ResponseBody string `json:"responseBody"` + ResponseHeaders map[string]string `json:"responseHeaders"` + StatusCode float64 `json:"statusCode"` + JSON eventListResponseEventSessionErrorPropertiesErrorAPIErrorDataJSON `json:"-"` +} + +// eventListResponseEventSessionErrorPropertiesErrorAPIErrorDataJSON contains the +// JSON metadata for the struct +// [EventListResponseEventSessionErrorPropertiesErrorAPIErrorData] +type eventListResponseEventSessionErrorPropertiesErrorAPIErrorDataJSON struct { + IsRetryable apijson.Field + Message apijson.Field + ResponseBody apijson.Field + ResponseHeaders apijson.Field + StatusCode apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionErrorPropertiesErrorAPIErrorData) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionErrorPropertiesErrorAPIErrorDataJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventSessionErrorPropertiesErrorAPIErrorName string + +const ( + EventListResponseEventSessionErrorPropertiesErrorAPIErrorNameAPIError EventListResponseEventSessionErrorPropertiesErrorAPIErrorName = "APIError" +) + +func (r EventListResponseEventSessionErrorPropertiesErrorAPIErrorName) IsKnown() bool { + switch r { + case EventListResponseEventSessionErrorPropertiesErrorAPIErrorNameAPIError: + return true + } + return false +} + type EventListResponseEventSessionErrorPropertiesErrorName string const ( @@ -1322,11 +1469,12 @@ const ( EventListResponseEventSessionErrorPropertiesErrorNameUnknownError EventListResponseEventSessionErrorPropertiesErrorName = "UnknownError" EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError EventListResponseEventSessionErrorPropertiesErrorName = "MessageOutputLengthError" EventListResponseEventSessionErrorPropertiesErrorNameMessageAbortedError EventListResponseEventSessionErrorPropertiesErrorName = "MessageAbortedError" + EventListResponseEventSessionErrorPropertiesErrorNameAPIError EventListResponseEventSessionErrorPropertiesErrorName = "APIError" ) func (r EventListResponseEventSessionErrorPropertiesErrorName) IsKnown() bool { switch r { - case EventListResponseEventSessionErrorPropertiesErrorNameProviderAuthError, EventListResponseEventSessionErrorPropertiesErrorNameUnknownError, EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError, EventListResponseEventSessionErrorPropertiesErrorNameMessageAbortedError: + case EventListResponseEventSessionErrorPropertiesErrorNameProviderAuthError, EventListResponseEventSessionErrorPropertiesErrorNameUnknownError, EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError, EventListResponseEventSessionErrorPropertiesErrorNameMessageAbortedError, EventListResponseEventSessionErrorPropertiesErrorNameAPIError: return true } return false @@ -1461,6 +1609,7 @@ const ( EventListResponseTypeFileWatcherUpdated EventListResponseType = "file.watcher.updated" EventListResponseTypeTodoUpdated EventListResponseType = "todo.updated" EventListResponseTypeSessionIdle EventListResponseType = "session.idle" + EventListResponseTypeSessionCreated EventListResponseType = "session.created" EventListResponseTypeSessionUpdated EventListResponseType = "session.updated" EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted" EventListResponseTypeSessionError EventListResponseType = "session.error" @@ -1470,7 +1619,7 @@ const ( func (r EventListResponseType) IsKnown() bool { switch r { - case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypeSessionCompacted, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeFileWatcherUpdated, EventListResponseTypeTodoUpdated, EventListResponseTypeSessionIdle, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionError, EventListResponseTypeServerConnected, EventListResponseTypeIdeInstalled: + case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypeSessionCompacted, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeFileWatcherUpdated, EventListResponseTypeTodoUpdated, EventListResponseTypeSessionIdle, EventListResponseTypeSessionCreated, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionError, EventListResponseTypeServerConnected, EventListResponseTypeIdeInstalled: return true } return false diff --git a/packages/sdk/go/file.go b/packages/sdk/go/file.go index 8833f425b..34a9c57d4 100644 --- a/packages/sdk/go/file.go +++ b/packages/sdk/go/file.go @@ -144,17 +144,23 @@ func (r FileNodeType) IsKnown() bool { } type FileReadResponse struct { - Content string `json:"content,required"` - Diff string `json:"diff"` - Patch FileReadResponsePatch `json:"patch"` - JSON fileReadResponseJSON `json:"-"` + Content string `json:"content,required"` + Type FileReadResponseType `json:"type,required"` + Diff string `json:"diff"` + Encoding FileReadResponseEncoding `json:"encoding"` + MimeType string `json:"mimeType"` + Patch FileReadResponsePatch `json:"patch"` + JSON fileReadResponseJSON `json:"-"` } // fileReadResponseJSON contains the JSON metadata for the struct // [FileReadResponse] type fileReadResponseJSON struct { Content apijson.Field + Type apijson.Field Diff apijson.Field + Encoding apijson.Field + MimeType apijson.Field Patch apijson.Field raw string ExtraFields map[string]apijson.Field @@ -168,6 +174,34 @@ func (r fileReadResponseJSON) RawJSON() string { return r.raw } +type FileReadResponseType string + +const ( + FileReadResponseTypeText FileReadResponseType = "text" +) + +func (r FileReadResponseType) IsKnown() bool { + switch r { + case FileReadResponseTypeText: + return true + } + return false +} + +type FileReadResponseEncoding string + +const ( + FileReadResponseEncodingBase64 FileReadResponseEncoding = "base64" +) + +func (r FileReadResponseEncoding) IsKnown() bool { + switch r { + case FileReadResponseEncodingBase64: + return true + } + return false +} + type FileReadResponsePatch struct { Hunks []FileReadResponsePatchHunk `json:"hunks,required"` NewFileName string `json:"newFileName,required"` diff --git a/packages/sdk/go/internal/version.go b/packages/sdk/go/internal/version.go index 93a271b9e..8dc40e747 100644 --- a/packages/sdk/go/internal/version.go +++ b/packages/sdk/go/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.16.2" // x-release-please-version +const PackageVersion = "0.18.0" // x-release-please-version diff --git a/packages/sdk/go/session.go b/packages/sdk/go/session.go index 0ee81faad..afd64cb9e 100644 --- a/packages/sdk/go/session.go +++ b/packages/sdk/go/session.go @@ -365,6 +365,7 @@ type AssistantMessage struct { Cost float64 `json:"cost,required"` Mode string `json:"mode,required"` ModelID string `json:"modelID,required"` + ParentID string `json:"parentID,required"` Path AssistantMessagePath `json:"path,required"` ProviderID string `json:"providerID,required"` Role AssistantMessageRole `json:"role,required"` @@ -384,6 +385,7 @@ type assistantMessageJSON struct { Cost apijson.Field Mode apijson.Field ModelID apijson.Field + ParentID apijson.Field Path apijson.Field ProviderID apijson.Field Role apijson.Field @@ -519,7 +521,8 @@ func (r assistantMessageTokensCacheJSON) RawJSON() string { type AssistantMessageError struct { // This field can have the runtime type of [shared.ProviderAuthErrorData], - // [shared.UnknownErrorData], [interface{}], [shared.MessageAbortedErrorData]. + // [shared.UnknownErrorData], [interface{}], [shared.MessageAbortedErrorData], + // [AssistantMessageErrorAPIErrorData]. Data interface{} `json:"data,required"` Name AssistantMessageErrorName `json:"name,required"` JSON assistantMessageErrorJSON `json:"-"` @@ -553,13 +556,14 @@ func (r *AssistantMessageError) UnmarshalJSON(data []byte) (err error) { // // Possible runtime types of the union are [shared.ProviderAuthError], // [shared.UnknownError], [AssistantMessageErrorMessageOutputLengthError], -// [shared.MessageAbortedError]. +// [shared.MessageAbortedError], [AssistantMessageErrorAPIError]. func (r AssistantMessageError) AsUnion() AssistantMessageErrorUnion { return r.union } // Union satisfied by [shared.ProviderAuthError], [shared.UnknownError], -// [AssistantMessageErrorMessageOutputLengthError] or [shared.MessageAbortedError]. +// [AssistantMessageErrorMessageOutputLengthError], [shared.MessageAbortedError] or +// [AssistantMessageErrorAPIError]. type AssistantMessageErrorUnion interface { ImplementsAssistantMessageError() } @@ -584,6 +588,10 @@ func init() { TypeFilter: gjson.JSON, Type: reflect.TypeOf(shared.MessageAbortedError{}), }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(AssistantMessageErrorAPIError{}), + }, ) } @@ -626,6 +634,74 @@ func (r AssistantMessageErrorMessageOutputLengthErrorName) IsKnown() bool { return false } +type AssistantMessageErrorAPIError struct { + Data AssistantMessageErrorAPIErrorData `json:"data,required"` + Name AssistantMessageErrorAPIErrorName `json:"name,required"` + JSON assistantMessageErrorAPIErrorJSON `json:"-"` +} + +// assistantMessageErrorAPIErrorJSON contains the JSON metadata for the struct +// [AssistantMessageErrorAPIError] +type assistantMessageErrorAPIErrorJSON struct { + Data apijson.Field + Name apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *AssistantMessageErrorAPIError) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r assistantMessageErrorAPIErrorJSON) RawJSON() string { + return r.raw +} + +func (r AssistantMessageErrorAPIError) ImplementsAssistantMessageError() {} + +type AssistantMessageErrorAPIErrorData struct { + IsRetryable bool `json:"isRetryable,required"` + Message string `json:"message,required"` + ResponseBody string `json:"responseBody"` + ResponseHeaders map[string]string `json:"responseHeaders"` + StatusCode float64 `json:"statusCode"` + JSON assistantMessageErrorAPIErrorDataJSON `json:"-"` +} + +// assistantMessageErrorAPIErrorDataJSON contains the JSON metadata for the struct +// [AssistantMessageErrorAPIErrorData] +type assistantMessageErrorAPIErrorDataJSON struct { + IsRetryable apijson.Field + Message apijson.Field + ResponseBody apijson.Field + ResponseHeaders apijson.Field + StatusCode apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *AssistantMessageErrorAPIErrorData) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r assistantMessageErrorAPIErrorDataJSON) RawJSON() string { + return r.raw +} + +type AssistantMessageErrorAPIErrorName string + +const ( + AssistantMessageErrorAPIErrorNameAPIError AssistantMessageErrorAPIErrorName = "APIError" +) + +func (r AssistantMessageErrorAPIErrorName) IsKnown() bool { + switch r { + case AssistantMessageErrorAPIErrorNameAPIError: + return true + } + return false +} + type AssistantMessageErrorName string const ( @@ -633,11 +709,12 @@ const ( AssistantMessageErrorNameUnknownError AssistantMessageErrorName = "UnknownError" AssistantMessageErrorNameMessageOutputLengthError AssistantMessageErrorName = "MessageOutputLengthError" AssistantMessageErrorNameMessageAbortedError AssistantMessageErrorName = "MessageAbortedError" + AssistantMessageErrorNameAPIError AssistantMessageErrorName = "APIError" ) func (r AssistantMessageErrorName) IsKnown() bool { switch r { - case AssistantMessageErrorNameProviderAuthError, AssistantMessageErrorNameUnknownError, AssistantMessageErrorNameMessageOutputLengthError, AssistantMessageErrorNameMessageAbortedError: + case AssistantMessageErrorNameProviderAuthError, AssistantMessageErrorNameUnknownError, AssistantMessageErrorNameMessageOutputLengthError, AssistantMessageErrorNameMessageAbortedError, AssistantMessageErrorNameAPIError: return true } return false @@ -918,13 +995,15 @@ type Message struct { Time interface{} `json:"time,required"` Cost float64 `json:"cost"` // This field can have the runtime type of [AssistantMessageError]. - Error interface{} `json:"error"` - Mode string `json:"mode"` - ModelID string `json:"modelID"` + Error interface{} `json:"error"` + Mode string `json:"mode"` + ModelID string `json:"modelID"` + ParentID string `json:"parentID"` // This field can have the runtime type of [AssistantMessagePath]. Path interface{} `json:"path"` ProviderID string `json:"providerID"` - Summary bool `json:"summary"` + // This field can have the runtime type of [UserMessageSummary], [bool]. + Summary interface{} `json:"summary"` // This field can have the runtime type of [[]string]. System interface{} `json:"system"` // This field can have the runtime type of [AssistantMessageTokens]. @@ -943,6 +1022,7 @@ type messageJSON struct { Error apijson.Field Mode apijson.Field ModelID apijson.Field + ParentID apijson.Field Path apijson.Field ProviderID apijson.Field Summary apijson.Field @@ -1013,9 +1093,12 @@ type Part struct { MessageID string `json:"messageID,required"` SessionID string `json:"sessionID,required"` Type PartType `json:"type,required"` + Attempt float64 `json:"attempt"` CallID string `json:"callID"` Cost float64 `json:"cost"` - Filename string `json:"filename"` + // This field can have the runtime type of [PartRetryPartError]. + Error interface{} `json:"error"` + Filename string `json:"filename"` // This field can have the runtime type of [[]string]. Files interface{} `json:"files"` Hash string `json:"hash"` @@ -1023,6 +1106,7 @@ type Part struct { Metadata interface{} `json:"metadata"` Mime string `json:"mime"` Name string `json:"name"` + Reason string `json:"reason"` Snapshot string `json:"snapshot"` // This field can have the runtime type of [FilePartSource], [AgentPartSource]. Source interface{} `json:"source"` @@ -1030,7 +1114,8 @@ type Part struct { State interface{} `json:"state"` Synthetic bool `json:"synthetic"` Text string `json:"text"` - // This field can have the runtime type of [TextPartTime], [ReasoningPartTime]. + // This field can have the runtime type of [TextPartTime], [ReasoningPartTime], + // [PartRetryPartTime]. Time interface{} `json:"time"` // This field can have the runtime type of [StepFinishPartTokens]. Tokens interface{} `json:"tokens"` @@ -1046,14 +1131,17 @@ type partJSON struct { MessageID apijson.Field SessionID apijson.Field Type apijson.Field + Attempt apijson.Field CallID apijson.Field Cost apijson.Field + Error apijson.Field Filename apijson.Field Files apijson.Field Hash apijson.Field Metadata apijson.Field Mime apijson.Field Name apijson.Field + Reason apijson.Field Snapshot apijson.Field Source apijson.Field State apijson.Field @@ -1085,14 +1173,14 @@ func (r *Part) UnmarshalJSON(data []byte) (err error) { // // Possible runtime types of the union are [TextPart], [ReasoningPart], [FilePart], // [ToolPart], [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart], -// [AgentPart]. +// [AgentPart], [PartRetryPart]. func (r Part) AsUnion() PartUnion { return r.union } // Union satisfied by [TextPart], [ReasoningPart], [FilePart], [ToolPart], -// [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart] or -// [AgentPart]. +// [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart], [AgentPart] +// or [PartRetryPart]. type PartUnion interface { implementsPart() } @@ -1137,6 +1225,10 @@ func init() { TypeFilter: gjson.JSON, Type: reflect.TypeOf(AgentPart{}), }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(PartRetryPart{}), + }, ) } @@ -1186,6 +1278,141 @@ func (r PartPatchPartType) IsKnown() bool { return false } +type PartRetryPart struct { + ID string `json:"id,required"` + Attempt float64 `json:"attempt,required"` + Error PartRetryPartError `json:"error,required"` + MessageID string `json:"messageID,required"` + SessionID string `json:"sessionID,required"` + Time PartRetryPartTime `json:"time,required"` + Type PartRetryPartType `json:"type,required"` + JSON partRetryPartJSON `json:"-"` +} + +// partRetryPartJSON contains the JSON metadata for the struct [PartRetryPart] +type partRetryPartJSON struct { + ID apijson.Field + Attempt apijson.Field + Error apijson.Field + MessageID apijson.Field + SessionID apijson.Field + Time apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *PartRetryPart) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r partRetryPartJSON) RawJSON() string { + return r.raw +} + +func (r PartRetryPart) implementsPart() {} + +type PartRetryPartError struct { + Data PartRetryPartErrorData `json:"data,required"` + Name PartRetryPartErrorName `json:"name,required"` + JSON partRetryPartErrorJSON `json:"-"` +} + +// partRetryPartErrorJSON contains the JSON metadata for the struct +// [PartRetryPartError] +type partRetryPartErrorJSON struct { + Data apijson.Field + Name apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *PartRetryPartError) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r partRetryPartErrorJSON) RawJSON() string { + return r.raw +} + +type PartRetryPartErrorData struct { + IsRetryable bool `json:"isRetryable,required"` + Message string `json:"message,required"` + ResponseBody string `json:"responseBody"` + ResponseHeaders map[string]string `json:"responseHeaders"` + StatusCode float64 `json:"statusCode"` + JSON partRetryPartErrorDataJSON `json:"-"` +} + +// partRetryPartErrorDataJSON contains the JSON metadata for the struct +// [PartRetryPartErrorData] +type partRetryPartErrorDataJSON struct { + IsRetryable apijson.Field + Message apijson.Field + ResponseBody apijson.Field + ResponseHeaders apijson.Field + StatusCode apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *PartRetryPartErrorData) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r partRetryPartErrorDataJSON) RawJSON() string { + return r.raw +} + +type PartRetryPartErrorName string + +const ( + PartRetryPartErrorNameAPIError PartRetryPartErrorName = "APIError" +) + +func (r PartRetryPartErrorName) IsKnown() bool { + switch r { + case PartRetryPartErrorNameAPIError: + return true + } + return false +} + +type PartRetryPartTime struct { + Created float64 `json:"created,required"` + JSON partRetryPartTimeJSON `json:"-"` +} + +// partRetryPartTimeJSON contains the JSON metadata for the struct +// [PartRetryPartTime] +type partRetryPartTimeJSON struct { + Created apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *PartRetryPartTime) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r partRetryPartTimeJSON) RawJSON() string { + return r.raw +} + +type PartRetryPartType string + +const ( + PartRetryPartTypeRetry PartRetryPartType = "retry" +) + +func (r PartRetryPartType) IsKnown() bool { + switch r { + case PartRetryPartTypeRetry: + return true + } + return false +} + type PartType string const ( @@ -1198,11 +1425,12 @@ const ( PartTypeSnapshot PartType = "snapshot" PartTypePatch PartType = "patch" PartTypeAgent PartType = "agent" + PartTypeRetry PartType = "retry" ) func (r PartType) IsKnown() bool { switch r { - case PartTypeText, PartTypeReasoning, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot, PartTypePatch, PartTypeAgent: + case PartTypeText, PartTypeReasoning, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot, PartTypePatch, PartTypeAgent, PartTypeRetry: return true } return false @@ -1280,16 +1508,17 @@ func (r ReasoningPartType) IsKnown() bool { } type Session struct { - ID string `json:"id,required"` - Directory string `json:"directory,required"` - ProjectID string `json:"projectID,required"` - Time SessionTime `json:"time,required"` - Title string `json:"title,required"` - Version string `json:"version,required"` - ParentID string `json:"parentID"` - Revert SessionRevert `json:"revert"` - Share SessionShare `json:"share"` - JSON sessionJSON `json:"-"` + ID string `json:"id,required"` + Directory string `json:"directory,required"` + ProjectID string `json:"projectID,required"` + Time SessionTime `json:"time,required"` + Title string `json:"title,required"` + Version string `json:"version,required"` + ParentID string `json:"parentID"` + Revert SessionRevert `json:"revert"` + Share SessionShare `json:"share"` + Summary SessionSummary `json:"summary"` + JSON sessionJSON `json:"-"` } // sessionJSON contains the JSON metadata for the struct [Session] @@ -1303,6 +1532,7 @@ type sessionJSON struct { ParentID apijson.Field Revert apijson.Field Share apijson.Field + Summary apijson.Field raw string ExtraFields map[string]apijson.Field } @@ -1385,6 +1615,55 @@ func (r sessionShareJSON) RawJSON() string { return r.raw } +type SessionSummary struct { + Diffs []SessionSummaryDiff `json:"diffs,required"` + JSON sessionSummaryJSON `json:"-"` +} + +// sessionSummaryJSON contains the JSON metadata for the struct [SessionSummary] +type sessionSummaryJSON struct { + Diffs apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *SessionSummary) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r sessionSummaryJSON) RawJSON() string { + return r.raw +} + +type SessionSummaryDiff struct { + Additions float64 `json:"additions,required"` + After string `json:"after,required"` + Before string `json:"before,required"` + Deletions float64 `json:"deletions,required"` + File string `json:"file,required"` + JSON sessionSummaryDiffJSON `json:"-"` +} + +// sessionSummaryDiffJSON contains the JSON metadata for the struct +// [SessionSummaryDiff] +type sessionSummaryDiffJSON struct { + Additions apijson.Field + After apijson.Field + Before apijson.Field + Deletions apijson.Field + File apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *SessionSummaryDiff) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r sessionSummaryDiffJSON) RawJSON() string { + return r.raw +} + type SnapshotPart struct { ID string `json:"id,required"` MessageID string `json:"messageID,required"` @@ -1433,9 +1712,11 @@ type StepFinishPart struct { ID string `json:"id,required"` Cost float64 `json:"cost,required"` MessageID string `json:"messageID,required"` + Reason string `json:"reason,required"` SessionID string `json:"sessionID,required"` Tokens StepFinishPartTokens `json:"tokens,required"` Type StepFinishPartType `json:"type,required"` + Snapshot string `json:"snapshot"` JSON stepFinishPartJSON `json:"-"` } @@ -1444,9 +1725,11 @@ type stepFinishPartJSON struct { ID apijson.Field Cost apijson.Field MessageID apijson.Field + Reason apijson.Field SessionID apijson.Field Tokens apijson.Field Type apijson.Field + Snapshot apijson.Field raw string ExtraFields map[string]apijson.Field } @@ -1530,6 +1813,7 @@ type StepStartPart struct { MessageID string `json:"messageID,required"` SessionID string `json:"sessionID,required"` Type StepStartPartType `json:"type,required"` + Snapshot string `json:"snapshot"` JSON stepStartPartJSON `json:"-"` } @@ -1539,6 +1823,7 @@ type stepStartPartJSON struct { MessageID apijson.Field SessionID apijson.Field Type apijson.Field + Snapshot apijson.Field raw string ExtraFields map[string]apijson.Field } @@ -1872,7 +2157,9 @@ func (r ToolPart) implementsPart() {} type ToolPartState struct { Status ToolPartStateStatus `json:"status,required"` - Error string `json:"error"` + // This field can have the runtime type of [[]FilePart]. + Attachments interface{} `json:"attachments"` + Error string `json:"error"` // This field can have the runtime type of [interface{}], [map[string]interface{}]. Input interface{} `json:"input"` // This field can have the runtime type of [map[string]interface{}]. @@ -1889,6 +2176,7 @@ type ToolPartState struct { // toolPartStateJSON contains the JSON metadata for the struct [ToolPartState] type toolPartStateJSON struct { Status apijson.Field + Attachments apijson.Field Error apijson.Field Input apijson.Field Metadata apijson.Field @@ -1982,13 +2270,14 @@ func (r ToolPartType) IsKnown() bool { } type ToolStateCompleted struct { - Input map[string]interface{} `json:"input,required"` - Metadata map[string]interface{} `json:"metadata,required"` - Output string `json:"output,required"` - Status ToolStateCompletedStatus `json:"status,required"` - Time ToolStateCompletedTime `json:"time,required"` - Title string `json:"title,required"` - JSON toolStateCompletedJSON `json:"-"` + Input map[string]interface{} `json:"input,required"` + Metadata map[string]interface{} `json:"metadata,required"` + Output string `json:"output,required"` + Status ToolStateCompletedStatus `json:"status,required"` + Time ToolStateCompletedTime `json:"time,required"` + Title string `json:"title,required"` + Attachments []FilePart `json:"attachments"` + JSON toolStateCompletedJSON `json:"-"` } // toolStateCompletedJSON contains the JSON metadata for the struct @@ -2000,6 +2289,7 @@ type toolStateCompletedJSON struct { Status apijson.Field Time apijson.Field Title apijson.Field + Attachments apijson.Field raw string ExtraFields map[string]apijson.Field } @@ -2224,11 +2514,12 @@ func (r toolStateRunningTimeJSON) RawJSON() string { } type UserMessage struct { - ID string `json:"id,required"` - Role UserMessageRole `json:"role,required"` - SessionID string `json:"sessionID,required"` - Time UserMessageTime `json:"time,required"` - JSON userMessageJSON `json:"-"` + ID string `json:"id,required"` + Role UserMessageRole `json:"role,required"` + SessionID string `json:"sessionID,required"` + Time UserMessageTime `json:"time,required"` + Summary UserMessageSummary `json:"summary"` + JSON userMessageJSON `json:"-"` } // userMessageJSON contains the JSON metadata for the struct [UserMessage] @@ -2237,6 +2528,7 @@ type userMessageJSON struct { Role apijson.Field SessionID apijson.Field Time apijson.Field + Summary apijson.Field raw string ExtraFields map[string]apijson.Field } @@ -2285,6 +2577,60 @@ func (r userMessageTimeJSON) RawJSON() string { return r.raw } +type UserMessageSummary struct { + Diffs []UserMessageSummaryDiff `json:"diffs,required"` + Body string `json:"body"` + Title string `json:"title"` + JSON userMessageSummaryJSON `json:"-"` +} + +// userMessageSummaryJSON contains the JSON metadata for the struct +// [UserMessageSummary] +type userMessageSummaryJSON struct { + Diffs apijson.Field + Body apijson.Field + Title apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *UserMessageSummary) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r userMessageSummaryJSON) RawJSON() string { + return r.raw +} + +type UserMessageSummaryDiff struct { + Additions float64 `json:"additions,required"` + After string `json:"after,required"` + Before string `json:"before,required"` + Deletions float64 `json:"deletions,required"` + File string `json:"file,required"` + JSON userMessageSummaryDiffJSON `json:"-"` +} + +// userMessageSummaryDiffJSON contains the JSON metadata for the struct +// [UserMessageSummaryDiff] +type userMessageSummaryDiffJSON struct { + Additions apijson.Field + After apijson.Field + Before apijson.Field + Deletions apijson.Field + File apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *UserMessageSummaryDiff) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r userMessageSummaryDiffJSON) RawJSON() string { + return r.raw +} + type SessionCommandResponse struct { Info AssistantMessage `json:"info,required"` Parts []Part `json:"parts,required"` @@ -2542,6 +2888,7 @@ type SessionPromptParams struct { Agent param.Field[string] `json:"agent"` MessageID param.Field[string] `json:"messageID"` Model param.Field[SessionPromptParamsModel] `json:"model"` + NoReply param.Field[bool] `json:"noReply"` System param.Field[string] `json:"system"` Tools param.Field[map[string]bool] `json:"tools"` } diff --git a/packages/sdk/go/session_test.go b/packages/sdk/go/session_test.go index f2263c7bc..6f910caf2 100644 --- a/packages/sdk/go/session_test.go +++ b/packages/sdk/go/session_test.go @@ -361,7 +361,8 @@ func TestSessionPromptWithOptionalParams(t *testing.T) { ModelID: opencode.F("modelID"), ProviderID: opencode.F("providerID"), }), - System: opencode.F("system"), + NoReply: opencode.F(true), + System: opencode.F("system"), Tools: opencode.F(map[string]bool{ "foo": true, }), From 49ea5aa2ada1aed3ed5c9fc8debad5a28348d6e8 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 28 Oct 2025 15:12:37 +0000 Subject: [PATCH 50/56] release: v0.15.20 --- bun.lock | 22 +++++++++++----------- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 14 files changed, 24 insertions(+), 24 deletions(-) diff --git a/bun.lock b/bun.lock index eec72e6c6..7e1fceab3 100644 --- a/bun.lock +++ b/bun.lock @@ -37,7 +37,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "0.15.19", + "version": "0.15.20", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -64,7 +64,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "0.15.19", + "version": "0.15.20", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -88,7 +88,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "0.15.19", + "version": "0.15.20", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -109,7 +109,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "0.15.19", + "version": "0.15.20", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -150,7 +150,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "0.15.19", + "version": "0.15.20", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -166,7 +166,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "0.15.19", + "version": "0.15.20", "bin": { "opencode": "./bin/opencode", }, @@ -230,7 +230,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "0.15.19", + "version": "0.15.20", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -250,7 +250,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "0.15.19", + "version": "0.15.20", "devDependencies": { "@hey-api/openapi-ts": "0.81.0", "@tsconfig/node22": "catalog:", @@ -261,7 +261,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "0.15.19", + "version": "0.15.20", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -274,7 +274,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "0.15.19", + "version": "0.15.20", "dependencies": { "@kobalte/core": "catalog:", "@pierre/precision-diffs": "catalog:", @@ -297,7 +297,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "0.15.19", + "version": "0.15.20", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 868aa1899..0f36562b4 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -7,7 +7,7 @@ "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json", "start": "vinxi start", - "version": "0.15.19" + "version": "0.15.20" }, "dependencies": { "@ibm/plex": "6.4.1", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 948e4af27..16de536d6 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "0.15.19", + "version": "0.15.20", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 4be5f460a..f2cb56d29 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "0.15.19", + "version": "0.15.20", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index bf62733e7..f93660af7 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "0.15.19", + "version": "0.15.20", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index e401c2242..a2184a75a 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "0.15.19", + "version": "0.15.20", "description": "", "type": "module", "scripts": { diff --git a/packages/function/package.json b/packages/function/package.json index db56da8de..b1dd969e2 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "0.15.19", + "version": "0.15.20", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 93a69fbed..c50d4eae0 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "0.15.19", + "version": "0.15.20", "name": "opencode", "type": "module", "private": true, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 4395fb3e0..27736e2a1 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "0.15.19", + "version": "0.15.20", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 042e35080..dbb890f60 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "0.15.19", + "version": "0.15.20", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/slack/package.json b/packages/slack/package.json index 5fc872c12..16756f5a1 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "0.15.19", + "version": "0.15.20", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index a6d6a0125..d85dbee58 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "0.15.19", + "version": "0.15.20", "type": "module", "exports": { ".": "./src/components/index.ts", diff --git a/packages/web/package.json b/packages/web/package.json index 5337a4446..6cacf9bd0 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "0.15.19", + "version": "0.15.20", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 918cde136..854797b6e 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "0.15.19", + "version": "0.15.20", "publisher": "sst-dev", "repository": { "type": "git", From 74acd08eadf4d6078ad0b8aa2da3fd42eed5cb49 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 28 Oct 2025 10:21:32 -0500 Subject: [PATCH 51/56] add catch for mcp tool execution --- packages/opencode/src/session/prompt.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 26a04cb8e..d27dc24bc 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -582,7 +582,17 @@ export namespace SessionPrompt { args, }, ) - const result = await execute(args, opts) + const result = await execute(args, opts).catch((err) => { + log.error("Error executing tool", { error: err, tool: key }) + return { + content: [ + { + type: "text", + text: `Failed to execute tool: ${err instanceof Error ? err.message : String(err)}`, + }, + ], + } + }) await Plugin.trigger( "tool.execute.after", From 643c22d21fb438236ed9e218f085aad0c73ca8c1 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 28 Oct 2025 10:22:53 -0500 Subject: [PATCH 52/56] add catch for mcp tool execution --- packages/opencode/src/session/prompt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index d27dc24bc..adaa79f31 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -582,7 +582,7 @@ export namespace SessionPrompt { args, }, ) - const result = await execute(args, opts).catch((err) => { + const result = await execute(args, opts).catch((err: unknown) => { log.error("Error executing tool", { error: err, tool: key }) return { content: [ From eb398f1951764af812c7ef01f37f01da862a852a Mon Sep 17 00:00:00 2001 From: oribi Date: Tue, 28 Oct 2025 18:50:09 +0200 Subject: [PATCH 53/56] add OPENCODE_CONFIG_DIR to allow loading a custom config directory (#3504) Co-authored-by: opencode-agent[bot] Co-authored-by: rekram1-node --- packages/opencode/src/config/config.ts | 5 +++++ packages/opencode/src/flag/flag.ts | 1 + packages/web/src/content/docs/config.mdx | 16 ++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 4c6003b9e..42d59226e 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -61,6 +61,11 @@ export namespace Config { )), ] + if (Flag.OPENCODE_CONFIG_DIR) { + directories.push(Flag.OPENCODE_CONFIG_DIR) + log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR }) + } + for (const dir of directories) { await assertValid(dir) installDependencies(dir) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 879aa758a..86ca07652 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -1,6 +1,7 @@ export namespace Flag { export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE") export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"] + export const OPENCODE_CONFIG_DIR = process.env["OPENCODE_CONFIG_DIR"] export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"] export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE") export const OPENCODE_DISABLE_PRUNE = truthy("OPENCODE_DISABLE_PRUNE") diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index f3b2a05a0..2a259ff37 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -61,6 +61,22 @@ opencode run "Hello world" --- +### Custom directory + +You can specify a custom config directory using the `OPENCODE_CONFIG_DIR` +environment variable. This directory will be searched for agents, commands, +modes, and plugins just like the standard `.opencode` directory, and should +follow the same structure. + +```bash +export OPENCODE_CONFIG_DIR=/path/to/my/config-directory +opencode run "Hello world" +``` + +Note: The custom directory is loaded after the global config and `.opencode` directories, so it can override their settings. + +--- + ## Schema The config file has a schema that's defined in [**`opencode.ai/config.json`**](https://opencode.ai/config.json). From b66e7b6fce07ff9a225dd225fc198b16718a1239 Mon Sep 17 00:00:00 2001 From: Danilo Favato Date: Tue, 28 Oct 2025 14:09:41 -0300 Subject: [PATCH 54/56] tweak: add experimental chatMaxRetries to config (#2116) Co-authored-by: GitHub Action Co-authored-by: Aiden Cline --- packages/opencode/src/config/config.ts | 1 + packages/opencode/src/session/compaction.ts | 9 ++++++--- packages/opencode/src/session/prompt.ts | 9 ++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 42d59226e..83c518a68 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -586,6 +586,7 @@ export namespace Config { .optional(), }) .optional(), + chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"), disable_paste_summary: z.boolean().optional(), }) .optional(), diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 657ac4475..4896d5f5e 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -16,6 +16,7 @@ import { Log } from "../util/log" import { SessionLock } from "./lock" import { ProviderTransform } from "@/provider/transform" import { SessionRetry } from "./retry" +import { Config } from "@/config/config" export namespace SessionCompaction { const log = Log.create({ service: "session.compaction" }) @@ -258,12 +259,14 @@ export namespace SessionCompaction { } let stream = doStream() + const cfg = await Config.get() + const maxRetries = cfg.experimental?.chatMaxRetries ?? MAX_RETRIES let result = await process(stream, { count: 0, - max: MAX_RETRIES, + max: maxRetries, }) if (result.shouldRetry) { - for (let retry = 1; retry < MAX_RETRIES; retry++) { + for (let retry = 1; retry < maxRetries; retry++) { const lastRetryPart = result.parts.findLast((p) => p.type === "retry") if (lastRetryPart) { @@ -300,7 +303,7 @@ export namespace SessionCompaction { stream = doStream() result = await process(stream, { count: retry, - max: MAX_RETRIES, + max: maxRetries, }) if (!result.shouldRetry) { break diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index adaa79f31..4d1aa0211 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -50,6 +50,7 @@ import { Command } from "../command" import { $, fileURLToPath } from "bun" import { ConfigMarkdown } from "../config/markdown" import { SessionSummary } from "./summary" +import { Config } from "@/config/config" export namespace SessionPrompt { const log = Log.create({ service: "session.prompt" }) @@ -330,12 +331,14 @@ export namespace SessionPrompt { }) let stream = doStream() + const cfg = await Config.get() + const maxRetries = cfg.experimental?.chatMaxRetries ?? MAX_RETRIES let result = await processor.process(stream, { count: 0, - max: MAX_RETRIES, + max: maxRetries, }) if (result.shouldRetry) { - for (let retry = 1; retry < MAX_RETRIES; retry++) { + for (let retry = 1; retry < maxRetries; retry++) { const lastRetryPart = result.parts.findLast((p) => p.type === "retry") if (lastRetryPart) { @@ -372,7 +375,7 @@ export namespace SessionPrompt { stream = doStream() result = await processor.process(stream, { count: retry, - max: MAX_RETRIES, + max: maxRetries, }) if (!result.shouldRetry) { break From c1515316f54f129aa7f504d1cee79476c194c797 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 28 Oct 2025 14:08:10 -0400 Subject: [PATCH 55/56] core: fix additions and deletions counting in edit tool filediff --- packages/opencode/src/tool/edit.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index a8e6fc3b6..7429c44b8 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -7,7 +7,7 @@ import z from "zod" import * as path from "path" import { Tool } from "./tool" import { LSP } from "../lsp" -import { createTwoFilesPatch } from "diff" +import { createTwoFilesPatch, diffLines } from "diff" import { Permission } from "../permission" import DESCRIPTION from "./edit.txt" import { File } from "../file" @@ -16,6 +16,7 @@ import { FileTime } from "../file/time" import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" import { Agent } from "../agent/agent" +import { Snapshot } from "@/snapshot" export const EditTool = Tool.define("edit", { description: DESCRIPTION, @@ -114,10 +115,23 @@ export const EditTool = Tool.define("edit", { } } + const filediff: Snapshot.FileDiff = { + file: filePath, + before: contentOld, + after: contentNew, + additions: 0, + deletions: 0, + } + for (const change of diffLines(contentOld, contentNew)) { + if (change.added) filediff.additions += change.count || 0 + if (change.removed) filediff.deletions += change.count || 0 + } + return { metadata: { diagnostics, diff, + filediff, }, title: `${path.relative(Instance.worktree, filePath)}`, output, From 1309ca7a812cdd548163d7bf8f21cb3c412c040f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 28 Oct 2025 14:13:47 -0400 Subject: [PATCH 56/56] ignore --- packages/opencode/src/session/summary.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 0e231e6fa..2a31f39ea 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -86,7 +86,7 @@ export namespace SessionSummary { ) { const result = await generateText({ model: small.language, - maxOutputTokens: 50, + maxOutputTokens: 100, messages: [ { role: "user",