From c093e4c7057f4b926ec8ce6911baf6dbfb1ad42c Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 9 Dec 2025 20:48:33 -0800 Subject: [PATCH 01/30] WebSocket-based IDE connections, supporting streamed cursor selection into prompt context, diff display view --- package.json | 1 + packages/opencode/bin/opencode-dev | 3 + packages/opencode/src/cli/cmd/tui/app.tsx | 9 + .../src/cli/cmd/tui/component/dialog-ide.tsx | 76 + .../cmd/tui/component/prompt/autocomplete.tsx | 5 + .../cli/cmd/tui/component/prompt/index.tsx | 95 + .../src/cli/cmd/tui/context/local.tsx | 25 + .../opencode/src/cli/cmd/tui/context/sync.tsx | 4 + .../src/cli/cmd/tui/routes/session/footer.tsx | 7 + packages/opencode/src/ide/connection.ts | 153 ++ packages/opencode/src/ide/index.ts | 135 ++ packages/opencode/src/mcp/index.ts | 17 +- packages/opencode/src/mcp/ws.ts | 64 + packages/opencode/src/permission/index.ts | 6 + packages/opencode/src/server/server.ts | 65 + packages/opencode/src/session/processor.ts | 7 +- packages/opencode/src/tool/edit.ts | 33 +- packages/sdk/js/openapi.json | 1746 ++++++++++++++--- packages/sdk/js/src/v2/gen/sdk.gen.ts | 82 + packages/sdk/js/src/v2/gen/types.gen.ts | 104 + packages/sdk/openapi.json | 1501 +++++++++++--- 21 files changed, 3597 insertions(+), 541 deletions(-) create mode 100755 packages/opencode/bin/opencode-dev create mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-ide.tsx create mode 100644 packages/opencode/src/ide/connection.ts create mode 100644 packages/opencode/src/mcp/ws.ts diff --git a/package.json b/package.json index b866c9bdf087..3d718b96490b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "typecheck": "bun turbo typecheck", "prepare": "husky", + "generate": "bun run --cwd packages/sdk/js build", "random": "echo 'Random script'", "hello": "echo 'Hello World!'" }, diff --git a/packages/opencode/bin/opencode-dev b/packages/opencode/bin/opencode-dev new file mode 100755 index 000000000000..65ba205faa8c --- /dev/null +++ b/packages/opencode/bin/opencode-dev @@ -0,0 +1,3 @@ +#!/bin/bash +cd /Users/tcdent/Work/opencode +exec bun run --cwd packages/opencode --conditions=browser src/index.ts "$@" diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 226de4796b18..fb063f8566fc 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -13,6 +13,7 @@ import { SyncProvider, useSync } from "@tui/context/sync" import { LocalProvider, useLocal } from "@tui/context/local" import { DialogModel, useConnected } from "@tui/component/dialog-model" import { DialogMcp } from "@tui/component/dialog-mcp" +import { DialogIde } from "@tui/component/dialog-ide" import { DialogStatus } from "@tui/component/dialog-status" import { DialogThemeList } from "@tui/component/dialog-theme-list" import { DialogHelp } from "./ui/dialog-help" @@ -310,6 +311,14 @@ function App() { dialog.replace(() => ) }, }, + { + title: "Toggle IDEs", + value: "ide.list", + category: "Agent", + onSelect: () => { + dialog.replace(() => ) + }, + }, { title: "Agent cycle", value: "agent.cycle", diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-ide.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-ide.tsx new file mode 100644 index 000000000000..8998f6f5a0b7 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-ide.tsx @@ -0,0 +1,76 @@ +import { createMemo, createSignal } from "solid-js" +import { useLocal } from "@tui/context/local" +import { useSync } from "@tui/context/sync" +import { map, pipe, entries, sortBy } from "remeda" +import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select" +import { useTheme } from "../context/theme" +import { Keybind } from "@/util/keybind" +import { TextAttributes } from "@opentui/core" + +function Status(props: { connected: boolean; loading: boolean }) { + const { theme } = useTheme() + if (props.loading) { + return ⋯ Loading + } + if (props.connected) { + return ✓ Connected + } + return ○ Disconnected +} + +export function DialogIde() { + const local = useLocal() + const sync = useSync() + const [, setRef] = createSignal>() + const [loading, setLoading] = createSignal(null) + + const options = createMemo(() => { + const ideData = sync.data.ide + const loadingIde = loading() + const projectDir = process.cwd() + + return pipe( + ideData ?? {}, + entries(), + sortBy( + ([key]) => { + const folders = local.ide.getWorkspaceFolders(key) + // Exact match - highest priority + if (folders.some((folder: string) => folder === projectDir)) return 0 + // IDE workspace contains current directory (we're in a subdirectory of IDE workspace) + if (folders.some((folder: string) => projectDir.startsWith(folder + "/"))) return 1 + return 2 + }, + ([, status]) => status.name, + ), + map(([key, status]) => { + return { + value: key, + title: status.name, + description: local.ide.getWorkspaceFolders(key)[0], + footer: , + category: undefined, + } + }), + ) + }) + + const keybinds = createMemo(() => [ + { + keybind: Keybind.parse("space")[0], + title: "toggle", + onTrigger: async (option: DialogSelectOption) => { + if (loading() !== null) return + + setLoading(option.value) + try { + await local.ide.toggle(option.value) + } finally { + setLoading(null) + } + }, + }, + ]) + + return {}} /> +} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index c40aa114ac83..b193b2e2b4de 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -315,6 +315,11 @@ export function Autocomplete(props: { description: "toggle MCPs", onSelect: () => command.trigger("mcp.list"), }, + { + display: "/ide", + description: "toggle IDEs", + onSelect: () => command.trigger("ide.list"), + }, { display: "/theme", description: "toggle theme", diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 7d5bbb9f06a0..d18a08412809 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -19,6 +19,7 @@ import { useExit } from "../../context/exit" import { Clipboard } from "../../util/clipboard" import type { FilePart } from "@opencode-ai/sdk/v2" import { TuiEvent } from "../../event" +import { Ide } from "@/ide" import { iife } from "@/util/iife" import { Locale } from "@/util/locale" import { createColors, createFrames } from "../../ui/spinner.ts" @@ -268,6 +269,10 @@ export function Prompt(props: PromptProps) { input.insertText(evt.properties.text) }) + sdk.event.on(Ide.Event.SelectionChanged.type, (evt) => { + updateIdeSelection(evt.properties.selection) + }) + createEffect(() => { if (props.disabled) input.cursorColor = theme.backgroundElement if (!props.disabled) input.cursorColor = theme.text @@ -298,6 +303,95 @@ export function Prompt(props: PromptProps) { promptPartTypeId = input.extmarks.registerType("prompt-part") }) + // Track IDE selection extmark so we can update/remove it + let ideSelectionExtmarkId: number | null = null + + function removeExtmark(extmarkId: number) { + const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId) + const extmark = allExtmarks.find((e) => e.id === extmarkId) + const partIndex = store.extmarkToPartIndex.get(extmarkId) + + if (partIndex !== undefined) { + setStore( + produce((draft) => { + draft.prompt.parts.splice(partIndex, 1) + draft.extmarkToPartIndex.delete(extmarkId) + const newMap = new Map() + for (const [id, idx] of draft.extmarkToPartIndex) { + newMap.set(id, idx > partIndex ? idx - 1 : idx) + } + draft.extmarkToPartIndex = newMap + }), + ) + } + + if (extmark) { + const savedOffset = input.cursorOffset + input.cursorOffset = extmark.start + const start = { ...input.logicalCursor } + input.cursorOffset = extmark.end + 1 + input.deleteRange(start.row, start.col, input.logicalCursor.row, input.logicalCursor.col) + input.cursorOffset = + savedOffset > extmark.start + ? Math.max(extmark.start, savedOffset - (extmark.end + 1 - extmark.start)) + : savedOffset + } + + input.extmarks.delete(extmarkId) + } + + function updateIdeSelection(selection: Ide.Selection | null) { + if (!input || promptPartTypeId === undefined) return + + if (ideSelectionExtmarkId !== null) { + removeExtmark(ideSelectionExtmarkId) + ideSelectionExtmarkId = null + } + + // Ignore empty selections (just a cursor position) + if (!selection || !selection.text) return + + const { filePath, text } = selection + const filename = filePath.split("/").pop() || filePath + const start = selection.selection.start.line + 1 + const end = selection.selection.end.line + 1 + const lines = text.split("\n").length + + const previewText = `[${filename}:${start}-${end} ~${lines} lines]` + const contextText = `\`\`\`\n# ${filePath}:${start}-${end}\n${text}\n\`\`\`\n\n` + + const extmarkStart = input.visualCursor.offset + const extmarkEnd = extmarkStart + previewText.length + + input.insertText(previewText + " ") + + ideSelectionExtmarkId = input.extmarks.create({ + start: extmarkStart, + end: extmarkEnd, + virtual: true, + styleId: pasteStyleId, + typeId: promptPartTypeId, + }) + + setStore( + produce((draft) => { + const partIndex = draft.prompt.parts.length + draft.prompt.parts.push({ + type: "text" as const, + text: contextText, + source: { + text: { + start: extmarkStart, + end: extmarkEnd, + value: previewText, + }, + }, + }) + draft.extmarkToPartIndex.set(ideSelectionExtmarkId!, partIndex) + }), + ) + } + function restoreExtmarksFromParts(parts: PromptInfo["parts"]) { input.extmarks.clear() setStore("extmarkToPartIndex", new Map()) @@ -498,6 +592,7 @@ export function Prompt(props: PromptProps) { parts: [], }) setStore("extmarkToPartIndex", new Map()) + ideSelectionExtmarkId = null props.onSubmit?.() // temporary hack to make sure the message is sent diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 6cc97e04167e..82bcefced13c 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -329,10 +329,35 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }, } + const ide = { + isConnected(name: string) { + const status = sync.data.ide[name] + return status?.status === "connected" + }, + getWorkspaceFolders(name: string) { + const status = sync.data.ide[name] + if (status && "workspaceFolders" in status && status.workspaceFolders) { + return status.workspaceFolders + } + return [] + }, + async toggle(name: string) { + const current = sync.data.ide[name] + if (current?.status === "connected") { + await sdk.client.ide.disconnect({ name }) + } else { + await sdk.client.ide.connect({ name }) + } + const status = await sdk.client.ide.status() + if (status.data) sync.set("ide", status.data) + }, + } + const result = { model, agent, mcp, + ide, } return result }, diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 28ea60a67ffc..570e75f2a421 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -10,6 +10,7 @@ import type { Permission, LspStatus, McpStatus, + IdeStatus, FormatterStatus, SessionStatus, ProviderListResponse, @@ -60,6 +61,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ mcp: { [key: string]: McpStatus } + ide: { [key: string]: IdeStatus } formatter: FormatterStatus[] vcs: VcsInfo | undefined }>({ @@ -84,6 +86,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ part: {}, lsp: [], mcp: {}, + ide: {}, formatter: [], vcs: undefined, }) @@ -282,6 +285,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ sdk.client.command.list().then((x) => setStore("command", x.data ?? [])), sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)), sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)), + sdk.client.ide.status().then((x) => setStore("ide", x.data!)), sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)), sdk.client.session.status().then((x) => setStore("session_status", x.data!)), sdk.client.provider.auth().then((x) => setStore("provider_auth", x.data ?? {})), diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx index e889373e6f8f..604b17c028ca 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx @@ -13,6 +13,7 @@ export function Footer() { const mcp = createMemo(() => Object.keys(sync.data.mcp)) const mcpError = createMemo(() => Object.values(sync.data.mcp).some((x) => x.status === "failed")) const lsp = createMemo(() => Object.keys(sync.data.lsp)) + const ide = createMemo(() => Object.values(sync.data.ide).find((x) => x.status === "connected")) const permissions = createMemo(() => { if (route.data.type !== "session") return [] return sync.data.permission[route.data.sessionID] ?? [] @@ -79,6 +80,12 @@ export function Footer() { {mcp().length} MCP + + + + {ide()!.name} + + /status diff --git a/packages/opencode/src/ide/connection.ts b/packages/opencode/src/ide/connection.ts new file mode 100644 index 000000000000..d7baf295f63b --- /dev/null +++ b/packages/opencode/src/ide/connection.ts @@ -0,0 +1,153 @@ +import z from "zod/v4" +import os from "os" +import path from "path" +import { Glob } from "bun" +import { Log } from "../util/log" +import { WebSocketClientTransport, McpError } from "../mcp/ws" + +const log = Log.create({ service: "ide" }) + +const LOCK_DIR = path.join(os.homedir(), ".claude", "ide") +const WS_PREFIX = "ws://127.0.0.1" + +const LockFile = { + schema: z.object({ + port: z.number(), + url: z.instanceof(URL), + pid: z.number(), + workspaceFolders: z.array(z.string()), + ideName: z.string(), + transport: z.string(), + authToken: z.string(), + }), + async fromFile(file: string) { + const port = parseInt(path.basename(file, ".lock")) + const url = new URL(`${WS_PREFIX}:${port}`) + const content = await Bun.file(file).text() + const parsed = this.schema.safeParse({ port, url, ...JSON.parse(content) }) + if (!parsed.success) { + log.warn("invalid lock file", { file, error: parsed.error }) + return undefined + } + return parsed.data + }, +} +type LockFile = z.infer + +export async function discoverLockFiles(): Promise> { + const results = new Map() + + const glob = new Glob("*.lock") + for await (const file of glob.scan({ cwd: LOCK_DIR, absolute: true })) { + const lockFile = await LockFile.fromFile(file) + if (!lockFile) continue + + try { + process.kill(lockFile.pid, 0) + } catch { + log.debug("stale lock file, process not running", { file, pid: lockFile.pid }) + continue + } + + results.set(String(lockFile.port), lockFile) + } + + return results +} + +export class Connection { + key: string + name: string + private transport: WebSocketClientTransport + private requestId = 0 + private pendingRequests = new Map>() + onNotification?: (method: string, params: unknown) => void + onClose?: () => void + + private constructor(key: string, name: string, transport: WebSocketClientTransport) { + this.key = key + this.name = name + this.transport = transport + } + + static async create(key: string): Promise { + const discovered = await discoverLockFiles() + const lockFile = discovered.get(key) + if (!lockFile) { + throw new Error(`IDE instance not found: ${key}`) + } + + const transport = new WebSocketClientTransport(lockFile.url, { + headers: { + // TODO research standardized header for this + "x-claude-code-ide-authorization": lockFile.authToken, + }, + }) + + const connection = new Connection(key, lockFile.ideName, transport) + + transport.onmessage = (message) => { + connection.handleMessage(message as any) + } + + transport.onclose = () => { + log.info("IDE transport closed", { key }) + connection.onClose?.() + } + + transport.onerror = (err) => { + log.error("IDE transport error", { key, error: err }) + } + + await transport.start() + + return connection + } + + private handleMessage(payload: { + id?: string | number + method?: string + params?: unknown + result?: unknown + error?: { code: number; message: string; data?: unknown } + }) { + // Handle responses to our requests + const pending = payload.id !== undefined ? this.pendingRequests.get(payload.id) : undefined + if (pending) { + this.pendingRequests.delete(payload.id!) + if (payload.error) { + const { code, message, data } = payload.error + // TODO put code in message on ws server. + pending.reject(new McpError(code, `${message} (code: ${code})`, data)) + } else { + pending.resolve(payload.result) + } + return + } + + // Handle notifications + if (payload.method) { + this.onNotification?.(payload.method, payload.params) + } + } + + async request(method: string, params?: Record): Promise { + const id = ++this.requestId + const pending = Promise.withResolvers() + this.pendingRequests.set(id, pending as PromiseWithResolvers) + this.transport.send({ + jsonrpc: "2.0" as const, + id, + method: `tools/call`, + params: { + name: method, + arguments: params ?? {}, + }, + }) + return pending.promise + } + + async close() { + await this.transport.close() + } +} diff --git a/packages/opencode/src/ide/index.ts b/packages/opencode/src/ide/index.ts index 268f115fc30d..771ebbaf028b 100644 --- a/packages/opencode/src/ide/index.ts +++ b/packages/opencode/src/ide/index.ts @@ -1,8 +1,12 @@ import { spawn } from "bun" import z from "zod" +import path from "path" import { NamedError } from "@opencode-ai/util/error" import { Log } from "../util/log" import { Bus } from "../bus" +import { Instance } from "../project/instance" +import { Connection, discoverLockFiles } from "./connection" +import { Permission } from "../permission" const SUPPORTED_IDES = [ { name: "Windsurf" as const, cmd: "windsurf" }, @@ -15,6 +19,30 @@ const SUPPORTED_IDES = [ export namespace Ide { const log = Log.create({ service: "ide" }) + export const Status = z + .object({ + status: z.enum(["connected", "disconnected", "failed"]), + name: z.string(), + workspaceFolders: z.array(z.string()).optional(), + error: z.string().optional(), + }) + .meta({ ref: "IdeStatus" }) + export type Status = z.infer + + export const Selection = z + .object({ + text: z.string(), + filePath: z.string(), + fileUrl: z.string(), + selection: z.object({ + start: z.object({ line: z.number(), character: z.number() }), + end: z.object({ line: z.number(), character: z.number() }), + isEmpty: z.boolean(), + }), + }) + .meta({ ref: "IdeSelection" }) + export type Selection = z.infer + export const Event = { Installed: Bus.event( "ide.installed", @@ -22,6 +50,12 @@ export namespace Ide { ide: z.string(), }), ), + SelectionChanged: Bus.event( + "ide.selection.updated", + z.object({ + selection: Selection, + }), + ), } export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", z.object({})) @@ -72,4 +106,105 @@ export namespace Ide { throw new AlreadyInstalledError({}) } } + + // Connection + let activeConnection: Connection | null = null + + function tabName(filePath: string) { + return `[opencode] Edit: ${path.basename(filePath)} ⧉` + } + + export async function status(): Promise> { + const discovered = await discoverLockFiles() + const result: Record = {} + + for (const [key, lockFile] of discovered) { + result[key] = { + status: activeConnection?.key === key ? "connected" : "disconnected", + name: lockFile.ideName, + workspaceFolders: lockFile.workspaceFolders, + } + } + + return result + } + + export async function connect(key: string): Promise> { + if (activeConnection) { + await disconnect() + } + + const instanceDirectory = Instance.directory + const connection = await Connection.create(key) + + connection.onNotification = (method, params) => { + handleNotification(method, params, instanceDirectory) + } + + connection.onClose = () => { + log.info("IDE connection closed callback", { key }) + if (activeConnection?.key === key) { + activeConnection = null + } + } + + activeConnection = connection + + return status() + } + + function handleNotification(method: string, params: unknown, instanceDirectory: string) { + if (method === "selection_changed") { + const parsed = Selection.safeParse(params) + if (!parsed.success) { + log.warn("failed to parse selection_changed params", { error: parsed.error }) + return + } + Instance.provide({ + directory: instanceDirectory, + fn: () => { + Bus.publish(Event.SelectionChanged, { selection: parsed.data }) + }, + }) + } + } + + export async function disconnect(): Promise> { + if (!activeConnection) { + return status() + } + + await activeConnection.close() + activeConnection = null + + return status() + } + + export function active(): Connection | null { + return activeConnection + } + + export async function openDiff(filePath: string, newContents: string): Promise { + if (!activeConnection) { + throw new Error("No IDE connected") + } + const name = tabName(filePath) + log.info("openDiff", { tabName: name }) + const result = await activeConnection.request<{ content: Array<{ type: string; text: string }> }>("openDiff", { + old_file_path: filePath, + new_file_path: filePath, + new_file_contents: newContents, + tab_name: name, + }) + log.info("openDiff result", { text: result.content?.[0]?.text }) + const text = result.content?.[0]?.text + if (text === "FILE_SAVED") return "once" + if (text === "DIFF_REJECTED") return "reject" + throw new Error(`Unexpected openDiff result: ${text}`) + } + + export async function closeTab(filePath: string): Promise { + if (!activeConnection) return + await activeConnection.request("close_tab", { tab_name: tabName(filePath) }) + } } diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index e030f83b53c8..3578cd91c84b 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -2,6 +2,7 @@ import { type Tool } from "ai" import { experimental_createMCPClient } from "@ai-sdk/mcp" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" +import { WebSocketClientTransport } from "./ws" import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" import { Config } from "../config/config" @@ -73,9 +74,13 @@ export namespace MCP { export type Status = z.infer type MCPClient = Awaited> - // Store transports for OAuth servers to allow finishing auth + // Transport types for MCP connections type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport - const pendingOAuthTransports = new Map() + type TransportWithoutAuth = WebSocketClientTransport + type Transport = TransportWithAuth | TransportWithoutAuth + + // Store transports for finishing auth + const pendingOAuthTransports = new Map() const state = Instance.state( async () => { @@ -184,7 +189,7 @@ export namespace MCP { ) } - const transports: Array<{ name: string; transport: TransportWithAuth }> = [ + const transports = [ { name: "StreamableHTTP", transport: new StreamableHTTPClientTransport(new URL(mcp.url), { @@ -199,6 +204,12 @@ export namespace MCP { requestInit: oauthDisabled && mcp.headers ? { headers: mcp.headers } : undefined, }), }, + { + name: "WebSocket", + transport: new WebSocketClientTransport(new URL(mcp.url), { + headers: oauthDisabled && mcp.headers ? mcp.headers : undefined, + }), + }, ] let lastError: Error | undefined diff --git a/packages/opencode/src/mcp/ws.ts b/packages/opencode/src/mcp/ws.ts new file mode 100644 index 000000000000..baf4acd51342 --- /dev/null +++ b/packages/opencode/src/mcp/ws.ts @@ -0,0 +1,64 @@ +import { WebSocketClientTransport as BaseWebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js" +import { McpError } from "@modelcontextprotocol/sdk/types.js" + +export { McpError } + +export interface WebSocketTransportOptions { + headers?: Record +} + +/** + * Extended WebSocket transport that supports custom headers. + * Bun's WebSocket implementation accepts headers in the constructor options. + * + * We override start() because the base class inlines WebSocket construction + * with no hook to customize it. + */ +export class WebSocketClientTransport extends BaseWebSocketClientTransport { + private headers: Record + + constructor(url: URL, options?: WebSocketTransportOptions) { + super(url) + this.headers = options?.headers ?? {} + } + + override start(): Promise { + // @ts-expect-error accessing private field + if (this._socket) { + throw new Error( + "WebSocketClientTransport already started! If using Client class, note that connect() calls start() automatically.", + ) + } + + // Inject our WebSocket with headers before calling super.start() + // @ts-expect-error accessing private field + this._socket = new WebSocket(this._url, { headers: this.headers } as any) + + // Now delegate to base class - it will see _socket exists and wire up events + // Unfortunately base class checks _socket at the start and throws, so we + // must duplicate the event wiring logic + return new Promise((resolve, reject) => { + // @ts-expect-error accessing private field + const socket = this._socket as WebSocket + + socket.onerror = (event) => { + const error = "error" in event ? (event as any).error : new Error(`WebSocket error: ${JSON.stringify(event)}`) + reject(error) + this.onerror?.(error) + } + + socket.onopen = () => resolve() + socket.onclose = () => this.onclose?.() + socket.onmessage = (event) => { + try { + this.onmessage?.(JSON.parse(event.data as string)) + } catch (error) { + this.onerror?.(error as Error) + } + } + }) + } + + // No-op to match HTTP transport interface (WebSocket doesn't support OAuth) + async finishAuth(_authorizationCode: string): Promise {} +} diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 32dbd5a03702..faa37c0a6608 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -57,6 +57,7 @@ export namespace Permission { info: Info resolve: () => void reject: (e: any) => void + onRespond?: (response: Response) => void } } } = {} @@ -93,6 +94,8 @@ export namespace Permission { sessionID: Info["sessionID"] messageID: Info["messageID"] metadata: Info["metadata"] + onSetup?: (info: Info) => void + onRespond?: (response: Response) => void }) { const { pending, approved } = state() log.info("asking", { @@ -135,8 +138,10 @@ export namespace Permission { info, resolve, reject, + onRespond: input.onRespond, } Bus.publish(Event.Updated, info) + input.onSetup?.(info) }) } @@ -154,6 +159,7 @@ export namespace Permission { permissionID: input.permissionID, response: input.response, }) + match.onRespond?.(input.response) if (input.response === "reject") { match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID, match.info.metadata)) return diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index f98670be3a79..517934f86131 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -36,6 +36,7 @@ import { lazy } from "../util/lazy" import { Todo } from "../session/todo" import { InstanceBootstrap } from "../project/bootstrap" import { MCP } from "../mcp" +import { Ide } from "../ide" import { Storage } from "../storage/storage" import type { ContentfulStatusCode } from "hono/utils/http-status" import { TuiEvent } from "@/cli/cmd/tui/event" @@ -2039,6 +2040,70 @@ export namespace Server { return c.json(true) }, ) + .get( + "/ide", + describeRoute({ + summary: "Get IDE status", + description: "Get the status of all IDE instances.", + operationId: "ide.status", + responses: { + 200: { + description: "IDE instance status", + content: { + "application/json": { + schema: resolver(z.record(z.string(), Ide.Status)), + }, + }, + }, + }, + }), + async (c) => { + return c.json(await Ide.status()) + }, + ) + .post( + "/ide/:name/connect", + describeRoute({ + description: "Connect to an IDE instance", + operationId: "ide.connect", + responses: { + 200: { + description: "IDE connected successfully", + content: { + "application/json": { + schema: resolver(z.record(z.string(), Ide.Status)), + }, + }, + }, + }, + }), + validator("param", z.object({ name: z.string() })), + async (c) => { + const { name } = c.req.valid("param") + return c.json(await Ide.connect(name)) + }, + ) + .post( + "/ide/:name/disconnect", + describeRoute({ + description: "Disconnect from an IDE instance", + operationId: "ide.disconnect", + responses: { + 200: { + description: "IDE disconnected successfully", + content: { + "application/json": { + schema: resolver(z.record(z.string(), Ide.Status)), + }, + }, + }, + }, + }), + validator("param", z.object({ name: z.string() })), + async (c) => { + return c.json(await Ide.disconnect()) + }, + ) .get( "/lsp", describeRoute({ diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index f1f7dd0964f4..01d8f44d2bb5 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -213,12 +213,17 @@ export namespace SessionProcessor { case "tool-error": { const match = toolcalls[value.toolCallId] if (match && match.state.status === "running") { + const err = value.error as any await Session.updatePart({ ...match, state: { status: "error", input: value.input, - error: (value.error as any).toString(), + error: [ + err?.code && `[${err.code}]`, + err?.toString(), + err?.data, + ].filter(Boolean).join(" "), metadata: value.error instanceof Permission.RejectedError ? value.error.metadata : undefined, time: { start: match.state.time.start, diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index a5d34c949ff1..84e5994bc355 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -17,6 +17,7 @@ import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" import { Agent } from "../agent/agent" import { Snapshot } from "@/snapshot" +import { Ide } from "../ide" function normalizeLineEndings(text: string): string { return text.replaceAll("\r\n", "\n") @@ -85,9 +86,19 @@ export const EditTool = Tool.define("edit", { messageID: ctx.messageID, callID: ctx.callID, title: "Edit this file: " + filePath, - metadata: { - filePath, - diff, + metadata: { filePath, diff }, + onSetup: (info) => { + if (!Ide.active()) return + Ide.openDiff(filePath, contentNew).then((response) => { + Permission.respond({ + sessionID: info.sessionID, + permissionID: info.id, + response, + }) + }) + }, + onRespond: () => { + Ide.closeTab(filePath).catch(() => {}) }, }) } @@ -116,9 +127,19 @@ export const EditTool = Tool.define("edit", { messageID: ctx.messageID, callID: ctx.callID, title: "Edit this file: " + filePath, - metadata: { - filePath, - diff, + metadata: { filePath, diff }, + onSetup: (info) => { + if (!Ide.active()) return + Ide.openDiff(filePath, contentNew).then((response) => { + Permission.respond({ + sessionID: info.sessionID, + permissionID: info.id, + response, + }) + }) + }, + onRespond: () => { + Ide.closeTab(filePath).catch(() => {}) }, }) } diff --git a/packages/sdk/js/openapi.json b/packages/sdk/js/openapi.json index 456f221bce1b..53ab92ca9bb8 100644 --- a/packages/sdk/js/openapi.json +++ b/packages/sdk/js/openapi.json @@ -329,7 +329,10 @@ "type": "number" } }, - "required": ["rows", "cols"] + "required": [ + "rows", + "cols" + ] } } } @@ -907,7 +910,9 @@ ], "summary": "Get session", "description": "Retrieve detailed information about a specific OpenCode session.", - "tags": ["Session"], + "tags": [ + "Session" + ], "responses": { "200": { "description": "Get session", @@ -1105,7 +1110,9 @@ } ], "summary": "Get session children", - "tags": ["Session"], + "tags": [ + "Session" + ], "description": "Retrieve all child sessions that were forked from the specified parent session.", "responses": { "200": { @@ -1288,7 +1295,11 @@ "pattern": "^msg.*" } }, - "required": ["modelID", "providerID", "messageID"] + "required": [ + "modelID", + "providerID", + "messageID" + ] } } } @@ -1686,7 +1697,10 @@ "type": "string" } }, - "required": ["providerID", "modelID"] + "required": [ + "providerID", + "modelID" + ] } } } @@ -1749,7 +1763,10 @@ } } }, - "required": ["info", "parts"] + "required": [ + "info", + "parts" + ] } } } @@ -1823,7 +1840,10 @@ } } }, - "required": ["info", "parts"] + "required": [ + "info", + "parts" + ] } } } @@ -1869,7 +1889,10 @@ "type": "string" } }, - "required": ["providerID", "modelID"] + "required": [ + "providerID", + "modelID" + ] }, "agent": { "type": "string" @@ -1909,7 +1932,9 @@ } } }, - "required": ["parts"] + "required": [ + "parts" + ] } } } @@ -1972,7 +1997,10 @@ } } }, - "required": ["info", "parts"] + "required": [ + "info", + "parts" + ] } } } @@ -2074,7 +2102,10 @@ "type": "string" } }, - "required": ["providerID", "modelID"] + "required": [ + "providerID", + "modelID" + ] }, "agent": { "type": "string" @@ -2114,7 +2145,9 @@ } } }, - "required": ["parts"] + "required": [ + "parts" + ] } } } @@ -2168,7 +2201,10 @@ } } }, - "required": ["info", "parts"] + "required": [ + "info", + "parts" + ] } } } @@ -2217,7 +2253,10 @@ "type": "string" } }, - "required": ["arguments", "command"] + "required": [ + "arguments", + "command" + ] } } } @@ -2304,13 +2343,19 @@ "type": "string" } }, - "required": ["providerID", "modelID"] + "required": [ + "providerID", + "modelID" + ] }, "command": { "type": "string" } }, - "required": ["agent", "command"] + "required": [ + "agent", + "command" + ] } } } @@ -2392,7 +2437,9 @@ "pattern": "^prt.*" } }, - "required": ["messageID"] + "required": [ + "messageID" + ] } } } @@ -2537,10 +2584,16 @@ "properties": { "response": { "type": "string", - "enum": ["once", "always", "reject"] + "enum": [ + "once", + "always", + "reject" + ] } }, - "required": ["response"] + "required": [ + "response" + ] } } } @@ -2628,7 +2681,10 @@ } } }, - "required": ["providers", "default"] + "required": [ + "providers", + "default" + ] } } } @@ -2747,10 +2803,16 @@ "type": "number" } }, - "required": ["input", "output"] + "required": [ + "input", + "output" + ] } }, - "required": ["input", "output"] + "required": [ + "input", + "output" + ] }, "limit": { "type": "object", @@ -2762,7 +2824,10 @@ "type": "number" } }, - "required": ["context", "output"] + "required": [ + "context", + "output" + ] }, "modalities": { "type": "object", @@ -2771,25 +2836,44 @@ "type": "array", "items": { "type": "string", - "enum": ["text", "audio", "image", "video", "pdf"] + "enum": [ + "text", + "audio", + "image", + "video", + "pdf" + ] } }, "output": { "type": "array", "items": { "type": "string", - "enum": ["text", "audio", "image", "video", "pdf"] + "enum": [ + "text", + "audio", + "image", + "video", + "pdf" + ] } } }, - "required": ["input", "output"] + "required": [ + "input", + "output" + ] }, "experimental": { "type": "boolean" }, "status": { "type": "string", - "enum": ["alpha", "beta", "deprecated"] + "enum": [ + "alpha", + "beta", + "deprecated" + ] }, "options": { "type": "object", @@ -2814,7 +2898,9 @@ "type": "string" } }, - "required": ["npm"] + "required": [ + "npm" + ] } }, "required": [ @@ -2831,7 +2917,12 @@ } } }, - "required": ["name", "env", "id", "models"] + "required": [ + "name", + "env", + "id", + "models" + ] } }, "default": { @@ -2850,7 +2941,11 @@ } } }, - "required": ["all", "default", "connected"] + "required": [ + "all", + "default", + "connected" + ] } } } @@ -2963,7 +3058,9 @@ "type": "number" } }, - "required": ["method"] + "required": [ + "method" + ] } } } @@ -3036,7 +3133,9 @@ "type": "string" } }, - "required": ["method"] + "required": [ + "method" + ] } } } @@ -3088,7 +3187,9 @@ "type": "string" } }, - "required": ["text"] + "required": [ + "text" + ] }, "lines": { "type": "object", @@ -3097,7 +3198,9 @@ "type": "string" } }, - "required": ["text"] + "required": [ + "text" + ] }, "line_number": { "type": "number" @@ -3117,7 +3220,9 @@ "type": "string" } }, - "required": ["text"] + "required": [ + "text" + ] }, "start": { "type": "number" @@ -3126,11 +3231,21 @@ "type": "number" } }, - "required": ["match", "start", "end"] + "required": [ + "match", + "start", + "end" + ] } } }, - "required": ["path", "lines", "line_number", "absolute_offset", "submatches"] + "required": [ + "path", + "lines", + "line_number", + "absolute_offset", + "submatches" + ] } } } @@ -3169,7 +3284,10 @@ "name": "dirs", "schema": { "type": "string", - "enum": ["true", "false"] + "enum": [ + "true", + "false" + ] } } ], @@ -3416,7 +3534,12 @@ "level": { "description": "Log level", "type": "string", - "enum": ["debug", "info", "error", "warn"] + "enum": [ + "debug", + "info", + "error", + "warn" + ] }, "message": { "description": "Log message", @@ -3431,7 +3554,11 @@ "additionalProperties": {} } }, - "required": ["service", "level", "message"] + "required": [ + "service", + "level", + "message" + ] } } } @@ -3581,7 +3708,10 @@ ] } }, - "required": ["name", "config"] + "required": [ + "name", + "config" + ] } } } @@ -3629,7 +3759,9 @@ "type": "string" } }, - "required": ["authorizationUrl"] + "required": [ + "authorizationUrl" + ] } } } @@ -3696,7 +3828,9 @@ "const": true } }, - "required": ["success"] + "required": [ + "success" + ] } } } @@ -3785,7 +3919,9 @@ "type": "string" } }, - "required": ["code"] + "required": [ + "code" + ] } } } @@ -3942,6 +4078,140 @@ ] } }, + "/ide": { + "get": { + "operationId": "ide.status", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Get IDE status", + "description": "Get the status of all IDE instances.", + "responses": { + "200": { + "description": "IDE instance status", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/components/schemas/IdeStatus" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.ide.status({\n ...\n})" + } + ] + } + }, + "/ide/{name}/connect": { + "post": { + "operationId": "ide.connect", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "name", + "schema": { + "type": "string" + }, + "required": true + } + ], + "description": "Connect to an IDE instance", + "responses": { + "200": { + "description": "IDE connected successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/components/schemas/IdeStatus" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.ide.connect({\n ...\n})" + } + ] + } + }, + "/ide/{name}/disconnect": { + "post": { + "operationId": "ide.disconnect", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "name", + "schema": { + "type": "string" + }, + "required": true + } + ], + "description": "Disconnect from an IDE instance", + "responses": { + "200": { + "description": "IDE disconnected successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/components/schemas/IdeStatus" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.ide.disconnect({\n ...\n})" + } + ] + } + }, "/lsp": { "get": { "operationId": "lsp.status", @@ -4062,7 +4332,9 @@ "type": "string" } }, - "required": ["text"] + "required": [ + "text" + ] } } } @@ -4325,7 +4597,9 @@ "type": "string" } }, - "required": ["command"] + "required": [ + "command" + ] } } } @@ -4378,7 +4652,12 @@ }, "variant": { "type": "string", - "enum": ["info", "success", "warning", "error"] + "enum": [ + "info", + "success", + "warning", + "error" + ] }, "duration": { "description": "Duration in milliseconds", @@ -4386,7 +4665,10 @@ "type": "number" } }, - "required": ["message", "variant"] + "required": [ + "message", + "variant" + ] } } } @@ -4489,7 +4771,10 @@ }, "body": {} }, - "required": ["path", "body"] + "required": [ + "path", + "body" + ] } } } @@ -4656,10 +4941,15 @@ "type": "string" } }, - "required": ["directory"] + "required": [ + "directory" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.installation.updated": { "type": "object", @@ -4675,10 +4965,15 @@ "type": "string" } }, - "required": ["version"] + "required": [ + "version" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.installation.update-available": { "type": "object", @@ -4694,10 +4989,15 @@ "type": "string" } }, - "required": ["version"] + "required": [ + "version" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.lsp.client.diagnostics": { "type": "object", @@ -4716,10 +5016,16 @@ "type": "string" } }, - "required": ["serverID", "path"] + "required": [ + "serverID", + "path" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.lsp.updated": { "type": "object", @@ -4733,7 +5039,10 @@ "properties": {} } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "FileDiff": { "type": "object", @@ -4754,7 +5063,13 @@ "type": "number" } }, - "required": ["file", "before", "after", "additions", "deletions"] + "required": [ + "file", + "before", + "after", + "additions", + "deletions" + ] }, "UserMessage": { "type": "object", @@ -4776,7 +5091,9 @@ "type": "number" } }, - "required": ["created"] + "required": [ + "created" + ] }, "summary": { "type": "object", @@ -4794,7 +5111,9 @@ } } }, - "required": ["diffs"] + "required": [ + "diffs" + ] }, "agent": { "type": "string" @@ -4809,7 +5128,10 @@ "type": "string" } }, - "required": ["providerID", "modelID"] + "required": [ + "providerID", + "modelID" + ] }, "system": { "type": "string" @@ -4824,7 +5146,14 @@ } } }, - "required": ["id", "sessionID", "role", "time", "agent", "model"] + "required": [ + "id", + "sessionID", + "role", + "time", + "agent", + "model" + ] }, "ProviderAuthError": { "type": "object", @@ -4843,10 +5172,16 @@ "type": "string" } }, - "required": ["providerID", "message"] + "required": [ + "providerID", + "message" + ] } }, - "required": ["name", "data"] + "required": [ + "name", + "data" + ] }, "UnknownError": { "type": "object", @@ -4862,10 +5197,15 @@ "type": "string" } }, - "required": ["message"] + "required": [ + "message" + ] } }, - "required": ["name", "data"] + "required": [ + "name", + "data" + ] }, "MessageOutputLengthError": { "type": "object", @@ -4879,7 +5219,10 @@ "properties": {} } }, - "required": ["name", "data"] + "required": [ + "name", + "data" + ] }, "MessageAbortedError": { "type": "object", @@ -4895,10 +5238,15 @@ "type": "string" } }, - "required": ["message"] + "required": [ + "message" + ] } }, - "required": ["name", "data"] + "required": [ + "name", + "data" + ] }, "APIError": { "type": "object", @@ -4932,10 +5280,16 @@ "type": "string" } }, - "required": ["message", "isRetryable"] + "required": [ + "message", + "isRetryable" + ] } }, - "required": ["name", "data"] + "required": [ + "name", + "data" + ] }, "AssistantMessage": { "type": "object", @@ -4960,7 +5314,9 @@ "type": "number" } }, - "required": ["created"] + "required": [ + "created" + ] }, "error": { "anyOf": [ @@ -5003,7 +5359,10 @@ "type": "string" } }, - "required": ["cwd", "root"] + "required": [ + "cwd", + "root" + ] }, "summary": { "type": "boolean" @@ -5033,10 +5392,18 @@ "type": "number" } }, - "required": ["read", "write"] + "required": [ + "read", + "write" + ] } }, - "required": ["input", "output", "reasoning", "cache"] + "required": [ + "input", + "output", + "reasoning", + "cache" + ] }, "finish": { "type": "string" @@ -5080,10 +5447,15 @@ "$ref": "#/components/schemas/Message" } }, - "required": ["info"] + "required": [ + "info" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.message.removed": { "type": "object", @@ -5102,10 +5474,16 @@ "type": "string" } }, - "required": ["sessionID", "messageID"] + "required": [ + "sessionID", + "messageID" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "TextPart": { "type": "object", @@ -5142,7 +5520,9 @@ "type": "number" } }, - "required": ["start"] + "required": [ + "start" + ] }, "metadata": { "type": "object", @@ -5152,7 +5532,13 @@ "additionalProperties": {} } }, - "required": ["id", "sessionID", "messageID", "type", "text"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "text" + ] }, "ReasoningPart": { "type": "object", @@ -5190,10 +5576,19 @@ "type": "number" } }, - "required": ["start"] + "required": [ + "start" + ] } }, - "required": ["id", "sessionID", "messageID", "type", "text", "time"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "text", + "time" + ] }, "FilePartSourceText": { "type": "object", @@ -5212,7 +5607,11 @@ "maximum": 9007199254740991 } }, - "required": ["value", "start", "end"] + "required": [ + "value", + "start", + "end" + ] }, "FileSource": { "type": "object", @@ -5228,7 +5627,11 @@ "type": "string" } }, - "required": ["text", "type", "path"] + "required": [ + "text", + "type", + "path" + ] }, "Range": { "type": "object", @@ -5243,7 +5646,10 @@ "type": "number" } }, - "required": ["line", "character"] + "required": [ + "line", + "character" + ] }, "end": { "type": "object", @@ -5255,10 +5661,16 @@ "type": "number" } }, - "required": ["line", "character"] + "required": [ + "line", + "character" + ] } }, - "required": ["start", "end"] + "required": [ + "start", + "end" + ] }, "SymbolSource": { "type": "object", @@ -5285,7 +5697,14 @@ "maximum": 9007199254740991 } }, - "required": ["text", "type", "path", "range", "name", "kind"] + "required": [ + "text", + "type", + "path", + "range", + "name", + "kind" + ] }, "FilePartSource": { "anyOf": [ @@ -5326,7 +5745,14 @@ "$ref": "#/components/schemas/FilePartSource" } }, - "required": ["id", "sessionID", "messageID", "type", "mime", "url"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "mime", + "url" + ] }, "ToolStatePending": { "type": "object", @@ -5346,7 +5772,11 @@ "type": "string" } }, - "required": ["status", "input", "raw"] + "required": [ + "status", + "input", + "raw" + ] }, "ToolStateRunning": { "type": "object", @@ -5379,10 +5809,16 @@ "type": "number" } }, - "required": ["start"] + "required": [ + "start" + ] } }, - "required": ["status", "input", "time"] + "required": [ + "status", + "input", + "time" + ] }, "ToolStateCompleted": { "type": "object", @@ -5424,7 +5860,10 @@ "type": "number" } }, - "required": ["start", "end"] + "required": [ + "start", + "end" + ] }, "attachments": { "type": "array", @@ -5433,7 +5872,14 @@ } } }, - "required": ["status", "input", "output", "title", "metadata", "time"] + "required": [ + "status", + "input", + "output", + "title", + "metadata", + "time" + ] }, "ToolStateError": { "type": "object", @@ -5469,10 +5915,18 @@ "type": "number" } }, - "required": ["start", "end"] + "required": [ + "start", + "end" + ] } }, - "required": ["status", "input", "error", "time"] + "required": [ + "status", + "input", + "error", + "time" + ] }, "ToolState": { "anyOf": [ @@ -5523,7 +5977,15 @@ "additionalProperties": {} } }, - "required": ["id", "sessionID", "messageID", "type", "callID", "tool", "state"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "callID", + "tool", + "state" + ] }, "StepStartPart": { "type": "object", @@ -5545,7 +6007,12 @@ "type": "string" } }, - "required": ["id", "sessionID", "messageID", "type"] + "required": [ + "id", + "sessionID", + "messageID", + "type" + ] }, "StepFinishPart": { "type": "object", @@ -5594,18 +6061,34 @@ "type": "number" } }, - "required": ["read", "write"] + "required": [ + "read", + "write" + ] } }, - "required": ["input", "output", "reasoning", "cache"] + "required": [ + "input", + "output", + "reasoning", + "cache" + ] } }, - "required": ["id", "sessionID", "messageID", "type", "reason", "cost", "tokens"] - }, - "SnapshotPart": { - "type": "object", - "properties": { - "id": { + "required": [ + "id", + "sessionID", + "messageID", + "type", + "reason", + "cost", + "tokens" + ] + }, + "SnapshotPart": { + "type": "object", + "properties": { + "id": { "type": "string" }, "sessionID": { @@ -5622,7 +6105,13 @@ "type": "string" } }, - "required": ["id", "sessionID", "messageID", "type", "snapshot"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "snapshot" + ] }, "PatchPart": { "type": "object", @@ -5650,7 +6139,14 @@ } } }, - "required": ["id", "sessionID", "messageID", "type", "hash", "files"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "hash", + "files" + ] }, "AgentPart": { "type": "object", @@ -5688,10 +6184,20 @@ "maximum": 9007199254740991 } }, - "required": ["value", "start", "end"] + "required": [ + "value", + "start", + "end" + ] } }, - "required": ["id", "sessionID", "messageID", "type", "name"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "name" + ] }, "RetryPart": { "type": "object", @@ -5722,10 +6228,20 @@ "type": "number" } }, - "required": ["created"] + "required": [ + "created" + ] } }, - "required": ["id", "sessionID", "messageID", "type", "attempt", "error", "time"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "attempt", + "error", + "time" + ] }, "CompactionPart": { "type": "object", @@ -5747,7 +6263,13 @@ "type": "boolean" } }, - "required": ["id", "sessionID", "messageID", "type", "auto"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "auto" + ] }, "Part": { "anyOf": [ @@ -5780,7 +6302,15 @@ "type": "string" } }, - "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "prompt", + "description", + "agent" + ] }, { "$ref": "#/components/schemas/ReasoningPart" @@ -5831,10 +6361,15 @@ "type": "string" } }, - "required": ["part"] + "required": [ + "part" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.message.part.removed": { "type": "object", @@ -5856,10 +6391,17 @@ "type": "string" } }, - "required": ["sessionID", "messageID", "partID"] + "required": [ + "sessionID", + "messageID", + "partID" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Permission": { "type": "object", @@ -5909,10 +6451,20 @@ "type": "number" } }, - "required": ["created"] + "required": [ + "created" + ] } }, - "required": ["id", "type", "sessionID", "messageID", "title", "metadata", "time"] + "required": [ + "id", + "type", + "sessionID", + "messageID", + "title", + "metadata", + "time" + ] }, "Event.permission.updated": { "type": "object", @@ -5925,7 +6477,10 @@ "$ref": "#/components/schemas/Permission" } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.permission.replied": { "type": "object", @@ -5947,10 +6502,17 @@ "type": "string" } }, - "required": ["sessionID", "permissionID", "response"] + "required": [ + "sessionID", + "permissionID", + "response" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "SessionStatus": { "anyOf": [ @@ -5962,7 +6524,9 @@ "const": "idle" } }, - "required": ["type"] + "required": [ + "type" + ] }, { "type": "object", @@ -5981,7 +6545,12 @@ "type": "number" } }, - "required": ["type", "attempt", "message", "next"] + "required": [ + "type", + "attempt", + "message", + "next" + ] }, { "type": "object", @@ -5991,7 +6560,9 @@ "const": "busy" } }, - "required": ["type"] + "required": [ + "type" + ] } ] }, @@ -6012,10 +6583,16 @@ "$ref": "#/components/schemas/SessionStatus" } }, - "required": ["sessionID", "status"] + "required": [ + "sessionID", + "status" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.session.idle": { "type": "object", @@ -6031,10 +6608,15 @@ "type": "string" } }, - "required": ["sessionID"] + "required": [ + "sessionID" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.session.compacted": { "type": "object", @@ -6050,10 +6632,15 @@ "type": "string" } }, - "required": ["sessionID"] + "required": [ + "sessionID" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.file.edited": { "type": "object", @@ -6069,10 +6656,126 @@ "type": "string" } }, - "required": ["file"] + "required": [ + "file" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] + }, + "Event.ide.installed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "ide.installed" + }, + "properties": { + "type": "object", + "properties": { + "ide": { + "type": "string" + } + }, + "required": [ + "ide" + ] + } + }, + "required": [ + "type", + "properties" + ] + }, + "IdeSelection": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "fileUrl": { + "type": "string" + }, + "selection": { + "type": "object", + "properties": { + "start": { + "type": "object", + "properties": { + "line": { + "type": "number" + }, + "character": { + "type": "number" + } + }, + "required": [ + "line", + "character" + ] + }, + "end": { + "type": "object", + "properties": { + "line": { + "type": "number" + }, + "character": { + "type": "number" + } + }, + "required": [ + "line", + "character" + ] + }, + "isEmpty": { + "type": "boolean" + } + }, + "required": [ + "start", + "end", + "isEmpty" + ] + } + }, + "required": [ + "text", + "filePath", + "fileUrl", + "selection" + ] + }, + "Event.ide.selection.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "ide.selection.updated" + }, + "properties": { + "type": "object", + "properties": { + "selection": { + "$ref": "#/components/schemas/IdeSelection" + } + }, + "required": [ + "selection" + ] + } + }, + "required": [ + "type", + "properties" + ] }, "Todo": { "type": "object", @@ -6094,7 +6797,12 @@ "type": "string" } }, - "required": ["content", "status", "priority", "id"] + "required": [ + "content", + "status", + "priority", + "id" + ] }, "Event.todo.updated": { "type": "object", @@ -6116,10 +6824,16 @@ } } }, - "required": ["sessionID", "todos"] + "required": [ + "sessionID", + "todos" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.command.executed": { "type": "object", @@ -6146,10 +6860,18 @@ "pattern": "^msg.*" } }, - "required": ["name", "sessionID", "arguments", "messageID"] + "required": [ + "name", + "sessionID", + "arguments", + "messageID" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Session": { "type": "object", @@ -6187,7 +6909,11 @@ } } }, - "required": ["additions", "deletions", "files"] + "required": [ + "additions", + "deletions", + "files" + ] }, "share": { "type": "object", @@ -6196,7 +6922,9 @@ "type": "string" } }, - "required": ["url"] + "required": [ + "url" + ] }, "title": { "type": "string" @@ -6217,7 +6945,10 @@ "type": "number" } }, - "required": ["created", "updated"] + "required": [ + "created", + "updated" + ] }, "revert": { "type": "object", @@ -6235,10 +6966,19 @@ "type": "string" } }, - "required": ["messageID"] + "required": [ + "messageID" + ] } }, - "required": ["id", "projectID", "directory", "title", "version", "time"] + "required": [ + "id", + "projectID", + "directory", + "title", + "version", + "time" + ] }, "Event.session.created": { "type": "object", @@ -6254,10 +6994,15 @@ "$ref": "#/components/schemas/Session" } }, - "required": ["info"] + "required": [ + "info" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.session.updated": { "type": "object", @@ -6273,10 +7018,15 @@ "$ref": "#/components/schemas/Session" } }, - "required": ["info"] + "required": [ + "info" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.session.deleted": { "type": "object", @@ -6292,10 +7042,15 @@ "$ref": "#/components/schemas/Session" } }, - "required": ["info"] + "required": [ + "info" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.session.diff": { "type": "object", @@ -6317,10 +7072,16 @@ } } }, - "required": ["sessionID", "diff"] + "required": [ + "sessionID", + "diff" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.session.error": { "type": "object", @@ -6357,7 +7118,10 @@ } } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.file.watcher.updated": { "type": "object", @@ -6389,10 +7153,16 @@ ] } }, - "required": ["file", "event"] + "required": [ + "file", + "event" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.vcs.branch.updated": { "type": "object", @@ -6410,7 +7180,10 @@ } } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.tui.prompt.append": { "type": "object", @@ -6426,10 +7199,15 @@ "type": "string" } }, - "required": ["text"] + "required": [ + "text" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.tui.command.execute": { "type": "object", @@ -6468,10 +7246,15 @@ ] } }, - "required": ["command"] + "required": [ + "command" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.tui.toast.show": { "type": "object", @@ -6491,7 +7274,12 @@ }, "variant": { "type": "string", - "enum": ["info", "success", "warning", "error"] + "enum": [ + "info", + "success", + "warning", + "error" + ] }, "duration": { "description": "Duration in milliseconds", @@ -6499,10 +7287,16 @@ "type": "number" } }, - "required": ["message", "variant"] + "required": [ + "message", + "variant" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Pty": { "type": "object", @@ -6528,13 +7322,24 @@ }, "status": { "type": "string", - "enum": ["running", "exited"] + "enum": [ + "running", + "exited" + ] }, "pid": { "type": "number" } }, - "required": ["id", "title", "command", "args", "cwd", "status", "pid"] + "required": [ + "id", + "title", + "command", + "args", + "cwd", + "status", + "pid" + ] }, "Event.pty.created": { "type": "object", @@ -6550,10 +7355,15 @@ "$ref": "#/components/schemas/Pty" } }, - "required": ["info"] + "required": [ + "info" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.pty.updated": { "type": "object", @@ -6569,10 +7379,15 @@ "$ref": "#/components/schemas/Pty" } }, - "required": ["info"] + "required": [ + "info" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.pty.exited": { "type": "object", @@ -6592,10 +7407,16 @@ "type": "number" } }, - "required": ["id", "exitCode"] + "required": [ + "id", + "exitCode" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.pty.deleted": { "type": "object", @@ -6612,10 +7433,15 @@ "pattern": "^pty.*" } }, - "required": ["id"] + "required": [ + "id" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.server.connected": { "type": "object", @@ -6629,7 +7455,10 @@ "properties": {} } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event": { "anyOf": [ @@ -6678,6 +7507,12 @@ { "$ref": "#/components/schemas/Event.file.edited" }, + { + "$ref": "#/components/schemas/Event.ide.installed" + }, + { + "$ref": "#/components/schemas/Event.ide.selection.updated" + }, { "$ref": "#/components/schemas/Event.todo.updated" }, @@ -6741,7 +7576,10 @@ "$ref": "#/components/schemas/Event" } }, - "required": ["directory", "payload"] + "required": [ + "directory", + "payload" + ] }, "Project": { "type": "object", @@ -6769,10 +7607,16 @@ "type": "number" } }, - "required": ["created"] + "required": [ + "created" + ] } }, - "required": ["id", "worktree", "time"] + "required": [ + "id", + "worktree", + "time" + ] }, "BadRequestError": { "type": "object", @@ -6793,7 +7637,11 @@ "const": false } }, - "required": ["data", "errors", "success"] + "required": [ + "data", + "errors", + "success" + ] }, "NotFoundError": { "type": "object", @@ -6809,10 +7657,15 @@ "type": "string" } }, - "required": ["message"] + "required": [ + "message" + ] } }, - "required": ["name", "data"] + "required": [ + "name", + "data" + ] }, "KeybindsConfig": { "description": "Custom keybind configurations", @@ -7079,7 +7932,11 @@ }, "mode": { "type": "string", - "enum": ["subagent", "primary", "all"] + "enum": [ + "subagent", + "primary", + "all" + ] }, "color": { "description": "Hex color code for the agent (e.g., #FF5733)", @@ -7097,13 +7954,21 @@ "properties": { "edit": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "bash": { "anyOf": [ { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, { "type": "object", @@ -7112,22 +7977,38 @@ }, "additionalProperties": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] } } ] }, "webfetch": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "doom_loop": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "external_directory": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] } } } @@ -7215,10 +8096,16 @@ "type": "number" } }, - "required": ["input", "output"] + "required": [ + "input", + "output" + ] } }, - "required": ["input", "output"] + "required": [ + "input", + "output" + ] }, "limit": { "type": "object", @@ -7230,7 +8117,10 @@ "type": "number" } }, - "required": ["context", "output"] + "required": [ + "context", + "output" + ] }, "modalities": { "type": "object", @@ -7239,25 +8129,44 @@ "type": "array", "items": { "type": "string", - "enum": ["text", "audio", "image", "video", "pdf"] + "enum": [ + "text", + "audio", + "image", + "video", + "pdf" + ] } }, "output": { "type": "array", "items": { "type": "string", - "enum": ["text", "audio", "image", "video", "pdf"] + "enum": [ + "text", + "audio", + "image", + "video", + "pdf" + ] } } }, - "required": ["input", "output"] + "required": [ + "input", + "output" + ] }, "experimental": { "type": "boolean" }, "status": { "type": "string", - "enum": ["alpha", "beta", "deprecated"] + "enum": [ + "alpha", + "beta", + "deprecated" + ] }, "options": { "type": "object", @@ -7282,7 +8191,9 @@ "type": "string" } }, - "required": ["npm"] + "required": [ + "npm" + ] } } } @@ -7374,7 +8285,10 @@ "maximum": 9007199254740991 } }, - "required": ["type", "command"], + "required": [ + "type", + "command" + ], "additionalProperties": false }, "McpOAuthConfig": { @@ -7440,13 +8354,19 @@ "maximum": 9007199254740991 } }, - "required": ["type", "url"], + "required": [ + "type", + "url" + ], "additionalProperties": false }, "LayoutConfig": { "description": "@deprecated Always uses stretch layout.", "type": "string", - "enum": ["auto", "stretch"] + "enum": [ + "auto", + "stretch" + ] }, "Config": { "type": "object", @@ -7480,12 +8400,17 @@ "type": "boolean" } }, - "required": ["enabled"] + "required": [ + "enabled" + ] }, "diff_style": { "description": "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column", "type": "string", - "enum": ["auto", "stacked"] + "enum": [ + "auto", + "stacked" + ] } } }, @@ -7514,7 +8439,9 @@ "type": "boolean" } }, - "required": ["template"] + "required": [ + "template" + ] } }, "watcher": { @@ -7540,7 +8467,11 @@ "share": { "description": "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing", "type": "string", - "enum": ["manual", "auto", "disabled"] + "enum": [ + "manual", + "auto", + "disabled" + ] }, "autoshare": { "description": "@deprecated Use 'share' field instead. Share newly created sessions automatically", @@ -7711,7 +8642,9 @@ "const": true } }, - "required": ["disabled"] + "required": [ + "disabled" + ] }, { "type": "object", @@ -7748,7 +8681,9 @@ "additionalProperties": {} } }, - "required": ["command"] + "required": [ + "command" + ] } ] } @@ -7770,13 +8705,21 @@ "properties": { "edit": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "bash": { "anyOf": [ { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, { "type": "object", @@ -7785,22 +8728,38 @@ }, "additionalProperties": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] } } ] }, "webfetch": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "doom_loop": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "external_directory": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] } } }, @@ -7854,7 +8813,9 @@ } } }, - "required": ["command"] + "required": [ + "command" + ] } } }, @@ -7879,7 +8840,9 @@ } } }, - "required": ["command"] + "required": [ + "command" + ] } } } @@ -7928,7 +8891,11 @@ }, "parameters": {} }, - "required": ["id", "description", "parameters"] + "required": [ + "id", + "description", + "parameters" + ] }, "ToolList": { "type": "array", @@ -7952,7 +8919,12 @@ "type": "string" } }, - "required": ["state", "config", "worktree", "directory"] + "required": [ + "state", + "config", + "worktree", + "directory" + ] }, "VcsInfo": { "type": "object", @@ -7961,7 +8933,9 @@ "type": "string" } }, - "required": ["branch"] + "required": [ + "branch" + ] }, "TextPartInput": { "type": "object", @@ -7992,7 +8966,9 @@ "type": "number" } }, - "required": ["start"] + "required": [ + "start" + ] }, "metadata": { "type": "object", @@ -8002,7 +8978,10 @@ "additionalProperties": {} } }, - "required": ["type", "text"] + "required": [ + "type", + "text" + ] }, "FilePartInput": { "type": "object", @@ -8027,7 +9006,11 @@ "$ref": "#/components/schemas/FilePartSource" } }, - "required": ["type", "mime", "url"] + "required": [ + "type", + "mime", + "url" + ] }, "AgentPartInput": { "type": "object", @@ -8059,10 +9042,17 @@ "maximum": 9007199254740991 } }, - "required": ["value", "start", "end"] + "required": [ + "value", + "start", + "end" + ] } }, - "required": ["type", "name"] + "required": [ + "type", + "name" + ] }, "SubtaskPartInput": { "type": "object", @@ -8084,7 +9074,12 @@ "type": "string" } }, - "required": ["type", "prompt", "description", "agent"] + "required": [ + "type", + "prompt", + "description", + "agent" + ] }, "Command": { "type": "object", @@ -8108,7 +9103,10 @@ "type": "boolean" } }, - "required": ["name", "template"] + "required": [ + "name", + "template" + ] }, "Model": { "type": "object", @@ -8132,7 +9130,11 @@ "type": "string" } }, - "required": ["id", "url", "npm"] + "required": [ + "id", + "url", + "npm" + ] }, "name": { "type": "string" @@ -8171,7 +9173,13 @@ "type": "boolean" } }, - "required": ["text", "audio", "image", "video", "pdf"] + "required": [ + "text", + "audio", + "image", + "video", + "pdf" + ] }, "output": { "type": "object", @@ -8192,10 +9200,23 @@ "type": "boolean" } }, - "required": ["text", "audio", "image", "video", "pdf"] + "required": [ + "text", + "audio", + "image", + "video", + "pdf" + ] } }, - "required": ["temperature", "reasoning", "attachment", "toolcall", "input", "output"] + "required": [ + "temperature", + "reasoning", + "attachment", + "toolcall", + "input", + "output" + ] }, "cost": { "type": "object", @@ -8216,7 +9237,10 @@ "type": "number" } }, - "required": ["read", "write"] + "required": [ + "read", + "write" + ] }, "experimentalOver200K": { "type": "object", @@ -8237,13 +9261,24 @@ "type": "number" } }, - "required": ["read", "write"] + "required": [ + "read", + "write" + ] } }, - "required": ["input", "output", "cache"] + "required": [ + "input", + "output", + "cache" + ] } }, - "required": ["input", "output", "cache"] + "required": [ + "input", + "output", + "cache" + ] }, "limit": { "type": "object", @@ -8255,11 +9290,19 @@ "type": "number" } }, - "required": ["context", "output"] + "required": [ + "context", + "output" + ] }, "status": { "type": "string", - "enum": ["alpha", "beta", "deprecated", "active"] + "enum": [ + "alpha", + "beta", + "deprecated", + "active" + ] }, "options": { "type": "object", @@ -8278,7 +9321,18 @@ } } }, - "required": ["id", "providerID", "api", "name", "capabilities", "cost", "limit", "status", "options", "headers"] + "required": [ + "id", + "providerID", + "api", + "name", + "capabilities", + "cost", + "limit", + "status", + "options", + "headers" + ] }, "Provider": { "type": "object", @@ -8291,7 +9345,12 @@ }, "source": { "type": "string", - "enum": ["env", "config", "custom", "api"] + "enum": [ + "env", + "config", + "custom", + "api" + ] }, "env": { "type": "array", @@ -8319,7 +9378,14 @@ } } }, - "required": ["id", "name", "source", "env", "options", "models"] + "required": [ + "id", + "name", + "source", + "env", + "options", + "models" + ] }, "ProviderAuthMethod": { "type": "object", @@ -8340,7 +9406,10 @@ "type": "string" } }, - "required": ["type", "label"] + "required": [ + "type", + "label" + ] }, "ProviderAuthAuthorization": { "type": "object", @@ -8364,7 +9433,11 @@ "type": "string" } }, - "required": ["url", "method", "instructions"] + "required": [ + "url", + "method", + "instructions" + ] }, "Symbol": { "type": "object", @@ -8385,10 +9458,17 @@ "$ref": "#/components/schemas/Range" } }, - "required": ["uri", "range"] + "required": [ + "uri", + "range" + ] } }, - "required": ["name", "kind", "location"] + "required": [ + "name", + "kind", + "location" + ] }, "FileNode": { "type": "object", @@ -8404,13 +9484,22 @@ }, "type": { "type": "string", - "enum": ["file", "directory"] + "enum": [ + "file", + "directory" + ] }, "ignored": { "type": "boolean" } }, - "required": ["name", "path", "absolute", "type", "ignored"] + "required": [ + "name", + "path", + "absolute", + "type", + "ignored" + ] }, "FileContent": { "type": "object", @@ -8464,14 +9553,24 @@ } } }, - "required": ["oldStart", "oldLines", "newStart", "newLines", "lines"] + "required": [ + "oldStart", + "oldLines", + "newStart", + "newLines", + "lines" + ] } }, "index": { "type": "string" } }, - "required": ["oldFileName", "newFileName", "hunks"] + "required": [ + "oldFileName", + "newFileName", + "hunks" + ] }, "encoding": { "type": "string", @@ -8481,7 +9580,10 @@ "type": "string" } }, - "required": ["type", "content"] + "required": [ + "type", + "content" + ] }, "File": { "type": "object", @@ -8501,10 +9603,19 @@ }, "status": { "type": "string", - "enum": ["added", "deleted", "modified"] + "enum": [ + "added", + "deleted", + "modified" + ] } }, - "required": ["path", "added", "removed", "status"] + "required": [ + "path", + "added", + "removed", + "status" + ] }, "Agent": { "type": "object", @@ -8517,7 +9628,11 @@ }, "mode": { "type": "string", - "enum": ["subagent", "primary", "all"] + "enum": [ + "subagent", + "primary", + "all" + ] }, "builtIn": { "type": "boolean" @@ -8536,7 +9651,11 @@ "properties": { "edit": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "bash": { "type": "object", @@ -8545,23 +9664,42 @@ }, "additionalProperties": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] } }, "webfetch": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "doom_loop": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "external_directory": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] } }, - "required": ["edit", "bash"] + "required": [ + "edit", + "bash" + ] }, "model": { "type": "object", @@ -8573,7 +9711,10 @@ "type": "string" } }, - "required": ["modelID", "providerID"] + "required": [ + "modelID", + "providerID" + ] }, "prompt": { "type": "string" @@ -8600,7 +9741,14 @@ "maximum": 9007199254740991 } }, - "required": ["name", "mode", "builtIn", "permission", "tools", "options"] + "required": [ + "name", + "mode", + "builtIn", + "permission", + "tools", + "options" + ] }, "MCPStatusConnected": { "type": "object", @@ -8610,7 +9758,9 @@ "const": "connected" } }, - "required": ["status"] + "required": [ + "status" + ] }, "MCPStatusDisabled": { "type": "object", @@ -8620,7 +9770,9 @@ "const": "disabled" } }, - "required": ["status"] + "required": [ + "status" + ] }, "MCPStatusFailed": { "type": "object", @@ -8633,7 +9785,10 @@ "type": "string" } }, - "required": ["status", "error"] + "required": [ + "status", + "error" + ] }, "MCPStatusNeedsAuth": { "type": "object", @@ -8643,7 +9798,9 @@ "const": "needs_auth" } }, - "required": ["status"] + "required": [ + "status" + ] }, "MCPStatusNeedsClientRegistration": { "type": "object", @@ -8656,7 +9813,10 @@ "type": "string" } }, - "required": ["status", "error"] + "required": [ + "status", + "error" + ] }, "MCPStatus": { "anyOf": [ @@ -8677,6 +9837,35 @@ } ] }, + "IdeStatus": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "connected", + "disconnected", + "failed" + ] + }, + "name": { + "type": "string" + }, + "workspaceFolders": { + "type": "array", + "items": { + "type": "string" + } + }, + "error": { + "type": "string" + } + }, + "required": [ + "status", + "name" + ] + }, "LSPStatus": { "type": "object", "properties": { @@ -8702,7 +9891,12 @@ ] } }, - "required": ["id", "name", "root", "status"] + "required": [ + "id", + "name", + "root", + "status" + ] }, "FormatterStatus": { "type": "object", @@ -8720,7 +9914,11 @@ "type": "boolean" } }, - "required": ["name", "extensions", "enabled"] + "required": [ + "name", + "extensions", + "enabled" + ] }, "OAuth": { "type": "object", @@ -8742,7 +9940,12 @@ "type": "string" } }, - "required": ["type", "refresh", "access", "expires"] + "required": [ + "type", + "refresh", + "access", + "expires" + ] }, "ApiAuth": { "type": "object", @@ -8755,7 +9958,10 @@ "type": "string" } }, - "required": ["type", "key"] + "required": [ + "type", + "key" + ] }, "WellKnownAuth": { "type": "object", @@ -8771,7 +9977,11 @@ "type": "string" } }, - "required": ["type", "key", "token"] + "required": [ + "type", + "key", + "token" + ] }, "Auth": { "anyOf": [ @@ -8788,4 +9998,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 38f39b2a9559..77ad0b7f3263 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -29,6 +29,9 @@ import type { FindTextResponses, FormatterStatusResponses, GlobalEventResponses, + IdeConnectResponses, + IdeDisconnectResponses, + IdeStatusResponses, InstanceDisposeResponses, LspStatusResponses, McpAddErrors, @@ -2138,6 +2141,83 @@ export class Mcp extends HeyApiClient { auth = new Auth({ client: this.client }) } +export class Ide extends HeyApiClient { + /** + * Get IDE status + * + * Get the status of all IDE instances. + */ + public status( + parameters?: { + directory?: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }]) + return (options?.client ?? this.client).get({ + url: "/ide", + ...options, + ...params, + }) + } + + /** + * Connect to an IDE instance + */ + public connect( + parameters: { + name: string + directory?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/ide/{name}/connect", + ...options, + ...params, + }) + } + + /** + * Disconnect from an IDE instance + */ + public disconnect( + parameters: { + name: string + directory?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/ide/{name}/disconnect", + ...options, + ...params, + }) + } +} + export class Lsp extends HeyApiClient { /** * Get LSP status @@ -2541,6 +2621,8 @@ export class OpencodeClient extends HeyApiClient { mcp = new Mcp({ client: this.client }) + ide = new Ide({ client: this.client }) + lsp = new Lsp({ client: this.client }) formatter = new Formatter({ client: this.client }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9b80026f067b..44efdbf12a0c 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -497,6 +497,37 @@ export type EventFileEdited = { } } +export type EventIdeInstalled = { + type: "ide.installed" + properties: { + ide: string + } +} + +export type IdeSelection = { + text: string + filePath: string + fileUrl: string + selection: { + start: { + line: number + character: number + } + end: { + line: number + character: number + } + isEmpty: boolean + } +} + +export type EventIdeSelectionUpdated = { + type: "ide.selection.updated" + properties: { + selection: IdeSelection + } +} + export type Todo = { /** * Brief description of the task @@ -719,6 +750,8 @@ export type Event = | EventSessionIdle | EventSessionCompacted | EventFileEdited + | EventIdeInstalled + | EventIdeSelectionUpdated | EventTodoUpdated | EventCommandExecuted | EventSessionCreated @@ -1629,6 +1662,13 @@ export type McpStatus = | McpStatusNeedsAuth | McpStatusNeedsClientRegistration +export type IdeStatus = { + status: "connected" | "disconnected" | "failed" + name: string + workspaceFolders?: Array + error?: string +} + export type LspStatus = { id: string name: string @@ -3543,6 +3583,70 @@ export type McpDisconnectResponses = { export type McpDisconnectResponse = McpDisconnectResponses[keyof McpDisconnectResponses] +export type IdeStatusData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/ide" +} + +export type IdeStatusResponses = { + /** + * IDE instance status + */ + 200: { + [key: string]: IdeStatus + } +} + +export type IdeStatusResponse = IdeStatusResponses[keyof IdeStatusResponses] + +export type IdeConnectData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + } + url: "/ide/{name}/connect" +} + +export type IdeConnectResponses = { + /** + * IDE connected successfully + */ + 200: { + [key: string]: IdeStatus + } +} + +export type IdeConnectResponse = IdeConnectResponses[keyof IdeConnectResponses] + +export type IdeDisconnectData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + } + url: "/ide/{name}/disconnect" +} + +export type IdeDisconnectResponses = { + /** + * IDE disconnected successfully + */ + 200: { + [key: string]: IdeStatus + } +} + +export type IdeDisconnectResponse = IdeDisconnectResponses[keyof IdeDisconnectResponses] + export type LspStatusData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 456f221bce1b..478dcb03e323 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -329,7 +329,10 @@ "type": "number" } }, - "required": ["rows", "cols"] + "required": [ + "rows", + "cols" + ] } } } @@ -907,7 +910,9 @@ ], "summary": "Get session", "description": "Retrieve detailed information about a specific OpenCode session.", - "tags": ["Session"], + "tags": [ + "Session" + ], "responses": { "200": { "description": "Get session", @@ -1105,7 +1110,9 @@ } ], "summary": "Get session children", - "tags": ["Session"], + "tags": [ + "Session" + ], "description": "Retrieve all child sessions that were forked from the specified parent session.", "responses": { "200": { @@ -1288,7 +1295,11 @@ "pattern": "^msg.*" } }, - "required": ["modelID", "providerID", "messageID"] + "required": [ + "modelID", + "providerID", + "messageID" + ] } } } @@ -1686,7 +1697,10 @@ "type": "string" } }, - "required": ["providerID", "modelID"] + "required": [ + "providerID", + "modelID" + ] } } } @@ -1749,7 +1763,10 @@ } } }, - "required": ["info", "parts"] + "required": [ + "info", + "parts" + ] } } } @@ -1823,7 +1840,10 @@ } } }, - "required": ["info", "parts"] + "required": [ + "info", + "parts" + ] } } } @@ -1869,7 +1889,10 @@ "type": "string" } }, - "required": ["providerID", "modelID"] + "required": [ + "providerID", + "modelID" + ] }, "agent": { "type": "string" @@ -1909,7 +1932,9 @@ } } }, - "required": ["parts"] + "required": [ + "parts" + ] } } } @@ -1972,7 +1997,10 @@ } } }, - "required": ["info", "parts"] + "required": [ + "info", + "parts" + ] } } } @@ -2074,7 +2102,10 @@ "type": "string" } }, - "required": ["providerID", "modelID"] + "required": [ + "providerID", + "modelID" + ] }, "agent": { "type": "string" @@ -2114,7 +2145,9 @@ } } }, - "required": ["parts"] + "required": [ + "parts" + ] } } } @@ -2168,7 +2201,10 @@ } } }, - "required": ["info", "parts"] + "required": [ + "info", + "parts" + ] } } } @@ -2217,7 +2253,10 @@ "type": "string" } }, - "required": ["arguments", "command"] + "required": [ + "arguments", + "command" + ] } } } @@ -2304,13 +2343,19 @@ "type": "string" } }, - "required": ["providerID", "modelID"] + "required": [ + "providerID", + "modelID" + ] }, "command": { "type": "string" } }, - "required": ["agent", "command"] + "required": [ + "agent", + "command" + ] } } } @@ -2392,7 +2437,9 @@ "pattern": "^prt.*" } }, - "required": ["messageID"] + "required": [ + "messageID" + ] } } } @@ -2537,10 +2584,16 @@ "properties": { "response": { "type": "string", - "enum": ["once", "always", "reject"] + "enum": [ + "once", + "always", + "reject" + ] } }, - "required": ["response"] + "required": [ + "response" + ] } } } @@ -2628,7 +2681,10 @@ } } }, - "required": ["providers", "default"] + "required": [ + "providers", + "default" + ] } } } @@ -2747,10 +2803,16 @@ "type": "number" } }, - "required": ["input", "output"] + "required": [ + "input", + "output" + ] } }, - "required": ["input", "output"] + "required": [ + "input", + "output" + ] }, "limit": { "type": "object", @@ -2762,7 +2824,10 @@ "type": "number" } }, - "required": ["context", "output"] + "required": [ + "context", + "output" + ] }, "modalities": { "type": "object", @@ -2771,25 +2836,44 @@ "type": "array", "items": { "type": "string", - "enum": ["text", "audio", "image", "video", "pdf"] + "enum": [ + "text", + "audio", + "image", + "video", + "pdf" + ] } }, "output": { "type": "array", "items": { "type": "string", - "enum": ["text", "audio", "image", "video", "pdf"] + "enum": [ + "text", + "audio", + "image", + "video", + "pdf" + ] } } }, - "required": ["input", "output"] + "required": [ + "input", + "output" + ] }, "experimental": { "type": "boolean" }, "status": { "type": "string", - "enum": ["alpha", "beta", "deprecated"] + "enum": [ + "alpha", + "beta", + "deprecated" + ] }, "options": { "type": "object", @@ -2814,7 +2898,9 @@ "type": "string" } }, - "required": ["npm"] + "required": [ + "npm" + ] } }, "required": [ @@ -2831,7 +2917,12 @@ } } }, - "required": ["name", "env", "id", "models"] + "required": [ + "name", + "env", + "id", + "models" + ] } }, "default": { @@ -2850,7 +2941,11 @@ } } }, - "required": ["all", "default", "connected"] + "required": [ + "all", + "default", + "connected" + ] } } } @@ -2963,7 +3058,9 @@ "type": "number" } }, - "required": ["method"] + "required": [ + "method" + ] } } } @@ -3036,7 +3133,9 @@ "type": "string" } }, - "required": ["method"] + "required": [ + "method" + ] } } } @@ -3088,7 +3187,9 @@ "type": "string" } }, - "required": ["text"] + "required": [ + "text" + ] }, "lines": { "type": "object", @@ -3097,7 +3198,9 @@ "type": "string" } }, - "required": ["text"] + "required": [ + "text" + ] }, "line_number": { "type": "number" @@ -3117,7 +3220,9 @@ "type": "string" } }, - "required": ["text"] + "required": [ + "text" + ] }, "start": { "type": "number" @@ -3126,11 +3231,21 @@ "type": "number" } }, - "required": ["match", "start", "end"] + "required": [ + "match", + "start", + "end" + ] } } }, - "required": ["path", "lines", "line_number", "absolute_offset", "submatches"] + "required": [ + "path", + "lines", + "line_number", + "absolute_offset", + "submatches" + ] } } } @@ -3169,7 +3284,10 @@ "name": "dirs", "schema": { "type": "string", - "enum": ["true", "false"] + "enum": [ + "true", + "false" + ] } } ], @@ -3416,7 +3534,12 @@ "level": { "description": "Log level", "type": "string", - "enum": ["debug", "info", "error", "warn"] + "enum": [ + "debug", + "info", + "error", + "warn" + ] }, "message": { "description": "Log message", @@ -3431,7 +3554,11 @@ "additionalProperties": {} } }, - "required": ["service", "level", "message"] + "required": [ + "service", + "level", + "message" + ] } } } @@ -3577,11 +3704,17 @@ }, { "$ref": "#/components/schemas/McpRemoteConfig" + }, + { + "$ref": "#/components/schemas/McpWsConfig" } ] } }, - "required": ["name", "config"] + "required": [ + "name", + "config" + ] } } } @@ -3629,7 +3762,9 @@ "type": "string" } }, - "required": ["authorizationUrl"] + "required": [ + "authorizationUrl" + ] } } } @@ -3696,7 +3831,9 @@ "const": true } }, - "required": ["success"] + "required": [ + "success" + ] } } } @@ -3785,7 +3922,9 @@ "type": "string" } }, - "required": ["code"] + "required": [ + "code" + ] } } } @@ -4062,7 +4201,9 @@ "type": "string" } }, - "required": ["text"] + "required": [ + "text" + ] } } } @@ -4325,7 +4466,9 @@ "type": "string" } }, - "required": ["command"] + "required": [ + "command" + ] } } } @@ -4378,7 +4521,12 @@ }, "variant": { "type": "string", - "enum": ["info", "success", "warning", "error"] + "enum": [ + "info", + "success", + "warning", + "error" + ] }, "duration": { "description": "Duration in milliseconds", @@ -4386,7 +4534,10 @@ "type": "number" } }, - "required": ["message", "variant"] + "required": [ + "message", + "variant" + ] } } } @@ -4489,7 +4640,10 @@ }, "body": {} }, - "required": ["path", "body"] + "required": [ + "path", + "body" + ] } } } @@ -4656,10 +4810,15 @@ "type": "string" } }, - "required": ["directory"] + "required": [ + "directory" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.installation.updated": { "type": "object", @@ -4675,10 +4834,15 @@ "type": "string" } }, - "required": ["version"] + "required": [ + "version" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.installation.update-available": { "type": "object", @@ -4694,10 +4858,15 @@ "type": "string" } }, - "required": ["version"] + "required": [ + "version" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.lsp.client.diagnostics": { "type": "object", @@ -4716,10 +4885,16 @@ "type": "string" } }, - "required": ["serverID", "path"] + "required": [ + "serverID", + "path" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.lsp.updated": { "type": "object", @@ -4733,7 +4908,10 @@ "properties": {} } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "FileDiff": { "type": "object", @@ -4754,7 +4932,13 @@ "type": "number" } }, - "required": ["file", "before", "after", "additions", "deletions"] + "required": [ + "file", + "before", + "after", + "additions", + "deletions" + ] }, "UserMessage": { "type": "object", @@ -4776,7 +4960,9 @@ "type": "number" } }, - "required": ["created"] + "required": [ + "created" + ] }, "summary": { "type": "object", @@ -4794,7 +4980,9 @@ } } }, - "required": ["diffs"] + "required": [ + "diffs" + ] }, "agent": { "type": "string" @@ -4809,7 +4997,10 @@ "type": "string" } }, - "required": ["providerID", "modelID"] + "required": [ + "providerID", + "modelID" + ] }, "system": { "type": "string" @@ -4824,7 +5015,14 @@ } } }, - "required": ["id", "sessionID", "role", "time", "agent", "model"] + "required": [ + "id", + "sessionID", + "role", + "time", + "agent", + "model" + ] }, "ProviderAuthError": { "type": "object", @@ -4843,10 +5041,16 @@ "type": "string" } }, - "required": ["providerID", "message"] + "required": [ + "providerID", + "message" + ] } }, - "required": ["name", "data"] + "required": [ + "name", + "data" + ] }, "UnknownError": { "type": "object", @@ -4862,10 +5066,15 @@ "type": "string" } }, - "required": ["message"] + "required": [ + "message" + ] } }, - "required": ["name", "data"] + "required": [ + "name", + "data" + ] }, "MessageOutputLengthError": { "type": "object", @@ -4879,7 +5088,10 @@ "properties": {} } }, - "required": ["name", "data"] + "required": [ + "name", + "data" + ] }, "MessageAbortedError": { "type": "object", @@ -4895,10 +5107,15 @@ "type": "string" } }, - "required": ["message"] + "required": [ + "message" + ] } }, - "required": ["name", "data"] + "required": [ + "name", + "data" + ] }, "APIError": { "type": "object", @@ -4932,10 +5149,16 @@ "type": "string" } }, - "required": ["message", "isRetryable"] + "required": [ + "message", + "isRetryable" + ] } }, - "required": ["name", "data"] + "required": [ + "name", + "data" + ] }, "AssistantMessage": { "type": "object", @@ -4960,7 +5183,9 @@ "type": "number" } }, - "required": ["created"] + "required": [ + "created" + ] }, "error": { "anyOf": [ @@ -5003,7 +5228,10 @@ "type": "string" } }, - "required": ["cwd", "root"] + "required": [ + "cwd", + "root" + ] }, "summary": { "type": "boolean" @@ -5033,10 +5261,18 @@ "type": "number" } }, - "required": ["read", "write"] + "required": [ + "read", + "write" + ] } }, - "required": ["input", "output", "reasoning", "cache"] + "required": [ + "input", + "output", + "reasoning", + "cache" + ] }, "finish": { "type": "string" @@ -5080,10 +5316,15 @@ "$ref": "#/components/schemas/Message" } }, - "required": ["info"] + "required": [ + "info" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.message.removed": { "type": "object", @@ -5102,10 +5343,16 @@ "type": "string" } }, - "required": ["sessionID", "messageID"] + "required": [ + "sessionID", + "messageID" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "TextPart": { "type": "object", @@ -5142,7 +5389,9 @@ "type": "number" } }, - "required": ["start"] + "required": [ + "start" + ] }, "metadata": { "type": "object", @@ -5152,7 +5401,13 @@ "additionalProperties": {} } }, - "required": ["id", "sessionID", "messageID", "type", "text"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "text" + ] }, "ReasoningPart": { "type": "object", @@ -5190,10 +5445,19 @@ "type": "number" } }, - "required": ["start"] + "required": [ + "start" + ] } }, - "required": ["id", "sessionID", "messageID", "type", "text", "time"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "text", + "time" + ] }, "FilePartSourceText": { "type": "object", @@ -5212,7 +5476,11 @@ "maximum": 9007199254740991 } }, - "required": ["value", "start", "end"] + "required": [ + "value", + "start", + "end" + ] }, "FileSource": { "type": "object", @@ -5228,7 +5496,11 @@ "type": "string" } }, - "required": ["text", "type", "path"] + "required": [ + "text", + "type", + "path" + ] }, "Range": { "type": "object", @@ -5243,7 +5515,10 @@ "type": "number" } }, - "required": ["line", "character"] + "required": [ + "line", + "character" + ] }, "end": { "type": "object", @@ -5255,10 +5530,16 @@ "type": "number" } }, - "required": ["line", "character"] + "required": [ + "line", + "character" + ] } }, - "required": ["start", "end"] + "required": [ + "start", + "end" + ] }, "SymbolSource": { "type": "object", @@ -5285,7 +5566,14 @@ "maximum": 9007199254740991 } }, - "required": ["text", "type", "path", "range", "name", "kind"] + "required": [ + "text", + "type", + "path", + "range", + "name", + "kind" + ] }, "FilePartSource": { "anyOf": [ @@ -5326,7 +5614,14 @@ "$ref": "#/components/schemas/FilePartSource" } }, - "required": ["id", "sessionID", "messageID", "type", "mime", "url"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "mime", + "url" + ] }, "ToolStatePending": { "type": "object", @@ -5346,7 +5641,11 @@ "type": "string" } }, - "required": ["status", "input", "raw"] + "required": [ + "status", + "input", + "raw" + ] }, "ToolStateRunning": { "type": "object", @@ -5379,10 +5678,16 @@ "type": "number" } }, - "required": ["start"] + "required": [ + "start" + ] } }, - "required": ["status", "input", "time"] + "required": [ + "status", + "input", + "time" + ] }, "ToolStateCompleted": { "type": "object", @@ -5424,7 +5729,10 @@ "type": "number" } }, - "required": ["start", "end"] + "required": [ + "start", + "end" + ] }, "attachments": { "type": "array", @@ -5433,7 +5741,14 @@ } } }, - "required": ["status", "input", "output", "title", "metadata", "time"] + "required": [ + "status", + "input", + "output", + "title", + "metadata", + "time" + ] }, "ToolStateError": { "type": "object", @@ -5469,10 +5784,18 @@ "type": "number" } }, - "required": ["start", "end"] + "required": [ + "start", + "end" + ] } }, - "required": ["status", "input", "error", "time"] + "required": [ + "status", + "input", + "error", + "time" + ] }, "ToolState": { "anyOf": [ @@ -5523,7 +5846,15 @@ "additionalProperties": {} } }, - "required": ["id", "sessionID", "messageID", "type", "callID", "tool", "state"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "callID", + "tool", + "state" + ] }, "StepStartPart": { "type": "object", @@ -5545,7 +5876,12 @@ "type": "string" } }, - "required": ["id", "sessionID", "messageID", "type"] + "required": [ + "id", + "sessionID", + "messageID", + "type" + ] }, "StepFinishPart": { "type": "object", @@ -5594,13 +5930,29 @@ "type": "number" } }, - "required": ["read", "write"] + "required": [ + "read", + "write" + ] } }, - "required": ["input", "output", "reasoning", "cache"] + "required": [ + "input", + "output", + "reasoning", + "cache" + ] } }, - "required": ["id", "sessionID", "messageID", "type", "reason", "cost", "tokens"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "reason", + "cost", + "tokens" + ] }, "SnapshotPart": { "type": "object", @@ -5622,7 +5974,13 @@ "type": "string" } }, - "required": ["id", "sessionID", "messageID", "type", "snapshot"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "snapshot" + ] }, "PatchPart": { "type": "object", @@ -5650,7 +6008,14 @@ } } }, - "required": ["id", "sessionID", "messageID", "type", "hash", "files"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "hash", + "files" + ] }, "AgentPart": { "type": "object", @@ -5688,10 +6053,20 @@ "maximum": 9007199254740991 } }, - "required": ["value", "start", "end"] + "required": [ + "value", + "start", + "end" + ] } }, - "required": ["id", "sessionID", "messageID", "type", "name"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "name" + ] }, "RetryPart": { "type": "object", @@ -5722,10 +6097,20 @@ "type": "number" } }, - "required": ["created"] + "required": [ + "created" + ] } }, - "required": ["id", "sessionID", "messageID", "type", "attempt", "error", "time"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "attempt", + "error", + "time" + ] }, "CompactionPart": { "type": "object", @@ -5747,7 +6132,13 @@ "type": "boolean" } }, - "required": ["id", "sessionID", "messageID", "type", "auto"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "auto" + ] }, "Part": { "anyOf": [ @@ -5780,7 +6171,15 @@ "type": "string" } }, - "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"] + "required": [ + "id", + "sessionID", + "messageID", + "type", + "prompt", + "description", + "agent" + ] }, { "$ref": "#/components/schemas/ReasoningPart" @@ -5831,10 +6230,15 @@ "type": "string" } }, - "required": ["part"] + "required": [ + "part" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.message.part.removed": { "type": "object", @@ -5856,10 +6260,17 @@ "type": "string" } }, - "required": ["sessionID", "messageID", "partID"] + "required": [ + "sessionID", + "messageID", + "partID" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Permission": { "type": "object", @@ -5909,10 +6320,20 @@ "type": "number" } }, - "required": ["created"] + "required": [ + "created" + ] } }, - "required": ["id", "type", "sessionID", "messageID", "title", "metadata", "time"] + "required": [ + "id", + "type", + "sessionID", + "messageID", + "title", + "metadata", + "time" + ] }, "Event.permission.updated": { "type": "object", @@ -5925,7 +6346,10 @@ "$ref": "#/components/schemas/Permission" } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.permission.replied": { "type": "object", @@ -5947,10 +6371,17 @@ "type": "string" } }, - "required": ["sessionID", "permissionID", "response"] + "required": [ + "sessionID", + "permissionID", + "response" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "SessionStatus": { "anyOf": [ @@ -5962,7 +6393,9 @@ "const": "idle" } }, - "required": ["type"] + "required": [ + "type" + ] }, { "type": "object", @@ -5981,7 +6414,12 @@ "type": "number" } }, - "required": ["type", "attempt", "message", "next"] + "required": [ + "type", + "attempt", + "message", + "next" + ] }, { "type": "object", @@ -5991,7 +6429,9 @@ "const": "busy" } }, - "required": ["type"] + "required": [ + "type" + ] } ] }, @@ -6012,10 +6452,16 @@ "$ref": "#/components/schemas/SessionStatus" } }, - "required": ["sessionID", "status"] + "required": [ + "sessionID", + "status" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.session.idle": { "type": "object", @@ -6031,10 +6477,15 @@ "type": "string" } }, - "required": ["sessionID"] + "required": [ + "sessionID" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.session.compacted": { "type": "object", @@ -6050,10 +6501,15 @@ "type": "string" } }, - "required": ["sessionID"] + "required": [ + "sessionID" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.file.edited": { "type": "object", @@ -6069,10 +6525,15 @@ "type": "string" } }, - "required": ["file"] + "required": [ + "file" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Todo": { "type": "object", @@ -6094,7 +6555,12 @@ "type": "string" } }, - "required": ["content", "status", "priority", "id"] + "required": [ + "content", + "status", + "priority", + "id" + ] }, "Event.todo.updated": { "type": "object", @@ -6116,10 +6582,16 @@ } } }, - "required": ["sessionID", "todos"] + "required": [ + "sessionID", + "todos" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.command.executed": { "type": "object", @@ -6146,10 +6618,18 @@ "pattern": "^msg.*" } }, - "required": ["name", "sessionID", "arguments", "messageID"] + "required": [ + "name", + "sessionID", + "arguments", + "messageID" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Session": { "type": "object", @@ -6187,7 +6667,11 @@ } } }, - "required": ["additions", "deletions", "files"] + "required": [ + "additions", + "deletions", + "files" + ] }, "share": { "type": "object", @@ -6196,7 +6680,9 @@ "type": "string" } }, - "required": ["url"] + "required": [ + "url" + ] }, "title": { "type": "string" @@ -6217,7 +6703,10 @@ "type": "number" } }, - "required": ["created", "updated"] + "required": [ + "created", + "updated" + ] }, "revert": { "type": "object", @@ -6235,10 +6724,19 @@ "type": "string" } }, - "required": ["messageID"] + "required": [ + "messageID" + ] } }, - "required": ["id", "projectID", "directory", "title", "version", "time"] + "required": [ + "id", + "projectID", + "directory", + "title", + "version", + "time" + ] }, "Event.session.created": { "type": "object", @@ -6254,10 +6752,15 @@ "$ref": "#/components/schemas/Session" } }, - "required": ["info"] + "required": [ + "info" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.session.updated": { "type": "object", @@ -6273,10 +6776,15 @@ "$ref": "#/components/schemas/Session" } }, - "required": ["info"] + "required": [ + "info" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.session.deleted": { "type": "object", @@ -6292,10 +6800,15 @@ "$ref": "#/components/schemas/Session" } }, - "required": ["info"] + "required": [ + "info" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.session.diff": { "type": "object", @@ -6317,10 +6830,16 @@ } } }, - "required": ["sessionID", "diff"] + "required": [ + "sessionID", + "diff" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.session.error": { "type": "object", @@ -6357,7 +6876,10 @@ } } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.file.watcher.updated": { "type": "object", @@ -6389,10 +6911,16 @@ ] } }, - "required": ["file", "event"] + "required": [ + "file", + "event" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.vcs.branch.updated": { "type": "object", @@ -6410,7 +6938,10 @@ } } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.tui.prompt.append": { "type": "object", @@ -6426,10 +6957,15 @@ "type": "string" } }, - "required": ["text"] + "required": [ + "text" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.tui.command.execute": { "type": "object", @@ -6468,10 +7004,15 @@ ] } }, - "required": ["command"] + "required": [ + "command" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.tui.toast.show": { "type": "object", @@ -6491,7 +7032,12 @@ }, "variant": { "type": "string", - "enum": ["info", "success", "warning", "error"] + "enum": [ + "info", + "success", + "warning", + "error" + ] }, "duration": { "description": "Duration in milliseconds", @@ -6499,10 +7045,16 @@ "type": "number" } }, - "required": ["message", "variant"] + "required": [ + "message", + "variant" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Pty": { "type": "object", @@ -6528,13 +7080,24 @@ }, "status": { "type": "string", - "enum": ["running", "exited"] + "enum": [ + "running", + "exited" + ] }, "pid": { "type": "number" } }, - "required": ["id", "title", "command", "args", "cwd", "status", "pid"] + "required": [ + "id", + "title", + "command", + "args", + "cwd", + "status", + "pid" + ] }, "Event.pty.created": { "type": "object", @@ -6550,10 +7113,15 @@ "$ref": "#/components/schemas/Pty" } }, - "required": ["info"] + "required": [ + "info" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.pty.updated": { "type": "object", @@ -6569,10 +7137,15 @@ "$ref": "#/components/schemas/Pty" } }, - "required": ["info"] + "required": [ + "info" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.pty.exited": { "type": "object", @@ -6592,10 +7165,16 @@ "type": "number" } }, - "required": ["id", "exitCode"] + "required": [ + "id", + "exitCode" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.pty.deleted": { "type": "object", @@ -6612,10 +7191,15 @@ "pattern": "^pty.*" } }, - "required": ["id"] + "required": [ + "id" + ] } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event.server.connected": { "type": "object", @@ -6629,7 +7213,10 @@ "properties": {} } }, - "required": ["type", "properties"] + "required": [ + "type", + "properties" + ] }, "Event": { "anyOf": [ @@ -6741,7 +7328,10 @@ "$ref": "#/components/schemas/Event" } }, - "required": ["directory", "payload"] + "required": [ + "directory", + "payload" + ] }, "Project": { "type": "object", @@ -6769,10 +7359,16 @@ "type": "number" } }, - "required": ["created"] + "required": [ + "created" + ] } }, - "required": ["id", "worktree", "time"] + "required": [ + "id", + "worktree", + "time" + ] }, "BadRequestError": { "type": "object", @@ -6793,7 +7389,11 @@ "const": false } }, - "required": ["data", "errors", "success"] + "required": [ + "data", + "errors", + "success" + ] }, "NotFoundError": { "type": "object", @@ -6809,10 +7409,15 @@ "type": "string" } }, - "required": ["message"] + "required": [ + "message" + ] } }, - "required": ["name", "data"] + "required": [ + "name", + "data" + ] }, "KeybindsConfig": { "description": "Custom keybind configurations", @@ -7079,7 +7684,11 @@ }, "mode": { "type": "string", - "enum": ["subagent", "primary", "all"] + "enum": [ + "subagent", + "primary", + "all" + ] }, "color": { "description": "Hex color code for the agent (e.g., #FF5733)", @@ -7097,13 +7706,21 @@ "properties": { "edit": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "bash": { "anyOf": [ { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, { "type": "object", @@ -7112,22 +7729,38 @@ }, "additionalProperties": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] } } ] }, "webfetch": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "doom_loop": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "external_directory": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] } } } @@ -7215,10 +7848,16 @@ "type": "number" } }, - "required": ["input", "output"] + "required": [ + "input", + "output" + ] } }, - "required": ["input", "output"] + "required": [ + "input", + "output" + ] }, "limit": { "type": "object", @@ -7230,7 +7869,10 @@ "type": "number" } }, - "required": ["context", "output"] + "required": [ + "context", + "output" + ] }, "modalities": { "type": "object", @@ -7239,25 +7881,44 @@ "type": "array", "items": { "type": "string", - "enum": ["text", "audio", "image", "video", "pdf"] + "enum": [ + "text", + "audio", + "image", + "video", + "pdf" + ] } }, "output": { "type": "array", "items": { "type": "string", - "enum": ["text", "audio", "image", "video", "pdf"] + "enum": [ + "text", + "audio", + "image", + "video", + "pdf" + ] } } }, - "required": ["input", "output"] + "required": [ + "input", + "output" + ] }, "experimental": { "type": "boolean" }, "status": { "type": "string", - "enum": ["alpha", "beta", "deprecated"] + "enum": [ + "alpha", + "beta", + "deprecated" + ] }, "options": { "type": "object", @@ -7282,7 +7943,9 @@ "type": "string" } }, - "required": ["npm"] + "required": [ + "npm" + ] } } } @@ -7374,7 +8037,10 @@ "maximum": 9007199254740991 } }, - "required": ["type", "command"], + "required": [ + "type", + "command" + ], "additionalProperties": false }, "McpOAuthConfig": { @@ -7440,13 +8106,58 @@ "maximum": 9007199254740991 } }, - "required": ["type", "url"], + "required": [ + "type", + "url" + ], + "additionalProperties": false + }, + "McpWsConfig": { + "type": "object", + "properties": { + "type": { + "description": "Type of MCP server connection", + "type": "string", + "const": "ws" + }, + "url": { + "description": "WebSocket URL of the MCP server (ws:// or wss://)", + "type": "string" + }, + "enabled": { + "description": "Enable or disable the MCP server on startup", + "type": "boolean" + }, + "headers": { + "description": "Headers to send with the WebSocket connection", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "timeout": { + "description": "Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": [ + "type", + "url" + ], "additionalProperties": false }, "LayoutConfig": { "description": "@deprecated Always uses stretch layout.", "type": "string", - "enum": ["auto", "stretch"] + "enum": [ + "auto", + "stretch" + ] }, "Config": { "type": "object", @@ -7480,12 +8191,17 @@ "type": "boolean" } }, - "required": ["enabled"] + "required": [ + "enabled" + ] }, "diff_style": { "description": "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column", "type": "string", - "enum": ["auto", "stacked"] + "enum": [ + "auto", + "stacked" + ] } } }, @@ -7514,7 +8230,9 @@ "type": "boolean" } }, - "required": ["template"] + "required": [ + "template" + ] } }, "watcher": { @@ -7540,7 +8258,11 @@ "share": { "description": "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing", "type": "string", - "enum": ["manual", "auto", "disabled"] + "enum": [ + "manual", + "auto", + "disabled" + ] }, "autoshare": { "description": "@deprecated Use 'share' field instead. Share newly created sessions automatically", @@ -7643,6 +8365,9 @@ }, { "$ref": "#/components/schemas/McpRemoteConfig" + }, + { + "$ref": "#/components/schemas/McpWsConfig" } ] } @@ -7711,7 +8436,9 @@ "const": true } }, - "required": ["disabled"] + "required": [ + "disabled" + ] }, { "type": "object", @@ -7748,7 +8475,9 @@ "additionalProperties": {} } }, - "required": ["command"] + "required": [ + "command" + ] } ] } @@ -7770,13 +8499,21 @@ "properties": { "edit": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "bash": { "anyOf": [ { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, { "type": "object", @@ -7785,22 +8522,38 @@ }, "additionalProperties": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] } } ] }, "webfetch": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "doom_loop": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "external_directory": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] } } }, @@ -7854,7 +8607,9 @@ } } }, - "required": ["command"] + "required": [ + "command" + ] } } }, @@ -7879,7 +8634,9 @@ } } }, - "required": ["command"] + "required": [ + "command" + ] } } } @@ -7928,7 +8685,11 @@ }, "parameters": {} }, - "required": ["id", "description", "parameters"] + "required": [ + "id", + "description", + "parameters" + ] }, "ToolList": { "type": "array", @@ -7952,7 +8713,12 @@ "type": "string" } }, - "required": ["state", "config", "worktree", "directory"] + "required": [ + "state", + "config", + "worktree", + "directory" + ] }, "VcsInfo": { "type": "object", @@ -7961,7 +8727,9 @@ "type": "string" } }, - "required": ["branch"] + "required": [ + "branch" + ] }, "TextPartInput": { "type": "object", @@ -7992,7 +8760,9 @@ "type": "number" } }, - "required": ["start"] + "required": [ + "start" + ] }, "metadata": { "type": "object", @@ -8002,7 +8772,10 @@ "additionalProperties": {} } }, - "required": ["type", "text"] + "required": [ + "type", + "text" + ] }, "FilePartInput": { "type": "object", @@ -8027,7 +8800,11 @@ "$ref": "#/components/schemas/FilePartSource" } }, - "required": ["type", "mime", "url"] + "required": [ + "type", + "mime", + "url" + ] }, "AgentPartInput": { "type": "object", @@ -8059,10 +8836,17 @@ "maximum": 9007199254740991 } }, - "required": ["value", "start", "end"] + "required": [ + "value", + "start", + "end" + ] } }, - "required": ["type", "name"] + "required": [ + "type", + "name" + ] }, "SubtaskPartInput": { "type": "object", @@ -8084,7 +8868,12 @@ "type": "string" } }, - "required": ["type", "prompt", "description", "agent"] + "required": [ + "type", + "prompt", + "description", + "agent" + ] }, "Command": { "type": "object", @@ -8108,7 +8897,10 @@ "type": "boolean" } }, - "required": ["name", "template"] + "required": [ + "name", + "template" + ] }, "Model": { "type": "object", @@ -8132,7 +8924,11 @@ "type": "string" } }, - "required": ["id", "url", "npm"] + "required": [ + "id", + "url", + "npm" + ] }, "name": { "type": "string" @@ -8171,7 +8967,13 @@ "type": "boolean" } }, - "required": ["text", "audio", "image", "video", "pdf"] + "required": [ + "text", + "audio", + "image", + "video", + "pdf" + ] }, "output": { "type": "object", @@ -8192,10 +8994,23 @@ "type": "boolean" } }, - "required": ["text", "audio", "image", "video", "pdf"] + "required": [ + "text", + "audio", + "image", + "video", + "pdf" + ] } }, - "required": ["temperature", "reasoning", "attachment", "toolcall", "input", "output"] + "required": [ + "temperature", + "reasoning", + "attachment", + "toolcall", + "input", + "output" + ] }, "cost": { "type": "object", @@ -8216,7 +9031,10 @@ "type": "number" } }, - "required": ["read", "write"] + "required": [ + "read", + "write" + ] }, "experimentalOver200K": { "type": "object", @@ -8237,13 +9055,24 @@ "type": "number" } }, - "required": ["read", "write"] + "required": [ + "read", + "write" + ] } }, - "required": ["input", "output", "cache"] + "required": [ + "input", + "output", + "cache" + ] } }, - "required": ["input", "output", "cache"] + "required": [ + "input", + "output", + "cache" + ] }, "limit": { "type": "object", @@ -8255,11 +9084,19 @@ "type": "number" } }, - "required": ["context", "output"] + "required": [ + "context", + "output" + ] }, "status": { "type": "string", - "enum": ["alpha", "beta", "deprecated", "active"] + "enum": [ + "alpha", + "beta", + "deprecated", + "active" + ] }, "options": { "type": "object", @@ -8278,7 +9115,18 @@ } } }, - "required": ["id", "providerID", "api", "name", "capabilities", "cost", "limit", "status", "options", "headers"] + "required": [ + "id", + "providerID", + "api", + "name", + "capabilities", + "cost", + "limit", + "status", + "options", + "headers" + ] }, "Provider": { "type": "object", @@ -8291,7 +9139,12 @@ }, "source": { "type": "string", - "enum": ["env", "config", "custom", "api"] + "enum": [ + "env", + "config", + "custom", + "api" + ] }, "env": { "type": "array", @@ -8319,7 +9172,14 @@ } } }, - "required": ["id", "name", "source", "env", "options", "models"] + "required": [ + "id", + "name", + "source", + "env", + "options", + "models" + ] }, "ProviderAuthMethod": { "type": "object", @@ -8340,7 +9200,10 @@ "type": "string" } }, - "required": ["type", "label"] + "required": [ + "type", + "label" + ] }, "ProviderAuthAuthorization": { "type": "object", @@ -8364,7 +9227,11 @@ "type": "string" } }, - "required": ["url", "method", "instructions"] + "required": [ + "url", + "method", + "instructions" + ] }, "Symbol": { "type": "object", @@ -8385,10 +9252,17 @@ "$ref": "#/components/schemas/Range" } }, - "required": ["uri", "range"] + "required": [ + "uri", + "range" + ] } }, - "required": ["name", "kind", "location"] + "required": [ + "name", + "kind", + "location" + ] }, "FileNode": { "type": "object", @@ -8404,13 +9278,22 @@ }, "type": { "type": "string", - "enum": ["file", "directory"] + "enum": [ + "file", + "directory" + ] }, "ignored": { "type": "boolean" } }, - "required": ["name", "path", "absolute", "type", "ignored"] + "required": [ + "name", + "path", + "absolute", + "type", + "ignored" + ] }, "FileContent": { "type": "object", @@ -8464,14 +9347,24 @@ } } }, - "required": ["oldStart", "oldLines", "newStart", "newLines", "lines"] + "required": [ + "oldStart", + "oldLines", + "newStart", + "newLines", + "lines" + ] } }, "index": { "type": "string" } }, - "required": ["oldFileName", "newFileName", "hunks"] + "required": [ + "oldFileName", + "newFileName", + "hunks" + ] }, "encoding": { "type": "string", @@ -8481,7 +9374,10 @@ "type": "string" } }, - "required": ["type", "content"] + "required": [ + "type", + "content" + ] }, "File": { "type": "object", @@ -8501,10 +9397,19 @@ }, "status": { "type": "string", - "enum": ["added", "deleted", "modified"] + "enum": [ + "added", + "deleted", + "modified" + ] } }, - "required": ["path", "added", "removed", "status"] + "required": [ + "path", + "added", + "removed", + "status" + ] }, "Agent": { "type": "object", @@ -8517,7 +9422,11 @@ }, "mode": { "type": "string", - "enum": ["subagent", "primary", "all"] + "enum": [ + "subagent", + "primary", + "all" + ] }, "builtIn": { "type": "boolean" @@ -8536,7 +9445,11 @@ "properties": { "edit": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "bash": { "type": "object", @@ -8545,23 +9458,42 @@ }, "additionalProperties": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] } }, "webfetch": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "doom_loop": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] }, "external_directory": { "type": "string", - "enum": ["ask", "allow", "deny"] + "enum": [ + "ask", + "allow", + "deny" + ] } }, - "required": ["edit", "bash"] + "required": [ + "edit", + "bash" + ] }, "model": { "type": "object", @@ -8573,7 +9505,10 @@ "type": "string" } }, - "required": ["modelID", "providerID"] + "required": [ + "modelID", + "providerID" + ] }, "prompt": { "type": "string" @@ -8600,7 +9535,14 @@ "maximum": 9007199254740991 } }, - "required": ["name", "mode", "builtIn", "permission", "tools", "options"] + "required": [ + "name", + "mode", + "builtIn", + "permission", + "tools", + "options" + ] }, "MCPStatusConnected": { "type": "object", @@ -8610,7 +9552,9 @@ "const": "connected" } }, - "required": ["status"] + "required": [ + "status" + ] }, "MCPStatusDisabled": { "type": "object", @@ -8620,7 +9564,9 @@ "const": "disabled" } }, - "required": ["status"] + "required": [ + "status" + ] }, "MCPStatusFailed": { "type": "object", @@ -8633,7 +9579,10 @@ "type": "string" } }, - "required": ["status", "error"] + "required": [ + "status", + "error" + ] }, "MCPStatusNeedsAuth": { "type": "object", @@ -8643,7 +9592,9 @@ "const": "needs_auth" } }, - "required": ["status"] + "required": [ + "status" + ] }, "MCPStatusNeedsClientRegistration": { "type": "object", @@ -8656,7 +9607,10 @@ "type": "string" } }, - "required": ["status", "error"] + "required": [ + "status", + "error" + ] }, "MCPStatus": { "anyOf": [ @@ -8702,7 +9656,12 @@ ] } }, - "required": ["id", "name", "root", "status"] + "required": [ + "id", + "name", + "root", + "status" + ] }, "FormatterStatus": { "type": "object", @@ -8720,7 +9679,11 @@ "type": "boolean" } }, - "required": ["name", "extensions", "enabled"] + "required": [ + "name", + "extensions", + "enabled" + ] }, "OAuth": { "type": "object", @@ -8742,7 +9705,12 @@ "type": "string" } }, - "required": ["type", "refresh", "access", "expires"] + "required": [ + "type", + "refresh", + "access", + "expires" + ] }, "ApiAuth": { "type": "object", @@ -8755,7 +9723,10 @@ "type": "string" } }, - "required": ["type", "key"] + "required": [ + "type", + "key" + ] }, "WellKnownAuth": { "type": "object", @@ -8771,7 +9742,11 @@ "type": "string" } }, - "required": ["type", "key", "token"] + "required": [ + "type", + "key", + "token" + ] }, "Auth": { "anyOf": [ @@ -8788,4 +9763,4 @@ } } } -} +} \ No newline at end of file From 0efa4d6578d727f4bf4fa23dc9da1ce646fa1188 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Fri, 12 Dec 2025 10:30:17 -0800 Subject: [PATCH 02/30] Move IDE params into config. Better conventions. --- packages/opencode/src/config/config.ts | 7 ++++ packages/opencode/src/ide/connection.ts | 19 +++++++--- packages/opencode/src/ide/index.ts | 44 ++++++++++++---------- packages/opencode/src/server/server.ts | 10 +++-- packages/opencode/src/session/processor.ts | 7 +--- packages/opencode/src/tool/edit.ts | 38 +++++++++++-------- 6 files changed, 75 insertions(+), 50 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 267278b747e9..f5d88a9e5b26 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -671,6 +671,13 @@ export namespace Config { url: z.string().optional().describe("Enterprise URL"), }) .optional(), + ide: z + .object({ + lockfile_dir: z.string().optional().describe("Directory containing IDE lock files for WebSocket connections"), + auth_header_name: z.string().optional().describe("HTTP header name for IDE WebSocket authentication"), + }) + .optional() + .describe("IDE integration settings"), experimental: z .object({ hook: z diff --git a/packages/opencode/src/ide/connection.ts b/packages/opencode/src/ide/connection.ts index d7baf295f63b..c2385d468e8b 100644 --- a/packages/opencode/src/ide/connection.ts +++ b/packages/opencode/src/ide/connection.ts @@ -1,13 +1,12 @@ import z from "zod/v4" -import os from "os" import path from "path" import { Glob } from "bun" import { Log } from "../util/log" import { WebSocketClientTransport, McpError } from "../mcp/ws" +import { Config } from "../config/config" const log = Log.create({ service: "ide" }) -const LOCK_DIR = path.join(os.homedir(), ".claude", "ide") const WS_PREFIX = "ws://127.0.0.1" const LockFile = { @@ -36,9 +35,15 @@ type LockFile = z.infer export async function discoverLockFiles(): Promise> { const results = new Map() + const config = await Config.get() + + if (!config.ide?.lockfile_dir) { + log.debug("ide.lockfile_dir not configured, skipping IDE discovery") + return results + } const glob = new Glob("*.lock") - for await (const file of glob.scan({ cwd: LOCK_DIR, absolute: true })) { + for await (const file of glob.scan({ cwd: config.ide.lockfile_dir, absolute: true })) { const lockFile = await LockFile.fromFile(file) if (!lockFile) continue @@ -71,6 +76,11 @@ export class Connection { } static async create(key: string): Promise { + const config = await Config.get() + if (!config.ide?.auth_header_name) { + throw new Error("ide.auth_header_name is required in config") + } + const discovered = await discoverLockFiles() const lockFile = discovered.get(key) if (!lockFile) { @@ -79,8 +89,7 @@ export class Connection { const transport = new WebSocketClientTransport(lockFile.url, { headers: { - // TODO research standardized header for this - "x-claude-code-ide-authorization": lockFile.authToken, + [config.ide.auth_header_name]: lockFile.authToken, }, }) diff --git a/packages/opencode/src/ide/index.ts b/packages/opencode/src/ide/index.ts index 3555530f7d4c..7ab0ab262056 100644 --- a/packages/opencode/src/ide/index.ts +++ b/packages/opencode/src/ide/index.ts @@ -112,7 +112,10 @@ export namespace Ide { let activeConnection: Connection | null = null function tabName(filePath: string) { - return `[opencode] Edit: ${path.basename(filePath)} ⧉` + // TODO this is used for a string match in claudecode.nvim that we could + // change if we incorporate a dedicated plugin + // (must start with ✻ and end with ⧉)) + return `✻ [opencode] Edit: ${path.basename(filePath)} ⧉` } export async function status(): Promise> { @@ -130,7 +133,7 @@ export namespace Ide { return result } - export async function connect(key: string): Promise> { + export async function connect(key: string): Promise { if (activeConnection) { await disconnect() } @@ -150,8 +153,6 @@ export namespace Ide { } activeConnection = connection - - return status() } function handleNotification(method: string, params: unknown, instanceDirectory: string) { @@ -170,42 +171,47 @@ export namespace Ide { } } - export async function disconnect(): Promise> { - if (!activeConnection) { - return status() + export async function disconnect(): Promise { + if (activeConnection) { + log.info("IDE disconnecting", { key: activeConnection.key }) + await activeConnection.close() + activeConnection = null } - - await activeConnection.close() - activeConnection = null - - return status() } export function active(): Connection | null { return activeConnection } + const DiffResponse = { + FILE_SAVED: "once", + DIFF_REJECTED: "reject", + } as const satisfies Record + export async function openDiff(filePath: string, newContents: string): Promise { - if (!activeConnection) { + const connection = active() + if (!connection) { throw new Error("No IDE connected") } const name = tabName(filePath) log.info("openDiff", { tabName: name }) - const result = await activeConnection.request<{ content: Array<{ type: string; text: string }> }>("openDiff", { + const result = await connection.request<{ content: Array<{ type: string; text: string }> }>("openDiff", { old_file_path: filePath, new_file_path: filePath, new_file_contents: newContents, tab_name: name, }) log.info("openDiff result", { text: result.content?.[0]?.text }) - const text = result.content?.[0]?.text - if (text === "FILE_SAVED") return "once" - if (text === "DIFF_REJECTED") return "reject" + const text = result.content?.[0]?.text as keyof typeof DiffResponse | undefined + if (text && text in DiffResponse) return DiffResponse[text] throw new Error(`Unexpected openDiff result: ${text}`) } export async function closeTab(filePath: string): Promise { - if (!activeConnection) return - await activeConnection.request("close_tab", { tab_name: tabName(filePath) }) + const connection = active() + if (!connection) { + throw new Error("No IDE connected") + } + await connection.request("close_tab", { tab_name: tabName(filePath) }) } } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index a289f3a05463..3d3b58df6687 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -2072,7 +2072,7 @@ export namespace Server { description: "IDE connected successfully", content: { "application/json": { - schema: resolver(z.record(z.string(), Ide.Status)), + schema: resolver(z.boolean()), }, }, }, @@ -2081,7 +2081,8 @@ export namespace Server { validator("param", z.object({ name: z.string() })), async (c) => { const { name } = c.req.valid("param") - return c.json(await Ide.connect(name)) + await Ide.connect(name) + return c.json(true) }, ) .post( @@ -2094,7 +2095,7 @@ export namespace Server { description: "IDE disconnected successfully", content: { "application/json": { - schema: resolver(z.record(z.string(), Ide.Status)), + schema: resolver(z.boolean()), }, }, }, @@ -2102,7 +2103,8 @@ export namespace Server { }), validator("param", z.object({ name: z.string() })), async (c) => { - return c.json(await Ide.disconnect()) + await Ide.disconnect() + return c.json(true) }, ) .get( diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 01d8f44d2bb5..f1f7dd0964f4 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -213,17 +213,12 @@ export namespace SessionProcessor { case "tool-error": { const match = toolcalls[value.toolCallId] if (match && match.state.status === "running") { - const err = value.error as any await Session.updatePart({ ...match, state: { status: "error", input: value.input, - error: [ - err?.code && `[${err.code}]`, - err?.toString(), - err?.data, - ].filter(Boolean).join(" "), + error: (value.error as any).toString(), metadata: value.error instanceof Permission.RejectedError ? value.error.metadata : undefined, time: { start: match.state.time.start, diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 84e5994bc355..d96e40c2c30f 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -88,17 +88,20 @@ export const EditTool = Tool.define("edit", { title: "Edit this file: " + filePath, metadata: { filePath, diff }, onSetup: (info) => { - if (!Ide.active()) return - Ide.openDiff(filePath, contentNew).then((response) => { - Permission.respond({ - sessionID: info.sessionID, - permissionID: info.id, - response, + if (Ide.active()) { + Ide.openDiff(filePath, contentNew).then((response) => { + Permission.respond({ + sessionID: info.sessionID, + permissionID: info.id, + response, + }) }) - }) + } }, onRespond: () => { - Ide.closeTab(filePath).catch(() => {}) + if (Ide.active()) { + Ide.closeTab(filePath).catch(() => {}) + } }, }) } @@ -129,17 +132,20 @@ export const EditTool = Tool.define("edit", { title: "Edit this file: " + filePath, metadata: { filePath, diff }, onSetup: (info) => { - if (!Ide.active()) return - Ide.openDiff(filePath, contentNew).then((response) => { - Permission.respond({ - sessionID: info.sessionID, - permissionID: info.id, - response, + if (Ide.active()) { + Ide.openDiff(filePath, contentNew).then((response) => { + Permission.respond({ + sessionID: info.sessionID, + permissionID: info.id, + response, + }) }) - }) + } }, onRespond: () => { - Ide.closeTab(filePath).catch(() => {}) + if (Ide.active()) { + Ide.closeTab(filePath).catch(() => {}) + } }, }) } From db1089ba4438ef7c39682435234961d066d4d86d Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Fri, 12 Dec 2025 10:40:36 -0800 Subject: [PATCH 03/30] Correct function names. Remove bin fragment. --- packages/opencode/bin/opencode-dev | 3 --- packages/opencode/src/ide/index.ts | 12 ++++++++---- packages/opencode/src/tool/edit.ts | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) delete mode 100755 packages/opencode/bin/opencode-dev diff --git a/packages/opencode/bin/opencode-dev b/packages/opencode/bin/opencode-dev deleted file mode 100755 index 65ba205faa8c..000000000000 --- a/packages/opencode/bin/opencode-dev +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -cd /Users/tcdent/Work/opencode -exec bun run --cwd packages/opencode --conditions=browser src/index.ts "$@" diff --git a/packages/opencode/src/ide/index.ts b/packages/opencode/src/ide/index.ts index 7ab0ab262056..2dd673d26fdd 100644 --- a/packages/opencode/src/ide/index.ts +++ b/packages/opencode/src/ide/index.ts @@ -111,7 +111,7 @@ export namespace Ide { // Connection let activeConnection: Connection | null = null - function tabName(filePath: string) { + function diffTabName(filePath: string) { // TODO this is used for a string match in claudecode.nvim that we could // change if we incorporate a dedicated plugin // (must start with ✻ and end with ⧉)) @@ -193,7 +193,7 @@ export namespace Ide { if (!connection) { throw new Error("No IDE connected") } - const name = tabName(filePath) + const name = diffTabName(filePath) log.info("openDiff", { tabName: name }) const result = await connection.request<{ content: Array<{ type: string; text: string }> }>("openDiff", { old_file_path: filePath, @@ -207,11 +207,15 @@ export namespace Ide { throw new Error(`Unexpected openDiff result: ${text}`) } - export async function closeTab(filePath: string): Promise { + async function closeTab(tabName: string): Promise { const connection = active() if (!connection) { throw new Error("No IDE connected") } - await connection.request("close_tab", { tab_name: tabName(filePath) }) + await connection.request("close_tab", { tab_name: tabName }) + } + + export async function closeDiff(filePath: string): Promise { + await closeTab(diffTabName(filePath)) } } diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index d96e40c2c30f..35e13dab258e 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -100,7 +100,7 @@ export const EditTool = Tool.define("edit", { }, onRespond: () => { if (Ide.active()) { - Ide.closeTab(filePath).catch(() => {}) + Ide.closeDiff(filePath).catch(() => {}) } }, }) @@ -144,7 +144,7 @@ export const EditTool = Tool.define("edit", { }, onRespond: () => { if (Ide.active()) { - Ide.closeTab(filePath).catch(() => {}) + Ide.closeDiff(filePath).catch(() => {}) } }, }) From 8ffea809807dd7fa075be74e66680edf935249aa Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 20 Dec 2025 15:54:39 +0000 Subject: [PATCH 04/30] chore: generate --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index a31f7d66629c..647bcb563ebe 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -24,4 +24,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index bec8856b2425..665499a6e900 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From 2f6d15a51eb59d107f016a4d8a768fd4d1579c69 Mon Sep 17 00:00:00 2001 From: Ryan Cassidy Date: Sat, 20 Dec 2025 16:56:27 +0000 Subject: [PATCH 05/30] feat: add cursor theme (#5850) --- .../src/cli/cmd/tui/context/theme.tsx | 2 + .../src/cli/cmd/tui/context/theme/cursor.json | 249 ++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 packages/opencode/src/cli/cmd/tui/context/theme/cursor.json diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 595d3025d5e3..8bca9fa88a7f 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -8,6 +8,7 @@ import ayu from "./theme/ayu.json" with { type: "json" } import catppuccin from "./theme/catppuccin.json" with { type: "json" } import catppuccinMacchiato from "./theme/catppuccin-macchiato.json" with { type: "json" } import cobalt2 from "./theme/cobalt2.json" with { type: "json" } +import cursor from "./theme/cursor.json" with { type: "json" } import dracula from "./theme/dracula.json" with { type: "json" } import everforest from "./theme/everforest.json" with { type: "json" } import flexoki from "./theme/flexoki.json" with { type: "json" } @@ -138,6 +139,7 @@ export const DEFAULT_THEMES: Record = { catppuccin, ["catppuccin-macchiato"]: catppuccinMacchiato, cobalt2, + cursor, dracula, everforest, flexoki, diff --git a/packages/opencode/src/cli/cmd/tui/context/theme/cursor.json b/packages/opencode/src/cli/cmd/tui/context/theme/cursor.json new file mode 100644 index 000000000000..ab518dbe7e2a --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/context/theme/cursor.json @@ -0,0 +1,249 @@ +{ + "$schema": "https://opencode.ai/theme.json", + "defs": { + "darkBg": "#181818", + "darkPanel": "#141414", + "darkElement": "#262626", + "darkFg": "#e4e4e4", + "darkMuted": "#e4e4e45e", + "darkBorder": "#e4e4e413", + "darkBorderActive": "#e4e4e426", + "darkCyan": "#88c0d0", + "darkBlue": "#81a1c1", + "darkGreen": "#3fa266", + "darkGreenBright": "#70b489", + "darkRed": "#e34671", + "darkRedBright": "#fc6b83", + "darkYellow": "#f1b467", + "darkOrange": "#d2943e", + "darkPink": "#E394DC", + "darkPurple": "#AAA0FA", + "darkTeal": "#82D2CE", + "darkSyntaxYellow": "#F8C762", + "darkSyntaxOrange": "#EFB080", + "darkSyntaxGreen": "#A8CC7C", + "darkSyntaxBlue": "#87C3FF", + "lightBg": "#fcfcfc", + "lightPanel": "#f3f3f3", + "lightElement": "#ededed", + "lightFg": "#141414", + "lightMuted": "#141414ad", + "lightBorder": "#14141413", + "lightBorderActive": "#14141426", + "lightTeal": "#6f9ba6", + "lightBlue": "#3c7cab", + "lightBlueDark": "#206595", + "lightGreen": "#1f8a65", + "lightGreenBright": "#55a583", + "lightRed": "#cf2d56", + "lightRedBright": "#e75e78", + "lightOrange": "#db704b", + "lightYellow": "#c08532", + "lightPurple": "#9e94d5", + "lightPurpleDark": "#6049b3", + "lightPink": "#b8448b", + "lightMagenta": "#b3003f" + }, + "theme": { + "primary": { + "dark": "darkCyan", + "light": "lightTeal" + }, + "secondary": { + "dark": "darkBlue", + "light": "lightBlue" + }, + "accent": { + "dark": "darkCyan", + "light": "lightTeal" + }, + "error": { + "dark": "darkRed", + "light": "lightRed" + }, + "warning": { + "dark": "darkYellow", + "light": "lightOrange" + }, + "success": { + "dark": "darkGreen", + "light": "lightGreen" + }, + "info": { + "dark": "darkBlue", + "light": "lightBlue" + }, + "text": { + "dark": "darkFg", + "light": "lightFg" + }, + "textMuted": { + "dark": "darkMuted", + "light": "lightMuted" + }, + "background": { + "dark": "darkBg", + "light": "lightBg" + }, + "backgroundPanel": { + "dark": "darkPanel", + "light": "lightPanel" + }, + "backgroundElement": { + "dark": "darkElement", + "light": "lightElement" + }, + "border": { + "dark": "darkBorder", + "light": "lightBorder" + }, + "borderActive": { + "dark": "darkCyan", + "light": "lightTeal" + }, + "borderSubtle": { + "dark": "#0f0f0f", + "light": "#e0e0e0" + }, + "diffAdded": { + "dark": "darkGreen", + "light": "lightGreen" + }, + "diffRemoved": { + "dark": "darkRed", + "light": "lightRed" + }, + "diffContext": { + "dark": "darkMuted", + "light": "lightMuted" + }, + "diffHunkHeader": { + "dark": "darkMuted", + "light": "lightMuted" + }, + "diffHighlightAdded": { + "dark": "darkGreenBright", + "light": "lightGreenBright" + }, + "diffHighlightRemoved": { + "dark": "darkRedBright", + "light": "lightRedBright" + }, + "diffAddedBg": { + "dark": "#3fa26633", + "light": "#1f8a651f" + }, + "diffRemovedBg": { + "dark": "#b8004933", + "light": "#cf2d5614" + }, + "diffContextBg": { + "dark": "darkPanel", + "light": "lightPanel" + }, + "diffLineNumber": { + "dark": "#e4e4e442", + "light": "#1414147a" + }, + "diffAddedLineNumberBg": { + "dark": "#3fa26633", + "light": "#1f8a651f" + }, + "diffRemovedLineNumberBg": { + "dark": "#b8004933", + "light": "#cf2d5614" + }, + "markdownText": { + "dark": "darkFg", + "light": "lightFg" + }, + "markdownHeading": { + "dark": "darkPurple", + "light": "lightBlueDark" + }, + "markdownLink": { + "dark": "darkTeal", + "light": "lightBlueDark" + }, + "markdownLinkText": { + "dark": "darkBlue", + "light": "lightMuted" + }, + "markdownCode": { + "dark": "darkPink", + "light": "lightGreen" + }, + "markdownBlockQuote": { + "dark": "darkMuted", + "light": "lightMuted" + }, + "markdownEmph": { + "dark": "darkTeal", + "light": "lightFg" + }, + "markdownStrong": { + "dark": "darkSyntaxYellow", + "light": "lightFg" + }, + "markdownHorizontalRule": { + "dark": "darkMuted", + "light": "lightMuted" + }, + "markdownListItem": { + "dark": "darkFg", + "light": "lightFg" + }, + "markdownListEnumeration": { + "dark": "darkCyan", + "light": "lightMuted" + }, + "markdownImage": { + "dark": "darkCyan", + "light": "lightBlueDark" + }, + "markdownImageText": { + "dark": "darkBlue", + "light": "lightMuted" + }, + "markdownCodeBlock": { + "dark": "darkFg", + "light": "lightFg" + }, + "syntaxComment": { + "dark": "darkMuted", + "light": "lightMuted" + }, + "syntaxKeyword": { + "dark": "darkTeal", + "light": "lightMagenta" + }, + "syntaxFunction": { + "dark": "darkSyntaxOrange", + "light": "lightOrange" + }, + "syntaxVariable": { + "dark": "darkFg", + "light": "lightFg" + }, + "syntaxString": { + "dark": "darkPink", + "light": "lightPurple" + }, + "syntaxNumber": { + "dark": "darkSyntaxYellow", + "light": "lightPink" + }, + "syntaxType": { + "dark": "darkSyntaxOrange", + "light": "lightBlueDark" + }, + "syntaxOperator": { + "dark": "darkFg", + "light": "lightFg" + }, + "syntaxPunctuation": { + "dark": "darkFg", + "light": "lightFg" + } + } +} From 34eb03f5b8d9103b456e004b4b529d7df0cbdd7b Mon Sep 17 00:00:00 2001 From: ja <51257127+anntnzrb@users.noreply.github.com> Date: Sat, 20 Dec 2025 12:39:26 -0500 Subject: [PATCH 06/30] fix: prioritize session list loading when resuming with -c (#5816) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/opencode/src/cli/cmd/tui/app.tsx | 3 ++- .../opencode/src/cli/cmd/tui/context/sync.tsx | 25 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 028905fc3ab8..e81de188943c 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -229,7 +229,8 @@ function App() { let continued = false createEffect(() => { - if (continued || sync.status !== "complete" || !args.continue) return + // When using -c, session list is loaded in blocking phase, so we can navigate at "partial" + if (continued || sync.status === "loading" || !args.continue) return const match = sync.data.session .toSorted((a, b) => b.time.updated - a.time.updated) .find((x) => x.parentID === undefined)?.id diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index f74f787db8cc..2528a4998965 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -22,6 +22,7 @@ import { Binary } from "@opencode-ai/util/binary" import { createSimpleContext } from "./helper" import type { Snapshot } from "@/snapshot" import { useExit } from "./exit" +import { useArgs } from "./args" import { batch, onMount } from "solid-js" import { Log } from "@/util/log" import type { Path } from "@opencode-ai/sdk" @@ -254,10 +255,18 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }) const exit = useExit() + const args = useArgs() async function bootstrap() { - // blocking - await Promise.all([ + const sessionListPromise = sdk.client.session.list().then((x) => + setStore( + "session", + (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)), + ), + ) + + // blocking - include session.list when continuing a session + const blockingRequests: Promise[] = [ sdk.client.config.providers({}, { throwOnError: true }).then((x) => { batch(() => { setStore("provider", x.data!.providers) @@ -271,17 +280,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }), sdk.client.app.agents({}, { throwOnError: true }).then((x) => setStore("agent", x.data ?? [])), sdk.client.config.get({}, { throwOnError: true }).then((x) => setStore("config", x.data!)), - ]) + ...(args.continue ? [sessionListPromise] : []), + ] + + await Promise.all(blockingRequests) .then(() => { if (store.status !== "complete") setStore("status", "partial") // non-blocking Promise.all([ - sdk.client.session.list().then((x) => - setStore( - "session", - (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)), - ), - ), + ...(args.continue ? [] : [sessionListPromise]), sdk.client.command.list().then((x) => setStore("command", x.data ?? [])), sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)), sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)), From d89b567b47ae2709f0fe3fd1e997860073829d72 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Sun, 21 Dec 2025 01:41:52 +0800 Subject: [PATCH 07/30] fix: add transform case for gemini if mcp tool has missing array items (#5846) --- packages/opencode/src/provider/transform.ts | 4 ++++ .../opencode/test/provider/transform.test.ts | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 320af4d39537..0aacf0935c2b 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -424,6 +424,10 @@ export namespace ProviderTransform { result.required = result.required.filter((field: any) => field in result.properties) } + if (result.type === "array" && result.items == null) { + result.items = {} + } + return result } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 17ae7f558084..78bd296c99c4 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -167,6 +167,30 @@ describe("ProviderTransform.maxOutputTokens", () => { }) }) +describe("ProviderTransform.schema - gemini array items", () => { + test("adds missing items for array properties", () => { + const geminiModel = { + providerID: "google", + api: { + id: "gemini-3-pro", + }, + } as any + + const schema = { + type: "object", + properties: { + nodes: { type: "array" }, + edges: { type: "array", items: { type: "string" } }, + }, + } as any + + const result = ProviderTransform.schema(geminiModel, schema) as any + + expect(result.properties.nodes.items).toBeDefined() + expect(result.properties.edges.items.type).toBe("string") + }) +}) + describe("ProviderTransform.message - DeepSeek reasoning content", () => { test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => { const msgs = [ From da6e0e60c0ca42d54595553fc1ab70f62be6e3b9 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 20 Dec 2025 11:43:59 -0600 Subject: [PATCH 08/30] ci: adjust review agent prompt to discourage bad diffs --- .github/workflows/review.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index 36f6df54f152..c0e3a5deb15a 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -67,6 +67,8 @@ jobs: When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts) Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block. + If you are writing suggested fixes, BE SURE THAT the change you are recommending is actually valid typescript, often I have seen missing closing "}" or other syntax errors. + Generally, write a comment instead of writing suggested change if you can help it. Command MUST be like this. \`\`\` From 8f6c8844d742b56858823e57388e8149f665cb7a Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sat, 20 Dec 2025 12:46:48 -0500 Subject: [PATCH 09/30] feat: support configuring a default_agent across all API/user surfaces (#5843) Co-authored-by: observerw --- github/action.yml | 5 ++++ github/index.ts | 28 ++++++++++++++++++- packages/opencode/src/acp/agent.ts | 6 ++-- packages/opencode/src/agent/agent.ts | 23 +++++++++++++++ packages/opencode/src/cli/cmd/github.ts | 2 +- packages/opencode/src/cli/cmd/run.ts | 28 +++++++++++++++++-- .../src/cli/cmd/tui/context/local.tsx | 2 +- packages/opencode/src/config/config.ts | 6 ++++ packages/opencode/src/server/server.ts | 4 +-- packages/opencode/src/session/prompt.ts | 6 ++-- packages/sdk/js/src/v2/gen/types.gen.ts | 5 ++++ packages/sdk/openapi.json | 7 +++++ packages/web/src/content/docs/config.mdx | 17 +++++++++++ packages/web/src/content/docs/github.mdx | 1 + 14 files changed, 128 insertions(+), 12 deletions(-) diff --git a/github/action.yml b/github/action.yml index cf276b51c8d8..57e26d8566c3 100644 --- a/github/action.yml +++ b/github/action.yml @@ -9,6 +9,10 @@ inputs: description: "Model to use" required: true + agent: + description: "Agent to use. Must be a primary agent. Falls back to default_agent from config or 'build' if not found." + required: false + share: description: "Share the opencode session (defaults to true for public repos)" required: false @@ -62,6 +66,7 @@ runs: run: opencode github run env: MODEL: ${{ inputs.model }} + AGENT: ${{ inputs.agent }} SHARE: ${{ inputs.share }} PROMPT: ${{ inputs.prompt }} USE_GITHUB_TOKEN: ${{ inputs.use_github_token }} diff --git a/github/index.ts b/github/index.ts index 6d826326ed7d..7f6018232977 100644 --- a/github/index.ts +++ b/github/index.ts @@ -318,6 +318,10 @@ function useEnvRunUrl() { return `/${repo.owner}/${repo.repo}/actions/runs/${runId}` } +function useEnvAgent() { + return process.env["AGENT"] || undefined +} + function useEnvShare() { const value = process.env["SHARE"] if (!value) return undefined @@ -578,16 +582,38 @@ async function summarize(response: string) { } } +async function resolveAgent(): Promise { + const envAgent = useEnvAgent() + if (!envAgent) return undefined + + // Validate the agent exists and is a primary agent + const agents = await client.agent.list() + const agent = agents.data?.find((a) => a.name === envAgent) + + if (!agent) { + console.warn(`agent "${envAgent}" not found. Falling back to default agent`) + return undefined + } + + if (agent.mode === "subagent") { + console.warn(`agent "${envAgent}" is a subagent, not a primary agent. Falling back to default agent`) + return undefined + } + + return envAgent +} + async function chat(text: string, files: PromptFiles = []) { console.log("Sending message to opencode...") const { providerID, modelID } = useEnvModel() + const agent = await resolveAgent() const chat = await client.session.chat({ path: session, body: { providerID, modelID, - agent: "build", + agent, parts: [ { type: "text", diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 2817adf5d12f..e6419dd7665c 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -22,6 +22,7 @@ import { Log } from "../util/log" import { ACPSessionManager } from "./session" import type { ACPConfig, ACPSessionState } from "./types" import { Provider } from "../provider/provider" +import { Agent as AgentModule } from "../agent/agent" import { Installation } from "@/installation" import { MessageV2 } from "@/session/message-v2" import { Config } from "@/config/config" @@ -705,7 +706,8 @@ export namespace ACP { description: agent.description, })) - const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id + const defaultAgentName = await AgentModule.defaultAgent() + const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id const mcpServers: Record = {} for (const server of params.mcpServers) { @@ -807,7 +809,7 @@ export namespace ACP { if (!current) { this.sessionManager.setModel(session.id, model) } - const agent = session.modeId ?? "build" + const agent = session.modeId ?? (await AgentModule.defaultAgent()) const parts: Array< { type: "text"; text: string } | { type: "file"; url: string; filename: string; mime: string } diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index add120f910c7..26f241fabbf0 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -5,6 +5,9 @@ import { generateObject, type ModelMessage } from "ai" import { SystemPrompt } from "../session/system" import { Instance } from "../project/instance" import { mergeDeep } from "remeda" +import { Log } from "../util/log" + +const log = Log.create({ service: "agent" }) import PROMPT_GENERATE from "./generate.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt" @@ -20,6 +23,7 @@ export namespace Agent { mode: z.enum(["subagent", "primary", "all"]), native: z.boolean().optional(), hidden: z.boolean().optional(), + default: z.boolean().optional(), topP: z.number().optional(), temperature: z.number().optional(), color: z.string().optional(), @@ -245,6 +249,19 @@ export namespace Agent { item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {}) } } + + // Mark the default agent + const defaultName = cfg.default_agent ?? "build" + const defaultCandidate = result[defaultName] + if (defaultCandidate && defaultCandidate.mode !== "subagent") { + defaultCandidate.default = true + } else { + // Fall back to "build" if configured default is invalid + if (result["build"]) { + result["build"].default = true + } + } + return result }) @@ -256,6 +273,12 @@ export namespace Agent { return state().then((x) => Object.values(x)) } + export async function defaultAgent(): Promise { + const agents = await state() + const defaultCandidate = Object.values(agents).find((a) => a.default) + return defaultCandidate?.name ?? "build" + } + export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) { const cfg = await Config.get() const defaultModel = input.model ?? (await Provider.defaultModel()) diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index f4f026d4c3a7..26340044c810 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -762,7 +762,7 @@ export const GithubRunCommand = cmd({ providerID, modelID, }, - agent: "build", + // agent is omitted - server will use default_agent from config or fall back to "build" parts: [ { id: Identifier.ascending("part"), diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 3a0b2f23fb77..0c371b864ce8 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -10,6 +10,7 @@ import { select } from "@clack/prompts" import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2" import { Server } from "../../server/server" import { Provider } from "../../provider/provider" +import { Agent } from "../../agent/agent" const TOOL: Record = { todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD], @@ -223,10 +224,33 @@ export const RunCommand = cmd({ } })() + // Validate agent if specified + const resolvedAgent = await (async () => { + if (!args.agent) return undefined + const agent = await Agent.get(args.agent) + if (!agent) { + UI.println( + UI.Style.TEXT_WARNING_BOLD + "!", + UI.Style.TEXT_NORMAL, + `agent "${args.agent}" not found. Falling back to default agent`, + ) + return undefined + } + if (agent.mode === "subagent") { + UI.println( + UI.Style.TEXT_WARNING_BOLD + "!", + UI.Style.TEXT_NORMAL, + `agent "${args.agent}" is a subagent, not a primary agent. Falling back to default agent`, + ) + return undefined + } + return args.agent + })() + if (args.command) { await sdk.session.command({ sessionID, - agent: args.agent || "build", + agent: resolvedAgent, model: args.model, command: args.command, arguments: message, @@ -235,7 +259,7 @@ export const RunCommand = cmd({ const modelParam = args.model ? Provider.parseModel(args.model) : undefined await sdk.session.prompt({ sessionID, - agent: args.agent || "build", + agent: resolvedAgent, model: modelParam, parts: [...fileParts, { type: "text", text: message }], }) diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index f04b79685c11..55c04621ef36 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -56,7 +56,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const [agentStore, setAgentStore] = createStore<{ current: string }>({ - current: agents()[0].name, + current: agents().find((x) => x.default)?.name ?? agents()[0].name, }) const { theme } = useTheme() const colors = createMemo(() => [ diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index a01cc832a097..031bdd31babd 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -666,6 +666,12 @@ export namespace Config { .string() .describe("Small model to use for tasks like title generation in the format of provider/model") .optional(), + default_agent: z + .string() + .optional() + .describe( + "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.", + ), username: z .string() .optional() diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 77bf5085b5c8..2f4b3b221c96 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1060,11 +1060,11 @@ export namespace Server { const sessionID = c.req.valid("param").sessionID const body = c.req.valid("json") const msgs = await Session.messages({ sessionID }) - let currentAgent = "build" + let currentAgent = await Agent.defaultAgent() for (let i = msgs.length - 1; i >= 0; i--) { const info = msgs[i].info if (info.role === "user") { - currentAgent = info.agent || "build" + currentAgent = info.agent || (await Agent.defaultAgent()) break } } diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index cbb3eedf345c..ebd54a6c802a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -715,7 +715,7 @@ export namespace SessionPrompt { } async function createUserMessage(input: PromptInput) { - const agent = await Agent.get(input.agent ?? "build") + const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent())) const info: MessageV2.Info = { id: input.messageID ?? Identifier.ascending("message"), role: "user", @@ -1282,7 +1282,7 @@ export namespace SessionPrompt { export async function command(input: CommandInput) { log.info("command", input) const command = await Command.get(input.command) - const agentName = command.agent ?? input.agent ?? "build" + const agentName = command.agent ?? input.agent ?? (await Agent.defaultAgent()) const raw = input.arguments.match(argsRegex) ?? [] const args = raw.map((arg) => arg.replace(quoteTrimRegex, "")) @@ -1425,7 +1425,7 @@ export namespace SessionPrompt { time: { created: Date.now(), }, - agent: input.message.info.role === "user" ? input.message.info.agent : "build", + agent: input.message.info.role === "user" ? input.message.info.agent : await Agent.defaultAgent(), model: { providerID: input.providerID, modelID: input.modelID, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index cdbdfdfda115..1b43d3f48a1e 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1414,6 +1414,10 @@ export type Config = { * Small model to use for tasks like title generation in the format of provider/model */ small_model?: string + /** + * Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid. + */ + default_agent?: string /** * Custom username to display in conversations instead of system username */ @@ -1767,6 +1771,7 @@ export type Agent = { mode: "subagent" | "primary" | "all" native?: boolean hidden?: boolean + default?: boolean topP?: number temperature?: number color?: string diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index eeb81a8443a5..f33d20069c43 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8153,6 +8153,10 @@ "description": "Small model to use for tasks like title generation in the format of provider/model", "type": "string" }, + "default_agent": { + "description": "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.", + "type": "string" + }, "username": { "description": "Custom username to display in conversations instead of system username", "type": "string" @@ -9152,6 +9156,9 @@ "hidden": { "type": "boolean" }, + "default": { + "type": "boolean" + }, "topP": { "type": "number" }, diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index 302d79d1724a..5ba22ff2d78b 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -194,6 +194,23 @@ You can also define agents using markdown files in `~/.config/opencode/agent/` o --- +### Default agent + +You can set the default agent using the `default_agent` option. This determines which agent is used when none is explicitly specified. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "default_agent": "plan" +} +``` + +The default agent must be a primary agent (not a subagent). This can be a built-in agent like `"build"` or `"plan"`, or a [custom agent](/docs/agents) you've defined. If the specified agent doesn't exist or is a subagent, OpenCode will fall back to `"build"` with a warning. + +This setting applies across all interfaces: TUI, CLI (`opencode run`), desktop app, and GitHub Action. + +--- + ### Sharing You can configure the [share](/docs/share) feature through the `share` option. diff --git a/packages/web/src/content/docs/github.mdx b/packages/web/src/content/docs/github.mdx index a38df68f4d3d..1d6078840035 100644 --- a/packages/web/src/content/docs/github.mdx +++ b/packages/web/src/content/docs/github.mdx @@ -81,6 +81,7 @@ Or you can set it up manually. ## Configuration - `model`: The model to use with OpenCode. Takes the format of `provider/model`. This is **required**. +- `agent`: The agent to use. Must be a primary agent. Falls back to `default_agent` from config or `"build"` if not found. - `share`: Whether to share the OpenCode session. Defaults to **true** for public repositories. - `prompt`: Optional custom prompt to override the default behavior. Use this to customize how OpenCode processes requests. - `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. From c7cade2494a08fc64cedc9025ad10d26d00d746a Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 20 Dec 2025 12:52:46 -0500 Subject: [PATCH 10/30] zen: sync --- packages/web/src/content/docs/zen.mdx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index 2cd8f57951b0..134271ca15fa 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -77,7 +77,6 @@ You can also access our models through the following API endpoints. | Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Opus 4.5 | claude-opus-4-5 | `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` | -| MiniMax M2.1 | minimax-m2.1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | | GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -110,7 +109,6 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | Model | Input | Output | Cached Read | Cached Write | | --------------------------------- | ------ | ------ | ----------- | ------------ | | Big Pickle | Free | Free | Free | - | -| MiniMax M2.1 | Free | Free | Free | - | | Grok Code Fast 1 | Free | Free | Free | - | | GLM 4.6 | $0.60 | $2.20 | $0.10 | - | | Kimi K2 | $0.40 | $2.50 | - | - | @@ -144,7 +142,6 @@ Credit card fees are passed along at cost; we don't charge anything beyond that. The free models: - Grok Code Fast 1 is currently free on OpenCode for a limited time. The xAI team is using this time to collect feedback and improve Grok Code. -- MiniMax M2.1 is currently free on OpenCode for a limited time. The MiniMax team is using this time to collect feedback and improve M2.1. - Big Pickle is a stealth model that's free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. Contact us if you have any questions. @@ -156,7 +153,6 @@ The free models: All our models are hosted in the US. Our providers follow a zero-retention policy and do not use your data for model training, with the following exceptions: - Grok Code Fast 1: During its free period, collected data may be used to improve Grok Code. -- MiniMax M2.1: During its free period, collected data may be used to improve M2.1. - Big Pickle: During its free period, collected data may be used to improve the model. - OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). - Anthropic APIs: Requests are retained for 30 days in accordance with [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage). From 426791f68a3a6a5212af5e88c932c2b41f17c294 Mon Sep 17 00:00:00 2001 From: Shpetim <32248437+ShpetimA@users.noreply.github.com> Date: Sat, 20 Dec 2025 18:53:46 +0100 Subject: [PATCH 11/30] fix: system theme flicker (#5842) Co-authored-by: Shpetim --- .../src/cli/cmd/tui/context/theme.tsx | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 8bca9fa88a7f..cbe836b1c71e 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -281,14 +281,23 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ ready: false, }) - createEffect(async () => { - const custom = await getCustomThemes() - setStore( - produce((draft) => { - Object.assign(draft.themes, custom) - draft.ready = true - }), - ) + createEffect(() => { + getCustomThemes() + .then((custom) => { + setStore( + produce((draft) => { + Object.assign(draft.themes, custom) + }), + ) + }) + .catch(() => { + setStore("active", "opencode") + }) + .finally(() => { + if (store.active !== "system") { + setStore("ready", true) + } + }) }) const renderer = useRenderer() @@ -297,8 +306,25 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ size: 16, }) .then((colors) => { - if (!colors.palette[0]) return - setStore("themes", "system", generateSystem(colors, store.mode)) + if (!colors.palette[0]) { + if (store.active === "system") { + setStore( + produce((draft) => { + draft.active = "opencode" + draft.ready = true + }), + ) + } + return + } + setStore( + produce((draft) => { + draft.themes.system = generateSystem(colors, store.mode) + if (store.active === "system") { + draft.ready = true + } + }), + ) }) const values = createMemo(() => { From 3b261e012547bbe33cfda83460e14ec83cf2f0d3 Mon Sep 17 00:00:00 2001 From: ja <51257127+anntnzrb@users.noreply.github.com> Date: Sat, 20 Dec 2025 12:54:49 -0500 Subject: [PATCH 12/30] docs: add name property to model configuration example (#5853) --- packages/web/src/content/docs/models.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/web/src/content/docs/models.mdx b/packages/web/src/content/docs/models.mdx index 0402d4973ecb..002f3f558e3d 100644 --- a/packages/web/src/content/docs/models.mdx +++ b/packages/web/src/content/docs/models.mdx @@ -117,6 +117,7 @@ You can also define custom models that extend built-in ones and can optionally u "models": { "gpt-5-high": { "id": "gpt-5", + "name": "MyGPT5 (High Reasoning)", "options": { "reasoningEffort": "high", "textVerbosity": "low", @@ -125,6 +126,7 @@ You can also define custom models that extend built-in ones and can optionally u }, "gpt-5-low": { "id": "gpt-5", + "name": "MyGPT5 (Low Reasoning)", "options": { "reasoningEffort": "low", "textVerbosity": "low", From 7dd8ea58c22005d430d4ea7e18f3f693477adc5b Mon Sep 17 00:00:00 2001 From: shamil2 <42864151+shamil2@users.noreply.github.com> Date: Sat, 20 Dec 2025 19:04:35 +0100 Subject: [PATCH 13/30] =?UTF-8?q?feat:=20add=20Catppuccin=20Frapp=C3=A9=20?= =?UTF-8?q?theme=20(#5821)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: shamil2 --- .../src/cli/cmd/tui/context/theme.tsx | 2 + .../tui/context/theme/catppuccin-frappe.json | 233 ++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 packages/opencode/src/cli/cmd/tui/context/theme/catppuccin-frappe.json diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index cbe836b1c71e..a17b13533797 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -6,6 +6,7 @@ import { createSimpleContext } from "./helper" import aura from "./theme/aura.json" with { type: "json" } import ayu from "./theme/ayu.json" with { type: "json" } import catppuccin from "./theme/catppuccin.json" with { type: "json" } +import catppuccinFrappe from "./theme/catppuccin-frappe.json" with { type: "json" } import catppuccinMacchiato from "./theme/catppuccin-macchiato.json" with { type: "json" } import cobalt2 from "./theme/cobalt2.json" with { type: "json" } import cursor from "./theme/cursor.json" with { type: "json" } @@ -137,6 +138,7 @@ export const DEFAULT_THEMES: Record = { aura, ayu, catppuccin, + ["catppuccin-frappe"]: catppuccinFrappe, ["catppuccin-macchiato"]: catppuccinMacchiato, cobalt2, cursor, diff --git a/packages/opencode/src/cli/cmd/tui/context/theme/catppuccin-frappe.json b/packages/opencode/src/cli/cmd/tui/context/theme/catppuccin-frappe.json new file mode 100644 index 000000000000..79e56ee9af05 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/context/theme/catppuccin-frappe.json @@ -0,0 +1,233 @@ +{ + "$schema": "https://opencode.ai/theme.json", + "defs": { + "frappeRosewater": "#f2d5cf", + "frappeFlamingo": "#eebebe", + "frappePink": "#f4b8e4", + "frappeMauve": "#ca9ee6", + "frappeRed": "#e78284", + "frappeMaroon": "#ea999c", + "frappePeach": "#ef9f76", + "frappeYellow": "#e5c890", + "frappeGreen": "#a6d189", + "frappeTeal": "#81c8be", + "frappeSky": "#99d1db", + "frappeSapphire": "#85c1dc", + "frappeBlue": "#8da4e2", + "frappeLavender": "#babbf1", + "frappeText": "#c6d0f5", + "frappeSubtext1": "#b5bfe2", + "frappeSubtext0": "#a5adce", + "frappeOverlay2": "#949cb8", + "frappeOverlay1": "#838ba7", + "frappeOverlay0": "#737994", + "frappeSurface2": "#626880", + "frappeSurface1": "#51576d", + "frappeSurface0": "#414559", + "frappeBase": "#303446", + "frappeMantle": "#292c3c", + "frappeCrust": "#232634" + }, + "theme": { + "primary": { + "dark": "frappeBlue", + "light": "frappeBlue" + }, + "secondary": { + "dark": "frappeMauve", + "light": "frappeMauve" + }, + "accent": { + "dark": "frappePink", + "light": "frappePink" + }, + "error": { + "dark": "frappeRed", + "light": "frappeRed" + }, + "warning": { + "dark": "frappeYellow", + "light": "frappeYellow" + }, + "success": { + "dark": "frappeGreen", + "light": "frappeGreen" + }, + "info": { + "dark": "frappeTeal", + "light": "frappeTeal" + }, + "text": { + "dark": "frappeText", + "light": "frappeText" + }, + "textMuted": { + "dark": "frappeSubtext1", + "light": "frappeSubtext1" + }, + "background": { + "dark": "frappeBase", + "light": "frappeBase" + }, + "backgroundPanel": { + "dark": "frappeMantle", + "light": "frappeMantle" + }, + "backgroundElement": { + "dark": "frappeCrust", + "light": "frappeCrust" + }, + "border": { + "dark": "frappeSurface0", + "light": "frappeSurface0" + }, + "borderActive": { + "dark": "frappeSurface1", + "light": "frappeSurface1" + }, + "borderSubtle": { + "dark": "frappeSurface2", + "light": "frappeSurface2" + }, + "diffAdded": { + "dark": "frappeGreen", + "light": "frappeGreen" + }, + "diffRemoved": { + "dark": "frappeRed", + "light": "frappeRed" + }, + "diffContext": { + "dark": "frappeOverlay2", + "light": "frappeOverlay2" + }, + "diffHunkHeader": { + "dark": "frappePeach", + "light": "frappePeach" + }, + "diffHighlightAdded": { + "dark": "frappeGreen", + "light": "frappeGreen" + }, + "diffHighlightRemoved": { + "dark": "frappeRed", + "light": "frappeRed" + }, + "diffAddedBg": { + "dark": "#29342b", + "light": "#29342b" + }, + "diffRemovedBg": { + "dark": "#3a2a31", + "light": "#3a2a31" + }, + "diffContextBg": { + "dark": "frappeMantle", + "light": "frappeMantle" + }, + "diffLineNumber": { + "dark": "frappeSurface1", + "light": "frappeSurface1" + }, + "diffAddedLineNumberBg": { + "dark": "#223025", + "light": "#223025" + }, + "diffRemovedLineNumberBg": { + "dark": "#2f242b", + "light": "#2f242b" + }, + "markdownText": { + "dark": "frappeText", + "light": "frappeText" + }, + "markdownHeading": { + "dark": "frappeMauve", + "light": "frappeMauve" + }, + "markdownLink": { + "dark": "frappeBlue", + "light": "frappeBlue" + }, + "markdownLinkText": { + "dark": "frappeSky", + "light": "frappeSky" + }, + "markdownCode": { + "dark": "frappeGreen", + "light": "frappeGreen" + }, + "markdownBlockQuote": { + "dark": "frappeYellow", + "light": "frappeYellow" + }, + "markdownEmph": { + "dark": "frappeYellow", + "light": "frappeYellow" + }, + "markdownStrong": { + "dark": "frappePeach", + "light": "frappePeach" + }, + "markdownHorizontalRule": { + "dark": "frappeSubtext0", + "light": "frappeSubtext0" + }, + "markdownListItem": { + "dark": "frappeBlue", + "light": "frappeBlue" + }, + "markdownListEnumeration": { + "dark": "frappeSky", + "light": "frappeSky" + }, + "markdownImage": { + "dark": "frappeBlue", + "light": "frappeBlue" + }, + "markdownImageText": { + "dark": "frappeSky", + "light": "frappeSky" + }, + "markdownCodeBlock": { + "dark": "frappeText", + "light": "frappeText" + }, + "syntaxComment": { + "dark": "frappeOverlay2", + "light": "frappeOverlay2" + }, + "syntaxKeyword": { + "dark": "frappeMauve", + "light": "frappeMauve" + }, + "syntaxFunction": { + "dark": "frappeBlue", + "light": "frappeBlue" + }, + "syntaxVariable": { + "dark": "frappeRed", + "light": "frappeRed" + }, + "syntaxString": { + "dark": "frappeGreen", + "light": "frappeGreen" + }, + "syntaxNumber": { + "dark": "frappePeach", + "light": "frappePeach" + }, + "syntaxType": { + "dark": "frappeYellow", + "light": "frappeYellow" + }, + "syntaxOperator": { + "dark": "frappeSky", + "light": "frappeSky" + }, + "syntaxPunctuation": { + "dark": "frappeText", + "light": "frappeText" + } + } +} From ad6a5e6157ea728d3d2cb29297d6bbb5f79dc6bc Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Sat, 20 Dec 2025 13:05:06 -0500 Subject: [PATCH 14/30] feat(docs): adding .md to docs pages shows raw markdown (#5823) --- packages/web/src/pages/[...slug].md.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 packages/web/src/pages/[...slug].md.ts diff --git a/packages/web/src/pages/[...slug].md.ts b/packages/web/src/pages/[...slug].md.ts new file mode 100644 index 000000000000..51c63b5a69de --- /dev/null +++ b/packages/web/src/pages/[...slug].md.ts @@ -0,0 +1,18 @@ +import type { APIRoute } from "astro" +import { getCollection } from "astro:content" + +export const GET: APIRoute = async ({ params }) => { + const slug = params.slug || "index" + const docs = await getCollection("docs") + const doc = docs.find((d) => d.id === slug) + + if (!doc) { + return new Response("Not found", { status: 404 }) + } + + return new Response(doc.body, { + headers: { + "Content-Type": "text/plain; charset=utf-8", + }, + }) +} From 9caaae6a185957b4dc58d4eadf86d8af8a8fdbd5 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 20 Dec 2025 13:47:28 -0600 Subject: [PATCH 15/30] tweak: better error message if no primary agents are enabled --- packages/opencode/src/cli/error.ts | 3 ++- packages/opencode/src/config/config.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index 07a53d293163..54ced0d7a765 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -32,7 +32,8 @@ export function FormatError(input: unknown) { } if (Config.InvalidError.isInstance(input)) return [ - `Config file at ${input.data.path} is invalid` + (input.data.message ? `: ${input.data.message}` : ""), + `Configuration is invalid${input.data.path && input.data.path !== "config" ? ` at ${input.data.path}` : ""}` + + (input.data.message ? `: ${input.data.message}` : ""), ...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []), ].join("\n") diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 031bdd31babd..1158d67f4cc3 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -141,6 +141,17 @@ export namespace Config { if (!result.keybinds) result.keybinds = Info.shape.keybinds.parse({}) + // Only validate if user has configured agents - if none configured, built-in agents will be used + if (Object.keys(result.agent).length > 0) { + const primaryAgents = Object.values(result.agent).filter((a) => a.mode !== "subagent" && !a.hidden && !a.disable) + if (primaryAgents.length === 0) { + throw new InvalidError({ + path: "config", + message: "No primary agents are available. Please configure at least one agent with mode 'primary' or 'all'.", + }) + } + } + return { config: result, directories, From 6c40bfe04322c14350c9bd3447174986800a57d7 Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Sat, 20 Dec 2025 14:51:13 -0500 Subject: [PATCH 16/30] docs: clarify model ID format for OpenCode provider (#5854) --- packages/web/src/content/docs/agents.mdx | 2 ++ packages/web/src/content/docs/models.mdx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index e9d4490a171e..6c1b15239c90 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -331,6 +331,8 @@ If you don’t specify a model, primary agents use the [model globally configure } ``` +The model ID in your OpenCode config uses the format `provider/model-id`. For example, if you're using [OpenCode Zen](/docs/zen), you would use `opencode/gpt-5.1-codex` for GPT 5.1 Codex. + --- ### Tools diff --git a/packages/web/src/content/docs/models.mdx b/packages/web/src/content/docs/models.mdx index 002f3f558e3d..2077b8e0b2e4 100644 --- a/packages/web/src/content/docs/models.mdx +++ b/packages/web/src/content/docs/models.mdx @@ -60,7 +60,7 @@ OpenCode config. } ``` -Here the full ID is `provider_id/model_id`. +Here the full ID is `provider_id/model_id`. For example, if you're using [OpenCode Zen](/docs/zen), you would use `opencode/gpt-5.1-codex` for GPT 5.1 Codex. If you've configured a [custom provider](/docs/providers#custom), the `provider_id` is key from the `provider` part of your config, and the `model_id` is the key from `provider.models`. From c81506b28d78c944da72bad33899bb0ef00ff816 Mon Sep 17 00:00:00 2001 From: YuY801103 Date: Sun, 21 Dec 2025 05:51:42 +0800 Subject: [PATCH 17/30] docs: add Traditional Chinese (Taiwan) README translation (#5861) Co-authored-by: Yu --- README.zh-TW.md | 115 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 README.zh-TW.md diff --git a/README.zh-TW.md b/README.zh-TW.md new file mode 100644 index 000000000000..d3cbb263c894 --- /dev/null +++ b/README.zh-TW.md @@ -0,0 +1,115 @@ +

