Compare commits

..

8 Commits

Author SHA1 Message Date
Dustin Deus fda66bfd40 fix(core): request default OpenAI reasoning summaries 2026-07-13 10:12:06 +00:00
Aiden Cline 828a4b3f15 fix(codemode): align array callback behavior (#36584) 2026-07-13 00:34:18 -05:00
Dax Raad a43de3f86d tui: remove legacy V1 CLI TUI integration to simplify startup 2026-07-13 01:05:14 -04:00
Dax Raad fe3b2e8f90 sync 2026-07-13 00:50:51 -04:00
Dax Raad 20af1f5e79 refactor(tui): compact settings values 2026-07-13 03:57:14 +00:00
Kit Langton 8daf912fbf feat(tui): add settings dialog 2026-07-13 03:57:14 +00:00
opencode-agent[bot] 3d545f960b fix(tui): restore clicked reverted prompt (#36567)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-07-12 21:51:50 -05:00
Kit Langton 58201a32c1 fix: standardize MCP server copy (#36598) 2026-07-12 22:23:59 -04:00
91 changed files with 33995 additions and 758 deletions
+969 -15
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -12,6 +12,7 @@
"dev:web": "bun --cwd packages/app dev",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
"dev:www": "bun run --cwd packages/www dev",
"dev:storybook": "bun --cwd packages/storybook storybook",
"lint": "oxlint",
"lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/core/src packages/server/src packages/protocol/src packages/cli/src",
@@ -102,6 +103,8 @@
"devDependencies": {
"@actions/artifact": "5.0.1",
"@ast-grep/cli": "0.44.0",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"@tsconfig/bun": "catalog:",
"@types/mime-types": "3.0.1",
"@typescript/native-preview": "catalog:",
+4 -4
View File
@@ -65,8 +65,8 @@ export const dict = {
"command.message.next.description": "Go to the next user message",
"command.model.choose": "Choose model",
"command.model.choose.description": "Select a different model",
"command.mcp.toggle": "Toggle MCPs",
"command.mcp.toggle.description": "Toggle MCPs",
"command.mcp.toggle": "Manage MCP servers",
"command.mcp.toggle.description": "Enable or disable MCP servers",
"command.agent.cycle": "Cycle agent",
"command.agent.cycle.description": "Switch to the next agent",
"command.agent.cycle.reverse": "Cycle agent backwards",
@@ -307,9 +307,9 @@ export const dict = {
"prompt.toast.promptSendFailed.title": "Failed to send prompt",
"prompt.toast.promptSendFailed.description": "Unable to retrieve session",
"dialog.mcp.title": "MCPs",
"dialog.mcp.title": "MCP servers",
"dialog.mcp.description": "{{enabled}} of {{total}} enabled",
"dialog.mcp.empty": "No MCPs configured",
"dialog.mcp.empty": "No MCP servers configured",
"dialog.lsp.empty": "LSPs auto-detected from file types",
"dialog.plugins.empty": "Plugins configured in opencode.json",
-1
View File
@@ -171,7 +171,6 @@ ultimate source of truth.
- [ ] `Array.prototype.toSpliced`.
- [ ] Canonical index handling: a key such as `"01"` must not alias index `1`.
- [ ] Complete sparse-array parity. Promise combinators do consume holes as `undefined` members, as in JS.
- [ ] Correct `findLast` return behavior when its predicate mutates the examined element.
## Strings
+22 -21
View File
@@ -727,16 +727,16 @@ const invokeArrayMethod = <R>(
}
return undefined
case "reduce": {
let accumulator: unknown
let start: number
if (args.length >= 2) {
accumulator = args[1]
start = 0
} else {
if (length === 0)
throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node)
accumulator = target[0]
start = 1
let start = 0
let accumulator = args[1]
if (args.length < 2) {
while (start < length && !(start in target)) start += 1
if (start === length)
throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node).as(
"TypeError",
)
accumulator = target[start]
start += 1
}
for (let index = start; index < length; index += 1) {
if (!(index in target)) continue
@@ -745,16 +745,16 @@ const invokeArrayMethod = <R>(
return accumulator
}
case "reduceRight": {
let accumulator: unknown
let start: number
if (args.length >= 2) {
accumulator = args[1]
start = length - 1
} else {
if (length === 0)
throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node)
accumulator = target[length - 1]
start = length - 2
let start = length - 1
let accumulator = args[1]
if (args.length < 2) {
while (start >= 0 && !(start in target)) start -= 1
if (start < 0)
throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node).as(
"TypeError",
)
accumulator = target[start]
start -= 1
}
for (let index = start; index >= 0; index -= 1) {
if (!(index in target)) continue
@@ -764,7 +764,8 @@ const invokeArrayMethod = <R>(
}
case "findLast":
for (let index = length - 1; index >= 0; index -= 1) {
if (yield* apply([target[index], index, target])) return target[index]
const item = target[index]
if (yield* apply([item, index, target])) return item
}
return undefined
case "findLastIndex":
@@ -26,9 +26,11 @@
* - test/built-ins/Array/prototype/forEach/15.4.4.18-7-1.js
* - test/built-ins/Array/prototype/forEach/15.4.4.18-7-2.js
* - test/built-ins/Array/prototype/reduce/15.4.4.21-9-5.js
* - test/built-ins/Array/prototype/reduce/15.4.4.21-9-c-ii-20.js
* - test/built-ins/Array/prototype/reduce/15.4.4.21-9-1.js
* - test/built-ins/Array/prototype/reduce/15.4.4.21-10-1.js
* - test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-5.js
* - test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-c-ii-20.js
* - test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-1.js
* - test/built-ins/Array/prototype/reduceRight/15.4.4.22-10-1.js
* - test/built-ins/Array/prototype/flatMap/depth-always-one.js
@@ -210,6 +212,11 @@ const cases = [
code: `let calls = 0; const result = [1].reduce(() => { calls += 1; return 2 }); return [result, calls]`,
expected: [1, 0],
},
{
path: "test/built-ins/Array/prototype/reduce/15.4.4.21-9-c-ii-20.js",
code: `let accessed = false; const result = [11].reduce((previous) => { accessed = true; return previous === undefined }, undefined); return [result, accessed]`,
expected: [true, true],
},
{
path: "test/built-ins/Array/prototype/reduce/15.4.4.21-10-1.js",
code: `const input = [1, 2, 3, 4, 5]; input.reduce(() => 1); return input`,
@@ -225,6 +232,11 @@ const cases = [
code: `let calls = 0; const result = [1].reduceRight(() => { calls += 1; return 2 }); return [result, calls]`,
expected: [1, 0],
},
{
path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-c-ii-20.js",
code: `let accessed = false; const result = [11].reduceRight((previous) => { accessed = true; return previous === undefined }, undefined); return [result, accessed]`,
expected: [true, true],
},
{
path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-10-1.js",
code: `const input = [1, 2, 3, 4, 5]; input.reduceRight(() => 1); return input`,
@@ -323,3 +335,46 @@ describe("Test262 Array callback adaptations", () => {
})
}
})
describe("Array callback regressions", () => {
test("reduce and reduceRight find the first present element", async () => {
expect(
await value(`
const left = []
left[2] = 3
const right = []
right[0] = 4
right[3] = 1
right.pop()
return [left.reduce((a, b) => a + b), right.reduceRight((a, b) => a + b)]
`),
).toEqual([3, 4])
})
test("reduce and reduceRight reject arrays containing only holes", async () => {
expect(
await value(`
const values = []
values[2] = 1
values.pop()
let left
let right
try { values.reduce((a, b) => a + b) } catch (error) { left = error.name }
try { values.reduceRight((a, b) => a + b) } catch (error) { right = error.name }
return [left, right]
`),
).toEqual(["TypeError", "TypeError"])
})
test("findLast returns the value observed before predicate mutation", async () => {
expect(
await value(`
const values = [1]
return values.findLast((item, index, array) => {
array[index] = 2
return true
})
`),
).toBe(1)
})
})
+1 -1
View File
@@ -17,7 +17,7 @@ type Summary = typeof Summary.Type
const entries = (servers: ReadonlyArray<Summary>) =>
servers.flatMap((server) => [
` <server name="${server.server}">`,
` Use tools from this server through \`execute\` under \`tools[${JSON.stringify(McpTool.namespace(server.server))}]\`.`,
` Use tools from this server through \`execute\` under \`tools[${JSON.stringify(McpTool.group(server.server))}]\`.`,
...server.instructions.split("\n").map((line) => ` ${line}`),
" </server>",
])
+20 -6
View File
@@ -305,13 +305,27 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}),
},
tool: {
// The tool domain is scoped registration, not State, so the host adapts the draft to register calls directly.
transform: (callback) =>
Effect.forEach(
Tool.fromDraft(callback),
({ name, tool, options }) => tools.register({ [name]: tool }, options),
{ discard: true },
).pipe(Effect.orDie, Effect.as({ dispose: Effect.void })),
Effect.gen(function* () {
const registrations: Array<{
readonly name: string
readonly tool: Tool.AnyTool
readonly options?: Tool.RegisterOptions
}> = []
yield* Effect.sync(() =>
callback({
add: (name, tool, options) => {
registrations.push({ name, tool, ...(options ? { options } : {}) })
},
}),
)
yield* Effect.forEach(
registrations,
(registration) => tools.register({ [registration.name]: registration.tool }, registration.options),
{ discard: true },
).pipe(Effect.orDie)
return { dispose: Effect.void }
}),
hook: (name, callback) => {
if (name === "execute.before") {
return toolHooks.hook.before((event) => {
+1 -5
View File
@@ -154,11 +154,7 @@ export function fromPromise(plugin: Plugin) {
register(
host.tool.transform((draft) =>
callback({
add: (tool: AnyTool) =>
draft.add(tool.name, fromPromiseTool(tool), {
...(tool.namespace !== undefined ? { namespace: tool.namespace } : {}),
...(tool.codemode !== undefined ? { codemode: tool.codemode } : {}),
}),
add: (tool: AnyTool) => draft.add(tool.name, fromPromiseTool(tool), tool.options),
}),
),
),
+5 -6
View File
@@ -2,6 +2,7 @@ export * as SessionRunnerModel from "./model"
import { makeLocationNode } from "../../effect/app-node"
import { Model } from "@opencode-ai/llm"
import { OpenAI } from "@opencode-ai/llm/providers"
// ast-grep-ignore: no-star-import
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
// ast-grep-ignore: no-star-import
@@ -191,20 +192,18 @@ export const fromCatalogModel = (
const packageName = ProviderV2.packageName(resolved.package)
const key = apiKey(resolved, credential)
if (
OpenAICodex.isChatGPT(credential) &&
!ProviderV2.isAISDK(resolved.package) &&
isNativeOpenAI(resolved.package)
) {
if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) {
return Effect.succeed(codexModel(resolved, credential, key))
}
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key))
const id = resolved.modelID ?? resolved.id
return Effect.succeed(
withDefaults(resolved, OpenAIResponses.route)
.with(OpenAI.OpenAIProviderOptions.withOpenAIOptions(id, {}))
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
.model({ id: resolved.modelID ?? resolved.id }),
.model({ id }),
)
}
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
+2 -2
View File
@@ -29,8 +29,8 @@ Leaves own resolution, permission, and side-effect ordering. Translate only expe
## Registration
Built-ins and plugin tools register through `Tools.Service.register({ [name]: tool })`. Registrations may provide a
`namespace`, which flattens native model names to `<namespace>_<tool>`, and default into CodeMode (`codemode` defaults
true; `codemode: false` keeps the tool on the provider's native tool list).
group, which flattens direct model names to `<group>_<tool>`, and default into CodeMode (`codemode` defaults true;
`codemode: false` keeps the tool on the provider's native tool list).
Registrations are scoped:
+8 -8
View File
@@ -39,7 +39,7 @@ type CollectedFiles = {
interface Registration {
readonly tool: AnyTool
readonly name: string
readonly namespace?: string
readonly group?: string
}
export const create = (registrations: ReadonlyMap<string, Registration>) => {
@@ -56,19 +56,19 @@ export const create = (registrations: ReadonlyMap<string, Registration>) => {
output: child.outputSchema,
run: (input) => invoke(name, registration, input),
})
if (registration.namespace === undefined) {
if (registration.group === undefined) {
const path = registration.name
if (Object.hasOwn(tools, path)) throw new TypeError(`CodeMode tool namespace conflict: ${path}`)
tools[path] = value
continue
}
const path = registration.name
const namespace = registration.namespace
const branch = tools[namespace]
if (branch && Tool.isDefinition(branch)) throw new TypeError(`CodeMode tool namespace conflict: ${namespace}`)
if (branch) {
if (Object.hasOwn(branch, path)) throw new TypeError(`CodeMode tool namespace conflict: ${namespace}.${path}`)
branch[path] = value
const namespace = registration.group
const group = tools[namespace]
if (group && Tool.isDefinition(group)) throw new TypeError(`CodeMode tool namespace conflict: ${namespace}`)
if (group) {
if (Object.hasOwn(group, path)) throw new TypeError(`CodeMode tool namespace conflict: ${namespace}.${path}`)
group[path] = value
continue
}
const entries: Record<string, Tool.Definition<never>> = {}
+14 -10
View File
@@ -13,10 +13,10 @@ import { Tools } from "./tools"
import { ToolRegistry } from "./registry"
/**
* Registry namespace and permission action names for MCP tools.
* Registry group and permission action names for MCP tools.
*/
export const namespace = (server: string) => server.replace(/[^a-zA-Z0-9_-]/g, "_")
export const name = (server: string, tool: string) => `${namespace(server)}_${tool.replace(/[^a-zA-Z0-9_-]/g, "_")}`
export const group = (server: string) => server.replace(/[^a-zA-Z0-9_-]/g, "_")
export const name = (server: string, tool: string) => `${group(server)}_${tool.replace(/[^a-zA-Z0-9_-]/g, "_")}`
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
@@ -32,11 +32,11 @@ export const layer = Layer.effectDiscard(
// registry never has a gap where MCP tools disappear mid-swap.
const reconcile = lock.withPermit(
Effect.gen(function* () {
const byServer = new Map<string, Record<string, Tool.AnyTool>>()
const groups = new Map<string, Record<string, Tool.AnyTool>>()
for (const tool of yield* mcp.tools()) {
const record = byServer.get(tool.server) ?? {}
const group = groups.get(tool.server) ?? {}
const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema
record[tool.name] = Tool.withPermission(
group[tool.name] = Tool.withPermission(
Tool.make({
description: tool.description ?? "",
jsonSchema: {
@@ -102,12 +102,16 @@ export const layer = Layer.effectDiscard(
}),
name(tool.server, tool.name),
)
byServer.set(tool.server, record)
groups.set(tool.server, group)
}
const next = yield* Scope.fork(scope)
yield* Effect.forEach(byServer, ([server, record]) => tools.register(record, { namespace: server }), {
discard: true,
}).pipe(Scope.provide(next), Effect.orDie)
yield* Effect.forEach(
groups,
([group, record]) => tools.register(record, { group }),
{
discard: true,
},
).pipe(Scope.provide(next), Effect.orDie)
if (current) yield* Scope.close(current, Exit.void)
current = next
}),
+3 -3
View File
@@ -54,7 +54,7 @@ const registryLayer = Layer.effect(
type Registration = {
readonly tool: AnyTool
readonly name: string
readonly namespace?: string
readonly group?: string
readonly codemode: boolean
}
const local = new Map<string, Array<{ readonly token: object; readonly registration: Registration }>>()
@@ -129,7 +129,7 @@ const registryLayer = Layer.effect(
return Service.of({
register: Effect.fn("ToolRegistry.register")(function* (tools, options) {
const entries = registrationEntries(tools, options?.namespace)
const entries = registrationEntries(tools, options?.group)
if (entries.length === 0) return
const codemode = options?.codemode ?? true
const reserved = codemode ? undefined : entries.find((entry) => entry.key === "execute")
@@ -148,7 +148,7 @@ const registryLayer = Layer.effect(
registration: {
tool: entry.tool,
name: entry.name,
namespace: entry.namespace,
group: entry.group,
codemode,
},
},
+18 -5
View File
@@ -46,11 +46,24 @@ export const registerToolPlugin = <R>(plugin: {
const context = host({
tool: {
transform: (callback) =>
Effect.forEach(
Tool.fromDraft(callback),
({ name, tool, options }) => tools.register({ [name]: tool }, options),
{ discard: true },
).pipe(Effect.orDie, Effect.as({ dispose: Effect.void })),
Effect.gen(function* () {
const registrations: Array<{
readonly name: string
readonly tool: Tool.AnyTool
readonly options?: Tool.RegisterOptions
}> = []
callback({
add: (name, tool, options) => {
registrations.push({ name, tool, ...(options ? { options } : {}) })
},
})
yield* Effect.forEach(
registrations,
(registration) => tools.register({ [registration.name]: registration.tool }, registration.options),
{ discard: true },
).pipe(Effect.orDie)
return { dispose: Effect.void }
}),
hook: () => Effect.die("registerToolPlugin does not support tool hooks"),
},
})
+4 -39
View File
@@ -274,7 +274,7 @@ describe("PluginV2", () => {
}),
)
it.effect("namespaces tool names and routes codemode registrations through execute", () =>
it.effect("groups tool names and routes codemode registrations through execute", () =>
Effect.gen(function* () {
const plugins = yield* PluginV2.Service
const registry = yield* ToolRegistry.Service
@@ -286,13 +286,13 @@ describe("PluginV2", () => {
execute: () => Effect.succeed({ ok: true }),
})
const plugin = EffectPlugin.define({
id: "namespaced-tools",
id: "grouped-tools",
effect: (ctx) =>
ctx.tool
.transform((draft) => {
draft.add("plain", tool("Plain"), { codemode: false })
draft.add("look/up", tool("Lookup"), { namespace: "context 7", codemode: false })
draft.add("search", tool("Search"), { namespace: "context 7" })
draft.add("look/up", tool("Lookup"), { group: "context 7", codemode: false })
draft.add("search", tool("Search"), { group: "context 7" })
})
.pipe(Effect.orDie),
})
@@ -307,41 +307,6 @@ describe("PluginV2", () => {
}),
)
it.effect("accepts flat Effect draft declarations with namespace", () =>
Effect.gen(function* () {
const plugins = yield* PluginV2.Service
const registry = yield* ToolRegistry.Service
const plugin = EffectPlugin.define({
id: "flat-tools",
effect: (ctx) =>
ctx.tool
.transform((draft) => {
draft.add({
name: "send",
namespace: "slack",
description: "Send a Slack message",
input: Schema.Struct({ text: Schema.String }),
output: Schema.Struct({ sent: Schema.Boolean }),
execute: () => Effect.succeed({ sent: true }),
})
draft.add({
name: "edit",
codemode: false,
description: "Edit a file",
input: Schema.Struct({}),
output: Schema.Struct({ ok: Schema.Boolean }),
execute: () => Effect.succeed({ ok: true }),
})
})
.pipe(Effect.orDie),
})
yield* plugins.activate([versioned(plugin)])
expect((yield* registry.materialize()).definitions.map((tool) => tool.name)).toEqual(["edit", "execute"])
}),
)
it.effect("fires before/after tool hooks with mutable events around settlement", () =>
Effect.gen(function* () {
const plugins = yield* PluginV2.Service
+1 -1
View File
@@ -135,7 +135,7 @@ describe("fromPromise", () => {
await ctx.tool.transform((tools) => {
tools.add({
name: "hello",
codemode: false,
options: { codemode: false },
description: "Hello",
input: Schema.Struct({ name: Schema.String }),
output: Schema.String,
@@ -64,6 +64,20 @@ describe("SessionRunnerModel", () => {
}),
)
it.effect("requests OpenAI reasoning summaries for default reasoning models", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
modelID: "gpt-5.2",
settings: { baseURL: "https://openai.example/v1" },
}),
)
const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
expect(prepared.body).toMatchObject({ reasoning: { summary: "auto" } })
}),
)
it.effect("keeps catalog apiKey credentials out of provider JSON", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
+4 -5
View File
@@ -312,13 +312,12 @@ export default Plugin.define({
})
```
Unsupported characters in tool and namespace names are normalized to underscores.
Unsupported characters in tool and group names are normalized to underscores.
The resulting exposed key must begin with a letter and contain at most 64
letters, digits, underscores, or hyphens. Set registration fields on the
declaration or options with `{ namespace, codemode }`:
letters, digits, underscores, or hyphens. Set `options` on the declaration to
configure registration with `{ group, codemode }`:
- `namespace` prefixes the exposed tool name (and becomes the CodeMode path
segment).
- `group` prefixes and groups the exposed tool name.
- `codemode` defaults to `true` and makes the tool available through the
`execute` CodeMode tool. Set `codemode: false` to expose it directly to the
provider.
+1
View File
@@ -7,6 +7,7 @@ import * as OpenAIResponses from "../protocols/openai-responses"
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
export type { OpenAIOptionsInput, OpenAIResponseIncludable } from "./openai-options"
export * as OpenAIProviderOptions from "./openai-options"
export const id = ProviderID.make("openai")
+1 -3
View File
@@ -159,7 +159,6 @@ for (const item of targets) {
const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
const workerPath = "./src/cli/tui/worker.ts"
// Use platform-specific bunfs root path based on target OS
const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
@@ -185,13 +184,12 @@ for (const item of targets) {
windows: {},
},
files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},
entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
entrypoints: ["./src/index.ts", parserWorker, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
define: {
FFF_LIBC: JSON.stringify(item.abi === "musl" ? "musl" : "gnu"),
OPENCODE_VERSION: `'${Script.version}'`,
OPENCODE_MODELS_DEV: generated.modelsData,
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
OPENCODE_WORKER_PATH: workerPath,
OPENCODE_CHANNEL: `'${Script.channel}'`,
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
-100
View File
@@ -1,100 +0,0 @@
import { cmd } from "./cmd"
import { UI } from "@/cli/ui"
import { errorMessage } from "@opencode-ai/tui/util/error"
import { validateSession } from "../tui/validate-session"
import { ServerAuth } from "@/server/auth"
import { OpenCode } from "@opencode-ai/client/promise"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
export const AttachCommand = cmd({
command: "attach <url>",
describe: "attach to a running opencode server",
builder: (yargs) =>
yargs
.positional("url", {
type: "string",
describe: "http://localhost:4096",
demandOption: true,
})
.option("dir", {
type: "string",
description: "directory to run in",
})
.option("continue", {
alias: ["c"],
describe: "continue the last session",
type: "boolean",
})
.option("session", {
alias: ["s"],
type: "string",
describe: "session id to continue",
})
.option("fork", {
type: "boolean",
describe: "fork the session when continuing (use with --continue or --session)",
})
.option("password", {
alias: ["p"],
type: "string",
describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
})
.option("username", {
alias: ["u"],
type: "string",
describe: "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')",
}),
handler: async (args) => {
const directory = (() => {
if (!args.dir) return undefined
try {
process.chdir(args.dir)
return process.cwd()
} catch {
// If the directory doesn't exist locally (remote attach), pass it through.
return args.dir
}
})()
const { TuiConfig } = await import("@/config/tui")
if (args.fork && !args.continue && !args.session) {
UI.error("--fork requires --continue or --session")
process.exitCode = 1
return
}
const headers = ServerAuth.headers({ password: args.password, username: args.username })
const config = await TuiConfig.get()
try {
await validateSession({
url: args.url,
sessionID: args.session,
directory,
headers,
})
} catch (error) {
UI.error(errorMessage(error))
process.exitCode = 1
return
}
const { Effect } = await import("effect")
const { run } = await import("../tui/layer")
const { createLegacyTuiPluginHost } = await import("@/plugin/tui/runtime")
await Effect.runPromise(
run({
// @ts-expect-error V1 does not consume the V2-only server input.
client: createOpencodeClient({ baseUrl: args.url, headers, directory }),
api: OpenCode.make({ baseUrl: args.url, headers }),
config,
pluginHost: createLegacyTuiPluginHost(),
args: {
continue: args.continue,
sessionID: args.session,
fork: args.fork,
},
}),
)
},
})
-198
View File
@@ -1,198 +0,0 @@
import { cmd } from "@/cli/cmd/cmd"
import { Rpc } from "@/util/rpc"
import { type rpc } from "../tui/worker"
import path from "path"
import { fileURLToPath } from "url"
import { UI } from "@/cli/ui"
import { errorMessage } from "@opencode-ai/tui/util/error"
import { withTimeout } from "@/util/timeout"
import { withNetworkOptions, resolveNetworkOptionsNoConfig, hasArg } from "@/cli/network"
import { Filesystem } from "@/util/filesystem"
import { OpenCode } from "@opencode-ai/client/promise"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { writeHeapSnapshot } from "v8"
import { ServerAuth } from "@/server/auth"
import { validateSession } from "../tui/validate-session"
import { win32InstallCtrlCGuard } from "@opencode-ai/tui/terminal-win32"
declare global {
const OPENCODE_WORKER_PATH: string
}
async function target() {
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
const dist = new URL("./cli/tui/worker.js", import.meta.url)
if (await Filesystem.exists(fileURLToPath(dist))) return dist
return new URL("../tui/worker.ts", import.meta.url)
}
async function input(value?: string) {
const piped = process.stdin.isTTY ? undefined : await Bun.stdin.text()
if (!value) return piped
if (!piped) return value
return piped + "\n" + value
}
export function resolveThreadDirectory(project?: string, envPWD = process.env.PWD, cwd = process.cwd()) {
const root = Filesystem.resolve(envPWD ?? cwd)
if (project) return Filesystem.resolve(path.isAbsolute(project) ? project : path.join(root, project))
return Filesystem.resolve(cwd)
}
export const TuiThreadCommand = cmd({
command: "$0 [project]",
describe: "start opencode tui",
builder: (yargs) =>
withNetworkOptions(yargs)
.positional("project", {
type: "string",
describe: "path to start opencode in",
})
.option("model", {
type: "string",
alias: ["m"],
describe: "model to use in the format of provider/model",
})
.option("continue", {
alias: ["c"],
describe: "continue the last session",
type: "boolean",
})
.option("session", {
alias: ["s"],
type: "string",
describe: "session id to continue",
})
.option("fork", {
type: "boolean",
describe: "fork the session when continuing (use with --continue or --session)",
})
.option("prompt", {
type: "string",
describe: "prompt to use",
})
.option("agent", {
type: "string",
describe: "agent to use",
})
.option("auto", {
type: "boolean",
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
default: false,
})
.option("yolo", {
type: "boolean",
hidden: true,
default: false,
})
.option("dangerously-skip-permissions", {
type: "boolean",
hidden: true,
default: false,
}),
handler: async (args) => {
const unguard = win32InstallCtrlCGuard()
try {
const { TuiConfig } = await import("@/config/tui")
if (args.fork && !args.continue && !args.session) {
UI.error("--fork requires --continue or --session")
process.exitCode = 1
return
}
// Resolve relative --project paths from PWD, then use the real cwd after
// chdir so the thread and worker share the same directory key.
const next = resolveThreadDirectory(args.project)
const file = await target()
try {
process.chdir(next)
} catch {
UI.error("Failed to change directory to " + next)
return
}
const cwd = Filesystem.resolve(process.cwd())
const worker = new Worker(file, {
env: Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
),
})
const client = Rpc.client<typeof rpc>(worker)
const reload = () => {
client.call("reload", undefined).catch(() => {})
}
process.on("SIGUSR2", reload)
let stopped = false
const stop = async () => {
if (stopped) return
stopped = true
process.off("SIGUSR2", reload)
await withTimeout(client.call("shutdown", undefined), 5000).catch(() => {})
worker.terminate()
}
const prompt = await input(args.prompt)
const config = await TuiConfig.get()
const network = resolveNetworkOptionsNoConfig(args)
const external = hasArg("--port") || hasArg("--hostname") || network.mdns === true
const headers = external ? ServerAuth.headers() : undefined
const url = (await client.call("server", network)).url
try {
await validateSession({
url,
sessionID: args.session,
directory: cwd,
headers,
})
} catch (error) {
UI.error(errorMessage(error))
process.exitCode = 1
return
}
setTimeout(() => {
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
}, 1000).unref?.()
try {
const { Effect } = await import("effect")
const { run } = await import("../tui/layer")
const { createLegacyTuiPluginHost } = await import("@/plugin/tui/runtime")
await Effect.runPromise(
run({
// @ts-expect-error V1 does not consume the V2-only server input.
client: createOpencodeClient({ baseUrl: url, headers, directory: cwd }),
api: OpenCode.make({ baseUrl: url, headers }),
async onSnapshot() {
const tui = writeHeapSnapshot("tui.heapsnapshot")
const server = await client.call("snapshot", undefined)
return [tui, server]
},
config,
pluginHost: createLegacyTuiPluginHost(),
args: {
continue: args.continue,
sessionID: args.session,
agent: args.agent,
model: args.model,
prompt,
fork: args.fork,
auto: args.auto || args.yolo || args["dangerously-skip-permissions"],
},
}),
)
} finally {
await stop()
}
} finally {
try {
unguard?.()
} catch {}
}
process.exit(0)
},
})
// scratch
-8
View File
@@ -1,8 +0,0 @@
import { run as runTui, type TuiInput } from "@opencode-ai/tui"
import { Global } from "@opencode-ai/core/global"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Effect } from "effect"
export function run(input: TuiInput) {
return runTui(input).pipe(Effect.provide(AppNodeBuilder.build(Global.node)))
}
-54
View File
@@ -1,54 +0,0 @@
import { Server } from "@/server/server"
import { InstanceRuntime } from "@/project/instance-runtime"
import { Rpc } from "@/util/rpc"
import { upgrade } from "@/cli/upgrade"
import { Config } from "@/config/config"
import { writeHeapSnapshot } from "node:v8"
import { Heap } from "@/cli/heap"
import { AppRuntime } from "@/effect/app-runtime"
import { Effect } from "effect"
import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle"
Heap.start()
const onUnhandledRejection = (_error: unknown) => {}
const onUncaughtException = (_error: Error) => {}
process.on("unhandledRejection", onUnhandledRejection)
process.on("uncaughtException", onUncaughtException)
let server: Awaited<ReturnType<typeof Server.listen>> | undefined
export const rpc = {
snapshot() {
const result = writeHeapSnapshot("server.heapsnapshot")
return result
},
async server(input: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
if (server) await server.stop(true)
server = await Server.listen(input)
return { url: server.url.toString() }
},
async checkUpgrade(input: { directory: string }) {
await InstanceRuntime.load({ directory: input.directory })
await upgrade().catch(() => {})
},
async reload() {
await AppRuntime.runPromise(
Effect.gen(function* () {
const cfg = yield* Config.Service
yield* cfg.invalidate()
yield* disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true })
}),
)
},
async shutdown() {
await InstanceRuntime.disposeAllInstances()
if (server) await server.stop(true)
process.off("unhandledRejection", onUnhandledRejection)
process.off("uncaughtException", onUncaughtException)
},
}
Rpc.listen(rpc)
-4
View File
@@ -18,9 +18,7 @@ import { McpCommand } from "./cli/cmd/mcp"
import { GithubCommand } from "./cli/cmd/github"
import { ExportCommand } from "./cli/cmd/export"
import { ImportCommand } from "./cli/cmd/import"
import { AttachCommand } from "./cli/cmd/attach"
import { V2ServeCommand } from "./cli/cmd/v2-serve"
import { TuiThreadCommand } from "./cli/cmd/tui"
import { AcpCommand } from "./cli/cmd/acp"
import { EOL } from "os"
import { WebCommand } from "./cli/cmd/web"
@@ -82,8 +80,6 @@ const cli = yargs(args)
.command(AcpCommand)
.command(McpCommand)
.command(V2ServeCommand)
.command(TuiThreadCommand)
.command(AttachCommand)
.command(RunCommand)
.command(GenerateCommand)
.command(DebugCommand)
+1 -8
View File
@@ -42,7 +42,7 @@ import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
import { createCommandShim } from "@opencode-ai/tui/plugin/command-shim"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Effect } from "effect"
import { createPluginRuntime, type PluginRuntime, type TuiPluginHost } from "@opencode-ai/tui/plugin/runtime"
import { createPluginRuntime, type PluginRuntime } from "@opencode-ai/tui/plugin/runtime"
ensureRuntimePluginSupport({ additional: keymapRuntimeModules })
@@ -1121,11 +1121,4 @@ async function load(input: {
}
}
export function createLegacyTuiPluginHost(): TuiPluginHost {
return {
start: init,
dispose,
}
}
export * as TuiPluginRuntime from "./runtime"
-2
View File
@@ -1,5 +1,4 @@
import yargs from "yargs"
import { TuiThreadCommand } from "./cli/cmd/tui"
import { V2ServeCommand } from "./cli/cmd/v2-serve"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { hideBin } from "yargs/helpers"
@@ -29,5 +28,4 @@ const cli = yargs(hideBin(process.argv))
if (opts.logLevel) process.env.OPENCODE_LOG_LEVEL = opts.logLevel
})
.command(V2ServeCommand)
.command(TuiThreadCommand)
.parse()
@@ -1,11 +0,0 @@
import { describe, expect, test } from "bun:test"
describe("tui attach", () => {
test("loads the TUI integration lazily", async () => {
const source = await Bun.file(new URL("../../../src/cli/cmd/attach.ts", import.meta.url)).text()
expect(source).toContain('await import("../tui/layer")')
expect(source).toMatch(/await import\(["']@\/plugin\/tui\/runtime["']\)/)
expect(source).not.toContain('import("./app")')
})
})
@@ -1,89 +0,0 @@
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import fs from "fs/promises"
import path from "path"
import yargs from "yargs"
import { tmpdir } from "../../fixture/fixture"
import { TuiThreadCommand, resolveThreadDirectory } from "../../../src/cli/cmd/tui"
import { cliIt } from "../../lib/cli-process"
describe("tui thread", () => {
test("loads the TUI integration lazily", async () => {
const source = await Bun.file(new URL("../../../src/cli/cmd/tui.ts", import.meta.url)).text()
expect(source).toContain('await import("../tui/layer")')
expect(source).toMatch(/await import\(["']@\/plugin\/tui\/runtime["']\)/)
expect(source).not.toContain('import("./app")')
})
test("forwards the CLI environment to the TUI worker", async () => {
const source = await Bun.file(new URL("../../../src/cli/cmd/tui.ts", import.meta.url)).text()
expect(source).toMatch(/new Worker\(file, \{\s*env: Object\.fromEntries\(\s*Object\.entries\(process\.env\)/)
})
async function check(project?: string) {
await using tmp = await tmpdir({ git: true })
const link = path.join(path.dirname(tmp.path), path.basename(tmp.path) + "-link")
const type = process.platform === "win32" ? "junction" : "dir"
try {
await fs.symlink(tmp.path, link, type)
expect(resolveThreadDirectory(project, link, tmp.path)).toBe(tmp.path)
} finally {
await fs.rm(link, { recursive: true, force: true }).catch(() => undefined)
}
}
test("uses the real cwd when PWD points at a symlink", async () => {
await check()
})
test("uses the real cwd after resolving a relative project from PWD", async () => {
await check(".")
})
test("resolves a relative project from PWD when cwd differs", async () => {
await using pwd = await tmpdir({ git: true })
await using cwd = await tmpdir({ git: true })
expect(resolveThreadDirectory(".", pwd.path, cwd.path)).toBe(pwd.path)
expect(resolveThreadDirectory(undefined, pwd.path, cwd.path)).toBe(cwd.path)
})
test("preserves boolean negation for existing options", async () => {
const args = await yargs([])
.command({ ...TuiThreadCommand, handler: () => {} })
.exitProcess(false)
.parse(["--mdns", "--no-mdns"])
expect(args.mdns).toBe(false)
})
cliIt.live("rejects removed top-level mini alias", ({ opencode }) =>
Effect.gen(function* () {
const result = yield* opencode.spawn(["--mini"])
opencode.expectExit(result, 1)
expect(result.stderr).not.toContain("opencode mini requires a TTY stdout")
}),
)
cliIt.live("rejects removed run mini flag", ({ opencode }) =>
Effect.gen(function* () {
const result = yield* opencode.spawn(["run", "--mini"])
opencode.expectExit(result, 1)
expect(result.stderr).not.toContain("opencode mini requires a TTY stdout")
}),
)
cliIt.live("rejects removed attach mini alias", ({ opencode }) =>
Effect.gen(function* () {
const result = yield* opencode.spawn(["attach", "http://127.0.0.1:1", "--mini"])
opencode.expectExit(result, 1)
expect(result.stderr).not.toContain("opencode mini requires a TTY stdout")
}),
)
})
+4 -68
View File
@@ -123,17 +123,6 @@ export function make(config: Config<any, any, any> | DynamicConfig): AnyTool {
return makeTyped(config)
}
/**
* Split a flat tool declaration into its registration name, opaque Tool value,
* and registration options. Narrowing on `jsonSchema` selects the matching
* constructor, so no cast is needed to build the Tool from the flat union.
*/
export function fromFlat(flat: FlatDefinition<any, any, any> | FlatDynamicDefinition) {
const { name, namespace, codemode, ...config } = flat
const tool = "jsonSchema" in config ? makeDynamic(config) : makeTyped(config)
return { name, tool, options: { namespace, codemode } satisfies RegisterOptions }
}
function makeTyped<
Input extends SchemaType<any>,
Output extends SchemaType<any>,
@@ -223,14 +212,14 @@ export const validateName = (name: string) =>
? Effect.void
: Effect.fail(new RegistrationError({ name, message: `Invalid tool name: ${name}` }))
export const registrationEntries = (tools: Readonly<Record<string, AnyTool>>, namespace?: string) =>
export const registrationEntries = (tools: Readonly<Record<string, AnyTool>>, group?: string) =>
Object.entries(tools).map(([name, tool]) => {
const normalized = name.replace(/[^a-zA-Z0-9_-]/g, "_")
const parent = namespace?.replace(/[^a-zA-Z0-9_-]/g, "_")
const parent = group?.replace(/[^a-zA-Z0-9_-]/g, "_")
return {
key: parent === undefined ? normalized : `${parent}_${normalized}`,
name: normalized,
namespace: parent,
group: parent,
tool,
}
})
@@ -282,68 +271,15 @@ export interface ToolExecuteAfterEvent {
}
export interface RegisterOptions {
/** Dotted CodeMode path prefix, e.g. "slack.admin". */
readonly namespace?: string
readonly group?: string
/** Defaults to true. False exposes the tool directly to the provider. */
readonly codemode?: boolean
}
export type FlatDefinition<
Input extends SchemaType<any>,
Output extends SchemaType<any>,
Structured extends SchemaType<any> = Output,
> = {
readonly name: string
readonly namespace?: string
readonly codemode?: boolean
readonly description: string
readonly input: Input
readonly output: Output
readonly structured?: Structured
readonly toStructuredOutput?: Config<Input, Output, Structured>["toStructuredOutput"]
readonly execute: Config<Input, Output, Structured>["execute"]
readonly toModelOutput?: Config<Input, Output, Structured>["toModelOutput"]
}
export type FlatDynamicDefinition = {
readonly name: string
readonly namespace?: string
readonly codemode?: boolean
readonly description: string
readonly jsonSchema: JsonSchema.JsonSchema
readonly outputSchema?: JsonSchema.JsonSchema
readonly execute: DynamicConfig["execute"]
}
export interface ToolDraft {
add<
Input extends SchemaType<any>,
Output extends SchemaType<any>,
Structured extends SchemaType<any> = Output,
>(tool: FlatDefinition<Input, Output, Structured>): void
add(tool: FlatDynamicDefinition): void
add(name: string, tool: AnyTool, options?: RegisterOptions): void
}
export type Registration = {
readonly name: string
readonly tool: AnyTool
readonly options?: RegisterOptions
}
/** Run a draft callback and collect the tools it declared, in registration order. */
export function fromDraft(callback: (draft: ToolDraft) => void) {
const registrations: Array<Registration> = []
const add = (
nameOrTool: string | FlatDefinition<any, any, any> | FlatDynamicDefinition,
tool?: AnyTool,
options?: RegisterOptions,
) =>
registrations.push(typeof nameOrTool === "string" ? { name: nameOrTool, tool: tool!, options } : fromFlat(nameOrTool))
callback({ add })
return registrations
}
export interface ToolHooks {
readonly "execute.before": ToolExecuteBeforeEvent
readonly "execute.after": ToolExecuteAfterEvent
+8 -4
View File
@@ -16,8 +16,7 @@ export type Definition<
Structured extends SchemaType<any> = Output,
> = {
readonly name: string
readonly namespace?: string
readonly codemode?: boolean
readonly options?: RegisterOptions
readonly description: string
readonly input: Input
readonly output: Output
@@ -38,8 +37,7 @@ export type Definition<
export type DynamicDefinition = {
readonly name: string
readonly namespace?: string
readonly codemode?: boolean
readonly options?: RegisterOptions
readonly description: string
readonly jsonSchema: JsonSchema.JsonSchema
readonly outputSchema?: JsonSchema.JsonSchema
@@ -69,6 +67,12 @@ export interface ToolExecuteAfterEvent {
outputPaths?: ReadonlyArray<string>
}
export interface RegisterOptions {
readonly group?: string
/** Defaults to true. False exposes the tool directly to the provider. */
readonly codemode?: boolean
}
export interface ToolDraft {
add<
Input extends SchemaType<any>,
+22 -63
View File
@@ -53,6 +53,7 @@ import { DialogModel } from "./component/dialog-model"
import { useConnected } from "./component/use-connected"
import { DialogMcp } from "./component/dialog-mcp"
import { DialogStatus } from "./component/dialog-status"
import { DialogConfig } from "./component/dialog-config"
import { DialogDebug } from "./component/dialog-debug"
import { DialogPair, type DialogPairCredentials } from "./component/dialog-pair"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
@@ -76,7 +77,6 @@ import { ArgsProvider, useArgs, type Args } from "./context/args"
import open from "open"
import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { Config, ConfigProvider, useConfig } from "./config"
import { TuiConfigV1 } from "./config/v1"
import { createTuiApiAdapters } from "./plugin/adapters"
import { createTuiApi } from "./plugin/api"
import { createPluginRuntime, PluginRuntimeProvider, usePluginRuntime, type TuiPluginHost } from "./plugin/runtime"
@@ -138,7 +138,6 @@ const appBindingCommands = [
"diff.open",
"app.debug",
"app.console",
"app.heap_snapshot",
"terminal.suspend",
"terminal.title.toggle",
"app.toggle.animations",
@@ -154,8 +153,7 @@ export type TuiInput = {
reload?: () => Promise<void>
}
args: Args
config: Config.Interface | TuiConfigV1.Resolved
onSnapshot?: () => Promise<string[]>
config: Config.Interface
pluginHost: TuiPluginHost
terminalHandoff?: () => Promise<
| {
@@ -200,43 +198,12 @@ function isVersionGreater(left: string, right: string) {
return a.prerelease.localeCompare(b.prerelease, undefined, { numeric: true }) > 0
}
function fromV1(config: TuiConfigV1.Resolved): Config.Info {
return {
theme: config.theme ? { name: config.theme } : undefined,
plugins: config.plugin?.map((plugin) =>
typeof plugin === "string" ? plugin : { package: plugin[0], options: plugin[1] },
),
leader: { timeout: config.leader_timeout },
scroll:
config.scroll_speed === undefined && config.scroll_acceleration === undefined
? undefined
: { speed: config.scroll_speed, acceleration: config.scroll_acceleration?.enabled },
attention: config.attention,
diffs: config.diff_style === undefined ? undefined : { view: config.diff_style === "stacked" ? "unified" : "auto" },
mouse: config.mouse,
}
}
function isConfigInterface(config: Config.Interface | TuiConfigV1.Resolved): config is Config.Interface {
return "get" in config && typeof config.get === "function" && "update" in config && typeof config.update === "function"
}
export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
const log = input.log ?? (() => {})
const global = yield* Global.Service
const configInput = input.config
const loaded = yield* Effect.gen(function* () {
if (isConfigInterface(configInput)) {
return {
service: configInput,
info: yield* Effect.tryPromise(() => configInput.get()),
legacy: undefined,
}
}
return { service: undefined, info: fromV1(configInput), legacy: configInput }
const config = Config.resolve(yield* Effect.tryPromise(() => input.config.get()), {
terminalSuspend: process.platform !== "win32",
})
const config = Config.resolve(loaded.info, { terminalSuspend: process.platform !== "win32" })
if (loaded.legacy) config.keybinds = loaded.legacy.keybinds
const options = { baseUrl: input.server.endpoint.url, headers: Service.headers(input.server.endpoint) }
const api = OpenCode.make(options)
const directory = yield* Effect.tryPromise(() => api.file.list({ location: { directory: process.cwd() } })).pipe(
@@ -373,7 +340,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
<ArgsProvider {...input.args}>
<ConfigProvider
config={config}
service={loaded.service}
service={input.config}
options={{ terminalSuspend: process.platform !== "win32" }}
>
<KVProvider>
@@ -409,9 +376,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
<EditorContextProvider>
<LocationProvider>
<App
onSnapshot={input.onSnapshot}
pluginHost={input.pluginHost}
pluginConfig={loaded.legacy ?? config}
pair={
input.server.endpoint.auth
? input.server.endpoint.auth
@@ -470,9 +435,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
})
function App(props: {
onSnapshot?: () => Promise<string[]>
pluginHost: TuiPluginHost
pluginConfig: any
pair?: DialogPairCredentials
}) {
const log = useLog({ component: "app" })
@@ -522,7 +485,7 @@ function App(props: {
toast.show({
variant: "error",
title: `MCP server failed: ${server.name}`,
message: "Open MCPs to view details.",
message: "Open MCP servers to view details.",
})
}
})
@@ -551,7 +514,6 @@ function App(props: {
props.pluginHost
.start({
api,
config: props.pluginConfig,
runtime: pluginRuntime,
dispose: () => attention.dispose(),
})
@@ -587,10 +549,12 @@ function App(props: {
renderer.clearSelection()
}
const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true))
const [pasteSummaryEnabled, setPasteSummaryEnabled] = createSignal(
kv.get("paste_summary_enabled", true),
)
const [terminalTitleEnabled, setTerminalTitleEnabled] = kv.signal("terminal_title_enabled", true)
const [pasteSummaryEnabled, setPasteSummaryEnabled] = kv.signal("paste_summary_enabled", true)
createEffect(() => {
renderer.useMouse = !Flag.OPENCODE_DISABLE_MOUSE && config.mouse
})
// Update terminal window title based on current route and session
createEffect(() => {
@@ -785,7 +749,7 @@ function App(props: {
},
{
name: "mcp.list",
title: "MCP Servers",
title: "MCP servers",
category: "Agent",
slashName: "mcps",
run: () => {
@@ -849,6 +813,15 @@ function App(props: {
},
category: "Integration",
},
{
name: "opencode.settings",
title: "Open settings",
slashName: "settings",
run: () => {
dialog.replace(() => <DialogConfig />)
},
category: "System",
},
{
name: "opencode.status",
title: "View status",
@@ -967,20 +940,6 @@ function App(props: {
dialog.clear()
},
},
{
name: "app.heap_snapshot",
title: "Write heap snapshot",
category: "System",
run: async () => {
const files = await props.onSnapshot?.()
toast.show({
variant: "info",
message: `Heap snapshot written to ${files?.join(", ")}`,
duration: 5000,
})
dialog.clear()
},
},
{
name: "terminal.suspend",
title: "Suspend terminal",
@@ -0,0 +1,420 @@
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
import { useTerminalDimensions } from "@opentui/solid"
import { createMemo, createSignal, For, onMount, Show } from "solid-js"
import { useConfig } from "../config"
import { useTheme } from "../context/theme"
import { useBindings } from "../keymap"
import { useDialog } from "../ui/dialog"
import { useToast } from "../ui/toast"
type Setting = {
title: string
category: string
description: string
detail?: string
path: string[]
default: unknown
values?: readonly unknown[]
labels?: readonly string[]
step?: number
min?: number
max?: number
format?: (value: unknown) => string
}
const settings: Setting[] = [
{
title: "Theme",
category: "Appearance",
description: "Interface color theme",
detail:
"Choose the color theme used throughout OpenCode. Custom themes discovered from your config directory appear here alongside the built-in themes.",
path: ["theme", "name"],
default: "opencode",
},
{
title: "Color mode",
category: "Appearance",
description: "Terminal color preference",
detail:
"Choose how OpenCode selects its colors. System follows your terminal preference, while dark and light keep the interface in a fixed mode.",
path: ["theme", "mode"],
default: "system",
values: ["system", "dark", "light"],
},
{
title: "Animations",
category: "Appearance",
description: "Interface motion",
path: ["animations"],
default: true,
values: [false, true],
labels: ["off", "on"],
},
{
title: "Tips",
category: "Appearance",
description: "Home screen hints",
path: ["hints", "tips"],
default: true,
values: [false, true],
labels: ["off", "on"],
},
{
title: "Onboarding",
category: "Appearance",
description: "Getting-started guidance",
path: ["hints", "onboarding"],
default: true,
values: [false, true],
labels: ["off", "on"],
},
{
title: "Sidebar",
category: "Session",
description: "Session sidebar visibility",
path: ["session", "sidebar"],
default: "auto",
values: ["hide", "auto"],
},
{
title: "Scrollbar",
category: "Session",
description: "Transcript scrollbar",
path: ["session", "scrollbar"],
default: false,
values: [false, true],
labels: ["off", "on"],
},
{
title: "Thinking",
category: "Session",
description: "Model reasoning by default",
path: ["session", "thinking"],
default: "hide",
values: ["hide", "show"],
},
{
title: "Grouping",
category: "Session",
description: "Related transcript items",
path: ["session", "grouping"],
default: "auto",
values: ["none", "auto"],
},
{
title: "Layout",
category: "Diffs",
description: "Diff presentation",
path: ["diffs", "view"],
default: "auto",
values: ["auto", "split", "unified"],
},
{
title: "Wrapping",
category: "Diffs",
description: "Long diff lines",
path: ["diffs", "wrap"],
default: "word",
values: ["none", "word"],
},
{
title: "File tree",
category: "Diffs",
description: "Diff file navigation",
path: ["diffs", "tree"],
default: true,
values: [false, true],
labels: ["off", "on"],
},
{
title: "Single patch",
category: "Diffs",
description: "Only the selected patch",
path: ["diffs", "single"],
default: false,
values: [false, true],
labels: ["off", "on"],
},
{
title: "Scroll speed",
category: "Input",
description: "Distance per input tick",
path: ["scroll", "speed"],
default: 3,
step: 0.25,
min: 0.25,
max: 10,
format: (value) => Number(value).toFixed(2),
},
{
title: "Acceleration",
category: "Input",
description: "Repeated scrolling",
path: ["scroll", "acceleration"],
default: false,
values: [false, true],
labels: ["off", "on"],
},
{
title: "Mouse",
category: "Input",
description: "Terminal mouse capture",
path: ["mouse"],
default: true,
values: [false, true],
labels: ["off", "on"],
},
{
title: "Editor context",
category: "Input",
description: "Active selection in prompts",
path: ["prompt", "editor"],
default: true,
values: [false, true],
labels: ["off", "on"],
},
{
title: "Large pastes",
category: "Input",
description: "Paste display style",
path: ["prompt", "paste"],
default: "compact",
values: ["compact", "full"],
},
{
title: "Leader timeout",
category: "Input",
description: "Wait after leader key",
path: ["leader", "timeout"],
default: 2000,
step: 250,
min: 250,
max: 10000,
format: (value) => `${value} ms`,
},
{
title: "Attention",
category: "Alerts",
description: "Alerts when input is needed",
path: ["attention", "enabled"],
default: false,
values: [false, true],
labels: ["off", "on"],
},
{
title: "Notifications",
category: "Alerts",
description: "System notifications",
path: ["attention", "notifications"],
default: true,
values: [false, true],
labels: ["off", "on"],
},
{
title: "Sounds",
category: "Alerts",
description: "Attention sounds",
path: ["attention", "sound"],
default: true,
values: [false, true],
labels: ["off", "on"],
},
{
title: "Volume",
category: "Alerts",
description: "Attention sound level",
path: ["attention", "volume"],
default: 0.4,
step: 0.1,
min: 0,
max: 1,
format: (value) => `${Math.round(Number(value) * 100)}%`,
},
{
title: "Window title",
category: "Terminal",
description: "Update terminal title",
path: ["terminal", "title"],
default: true,
values: [false, true],
labels: ["off", "on"],
},
]
export function DialogConfig() {
const config = useConfig()
const dialog = useDialog()
const toast = useToast()
const themeState = useTheme()
const { theme } = themeState
const dimensions = useTerminalDimensions()
const [selected, setSelected] = createSignal(0)
const [saving, setSaving] = createSignal(false)
let scroll: ScrollBoxRenderable | undefined
onMount(() => {
dialog.setSize("xlarge")
dialog.setCentered(true)
})
const value = (setting: Setting) => {
const current = setting.path.reduce<unknown>((result, key) => {
if (!result || typeof result !== "object") return undefined
return (result as Record<string, unknown>)[key]
}, config.data)
if (setting.path.join(".") === "theme.name") return current ?? themeState.selected
return current ?? setting.default
}
const values = (setting: Setting) =>
setting.path.join(".") === "theme.name"
? Object.keys(themeState.all()).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }))
: setting.values
const display = (setting: Setting) => {
const current = value(setting)
if (setting.format) return setting.format(current)
const index = setting.values?.indexOf(current)
return index === undefined || index < 0 ? String(current) : (setting.labels?.[index] ?? String(current))
}
const rows = createMemo(() =>
settings.map((setting, index) => ({
setting,
index,
heading: index === 0 || settings[index - 1].category !== setting.category,
})),
)
const split = createMemo(() => dimensions().width >= 110)
const height = createMemo(() => Math.max(8, Math.min(36, dimensions().height - 12)))
function move(direction: number) {
const next = (selected() + direction + settings.length) % settings.length
setSelected(next)
queueMicrotask(() => {
if (!scroll) return
const row =
next +
settings.slice(0, next + 1).filter((setting, index) => {
return index === 0 || settings[index - 1].category !== setting.category
}).length
if (row < scroll.scrollTop) scroll.scrollTo(row)
if (row >= scroll.scrollTop + scroll.viewport.height) scroll.scrollTo(row - scroll.viewport.height + 1)
})
}
async function change(direction: number) {
if (saving()) return
const setting = settings[selected()]
const current = value(setting)
const choices = values(setting)
const next = choices
? choices[(choices.indexOf(current) + direction + choices.length) % choices.length]
: Math.min(setting.max!, Math.max(setting.min!, Number(current) + direction * setting.step!))
if (next === current) return
setSaving(true)
await config
.update((draft) => {
const parent = setting.path.slice(0, -1).reduce<Record<string, unknown>>((result, key) => {
if (!result[key] || typeof result[key] !== "object") result[key] = {}
return result[key] as Record<string, unknown>
}, draft)
parent[setting.path.at(-1)!] = next
})
.catch(toast.error)
.finally(() => setSaving(false))
}
useBindings(() => ({
bindings: [
{
key: "up",
desc: "Previous setting",
group: "Settings",
cmd: () => move(-1),
},
{
key: "down",
desc: "Next setting",
group: "Settings",
cmd: () => move(1),
},
{ key: "left", desc: "Previous value", group: "Settings", cmd: () => void change(-1) },
{ key: "right", desc: "Next value", group: "Settings", cmd: () => void change(1) },
{ key: "return", desc: "Next value", group: "Settings", cmd: () => void change(1) },
],
}))
return (
<box flexDirection="row" height={height() + 1}>
<box width={split() ? "54%" : "100%"} paddingLeft={4} paddingRight={split() ? 3 : 4} paddingBottom={1}>
<text fg={theme.text} attributes={TextAttributes.BOLD} paddingBottom={1}>
Settings
</text>
<scrollbox
ref={(element: ScrollBoxRenderable) => (scroll = element)}
flexGrow={1}
scrollbarOptions={{ visible: false }}
>
<For each={rows()}>
{(row) => (
<>
<Show when={row.heading}>
<box paddingTop={row.index === 0 ? 0 : 1}>
<text fg={theme.primary} attributes={TextAttributes.BOLD}>
{row.setting.category}
</text>
</box>
</Show>
<box flexDirection="row" height={1}>
<text
width={25}
fg={row.index === selected() ? theme.text : theme.textMuted}
attributes={row.index === selected() ? TextAttributes.BOLD : undefined}
>
{row.setting.title}
</text>
<box flexGrow={1} flexDirection="row" justifyContent="flex-end">
<box flexDirection="row">
<text fg={theme.textMuted}>{row.index === selected() ? " " : " "}</text>
<text
fg={row.index === selected() ? theme.primary : theme.textMuted}
attributes={row.index === selected() ? TextAttributes.BOLD : undefined}
>
{display(row.setting)}
</text>
<text fg={theme.textMuted}>{row.index === selected() ? " " : " "}</text>
</box>
</box>
</box>
</>
)}
</For>
</scrollbox>
</box>
<Show when={split()}>
<box
position="relative"
top={-1}
width="46%"
height={height() + 2}
paddingTop={1}
paddingLeft={2}
paddingRight={2}
backgroundColor={theme.backgroundElement}
>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.primary} attributes={TextAttributes.BOLD}>
{settings[selected()].title}
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingTop={1}>
<text fg={theme.text} wrapMode="word">
{settings[selected()].detail ?? settings[selected()].description}
</text>
</box>
</box>
</Show>
</box>
)
}
+2 -2
View File
@@ -92,7 +92,7 @@ export function DialogMcp() {
when={detail()}
fallback={
<DialogSelect
title="MCPs"
title="MCP servers"
options={options()}
current={focused()}
preserveSelection
@@ -152,7 +152,7 @@ function DialogMcpError(props: { server: McpServer; onBack: () => void }) {
<box paddingLeft={4} paddingRight={4} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
MCP / {props.server.name}
MCP server: {props.server.name}
</text>
<text fg={theme.textMuted} onMouseUp={props.onBack}>
esc back
+4 -2
View File
@@ -22,9 +22,11 @@ export function DialogStatus() {
esc
</text>
</box>
<Show when={mcp().length > 0} fallback={<text fg={theme.text}>No MCP Servers</text>}>
<Show when={mcp().length > 0} fallback={<text fg={theme.text}>No MCP servers</text>}>
<box>
<text fg={theme.text}>{mcp().length} MCP Servers</text>
<text fg={theme.text}>
{mcp().length} MCP server{mcp().length === 1 ? "" : "s"}
</text>
<For each={mcp()}>
{(item) => (
<box flexDirection="row" gap={1}>
+3 -1
View File
@@ -195,7 +195,9 @@ export function ConfigProvider(props: {
setConfig(reconcile(resolve(info, props.options ?? { terminalSuspend: true })))
return info
}
return <ConfigContext.Provider value={{ data: config, update }}>{props.children}</ConfigContext.Provider>
return (
<ConfigContext.Provider value={{ data: config, update }}>{props.children}</ConfigContext.Provider>
)
}
export function useConfig() {
+9
View File
@@ -129,6 +129,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
if (theme) setStore("active", theme)
})
createEffect(() => {
const mode = config.theme?.mode
if (mode === "dark" || mode === "light") {
pin(mode)
return
}
if (mode === "system" && store.lock !== undefined) free()
})
function syncCustomThemes() {
return themes
.discover()
-1
View File
@@ -60,7 +60,6 @@ export type PluginRuntime = ReturnType<typeof createPluginRuntime>
export type TuiPluginHost = {
start(input: {
api: TuiPluginApi
config: any
runtime: PluginRuntime
dispose?: () => void
}): Promise<void>
@@ -6,8 +6,13 @@ import { useToast } from "../../ui/toast"
import { useSDK } from "../../context/sdk"
import { errorMessage } from "../../util/error"
import { DialogFork } from "./dialog-fork"
import type { PromptInfo } from "../../prompt/history"
export function DialogMessage(props: { messageID: string; sessionID: string }) {
export function DialogMessage(props: {
messageID: string
sessionID: string
setPrompt?: (prompt: PromptInfo) => void
}) {
const data = useData()
const clipboard = useClipboard()
const toast = useToast()
@@ -22,9 +27,26 @@ export function DialogMessage(props: { messageID: string; sessionID: string }) {
title: "Revert",
value: "session.revert",
description: "undo messages and file changes",
onSelect: async (dialog) => {
await sdk.api.session
.revert.stage({ sessionID: props.sessionID, messageID: props.messageID })
onSelect: (dialog) => {
const value = message()
if (value?.type === "user") {
props.setPrompt?.({
text: value.text,
files: value.files?.map((file) => ({
uri: file.source.type === "uri" ? file.source.uri : `data:${file.mime};base64,${file.data}`,
name: file.name,
description: file.description,
mention: file.mention ? { ...file.mention } : undefined,
})),
agents: value.agents?.map((agent) => ({
name: agent.name,
mention: agent.mention ? { ...agent.mention } : undefined,
})),
pasted: [],
})
}
void sdk.api.session.revert
.stage({ sessionID: props.sessionID, messageID: props.messageID })
.catch((error) => toast.show({ message: errorMessage(error), variant: "error", duration: 5000 }))
dialog.clear()
},
+8 -1
View File
@@ -1468,6 +1468,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
)
const dialog = useDialog()
const renderer = useRenderer()
const promptRef = usePromptRef()
return (
<Show when={props.message.text.trim() || files().length}>
@@ -1486,7 +1487,13 @@ function UserMessage(props: { message: SessionMessageUser }) {
}}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
dialog.replace(() => <DialogMessage messageID={props.message.id} sessionID={ctx.sessionID} />)
dialog.replace(() => (
<DialogMessage
messageID={props.message.id}
sessionID={ctx.sessionID}
setPrompt={(value) => promptRef.current?.set(value)}
/>
))
}}
paddingTop={1}
paddingBottom={1}
@@ -292,7 +292,7 @@ The `mcp debug` command shows the current auth status, tests HTTP connectivity,
## Manage
Your MCPs are available as tools in OpenCode, alongside built-in tools. So you can manage them through the OpenCode config like any other tool.
Tools from your MCP servers are available in OpenCode alongside built-in tools. You can manage them through the OpenCode config like any other tool.
---
@@ -319,7 +319,7 @@ This means that you can enable or disable them globally.
}
```
We can also use a glob pattern to disable all matching MCPs.
We can also use a glob pattern to disable all matching MCP tools.
```json title="opencode.json" {14}
{
@@ -340,7 +340,7 @@ We can also use a glob pattern to disable all matching MCPs.
}
```
Here we are using the glob pattern `my-mcp*` to disable all MCPs.
Here we are using the glob pattern `my-mcp*` to disable all matching MCP tools.
---
+6
View File
@@ -0,0 +1,6 @@
.source
.tanstack
.wrangler
dist
node_modules
worker-configuration.d.ts
+22
View File
@@ -0,0 +1,22 @@
# OpenCode website
The server-rendered `opencode.ai` website. It uses TanStack Start on Cloudflare Workers and serves the V2 documentation with Fumadocs.
## Development
From this directory, run:
```bash
bun dev
```
The site opens at `http://localhost:3000`; documentation is available at `http://localhost:3000/docs`.
## Verification
```bash
bun typecheck
bun run build
```
The existing `packages/web`, `packages/docs`, and `packages/stats/app` deployments remain in place until their routes have been migrated and verified.
+284
View File
@@ -0,0 +1,284 @@
---
title: "Agents"
description: ""
---
Agents combine a system prompt, model preference, tool permissions, and display
metadata into a reusable assistant profile. OpenCode includes agents for common
workflows, and you can override them or add your own in configuration or
Markdown files.
## Built-in agents
| Agent | Mode | Purpose |
| --- | --- | --- |
| **Build** (`build`) | `primary` | Default coding agent. Tools are allowed by default, sensitive environment-file reads ask for approval, and access outside the workspace asks for approval. |
| **Plan** (`plan`) | `primary` | Planning agent. File edits are denied except for OpenCode plan files. Shell commands are not generally denied. |
| **General** (`general`) | `subagent` | General-purpose research and multi-step work. It has broad tool access but cannot launch more subagents. |
| **Explore** (`explore`) | `subagent` | Read-only code and web exploration using `read`, `glob`, `grep`, `webfetch`, and `websearch`. |
OpenCode also has hidden `compaction`, `title`, and `summary` system agents.
They run internal maintenance tasks and are not selectable. There is no built-in
`scout` agent in V2.
You can override a built-in agent with an entry of the same ID. Set
`disabled: true` to remove one.
## Default agent
Set the primary agent used when a session has not selected one:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"default_agent": "reviewer"
}
```
The configured agent must exist, must not have `mode: "subagent"`, and must not
be hidden. If it is unavailable, OpenCode falls back to `build`, then to the
first visible agent that can run as a primary agent. This selection does not
rewrite the agent already stored on an existing session.
## Modes
An agent's `mode` controls where it can run:
| Mode | Behavior |
| --- | --- |
| `primary` | Can be selected as the main agent for a session. It cannot be launched as a subagent. |
| `subagent` | Can run in a child session through the `subagent` tool, but cannot be selected as the main agent. |
| `all` | Can be used either way. This is the default for a custom agent when `mode` is omitted. |
In the TUI, press <kbd>Tab</kbd> and <kbd>Shift</kbd>+<kbd>Tab</kbd> to cycle
through visible primary and `all` agents, or use `/agents` to choose one.
Subagents run in child sessions with fresh context. A primary agent can invoke
one with the `subagent` tool, either in the foreground or in the background.
You can also `@` mention a visible subagent to ask the current agent to delegate
work to it:
```text
@explore find where authentication errors are handled
```
The parent agent's `subagent` permission controls which agents it may launch.
The child currently uses its own configured permissions, not a restricted copy
of the parent's permissions.
## Configure agents
### Markdown files
The recommended file locations are:
```text
~/.config/opencode/agents/<name>.md
.opencode/agents/<name>.md
```
OpenCode discovers project `.opencode` directories from the current directory
up to the project root. The path below `agents/` becomes the agent ID, so
`.opencode/agents/team/reviewer.md` defines `team/reviewer`.
Frontmatter uses the same fields as an entry under `agents`. The Markdown body
becomes `system`:
```md title=".opencode/agents/reviewer.md"
---
description: Reviews changes without modifying files
mode: subagent
model: anthropic/claude-sonnet-4-5#high
color: warning
steps: 8
permissions:
- action: edit
resource: "*"
effect: deny
- action: shell
resource: "*"
effect: deny
---
Review for correctness, security, regressions, and missing tests.
List findings in severity order with file and line references.
```
### JSON or JSONC
Use the `agents` field in any [OpenCode configuration file](/docs/config):
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"default_agent": "reviewer",
"agents": {
"reviewer": {
"description": "Reviews changes for correctness, security, and missing tests",
"mode": "all",
"model": "anthropic/claude-sonnet-4-5#high",
"system": "Review the current changes. Report findings before any summary.",
"color": "warning",
"steps": 8,
"permissions": [
{ "action": "edit", "resource": "*", "effect": "deny" },
{ "action": "shell", "resource": "*", "effect": "deny" }
]
},
"build": {
"permissions": [
{ "action": "shell", "resource": "git push *", "effect": "ask" }
]
}
}
}
```
Agent definitions merge in configuration order. Later scalar fields replace
earlier values, request maps merge by key, and permission rules are appended.
Global `permissions` are applied to every agent before its agent-specific rules,
so a later agent rule can refine a global rule.
## Options
### `description`
Explains the agent's purpose. It is optional, but strongly recommended for
subagents because OpenCode includes it in the subagent catalog shown to the
model.
### `mode`
Accepts `primary`, `subagent`, or `all`. The default is `all`.
### `model`
Selects a model using `provider/model` with an optional `#variant`:
```jsonc
{
"agents": {
"reviewer": {
"model": "anthropic/claude-sonnet-4-5#high"
}
}
}
```
The equivalent expanded form is:
```jsonc
{
"agents": {
"reviewer": {
"model": {
"providerID": "anthropic",
"model": "claude-sonnet-4-5",
"variant": "high"
}
}
}
}
```
The TUI uses this as the preferred model when the agent is selected. A child
session uses its subagent's configured model, or inherits the parent session's
model when none is configured. In the API, the session's selected model is
stored separately; creating or switching a primary session with only an agent
ID does not itself change that session model.
### `system`
Sets the agent's system prompt. A non-empty value replaces OpenCode's
provider-specific base prompt for that agent. Project instructions, skills,
references, and other instruction sources are still added separately.
For a Markdown agent, use the document body instead of a `system` frontmatter
field.
### `permissions`
Permissions are an ordered array of rules:
```jsonc
{
"agents": {
"orchestrator": {
"permissions": [
{ "action": "subagent", "resource": "*", "effect": "deny" },
{ "action": "subagent", "resource": "explore", "effect": "allow" },
{ "action": "shell", "resource": "git *", "effect": "ask" }
]
}
}
}
```
Each rule has:
| Field | Meaning |
| --- | --- |
| `action` | Tool or permission action, with `*` wildcards supported. |
| `resource` | The path, command, agent ID, or other resource matched by the action. Wildcards are supported. |
| `effect` | `allow`, `ask`, or `deny`. |
The last matching rule wins. Important V2 action names include `shell` for
shell commands, `edit` for all edit/write/patch tools, and `subagent` for child
agents. Other tools generally use their tool name, such as `read`, `glob`,
`grep`, `webfetch`, `websearch`, and `skill`.
<Tip>
Put broad wildcard rules first and exceptions afterward. For example, deny
all subagents first, then allow `explore`.
</Tip>
`~` and `$HOME` are expanded in filesystem resources for `read`, `edit`, and
`external_directory`. Shell resources are raw command text and are not
expanded.
### `steps`
Sets a positive maximum number of model steps. On the final allowed step,
OpenCode removes tools and asks the model to summarize its work in text. New
user input resets the allowance.
### `hidden`
When `true`, removes the agent from normal selectors, `@` autocomplete, and the
subagent catalog advertised to models. It is a visibility setting, not a
security boundary.
### `color`
Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`, or one
of `primary`, `secondary`, `accent`, `success`, `warning`, `error`, or `info`.
### `disabled`
When `true`, removes the agent definition at that point in configuration
loading. This works for built-in and custom agents.
### `request`
The V2 schema accepts per-agent request `headers` and JSON `body` overlays:
```jsonc
{
"agents": {
"reviewer": {
"request": {
"headers": { "x-agent": "reviewer" },
"body": { "temperature": 0.1 }
}
}
}
}
```
<Warning>
The current V2 session runner preserves these overlays on the agent
definition but does not yet apply them to model requests. Configure effective
request settings on the provider, model, or model variant instead. Do not use
legacy top-level agent fields such as `temperature`, `top_p`, `prompt`,
`permission`, `tools`, `disable`, or `maxSteps` in new V2 configuration.
</Warning>
@@ -0,0 +1,168 @@
---
title: "Attachments"
description: ""
---
OpenCode can add local context to a prompt as text or image media. Current V2
sessions make these attachment types visible to the model:
| Input | Model receives |
| --- | --- |
| UTF-8 text file | The filename and decoded text |
| Directory | A non-recursive listing of its immediate files and directories |
| PNG, JPEG, GIF, or WebP | Image media |
SVG files are treated as text, not image media. PDF, AVIF, BMP, audio, video,
and other binary prompt attachments are not currently included in the model
request. Some clients may let you select a PDF, but V2 does not yet make that
PDF visible to the model.
<Warning>
Use a model that supports image input before attaching an image. OpenCode
passes supported image media to the selected provider, but the provider and
model still enforce their own formats, dimensions, file counts, and size
limits. A text-only model may reject the request.
</Warning>
## Add attachments
### TUI
Type `@` followed by a filename and select the result to attach a project file.
This is the preferred way to add source code and other text files:
```text
Explain the error handling in @src/server.ts
```
Paste an image from the clipboard with the configured paste key, `Ctrl+V` by
default. You can also drag a supported image into a terminal that exposes the
dropped file path to the TUI. The TUI reads PNG, JPEG, GIF, and WebP as image
attachments; a dropped SVG is inserted as text.
### Desktop and web
Use **Attach file**, paste, or drag and drop. Attach UTF-8 text or a PNG, JPEG,
GIF, or WebP image. The desktop file picker limits one selection to 20 MiB in
total; the server also applies the per-attachment limit described below.
### CLI
Pass `--file` or `-f` to `opencode2 run`. Repeat the flag for multiple files:
```bash
opencode2 run -f src/server.ts -f screenshot.png "Explain the failure"
```
The run command accepts at most 100 file flags and reads at most 10 MiB per
file. Use it for text files and the four supported image formats; other binary
files do not become model context.
### API
The V2 prompt and command payloads accept a `files` array. Each item requires a
`uri` and can include `name` and `description`:
```bash
opencode2 api post /api/session/ses_example/prompt --data '{
"text": "Review this file",
"files": [
{
"uri": "file:///home/me/project/src/server.ts",
"name": "server.ts",
"description": "Request handler"
}
]
}'
```
Use an absolute `file:` URL for a file available to the server, or an inline
data URL:
```json
{
"text": "What is wrong with this layout?",
"files": [
{
"uri": "data:image/png;base64,<base64-data>",
"name": "layout.png"
}
]
}
```
HTTP and HTTPS attachment URLs are not supported. OpenCode materializes each
attachment before admitting the prompt and rejects invalid URLs, unreadable
paths, non-files other than directories, and attachments over 20 MiB decoded.
For a text `file:` URL, optional positive `start` and `end` query parameters
select one-based lines:
```text
file:///home/me/project/src/server.ts?start=20&end=60
```
The server infers the media type from the bytes. A supplied filename or data
URL media type does not make an unsupported binary format model-visible.
## Configure image processing
Configure image normalization in `opencode.json` or `opencode.jsonc`:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"attachments": {
"image": {
"auto_resize": true,
"max_width": 2000,
"max_height": 2000,
"max_base64_bytes": 5242880
}
}
}
```
All fields are optional:
| Field | Default | Behavior |
| --- | ---: | --- |
| `auto_resize` | `true` | Resize an image that exceeds any configured limit. If `false`, reject it. |
| `max_width` | `2000` | Maximum width in pixels. Must be a positive integer. |
| `max_height` | `2000` | Maximum height in pixels. Must be a positive integer. |
| `max_base64_bytes` | `5242880` | Maximum byte length of the Base64-encoded image string. Must be a positive integer. |
<Note>
In the current V2 runtime, these settings apply to image media produced by
the built-in `read` tool. Images attached directly through the TUI, desktop,
web, CLI, or API bypass this normalization. Resize direct attachments before
adding them if the provider requires smaller media.
</Note>
The `read` tool recognizes PNG, JPEG, GIF, and WebP by their contents and will
ingest at most 20 MiB of source image bytes. It decodes the image and compares
its width, height, and encoded Base64 length with all three configured limits.
When `auto_resize` is `true`, OpenCode preserves the aspect ratio, scales the
image down to the dimension limits, and tries progressively smaller PNG and
JPEG encodings until the Base64 limit is met. The resulting media type can
therefore change to PNG or JPEG. If no encoding fits, the tool call fails.
When `auto_resize` is `false`, exceeding any limit fails the tool call without
modifying the image. An image that cannot be decoded also fails. If the image
resizer cannot be loaded, the `read` tool returns the
original image instead, so these settings are processing limits rather than an
upload or security boundary.
## Limits and provider behavior
- Direct prompt attachments are limited to 20 MiB decoded per item by the V2
server. Client-specific limits can be lower.
- `max_base64_bytes` counts the encoded Base64 characters in bytes, not the
decoded file size and not the complete `data:` URL.
- Text attachments are inserted into the prompt as text and do not require a
multimodal model. Large text read through the `read` tool has separate
paging and truncation limits.
- Image attachments use provider-native image input. Provider errors can still
occur when OpenCode's limits pass but the selected model's limits do not.
- PDFs and other unsupported binary prompt attachments should be converted to
text or supported images before attaching them.
@@ -0,0 +1,163 @@
---
title: "Commands"
description: ""
---
Custom commands turn a named prompt template into a slash command. Type the
command in the TUI, followed by any arguments:
```text
/review src/auth
```
## Configure with Markdown
OpenCode discovers `.md` command files in `commands/` directories:
```text
~/.config/opencode/commands/ # Global
.opencode/commands/ # Project
```
Files may be nested; for example, `.opencode/commands/team/review.md` defines
`/team/review`. Files with other extensions, including `.mdx`, are not
discovered.
```md title=".opencode/commands/review.md"
---
description: Review code for correctness and missing tests
agent: plan
model: anthropic/claude-sonnet-4-5#high
---
Review $ARGUMENTS. Report bugs first, then missing tests.
```
The file body, with surrounding whitespace removed, is the command template.
JSON and Markdown commands share one registry. Project definitions take
precedence over global definitions, and a later definition can override a
built-in or earlier command with the same name. Changes are reloaded
automatically.
Run it with:
```text
/review src/auth
```
## Configure with JSON
Add commands under the `commands` key in any OpenCode JSON or JSONC
[configuration file](/docs/config). Each entry's key is the command name and
`template` is required.
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"commands": {
"review": {
"description": "Review code for correctness and missing tests",
"template": "Review $ARGUMENTS. Report bugs first, then missing tests.",
"agent": "plan",
"model": "anthropic/claude-sonnet-4-5#high"
}
}
}
```
## Fields
| Field | Required | Behavior |
| --- | --- | --- |
| `template` | JSON only | Prompt template. In a Markdown command, the file body supplies it. |
| `description` | No | Text shown with the command in autocomplete. |
| `agent` | No | Agent selected before the prompt runs. |
| `model` | No | Model override in `provider/model` or `provider/model#variant` format. |
| `subtask` | No | Accepted as a boolean, but currently has no execution effect in V2. |
The four optional fields can be used in JSON or YAML frontmatter. Do not put
`template` in frontmatter because the Markdown body always supplies it.
## Arguments
Use `$ARGUMENTS` for the complete argument string:
```md title=".opencode/commands/component.md"
---
description: Create a component
---
Create a typed React component named $ARGUMENTS.
```
```text
/component Button
```
Use `$1`, `$2`, and higher numbers for parsed positional arguments. Single and
double quotes group text containing spaces and are removed during parsing.
```md title=".opencode/commands/check.md"
---
description: Check one area with a specific focus
---
Check $1. Focus on $2.
```
```text
/check src/auth "error handling and missing tests"
```
The highest-numbered positional placeholder present in the template consumes
that argument and all remaining arguments. For example, if a template contains
only `$1`, then `$1` receives the full parsed argument list. Missing positions
become empty strings.
If a template contains neither positional placeholders nor `$ARGUMENTS`,
OpenCode appends non-empty arguments to the template after a blank line.
## Shell interpolation
Wrap a shell command in `!` followed by backticks to insert its output before
the prompt is submitted:
```md title=".opencode/commands/review-diff.md"
---
description: Review the current diff
---
Review this diff:
!`git diff --stat && git diff`
```
OpenCode runs each interpolation with the configured shell in the active
project location and inserts its combined output into the template. Argument
interpolation happens first, so avoid placing untrusted arguments inside shell
interpolations.
<Warning>
Shell interpolations run when the command is evaluated, outside the agent's
tool permission flow. Only use commands from sources you trust.
</Warning>
No other template interpolation is performed. In particular, an `@path`
written into a stored template remains ordinary prompt text; V2 does not
automatically attach that file.
## Agent, model, and execution
Running a command evaluates its arguments and shell blocks, submits the result
as a durable user prompt in the current session, and schedules normal model
execution.
If `agent` is set, it overrides the agent selected when the command was
invoked and becomes the session's active agent. If `model` is set, it overrides
the model. Otherwise, a model configured on the command's agent takes
precedence over the model selected at invocation.
Although `subtask` is accepted in JSON and frontmatter, V2 currently ignores
it: commands run in the current session and do not create a child session.
Selecting an agent whose mode is `subagent` also does not turn the command into
a subtask.
@@ -0,0 +1,153 @@
---
title: "Compaction"
description: ""
---
Compaction replaces the active model context from an older part of a session
with a generated checkpoint. The checkpoint contains a structured summary and
a serialized tail of recent context, so the agent can continue with more room
in the model's context window.
Compaction is lossy, but it does not delete the earlier durable session
messages. After a successful compaction, V2 builds model requests from the
latest completed checkpoint and the messages that follow it.
## Automatic compaction
Automatic compaction is enabled by default. Before a model call, V2 estimates
the size of the final system prompt, messages, and advertised tools. It starts
compaction when:
```text
estimated tokens > context limit - max(requested output tokens, buffer)
```
The estimate is approximate: V2 JSON-serializes the request and assumes four
characters per token. When compaction succeeds, V2 rebuilds the request from
the new checkpoint and retries the step without promoting the input again.
V2 also recognizes provider errors classified as context overflow. If an
overflow occurs before the provider produces assistant output or other retry
evidence, V2 can compact and retry that step once. This recovery is attempted
even when `auto` is `false`; `auto` controls only the preflight size check. A
second overflow after recovery is returned as an error.
## Manual compaction
In the TUI, run:
```text
/compact
```
`/summarize` is an alias. The default keybind is `<leader>c`, configured as
`session_compact`.
A manual request is durably admitted and wakes the session runner. It can
compact short histories that would not trigger automatic compaction. If the
session is busy, compaction runs at the next safe drain boundary before later
steered or queued prompts are promoted. Repeated requests while one is pending
coalesce into that pending request. Whether compaction completes or fails, the
barrier is then settled so later prompts can proceed.
The CLI has no separate `compact` subcommand. Use the TUI command or the server
API. For example:
```bash
opencode2 api v2.session.compact \
--param sessionID=ses_example \
--data '{}'
```
The equivalent raw request is:
```bash
opencode2 api post /api/session/ses_example/compact --data '{}'
```
`POST /api/session/:sessionID/compact` returns the admitted compaction input;
it does not wait for summary generation. Clients can call
`client.session.compact({ sessionID })` and then wait for the session or follow
the `session.compaction.*` events. Supplying an optional message `id` makes an
exact retry idempotent, but reusing an ID owned by another record returns a
conflict.
## Configuration
Add `compaction` to any [OpenCode configuration file](/docs/config):
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"compaction": {
"auto": true,
"prune": false,
"keep": {
"tokens": 8000
},
"buffer": 20000
}
}
```
| Field | Default | V2 behavior |
| --- | ---: | --- |
| `auto` | `true` | Runs the preflight context-size check. It does not disable manual compaction or one-shot provider-overflow recovery. |
| `prune` | None | Accepted by the V2 schema, but currently has no runtime effect. V2 does not prune old tool outputs in place. |
| `keep.tokens` | `8000` | Approximate number of tokens from the newest serialized conversation context to retain beside the summary. |
| `buffer` | `20000` | Token reserve used by the automatic threshold. The requested model output allowance wins when it is larger. |
`keep.tokens` and `buffer` accept non-negative integers. Larger `keep.tokens`
preserves more recent detail but leaves less room for future work. Larger
`buffer` triggers preflight compaction earlier.
## Checkpoint contents
V2 uses the session's selected or default model to generate the summary, with
tools disabled and at most 4096 output tokens. The summary records the
objective, important details, completed and active work, blockers, next moves,
and relevant files.
The newest serialized context up to `keep.tokens` is retained separately. This
is not a byte-for-byte transcript: tool output is limited to 2000 characters,
and file or media attachments become textual descriptors rather than embedded
data. On later compactions, V2 updates the previous summary and carries forward
its retained recent context before selecting a new tail.
The completed compaction is presented to the model as historical conversation
context, explicitly not as new instructions. Running and failed compactions are
not included in model context.
## Compaction advances the instruction epoch
Conversation compaction and instruction synchronization are separate. Before
promoting pending input, V2 compares live instruction sources with the latest
admitted values. Ordinary changes become durable value deltas; their
model-facing System messages are derived during request assembly rather than
persisted.
Completed compaction advances the instruction epoch at the exact ended-event
sequence and makes the currently admitted values initial. It does not reread
sources or publish an instruction event. Session movement and committed revert
clear the instruction fold so the next safe boundary requires one complete
source read. See [Instructions](/docs/instructions) for source ordering and update
behavior.
## Current limitations
- `prune` is reserved configuration; V1-style in-place tool-output pruning is
not implemented in V2.
- Compaction requires a resolvable model with a positive catalog context limit.
There is no separate compaction-model setting or fallback model.
- Summary generation can fail if the summary prompt itself cannot fit beside
its output allowance, the model returns no summary, or the provider fails.
- Automatic and overflow compaction need older conversation context that can be
replaced. A provider overflow can still surface when there is no compressible
head or fixed instructions and tool schemas dominate the request.
- Overflow recovery retries only once per step. Token estimation is heuristic,
so it cannot prevent every provider-specific overflow.
- Earlier durable messages remain stored even though they are no longer in the
active model context.
V1 used additional tail-turn and pruning behavior. Those V1 details are only
migration context; the settings and behavior on this page describe V2.
+450
View File
@@ -0,0 +1,450 @@
---
title: "Config"
description: ""
---
<Tip>
You shouldn't have to configure OpenCode manually. Ask OpenCode to update its configuration for you.
</Tip>
## Format
OpenCode supports both **JSON** and **JSONC** (JSON with Comments) configuration files.
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"model": "openai/gpt-5.2-custom",
"providers": {
"openai": {
"models": {
"gpt-5.2-custom": {
"modelID": "gpt-5.2",
"name": "GPT-5.2 Custom"
}
}
}
}
}
```
## Locations
OpenCode loads global configuration from:
```text
~/.config/opencode/opencode.json(c)
```
Project-specific configuration can use either form:
```text
/home/user/projects/my-app/opencode.json(c)
/home/user/projects/my-app/.opencode/opencode.json(c)
```
When OpenCode starts, it searches for configuration files from the current
directory upward to the project root. It merges direct `opencode.json(c)` files
from the project root toward the current directory, then does the same for
files inside `.opencode` directories. A `.opencode` config therefore overrides
every direct config, even when the direct config is closer to the current
directory. Avoid mixing the two forms across one project hierarchy unless this
precedence is intentional.
For example, consider a monorepo with OpenCode started from
`/home/user/projects/acme/packages/web`:
```text
~/.config/opencode/opencode.json
/home/user/projects/acme/
├── opencode.json
└── packages/
└── web/
├── opencode.json
└── src/
```
OpenCode applies these files from lowest to highest precedence:
1. `~/.config/opencode/opencode.json`
2. `/home/user/projects/acme/opencode.json`
3. `/home/user/projects/acme/packages/web/opencode.json`
In this direct-config example, the package config overrides matching settings
from the repository config, which overrides matching settings from the global
config. Settings that do not conflict are preserved from every file.
## Schema
The complete OpenCode configuration schema is available at
[opencode.ai/config.json](https://opencode.ai/config.json).
Add the `$schema` field to your configuration file to enable validation and
autocomplete in editors that support JSON Schema:
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json"
}
```
Use the schema as the source of truth for available fields, accepted values,
and nested configuration shapes.
### Shell
Set the shell used by the terminal and shell tools.
```jsonc
{
"shell": "/bin/zsh"
}
```
### Model
Set the default model in `provider/model` format. The root default currently
does not retain a `#variant`; select variants in the TUI or on an agent or command.
```jsonc
{
"model": "anthropic/claude-sonnet-4-5"
}
```
See the [models guide](/docs/models) for model selection
and local models.
### Default agent
Choose the primary agent used when a session does not select one explicitly.
```jsonc
{
"default_agent": "build"
}
```
See the [agents guide](/docs/agents) for built-in and custom
agents.
### Autoupdate
Control automatic updates from the global config. Set this to `false` to
disable updates. The current beta treats `true` and `"notify"` identically and
automatically installs compatible non-major updates; project-level values are
ignored.
```jsonc
{
"autoupdate": false
}
```
### Sharing
Set the intended session sharing policy. V2 accepts this field, but session
sharing is not implemented yet.
```jsonc
{
"share": "manual"
}
```
See the [sharing guide](/docs/sharing) for more details.
### Username
Set a username for future display behavior. V2 accepts this field but does not
currently display it in conversations.
```jsonc
{
"username": "alice"
}
```
### Permissions
Define ordered rules that allow, deny, or ask before an agent uses a tool on a
matching resource.
```jsonc
{
"permissions": [
{
"action": "shell",
"resource": "git push *",
"effect": "ask"
}
]
}
```
See the [permissions guide](/docs/permissions) for rule matching and available actions.
### Agents
Override built-in agents or define specialized agents with their own model,
instructions, mode, and permissions.
```jsonc
{
"agents": {
"reviewer": {
"description": "Review changes without editing files",
"mode": "subagent",
"system": "Focus on correctness, security, and missing tests.",
"permissions": [
{ "action": "edit", "resource": "*", "effect": "deny" }
]
}
}
}
```
See the [agents guide](/docs/agents) for all agent options and file-based agents.
### Snapshots
Enable or disable filesystem snapshots used by undo and revert behavior.
```jsonc
{
"snapshots": false
}
```
See the [snapshots guide](/docs/snapshots) for undo and redo behavior.
### Watcher
Ignore files and directories that should not trigger filesystem updates.
```jsonc
{
"watcher": {
"ignore": ["dist/**", "coverage/**"]
}
}
```
### Formatter
Define formatter settings for compatibility and future use. V2 accepts this
field, but it does not run formatters yet.
```jsonc
{
"formatter": {
"prettier": {
"command": ["bunx", "prettier", "--write", "$FILE"],
"extensions": [".js", ".ts", ".tsx"]
}
}
}
```
See the [formatters guide](/docs/formatters) for accepted fields and current limitations.
### LSP
Define language server settings for compatibility and future use. V2 accepts
this field, but it does not start language servers yet.
```jsonc
{
"lsp": {
"typescript": {
"command": ["typescript-language-server", "--stdio"],
"extensions": [".ts", ".tsx"]
}
}
}
```
See the [LSP guide](/docs/lsp) for accepted fields and current limitations.
### Attachments
Control how oversized images loaded by the `read` tool are resized or rejected
before they are sent to a model.
```jsonc
{
"attachments": {
"image": {
"auto_resize": true,
"max_width": 2000,
"max_height": 2000,
"max_base64_bytes": 5242880
}
}
}
```
See the [attachments guide](/docs/attachments) for image processing and limits.
### Tool output
Set the maximum number of lines and bytes retained from a tool result.
```jsonc
{
"tool_output": {
"max_lines": 2000,
"max_bytes": 51200
}
}
```
### MCP
Configure local and remote Model Context Protocol servers. Global timeouts can
be overridden by an individual server.
```jsonc
{
"mcp": {
"servers": {
"playwright": {
"type": "local",
"command": ["bunx", "@playwright/mcp"]
}
}
}
}
```
See the [MCP guide](/docs/mcp-servers) for remote servers, OAuth, environment variables, and timeouts.
### Compaction
Control automatic context compaction and how much recent context it preserves.
```jsonc
{
"compaction": {
"auto": true,
"keep": {
"tokens": 8000
},
"buffer": 20000
}
}
```
See the [compaction guide](/docs/compaction) for automatic context management.
### Skills
Add directories or URLs that OpenCode should search for agent skills.
```jsonc
{
"skills": ["./team-skills", "https://example.com/.well-known/skills/"]
}
```
See the [skills guide](/docs/skills) for skill structure and automatic discovery under `.opencode/skills/`.
### Commands
Define reusable slash commands as named prompt templates.
```jsonc
{
"commands": {
"review": {
"description": "Review the current changes",
"template": "Review the current diff for correctness and missing tests."
}
}
}
```
See the [commands guide](/docs/commands) for arguments, models, agents, and file-based commands.
### Instructions
Declare additional instruction files, globs, or URLs. V2 accepts this field,
but does not load these entries yet; use `AGENTS.md` for active instructions.
```jsonc
{
"instructions": ["CONTRIBUTING.md", "docs/guidelines/*.md"]
}
```
See the [instructions guide](/docs/instructions) for project instructions and `AGENTS.md`.
### References
Make local directories or Git repositories available as named supporting
context.
```jsonc
{
"references": {
"docs": {
"path": "../product-docs",
"description": "Product behavior and terminology"
},
"effect": {
"repository": "Effect-TS/effect",
"branch": "main"
}
}
}
```
See the [references guide](/docs/references) for shorthand, visibility, and path resolution.
### Plugins
Load plugins from packages or local files. Use the object form when a plugin
accepts options.
```jsonc
{
"plugins": [
"opencode-example-plugin",
{
"package": "./plugins/local.ts",
"options": {
"enabled": true
}
}
]
}
```
See the [plugins guide](/docs/build/plugins) for plugin development and configuration.
### Providers
Configure providers and add or override their models, request settings,
headers, and model variants.
```jsonc
{
"providers": {
"openai": {
"models": {
"gpt-5.2-custom": {
"modelID": "gpt-5.2",
"name": "GPT-5.2 Custom",
"limit": {
"context": 200000,
"output": 32000
}
}
}
}
}
}
```
See the [providers guide](/docs/providers) for credentials, custom endpoints, provider packages, and model configuration.
@@ -0,0 +1,93 @@
---
title: "Formatters"
description: ""
---
OpenCode V2 accepts formatter configuration, but it does not yet include a
formatter runtime. File writes and edits are not automatically formatted.
<Warning>
V2 currently has no built-in formatters. The built-in formatter list and
automatic post-edit formatting documented for V1 do not apply to V2.
</Warning>
## Configuration
The `formatter` field accepts a boolean or an object keyed by formatter name:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"formatter": {
"prettier": {
"disabled": false,
"command": ["prettier", "--write", "$FILE"],
"environment": {
"NODE_ENV": "development"
},
"extensions": [".js", ".jsx", ".ts", ".tsx"]
}
}
}
```
This example is valid V2 configuration, but V2 does not currently execute the
command.
Each named formatter entry supports these optional fields:
| Field | Type | Current V2 behavior |
| --- | --- | --- |
| `disabled` | `boolean` | Accepted, but there is no runtime formatter to enable or disable. |
| `command` | `string[]` | Accepted as an argument array, but not executed. |
| `environment` | `Record<string, string>` | Accepts string environment variable names and values, but they are not applied. |
| `extensions` | `string[]` | Accepted without extension-specific validation, but files are not matched against it. |
All entry fields are optional. The schema therefore also accepts an empty entry
such as `"prettier": {}`.
## Enable and disable
The schema accepts all of the following forms:
```jsonc
// Omit `formatter`, or use false, when formatting is not requested.
{
"formatter": false
}
```
```jsonc
// Reserved for enabling all built-ins once a V2 runtime provides them.
{
"formatter": true
}
```
```jsonc
// Configure named entries or mark one as disabled.
{
"formatter": {
"prettier": { "disabled": true },
"custom": {
"command": ["custom-fmt", "$FILE"],
"extensions": [".foo"]
}
}
}
```
At present, omitted, `false`, `true`, and object forms have the same runtime
result: V2 runs no formatter. `disabled` is retained as configuration data but
does not control an executable formatter.
## Commands and placeholders
`command` is an array of strings, not a shell command string. `$FILE` is the V1
file-path placeholder and is often retained in migrated configuration. V2 does
not currently substitute `$FILE` or define another formatter placeholder.
Likewise, V2 does not currently use `extensions` to select commands, merge
`environment` into a child process, discover formatter executables or project
configuration, or run multiple matching formatters. These behaviors will only
be available after a V2 formatter runtime is implemented.
+160
View File
@@ -0,0 +1,160 @@
---
title: "Intro"
description: "Get started with OpenCode."
---
<Warning>
These docs are for the beta version of OpenCode, which will become OpenCode 2.0. The beta is still changing: we may
wipe your data, things may break, and APIs, configuration, and plugin APIs may change.
</Warning>
## Install
<Note>The curl install script is not available in beta.</Note>
You can also install it with the following package managers.
<Tabs>
<Tab title="npm">
```bash
npm install -g @opencode-ai/cli@next
```
</Tab>
<Tab title="bun">
```bash
bun install -g @opencode-ai/cli@next
```
</Tab>
<Tab title="pnpm">
```bash
pnpm install -g @opencode-ai/cli@next
```
</Tab>
<Tab title="Yarn">
```bash
yarn global add @opencode-ai/cli@next
```
</Tab>
</Tabs>
<Note>During beta, the binary is called `opencode2`.</Note>
### Homebrew
Homebrew installation is not available in beta.
### Arch Linux
Arch Linux installation is not available in beta.
### Windows
<Tip>
For the best experience on Windows, install [Windows Subsystem for Linux
(WSL)](https://learn.microsoft.com/windows/wsl/install), open your Linux distribution, and use one of the beta package
manager commands above.
</Tip>
<Tabs>
<Tab title="chocolatey">
Chocolatey installation is not available in beta.
</Tab>
<Tab title="scoop">
Scoop installation is not available in beta.
</Tab>
<Tab title="mise">
Mise installation is not available in beta.
</Tab>
<Tab title="docker">
Docker installation is not available in beta.
</Tab>
</Tabs>
Standalone binaries are not available in beta.
---
#### Prerequisites
To use OpenCode in your terminal, you'll need:
1. A modern terminal emulator like:
- [Ghostty](https://ghostty.org), Linux and macOS
- [WezTerm](https://wezterm.org), cross-platform
- [Alacritty](https://alacritty.org), cross-platform
- [Kitty](https://sw.kovidgoyal.net/kitty/), Linux and macOS
2. API keys for the LLM providers you want to use.
---
## Connect
With OpenCode you can use any LLM provider by configuring its API key.
Run `/connect` in the TUI and select your provider.
```text
/connect
```
If you'd like easy access to all the best coding models you can try out
[OpenCode Console](https://console.opencode.ai).
You can also try [OpenCode Go](https://opencode.ai/go) a $10/month subscription
plan that grants you access to the best open source models.
Use `/models` to browse the providers and models available to your project. See [Providers](/docs/providers) for connection and
configuration details.
---
## Usage
You are now ready to use OpenCode in your project. Here are a few common workflows.
### Ask questions
Ask OpenCode to explain your codebase.
<Tip>Use `@` to fuzzy search for files in the project.</Tip>
```text
How is authentication handled in @packages/functions/src/api/index.ts
```
### Add features
Ask OpenCode to add a feature by describing the desired behavior and providing relevant context.
```text
When a user deletes a note, flag it as deleted in the database.
Create a screen that shows recently deleted notes.
From this screen, the user can restore a note or permanently delete it.
```
<Tip>Give OpenCode plenty of context and examples.</Tip>
### Undo changes
Use `/undo` when a change isn't what you wanted.
```text
/undo
```
OpenCode stages a conversation revert and restores your original message so you can revise it. In a Git repository, it
also restores file changes when snapshots were captured successfully. Run `/undo` multiple times to move the conversation
boundary back, or use `/redo` to restore the staged conversation and files. See [Undo](/docs/snapshots) for
limitations and safety details.
```text
/redo
```
---
## Customize
Make OpenCode your own by [picking a theme](https://opencode.ai/docs/themes), [customizing
keybinds](https://opencode.ai/docs/keybinds), [configuring formatters](/docs/formatters), [creating commands](/docs/commands), or
editing the [OpenCode config](/docs/config).
@@ -0,0 +1,128 @@
---
title: "Instructions"
description: ""
---
Instructions are privileged context that guide an agent throughout a session.
V2 combines built-in context, discovered `AGENTS.md` files, and dynamic sources
such as skill, reference, MCP, and session context. It stores source values as
durable deltas, then renders initial instructions and chronological updates when
assembling each model request.
## AGENTS.md
Use `AGENTS.md` for persistent guidance such as build commands, architecture,
code conventions, and verification requirements. Commit project files so the
whole team receives the same instructions.
V2 loads:
1. The global file at `$XDG_CONFIG_HOME/opencode/AGENTS.md`, normally
`~/.config/opencode/AGENTS.md`.
2. Every `AGENTS.md` from the current Location up to and including the project
root.
For example, when the Location is `packages/web`, OpenCode can load all three
project files below:
```text
my-project/
├── AGENTS.md
└── packages/
├── AGENTS.md
└── web/
└── AGENTS.md
```
The files are combined rather than selecting a single winner. They are rendered
in this order: global, then project files from the Location toward the project
root. OpenCode does not resolve conflicts between their contents, so keep broad
guidance global and put scoped guidance in the relevant project directory.
If the Location is outside the project root, only the global file is loaded.
Setting `OPENCODE_DISABLE_PROJECT_CONFIG=1` also skips project `AGENTS.md`
discovery but does not disable the global file.
<Note>
Current V2 discovery only recognizes `AGENTS.md`. The `CLAUDE.md` fallback
and related precedence described by older OpenCode documentation do not apply.
</Note>
### Nested instructions
An `AGENTS.md` below the Location is not part of the initial upward scan. When
the read tool successfully reads a file or lists a directory, OpenCode discovers
`AGENTS.md` files from that target upward to, but not including, the Location.
It adds newly discovered files to the session in nearest-first order.
Each nested file is injected once per session and recorded in durable session
history. Reading the same area again does not inject it again. Consequently,
editing an already injected nested `AGENTS.md` does not replace its earlier
session entry automatically; start a new session if the updated text must apply
immediately.
## Config entries
The V2 config schema accepts an `instructions` array of strings:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"instructions": [
"CONTRIBUTING.md",
"docs/guidelines/*.md",
"https://example.com/shared-instructions.md"
]
}
```
Configuration is loaded from global through project-local files. If more than
one config defines `instructions`, the highest-precedence, closest config's
entire array is selected; arrays are not merged.
<Warning>
V2 currently parses and retains this field but does not resolve its entries
into instruction sources. Local files, glob patterns, and HTTP or HTTPS URLs
in `instructions` therefore do not reach the model yet. Use `AGENTS.md` for
active V2 instructions. URL fetching and timeout behavior documented for V1
are not supported by the current V2 implementation.
</Warning>
See [Config](/docs/config) for config locations and general precedence.
## Ordering
The selected agent or provider system prompt is sent first. OpenCode then sends
the session's initial instructions, composed in this order:
1. Built-in environment and date context.
2. Ambient `AGENTS.md` discovery.
3. Available skill, reference, and MCP guidance.
4. Session-specific instruction entries supplied through the API.
These sources are combined; ordering is not an override mechanism. Nested
`AGENTS.md` files discovered by reads are chronological session entries rather
than part of the initial instructions.
## Changes
Before promoting pending input, V2 compares live instruction sources with the
latest admitted source values:
- A new or changed ambient `AGENTS.md` aggregate is announced as a system update
that replaces the previous ambient aggregate.
- Removing all ambient files announces that the previous ambient instructions
no longer apply.
- A temporary read or discovery failure preserves the session's last known
instructions instead of treating them as deleted. If no instruction epoch
exists yet, pending input waits until every source is available.
- Completed conversation compaction advances the instruction epoch, making the
currently admitted values initial without rereading sources or authoring an
instruction event.
- Moving a session or committing a revert clears the instruction fold. The next
safe boundary requires one complete source read before promoting input.
The durable event stores changed source keys and value hashes, not rendered
prose. During request assembly, OpenCode renders the epoch's initial values and
interleaves later changes as chronological System messages. Clients see changed
keys but never the privileged value bodies.
+105
View File
@@ -0,0 +1,105 @@
---
title: "LSP"
description: ""
---
Language Server Protocol (LSP) integrations can provide code diagnostics,
symbols, definitions, references, and other language-aware context.
<Warning>
OpenCode V2 does not yet have an LSP runtime or built-in language servers.
The `lsp` configuration is accepted and preserved, but it does not currently
start or download servers, expose an LSP tool, or add diagnostics to file tool
results.
</Warning>
## Built-in servers
There are no built-in LSP servers in the current V2 implementation. Setting
`lsp` to `true` declares that built-ins should be enabled, but has no runtime
effect until V2 provides a server registry and LSP runtime.
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"lsp": true
}
```
## Configuration
The `lsp` field accepts a boolean or an object keyed by server name:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"lsp": {
"custom-typescript": {
"command": ["typescript-language-server", "--stdio"],
"extensions": [".ts", ".tsx"],
"env": {
"TSS_LOG": "-level verbose"
},
"initialization": {
"preferences": {
"importModuleSpecifierPreference": "relative"
}
}
}
}
}
```
Each enabled server entry has this shape:
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `command` | `string[]` | Yes | Executable followed by any arguments. |
| `extensions` | `string[]` | No | File extensions associated with the server, including the leading dot. |
| `disabled` | `boolean` | No | Disables the entry when `true`. |
| `env` | `Record<string, string>` | No | Environment variables for the server process. The property is named `env`, not `environment`. |
| `initialization` | `Record<string, unknown>` | No | Server-specific options for the LSP `initialize` request. |
The only entry that may omit `command` is the disable-only form:
```jsonc
{
"lsp": {
"typescript": {
"disabled": true
}
}
}
```
Server names are arbitrary. The V2 schema permits `extensions` to be omitted,
including for a custom server, although a future runtime will need a way to
associate that server with files.
## Disable LSP
Omit `lsp` when no configuration is needed. Set it to `false` to explicitly
disable the whole integration, including when a lower-priority configuration
set it to `true` or supplied an object:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"lsp": false
}
```
Use `{ "disabled": true }` under a server name to disable one server while
retaining the object form. `OPENCODE_DISABLE_LSP_DOWNLOAD` is not used by V2;
V2 currently performs no automatic LSP downloads.
## Current usage
V2 loads and validates the configuration shape for compatibility and future
integration. It does not currently use LSP when reading, writing, editing, or
patching files, and those tools do not notify a language server or return LSP
diagnostics.
For reliable feedback today, have the agent run the project's lint, typecheck,
test, or compiler commands. Record those commands in an `AGENTS.md` file or a
skill so the agent knows when and where to run them.
@@ -0,0 +1,262 @@
---
title: "MCP servers"
description: ""
---
OpenCode can connect to [Model Context Protocol](https://modelcontextprotocol.io/) servers and make their tools, prompts, and instructions available to agents. MCP tools consume model context, so enable only the servers you need.
## Configure servers
Define each server by a unique name under `mcp.servers` in your [OpenCode configuration](/docs/config). V2 does not place server names directly under `mcp`.
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"servers": {
"my-server": {
"type": "local",
"command": ["npx", "-y", "example-mcp-server"]
}
}
}
}
```
Servers connect automatically unless `disabled` is `true`. There is no V2 `enabled` field.
```jsonc
{
"mcp": {
"servers": {
"my-server": {
"type": "local",
"command": ["npx", "-y", "example-mcp-server"],
"disabled": true
}
}
}
}
```
As with other configuration, a server in a higher-precedence project config replaces a server with the same name from a lower-precedence config. Use different names when you need separate connections or accounts.
## Local servers
A local server is a command that OpenCode starts using the MCP stdio transport.
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"servers": {
"everything": {
"type": "local",
"command": [
"npx",
"-y",
"@modelcontextprotocol/server-everything"
],
"cwd": ".",
"environment": {
"LOG_LEVEL": "info",
"MCP_API_KEY": "{env:MCP_API_KEY}"
}
}
}
}
}
```
| Field | Required | Description |
| --- | --- | --- |
| `type` | Yes | Must be `"local"`. |
| `command` | Yes | Executable followed by its arguments. |
| `cwd` | No | Process working directory. Relative paths resolve from the workspace directory; the workspace is the default. |
| `environment` | No | String environment variables added to the inherited OpenCode process environment. |
| `disabled` | No | Set to `true` to prevent the server from connecting. Defaults to `false`. |
| `timeout` | No | Per-server timeout overrides. |
Use `{env:NAME}` to substitute an environment variable while loading config. Shell expressions such as `$NAME` are not expanded in JSON strings.
## Remote servers
A remote server uses the MCP Streamable HTTP transport. Its `url` must be a valid absolute URL.
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"servers": {
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/mcp",
"oauth": false,
"headers": {
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
}
}
}
}
}
```
| Field | Required | Description |
| --- | --- | --- |
| `type` | Yes | Must be `"remote"`. |
| `url` | Yes | Streamable HTTP endpoint. |
| `headers` | No | String HTTP headers sent to the MCP endpoint. |
| `oauth` | No | OAuth client settings, or `false` to disable OAuth support. |
| `disabled` | No | Set to `true` to prevent the server from connecting. Defaults to `false`. |
| `timeout` | No | Per-server timeout overrides. |
Use `oauth: false` for a server that exclusively uses an API key or another header-based credential.
## OAuth
OAuth support is enabled for remote servers unless `oauth` is `false`. OpenCode discovers the authorization server, uses PKCE, refreshes tokens, and attempts dynamic client registration when the server supports it. OAuth credentials are stored outside project configuration.
For a server that supports dynamic client registration, only the remote server is required:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"servers": {
"sentry": {
"type": "remote",
"url": "https://mcp.sentry.dev/mcp"
}
}
}
}
```
When the server reports that it needs authentication, run `/connect` in the TUI:
```text
/connect
```
Select the MCP server under **Services**, then complete the browser authorization flow.
You can also authenticate from the command line:
```bash
opencode2 mcp auth sentry
```
The CLI command prints the authorization URL and waits for the redirect to OpenCode's loopback callback server.
If the provider issued client credentials, configure them using V2's snake_case field names:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"servers": {
"company-tools": {
"type": "remote",
"url": "https://mcp.example.com/mcp",
"oauth": {
"client_id": "{env:MCP_CLIENT_ID}",
"client_secret": "{env:MCP_CLIENT_SECRET}",
"scope": "tools:read tools:execute",
"callback_port": 19876,
"redirect_uri": "http://127.0.0.1:19876/callback"
}
}
}
}
}
```
| OAuth field | Description |
| --- | --- |
| `client_id` | Pre-registered OAuth client ID. If omitted, OpenCode attempts dynamic client registration. |
| `client_secret` | Client secret for a pre-registered client. |
| `scope` | Space-delimited scopes to request. |
| `callback_port` | Local callback port, from `1` through `65535`. An available ephemeral port is used by default. |
| `redirect_uri` | Pre-registered loopback redirect URI. Its path and port must reach the local callback listener. |
Remove stored credentials with:
```bash
opencode2 mcp logout sentry
```
## Timeouts
Timeouts are positive integer milliseconds. Configure defaults under `mcp.timeout`; a server's `timeout` fields override matching defaults.
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"timeout": {
"startup": 45000,
"catalog": 30000,
"execution": 600000
},
"servers": {
"slow-tools": {
"type": "remote",
"url": "https://mcp.example.com/mcp",
"timeout": {
"catalog": 60000
}
}
}
}
}
```
| Timeout | Default | Applies to |
| --- | --- | --- |
| `startup` | 30 seconds | Establishing the transport and initializing the server. |
| `catalog` | 30 seconds | Listing tools, prompts, resources, and resource templates. |
| `execution` | 12 hours | Calling tools, getting prompts, and reading resources. |
## Names and permissions
OpenCode combines the server name and MCP tool name as `<server>_<tool>`. Characters other than letters, numbers, `_`, and `-` are replaced with `_`; for example, server `context 7` and tool `resolve.library/id` become `context_7_resolve_library_id`. MCP prompts appear as slash commands named `<server>:<prompt>` using the same normalization.
Choose short server names that remain unique after normalization. Under the default Code Mode, MCP tools are grouped by the normalized server name.
Use permission actions to hide or deny a server's tools without stopping its connection:
```jsonc
{
"permissions": [
{
"action": "context7_*",
"resource": "*",
"effect": "deny"
}
]
}
```
## CLI commands
V2 provides these MCP management commands:
```bash
# Add a local server to the project config
opencode2 mcp add everything --env LOG_LEVEL=info -- npx -y @modelcontextprotocol/server-everything
# Add a remote server to the project config
opencode2 mcp add context7 --url https://mcp.context7.com/mcp --header 'CONTEXT7_API_KEY={env:CONTEXT7_API_KEY}'
# Add to the global config instead
opencode2 mcp add context7 --global --url https://mcp.context7.com/mcp
# List configured servers and connection status
opencode2 mcp list
# Authenticate or remove OAuth credentials
opencode2 mcp auth context7
opencode2 mcp logout context7
```
`mcp add` accepts either `--url` for a remote server or a command after `--` for a local server, not both. Use `--header NAME=VALUE` only with remote servers and `--env NAME=VALUE` only with local servers. Edit the config directly for OAuth, timeout, working-directory, or enablement settings.
@@ -0,0 +1,28 @@
{
"title": "Docs",
"description": "Use and configure OpenCode.",
"root": true,
"pages": [
"---Get started---",
"index",
"migrate-v1",
"config",
"troubleshooting",
"---Configure---",
"providers",
"models",
"agents",
"permissions",
"sharing",
"snapshots",
"commands",
"skills",
"instructions",
"mcp-servers",
"attachments",
"compaction",
"formatters",
"lsp",
"references"
]
}
@@ -0,0 +1,576 @@
---
title: "Migrate from V1"
description: "Move from OpenCode V1 to the OpenCode 2.0 beta."
---
## Breaking changes
V2 has three intentional breaking changes:
- [Plugins](#plugins) use a new plugin API.
- The [server API and clients](#server-api-and-clients) have new contracts.
- [TUI configuration](#tui-configuration) moves from layered `tui.json(c)` files to one global `cli.json` file (auto migrated).
All other functionality is intended to remain compatible with V1.
Existing server config files, agent definitions, command definitions, skills, and other files in `.opencode/` should
continue to work without changes. If one of these stops working in V2, treat it as a beta compatibility bug rather than
an expected migration requirement.
<Tip>
Run `/report` if existing V1 functionality does not work in V2. The report skill collects diagnostics and helps you file
a compatibility issue.
</Tip>
<Warning>
OpenCode 2.0 is in beta. Beta data may be wiped, features may break unintentionally, and the server and plugin APIs may
continue to change.
</Warning>
During the beta, OpenCode V1 and V2 use different executable names. You can keep using `opencode` for V1 while trying V2
with `opencode2`.
## Install the beta
Install the beta from the `next` distribution tag:
```bash
npm install -g @opencode-ai/cli@next
```
Start it in your project with:
```bash
opencode2
```
## Configuration
This section covers both JSON/JSONC configuration and file-based definitions under `.opencode/`.
### Use your existing configuration
V2 reads existing global and project configuration from the same locations as V1:
```text
~/.config/opencode/opencode.json(c)
<project>/opencode.json(c)
<project>/.opencode/opencode.json(c)
```
V2 reads these same locations. It detects V1-shaped configuration and translates it in memory without rewriting the
source file. Existing V1 configuration is intended to keep working, so you do not need to convert it to try or adopt V2.
### Ask OpenCode to migrate
The V1 config format remains supported. The native V2 format is optional and makes several settings more explicit and
ergonomic.
The recommended migration path is to ask OpenCode to update the configuration for you:
```text
Migrate my OpenCode configuration, including file-based definitions, from the V1 format to the native V2 format.
Preserve its behavior and all unrelated settings.
```
OpenCode can inspect the complete file, apply the relevant changes below, and avoid rewriting settings that do not need to
change. Do not mix V1 and V2 field names manually in one file.
### Sharing
The deprecated V1 `autoshare` boolean becomes the explicit `share` policy:
```jsonc
// V1
{ "autoshare": true }
// V2
{ "share": "auto" }
```
Use `"manual"`, `"auto"`, or `"disabled"`. If the V1 file already uses `share`, no change is needed.
### Permissions and tools
V1 groups permission effects by tool. V2 uses one ordered `permissions` array, making precedence and exceptions explicit:
```jsonc
// V1
{
"permission": {
"bash": {
"git push *": "ask"
},
"edit": "allow"
},
"tools": {
"websearch": false
}
}
// V2
{
"permissions": [
{ "action": "shell", "resource": "git push *", "effect": "ask" },
{ "action": "edit", "resource": "*", "effect": "allow" },
{ "action": "websearch", "resource": "*", "effect": "deny" }
]
}
```
Permission actions also changed: `bash` is now `shell`, `task` is now `subagent`, and `write` and `patch` are now `edit`.
See [Permissions](/docs/permissions) for the ordered V2 rule format.
### Agents and modes
The singular `agent` and deprecated `mode` maps become `agents`. Agent fields become more consistent with the rest of the
V2 config:
```jsonc
// V1
{
"agent": {
"reviewer": {
"prompt": "Review for correctness and missing tests.",
"model": "anthropic/claude-sonnet-4-5",
"variant": "high",
"disable": false,
"permission": {
"edit": "deny"
}
}
}
}
// V2
{
"agents": {
"reviewer": {
"system": "Review for correctness and missing tests.",
"model": "anthropic/claude-sonnet-4-5#high",
"disabled": false,
"permissions": [
{ "action": "edit", "resource": "*", "effect": "deny" }
]
}
}
}
```
`prompt` becomes `system`, `disable` becomes `disabled`, and a separate `variant` joins the model reference after `#`.
`temperature`, `top_p`, and provider-specific `options` move under `request.body`. `maxSteps` becomes `steps`. Entries from
the old `mode` map become primary agents.
### Snapshots
Rename the singular `snapshot` field to `snapshots`. Its boolean value does not change:
```jsonc
// V1
{ "snapshot": false }
// V2
{ "snapshots": false }
```
### Attachments
Rename the singular `attachment` object to `attachments`. Nested image settings keep the same names:
```jsonc
// V1
{ "attachment": { "image": { "auto_resize": true } } }
// V2
{ "attachments": { "image": { "auto_resize": true } } }
```
### MCP servers
V2 groups servers under `mcp.servers`, replaces `enabled` with the inverse `disabled`, and separates timeout purposes:
```jsonc
// V1
{
"mcp": {
"playwright": {
"type": "local",
"command": ["npx", "@playwright/mcp"],
"enabled": true,
"timeout": 30000
}
}
}
// V2
{
"mcp": {
"servers": {
"playwright": {
"type": "local",
"command": ["npx", "@playwright/mcp"],
"disabled": false,
"timeout": {
"catalog": 30000,
"execution": 30000
}
}
}
}
}
```
Remote OAuth fields use snake case: `clientId` becomes `client_id`, `clientSecret` becomes `client_secret`,
`callbackPort` becomes `callback_port`, and `redirectUri` becomes `redirect_uri`. The V1 `experimental.mcp_timeout` value
also becomes the default `mcp.timeout.catalog` and `mcp.timeout.execution` values. See [MCP servers](/docs/mcp-servers).
### Compaction
V2 groups the retained-context token budget under `keep` and gives the reserve a clearer name:
```jsonc
// V1
{
"compaction": {
"preserve_recent_tokens": 8000,
"reserved": 20000
}
}
// V2
{
"compaction": {
"keep": {
"tokens": 8000
},
"buffer": 20000
}
}
```
`auto` and `prune` keep their names. V2 has no native `tail_turns` field; recent context is retained by token budget instead.
See [Compaction](/docs/compaction).
### Skills
V1 separates extra skill paths and URLs. V2 combines both into one ordered array:
```jsonc
// V1
{
"skills": {
"paths": ["./team-skills"],
"urls": ["https://example.com/skills/"]
}
}
// V2
{
"skills": ["./team-skills", "https://example.com/skills/"]
}
```
Existing skill files and automatic `.opencode/skills/` discovery do not change. See [Skills](/docs/skills).
### Commands
Rename the singular `command` map to `commands`. Join a separate model `variant` to the model reference:
```jsonc
// V1
{
"command": {
"review": {
"template": "Review the current changes.",
"model": "anthropic/claude-sonnet-4-5",
"variant": "high"
}
}
}
// V2
{
"commands": {
"review": {
"template": "Review the current changes.",
"model": "anthropic/claude-sonnet-4-5#high"
}
}
}
```
`template`, `description`, `agent`, and `subtask` keep their names. Existing Markdown command definitions remain supported.
See [Commands](/docs/commands).
### References
Rename the deprecated singular `reference` map to `references`:
```jsonc
// V1
{ "reference": { "docs": "../docs" } }
// V2
{ "references": { "docs": "../docs" } }
```
V1 already accepts `references`, so no change is needed when the file uses it. Reference entries keep the
same shapes. See [References](/docs/references).
### Providers
Rename the singular `provider` map to `providers`. V2 separates the runtime package, endpoint, and request settings:
```jsonc
// V1
{
"provider": {
"acme": {
"npm": "@ai-sdk/openai-compatible",
"api": "https://llm.example.com/v1",
"options": {
"apiKey": "{env:ACME_API_KEY}"
}
}
}
}
// V2
{
"providers": {
"acme": {
"package": "aisdk:@ai-sdk/openai-compatible",
"settings": {
"baseURL": "https://llm.example.com/v1",
"apiKey": "{env:ACME_API_KEY}"
}
}
}
}
```
V1 `npm` becomes `package`, and AI SDK packages receive the `aisdk:` prefix. `api` becomes `settings.baseURL`. Provider
`options` are separated into `settings`, `headers`, and `body` according to their request role. See [Providers](/docs/providers).
### Models and variants
Models remain nested under their provider, but several model fields become more explicit:
- `id` becomes `modelID`.
- `tool_call` and `modalities` become `capabilities.tools`, `capabilities.input`, and `capabilities.output`.
- A `status` of `"deprecated"` becomes `disabled: true`.
- Cache costs move from `cache_read` and `cache_write` to `cache.read` and `cache.write`.
- Provider-specific `options` become `settings`.
- A V1 variants object becomes a V2 array with an `id` on each entry.
```jsonc
// V1
{
"variants": {
"high": {
"reasoningEffort": "high"
}
}
}
// V2
{
"variants": [
{
"id": "high",
"settings": {
"reasoningEffort": "high"
}
}
]
}
```
See [Models](/docs/models) for the complete native model shape.
### Fields without native equivalents
Most fields that keep the same shape, including `shell`, `model`, `default_agent`, `autoupdate`, `watcher`, `formatter`,
`lsp`, `instructions`, `enterprise`, and `tool_output`, require no migration.
These V1 fields do not have one-to-one native V2 config fields:
- `logLevel`: use `OPENCODE_LOG_LEVEL` when starting OpenCode.
- `server`: use the V2 service and explicit server options; the server API is an intentional breaking change.
- `layout`: remove it; V1 already treated it as deprecated and always used stretch layout.
- `enabled_providers` and `disabled_providers`: there is no native provider allowlist or denylist field yet.
- `small_model`: V2 selects models for internal maintenance agents without a separate top-level field.
- `compaction.tail_turns`: V2 uses `compaction.keep.tokens` instead.
If your V1 configuration relies on a field without a native equivalent, keep using the supported V1 format rather than
forcing a manual conversion. Run `/report` if V2 does not preserve the behavior you rely on.
### Agent files
V1 agent files may use `agent/`, `agents/`, `mode/`, or `modes/`. V2 still discovers all four directories. The preferred
V2 location is:
```text
.opencode/agents/<name>.md
```
Files under a V1 `mode/` or `modes/` directory represent primary agents. When moving one into `agents/`, add
`mode: primary` to its frontmatter. Files under `agent/` can move to `agents/` without changing their path-derived ID.
When converting the frontmatter to native V2 fields:
- Keep the Markdown body as the agent's system instructions.
- Rename `prompt` to `system` when it appears in JSON configuration; file bodies do not need a `system` field.
- Rename `disable` to `disabled` and `permission` to `permissions`.
- Join `model` and `variant` as `provider/model#variant`.
- Move `temperature`, `top_p`, and provider-specific options under `request.body`.
V2 translates legacy agent frontmatter automatically, so these edits are optional. See [Agents](/docs/agents).
### Command files
V1 command files may use `command/` or `commands/`. V2 discovers both. The preferred location is:
```text
.opencode/commands/<name>.md
```
Move files from `command/` to the same relative path under `commands/` to preserve command names. The Markdown body remains
the command template, and `description`, `agent`, and `subtask` frontmatter keep the same names. If frontmatter has separate
`model` and `variant` fields, append the variant to the model and remove `variant`:
```yaml
# V1
model: anthropic/claude-sonnet-4-5
variant: high
# V2
model: anthropic/claude-sonnet-4-5#high
```
See [Commands](/docs/commands).
### Skill files
V2 discovers skills from both `.opencode/skill/` and `.opencode/skills/`. The preferred layout is:
```text
.opencode/skills/<skill-id>/SKILL.md
```
Move the complete skill directory, not only `SKILL.md`, so relative scripts, references, and other supporting files remain
available. Keep the directory name stable to preserve the skill ID. Existing skill frontmatter and Markdown bodies do not
require a V2 rewrite. See [Skills](/docs/skills).
### Instruction files
Existing `AGENTS.md` files stay in place. V2 discovers the global `~/.config/opencode/AGENTS.md` and project `AGENTS.md`
files from the current directory up to the project root.
If a V1 setup relied on a `CLAUDE.md` fallback, move that guidance into the applicable `AGENTS.md`. V2 currently only
discovers `AGENTS.md`; because non-API V1 behavior is intended to remain compatible, also run `/report` with the affected
project details. See [Instructions](/docs/instructions).
## TUI configuration
V1 loaded `tui.json(c)` from the global config directory and from project directories discovered while walking up from
the current directory. V2 instead stores CLI and TUI settings in one global file:
```text
~/.config/opencode/cli.json
```
The CLI owns this file. The background service does not load it, and V2 does not discover or merge project-local
`tui.json(c)` or `cli.json` files.
The native V2 format groups related settings. For example:
```jsonc
// V1: ~/.config/opencode/tui.json
{
"theme": "tokyonight",
"scroll_speed": 2,
"scroll_acceleration": {
"enabled": true
}
}
// V2: ~/.config/opencode/cli.json
{
"theme": {
"name": "tokyonight"
},
"scroll": {
"speed": 2,
"acceleration": true
}
}
```
V2 migrates the global TUI configuration automatically. On the first CLI or TUI startup, when `cli.json` does not already
exist, it:
- Reads `~/.config/opencode/tui.json`.
- Reads persisted TUI preferences from the legacy `kv.json` state file.
- Converts supported settings to the native grouped format and writes `~/.config/opencode/cli.json`.
- Leaves the V1 files unchanged so V1 can continue using them.
Migration runs only while `cli.json` is absent. Once that file exists, V2 treats it as the source of truth and does not
continually synchronize later changes from `tui.json` or `kv.json`. If you created `cli.json` before starting V2, merge any
V1 settings you still need into it manually.
Project-local V1 TUI configuration is not migrated because V2 has no project-local CLI configuration. Move settings you
still want into the global `cli.json`; when multiple projects used different values for the same setting, choose the
global behavior you want V2 to use.
## Plugins
Rename `plugin` to `plugins`. Replace a package-and-options tuple with an object:
```jsonc
// V1
{
"plugin": [
"opencode-example-plugin",
["./plugin/local.ts", { "enabled": true }]
]
}
// V2
{
"plugins": [
"opencode-example-plugin",
{
"package": "./plugin/local.ts",
"options": { "enabled": true }
}
]
}
```
V2 discovers local plugins from both `.opencode/plugin/` and `.opencode/plugins/`; use `.opencode/plugins/` for V2 files.
Moving a file between these directories does not migrate its implementation.
<Warning>V1 plugins will not work in V2.</Warning>
The config entry can be translated automatically, but plugin implementation code must be ported to the new API. The V2
plugin API is still being finalized during beta, and detailed plugin migration guidance will be published when it is
ready.
Once the V2 plugin API is finalized, OpenCode should be able to migrate the majority of V1 plugins while keeping related
local modules and dependencies together. See the current beta [Plugins guide](/docs/build/plugins).
## Server API and clients
OpenCode 2 has a revised, more ergonomic server API and a new set of clients. Integrations that call the V1 server API
must migrate to the V2 API.
Use the `@opencode-ai/client` package to access the new clients. The server API and clients are still being finalized
during beta, so their contracts may continue to change. See the generated [API reference](/docs/api) for the current endpoints,
request types, and responses.
## Verify your setup
Start `opencode2` in a project and verify your model, provider credentials, agents, permissions, MCP servers, and plugins
before relying on the beta for regular work. Keep your V1 setup until you have confirmed the V2 behavior you need, and do
not point V1 at configuration that you have converted to the native V2 shape.
+213
View File
@@ -0,0 +1,213 @@
---
title: "Models"
description: ""
---
OpenCode builds its model catalog from [Models.dev](https://models.dev), provider integrations, and your configuration.
Only enabled models whose provider is available for the current project appear in the model picker.
Connect a provider with `/connect` in the TUI, or configure it in [Providers](/docs/providers).
## Choose a model
Open the model picker with `/models` or the default `<leader>m` keybind. The picker shows the models available from
providers connected to the current project.
Select a model to use it in the current session. Switching models updates that session without changing your config. Use
the catalog entries shown in the picker rather than guessing a provider or model name.
## Per-run model
Select a model for one non-interactive run with `--model` or `-m`:
```bash
opencode2 run --model openai/gpt-5.2 "Explain this repository"
opencode2 run -m openai/gpt-5.2#high "Review the current changes"
```
Agents and commands can also select their own model. See [Agents](/docs/agents) and [Commands](/docs/commands).
## Variants
Variants are named request overlays for one model, commonly used for reasoning effort or token budgets. Available names
are model-specific and are derived from current catalog metadata. Do not assume that names such as `low`, `high`, or
`max` exist for every model; `/variants` shows the valid choices.
Use `/variants` to choose one for the current model, or press `ctrl+t` to cycle through available variants.
## Configure
### Default model
Set `model` in `opencode.json` or `opencode.jsonc`:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"model": "anthropic/claude-sonnet-4-5"
}
```
The configured model becomes the catalog default when its provider is available and the model is enabled. Otherwise,
session execution falls back to the newest available supported model. An explicit model already selected on a session
takes precedence over the default; switching models changes that session and does not rewrite your config.
See [Config](/docs/config) for configuration locations and precedence.
### Model settings
Provider and model entries can supply three kinds of request configuration:
- `settings` contains provider-package options such as `baseURL`, `reasoningEffort`, or `thinkingConfig`.
- `headers` adds HTTP request headers.
- `body` adds provider-specific fields to the request body.
These values are provider-specific JSON. OpenCode applies provider values first, then model values, then the selected
variant. Nested `settings` and `body` objects are merged; later array and scalar values replace earlier values. Header
names are matched case-insensitively.
You can also map a friendly catalog ID to a different API model ID with `modelID`:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"model": "openai/coding-default",
"providers": {
"openai": {
"models": {
"coding-default": {
"modelID": "gpt-5.2",
"name": "Coding default",
"capabilities": {
"tools": true,
"input": ["text", "image"],
"output": ["text"]
},
"limit": {
"context": 200000,
"output": 32000
}
}
}
}
}
}
```
Here `openai/coding-default` is the selectable catalog reference, while `gpt-5.2` is sent to the provider. When adding a
model that is not already in the catalog, set accurate `capabilities` and `limit` values so OpenCode can expose tools and
enforce the correct context limits. Set `disabled: true` on a model entry to hide it from the available catalog.
### Custom variants
Add a variant, or override a catalog variant with the same ID, under the model's `variants` array:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"providers": {
"openai": {
"models": {
"gpt-5.2": {
"settings": {
"reasoningEffort": "medium"
},
"variants": [
{
"id": "fast",
"settings": {
"reasoningEffort": "low"
}
},
{
"id": "deep",
"settings": {
"reasoningEffort": "high",
"reasoningSummary": "auto"
}
}
]
}
}
}
}
}
```
Variant entries support `settings`, `headers`, and `body`. Selecting one deeply overlays its values on the effective
provider and model configuration. An unknown variant fails model resolution instead of silently using the base model.
### Local models
For an OpenAI-compatible server, define a provider package, endpoint, and at least one model:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"model": "local/coder",
"providers": {
"local": {
"name": "Local server",
"package": "aisdk:@ai-sdk/openai-compatible",
"settings": {
"baseURL": "http://127.0.0.1:1234/v1"
},
"models": {
"coder": {
"modelID": "model-name-on-server",
"capabilities": {
"tools": true,
"input": ["text"],
"output": ["text"]
},
"limit": {
"context": 32768,
"output": 8192
}
}
}
}
}
}
```
Use the server's real model name, limits, modalities, and tool support. OpenCode cannot infer these for a model you add
manually. If the endpoint requires a key, add `apiKey` to provider `settings` using an environment substitution such as
`"apiKey": "{env:LOCAL_API_KEY}"`; do not commit secrets.
### Model references
Configuration and CLI options identify a model as `provider/model`, with an optional `#variant`:
```text
openai/gpt-5.2
openai/gpt-5.2#high
openrouter/anthropic/claude-sonnet-4.5#high
```
OpenCode splits the reference at the first `/`, so model IDs may contain additional slashes. Provider and model IDs are
case-sensitive. Provider IDs cannot contain `/` or `#`, and model IDs cannot contain `#`.
The expanded config form is equivalent when generated or programmatic configuration is more convenient:
```jsonc
{
"model": {
"providerID": "openrouter",
"model": "anthropic/claude-sonnet-4.5"
}
}
```
Root, agent, and command `model` fields accept both forms. Use the IDs shown by `/models`, not provider display names.
### Caveats
- The selector object uses `model`, while a provider catalog entry uses `modelID` for the upstream API identifier.
- The root `model` currently sets the default provider and model only. Although its selection shape accepts a variant,
the V2 catalog default does not retain it; select a variant in the TUI, with `opencode2 run`, or on an agent or command.
- Model options are provider-specific. A setting accepted by one provider package may be ignored or rejected by another.
- Catalog data, credentials, and config are location-scoped. A model available in one project may be unavailable in
another.
- Configuration files are watched and normally reload automatically, but an in-flight model request keeps the settings
with which it started.
@@ -0,0 +1,209 @@
---
title: "Permissions"
description: ""
---
Permissions control whether an agent may perform an action on a resource. V2
configuration uses the `permissions` field and an ordered array of rules.
<Warning>
The V1 object syntax uses different field and action names. Do not use
`permission`, `bash`, or `task` in V2 configuration; use `permissions`,
`shell`, and `subagent`.
</Warning>
## Rule schema
Each rule has three required string fields:
```jsonc
{
"$schema": "https://opencode.ai/config.json",
"permissions": [
{ "action": "*", "resource": "*", "effect": "ask" },
{ "action": "read", "resource": "*", "effect": "allow" },
{ "action": "read", "resource": "*.env", "effect": "deny" },
{ "action": "shell", "resource": "git status *", "effect": "allow" },
{ "action": "shell", "resource": "git push *", "effect": "deny" },
{ "action": "edit", "resource": "packages/docs/*.mdx", "effect": "allow" }
]
}
```
- `action` matches a tool permission action.
- `resource` matches the value the tool is trying to use, such as a path,
command, URL, query, or agent ID.
- `effect` is `"allow"`, `"deny"`, or `"ask"`.
`allow` proceeds without prompting, `deny` blocks the operation, and `ask`
waits for a user decision. If no rule matches, the result is `ask`.
## Matching and order
Both `action` and `resource` support simple wildcards:
- `*` matches zero or more characters, including `/`.
- `?` matches exactly one character.
- All other characters are literal.
Matches cover the entire value. Slashes are normalized, and matching is
case-insensitive on Windows. For shell convenience, a pattern ending in
`" *"` also matches the command without arguments: `"git status *"` matches
both `git status` and `git status --short`.
The **last matching rule wins**. Put broad rules first and exceptions later.
Rules from lower-priority configuration files are loaded first. OpenCode then
appends all global rules before agent-specific rules, so a matching agent rule
overrides a global rule.
Some operations check several resources at once, such as a patch touching
multiple files. OpenCode denies the operation if any resource resolves to
`deny`; otherwise it asks if any resolves to `ask`; otherwise it allows it.
## Actions and resources
V2 action names are strings, so plugins may introduce additional actions. The
current built-in actions use these resources:
| Action | Resource matched |
| --- | --- |
| `read` | Location-relative path for an internal file or directory; canonical absolute path for an external target |
| `edit` | Target path for `edit`, `write`, and `patch`; all three tools share this action |
| `glob` | The requested glob pattern |
| `grep` | The requested regular expression, not the search path |
| `shell` | The complete raw shell command string |
| `subagent` | The target agent ID |
| `skill` | The skill ID |
| `question` | `*` |
| `webfetch` | The requested URL |
| `websearch` | The search query |
| `external_directory` | A canonical external directory boundary, normally ending in `/*` |
| `<server>_<tool>` | `*` for an MCP tool; unsupported characters in both names become `_` |
| `execute` | `*`; controls availability of the Code Mode dispatcher, while each nested tool still enforces its own permission |
Built-in agent policy also reserves `plan_enter` and `plan_exit` for plan-mode
transitions. `doom_loop` and `lsp` are not current V2 Core permission actions.
## External directories
An external path requires a separate `external_directory` decision before the
tool's own `read` or `edit` decision. This applies to external paths used by
`read`, `edit`, `write`, and `patch`, and to an external `shell` working
directory.
```jsonc
{
"$schema": "https://opencode.ai/config.json",
"permissions": [
{
"action": "external_directory",
"resource": "~/projects/reference/*",
"effect": "allow"
},
{
"action": "read",
"resource": "~/projects/reference/*",
"effect": "allow"
},
{
"action": "edit",
"resource": "~/projects/reference/*",
"effect": "deny"
}
]
}
```
For `external_directory`, `read`, and `edit` resources, a leading `~`, `~/`,
`$HOME`, or `$HOME/` is expanded when configuration loads. Shell resources are
raw command text and are **not** home-expanded.
<Warning>
`shell` runs with the host user's filesystem, process, and network authority.
Its resource is raw text, not a parsed command. External command arguments
produce only best-effort warnings; `external_directory` is enforced for the
working directory, not every path embedded in a command. Prefer a narrow
shell allowlist over patterns intended to identify every dangerous command.
</Warning>
Relative mutation paths cannot escape the active Location, and symlink escapes
from inside it are rejected. Explicit external paths are canonicalized before
matching, so authorize only trusted directory boundaries.
## Defaults
The evaluator's fallback is `ask`, but shipped agents include ordered defaults:
| Agent | Effective default policy |
| --- | --- |
| `build` | Allows most actions; asks for external directories and `.env` reads; allows questions and entering plan mode; denies exiting plan mode |
| `plan` | Uses the same base, allows questions and exiting plan mode, and denies edits except OpenCode plan files |
| `general` | Uses the base policy but cannot launch another subagent; questions and plan transitions remain denied |
| `explore` | Denies everything except `read`, `glob`, `grep`, `webfetch`, and `websearch`; cannot launch subagents and asks for external directories |
| Hidden maintenance agents | Deny all actions |
The base read rules are ordered as follows:
```jsonc
[
{ "action": "read", "resource": "*", "effect": "allow" },
{ "action": "read", "resource": "*.env", "effect": "ask" },
{ "action": "read", "resource": "*.env.*", "effect": "ask" },
{ "action": "read", "resource": "*.env.example", "effect": "allow" }
]
```
OpenCode also permits its managed tool-output and temporary directories where
needed. These exceptions do not grant general external-directory access.
## Agent overrides
Configure shared policy at the top level and append narrower rules to a named
agent under `agents.<id>.permissions`:
```jsonc
{
"$schema": "https://opencode.ai/config.json",
"permissions": [
{ "action": "shell", "resource": "*", "effect": "ask" },
{ "action": "shell", "resource": "git diff *", "effect": "allow" },
{ "action": "shell", "resource": "git status *", "effect": "allow" }
],
"agents": {
"reviewer": {
"description": "Review code without changing it",
"mode": "subagent",
"permissions": [
{ "action": "edit", "resource": "*", "effect": "deny" },
{ "action": "shell", "resource": "git diff *", "effect": "allow" },
{ "action": "shell", "resource": "git status *", "effect": "allow" }
]
}
}
}
```
Agent rules do not replace the global array; they are appended after it. A
custom subagent executes with its own permissions, not a permission subset
derived from the parent agent.
## Approval choices
When an `ask` rule matches, clients can reply with:
- **Allow once** (`once`): approve only the pending request.
- **Allow always** (`always`): approve this request and save the patterns
proposed by the tool for the current project.
- **Reject** (`reject`): reject the request. Rejecting also rejects other
pending permission requests in the same session; clients may attach feedback.
Saved approvals are durable and project-scoped. They are additional `allow`
rules, but they can never override a configured `deny`. The proposed saved
pattern may be broader than the displayed resource: several tools propose `*`,
shell proposes the exact command text, and skills and subagents propose their
IDs. Review the confirmation carefully and remove saved approvals that are no
longer needed.
For non-interactive runs, `opencode2 run --auto` replies `once` to permission
requests. It does not save approvals, and explicit `deny` rules remain enforced.
Without `--auto`, a non-interactive run rejects permission requests.
@@ -0,0 +1,204 @@
---
title: "Providers"
description: ""
---
OpenCode builds its provider and model catalog from [Models.dev](https://models.dev), then applies the `providers`
overlays from your [configuration](/docs/config). A provider needs both a usable runtime package and, when required, an
active connection.
## Connect a provider
Run `/connect` in the TUI, choose an integration, and complete one of the methods it offers:
```text
/connect
```
An integration may support an API key, OAuth, environment variables, or a combination of them. API keys and OAuth
tokens entered through `/connect` are stored by the OpenCode service in its database. Run `/connect` again to replace
or remove a stored credential.
Providers from Models.dev also declare their standard environment variables. A non-empty declared variable is exposed
as an environment connection automatically, so common providers usually need no config:
```bash
export ANTHROPIC_API_KEY="your-key"
```
For a custom provider, `env` declares the variables that can supply its key:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"providers": {
"acme": {
"env": ["ACME_API_KEY"]
}
}
}
```
When several credential sources exist, OpenCode uses the stored credential first, then the first non-empty variable in
`env`, then `settings.apiKey`. Use config substitution instead of committing a literal key:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"providers": {
"acme": {
"settings": {
"apiKey": "{env:ACME_API_KEY}"
}
}
}
}
```
<Warning>Do not commit API keys or authorization headers to your repository.</Warning>
## Configure
The `providers` object is keyed by provider ID. Each provider accepts these fields:
| Field | Purpose |
| --- | --- |
| `name` | Display name. |
| `env` | Ordered environment variable names that provide a connection. |
| `package` | Runtime provider package. |
| `settings` | JSON settings passed to the runtime package, such as `baseURL`. |
| `headers` | String-valued HTTP headers added to requests. |
| `body` | JSON fields merged into request bodies. |
| `models` | Models to add or override, keyed by catalog model ID. |
Configuration files are applied from lowest to highest precedence. `settings` and `body` are deep-merged. Headers are
merged case-insensitively. At request time, provider values are inherited by the model, model values override them, and
the selected variant is applied last.
### Endpoint
Override `settings.baseURL` to send an existing provider through a proxy or compatible endpoint. Its existing package,
models, and connection continue to apply:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"providers": {
"anthropic": {
"settings": {
"baseURL": "https://llm-proxy.example.com/anthropic"
}
}
}
}
```
`settings` is package-specific. A field only has an effect when the selected package supports it.
### Headers and body
Headers and body fields can be set at provider, model, or variant scope:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"providers": {
"openai": {
"headers": {
"X-Gateway-Tenant": "engineering"
},
"body": {
"metadata": {
"application": "opencode"
}
},
"models": {
"gpt-5.2": {
"headers": {
"X-Model-Policy": "coding"
}
}
}
}
}
}
```
These are request overlays, not a generic authentication scheme. Prefer `/connect`, `env`, or `settings.apiKey` for
provider credentials unless the endpoint explicitly requires a custom header.
### Provider packages
For an OpenAI-compatible service, use the V2 native compatible package. The model map is explicit because a custom
provider has no Models.dev catalog entries:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"model": "acme/qwen3-coder",
"providers": {
"acme": {
"name": "Acme Gateway",
"env": ["ACME_API_KEY"],
"package": "@opencode-ai/llm/providers/openai-compatible",
"settings": {
"baseURL": "https://llm.acme.example/v1"
},
"models": {
"qwen3-coder": {
"name": "Qwen 3 Coder",
"capabilities": {
"tools": true,
"input": ["text"],
"output": ["text"]
},
"limit": {
"context": 131072,
"output": 32768
}
}
}
}
}
}
```
Omit `env` for an endpoint that does not require authentication. The native compatible package requires
`settings.baseURL` and uses bearer authentication when a key is available.
The `package` field supports two runtime contracts:
| Form | Contract |
| --- | --- |
| `"@opencode-ai/llm/providers/openai-compatible"` | A V2 native package exporting `model(modelID, settings)`. An npm specifier or absolute `file://` URL may use the same contract. |
| `"aisdk:@ai-sdk/openai-compatible"` | An AI SDK provider package. The `aisdk:` prefix is required. |
Native packages receive the merged `settings` plus the resolved `apiKey`, `headers`, `body`, and `limits`. AI SDK
packages receive their merged provider options. Use a package's own documentation for accepted settings; OpenCode does
not validate package-specific keys.
`package` may also be set on one model to override the provider package for that model.
### Models
Add a model under a provider's `models` map. The object key is the model ID used in OpenCode; `modelID` is the ID sent to
the provider:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"model": "openai/coding",
"providers": {
"openai": {
"models": {
"coding": {
"modelID": "gpt-5.2",
"name": "GPT-5.2 Coding"
}
}
}
}
}
```
See [Models](/docs/models) for model selection, defaults, capabilities, limits, costs, and variants.
@@ -0,0 +1,177 @@
---
title: "References"
description: ""
---
References give OpenCode named access to directories outside the current
project. Use them for documentation, shared libraries, examples, or source from
another repository.
Configure references by alias in `opencode.json` or `opencode.jsonc`:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"references": {
"docs": {
"path": "../product-docs",
"description": "Use for product behavior and terminology"
},
"effect": {
"repository": "Effect-TS/effect",
"branch": "main",
"description": "Use for Effect implementation details"
}
}
}
```
## Local directories
Use `path` for a local directory:
```jsonc
{
"references": {
"design-system": {
"path": "../design-system",
"description": "Use when working with components or design tokens"
}
}
}
```
Relative paths resolve from the directory containing the config file that
defines them. Absolute paths and home-relative paths such as `~/docs` are also
supported.
The string shorthand is useful when no other fields are needed:
```jsonc
{
"references": {
"docs": "../docs",
"shared": "~/work/shared"
}
}
```
<Note>
A shorthand string is treated as a local path only when it starts with `.`,
`/`, or `~`. Use `./docs`, not `docs`; a bare `docs` value is interpreted as
a Git repository.
</Note>
## Git repositories
Use `repository` for a remote Git repository. GitHub `owner/repo` shorthand,
Git URLs, host/path forms, and SCP-style remotes are supported.
```jsonc
{
"references": {
"effect": {
"repository": "Effect-TS/effect",
"branch": "main"
},
"internal-sdk": {
"repository": "git@gitlab.example.com:platform/sdk.git",
"branch": "release/v2"
}
}
}
```
Without `branch`, OpenCode checks out and refreshes the remote's default
branch. Branch names may contain letters, numbers, `/`, `_`, `.`, and `-`, but
cannot start with `-` or contain `..`. Local `file:` repositories are not
supported.
Git references also support shorthand:
```jsonc
{
"references": {
"effect": "Effect-TS/effect",
"sdk": "gitlab.com/platform/sdk"
}
}
```
### Cloning and storage
OpenCode normalizes a remote and stores one checkout under its global data
directory at `opencode/repos/<host>/<repository-path>`. On a typical Linux
installation, for example, `Effect-TS/effect` is stored at:
```text
~/.local/share/opencode/repos/github.com/Effect-TS/effect
```
Missing repositories are cloned. Existing checkouts are fetched and reset to
the requested branch, or to the remote default branch when `branch` is omitted.
Materialization runs asynchronously when references load or reload, so a new
reference can appear before its checkout is ready. Clone and refresh failures
are logged and do not stop other references from loading.
<Warning>
The cache has one checkout per normalized remote, not one per branch. Do not
configure the same repository at multiple branches; only one branch can be
exposed. Avoid editing cached checkouts because a refresh resets them.
</Warning>
## Description and visibility
`description` tells agents when a reference is relevant. References with a
description are included in agent instructions with their alias and resolved
path. References without one remain available in `@` autocomplete but are not
advertised automatically.
Set `hidden` to `true` to remove a reference from TUI `@` autocomplete:
```jsonc
{
"references": {
"internal": {
"path": "../internal",
"description": "Use for internal service behavior",
"hidden": true
}
}
}
```
`hidden` controls only autocomplete visibility. It does not remove the
reference from the reference API or agent instructions when a description is
present.
## Use references
Type `@` in the TUI and select a reference alias to attach its root directory:
```text
Compare the current implementation with @effect
```
The attachment provides a non-recursive listing of the root's immediate files
and directories. V2 currently attaches references by root alias;
`@alias/path` is not a reference-specific file browser. Ask the agent to
inspect a particular path when more detail is needed.
References do not grant extra tool permissions. Access outside the active
Location remains subject to the agent's normal tool rules and the
`external_directory` permission. Editing a reference additionally requires the
applicable edit permission.
## Fields
| Field | Local | Git | Description |
| --- | --- | --- | --- |
| `path` | Required | No | Local directory path |
| `repository` | No | Required | Remote Git repository |
| `branch` | No | Optional | Branch to fetch and check out |
| `description` | Optional | Optional | Guidance describing when agents should use it |
| `hidden` | Optional | Optional | Hide it from TUI `@` autocomplete |
An alias cannot be empty or contain `/`, `\`, whitespace, a backtick, or a
comma.
@@ -0,0 +1,39 @@
---
title: "Session sharing"
description: ""
---
Session sharing is not yet available in OpenCode V2. V2 does not currently
publish sessions, upload conversation history to a sharing service, or create
public links.
<Warning>
The V2 TUI registers `/share`, but it currently only reports that sharing is unavailable. There is no functional
share/unshare command or server API endpoint.
</Warning>
## Configuration
The V2 configuration schema accepts a `share` field with three values:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"share": "manual"
}
```
- `"manual"` represents sharing only when explicitly requested.
- `"auto"` represents automatically sharing new sessions.
- `"disabled"` represents preventing session sharing.
These values are parsed but are not acted on by the current V2 runtime. In
particular, setting `"auto"` does not publish sessions. If `share` is omitted,
V2 leaves the sharing policy unspecified.
## Beta limitations
V2 currently provides no public session viewer, share URL, history sync,
retention controls, or unshare/delete operation. Until those surfaces are
implemented in the V2 server and protocol, keep using sessions locally and do
not treat the `share` configuration field as a privacy or publishing control.
+227
View File
@@ -0,0 +1,227 @@
---
title: "Skills"
description: ""
---
Skills are Markdown instructions that OpenCode can advertise to an agent and
load when they are relevant. A skill can include supporting scripts,
references, and other files in the same directory.
## Create a skill
Create one directory per skill with a `SKILL.md` file:
```text
.opencode/skills/
└── git-release/
├── SKILL.md
├── scripts/
│ └── changelog.ts
└── references/
└── release-policy.md
```
```markdown title=".opencode/skills/git-release/SKILL.md"
---
name: Git Release
description: Prepare release notes, version bumps, and GitHub releases
metadata:
opencode/slash: "true"
---
## Workflow
1. Read `references/release-policy.md`.
2. Summarize merged changes since the previous tag.
3. Propose the version bump before changing files.
4. Run `scripts/changelog.ts` only after the user approves the version.
```
Paths in a skill are relative to the directory containing `SKILL.md`.
## Discovery
OpenCode automatically adds the following source directories:
| Scope | Sources |
| --- | --- |
| Global | `~/.config/opencode/skills` |
| Global compatibility | `~/.claude/skills`, `~/.agents/skills` |
| Project | `.opencode/skills` |
| Project compatibility | `.claude/skills`, `.agents/skills` |
For project sources, OpenCode searches from the current directory upward to
the project root and includes matching directories at every level.
Within each source directory, OpenCode discovers:
- Markdown files at the source root, such as `skills/git-release.md`
- `SKILL.md` files at any depth, such as `skills/git-release/SKILL.md`
The directory form is recommended because it gives the skill a private base
directory for supporting files.
## Configure sources
Use the `skills` array in any `opencode.json` or `opencode.jsonc` to add local
directories or HTTP catalogs:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"skills": [
"./team-skills",
"~/shared/opencode-skills",
"/opt/company-skills",
"https://example.com/opencode/skills/"
]
}
```
Relative paths are resolved from the active OpenCode working directory, not
from the directory containing the config file. Paths beginning with `~/` use
the current user's home directory. Only `http://` and `https://` values are
treated as URL sources.
Every discovered config document contributes its `skills` entries; the arrays
are additive rather than replacing one another.
### HTTP catalogs
An HTTP source is a base URL containing an `index.json`:
```json title="index.json"
{
"skills": [
{
"name": "git-release",
"version": "3",
"files": [
"git-release.md",
"references/release-policy.md"
]
}
]
}
```
OpenCode downloads those files from
`<base-url>/git-release/<file>`. File paths must be safe, relative,
same-origin paths. Each entry must include either `SKILL.md` or a Markdown file
named after the index entry, such as `git-release.md`.
Use the named Markdown form for HTTP catalogs. Each downloaded skill directory
is itself a source root, so `git-release.md` produces the ID `git-release`; a
root-level `SKILL.md` produces the literal ID `SKILL` in the current V2
implementation. Increment `version` when files change so OpenCode refreshes
the cached copy.
## Frontmatter
V2 reads these fields:
| Field | Purpose |
| --- | --- |
| `name` | Display name; defaults to the path-derived ID |
| `description` | Summary shown to the model and command catalog |
| `slash` | Set to `false` to hide the skill from the V2 slash-command catalog |
| `metadata.opencode/slash` | Boolean or `"true"`/`"false"`; overrides `slash` |
| `metadata.opencode/autoinvoke` | Set to `false` to omit the skill from model-facing discovery |
Frontmatter, `name`, and `description` are optional at runtime. However, a
clear `description` is strongly recommended: skills without one are not
advertised to the model. `license`, `compatibility`, and other metadata may be
included for portability, but V2 does not interpret them.
`opencode/autoinvoke: false` only removes the skill from the model's available
skills list. The skill remains registered and can still be activated explicitly
by its ID.
## IDs and validation
The skill ID comes from its path, not its frontmatter:
| File | ID |
| --- | --- |
| `<source>/git-release.md` | `git-release` |
| `<source>/git-release/SKILL.md` | `git-release` |
| `<source>/teams/release/SKILL.md` | `release` |
IDs are exact and case-sensitive. V2 currently does not enforce the Agent
Skills name regex, length limits, a match between `name` and the directory, or
a maximum description length. The frontmatter `name` is only a display label.
For portable, predictable skills, use a unique lowercase kebab-case ID of 1-64
characters and keep it aligned with the directory name:
```text
^[a-z0-9]+(-[a-z0-9]+)*$
```
## Precedence
Skills are keyed by ID. If several sources define the same ID, the later source
wins. Sources are registered in this order, from lower to higher precedence:
1. Built-in skills
2. `.claude/skills` sources, global first and then from the current directory upward
3. `.agents/skills` sources, global first and then from the current directory upward
4. `~/.config/opencode/skills`
5. Project `.opencode/skills`, from the project root toward the current directory
6. Explicit `skills` config entries, in config priority and array order
Avoid duplicate IDs unless an override is intentional.
## Runtime loading
At each model step, OpenCode advertises permitted skills that have a
description and do not set `opencode/autoinvoke` to `false`. The advertisement
contains only each skill's ID, name, and description; it does not add every
skill body to the prompt.
When the model calls the `skill` tool with an exact ID, OpenCode:
1. Resolves the current winning definition for that ID
2. Checks the `skill` permission for the selected agent
3. Adds the Markdown body, without frontmatter, to the conversation
4. Provides the skill's base directory and a sample of up to ten supporting file paths
Supporting file contents are not loaded automatically. The agent can read a
referenced file when the skill instructs it to do so. The supporting-file
sample is available for directory-based `SKILL.md` skills; flat Markdown skills
receive no neighboring file list.
In the V2 CLI, skills appear as `/id` commands unless `slash` resolves to
`false`. Selecting one appends the skill body as a skill message and resumes
the session.
## Permissions
Permission rules use the `skill` action and the skill ID as the resource. Rules
are evaluated in order, with the last matching rule winning:
```jsonc title="opencode.jsonc"
{
"permissions": [
{ "action": "skill", "resource": "*", "effect": "allow" },
{ "action": "skill", "resource": "internal-*", "effect": "deny" },
{ "action": "skill", "resource": "experimental-*", "effect": "ask" }
]
}
```
`deny` removes matching skills from model-facing discovery and rejects skill
tool loading. `ask` advertises the skill but requests approval when the model
loads it. The same rules can be placed under an individual
`agents.<id>.permissions` array.
## Troubleshooting
If a skill is missing or loads the wrong content:
1. Confirm the file is either a root-level `*.md` or a nested file named exactly `SKILL.md`.
2. Check the path-derived ID rather than the frontmatter `name`.
3. Add a `description` if the skill should be advertised to the model.
4. Check `opencode/autoinvoke` and the selected agent's `skill` permissions.
5. Look for a later source defining the same ID.
6. For HTTP catalogs, verify `index.json`, same-origin file paths, and a changed `version`.
@@ -0,0 +1,108 @@
---
title: "Undo"
description: ""
---
OpenCode snapshots let the default interactive TUI roll back conversation history and related file changes. They are a
convenience for revising recent work, not a replacement for Git commits or backups.
## Configuration
Snapshots are enabled by default. Set `snapshots` to `false` in your [configuration](/docs/config#snapshots) to stop capturing
filesystem state:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"snapshots": false
}
```
Filesystem snapshots require a Git repository. With snapshots disabled, unavailable, or missing, undo can still stage a
conversation rollback, but it has no captured file state to restore. Disabling snapshots does not delete snapshots that
were already stored.
## What is captured
For each model step, OpenCode attempts to capture the worktree immediately before the model call and after a cleanly
completed step. It records the paths changed between those two points on the assistant message.
Snapshots use a separate internal Git object database in the OpenCode data directory. They do not create commits, move
branches, or intentionally modify your repository's Git index. Capture is limited to the session's active directory, which
may be a subdirectory of the repository.
Within that directory, snapshots include tracked files and untracked files that are not ignored by Git. An individual
untracked file larger than 2 MiB is excluded. Ignored files, files outside the active directory, and changes to Git
metadata are not captured.
During undo, OpenCode does not check out an entire tree. It restores only paths attributed to cleanly completed assistant
steps after the selected conversation boundary. Each path is restored to its state before the first affected step.
## Undo
Wait for the session to become idle, then run:
```text
/undo
```
The TUI finds the latest non-empty user message and stages a revert at that message:
- The selected user message and every later message are hidden, but not deleted yet.
- The selected message's text, attachments, and agent mentions are placed in the composer for revision.
- Captured files changed by the affected assistant steps are restored to their earlier contents. Files created by those
steps are removed when they did not exist in the earlier snapshot.
- A summary shows the staged message count and restored paths.
Running `/undo` again moves the staged boundary to an earlier user message. OpenCode keeps the filesystem state from
immediately before the first undo as the redo baseline, so repeated undos form one wider staged revert rather than a redo
stack.
<Warning>
Sending a new prompt while an undo is staged commits the revert. The hidden message range is removed from the active
session history, the currently reverted files are kept, and redo is no longer available.
</Warning>
## Redo
While a revert is staged, run:
```text
/redo
```
Redo clears the staged boundary, makes the hidden messages visible again, and restores affected files to their exact state
immediately before the first undo. It does not rerun the model. After multiple undos, one redo restores the whole staged
range; there is no step-by-step redo stack.
## Revert a message
The TUI's **Message Actions** menu also provides **Revert**. It stages the selected message as the conversation boundary
and uses the same file restoration and redo behavior, but it does not copy that message into the composer.
For a conversation-and-files rollback, select a user message. If an assistant message is selected, that message is hidden,
but only file changes attributed to later assistant steps are restored; the selected assistant message's own file changes
are not included.
## Limitations and safety
- Capture is best effort. A failed capture is logged and the model step continues, so conversation rollback may have no
matching file rollback.
- Interrupted or failed steps do not receive a completed end snapshot. File changes made before the failure may remain.
- Shell commands can change databases, services, processes, network resources, Git state, ignored build output, or files
outside the active directory. Undo and redo do not reverse those side effects.
- Undo overwrites the current contents of affected paths with older contents. Redo likewise overwrites those paths with
the pre-undo state, including edits made after running undo.
- Other processes can edit the worktree between capture and restore. The server rejects revert operations while the
session is actively running, but it cannot protect against external editors or commands.
- Snapshot objects can contain complete contents of tracked and non-ignored untracked files. They are stored locally in
the OpenCode data directory; do not treat snapshots as secret-free metadata.
- Undo is not secure erasure. Committing a revert removes messages from the active projection, not from durable session
history or existing snapshot storage.
Review the staged file summary and your Git diff before continuing. Commit or back up important work independently before
using undo on a dirty worktree.
<Note>
`/undo` and `/redo` are interactive TUI commands. The non-interactive `run` command does not provide them.
</Note>
@@ -0,0 +1,177 @@
---
title: "Troubleshooting"
description: "Diagnose OpenCode startup, server, and session issues."
---
<Tip>
You can ask OpenCode to debug itself. Describe the problem and ask it to use this troubleshooting page; it can read the
steps below, inspect its service and logs, and help identify the issue.
</Tip>
OpenCode runs as two processes: the TUI is a client, while a background server owns sessions, plugins, permissions, and
other application state. Start by determining whether an issue is in the client, the shared server, or a specific project.
## Check the background service
Show the current server status:
```bash
opencode2 service status
```
Verify that its API is healthy:
```bash
opencode2 api get /api/health
```
If the service is stuck or unhealthy, restart it:
```bash
opencode2 service restart
```
From inside the TUI, run `/reload` to restart the managed service and reconnect:
```text
/reload
```
You can also stop and start it explicitly:
```bash
opencode2 service stop
opencode2 service start
```
<Note>
OpenCode normally discovers or starts the shared background service automatically. The service commands are only needed
when diagnosing its lifecycle.
</Note>
## Run an isolated session
Use standalone mode to run the TUI with a private server that exits with it:
```bash
opencode2 --standalone
```
If an issue disappears in standalone mode, it is likely related to the shared background service rather than the TUI or
project itself.
## Inspect the API
The `api` command uses the same discovery and authentication flow as the TUI. It accepts either an HTTP method and path or
an OpenAPI operation ID.
See the [API reference](/docs/api) for all endpoints and operation IDs.
Pass a JSON request body with `--data` or `-d`, and add headers with `--header` or `-H`.
<Warning>
Running `opencode2 api` may start the background service when no compatible healthy service is available.
</Warning>
## Read logs
Installed builds write logs to:
```text
~/.local/share/opencode/log/opencode.log
```
Follow the log while reproducing the problem:
```bash
tail -f ~/.local/share/opencode/log/opencode.log
```
Each line includes a process `run` ID and a `role` field. Use `role=cli` for TUI and command startup, and `role=server` for
session, provider, plugin, permission, and tool activity.
```bash
grep 'role=cli' ~/.local/share/opencode/log/opencode.log
grep 'role=server' ~/.local/share/opencode/log/opencode.log
grep 'run=8fc3b1d5' ~/.local/share/opencode/log/opencode.log
```
Increase verbosity for one reproduction:
```bash
OPENCODE_LOG_LEVEL=DEBUG opencode2
```
## Service files
The shared server registers itself at:
```text
~/.local/state/opencode/service.json
```
Its private service configuration is stored separately at:
```text
~/.config/opencode/service.json
```
The database normally lives at:
```text
~/.local/share/opencode/opencode-next.db
```
`OPENCODE_DB` can override the database location.
<Warning>
Do not delete or edit service files or the database while troubleshooting. Use the service commands to manage the daemon,
and make a backup before inspecting persistent data with external tools.
</Warning>
## Explicit servers
When connecting with `--server`, set `OPENCODE_PASSWORD` if the server requires authentication:
```bash
OPENCODE_PASSWORD=secret opencode2 --server http://127.0.0.1:4096
```
The CLI checks the server before opening the TUI and reports whether it is unreachable, requires a password, or rejected
the supplied password.
## Report an issue
Include the following when reporting a reproducible problem:
- Output from `opencode2 --version`
- Output from `opencode2 service status`
- The smallest sequence of steps that reproduces the issue
- Whether the issue also occurs with `opencode2 --standalone`
- Relevant log lines, including their `run` and `role` fields
Remove API keys, authorization headers, prompts, file contents, and other sensitive data before sharing logs.
## Local development
When working from the OpenCode repository, run V2 commands from the repository root:
```bash
bun dev
```
The local development channel keeps its logs, SQLite database, and service registration separate from installed builds:
```text
~/.local/share/opencode/log/opencode-local.log
~/.local/share/opencode/opencode-local.db
~/.local/state/opencode/service-local.json
```
Use the same diagnostics through the package development command:
```bash
bun dev service status
bun dev service restart
bun dev api get /api/health
```
+6
View File
@@ -0,0 +1,6 @@
---
title: "API Reference"
description: "OpenCode HTTP API."
---
The endpoint reference is generated from the current OpenCode V2 [OpenAPI specification](/openapi.json).
+6
View File
@@ -0,0 +1,6 @@
{
"title": "API",
"description": "Use the OpenCode HTTP API.",
"root": true,
"pages": ["index"]
}
+145
View File
@@ -0,0 +1,145 @@
---
title: "Client"
description: "Connect an application to the OpenCode HTTP API."
---
`@opencode-ai/client` is the generated TypeScript client for the OpenCode HTTP
API. Use it when your application connects to an OpenCode server over the
network. Its types and methods are generated from the same contract as the
[API reference](/docs/api).
<Warning>
The V2 API and client are beta. Method names, inputs, and outputs may change
before the stable release.
</Warning>
## Install
```sh
bun add @opencode-ai/client@next
```
## Create a client
Create a client with the server URL, then call methods grouped by API resource:
```ts
import { OpenCode } from "@opencode-ai/client"
const client = OpenCode.make({
baseUrl: "http://localhost:4096",
})
const session = await client.session.create({
location: { directory: "/workspace" },
})
await client.session.prompt({
sessionID: session.id,
text: "Review the current changes",
})
```
## Headers and requests
Pass default authentication or application headers to `OpenCode.make` with
`headers`. You can also supply a custom `fetch` implementation. Each operation
accepts request options as its final argument for an `AbortSignal` or
per-request headers.
```ts
const client = OpenCode.make({
baseUrl: "https://opencode.example.com",
headers: {
authorization: `Bearer ${process.env.OPENCODE_TOKEN}`,
},
})
await client.session.list(undefined, {
signal: AbortSignal.timeout(10_000),
})
```
## Stream events
Streaming endpoints return async iterables:
```ts
for await (const event of client.event.subscribe()) {
console.log(event.type)
}
```
## Effect
OpenCode provides a first-class Effect client through the
`@opencode-ai/client/effect` entrypoint. It returns typed Effects and Streams
and decodes responses into OpenCode schema values.
```sh
bun add @opencode-ai/client@next effect
```
### Create a client
```ts
import { AbsolutePath, Location, OpenCode } from "@opencode-ai/client/effect"
import { Effect } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
const program = Effect.gen(function* () {
const client = yield* OpenCode.make({ baseUrl: "http://localhost:4096" })
const session = yield* client.session.create({
location: Location.Ref.make({
directory: AbsolutePath.make("/workspace"),
}),
})
return yield* client.session.get({ sessionID: session.id })
})
const session = await Effect.runPromise(
program.pipe(Effect.provide(FetchHttpClient.layer)),
)
```
Streaming operations, including `client.event.subscribe()` and
`client.session.log(...)`, return Effect `Stream` values.
### Service
`Service` discovers and manages the local OpenCode background service from a
Node application:
- `Service.discover()` returns a healthy registered endpoint without starting
a process.
- `Service.start()` reuses a compatible service or starts one when needed.
- `Service.stop()` stops the registered service.
- `Service.headers(endpoint)` creates the authentication headers for a client.
```sh
bun add @effect/platform-node
```
```ts
import { NodeFileSystem } from "@effect/platform-node"
import { OpenCode, Service } from "@opencode-ai/client/effect"
import { Effect } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
const program = Effect.gen(function* () {
const endpoint = yield* Service.start()
const client = yield* OpenCode.make({
baseUrl: endpoint.url,
headers: Service.headers(endpoint),
})
return yield* client.health.get()
})
const health = await Effect.runPromise(
program.pipe(
Effect.provide(FetchHttpClient.layer),
Effect.provide(NodeFileSystem.layer),
),
)
```
+24
View File
@@ -0,0 +1,24 @@
---
title: "Build"
description: "Build on the engine used by millions daily."
mode: "wide"
---
<CardGroup cols={1}>
<Card title="Extend OpenCode" href="/docs/build/plugins">
Build plugins that add tools, integrations, commands, agents, and custom behavior while keeping the rest of OpenCode
intact.
</Card>
<Card title="Run it as a server" href="/docs/build/client">
Connect to OpenCode with the same client used by the TUI and desktop app, then build any interface, workflow, or agent
experience around it.
</Card>
<Card title="Embed it" href="/docs/build/sdk">
Embed OpenCode directly into your application and build a completely custom agent, interface, or developer product
around it.
</Card>
</CardGroup>
<Warning>
The plugin API, client, and SDK are still being finalized during beta and may change before OpenCode 2.0 is stable.
</Warning>
+7
View File
@@ -0,0 +1,7 @@
{
"title": "Build",
"description": "Extend and embed OpenCode.",
"root": true,
"defaultOpen": true,
"pages": ["index", "plugins", "client", "sdk"]
}
+430
View File
@@ -0,0 +1,430 @@
---
title: "Plugins"
description: "Extend OpenCode with plugins."
---
Plugins extend OpenCode in-process. They can transform agents, models, commands,
integrations, references, skills, and tools; intercept model requests and tool
execution; and call a subset of the V2 client.
<Warning>
The V2 plugin API is beta. Entrypoints, hooks, draft shapes, and configuration may change before the stable release.
Use the `/v2` exports described on this page.
</Warning>
## Load plugins
Plugins can be loaded from npm packages, explicit local paths, or config
directories. Each module must have one default export containing a unique
plugin `id` and a `setup` function.
### Configuration
Add ordered entries to the `plugins` field in `opencode.json(c)`:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
"opencode-acme-plugin@1.2.0",
"@acme/opencode-plugin",
"./plugins/local.ts",
{
"package": "./plugins/reviewer.ts",
"options": {
"agent": "reviewer",
"strict": true,
},
},
],
}
```
A string is either a package specifier or a local path. Local paths must start
with `./` or `../` and resolve relative to the configuration file containing
the entry. Absolute paths and `file://` URLs are also supported. Both scoped
packages and versioned package specifiers are supported.
Use the object form to pass JSON configuration to the plugin. OpenCode passes
`options` unchanged as `ctx.options`; omitted options become an empty object.
The plugin owns validation and defaults for its options.
See [Config](/docs/config#locations) for configuration locations and precedence.
Entries from all applicable files are processed from lowest to highest
precedence rather than replacing the entire array.
### Local discovery
OpenCode automatically scans this directory in every discovered OpenCode config
directory:
```text
.opencode/plugins/
```
The equivalent global directory is `~/.config/opencode/plugins/`. Direct `.ts`
and `.js` children are loaded. An immediate child directory is also loaded as a
package when OpenCode can resolve a string `exports`, `module`, or `main`
entrypoint, or an `index.ts` or `index.js` file.
A `plugins/` directory beside a project-root `opencode.json` is not discovered
automatically. Put it under `.opencode/`, or add its file explicitly with a
relative config entry.
### Enable and disable
A string beginning with `-` disables plugins by their exported `id`. `*`
matches every ID, and a suffix of `.*` matches an ID prefix. Directives are
applied in order:
```jsonc title="opencode.jsonc"
{
"plugins": ["./plugins/reviewer.ts", "-acme.reviewer", "-opencode.provider.*", "opencode.provider.openai"],
}
```
Package specifiers and local paths locate plugin modules; they are not disable
selectors. Use the `id` from the plugin's default export to disable it. A later
ID entry re-enables a loaded or built-in plugin. Explicit config directives run
after local auto-discovery, so they can disable discovered plugins by ID.
User plugins are activated in configured order between OpenCode's internal
plugin phases. Hooks run sequentially in registration order, and later hooks
observe earlier mutations. Do not depend on the internal phase ordering while
the API is beta.
### Installation and dependencies
OpenCode installs bare package entries and their production dependencies into
an isolated cache. Package installation does not run lifecycle scripts.
Published packages should expose their plugin entrypoint and include every
runtime import in `dependencies`.
Local files and local package directories are imported directly. OpenCode does
**not** install their dependencies. Install dependencies in a `package.json`
visible from the plugin file, for example:
```sh
cd .opencode
bun add @opencode-ai/plugin@next
```
Match the plugin package version to the OpenCode release you target.
Configuration and discovered plugin files under watched config directories are
reloaded when they change. Reloading replaces the active plugin generation and
releases its scoped registrations. Restart OpenCode after changing an npm
package version or a local dependency when no watched file changed.
## Create a plugin
Export the result of `Plugin.define` as the module default:
```ts title=".opencode/plugins/reviewer.ts"
import { Plugin } from "@opencode-ai/plugin/v2"
export default Plugin.define({
id: "acme.reviewer",
setup: async (ctx) => {
const description =
typeof ctx.options.description === "string" ? ctx.options.description : "Reviews code for regressions"
await ctx.agent.transform((agents) => {
agents.update("reviewer", (agent) => {
agent.description = description
agent.mode = "subagent"
})
})
},
})
```
`setup` runs each time the plugin is activated. Register long-lived behavior
during setup; do not wait there on an infinite event stream. It may return a
synchronous or asynchronous cleanup function. OpenCode awaits that cleanup
when the plugin is disabled, reloaded, or shut down:
```ts
setup: async (ctx) => {
const controller = new AbortController()
const task = synchronize(ctx, controller.signal)
return async () => {
controller.abort()
await task
}
}
```
Hook registrations are released automatically with the same plugin scope. Use
the returned cleanup for resources the plugin owns, such as timers, watchers,
connections, and background tasks.
### Context
The plugin context is essentially an [OpenCode server client](/docs/build/client).
Its read and action methods use the same inputs and responses as the client. It
adds plugin-only methods for transforms, runtime hooks, reloads, registrations,
and plugin options.
| Capability | Available operations |
| ---------------------- | -------------------------------------------------------------------------------------------- |
| `ctx.agent` | `list`, `transform`, `reload` |
| `ctx.catalog.provider` | `list`, `get` |
| `ctx.catalog.model` | `list`, `default` |
| `ctx.catalog` | `transform`, `reload` |
| `ctx.command` | `list`, `transform`, `reload` |
| `ctx.integration` | `list`, `get`, `connect`, `attempt`, `transform`, `reload`, and connection lookup/resolution |
| `ctx.plugin` | `list` currently active plugin IDs |
| `ctx.reference` | `list`, `transform`, `reload` |
| `ctx.session` | `create`, `get`, `prompt`, `command`, `interrupt`, and `hook` |
| `ctx.skill` | `list`, `transform`, `reload` |
| `ctx.tool` | `transform` and `hook` |
| `ctx.aisdk` | `hook` |
| `ctx.event` | `subscribe` to the current public server event stream |
| `ctx.options` | Readonly options from the matching config object |
### Transform hooks
Transform hooks let a plugin modify how OpenCode is configured. Use them to add
or remove definitions, override settings, choose defaults, and provide tools or
other sources.
| Transform | Draft operations |
| ----------------------- | ------------------------------------------------------------------------------------------------------- |
| `agent.transform` | `list`, `get`, `default`, `update`, `remove` |
| `catalog.transform` | Provider `list`, `get`, `update`, `remove`; model `get`, `update`, `remove`; default model `get`, `set` |
| `command.transform` | `list`, `get`, `update`, `remove` |
| `integration.transform` | Integration `list`, `get`, `update`, `remove`; method `list`, `update`, `remove` |
| `reference.transform` | `add`, `remove`, `list` |
| `skill.transform` | `source`, `list` |
| `tool.transform` | `add` |
Here's an example that keeps models synced from a remote source:
```js title=".opencode/plugins/remote-models.js"
import { Plugin } from "@opencode-ai/plugin/v2"
export default Plugin.define({
id: "acme.remote-models",
setup: async (ctx) => {
let models = []
await ctx.catalog.transform((catalog) => {
for (const model of models) {
catalog.model.update(model.providerID, model.id, (draft) => Object.assign(draft, model))
}
})
const refresh = async () => {
const response = await fetch("https://example.com/opencode/models.json", {
signal: AbortSignal.timeout(10_000),
})
if (!response.ok) return
models = await response.json()
await ctx.catalog.reload()
}
await refresh()
const timer = setInterval(() => void refresh().catch(console.error), 60_000)
return () => clearInterval(timer)
},
})
```
`ctx.catalog.reload()` replays every catalog transform to derive the new
catalog. Each plugin's logic remains composed with the others, so a later
plugin can still modify models added by an earlier one. The catalog updates
without restarting OpenCode.
### Runtime hooks
Runtime hooks intercept live operations. Their event objects expose specific
mutable fields:
| Hook | Mutable fields |
| ------------------------------------------- | ------------------------------------------------------------------------------ |
| `ctx.aisdk.hook("sdk", callback)` | `sdk`, after inspecting `model`, `package`, and `options` |
| `ctx.aisdk.hook("language", callback)` | `language`, after inspecting `model`, `sdk`, and `options` |
| `ctx.session.hook("request", callback)` | `system`, `messages`, and the `tools` record immediately before model dispatch |
| `ctx.tool.hook("execute.before", callback)` | `input`, before the selected tool executes |
| `ctx.tool.hook("execute.after", callback)` | `result`, `output`, and `outputPaths`, after execution settles |
For example, remove a tool from selected model requests and normalize another
tool's input:
```ts title=".opencode/plugins/guards.ts"
import { Plugin } from "@opencode-ai/plugin/v2"
export default Plugin.define({
id: "acme.guards",
setup: async (ctx) => {
await ctx.session.hook("request", (event) => {
delete event.tools.write
})
await ctx.tool.hook("execute.before", (event) => {
if (event.tool !== "lookup" || typeof event.input !== "object" || event.input === null) return
event.input = { ...event.input, source: "plugin" }
})
},
})
```
A hook failure fails the operation it intercepts. Keep runtime hooks fast and
handle expected errors inside the callback.
## Examples
### Add a tool
Pass a tool declaration to `tools.add`. Define its input with JSON Schema and
use an async executor:
```js title=".opencode/plugins/greeting.js"
import { Plugin } from "@opencode-ai/plugin/v2"
export default Plugin.define({
id: "acme.greeting",
setup: async (ctx) => {
await ctx.tool.transform((tools) => {
tools.add({
name: "greeting",
description: "Create a greeting",
jsonSchema: {
type: "object",
properties: {
name: { type: "string" },
},
required: ["name"],
additionalProperties: false,
},
execute: async ({ name }) => {
const text = `Hello, ${name}!`
return {
structured: { greeting: text },
content: [{ type: "text", text }],
}
},
})
})
},
})
```
Unsupported characters in tool and group names are normalized to underscores.
The resulting exposed key must begin with a letter and contain at most 64
letters, digits, underscores, or hyphens. Set `options` on the declaration to
configure registration with `{ group, deferred }`:
- `group` prefixes and groups the exposed tool name.
- `deferred: true` makes the tool available through the deferred `execute`
tool instead of exposing it directly.
The executor receives a second context argument containing `sessionID`,
`agent`, `assistantMessageID`, and `toolCallID`.
### Add a command
```js title=".opencode/plugins/review-command.js"
import { Plugin } from "@opencode-ai/plugin/v2"
export default Plugin.define({
id: "acme.review-command",
setup: async (ctx) => {
await ctx.command.transform((commands) => {
commands.update("review", (command) => {
command.description = "Review the current changes"
command.template = "Review the current changes for correctness and missing tests."
})
})
},
})
```
### Set the default model
```js title=".opencode/plugins/default-model.js"
import { Plugin } from "@opencode-ai/plugin/v2"
export default Plugin.define({
id: "acme.default-model",
setup: async (ctx) => {
await ctx.catalog.transform((catalog) => {
catalog.model.default.set("anthropic", "claude-sonnet-4-5")
})
},
})
```
## Publish a package
A package plugin uses the same default export as a local plugin. A minimal
manifest is:
```json title="package.json"
{
"name": "opencode-acme-plugin",
"version": "1.0.0",
"type": "module",
"exports": "./src/index.ts",
"dependencies": {
"@opencode-ai/plugin": "next"
}
}
```
Use versions compatible with the OpenCode release you target and test the
installed package, not only a workspace-linked copy. Because the plugin API is
beta, publish compatible plugin updates when V2 entrypoints or contracts
change.
## Verify loading
List active plugin IDs through the V2 API:
```sh
opencode2 api get /api/plugin
```
If a plugin is absent, check the server log described in
[Troubleshooting](/docs/troubleshooting#read-logs). Invalid modules and setup failures are
logged; one failing package does not prevent unrelated valid packages from
being resolved.
## Effect
OpenCode provides a first-class Effect API for plugins through the
`@opencode-ai/plugin/v2/effect` entrypoint. Install `effect` alongside the
plugin package and export an `effect` function instead of `setup`:
```sh
bun add @opencode-ai/plugin@next effect
```
```ts title=".opencode/plugins/reviewer-effect.ts"
import { Plugin } from "@opencode-ai/plugin/v2/effect"
import { Effect } from "effect"
export default Plugin.define({
id: "acme.reviewer-effect",
effect: (ctx) =>
Effect.gen(function* () {
yield* ctx.agent.transform((agents) => {
agents.update("reviewer", (agent) => {
agent.description = "Reviews code for regressions"
agent.mode = "subagent"
})
})
}),
})
```
Context operations return Effects. The plugin effect is scoped, so finalizers,
fibers, and registrations are released when the plugin reloads or unloads.
OpenCode does not expose its private Core services to the plugin; use the
capabilities on `ctx`.
Typed tools can use `Schema` from `effect` and the contracts exported from
`@opencode-ai/plugin/v2/effect/tool`. Their executors return an Effect and may
fail with the typed tool failure channel.
+75
View File
@@ -0,0 +1,75 @@
---
title: "SDK"
description: "Embed an OpenCode host in an Effect application."
---
`@opencode-ai/sdk-next` is the Effect-native SDK for applications that need to
host OpenCode in-process. Unlike the [network client](/docs/build/client), it assembles the
OpenCode server and routes API calls through its HTTP router in memory. It opens
no HTTP listener and adds no network hop between the client and server.
<Warning>
The V2 SDK is beta and currently private to the OpenCode workspace. It is not
published for external installation yet, and its package name and API may
change before release.
</Warning>
## Create a host
`OpenCode.create()` creates a scoped host. Closing its Effect Scope releases
the router, location services, fibers, and scoped plugin registrations.
```ts
import {
AbsolutePath,
Location,
OpenCode,
} from "@opencode-ai/sdk-next"
import { Effect } from "effect"
const program = Effect.scoped(
Effect.gen(function* () {
const opencode = yield* OpenCode.create()
const session = yield* opencode.sessions.create({
location: Location.Ref.make({
directory: AbsolutePath.make("/workspace"),
}),
})
return yield* opencode.sessions.get({ sessionID: session.id })
}),
)
const session = await Effect.runPromise(program)
```
The embedded host uses the same routes, middleware, codecs, errors, and schema
values as `@opencode-ai/client/effect`. It exposes the full generated client and
adds the convenience aliases `sessions` and `events` for the session and event
groups.
## Use as a service
Use `OpenCode.layer` when the host should be provided through Effect dependency
injection:
```ts
import { OpenCode } from "@opencode-ai/sdk-next"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const opencode = yield* OpenCode.Service
return yield* opencode.sessions.active()
})
const active = await Effect.runPromise(
program.pipe(Effect.provide(OpenCode.layer)),
)
```
## Register plugins
Call `opencode.plugin(...)` to register an embedded V2 plugin. Embedded plugins
use the same discovery and location-scoped activation path as configured
plugins. The SDK also exports `Tool` for plugin-defined tools. See the
[Plugins guide](/docs/build/plugins) for the plugin shape and available hooks.
+4
View File
@@ -0,0 +1,4 @@
{
"title": "Documentation",
"pages": ["(docs)", "build", "api"]
}
+40
View File
@@ -0,0 +1,40 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/www",
"version": "1.17.18",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000",
"build": "vite build",
"preview": "vite preview --host 0.0.0.0",
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"@cloudflare/vite-plugin": "1.44.0",
"@tailwindcss/vite": "4.3.2",
"@tanstack/react-router": "1.170.17",
"@tanstack/react-start": "1.168.27",
"@tanstack/router-plugin": "1.168.19",
"fumadocs-core": "16.11.1",
"fumadocs-mdx": "15.1.0",
"fumadocs-ui": "16.11.1",
"react": "19.2.7",
"react-dom": "19.2.7",
"tailwindcss": "4.3.2",
"vite": "8.1.4"
},
"devDependencies": {
"@types/mdx": "2.0.14",
"@types/node": "catalog:",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"@typescript/native-preview": "catalog:",
"@vitejs/plugin-react": "6.0.3",
"typescript": "catalog:",
"wrangler": "4.110.0"
},
"engines": {
"node": ">=22"
}
}
+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 18H6V6H18V18Z" fill="#F5F5F5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 3H21V21H3V3ZM8 8V16H16V8H8Z" fill="#3B7DD8"/>
<path d="M16 8H20V12H16V8Z" fill="#FAB283"/>
</svg>

After

Width:  |  Height:  |  Size: 297 B

+10
View File
@@ -0,0 +1,10 @@
<svg width="234" height="42" viewBox="0 0 234 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 30H6V18H18V30Z" fill="#4B4646"/><path d="M18 12H6V30H18V12ZM24 36H0V6H24V36Z" fill="#B7B1B1"/>
<path d="M48 30H36V18H48V30Z" fill="#4B4646"/><path d="M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z" fill="#B7B1B1"/>
<path d="M84 24V30H66V24H84Z" fill="#4B4646"/><path d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="#B7B1B1"/>
<path d="M108 36H96V18H108V36Z" fill="#4B4646"/><path d="M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z" fill="#B7B1B1"/>
<path d="M144 30H126V18H144V30Z" fill="#4B4646"/><path d="M144 12H126V30H144V36H120V6H144V12Z" fill="#F1ECEC"/>
<path d="M168 30H156V18H168V30Z" fill="#4B4646"/><path d="M168 12H156V30H168V12ZM174 36H150V6H174V36Z" fill="#F1ECEC"/>
<path d="M198 30H186V18H198V30Z" fill="#4B4646"/><path d="M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z" fill="#F1ECEC"/>
<path d="M234 24V30H216V24H234Z" fill="#4B4646"/><path d="M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z" fill="#F1ECEC"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg width="234" height="42" viewBox="0 0 234 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 30H6V18H18V30Z" fill="#CFCECD"/><path d="M18 12H6V30H18V12ZM24 36H0V6H24V36Z" fill="#656363"/>
<path d="M48 30H36V18H48V30Z" fill="#CFCECD"/><path d="M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z" fill="#656363"/>
<path d="M84 24V30H66V24H84Z" fill="#CFCECD"/><path d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="#656363"/>
<path d="M108 36H96V18H108V36Z" fill="#CFCECD"/><path d="M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z" fill="#656363"/>
<path d="M144 30H126V18H144V30Z" fill="#CFCECD"/><path d="M144 12H126V30H144V36H120V6H144V12Z" fill="#211E1E"/>
<path d="M168 30H156V18H168V30Z" fill="#CFCECD"/><path d="M168 12H156V30H168V12ZM174 36H150V6H174V36Z" fill="#211E1E"/>
<path d="M198 30H186V18H198V30Z" fill="#CFCECD"/><path d="M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z" fill="#211E1E"/>
<path d="M234 24V30H216V24H234Z" fill="#CFCECD"/><path d="M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z" fill="#211E1E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
import { defineDocs } from "fumadocs-mdx/config"
export const docs = defineDocs({
dir: "content/docs",
})
+55
View File
@@ -0,0 +1,55 @@
import { Children, isValidElement, type ComponentProps, type ReactNode } from "react"
import { Callout } from "fumadocs-ui/components/callout"
import { Card, Cards } from "fumadocs-ui/components/card"
import { Tab as FumadocsTab, Tabs as FumadocsTabs } from "fumadocs-ui/components/tabs"
import defaultMdxComponents from "fumadocs-ui/mdx"
import type { MDXComponents } from "mdx/types"
function Tip(props: { children: ReactNode }) {
return <Callout type="idea">{props.children}</Callout>
}
function Note(props: { children: ReactNode }) {
return <Callout>{props.children}</Callout>
}
function Warning(props: { children: ReactNode }) {
return <Callout type="warn">{props.children}</Callout>
}
function Tab(props: { title?: string; value?: string; children: ReactNode }) {
return <FumadocsTab value={props.value ?? props.title}>{props.children}</FumadocsTab>
}
function Tabs(props: { children: ReactNode }) {
const items = Children.toArray(props.children).flatMap((child) => {
if (!isValidElement<ComponentProps<typeof Tab>>(child)) return []
const value = child.props.value ?? child.props.title
return value ? [value] : []
})
return <FumadocsTabs items={items}>{props.children}</FumadocsTabs>
}
function CardGroup(props: { children: ReactNode; cols?: number }) {
return <Cards>{props.children}</Cards>
}
export function getMdxComponents(components?: MDXComponents) {
return {
...defaultMdxComponents,
Tip,
Note,
Warning,
Tabs,
Tab,
CardGroup,
Card,
...components,
} satisfies MDXComponents
}
export const useMDXComponents = getMdxComponents
declare global {
type MDXProvidedComponents = ReturnType<typeof getMdxComponents>
}
+22
View File
@@ -0,0 +1,22 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"
export function baseOptions(): BaseLayoutProps {
return {
nav: {
title: "OpenCode",
url: "/",
},
links: [
{
text: "Docs",
url: "/docs",
active: "nested-url",
},
{
text: "GitHub",
url: "https://github.com/anomalyco/opencode",
external: true,
},
],
}
}
+7
View File
@@ -0,0 +1,7 @@
import { docs } from "collections/server"
import { loader } from "fumadocs-core/source"
export const source = loader({
baseUrl: "/docs",
source: docs.toFumadocsSource(),
})
+104
View File
@@ -0,0 +1,104 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as DocsSplatRouteImport } from './routes/docs/$'
import { Route as ApiSearchRouteImport } from './routes/api/search'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const DocsSplatRoute = DocsSplatRouteImport.update({
id: '/docs/$',
path: '/docs/$',
getParentRoute: () => rootRouteImport,
} as any)
const ApiSearchRoute = ApiSearchRouteImport.update({
id: '/api/search',
path: '/api/search',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/api/search': typeof ApiSearchRoute
'/docs/$': typeof DocsSplatRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/api/search': typeof ApiSearchRoute
'/docs/$': typeof DocsSplatRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/api/search': typeof ApiSearchRoute
'/docs/$': typeof DocsSplatRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/api/search' | '/docs/$'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/api/search' | '/docs/$'
id: '__root__' | '/' | '/api/search' | '/docs/$'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ApiSearchRoute: typeof ApiSearchRoute
DocsSplatRoute: typeof DocsSplatRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/docs/$': {
id: '/docs/$'
path: '/docs/$'
fullPath: '/docs/$'
preLoaderRoute: typeof DocsSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/api/search': {
id: '/api/search'
path: '/api/search'
fullPath: '/api/search'
preLoaderRoute: typeof ApiSearchRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ApiSearchRoute: ApiSearchRoute,
DocsSplatRoute: DocsSplatRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}
+16
View File
@@ -0,0 +1,16 @@
import { createRouter } from "@tanstack/react-router"
import { routeTree } from "./routeTree.gen"
export function getRouter() {
return createRouter({
routeTree,
scrollRestoration: true,
defaultPreload: "intent",
})
}
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof getRouter>
}
}
+34
View File
@@ -0,0 +1,34 @@
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router"
import { RootProvider } from "fumadocs-ui/provider/tanstack"
import type { ReactNode } from "react"
import styles from "../styles.css?url"
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ title: "OpenCode" },
{
name: "description",
content: "The open source AI coding agent.",
},
],
links: [{ rel: "stylesheet", href: styles }],
}),
shellComponent: RootDocument,
})
function RootDocument(props: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<HeadContent />
</head>
<body>
<RootProvider>{props.children}</RootProvider>
<Scripts />
</body>
</html>
)
}
+15
View File
@@ -0,0 +1,15 @@
import { createFileRoute } from "@tanstack/react-router"
import { createFromSource } from "fumadocs-core/search/server"
import { source } from "@/lib/source"
const search = createFromSource(source, {
language: "english",
})
export const Route = createFileRoute("/api/search")({
server: {
handlers: {
GET: ({ request }) => search.GET(request),
},
},
})
+62
View File
@@ -0,0 +1,62 @@
import { createFileRoute, notFound } from "@tanstack/react-router"
import { createServerFn } from "@tanstack/react-start"
import browserCollections from "collections/browser"
import { useFumadocsLoader } from "fumadocs-core/source/client"
import { DocsLayout } from "fumadocs-ui/layouts/docs"
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/layouts/docs/page"
import { Suspense } from "react"
import { getMdxComponents } from "@/components/mdx"
import { baseOptions } from "@/lib/layout"
import { source } from "@/lib/source"
export const Route = createFileRoute("/docs/$")({
loader: async ({ params }) => {
const slugs = params._splat?.split("/").filter(Boolean) ?? []
const data = await loadPage({ data: slugs })
await clientLoader.preload(data.path)
return data
},
head: ({ loaderData }) => ({
meta: [
{ title: `${loaderData?.title ?? "Docs"} | OpenCode` },
{ name: "description", content: loaderData?.description ?? "OpenCode documentation." },
],
}),
component: Documentation,
})
const loadPage = createServerFn({ method: "GET" })
.validator((slugs: string[]) => slugs)
.handler(async ({ data: slugs }) => {
const page = source.getPage(slugs)
if (!page) throw notFound()
return {
path: page.path,
title: page.data.title,
description: page.data.description,
tree: await source.serializePageTree(source.getPageTree()),
}
})
const clientLoader = browserCollections.docs.createClientLoader({
component({ toc, frontmatter, default: Content }) {
return (
<DocsPage toc={toc}>
<DocsTitle>{frontmatter.title}</DocsTitle>
<DocsDescription>{frontmatter.description}</DocsDescription>
<DocsBody>
<Content components={getMdxComponents()} />
</DocsBody>
</DocsPage>
)
},
})
function Documentation() {
const data = useFumadocsLoader(Route.useLoaderData())
return (
<DocsLayout {...baseOptions()} tree={data.tree} tabMode="top">
<Suspense>{clientLoader.useContent(data.path)}</Suspense>
</DocsLayout>
)
}
+18
View File
@@ -0,0 +1,18 @@
import { createFileRoute } from "@tanstack/react-router"
export const Route = createFileRoute("/")({
component: Home,
})
function Home() {
return (
<main className="home">
<p className="home-kicker">OpenCode</p>
<h1>The open source AI coding agent</h1>
<p className="home-copy">The new opencode.ai SSR site is running. The V2 documentation is available now.</p>
<a className="home-link" href="/docs">
Read the docs
</a>
</main>
)
}
+79
View File
@@ -0,0 +1,79 @@
@import "tailwindcss";
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";
:root {
--site-background: #f7f7f5;
--site-foreground: #171717;
--site-border: #d8d8d2;
--site-accent: #3b7dd8;
--fd-layout-width: 90rem;
}
* {
box-sizing: border-box;
}
html,
body {
min-height: 100%;
}
body {
margin: 0;
}
.home {
min-height: 100vh;
display: grid;
align-content: center;
justify-items: start;
gap: 1.5rem;
padding: clamp(1.5rem, 7vw, 7rem);
color: var(--site-foreground);
background: var(--site-background);
}
.home-kicker,
.home-copy,
.home h1 {
margin: 0;
}
.home-kicker {
font:
600 0.75rem/1.2 ui-monospace,
monospace;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.home h1 {
max-width: 13ch;
font:
500 clamp(3rem, 9vw, 8rem) / 0.92 ui-monospace,
monospace;
letter-spacing: -0.07em;
}
.home-copy {
max-width: 42rem;
font:
400 clamp(1rem, 2vw, 1.25rem) / 1.5 ui-monospace,
monospace;
}
.home-link {
display: inline-block;
padding: 0.8rem 1rem;
color: var(--site-background);
background: var(--site-foreground);
font:
500 0.875rem/1 ui-monospace,
monospace;
text-decoration: none;
}
.home-link:hover {
background: var(--site-accent);
}
+24
View File
@@ -0,0 +1,24 @@
{
"include": ["src/**/*.ts", "src/**/*.tsx", "source.config.ts", "vite.config.ts", ".source/**/*.ts"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"paths": {
"@/*": ["./src/*"],
"collections/*": ["./.source/*"]
}
}
}
+22
View File
@@ -0,0 +1,22 @@
import { cloudflare } from "@cloudflare/vite-plugin"
import tailwindcss from "@tailwindcss/vite"
import { tanstackStart } from "@tanstack/react-start/plugin/vite"
import viteReact from "@vitejs/plugin-react"
import mdx from "fumadocs-mdx/vite"
import { defineConfig } from "vite"
export default defineConfig({
resolve: {
tsconfigPaths: true,
},
plugins: [
cloudflare({
viteEnvironment: { name: "ssr" },
configPath: process.env.SST_WRANGLER_PATH,
}),
tailwindcss(),
mdx(),
tanstackStart(),
viteReact(),
],
})
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "opencode-www",
"compatibility_date": "2026-07-12",
"compatibility_flags": ["nodejs_compat"],
"main": "@tanstack/react-start/server-entry",
}