mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-28 06:02:05 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6714f74433 |
@@ -12,7 +12,6 @@
|
||||
"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",
|
||||
@@ -103,8 +102,6 @@
|
||||
"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:",
|
||||
|
||||
@@ -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": "Manage MCP servers",
|
||||
"command.mcp.toggle.description": "Enable or disable MCP servers",
|
||||
"command.mcp.toggle": "Toggle MCPs",
|
||||
"command.mcp.toggle.description": "Toggle MCPs",
|
||||
"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": "MCP servers",
|
||||
"dialog.mcp.title": "MCPs",
|
||||
"dialog.mcp.description": "{{enabled}} of {{total}} enabled",
|
||||
"dialog.mcp.empty": "No MCP servers configured",
|
||||
"dialog.mcp.empty": "No MCPs configured",
|
||||
|
||||
"dialog.lsp.empty": "LSPs auto-detected from file types",
|
||||
"dialog.plugins.empty": "Plugins configured in opencode.json",
|
||||
|
||||
@@ -171,6 +171,7 @@ 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
|
||||
|
||||
|
||||
@@ -727,16 +727,16 @@ const invokeArrayMethod = <R>(
|
||||
}
|
||||
return undefined
|
||||
case "reduce": {
|
||||
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
|
||||
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
|
||||
}
|
||||
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 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
|
||||
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
|
||||
}
|
||||
for (let index = start; index >= 0; index -= 1) {
|
||||
if (!(index in target)) continue
|
||||
@@ -764,8 +764,7 @@ const invokeArrayMethod = <R>(
|
||||
}
|
||||
case "findLast":
|
||||
for (let index = length - 1; index >= 0; index -= 1) {
|
||||
const item = target[index]
|
||||
if (yield* apply([item, index, target])) return item
|
||||
if (yield* apply([target[index], index, target])) return target[index]
|
||||
}
|
||||
return undefined
|
||||
case "findLastIndex":
|
||||
|
||||
@@ -26,11 +26,9 @@
|
||||
* - 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
|
||||
@@ -212,11 +210,6 @@ 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`,
|
||||
@@ -232,11 +225,6 @@ 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`,
|
||||
@@ -335,46 +323,3 @@ 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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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.group(server.server))}]\`.`,
|
||||
` Use tools from this server through \`execute\` under \`tools[${JSON.stringify(McpTool.namespace(server.server))}]\`.`,
|
||||
...server.instructions.split("\n").map((line) => ` ${line}`),
|
||||
" </server>",
|
||||
])
|
||||
|
||||
@@ -305,27 +305,13 @@ 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.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 }
|
||||
}),
|
||||
Effect.forEach(
|
||||
Tool.fromDraft(callback),
|
||||
({ name, tool, options }) => tools.register({ [name]: tool }, options),
|
||||
{ discard: true },
|
||||
).pipe(Effect.orDie, Effect.as({ dispose: Effect.void })),
|
||||
hook: (name, callback) => {
|
||||
if (name === "execute.before") {
|
||||
return toolHooks.hook.before((event) => {
|
||||
|
||||
@@ -154,7 +154,11 @@ export function fromPromise(plugin: Plugin) {
|
||||
register(
|
||||
host.tool.transform((draft) =>
|
||||
callback({
|
||||
add: (tool: AnyTool) => draft.add(tool.name, fromPromiseTool(tool), tool.options),
|
||||
add: (tool: AnyTool) =>
|
||||
draft.add(tool.name, fromPromiseTool(tool), {
|
||||
...(tool.namespace !== undefined ? { namespace: tool.namespace } : {}),
|
||||
...(tool.codemode !== undefined ? { codemode: tool.codemode } : {}),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,7 +2,6 @@ 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
|
||||
@@ -192,18 +191,20 @@ 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 }),
|
||||
.model({ id: resolved.modelID ?? resolved.id }),
|
||||
)
|
||||
}
|
||||
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
|
||||
|
||||
@@ -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
|
||||
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).
|
||||
`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).
|
||||
|
||||
Registrations are scoped:
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ type CollectedFiles = {
|
||||
interface Registration {
|
||||
readonly tool: AnyTool
|
||||
readonly name: string
|
||||
readonly group?: string
|
||||
readonly namespace?: 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.group === undefined) {
|
||||
if (registration.namespace === 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.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
|
||||
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
|
||||
continue
|
||||
}
|
||||
const entries: Record<string, Tool.Definition<never>> = {}
|
||||
|
||||
@@ -13,10 +13,10 @@ import { Tools } from "./tools"
|
||||
import { ToolRegistry } from "./registry"
|
||||
|
||||
/**
|
||||
* Registry group and permission action names for MCP tools.
|
||||
* Registry namespace and permission action names for MCP tools.
|
||||
*/
|
||||
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 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 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 groups = new Map<string, Record<string, Tool.AnyTool>>()
|
||||
const byServer = new Map<string, Record<string, Tool.AnyTool>>()
|
||||
for (const tool of yield* mcp.tools()) {
|
||||
const group = groups.get(tool.server) ?? {}
|
||||
const record = byServer.get(tool.server) ?? {}
|
||||
const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema
|
||||
group[tool.name] = Tool.withPermission(
|
||||
record[tool.name] = Tool.withPermission(
|
||||
Tool.make({
|
||||
description: tool.description ?? "",
|
||||
jsonSchema: {
|
||||
@@ -102,16 +102,12 @@ export const layer = Layer.effectDiscard(
|
||||
}),
|
||||
name(tool.server, tool.name),
|
||||
)
|
||||
groups.set(tool.server, group)
|
||||
byServer.set(tool.server, record)
|
||||
}
|
||||
const next = yield* Scope.fork(scope)
|
||||
yield* Effect.forEach(
|
||||
groups,
|
||||
([group, record]) => tools.register(record, { group }),
|
||||
{
|
||||
discard: true,
|
||||
},
|
||||
).pipe(Scope.provide(next), Effect.orDie)
|
||||
yield* Effect.forEach(byServer, ([server, record]) => tools.register(record, { namespace: server }), {
|
||||
discard: true,
|
||||
}).pipe(Scope.provide(next), Effect.orDie)
|
||||
if (current) yield* Scope.close(current, Exit.void)
|
||||
current = next
|
||||
}),
|
||||
|
||||
@@ -54,7 +54,7 @@ const registryLayer = Layer.effect(
|
||||
type Registration = {
|
||||
readonly tool: AnyTool
|
||||
readonly name: string
|
||||
readonly group?: string
|
||||
readonly namespace?: 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?.group)
|
||||
const entries = registrationEntries(tools, options?.namespace)
|
||||
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,
|
||||
group: entry.group,
|
||||
namespace: entry.namespace,
|
||||
codemode,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -46,24 +46,11 @@ export const registerToolPlugin = <R>(plugin: {
|
||||
const context = host({
|
||||
tool: {
|
||||
transform: (callback) =>
|
||||
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 }
|
||||
}),
|
||||
Effect.forEach(
|
||||
Tool.fromDraft(callback),
|
||||
({ name, tool, options }) => tools.register({ [name]: tool }, options),
|
||||
{ discard: true },
|
||||
).pipe(Effect.orDie, Effect.as({ dispose: Effect.void })),
|
||||
hook: () => Effect.die("registerToolPlugin does not support tool hooks"),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -274,7 +274,7 @@ describe("PluginV2", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("groups tool names and routes codemode registrations through execute", () =>
|
||||
it.effect("namespaces 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: "grouped-tools",
|
||||
id: "namespaced-tools",
|
||||
effect: (ctx) =>
|
||||
ctx.tool
|
||||
.transform((draft) => {
|
||||
draft.add("plain", tool("Plain"), { codemode: false })
|
||||
draft.add("look/up", tool("Lookup"), { group: "context 7", codemode: false })
|
||||
draft.add("search", tool("Search"), { group: "context 7" })
|
||||
draft.add("look/up", tool("Lookup"), { namespace: "context 7", codemode: false })
|
||||
draft.add("search", tool("Search"), { namespace: "context 7" })
|
||||
})
|
||||
.pipe(Effect.orDie),
|
||||
})
|
||||
@@ -307,6 +307,41 @@ 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
|
||||
|
||||
@@ -135,7 +135,7 @@ describe("fromPromise", () => {
|
||||
await ctx.tool.transform((tools) => {
|
||||
tools.add({
|
||||
name: "hello",
|
||||
options: { codemode: false },
|
||||
codemode: false,
|
||||
description: "Hello",
|
||||
input: Schema.Struct({ name: Schema.String }),
|
||||
output: Schema.String,
|
||||
|
||||
@@ -64,20 +64,6 @@ 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(
|
||||
|
||||
Vendored
+5
-4
@@ -312,12 +312,13 @@ export default Plugin.define({
|
||||
})
|
||||
```
|
||||
|
||||
Unsupported characters in tool and group names are normalized to underscores.
|
||||
Unsupported characters in tool and namespace 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, codemode }`:
|
||||
letters, digits, underscores, or hyphens. Set registration fields on the
|
||||
declaration or options with `{ namespace, codemode }`:
|
||||
|
||||
- `group` prefixes and groups the exposed tool name.
|
||||
- `namespace` prefixes the exposed tool name (and becomes the CodeMode path
|
||||
segment).
|
||||
- `codemode` defaults to `true` and makes the tool available through the
|
||||
`execute` CodeMode tool. Set `codemode: false` to expose it directly to the
|
||||
provider.
|
||||
|
||||
@@ -7,7 +7,6 @@ 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")
|
||||
|
||||
|
||||
@@ -159,6 +159,7 @@ 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/"
|
||||
@@ -184,12 +185,13 @@ for (const item of targets) {
|
||||
windows: {},
|
||||
},
|
||||
files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},
|
||||
entrypoints: ["./src/index.ts", parserWorker, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
|
||||
entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(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") } : {}),
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
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,
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,198 @@
|
||||
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
|
||||
@@ -0,0 +1,8 @@
|
||||
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)))
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
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)
|
||||
@@ -18,7 +18,9 @@ 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"
|
||||
@@ -80,6 +82,8 @@ const cli = yargs(args)
|
||||
.command(AcpCommand)
|
||||
.command(McpCommand)
|
||||
.command(V2ServeCommand)
|
||||
.command(TuiThreadCommand)
|
||||
.command(AttachCommand)
|
||||
.command(RunCommand)
|
||||
.command(GenerateCommand)
|
||||
.command(DebugCommand)
|
||||
|
||||
@@ -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 } from "@opencode-ai/tui/plugin/runtime"
|
||||
import { createPluginRuntime, type PluginRuntime, type TuiPluginHost } from "@opencode-ai/tui/plugin/runtime"
|
||||
|
||||
ensureRuntimePluginSupport({ additional: keymapRuntimeModules })
|
||||
|
||||
@@ -1121,4 +1121,11 @@ async function load(input: {
|
||||
}
|
||||
}
|
||||
|
||||
export function createLegacyTuiPluginHost(): TuiPluginHost {
|
||||
return {
|
||||
start: init,
|
||||
dispose,
|
||||
}
|
||||
}
|
||||
|
||||
export * as TuiPluginRuntime from "./runtime"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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"
|
||||
@@ -28,4 +29,5 @@ const cli = yargs(hideBin(process.argv))
|
||||
if (opts.logLevel) process.env.OPENCODE_LOG_LEVEL = opts.logLevel
|
||||
})
|
||||
.command(V2ServeCommand)
|
||||
.command(TuiThreadCommand)
|
||||
.parse()
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
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")')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,89 @@
|
||||
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")
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -123,6 +123,17 @@ 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>,
|
||||
@@ -212,14 +223,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>>, group?: string) =>
|
||||
export const registrationEntries = (tools: Readonly<Record<string, AnyTool>>, namespace?: string) =>
|
||||
Object.entries(tools).map(([name, tool]) => {
|
||||
const normalized = name.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
const parent = group?.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
const parent = namespace?.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
return {
|
||||
key: parent === undefined ? normalized : `${parent}_${normalized}`,
|
||||
name: normalized,
|
||||
group: parent,
|
||||
namespace: parent,
|
||||
tool,
|
||||
}
|
||||
})
|
||||
@@ -271,15 +282,68 @@ export interface ToolExecuteAfterEvent {
|
||||
}
|
||||
|
||||
export interface RegisterOptions {
|
||||
readonly group?: string
|
||||
/** Dotted CodeMode path prefix, e.g. "slack.admin". */
|
||||
readonly namespace?: 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
|
||||
|
||||
@@ -16,7 +16,8 @@ export type Definition<
|
||||
Structured extends SchemaType<any> = Output,
|
||||
> = {
|
||||
readonly name: string
|
||||
readonly options?: RegisterOptions
|
||||
readonly namespace?: string
|
||||
readonly codemode?: boolean
|
||||
readonly description: string
|
||||
readonly input: Input
|
||||
readonly output: Output
|
||||
@@ -37,7 +38,8 @@ export type Definition<
|
||||
|
||||
export type DynamicDefinition = {
|
||||
readonly name: string
|
||||
readonly options?: RegisterOptions
|
||||
readonly namespace?: string
|
||||
readonly codemode?: boolean
|
||||
readonly description: string
|
||||
readonly jsonSchema: JsonSchema.JsonSchema
|
||||
readonly outputSchema?: JsonSchema.JsonSchema
|
||||
@@ -67,12 +69,6 @@ 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>,
|
||||
|
||||
+63
-22
@@ -53,7 +53,6 @@ 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"
|
||||
@@ -77,6 +76,7 @@ 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,6 +138,7 @@ const appBindingCommands = [
|
||||
"diff.open",
|
||||
"app.debug",
|
||||
"app.console",
|
||||
"app.heap_snapshot",
|
||||
"terminal.suspend",
|
||||
"terminal.title.toggle",
|
||||
"app.toggle.animations",
|
||||
@@ -153,7 +154,8 @@ export type TuiInput = {
|
||||
reload?: () => Promise<void>
|
||||
}
|
||||
args: Args
|
||||
config: Config.Interface
|
||||
config: Config.Interface | TuiConfigV1.Resolved
|
||||
onSnapshot?: () => Promise<string[]>
|
||||
pluginHost: TuiPluginHost
|
||||
terminalHandoff?: () => Promise<
|
||||
| {
|
||||
@@ -198,12 +200,43 @@ 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 config = Config.resolve(yield* Effect.tryPromise(() => input.config.get()), {
|
||||
terminalSuspend: process.platform !== "win32",
|
||||
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(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(
|
||||
@@ -340,7 +373,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
<ArgsProvider {...input.args}>
|
||||
<ConfigProvider
|
||||
config={config}
|
||||
service={input.config}
|
||||
service={loaded.service}
|
||||
options={{ terminalSuspend: process.platform !== "win32" }}
|
||||
>
|
||||
<KVProvider>
|
||||
@@ -376,7 +409,9 @@ 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
|
||||
@@ -435,7 +470,9 @@ 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" })
|
||||
@@ -485,7 +522,7 @@ function App(props: {
|
||||
toast.show({
|
||||
variant: "error",
|
||||
title: `MCP server failed: ${server.name}`,
|
||||
message: "Open MCP servers to view details.",
|
||||
message: "Open MCPs to view details.",
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -514,6 +551,7 @@ function App(props: {
|
||||
props.pluginHost
|
||||
.start({
|
||||
api,
|
||||
config: props.pluginConfig,
|
||||
runtime: pluginRuntime,
|
||||
dispose: () => attention.dispose(),
|
||||
})
|
||||
@@ -549,12 +587,10 @@ function App(props: {
|
||||
|
||||
renderer.clearSelection()
|
||||
}
|
||||
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
|
||||
})
|
||||
const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true))
|
||||
const [pasteSummaryEnabled, setPasteSummaryEnabled] = createSignal(
|
||||
kv.get("paste_summary_enabled", true),
|
||||
)
|
||||
|
||||
// Update terminal window title based on current route and session
|
||||
createEffect(() => {
|
||||
@@ -749,7 +785,7 @@ function App(props: {
|
||||
},
|
||||
{
|
||||
name: "mcp.list",
|
||||
title: "MCP servers",
|
||||
title: "MCP Servers",
|
||||
category: "Agent",
|
||||
slashName: "mcps",
|
||||
run: () => {
|
||||
@@ -813,15 +849,6 @@ 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",
|
||||
@@ -940,6 +967,20 @@ 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",
|
||||
|
||||
@@ -1,420 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -92,7 +92,7 @@ export function DialogMcp() {
|
||||
when={detail()}
|
||||
fallback={
|
||||
<DialogSelect
|
||||
title="MCP servers"
|
||||
title="MCPs"
|
||||
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 server: {props.server.name}
|
||||
MCP / {props.server.name}
|
||||
</text>
|
||||
<text fg={theme.textMuted} onMouseUp={props.onBack}>
|
||||
esc back
|
||||
|
||||
@@ -22,11 +22,9 @@ 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 server{mcp().length === 1 ? "" : "s"}
|
||||
</text>
|
||||
<text fg={theme.text}>{mcp().length} MCP Servers</text>
|
||||
<For each={mcp()}>
|
||||
{(item) => (
|
||||
<box flexDirection="row" gap={1}>
|
||||
|
||||
@@ -195,9 +195,7 @@ 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() {
|
||||
|
||||
@@ -129,15 +129,6 @@ 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()
|
||||
|
||||
@@ -60,6 +60,7 @@ export type PluginRuntime = ReturnType<typeof createPluginRuntime>
|
||||
export type TuiPluginHost = {
|
||||
start(input: {
|
||||
api: TuiPluginApi
|
||||
config: any
|
||||
runtime: PluginRuntime
|
||||
dispose?: () => void
|
||||
}): Promise<void>
|
||||
|
||||
@@ -6,13 +6,8 @@ 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
|
||||
setPrompt?: (prompt: PromptInfo) => void
|
||||
}) {
|
||||
export function DialogMessage(props: { messageID: string; sessionID: string }) {
|
||||
const data = useData()
|
||||
const clipboard = useClipboard()
|
||||
const toast = useToast()
|
||||
@@ -27,26 +22,9 @@ export function DialogMessage(props: {
|
||||
title: "Revert",
|
||||
value: "session.revert",
|
||||
description: "undo messages and file changes",
|
||||
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 })
|
||||
onSelect: async (dialog) => {
|
||||
await sdk.api.session
|
||||
.revert.stage({ sessionID: props.sessionID, messageID: props.messageID })
|
||||
.catch((error) => toast.show({ message: errorMessage(error), variant: "error", duration: 5000 }))
|
||||
dialog.clear()
|
||||
},
|
||||
|
||||
@@ -1468,7 +1468,6 @@ function UserMessage(props: { message: SessionMessageUser }) {
|
||||
)
|
||||
const dialog = useDialog()
|
||||
const renderer = useRenderer()
|
||||
const promptRef = usePromptRef()
|
||||
|
||||
return (
|
||||
<Show when={props.message.text.trim() || files().length}>
|
||||
@@ -1487,13 +1486,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
if (renderer.getSelection()?.getSelectedText()) return
|
||||
dialog.replace(() => (
|
||||
<DialogMessage
|
||||
messageID={props.message.id}
|
||||
sessionID={ctx.sessionID}
|
||||
setPrompt={(value) => promptRef.current?.set(value)}
|
||||
/>
|
||||
))
|
||||
dialog.replace(() => <DialogMessage messageID={props.message.id} sessionID={ctx.sessionID} />)
|
||||
}}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
|
||||
@@ -292,7 +292,7 @@ The `mcp debug` command shows the current auth status, tests HTTP connectivity,
|
||||
|
||||
## Manage
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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 MCP tools.
|
||||
We can also use a glob pattern to disable all matching MCPs.
|
||||
|
||||
```json title="opencode.json" {14}
|
||||
{
|
||||
@@ -340,7 +340,7 @@ We can also use a glob pattern to disable all matching MCP tools.
|
||||
}
|
||||
```
|
||||
|
||||
Here we are using the glob pattern `my-mcp*` to disable all matching MCP tools.
|
||||
Here we are using the glob pattern `my-mcp*` to disable all MCPs.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.source
|
||||
.tanstack
|
||||
.wrangler
|
||||
dist
|
||||
node_modules
|
||||
worker-configuration.d.ts
|
||||
@@ -1,22 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,284 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,168 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,163 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,153 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,450 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,160 +0,0 @@
|
||||
---
|
||||
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).
|
||||
@@ -1,128 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,105 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,262 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,576 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,213 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,209 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,204 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,177 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,227 +0,0 @@
|
||||
---
|
||||
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`.
|
||||
@@ -1,108 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,177 +0,0 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
title: "API Reference"
|
||||
description: "OpenCode HTTP API."
|
||||
---
|
||||
|
||||
The endpoint reference is generated from the current OpenCode V2 [OpenAPI specification](/openapi.json).
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"title": "API",
|
||||
"description": "Use the OpenCode HTTP API.",
|
||||
"root": true,
|
||||
"pages": ["index"]
|
||||
}
|
||||
-145
@@ -1,145 +0,0 @@
|
||||
---
|
||||
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
@@ -1,24 +0,0 @@
|
||||
---
|
||||
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
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"title": "Build",
|
||||
"description": "Extend and embed OpenCode.",
|
||||
"root": true,
|
||||
"defaultOpen": true,
|
||||
"pages": ["index", "plugins", "client", "sdk"]
|
||||
}
|
||||
-430
@@ -1,430 +0,0 @@
|
||||
---
|
||||
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
@@ -1,75 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"title": "Documentation",
|
||||
"pages": ["(docs)", "build", "api"]
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 297 B |
@@ -1,10 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,10 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
import { defineDocs } from "fumadocs-mdx/config"
|
||||
|
||||
export const docs = defineDocs({
|
||||
dir: "content/docs",
|
||||
})
|
||||
@@ -1,55 +0,0 @@
|
||||
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>
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { docs } from "collections/server"
|
||||
import { loader } from "fumadocs-core/source"
|
||||
|
||||
export const source = loader({
|
||||
baseUrl: "/docs",
|
||||
source: docs.toFumadocsSource(),
|
||||
})
|
||||
@@ -1,104 +0,0 @@
|
||||
/* 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>>
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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>
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
@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);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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(),
|
||||
],
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"$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",
|
||||
}
|
||||
Reference in New Issue
Block a user