diff --git a/STATS.md b/STATS.md
index e35377dbe..aedfa87ce 100644
--- a/STATS.md
+++ b/STATS.md
@@ -123,3 +123,5 @@
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
+| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
+| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
diff --git a/bun.lock b/bun.lock
index 3a0ce8d35..5d8d086f5 100644
--- a/bun.lock
+++ b/bun.lock
@@ -39,7 +39,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
- "version": "0.15.28",
+ "version": "0.15.29",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -66,7 +66,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
- "version": "0.15.28",
+ "version": "0.15.29",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -90,7 +90,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
- "version": "0.15.28",
+ "version": "0.15.29",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -111,7 +111,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
- "version": "0.15.28",
+ "version": "0.15.29",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -152,7 +152,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
- "version": "0.15.28",
+ "version": "0.15.29",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -168,7 +168,7 @@
},
"packages/opencode": {
"name": "opencode",
- "version": "0.15.28",
+ "version": "0.15.29",
"bin": {
"opencode": "./bin/opencode",
},
@@ -248,7 +248,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
- "version": "0.15.28",
+ "version": "0.15.29",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -268,7 +268,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
- "version": "0.15.28",
+ "version": "0.15.29",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -279,7 +279,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
- "version": "0.15.28",
+ "version": "0.15.29",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -292,7 +292,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
- "version": "0.15.28",
+ "version": "0.15.29",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/precision-diffs": "catalog:",
@@ -315,7 +315,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
- "version": "0.15.28",
+ "version": "0.15.29",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
diff --git a/packages/console/app/package.json b/packages/console/app/package.json
index 9bd3ae9bf..88602cc65 100644
--- a/packages/console/app/package.json
+++ b/packages/console/app/package.json
@@ -7,7 +7,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
- "version": "0.15.28"
+ "version": "0.15.29"
},
"dependencies": {
"@ibm/plex": "6.4.1",
diff --git a/packages/console/app/src/routes/api/enterprise.ts b/packages/console/app/src/routes/api/enterprise.ts
index d937be543..e33737d57 100644
--- a/packages/console/app/src/routes/api/enterprise.ts
+++ b/packages/console/app/src/routes/api/enterprise.ts
@@ -25,19 +25,15 @@ export async function POST(event: APIEvent) {
// Create email content
const emailContent = `
-New Enterprise Inquiry
-
-Name: ${body.name}
-Role: ${body.role}
-Email: ${body.email}
-
-Message:
-${body.message}
- `.trim()
+${body.message}
+--
+${body.name}
+${body.role}
+${body.email}`.trim()
// Send email using AWS SES
await AWS.sendEmail({
- to: "enterprise@opencode.ai",
+ to: "contact@anoma.ly",
subject: `Enterprise Inquiry from ${body.name}`,
body: emailContent,
})
diff --git a/packages/console/app/src/routes/enterprise/index.tsx b/packages/console/app/src/routes/enterprise/index.tsx
index 9599ad38b..5bca6f389 100644
--- a/packages/console/app/src/routes/enterprise/index.tsx
+++ b/packages/console/app/src/routes/enterprise/index.tsx
@@ -1,6 +1,6 @@
import "./index.css"
import { Title, Meta } from "@solidjs/meta"
-import { createSignal } from "solid-js"
+import { createSignal, Show } from "solid-js"
import { Header } from "~/component/header"
import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal"
@@ -64,60 +64,96 @@ export default function Enterprise() {
Your code is yours
- OpenCode operates securely inside your organization with no data or context stored and no licensing restrictions or ownership claims. Start a trial with your team today, then scale confidently with enterprise-grade features including SSO, private registries, and self-hosting.
-
-
- Let us know and how we can help.
+ OpenCode operates securely inside your organization with no data or context stored
+ and no licensing restrictions or ownership claims. Start a trial with your team
+ , then deploy it across your organization by integrating it with your SSO and internal AI gateway.
+
Let us know and how we can help.
+
-
+
+ fill="currentColor"
+ />
-
-
- Thanks to OpenCode, we found a way to create software to track all our assets — even the imaginary ones.
-
+ Thanks to OpenCode, we found a way to create software to track all our assets —
+ even the imaginary ones.
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
@@ -130,7 +166,7 @@ export default function Enterprise() {
type="text"
required
value={formData().name}
- onInput={handleInputChange('name')}
+ onInput={handleInputChange("name")}
placeholder="Jeff Bezos"
/>
@@ -142,7 +178,7 @@ export default function Enterprise() {
type="text"
required
value={formData().role}
- onInput={handleInputChange('role')}
+ onInput={handleInputChange("role")}
placeholder="Executive Chairman"
/>
@@ -154,27 +190,25 @@ export default function Enterprise() {
type="email"
required
value={formData().email}
- onInput={handleInputChange('email')}
+ onInput={handleInputChange("email")}
placeholder="jeff@amazon.com"
/>
- What problem are you trying to
- solve?
+ What problem are you trying to solve?
-
- {isSubmitting() ? 'Sending...' : 'Send'}
+
+ {isSubmitting() ? "Sending..." : "Send"}
@@ -194,53 +228,32 @@ export default function Enterprise() {
-
- No. OpenCode never stores your code or context data. All
- processing happens locally or directly with your AI provider.
+
+ OpenCode Enterprise is for organizations that want to ensure that their code and
+ data never leaves their infrastructure. It can do this by using a centralized
+ config that integrates with your SSO and internal AI gateway.
-
- You do. All code produced is yours, with no licensing
- restrictions or ownership claims.
+
+ Simply start with an internal trial with your team. OpenCode by default does not
+ store your code or context data, making it easy to get started. Then contact us to
+ discuss pricing and implementation options.
-
- Simply install and run an internal trial with your team. Since
- OpenCode doesn’t store any data, your developers can get
- started right away.
+
+ We offer per-seat enterprise pricing. If you have your own LLM gateway, we do not
+ charge for tokens used. For further details, contact us for a custom quote based
+ on your organization's needs.
-
- By default, sharing is disabled. If enabled, conversations are
- sent to our share service and cached through our CDN. For
- enterprise use, we recommend disabling or self-hosting this
- feature.
-
-
-
-
- Yes. Enterprise deployments can include SSO integration so all
- sessions and shared conversations are protected by your
- authentication system.
-
-
-
-
- Absolutely. You can fully self-host OpenCode, including the
- share feature, ensuring that data and pages are accessible
- only after authentication.
-
-
-
-
- Contact us to discuss pricing, implementation, and enterprise
- options like SSO, private registries, and self-hosting.
+
+ Yes. OpenCode does not store your code or context data. All processing happens
+ locally or through direct API calls to your AI provider. With central config and
+ SSO integration, your data remains secure within your organization's
+ infrastructure.
diff --git a/packages/console/app/src/routes/workspace/common.tsx b/packages/console/app/src/routes/workspace/common.tsx
index 03a40b579..15839666a 100644
--- a/packages/console/app/src/routes/workspace/common.tsx
+++ b/packages/console/app/src/routes/workspace/common.tsx
@@ -16,7 +16,7 @@ export function formatDateForTable(date: Date) {
minute: "2-digit",
hour12: true,
}
- return date.toLocaleDateString("en-GB", options).replace(",", ",")
+ return date.toLocaleDateString(undefined, options).replace(",", ",")
}
export function formatDateUTC(date: Date) {
diff --git a/packages/console/core/package.json b/packages/console/core/package.json
index b084bcd22..f8bd71d0d 100644
--- a/packages/console/core/package.json
+++ b/packages/console/core/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
- "version": "0.15.28",
+ "version": "0.15.29",
"private": true,
"type": "module",
"dependencies": {
diff --git a/packages/console/function/package.json b/packages/console/function/package.json
index 179ee007d..6f13c9481 100644
--- a/packages/console/function/package.json
+++ b/packages/console/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
- "version": "0.15.28",
+ "version": "0.15.29",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json
index 5aa23d2cc..b01703b4d 100644
--- a/packages/console/mail/package.json
+++ b/packages/console/mail/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
- "version": "0.15.28",
+ "version": "0.15.29",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index cf6057880..400815215 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
- "version": "0.15.28",
+ "version": "0.15.29",
"description": "",
"type": "module",
"scripts": {
diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx
index ac6b6f9c8..5216c4272 100644
--- a/packages/desktop/src/pages/index.tsx
+++ b/packages/desktop/src/pages/index.tsx
@@ -36,6 +36,7 @@ import { ProgressCircle } from "@/components/progress-circle"
import { Message, Part } from "@/components/message"
import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
import { DiffChanges } from "@/components/diff-changes"
+import { Markdown } from "@/components/markdown"
export default function Page() {
const local = useLocal()
@@ -491,8 +492,8 @@ export default function Page() {
-
-
+
+
{(activeSession) => (
-
+
1}>
-
+
{(message) => {
const countLines = (text: string) => {
@@ -648,16 +652,12 @@ export default function Page() {
(m) => m.role === "assistant" && m.parentID == message.id,
) as AssistantMessageType[]
})
- const working = createMemo(() => {
- const last = assistantMessages()[assistantMessages().length - 1]
- if (!last) return false
- return !last.time.completed
- })
+ const working = createMemo(() => !summary())
return (
{/* Title */}
@@ -670,104 +670,14 @@ export default function Page() {
- {/* Response */}
-
-
-
-
-
-
- Hide steps
- Show steps
-
-
-
-
-
-
-
-
- {(assistantMessage) => {
- const parts = createMemo(() => sync.data.part[assistantMessage.id])
- return
- }}
-
-
-
-
-
- {(_) => {
- const lastMessageWithText = createMemo(() =>
- assistantMessages().findLast((m) => {
- const parts = sync.data.part[m.id]
- return parts?.find((p) => p.type === "text")
- }),
- )
- const lastMessageWithReasoning = createMemo(() =>
- assistantMessages().findLast((m) => {
- const parts = sync.data.part[m.id]
- return parts?.find((p) => p.type === "reasoning")
- }),
- )
- const lastMessageWithTool = createMemo(() =>
- assistantMessages().findLast((m) => {
- const parts = sync.data.part[m.id]
- return parts?.find(
- (p) => p.type === "tool" && p.state.status === "completed",
- )
- }),
- )
- return (
-
-
-
- {(last) => {
- const lastTextPart = createMemo(() =>
- sync.data.part[last().id].findLast((p) => p.type === "text"),
- )
- return (
-
- )
- }}
-
-
- {(last) => {
- const lastReasoningPart = createMemo(() =>
- sync.data.part[last().id].findLast(
- (p) => p.type === "reasoning",
- ),
- )
- return (
-
- )
- }}
-
-
-
- {(last) => {
- const lastToolPart = createMemo(() =>
- sync.data.part[last().id].findLast(
- (p) => p.type === "tool" && p.state.status === "completed",
- ),
- )
- return
- }}
-
-
- )
- }}
-
-
{/* Summary */}
Summary
-
{summary()}
+
+
+
@@ -817,6 +727,119 @@ export default function Page() {
+ {/* Response */}
+
+
+
+ {(_) => {
+ const items = createMemo(() =>
+ assistantMessages().flatMap((m) => sync.data.part[m.id]),
+ )
+ const finishedItems = createMemo(() =>
+ items().filter(
+ (p) =>
+ (p?.type === "text" && p.time?.end) ||
+ (p?.type === "reasoning" && p.time?.end) ||
+ (p?.type === "tool" && p.state.status === "completed"),
+ ),
+ )
+
+ const MINIMUM_DELAY = 800
+ const [visibleCount, setVisibleCount] = createSignal(1)
+
+ createEffect(() => {
+ const total = finishedItems().length
+ if (total > visibleCount()) {
+ const timer = setTimeout(() => {
+ setVisibleCount((prev) => prev + 1)
+ }, MINIMUM_DELAY)
+ onCleanup(() => clearTimeout(timer))
+ } else if (total < visibleCount()) {
+ setVisibleCount(total)
+ }
+ })
+
+ const translateY = createMemo(() => {
+ const total = visibleCount()
+ if (total < 2) return "0px"
+ return `-${(total - 2) * 48 - 8}px`
+ })
+
+ return (
+
+ )
+ }}
+
+
+
+
+
+
+
+ Hide steps
+ Show steps
+
+
+
+
+
+
+
+
+ {(assistantMessage) => {
+ const parts = createMemo(
+ () => sync.data.part[assistantMessage.id],
+ )
+ return
+ }}
+
+
+
+
+
+
+
)
}}
@@ -857,14 +880,7 @@ export default function Page() {
})()}
-
+
{
inputRef = el
diff --git a/packages/function/package.json b/packages/function/package.json
index fabb7168a..eb1858092 100644
--- a/packages/function/package.json
+++ b/packages/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
- "version": "0.15.28",
+ "version": "0.15.29",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index c63e9c218..e0e7c7d4d 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "version": "0.15.28",
+ "version": "0.15.29",
"name": "opencode",
"type": "module",
"private": true,
diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts
index 1bc20de32..7c873ae50 100644
--- a/packages/opencode/src/cli/error.ts
+++ b/packages/opencode/src/cli/error.ts
@@ -1,3 +1,4 @@
+import { ConfigMarkdown } from "@/config/markdown"
import { Config } from "../config/config"
import { MCP } from "../mcp"
import { UI } from "./ui"
@@ -7,16 +8,22 @@ export function FormatError(input: unknown) {
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
if (Config.JsonError.isInstance(input)) {
return (
- `Config file at ${input.data.path} is not valid JSON(C)` + (input.data.message ? `: ${input.data.message}` : "")
+ `Config file at ${input.data.path} is not valid JSON(C)` +
+ (input.data.message ? `: ${input.data.message}` : "")
)
}
if (Config.ConfigDirectoryTypoError.isInstance(input)) {
return `Directory "${input.data.dir}" in ${input.data.path} is not valid. Use "${input.data.suggestion}" instead. This is a common typo.`
}
+ if (ConfigMarkdown.FrontmatterError.isInstance(input)) {
+ return `Failed to parse frontmatter in ${input.data.path}:\n${input.data.message}`
+ }
if (Config.InvalidError.isInstance(input))
return [
- `Config file at ${input.data.path} is invalid` + (input.data.message ? `: ${input.data.message}` : ""),
- ...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []),
+ `Config file at ${input.data.path} is invalid` +
+ (input.data.message ? `: ${input.data.message}` : ""),
+ ...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ??
+ []),
].join("\n")
if (UI.CancelledError.isInstance(input)) return ""
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 8d105c836..94beb23db 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -9,7 +9,6 @@ import { Global } from "../global"
import fs from "fs/promises"
import { lazy } from "../util/lazy"
import { NamedError } from "../util/error"
-import matter from "gray-matter"
import { Flag } from "../flag/flag"
import { Auth } from "../auth"
import {
@@ -21,6 +20,7 @@ import { Instance } from "../project/instance"
import { LSPServer } from "../lsp/server"
import { BunProc } from "@/bun"
import { Installation } from "@/installation"
+import { ConfigMarkdown } from "./markdown"
export namespace Config {
const log = Log.create({ service: "config" })
@@ -175,8 +175,7 @@ export namespace Config {
dot: true,
cwd: dir,
})) {
- const content = await Bun.file(item).text()
- const md = matter(content)
+ const md = await ConfigMarkdown.parse(item)
if (!md.data) continue
const name = (() => {
@@ -215,8 +214,7 @@ export namespace Config {
dot: true,
cwd: dir,
})) {
- const content = await Bun.file(item).text()
- const md = matter(content)
+ const md = await ConfigMarkdown.parse(item)
if (!md.data) continue
// Extract relative path from agent folder for nested agents
@@ -258,8 +256,7 @@ export namespace Config {
dot: true,
cwd: dir,
})) {
- const content = await Bun.file(item).text()
- const md = matter(content)
+ const md = await ConfigMarkdown.parse(item)
if (!md.data) continue
const config = {
@@ -303,6 +300,14 @@ export namespace Config {
.optional()
.describe("Environment variables to set when running the MCP server"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
+ timeout: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe(
+ "Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
+ ),
})
.strict()
.meta({
@@ -318,6 +323,14 @@ export namespace Config {
.record(z.string(), z.string())
.optional()
.describe("Headers to send with the request"),
+ timeout: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe(
+ "Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
+ ),
})
.strict()
.meta({
diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts
index a4dcbf5d4..3e84bbf43 100644
--- a/packages/opencode/src/config/markdown.ts
+++ b/packages/opencode/src/config/markdown.ts
@@ -1,3 +1,7 @@
+import { NamedError } from "@/util/error"
+import matter from "gray-matter"
+import { z } from "zod"
+
export namespace ConfigMarkdown {
export const FILE_REGEX = /(? {
if (!clients.includes(client)) return
- const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
+
+ const wait = waitForDiagnostics
+ ? client.waitForDiagnostics({ path: input })
+ : Promise.resolve()
await client.notify.open({ path: input })
return wait
}).catch((err) => {
diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts
index 4f0b90fb8..d0fb11e94 100644
--- a/packages/opencode/src/lsp/server.ts
+++ b/packages/opencode/src/lsp/server.ts
@@ -54,7 +54,17 @@ export namespace LSPServer {
export const Deno: Info = {
id: "deno",
- root: NearestRoot(["deno.json", "deno.jsonc"]),
+ root: async (file) => {
+ const files = Filesystem.up({
+ targets: ["deno.json", "deno.jsonc"],
+ start: path.dirname(file),
+ stop: Instance.directory,
+ })
+ const first = await files.next()
+ await files.return()
+ if (!first.value) return undefined
+ return path.dirname(first.value)
+ },
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"],
async spawn(root) {
const deno = Bun.which("deno")
@@ -78,7 +88,9 @@ export namespace LSPServer {
),
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
async spawn(root) {
- const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
+ const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(
+ () => {},
+ )
if (!tsserver) return
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
cwd: root,
@@ -101,7 +113,13 @@ export namespace LSPServer {
export const Vue: Info = {
id: "vue",
extensions: [".vue"],
- root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
+ root: NearestRoot([
+ "package-lock.json",
+ "bun.lockb",
+ "bun.lock",
+ "pnpm-lock.yaml",
+ "yarn.lock",
+ ]),
async spawn(root) {
let binary = Bun.which("vue-language-server")
const args: string[] = []
@@ -149,17 +167,31 @@ export namespace LSPServer {
export const ESLint: Info = {
id: "eslint",
- root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
+ root: NearestRoot([
+ "package-lock.json",
+ "bun.lockb",
+ "bun.lock",
+ "pnpm-lock.yaml",
+ "yarn.lock",
+ ]),
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
async spawn(root) {
const eslint = await Bun.resolve("eslint", Instance.directory).catch(() => {})
if (!eslint) return
log.info("spawning eslint server")
- const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
+ const serverPath = path.join(
+ Global.Path.bin,
+ "vscode-eslint",
+ "server",
+ "out",
+ "eslintServer.js",
+ )
if (!(await Bun.file(serverPath).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading and building VS Code ESLint server")
- const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
+ const response = await fetch(
+ "https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip",
+ )
if (!response.ok) return
const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
@@ -284,12 +316,25 @@ export namespace LSPServer {
export const Pyright: Info = {
id: "pyright",
extensions: [".py", ".pyi"],
- root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
+ root: NearestRoot([
+ "pyproject.toml",
+ "setup.py",
+ "setup.cfg",
+ "requirements.txt",
+ "Pipfile",
+ "pyrightconfig.json",
+ ]),
async spawn(root) {
let binary = Bun.which("pyright-langserver")
const args = []
if (!binary) {
- const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
+ const js = path.join(
+ Global.Path.bin,
+ "node_modules",
+ "pyright",
+ "dist",
+ "pyright-langserver.js",
+ )
if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "pyright"], {
@@ -307,9 +352,11 @@ export namespace LSPServer {
const initialization: Record = {}
- const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
- (p): p is string => p !== undefined,
- )
+ const potentialVenvPaths = [
+ process.env["VIRTUAL_ENV"],
+ path.join(root, ".venv"),
+ path.join(root, "venv"),
+ ].filter((p): p is string => p !== undefined)
for (const venvPath of potentialVenvPaths) {
const isWindows = process.platform === "win32"
const potentialPythonPath = isWindows
@@ -360,7 +407,9 @@ export namespace LSPServer {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading elixir-ls from GitHub releases")
- const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
+ const response = await fetch(
+ "https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip",
+ )
if (!response.ok) return
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
await Bun.file(zipPath).write(response)
@@ -410,7 +459,9 @@ export namespace LSPServer {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading zls from GitHub releases")
- const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
+ const releaseResponse = await fetch(
+ "https://api.github.com/repos/zigtools/zls/releases/latest",
+ )
if (!releaseResponse.ok) {
log.error("Failed to fetch zls release info")
return
@@ -585,7 +636,13 @@ export namespace LSPServer {
export const Clangd: Info = {
id: "clangd",
- root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
+ root: NearestRoot([
+ "compile_commands.json",
+ "compile_flags.txt",
+ ".clangd",
+ "CMakeLists.txt",
+ "Makefile",
+ ]),
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
async spawn(root) {
let bin = Bun.which("clangd", {
@@ -595,7 +652,9 @@ export namespace LSPServer {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading clangd from GitHub releases")
- const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
+ const releaseResponse = await fetch(
+ "https://api.github.com/repos/clangd/clangd/releases/latest",
+ )
if (!releaseResponse.ok) {
log.error("Failed to fetch clangd release info")
return
@@ -664,12 +723,24 @@ export namespace LSPServer {
export const Svelte: Info = {
id: "svelte",
extensions: [".svelte"],
- root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
+ root: NearestRoot([
+ "package-lock.json",
+ "bun.lockb",
+ "bun.lock",
+ "pnpm-lock.yaml",
+ "yarn.lock",
+ ]),
async spawn(root) {
let binary = Bun.which("svelteserver")
const args: string[] = []
if (!binary) {
- const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
+ const js = path.join(
+ Global.Path.bin,
+ "node_modules",
+ "svelte-language-server",
+ "bin",
+ "server.js",
+ )
if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], {
@@ -704,9 +775,17 @@ export namespace LSPServer {
export const Astro: Info = {
id: "astro",
extensions: [".astro"],
- root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
+ root: NearestRoot([
+ "package-lock.json",
+ "bun.lockb",
+ "bun.lock",
+ "pnpm-lock.yaml",
+ "yarn.lock",
+ ]),
async spawn(root) {
- const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
+ const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(
+ () => {},
+ )
if (!tsserver) {
log.info("typescript not found, required for Astro language server")
return
@@ -716,7 +795,14 @@ export namespace LSPServer {
let binary = Bun.which("astro-ls")
const args: string[] = []
if (!binary) {
- const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
+ const js = path.join(
+ Global.Path.bin,
+ "node_modules",
+ "@astrojs",
+ "language-server",
+ "bin",
+ "nodeServer.js",
+ )
if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
@@ -794,7 +880,9 @@ export namespace LSPServer {
.then(({ stdout }) => stdout.toString().trim())
const launcherJar = path.join(launcherDir, jarFileName)
if (!(await fs.exists(launcherJar))) {
- log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
+ log.error(
+ `Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`,
+ )
return
}
const configFile = path.join(
@@ -860,7 +948,9 @@ export namespace LSPServer {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading lua-language-server from GitHub releases")
- const releaseResponse = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest")
+ const releaseResponse = await fetch(
+ "https://api.github.com/repos/LuaLS/lua-language-server/releases/latest",
+ )
if (!releaseResponse.ok) {
log.error("Failed to fetch lua-language-server release info")
return
@@ -897,7 +987,9 @@ export namespace LSPServer {
const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}`
if (!supportedCombos.includes(assetSuffix)) {
- log.error(`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`)
+ log.error(
+ `Platform ${platform} and architecture ${arch} is not supported by lua-language-server`,
+ )
return
}
@@ -920,7 +1012,10 @@ export namespace LSPServer {
// Unlike zls which is a single self-contained binary,
// lua-language-server needs supporting files (meta/, locale/, etc.)
// Extract entire archive to dedicated directory to preserve all files
- const installDir = path.join(Global.Path.bin, `lua-language-server-${lualsArch}-${lualsPlatform}`)
+ const installDir = path.join(
+ Global.Path.bin,
+ `lua-language-server-${lualsArch}-${lualsPlatform}`,
+ )
// Remove old installation if exists
const stats = await fs.stat(installDir).catch(() => undefined)
@@ -945,7 +1040,11 @@ export namespace LSPServer {
await fs.rm(tempPath, { force: true })
// Binary is located in bin/ subdirectory within the extracted archive
- bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
+ bin = path.join(
+ installDir,
+ "bin",
+ "lua-language-server" + (platform === "win32" ? ".exe" : ""),
+ )
if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract lua-language-server binary")
@@ -954,7 +1053,9 @@ export namespace LSPServer {
if (platform !== "win32") {
const ok = await $`chmod +x ${bin}`.quiet().catch((error) => {
- log.error("Failed to set executable permission for lua-language-server binary", { error })
+ log.error("Failed to set executable permission for lua-language-server binary", {
+ error,
+ })
})
if (!ok) return
}
diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts
index e60c6a827..9a18c63a0 100644
--- a/packages/opencode/src/mcp/index.ts
+++ b/packages/opencode/src/mcp/index.ts
@@ -199,7 +199,7 @@ export namespace MCP {
}
}
- const result = await withTimeout(mcpClient.tools(), 5000).catch(() => {})
+ const result = await withTimeout(mcpClient.tools(), mcp.timeout ?? 5000).catch(() => { })
if (!result) {
await mcpClient.close()
status = {
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 40d8dcd49..cf9899b92 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -56,6 +56,7 @@ export namespace SessionPrompt {
const log = Log.create({ service: "session.prompt" })
export const OUTPUT_TOKEN_MAX = 32_000
const MAX_RETRIES = 10
+ const DOOM_LOOP_THRESHOLD = 3
export const Event = {
Idle: Bus.event(
@@ -360,21 +361,21 @@ export namespace SessionPrompt {
const stop = await SessionRetry.sleep(delayMs, abort.signal)
.then(() => false)
.catch((error) => {
+ let err = error
if (error instanceof DOMException && error.name === "AbortError") {
- const err = new MessageV2.AbortedError(
+ err = new MessageV2.AbortedError(
{ message: error.message },
{
cause: error,
},
).toObject()
- result.info.error = err
- Bus.publish(Session.Event.Error, {
- sessionID: result.info.sessionID,
- error: result.info.error,
- })
- return true
}
- throw error
+ result.info.error = err
+ Bus.publish(Session.Event.Error, {
+ sessionID: result.info.sessionID,
+ error: result.info.error,
+ })
+ return true
})
if (stop) break
@@ -533,7 +534,6 @@ export namespace SessionPrompt {
args,
},
)
- item.parameters.parse(args)
const result = await item.execute(args, {
sessionID: input.sessionID,
abort: options.abortSignal!,
@@ -617,7 +617,7 @@ export namespace SessionPrompt {
return {
title: "",
- metadata: {},
+ metadata: result.metadata ?? {},
output,
}
}
@@ -1070,6 +1070,32 @@ export namespace SessionPrompt {
metadata: value.providerMetadata,
})
toolcalls[value.toolCallId] = part as MessageV2.ToolPart
+
+ const parts = await Session.getParts(assistantMsg.id)
+ const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD)
+ if (
+ lastThree.length === DOOM_LOOP_THRESHOLD &&
+ lastThree.every(
+ (p) =>
+ p.type === "tool" &&
+ p.tool === value.toolName &&
+ p.state.status !== "pending" &&
+ JSON.stringify(p.state.input) === JSON.stringify(value.input),
+ )
+ ) {
+ await Permission.ask({
+ type: "doom-loop",
+ pattern: value.toolName,
+ sessionID: assistantMsg.sessionID,
+ messageID: assistantMsg.id,
+ callID: value.toolCallId,
+ title: `Possible doom loop: "${value.toolName}" called ${DOOM_LOOP_THRESHOLD} times with identical arguments`,
+ metadata: {
+ tool: value.toolName,
+ input: value.input,
+ },
+ })
+ }
}
break
}
diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts
index c7a28c516..f826d0c99 100644
--- a/packages/opencode/src/tool/tool.ts
+++ b/packages/opencode/src/tool/tool.ts
@@ -42,8 +42,13 @@ export namespace Tool {
return {
id,
init: async () => {
- if (init instanceof Function) return init()
- return init
+ const toolInfo = init instanceof Function ? await init() : init
+ const execute = toolInfo.execute
+ toolInfo.execute = (args, ctx) => {
+ toolInfo.parameters.parse(args)
+ return execute(args, ctx)
+ }
+ return toolInfo
},
}
}
diff --git a/packages/plugin/package.json b/packages/plugin/package.json
index 2409aa979..4c35c75c1 100644
--- a/packages/plugin/package.json
+++ b/packages/plugin/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
- "version": "0.15.28",
+ "version": "0.15.29",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json
index ba6a1b8b7..44eabd4f4 100644
--- a/packages/sdk/js/package.json
+++ b/packages/sdk/js/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
- "version": "0.15.28",
+ "version": "0.15.29",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts
index ed1c9d136..1981b63f8 100644
--- a/packages/sdk/js/src/gen/types.gen.ts
+++ b/packages/sdk/js/src/gen/types.gen.ts
@@ -287,6 +287,10 @@ export type McpLocalConfig = {
* Enable or disable the MCP server on startup
*/
enabled?: boolean
+ /**
+ * Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.
+ */
+ timeout?: number
}
export type McpRemoteConfig = {
@@ -308,6 +312,10 @@ export type McpRemoteConfig = {
headers?: {
[key: string]: string
}
+ /**
+ * Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.
+ */
+ timeout?: number
}
/**
diff --git a/packages/slack/package.json b/packages/slack/package.json
index e41e63cd9..28df57736 100644
--- a/packages/slack/package.json
+++ b/packages/slack/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
- "version": "0.15.28",
+ "version": "0.15.29",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
diff --git a/packages/tui/cmd/opencode/main.go b/packages/tui/cmd/opencode/main.go
index 22841fc89..3a7d1848a 100644
--- a/packages/tui/cmd/opencode/main.go
+++ b/packages/tui/cmd/opencode/main.go
@@ -13,9 +13,11 @@ import (
flag "github.com/spf13/pflag"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode-sdk-go/option"
+ "github.com/sst/opencode-sdk-go/packages/ssestream"
"github.com/sst/opencode/internal/api"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/clipboard"
+ "github.com/sst/opencode/internal/decoders"
"github.com/sst/opencode/internal/tui"
"github.com/sst/opencode/internal/util"
"golang.org/x/sync/errgroup"
@@ -61,6 +63,11 @@ func main() {
}
}
+ // Register custom SSE decoder to handle large events (>32MB)
+ // This is a workaround for the bufio.Scanner token size limit in the auto-generated SDK
+ // See: packages/tui/internal/decoders/decoder.go
+ ssestream.RegisterDecoder("text/event-stream", decoders.NewUnboundedDecoder)
+
httpClient := opencode.NewClient(
option.WithBaseURL(url),
)
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go
index 708b92577..e0f1d9920 100644
--- a/packages/tui/internal/app/app.go
+++ b/packages/tui/internal/app/app.go
@@ -490,19 +490,7 @@ func (a *App) InitializeProvider() tea.Cmd {
}
}
- // Priority 2: Config file model setting
- if selectedProvider == nil && a.Config.Model != "" {
- if provider, model := findModelByFullID(providers, a.Config.Model); provider != nil &&
- model != nil {
- selectedProvider = provider
- selectedModel = model
- slog.Debug("Selected model from config", "provider", provider.ID, "model", model.ID)
- } else {
- slog.Debug("Config model not found", "model", a.Config.Model)
- }
- }
-
- // Priority 3: Current agent's preferred model
+ // Priority 2: Current agent's preferred model
if selectedProvider == nil && a.Agent().Model.ModelID != "" {
if provider, model := findModelByProviderAndModelID(providers, a.Agent().Model.ProviderID, a.Agent().Model.ModelID); provider != nil &&
model != nil {
@@ -522,6 +510,18 @@ func (a *App) InitializeProvider() tea.Cmd {
}
}
+ // Priority 3: Config file model setting
+ if selectedProvider == nil && a.Config.Model != "" {
+ if provider, model := findModelByFullID(providers, a.Config.Model); provider != nil &&
+ model != nil {
+ selectedProvider = provider
+ selectedModel = model
+ slog.Debug("Selected model from config", "provider", provider.ID, "model", model.ID)
+ } else {
+ slog.Debug("Config model not found", "model", a.Config.Model)
+ }
+ }
+
// Priority 4: Recent model usage (most recently used model)
if selectedProvider == nil && len(a.State.RecentlyUsedModels) > 0 {
recentUsage := a.State.RecentlyUsedModels[0] // Most recent is first
diff --git a/packages/tui/internal/app/app_test.go b/packages/tui/internal/app/app_test.go
index 9260a9915..e716d4376 100644
--- a/packages/tui/internal/app/app_test.go
+++ b/packages/tui/internal/app/app_test.go
@@ -226,3 +226,79 @@ func TestFindProviderByID(t *testing.T) {
})
}
}
+
+// TestModelSelectionPriority tests the priority order for model selection
+func TestModelSelectionPriority(t *testing.T) {
+ providers := []opencode.Provider{
+ {
+ ID: "anthropic",
+ Models: map[string]opencode.Model{
+ "claude-opus": {ID: "claude-opus"},
+ },
+ },
+ {
+ ID: "openai",
+ Models: map[string]opencode.Model{
+ "gpt-4": {ID: "gpt-4"},
+ },
+ },
+ }
+
+ tests := []struct {
+ name string
+ agentProviderID string
+ agentModelID string
+ configModel string
+ expectedProviderID string
+ expectedModelID string
+ description string
+ }{
+ {
+ name: "agent model takes priority over config",
+ agentProviderID: "openai",
+ agentModelID: "gpt-4",
+ configModel: "anthropic/claude-opus",
+ expectedProviderID: "openai",
+ expectedModelID: "gpt-4",
+ description: "When agent specifies a model, it should be used even if config has a different model",
+ },
+ {
+ name: "config model used when agent has no model",
+ agentProviderID: "",
+ agentModelID: "",
+ configModel: "anthropic/claude-opus",
+ expectedProviderID: "anthropic",
+ expectedModelID: "claude-opus",
+ description: "When agent has no model specified, config model should be used as fallback",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var selectedProvider *opencode.Provider
+ var selectedModel *opencode.Model
+
+ // Simulate priority 2: Agent model check
+ if tt.agentModelID != "" {
+ selectedProvider, selectedModel = findModelByProviderAndModelID(providers, tt.agentProviderID, tt.agentModelID)
+ }
+
+ // Simulate priority 3: Config model fallback
+ if selectedProvider == nil && tt.configModel != "" {
+ selectedProvider, selectedModel = findModelByFullID(providers, tt.configModel)
+ }
+
+ if selectedProvider == nil || selectedModel == nil {
+ t.Fatalf("Expected to find model, but got nil - %s", tt.description)
+ }
+
+ if selectedProvider.ID != tt.expectedProviderID {
+ t.Errorf("Expected provider %s, got %s - %s", tt.expectedProviderID, selectedProvider.ID, tt.description)
+ }
+
+ if selectedModel.ID != tt.expectedModelID {
+ t.Errorf("Expected model %s, got %s - %s", tt.expectedModelID, selectedModel.ID, tt.description)
+ }
+ })
+ }
+}
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index fc5a21ad1..801545a88 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -504,7 +504,11 @@ func renderToolDetails(
base := styles.NewStyle().Background(backgroundColor)
text := base.Foreground(t.Text()).Bold(true).Render
muted := base.Foreground(t.TextMuted()).Render
- permissionContent = "Permission required to run this tool:\n\n"
+ if permission.Type == "doom-loop" {
+ permissionContent = permission.Title + "\n\n"
+ } else {
+ permissionContent = "Permission required to run this tool:\n\n"
+ }
permissionContent += text(
"enter ",
) + muted(
@@ -642,9 +646,9 @@ func renderToolDetails(
for _, item := range todos.([]any) {
todo := item.(map[string]any)
content := todo["content"]
- if content == nil {
- continue
- }
+ if content == nil {
+ continue
+ }
switch todo["status"] {
case "completed":
body += fmt.Sprintf("- [x] %s\n", content)
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index a09e809f0..3d52b84e5 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -656,6 +656,8 @@ func (m *messagesComponent) renderView() tea.Cmd {
case nil:
case opencode.AssistantMessageErrorMessageOutputLengthError:
error = "Message output length exceeded"
+ case opencode.AssistantMessageErrorAPIError:
+ error = err.Data.Message
case opencode.ProviderAuthError:
error = err.Data.Message
case opencode.MessageAbortedError:
diff --git a/packages/tui/internal/decoders/decoder.go b/packages/tui/internal/decoders/decoder.go
new file mode 100644
index 000000000..efb699202
--- /dev/null
+++ b/packages/tui/internal/decoders/decoder.go
@@ -0,0 +1,118 @@
+package decoders
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+
+ "github.com/sst/opencode-sdk-go/packages/ssestream"
+)
+
+// UnboundedDecoder is an SSE decoder that uses bufio.Reader instead of bufio.Scanner
+// to avoid the 32MB token size limit. This is a workaround for large SSE events until
+// the upstream Stainless SDK is fixed.
+//
+// This decoder handles SSE events of unlimited size by reading line-by-line with
+// bufio.Reader.ReadBytes('\n'), which dynamically grows the buffer as needed.
+type UnboundedDecoder struct {
+ reader *bufio.Reader
+ closer io.ReadCloser
+ evt ssestream.Event
+ err error
+}
+
+// NewUnboundedDecoder creates a new unbounded SSE decoder with a 1MB initial buffer size
+func NewUnboundedDecoder(rc io.ReadCloser) ssestream.Decoder {
+ reader := bufio.NewReaderSize(rc, 1024*1024) // 1MB initial buffer
+ return &UnboundedDecoder{
+ reader: reader,
+ closer: rc,
+ }
+}
+
+// Next reads and decodes the next SSE event from the stream
+func (d *UnboundedDecoder) Next() bool {
+ if d.err != nil {
+ return false
+ }
+
+ event := ""
+ data := bytes.NewBuffer(nil)
+
+ for {
+ line, err := d.reader.ReadBytes('\n')
+ if err != nil {
+ if err == io.EOF && len(line) == 0 {
+ return false
+ }
+ if err != io.EOF {
+ d.err = err
+ return false
+ }
+ }
+
+ // Remove trailing newline characters
+ line = bytes.TrimRight(line, "\r\n")
+
+ // Empty line indicates end of event
+ if len(line) == 0 {
+ if data.Len() > 0 || event != "" {
+ d.evt = ssestream.Event{
+ Type: event,
+ Data: data.Bytes(),
+ }
+ return true
+ }
+ continue
+ }
+
+ // Skip comments (lines starting with ':')
+ if line[0] == ':' {
+ continue
+ }
+
+ // Parse field
+ name, value, found := bytes.Cut(line, []byte(":"))
+ if !found {
+ // Field with no value
+ continue
+ }
+
+ // Remove leading space from value
+ if len(value) > 0 && value[0] == ' ' {
+ value = value[1:]
+ }
+
+ switch string(name) {
+ case "":
+ // An empty line in the form ": something" is a comment and should be ignored
+ continue
+ case "event":
+ event = string(value)
+ case "data":
+ _, d.err = data.Write(value)
+ if d.err != nil {
+ return false
+ }
+ _, d.err = data.WriteRune('\n')
+ if d.err != nil {
+ return false
+ }
+ }
+ }
+}
+
+// Event returns the current event
+func (d *UnboundedDecoder) Event() ssestream.Event {
+ return d.evt
+}
+
+// Close closes the underlying reader
+func (d *UnboundedDecoder) Close() error {
+ return d.closer.Close()
+}
+
+// Err returns any error that occurred during decoding
+func (d *UnboundedDecoder) Err() error {
+ return d.err
+}
diff --git a/packages/tui/internal/decoders/decoder_test.go b/packages/tui/internal/decoders/decoder_test.go
new file mode 100644
index 000000000..e5ad1d55a
--- /dev/null
+++ b/packages/tui/internal/decoders/decoder_test.go
@@ -0,0 +1,194 @@
+package decoders
+
+import (
+ "bytes"
+ "io"
+ "strings"
+ "testing"
+)
+
+func TestUnboundedDecoder_SmallEvent(t *testing.T) {
+ data := "event: test\ndata: hello world\n\n"
+ rc := io.NopCloser(strings.NewReader(data))
+ decoder := NewUnboundedDecoder(rc)
+
+ if !decoder.Next() {
+ t.Fatal("Expected Next() to return true")
+ }
+
+ evt := decoder.Event()
+ if evt.Type != "test" {
+ t.Errorf("Expected event type 'test', got '%s'", evt.Type)
+ }
+ if string(evt.Data) != "hello world\n" {
+ t.Errorf("Expected data 'hello world\\n', got '%s'", string(evt.Data))
+ }
+
+ if decoder.Next() {
+ t.Error("Expected Next() to return false at end of stream")
+ }
+
+ if err := decoder.Err(); err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+}
+
+func TestUnboundedDecoder_LargeEvent(t *testing.T) {
+ // Create a large event (50MB)
+ size := 50 * 1024 * 1024
+ largeData := strings.Repeat("x", size)
+
+ var buf bytes.Buffer
+ buf.WriteString("event: large\n")
+ buf.WriteString("data: ")
+ buf.WriteString(largeData)
+ buf.WriteString("\n\n")
+
+ rc := io.NopCloser(&buf)
+ decoder := NewUnboundedDecoder(rc)
+
+ if !decoder.Next() {
+ t.Fatal("Expected Next() to return true")
+ }
+
+ evt := decoder.Event()
+ if evt.Type != "large" {
+ t.Errorf("Expected event type 'large', got '%s'", evt.Type)
+ }
+
+ expectedData := largeData + "\n"
+ if string(evt.Data) != expectedData {
+ t.Errorf("Data size mismatch: expected %d, got %d", len(expectedData), len(evt.Data))
+ }
+
+ if decoder.Next() {
+ t.Error("Expected Next() to return false at end of stream")
+ }
+
+ if err := decoder.Err(); err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+}
+
+func TestUnboundedDecoder_MultipleEvents(t *testing.T) {
+ data := "event: first\ndata: data1\n\nevent: second\ndata: data2\n\n"
+ rc := io.NopCloser(strings.NewReader(data))
+ decoder := NewUnboundedDecoder(rc)
+
+ // First event
+ if !decoder.Next() {
+ t.Fatal("Expected Next() to return true for first event")
+ }
+ evt := decoder.Event()
+ if evt.Type != "first" {
+ t.Errorf("Expected event type 'first', got '%s'", evt.Type)
+ }
+ if string(evt.Data) != "data1\n" {
+ t.Errorf("Expected data 'data1\\n', got '%s'", string(evt.Data))
+ }
+
+ // Second event
+ if !decoder.Next() {
+ t.Fatal("Expected Next() to return true for second event")
+ }
+ evt = decoder.Event()
+ if evt.Type != "second" {
+ t.Errorf("Expected event type 'second', got '%s'", evt.Type)
+ }
+ if string(evt.Data) != "data2\n" {
+ t.Errorf("Expected data 'data2\\n', got '%s'", string(evt.Data))
+ }
+
+ // No more events
+ if decoder.Next() {
+ t.Error("Expected Next() to return false at end of stream")
+ }
+
+ if err := decoder.Err(); err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+}
+
+func TestUnboundedDecoder_MultilineData(t *testing.T) {
+ data := "event: multiline\ndata: line1\ndata: line2\ndata: line3\n\n"
+ rc := io.NopCloser(strings.NewReader(data))
+ decoder := NewUnboundedDecoder(rc)
+
+ if !decoder.Next() {
+ t.Fatal("Expected Next() to return true")
+ }
+
+ evt := decoder.Event()
+ if evt.Type != "multiline" {
+ t.Errorf("Expected event type 'multiline', got '%s'", evt.Type)
+ }
+
+ expectedData := "line1\nline2\nline3\n"
+ if string(evt.Data) != expectedData {
+ t.Errorf("Expected data '%s', got '%s'", expectedData, string(evt.Data))
+ }
+}
+
+func TestUnboundedDecoder_Comments(t *testing.T) {
+ data := ": this is a comment\nevent: test\n: another comment\ndata: hello\n\n"
+ rc := io.NopCloser(strings.NewReader(data))
+ decoder := NewUnboundedDecoder(rc)
+
+ if !decoder.Next() {
+ t.Fatal("Expected Next() to return true")
+ }
+
+ evt := decoder.Event()
+ if evt.Type != "test" {
+ t.Errorf("Expected event type 'test', got '%s'", evt.Type)
+ }
+ if string(evt.Data) != "hello\n" {
+ t.Errorf("Expected data 'hello\\n', got '%s'", string(evt.Data))
+ }
+}
+
+func TestUnboundedDecoder_NoEventType(t *testing.T) {
+ data := "data: hello\n\n"
+ rc := io.NopCloser(strings.NewReader(data))
+ decoder := NewUnboundedDecoder(rc)
+
+ if !decoder.Next() {
+ t.Fatal("Expected Next() to return true")
+ }
+
+ evt := decoder.Event()
+ if evt.Type != "" {
+ t.Errorf("Expected empty event type, got '%s'", evt.Type)
+ }
+ if string(evt.Data) != "hello\n" {
+ t.Errorf("Expected data 'hello\\n', got '%s'", string(evt.Data))
+ }
+}
+
+func BenchmarkUnboundedDecoder_LargeEvent(b *testing.B) {
+ // Create a 10MB event for benchmarking
+ size := 10 * 1024 * 1024
+ largeData := strings.Repeat("x", size)
+
+ var buf bytes.Buffer
+ buf.WriteString("event: bench\n")
+ buf.WriteString("data: ")
+ buf.WriteString(largeData)
+ buf.WriteString("\n\n")
+
+ data := buf.Bytes()
+
+ b.ResetTimer()
+ b.SetBytes(int64(len(data)))
+
+ for i := 0; i < b.N; i++ {
+ rc := io.NopCloser(bytes.NewReader(data))
+ decoder := NewUnboundedDecoder(rc)
+
+ if !decoder.Next() {
+ b.Fatal("Expected Next() to return true")
+ }
+
+ _ = decoder.Event()
+ }
+}
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 279443674..3a0bc3730 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -656,12 +656,26 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case opencode.EventListResponseEventSessionError:
switch err := msg.Properties.Error.AsUnion().(type) {
case nil:
+ // No error details provided
case opencode.ProviderAuthError:
slog.Error("Failed to authenticate with provider", "error", err.Data.Message)
return a, toast.NewErrorToast("Provider error: " + err.Data.Message)
case opencode.UnknownError:
slog.Error("Server error", "name", err.Name, "message", err.Data.Message)
return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name)))
+ case opencode.EventListResponseEventSessionErrorPropertiesErrorAPIError:
+ slog.Error("API error", "message", err.Data.Message, "statusCode", err.Data.StatusCode)
+ return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name)))
+ case opencode.MessageAbortedError:
+ // Message was aborted - this is expected when user cancels, so just log it
+ slog.Debug("Message aborted", "message", err.Data.Message)
+ case opencode.EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError:
+ slog.Error("Message output length error")
+ return a, toast.NewErrorToast("Message output length exceeded limit")
+ default:
+ // Handle any unhandled error types
+ slog.Error("Unhandled session error type", "type", fmt.Sprintf("%T", err))
+ return a, toast.NewErrorToast("An unexpected error occurred")
}
case opencode.EventListResponseEventSessionCompacted:
if msg.Properties.SessionID == a.app.Session.ID {
diff --git a/packages/ui/package.json b/packages/ui/package.json
index dcec177ba..8fd6bff6b 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
- "version": "0.15.28",
+ "version": "0.15.29",
"type": "module",
"exports": {
".": "./src/components/index.ts",
diff --git a/packages/ui/src/components/diff.tsx b/packages/ui/src/components/diff.tsx
index 3eeab6d6f..731b1bfe0 100644
--- a/packages/ui/src/components/diff.tsx
+++ b/packages/ui/src/components/diff.tsx
@@ -54,18 +54,20 @@ export function Diff(props: DiffProps) {
// When ready to render, simply call .render with old/new file, optional
// annotations and a container element to hold the diff
createEffect(() => {
+ // @ts-expect-error
const instance = new FileDiff({
- theme: "pierre-light",
+ // theme: "pierre-light",
+ // theme: "pierre-light",
// Or can also provide a 'themes' prop, which allows the code to adapt
// to your OS light or dark theme
- // themes: { dark: 'pierre-night', light: 'pierre-light' },
+ themes: { dark: "pierre-dark", light: "pierre-light" },
// When using the 'themes' prop, 'themeType' allows you to force 'dark'
// or 'light' theme, or inherit from the OS ('system') theme.
themeType: "system",
// Disable the line numbers for your diffs, generally not recommended
disableLineNumbers: false,
// Whether code should 'wrap' with long lines or 'scroll'.
- overflow: "scroll",
+ overflow: "wrap",
// Normally you shouldn't need this prop, but if you don't provide a
// valid filename or your file doesn't have an extension you may want to
// override the automatic detection. You can specify that language here:
diff --git a/packages/ui/src/styles/tailwind/index.css b/packages/ui/src/styles/tailwind/index.css
index e8e9641b4..76d8c7d3e 100644
--- a/packages/ui/src/styles/tailwind/index.css
+++ b/packages/ui/src/styles/tailwind/index.css
@@ -11,6 +11,26 @@
--spacing: 0.25rem;
/* --spacing: var(--spacing); */
+ --breakpoint-sm: 40rem;
+ --breakpoint-md: 48rem;
+ --breakpoint-lg: 64rem;
+ --breakpoint-xl: 80rem;
+ --breakpoint-2xl: 96rem;
+
+ --container-3xs: 16rem;
+ --container-2xs: 18rem;
+ --container-xs: 20rem;
+ --container-sm: 24rem;
+ --container-md: 28rem;
+ --container-lg: 32rem;
+ --container-xl: 36rem;
+ --container-2xl: 42rem;
+ --container-3xl: 48rem;
+ --container-4xl: 56rem;
+ --container-5xl: 64rem;
+ --container-6xl: 72rem;
+ --container-7xl: 80rem;
+
--font-sans: var(--font-family-sans);
--font-sans--font-feature-settings: var(--font-family-sans--font-feature-settings);
--font-mono: var(--font-family-mono);
diff --git a/packages/ui/src/styles/utilities.css b/packages/ui/src/styles/utilities.css
index 99b7760a0..9c6b73f9c 100644
--- a/packages/ui/src/styles/utilities.css
+++ b/packages/ui/src/styles/utilities.css
@@ -48,6 +48,71 @@
border-width: 0;
}
+.scroller {
+ /* --fade-height: 1.5rem; */
+ /**/
+ /* --mask-top: linear-gradient(to bottom, transparent, black var(--fade-height)); */
+ /* --mask-bottom: linear-gradient(to top, transparent, black var(--fade-height)); */
+ /**/
+ /* mask-image: var(--mask-top), var(--mask-bottom); */
+ /* mask-repeat: no-repeat; */
+ /* mask-size: 100% var(--fade-height); */
+
+ animation: scroll-fade linear;
+ animation-timeline: scroll(self);
+}
+
+/* Define the keyframes for the mask.
+ These percentages now map to scroll positions:
+ 0% = Scrolled to the top
+ 100% = Scrolled to the bottom
+*/
+@keyframes scroll-fade {
+ /* At the very top (0% scroll) */
+ 0% {
+ mask-image: linear-gradient(
+ to bottom,
+ black 90%,
+ /* Opaque, but start fade to bottom */ transparent 100%
+ );
+ }
+
+ /* A small amount scrolled (e.g., 5%)
+ This is where the top fade should be fully visible.
+ */
+ 5% {
+ mask-image: linear-gradient(
+ to bottom,
+ transparent 0%,
+ black 10%,
+ /* Fade-in top */ black 90%,
+ /* Fade-out bottom */ transparent 100%
+ );
+ }
+
+ /* Nearing the bottom (e.g., 95%)
+ The bottom fade should start disappearing.
+ */
+ 95% {
+ mask-image: linear-gradient(
+ to bottom,
+ transparent 0%,
+ black 10%,
+ /* Fade-in top */ black 90%,
+ /* Fade-out bottom */ transparent 100%
+ );
+ }
+
+ /* At the very bottom (100% scroll) */
+ 100% {
+ mask-image: linear-gradient(
+ to bottom,
+ transparent 0%,
+ black 10% /* Opaque, but start fade from top */
+ );
+ }
+}
+
.truncate-start {
text-overflow: ellipsis;
overflow: hidden;
diff --git a/packages/web/package.json b/packages/web/package.json
index 0bd80e056..797bf3a62 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
- "version": "0.15.28",
+ "version": "0.15.29",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
diff --git a/packages/web/src/content/docs/enterprise.mdx b/packages/web/src/content/docs/enterprise.mdx
index 0899d4858..6bfc9b7a9 100644
--- a/packages/web/src/content/docs/enterprise.mdx
+++ b/packages/web/src/content/docs/enterprise.mdx
@@ -1,15 +1,18 @@
---
title: Enterprise
-description: Using OpenCode in your organization.
+description: Using OpenCode securely in your organization.
---
import config from "../../../config.mjs"
export const email = `mailto:${config.email}`
-OpenCode does not store any of your code or context data. This makes it easy for
-you to use OpenCode at your organization.
+OpenCode Enterprise is for organizations that want to ensure that their code and data never leaves their infrastructure. It can do this by using a centralized config that integrates with your SSO and internal AI gateway.
-To get started, we recommend:
+:::note
+OpenCode does not store any of your code or context data.
+:::
+
+To get started with OpenCode Enterprise:
1. Do a trial internally with your team.
2. **Contact us ** to discuss pricing and implementation options.
@@ -18,13 +21,16 @@ To get started, we recommend:
## Trial
-Since OpenCode is open source and does not store any of your code or context data, your developers can simply [get started](/docs/) and carry out a trial.
+OpenCode is open source and does not store any of your code or context data, so your developers can simply [get started](/docs/) and carry out a trial.
---
### Data handling
-**opencode does not store your code or context data.** All processing happens locally or through direct API calls to your AI provider.
+**OpenCode does not store your code or context data.** All processing happens locally or through direct API calls to your AI provider.
+
+This means that as long as you are using a provider you trust, or an internal
+AI gateway, you can use OpenCode securely.
The only caveat here is the optional `/share` feature.
@@ -32,7 +38,7 @@ The only caveat here is the optional `/share` feature.
#### Sharing conversations
-If a user enables the `/share` feature, the conversation and the data associated with it are sent to the service we use to host these shares pages at opencode.ai.
+If a user enables the `/share` feature, the conversation and the data associated with it are sent to the service we use to host these share pages at opencode.ai.
The data is currently served through our CDN's edge network, and is cached on the edge near your users.
@@ -51,7 +57,54 @@ We recommend you disable this for your trial.
### Code ownership
-**You own all code produced by opencode.** There are no licensing restrictions or ownership claims.
+**You own all code produced by OpenCode.** There are no licensing restrictions or ownership claims.
+
+---
+
+## Pricing
+
+We use a per-seat model for OpenCode Enterprise. If you have your own LLM gateway, we do not charge for tokens used. For further details about pricing and implementation options, **contact us **.
+
+---
+
+## Deployment
+
+Once you have completed your trial and you are ready to use OpenCode at
+your organization, you can **contact us ** to discuss
+pricing and implementation options.
+
+---
+
+### Central Config
+
+We can set up OpenCode to use a single central config for your entire organization.
+
+This centralized config can integrate with your SSO provider and ensures all users access only your internal AI gateway.
+
+---
+
+### SSO integration
+
+Through the central config, OpenCode can integrate with your organization's SSO provider for authentication.
+
+This allows OpenCode to obtain credentials for your internal AI gateway through your existing identity management system.
+
+---
+
+### Internal AI gateway
+
+With the central config, OpenCode can also be configured to use only your internal AI gateway.
+
+You can also disable all other AI providers, ensuring all requests go through your organization's approved infrastructure.
+
+---
+
+### Self-hosting
+
+While we recommend disabling the share pages to ensure your data never leaves
+your organization, we can also help you self-host them on your infrastructure.
+
+This is currently on our roadmap. If you're interested, **let us know **.
---
@@ -60,59 +113,37 @@ We recommend you disable this for your trial.
What is OpenCode Enterprise?
-OpenCode Enterprise provides self-hosted deployment options with enhanced security, SSO integration, and dedicated support for organizations that need to maintain full control over their development environment.
+OpenCode Enterprise is for organizations that want to ensure that their code and data never leaves their infrastructure. It can do this by using a centralized config that integrates with your SSO and internal AI gateway.
+
+
+
+
+How do I get started with OpenCode Enterprise?
+
+Simply start with an internal trial with your team. OpenCode by default does not store your code or context data, making it easy to get started.
+
+Then **contact us ** to discuss pricing and implementation options.
How does enterprise pricing work?
-Enterprise pricing is based on team size and deployment requirements. Contact us at {config.email} for a custom quote based on your organization's needs.
+We offer per-seat enterprise pricing. If you have your own LLM gateway, we do not charge for tokens used. For further details, **contact us ** for a custom quote based on your organization's needs.
-What deployment options are available?
+Is my data secure with OpenCode Enterprise?
-We offer cloud-hosted, on-premises, and air-gapped deployment options. Each includes SSO integration, private package registry support, and customizable security configurations.
+Yes. OpenCode does not store your code or context data. All processing happens locally or through direct API calls to your AI provider. With central config and SSO integration, your data remains secure within your organization's infrastructure.
-Is my data secure with enterprise?
+Can we use our own private NPM registry?
-Yes. OpenCode does not store your code or context data. All processing happens locally or through direct API calls to your AI provider. Enterprise deployments add SSO protection and can be fully air-gapped for maximum security.
-
-
-
-
-Can we integrate with existing tools?
-
-Yes. OpenCode supports private npm registries, custom authentication providers, and can be integrated into your existing CI/CD pipelines and development workflows.
-
-
-
----
-
-## Deployment
-
-Once you have completed your trial and you are ready to self-host opencode at
-your organization, you can **contact us ** to discuss
-pricing and implementation options.
-
----
-
-### SSO
-
-SSO integration can be implemented for enterprise deployments after your trial.
-This will allow your team's session data and shared conversations to be protected
-by your enterprise's authentication system.
-
----
-
-### Private NPM
-
-opencode supports private npm registries through Bun's native `.npmrc` file support. If your organization uses a private registry, such as JFrog Artifactory, Nexus, or similar, ensure developers are authenticated before running opencode.
+OpenCode supports private npm registries through Bun's native `.npmrc` file support. If your organization uses a private registry, such as JFrog Artifactory, Nexus, or similar, ensure developers are authenticated before running OpenCode.
To set up authentication with your private registry:
@@ -120,11 +151,11 @@ To set up authentication with your private registry:
npm login --registry=https://your-company.jfrog.io/api/npm/npm-virtual/
```
-This creates `~/.npmrc` with authentication details. opencode will automatically
+This creates `~/.npmrc` with authentication details. OpenCode will automatically
pick this up.
:::caution
-You must be logged into the private registry before running opencode.
+You must be logged into the private registry before running OpenCode.
:::
Alternatively, you can manually configure a `.npmrc` file:
@@ -134,11 +165,6 @@ registry=https://your-company.jfrog.io/api/npm/npm-virtual/
//your-company.jfrog.io/api/npm/npm-virtual/:_authToken=${NPM_AUTH_TOKEN}
```
-Developers must be logged into the private registry before running opencode to ensure packages can be installed from your enterprise registry.
+Developers must be logged into the private registry before running OpenCode to ensure packages can be installed from your enterprise registry.
----
-
-### Self-hosting
-
-The share feature can be self-hosted and the share pages can be made accessible
-only after the user has been authenticated.
+
diff --git a/packages/web/src/content/docs/mcp-servers.mdx b/packages/web/src/content/docs/mcp-servers.mdx
index ab87449e0..132537b05 100644
--- a/packages/web/src/content/docs/mcp-servers.mdx
+++ b/packages/web/src/content/docs/mcp-servers.mdx
@@ -45,12 +45,12 @@ with a unique name. You can refer to that MCP by name when prompting the LLM.
"mcp": {
"name-of-mcp-server": {
// ...
- "enabled": true
+ "enabled": true,
},
"name-of-other-mcp-server": {
// ...
- }
- }
+ },
+ },
}
```
@@ -72,10 +72,10 @@ Add local MCP servers using `type` to `"local"` within the MCP object.
"command": ["npx", "-y", "my-mcp-command"],
"enabled": true,
"environment": {
- "MY_ENV_VAR": "my_env_var_value"
- }
- }
- }
+ "MY_ENV_VAR": "my_env_var_value",
+ },
+ },
+ },
}
```
@@ -91,8 +91,8 @@ For example, here's how I can add the test
"mcp_everything": {
"type": "local",
"command": ["npx", "-y", "@modelcontextprotocol/server-everything"],
- }
- }
+ },
+ },
}
```
@@ -106,12 +106,13 @@ use the mcp_everything tool to add the number 3 and 4
Here are all the options for configuring a local MCP server.
-| Option | Type | Required | Description |
-| ------------- | ------- | -------- | ----------------------------------------------------- |
-| `type` | String | Y | Type of MCP server connection, must be `"local"`. |
-| `command` | Array | Y | Command and arguments to run the MCP server. |
-| `environment` | Object | | Environment variables to set when running the server. |
-| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
+| Option | Type | Required | Description |
+| ------------- | ------- | -------- | ----------------------------------------------------------------------------------- |
+| `type` | String | Y | Type of MCP server connection, must be `"local"`. |
+| `command` | Array | Y | Command and arguments to run the MCP server. |
+| `environment` | Object | | Environment variables to set when running the server. |
+| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
+| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
---
@@ -139,12 +140,13 @@ Here the `url` is the URL of the remote MCP server and with the `headers` option
#### Options
-| Option | Type | Required | Description |
-| --------- | ------- | -------- | -------------------------------------------------- |
-| `type` | String | Y | Type of MCP server connection, must be `"remote"`. |
-| `url` | String | Y | URL of the remote MCP server. |
-| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
-| `headers` | Object | | Headers to send with the request. |
+| Option | Type | Required | Description |
+| --------- | ------- | -------- | ----------------------------------------------------------------------------------- |
+| `type` | String | Y | Type of MCP server connection, must be `"remote"`. |
+| `url` | String | Y | URL of the remote MCP server. |
+| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
+| `headers` | Object | | Headers to send with the request. |
+| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
---
diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json
index bd671373f..c65cb039b 100644
--- a/sdks/vscode/package.json
+++ b/sdks/vscode/package.json
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
- "version": "0.15.28",
+ "version": "0.15.29",
"publisher": "sst-dev",
"repository": {
"type": "git",