+ + + + + OpenCode logo + + +

+

開源的 AI Coding Agent。

+

+ Discord + npm + Build status +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### 安裝 + +```bash +# 直接安裝 (YOLO) +curl -fsSL https://opencode.ai/install | bash + +# 套件管理員 +npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn +scoop bucket add extras; scoop install extras/opencode # Windows +choco install opencode # Windows +brew install opencode # macOS 與 Linux +paru -S opencode-bin # Arch Linux +mise use -g github:sst/opencode # 任何作業系統 +nix run nixpkgs#opencode # 或使用 github:sst/opencode 以取得最新開發分支 +``` + +> [!TIP] +> 安裝前請先移除 0.1.x 以前的舊版本。 + +### 桌面應用程式 (BETA) + +OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/sst/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。 + +| 平台 | 下載連結 | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, 或 AppImage | + +```bash +# macOS (Homebrew Cask) +brew install --cask opencode-desktop +``` + +#### 安裝目錄 + +安裝腳本會依據以下優先順序決定安裝路徑: + +1. `$OPENCODE_INSTALL_DIR` - 自定義安裝目錄 +2. `$XDG_BIN_DIR` - 符合 XDG 基礎目錄規範的路徑 +3. `$HOME/bin` - 標準使用者執行檔目錄 (若存在或可建立) +4. `$HOME/.opencode/bin` - 預設備用路徑 + +```bash +# 範例 +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。 + +- **build** - 預設模式,具備完整權限的 Agent,適用於開發工作。 +- **plan** - 唯讀模式,適用於程式碼分析與探索。 + - 預設禁止修改檔案。 + - 執行 bash 指令前會詢問權限。 + - 非常適合用來探索陌生的程式碼庫或規劃變更。 + +此外,OpenCode 還包含一個 **general** 子 Agent,用於處理複雜搜尋與多步驟任務。此 Agent 供系統內部使用,亦可透過在訊息中輸入 `@general` 來呼叫。 + +了解更多關於 [Agents](https://opencode.ai/docs/agents) 的資訊。 + +### 線上文件 + +關於如何設定 OpenCode 的詳細資訊,請參閱我們的 [**官方文件**](https://opencode.ai/docs)。 + +### 參與貢獻 + +如果您有興趣參與 OpenCode 的開發,請在提交 Pull Request 前先閱讀我們的 [貢獻指南 (Contributing Docs)](./CONTRIBUTING.md)。 + +### 基於 OpenCode 進行開發 + +如果您正在開發與 OpenCode 相關的專案,並在名稱中使用了 "opencode"(例如 "opencode-dashboard" 或 "opencode-mobile"),請在您的 README 中加入聲明,說明該專案並非由 OpenCode 團隊開發,且與我們沒有任何隸屬關係。 + +### 常見問題 (FAQ) + +#### 這跟 Claude Code 有什麼不同? + +在功能面上與 Claude Code 非常相似。以下是關鍵差異: + +- 100% 開源。 +- 不綁定特定的服務提供商。雖然我們推薦使用透過 [OpenCode Zen](https://opencode.ai/zen) 提供的模型,但 OpenCode 也可搭配 Claude, OpenAI, Google 甚至本地模型使用。隨著模型不斷演進,彼此間的差距會縮小且價格會下降,因此具備「不限廠商 (provider-agnostic)」的特性至關重要。 +- 內建 LSP (語言伺服器協定) 支援。 +- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造;我們將不斷挑戰終端機介面的極限。 +- 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。 + +#### 另一個同名的 Repo 是什麼? + +另一個名稱相近的儲存庫與本專案無關。您可以點此[閱讀背後的故事](https://x.com/thdxr/status/1933561254481666466)。 + +--- + +**加入我們的社群** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) From 328734904ace8fe4221d80bf91d811ab8d1b49fe Mon Sep 17 00:00:00 2001 From: Numerilab Date: Sat, 20 Dec 2025 19:42:55 -0500 Subject: [PATCH 18/30] feat(ide): IDE integration with Cursor/VSCode and improved UX - Add IDE connection via WebSocket with JSON-RPC - Live text selection from editor displayed in footer - Selection sent as synthetic part (invisible but included in context) - IDE status visible in home screen footer - Fix reactivity with reconcile for IDE status updates Based on initial work from #5447, with additional UX improvements. --- .../cli/cmd/tui/component/prompt/index.tsx | 93 +++++++--- .../src/cli/cmd/tui/context/local.tsx | 58 ++++++- .../opencode/src/cli/cmd/tui/routes/home.tsx | 18 +- .../src/cli/cmd/tui/routes/session/footer.tsx | 15 ++ packages/opencode/src/config/config.ts | 51 ++---- packages/opencode/src/ide/connection.ts | 162 ++++++++++++++++++ packages/opencode/src/ide/index.ts | 145 ++++++++++++++++ packages/opencode/src/mcp/ws.ts | 64 +++++++ 8 files changed, 536 insertions(+), 70 deletions(-) create mode 100644 packages/opencode/src/ide/connection.ts create mode 100644 packages/opencode/src/mcp/ws.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 99a90ab46acd..bc14c06128d5 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -20,6 +20,7 @@ import { useExit } from "../../context/exit" import { Clipboard } from "../../util/clipboard" import type { FilePart } from "@opencode-ai/sdk/v2" import { TuiEvent } from "../../event" +import { Ide } from "@/ide" import { iife } from "@/util/iife" import { Locale } from "@/util/locale" import { createColors, createFrames } from "../../ui/spinner.ts" @@ -44,7 +45,6 @@ export type PromptRef = { reset(): void blur(): void focus(): void - submit(): void } const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"] @@ -116,7 +116,7 @@ export function Prompt(props: PromptProps) { const sync = useSync() const dialog = useDialog() const toast = useToast() - const status = createMemo(() => sync.data.session_status?.[props.sessionID ?? ""] ?? { type: "idle" }) + const status = createMemo(() => sync.data.session_status[props.sessionID ?? ""] ?? { type: "idle" }) const history = usePromptHistory() const command = useCommandDialog() const renderer = useRenderer() @@ -312,6 +312,10 @@ export function Prompt(props: PromptProps) { input.insertText(evt.properties.text) }) + sdk.event.on(Ide.Event.SelectionChanged.type, (evt) => { + updateIdeSelection(evt.properties.selection) + }) + createEffect(() => { if (props.disabled) input.cursorColor = theme.backgroundElement if (!props.disabled) input.cursorColor = theme.text @@ -342,6 +346,49 @@ export function Prompt(props: PromptProps) { promptPartTypeId = input.extmarks.registerType("prompt-part") }) + // Track IDE selection extmark so we can update/remove it + let ideSelectionExtmarkId: number | null = null + + function removeExtmark(extmarkId: number) { + const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId) + const extmark = allExtmarks.find((e) => e.id === extmarkId) + const partIndex = store.extmarkToPartIndex.get(extmarkId) + + if (partIndex !== undefined) { + setStore( + produce((draft) => { + draft.prompt.parts.splice(partIndex, 1) + draft.extmarkToPartIndex.delete(extmarkId) + const newMap = new Map() + for (const [id, idx] of draft.extmarkToPartIndex) { + newMap.set(id, idx > partIndex ? idx - 1 : idx) + } + draft.extmarkToPartIndex = newMap + }), + ) + } + + if (extmark) { + const savedOffset = input.cursorOffset + input.cursorOffset = extmark.start + const start = { ...input.logicalCursor } + input.cursorOffset = extmark.end + 1 + input.deleteRange(start.row, start.col, input.logicalCursor.row, input.logicalCursor.col) + input.cursorOffset = + savedOffset > extmark.start + ? Math.max(extmark.start, savedOffset - (extmark.end + 1 - extmark.start)) + : savedOffset + } + + input.extmarks.delete(extmarkId) + } + + function updateIdeSelection(selection: Ide.Selection | null) { + // Selection is now displayed in footer via local.selection + // No visual insertion in the input needed + // Content will be included at submit time from local.selection + } + function restoreExtmarksFromParts(parts: PromptInfo["parts"]) { input.extmarks.clear() setStore("extmarkToPartIndex", new Map()) @@ -448,14 +495,11 @@ export function Prompt(props: PromptProps) { }) setStore("extmarkToPartIndex", new Map()) }, - submit() { - submit() - }, }) async function submit() { if (props.disabled) return - if (autocomplete?.visible) return + if (autocomplete.visible) return if (!store.prompt.input) return const trimmed = store.prompt.input.trim() if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") { @@ -476,6 +520,8 @@ export function Prompt(props: PromptProps) { const messageID = Identifier.ascending("message") let inputText = store.prompt.input + // IDE selection is displayed in footer only - not injected into message + // Expand pasted text inline before submitting const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId) const sortedExtmarks = allExtmarks.sort((a: { start: number }, b: { start: number }) => b.start - a.start) @@ -495,9 +541,6 @@ export function Prompt(props: PromptProps) { // Filter out text parts (pasted content) since they're now expanded inline const nonTextParts = store.prompt.parts.filter((part) => part.type !== "text") - // Capture mode before it gets reset - const currentMode = store.mode - if (store.mode === "shell") { sdk.client.session.shell({ sessionID, @@ -539,6 +582,12 @@ export function Prompt(props: PromptProps) { type: "text", text: inputText, }, + ...(local.selection.current()?.text ? [{ + id: Identifier.ascending("part"), + type: "text" as const, + text: `\n\n[IDE Selection: ${local.selection.current()!.filePath.split("/").pop() || local.selection.current()!.filePath}:${local.selection.current()!.selection.start.line + 1}-${local.selection.current()!.selection.end.line + 1}]\n\`\`\`\n${local.selection.current()!.text}\n\`\`\``, + synthetic: true, + }] : []), ...nonTextParts.map((x) => ({ id: Identifier.ascending("part"), ...x, @@ -546,16 +595,14 @@ export function Prompt(props: PromptProps) { ], }) } - history.append({ - ...store.prompt, - mode: currentMode, - }) + history.append(store.prompt) input.extmarks.clear() setStore("prompt", { input: "", parts: [], }) setStore("extmarkToPartIndex", new Map()) + ideSelectionExtmarkId = null props.onSubmit?.() // temporary hack to make sure the message is sent @@ -715,8 +762,8 @@ export function Prompt(props: PromptProps) { >