mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-11 11:26:24 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c52e9cf6c | ||
|
|
b844971cf4 | ||
|
|
79041059c2 | ||
|
|
eb82c26b75 | ||
|
|
1c723c56fa | ||
|
|
181428a2f3 | ||
|
|
9b1891fb7e | ||
|
|
d4bf78b348 | ||
|
|
d4ceffe787 | ||
|
|
872e38055e | ||
|
|
0c1dfa9186 | ||
|
|
8f4d706647 | ||
|
|
929374cdfd | ||
|
|
cfa5ba700e | ||
|
|
45a2ed9a97 | ||
|
|
f6333546f8 | ||
|
|
9e153ce7b3 | ||
|
|
eb357f17cf |
@@ -1,4 +1,4 @@
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { Effect, Option, Schema, SchemaGetter } from "effect"
|
||||
import type { Content } from "@opencode/schema/tool"
|
||||
import { HttpTransport } from "../route/transport/index.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
@@ -325,9 +325,8 @@ export const StreamItem = Schema.StructWithRest(
|
||||
export type StreamItem = Schema.Schema.Type<typeof StreamItem>
|
||||
export type OutputItem = StreamItem & { readonly id: string }
|
||||
|
||||
// The Responses schema puts streaming error details at the top level and
|
||||
// response failures under `response.error`. WebSocket failures use an
|
||||
// event-level `error` envelope, so accept all three shapes here.
|
||||
// Responses-compatible providers put streaming error details at the top level or
|
||||
// under `error`, and response failures under `response.error`. Accept all three shapes.
|
||||
// https://www.openresponses.org/specification
|
||||
const OpenResponsesErrorPayload = Schema.Struct({
|
||||
type: optionalNull(Schema.String),
|
||||
@@ -401,6 +400,17 @@ export const Event = Schema.StructWithRest(
|
||||
headers: Schema.optional(Schema.Unknown),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
).pipe(
|
||||
Schema.decode({
|
||||
decode: SchemaGetter.transform((event) => {
|
||||
if (event.type !== "error" || event.error != null) return event
|
||||
const { code, message, param, ...rest } = event
|
||||
if (code === undefined && message === undefined && param === undefined) return event
|
||||
// Flat errors (for example, Meta's) can also arrive through generic Responses endpoints.
|
||||
return { ...rest, error: { code, message, param } }
|
||||
}),
|
||||
encode: SchemaGetter.passthrough(),
|
||||
}),
|
||||
)
|
||||
export type Event = Schema.Schema.Type<typeof Event>
|
||||
export type NormalizedEvent = Event & { readonly item?: OutputItem | null }
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { LLM, LLMClient } from "../../src/index.js"
|
||||
import { OpenResponses } from "../../src/protocols/open-responses.js"
|
||||
import { Meta } from "../../src/providers/index.js"
|
||||
import { configure } from "../../src/providers/openai-compatible-responses.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
import { fixedResponse } from "../lib/http.js"
|
||||
import { sseEvents } from "../lib/sse.js"
|
||||
|
||||
const decodeEvent = Schema.decodeUnknownEffect(OpenResponses.protocol.stream.event)
|
||||
|
||||
it.effect("normalizes flat errors in shared SSE and WebSocket decoding", () =>
|
||||
Effect.gen(function* () {
|
||||
const frame = {
|
||||
type: "error",
|
||||
sequence_number: 4,
|
||||
code: "server_shutting_down",
|
||||
message: "Server is shutting down. Please retry your request.",
|
||||
param: null,
|
||||
}
|
||||
for (const decode of [decodeEvent, OpenResponses.decodeChannelEvent]) {
|
||||
const event = yield* decode(JSON.stringify(frame))
|
||||
expect(event).toEqual({
|
||||
type: "error",
|
||||
sequence_number: 4,
|
||||
error: { code: frame.code, message: frame.message, param: null },
|
||||
})
|
||||
|
||||
for (const unchanged of [
|
||||
event,
|
||||
{ type: "error" },
|
||||
{
|
||||
type: "response.failed",
|
||||
response: { id: "resp_failed", error: { code: "server_error", message: "Internal server error" } },
|
||||
},
|
||||
{ type: "response.output_text.delta", item_id: "msg_text", delta: "Hello" },
|
||||
]) {
|
||||
expect(yield* decode(JSON.stringify(unchanged))).toEqual(unchanged)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("continues to normalize untyped xAI WebSocket errors", () =>
|
||||
Effect.gen(function* () {
|
||||
const frame = { error: { type: "api_error", message: "gRPC error: Response with id=resp_missing not found" } }
|
||||
expect(yield* OpenResponses.decodeChannelEvent(JSON.stringify(frame))).toEqual({ ...frame, type: "error" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("retains classification and original error bodies through Meta and generic Responses routes", () =>
|
||||
Effect.gen(function* () {
|
||||
const raw = `{
|
||||
"type": "error",
|
||||
"sequence_number": 4,
|
||||
"code": "server_shutting_down",
|
||||
"message": "Server is shutting down. Please retry your request.",
|
||||
"param": null,
|
||||
"diagnostic": "retain-original-frame"
|
||||
}`
|
||||
for (const model of [
|
||||
Meta.configure({ apiKey: "fixture" }).responses("muse-spark-1.3"),
|
||||
configure({ apiKey: "fixture", provider: "gateway", baseURL: "https://responses.example.test/v1" }).model(
|
||||
"example-model",
|
||||
),
|
||||
]) {
|
||||
const error = yield* LLMClient.generate(LLM.request({ model, prompt: "Hello" })).pipe(
|
||||
Effect.provide(fixedResponse(sseEvents(raw.replaceAll("\n", "\ndata: ")))),
|
||||
Effect.flip,
|
||||
)
|
||||
expect(error.reason._tag).toBe("ProviderInternal")
|
||||
expect(error.message).toBe("server_shutting_down: Server is shutting down. Please retry your request.")
|
||||
expect(error.reason.body).toBe(raw)
|
||||
expect(error.reason.http?.status).toBe(200)
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -47,7 +47,7 @@ const handler = Effect.fn("cli.session.list")(function* (
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: formatTable(page.data)) + EOL
|
||||
: formatList(page.data)) + EOL
|
||||
const write = Effect.tryPromise(
|
||||
() =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
@@ -96,18 +96,14 @@ export default Runtime.handler(Commands.commands.session.commands.list, (input)
|
||||
),
|
||||
)
|
||||
|
||||
function formatTable(sessions: ReadonlyArray<SessionInfo>) {
|
||||
const rows = sessions.map((session) => ({
|
||||
id: session.id,
|
||||
title: (session.title ?? "Untitled session").replace(/[\r\n\t]/g, " "),
|
||||
updated: new Date(session.time.updated).toLocaleString(),
|
||||
}))
|
||||
const idWidth = Math.max(20, ...rows.map((row) => row.id.length))
|
||||
const titleWidth = Math.max(25, ...rows.map((row) => row.title.length))
|
||||
const header = `${"Session ID".padEnd(idWidth)} ${"Title".padEnd(titleWidth)} Updated`
|
||||
return [
|
||||
header,
|
||||
"─".repeat(header.length),
|
||||
...rows.map((row) => `${row.id.padEnd(idWidth)} ${row.title.padEnd(titleWidth)} ${row.updated}`),
|
||||
].join(EOL)
|
||||
function formatList(sessions: ReadonlyArray<SessionInfo>) {
|
||||
return sessions
|
||||
.map((session) =>
|
||||
[
|
||||
session.id,
|
||||
(session.title ?? "Untitled session").replace(/[\r\n\t]/g, " "),
|
||||
new Date(session.time.updated).toLocaleString(),
|
||||
].join("\t"),
|
||||
)
|
||||
.join(EOL)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { RelativePath } from "@opencode/schema/schema"
|
||||
import type { Brand } from "effect"
|
||||
import type { Model } from "@opencode/schema/model"
|
||||
import type { DateTime } from "effect"
|
||||
import type { Permission } from "@opencode/schema/permission"
|
||||
import type { SessionMessage } from "@opencode/schema/session-message"
|
||||
import type { SessionInbox } from "@opencode/schema/session-inbox"
|
||||
import type { PromptInput } from "@opencode/schema/prompt-input"
|
||||
@@ -26,7 +27,6 @@ import type { Integration } from "@opencode/schema/integration"
|
||||
import type { Form } from "@opencode/schema/form"
|
||||
import type { Mcp } from "@opencode/schema/mcp"
|
||||
import type { Credential } from "@opencode/schema/credential"
|
||||
import type { Permission } from "@opencode/schema/permission"
|
||||
import type { PermissionSaved } from "@opencode/schema/permission-saved"
|
||||
import type { FileSystem } from "@opencode/schema/filesystem"
|
||||
import type { Command } from "@opencode/schema/command"
|
||||
@@ -209,6 +209,7 @@ export type SessionCreateInput = {
|
||||
readonly model?: Model.Ref | undefined
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly metadata?: Session.Metadata | undefined
|
||||
readonly permissions?: Permission.Ruleset | undefined
|
||||
}
|
||||
export type SessionCreateOutput = Session.Info
|
||||
export type SessionCreateOperation<E = never> = (input?: SessionCreateInput) => Effect.Effect<SessionCreateOutput, E>
|
||||
@@ -437,6 +438,7 @@ export type SessionLogOutput =
|
||||
readonly agent?: Agent.ID | undefined
|
||||
readonly model?: Model.Ref | undefined
|
||||
readonly metadata?: Session.Metadata | undefined
|
||||
readonly permissions?: Permission.Ruleset | undefined
|
||||
readonly version: string
|
||||
}
|
||||
}
|
||||
@@ -489,6 +491,15 @@ export type SessionLogOutput =
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly title: string }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: number
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.permissions.updated"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly permissions: Permission.Ruleset }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: number
|
||||
@@ -1585,6 +1596,12 @@ export type PermissionReplyOperation<E = never> = (
|
||||
input: PermissionReplyInput,
|
||||
) => Effect.Effect<PermissionReplyOutput, E>
|
||||
|
||||
export type PermissionRulesInput = { readonly sessionID: Session.ID; readonly permissions: Permission.Ruleset }
|
||||
export type PermissionRulesOutput = void
|
||||
export type PermissionRulesOperation<E = never> = (
|
||||
input: PermissionRulesInput,
|
||||
) => Effect.Effect<PermissionRulesOutput, E>
|
||||
|
||||
export interface PermissionApi<E = never> {
|
||||
readonly request: { readonly list: PermissionRequestListOperation<E> }
|
||||
readonly saved: { readonly list: PermissionSavedListOperation<E>; readonly remove: PermissionSavedRemoveOperation<E> }
|
||||
@@ -1592,6 +1609,7 @@ export interface PermissionApi<E = never> {
|
||||
readonly list: PermissionListOperation<E>
|
||||
readonly get: PermissionGetOperation<E>
|
||||
readonly reply: PermissionReplyOperation<E>
|
||||
readonly rules: PermissionRulesOperation<E>
|
||||
}
|
||||
|
||||
export type FileListInput = {
|
||||
|
||||
@@ -181,6 +181,8 @@ import type {
|
||||
PermissionGetOutput,
|
||||
PermissionReplyInput,
|
||||
PermissionReplyOutput,
|
||||
PermissionRulesInput,
|
||||
PermissionRulesOutput,
|
||||
FileListInput,
|
||||
FileListOutput,
|
||||
FileFindInput,
|
||||
@@ -395,6 +397,7 @@ const EndpointSessionCreate = (raw: RawClient["server.session"]) => (input?: Ses
|
||||
model: input?.["model"],
|
||||
location: input?.["location"],
|
||||
metadata: input?.["metadata"],
|
||||
permissions: input?.["permissions"],
|
||||
},
|
||||
}).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
@@ -1145,6 +1148,14 @@ const EndpointPermissionReply = (raw: RawClient["server.permission"]) => (input:
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const EndpointPermissionRules = (raw: RawClient["server.permission"]) => (input: PermissionRulesInput) =>
|
||||
preserveEffect<PermissionRulesOutput>()(
|
||||
raw["session.permission.rules"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { permissions: input["permissions"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const adaptGroupPermission = (raw: RawClient["server.permission"]) => ({
|
||||
request: { list: EndpointPermissionRequestList(raw) },
|
||||
saved: { list: EndpointPermissionSavedList(raw), remove: EndpointPermissionSavedRemove(raw) },
|
||||
@@ -1152,6 +1163,7 @@ const adaptGroupPermission = (raw: RawClient["server.permission"]) => ({
|
||||
list: EndpointPermissionList(raw),
|
||||
get: EndpointPermissionGet(raw),
|
||||
reply: EndpointPermissionReply(raw),
|
||||
rules: EndpointPermissionRules(raw),
|
||||
})
|
||||
|
||||
const EndpointFileList = (raw: RawClient["server.fs"]) => (input?: FileListInput) =>
|
||||
|
||||
@@ -175,6 +175,8 @@ import type {
|
||||
PermissionGetOutput,
|
||||
PermissionReplyInput,
|
||||
PermissionReplyOutput,
|
||||
PermissionRulesInput,
|
||||
PermissionRulesOutput,
|
||||
FileReadInput,
|
||||
FileReadOutput,
|
||||
FileListInput,
|
||||
@@ -565,6 +567,7 @@ export function make(options: ClientOptions) {
|
||||
model: input?.["model"],
|
||||
location: input?.["location"],
|
||||
metadata: input?.["metadata"],
|
||||
permissions: input?.["permissions"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
@@ -1566,6 +1569,18 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
rules: (input: PermissionRulesInput, requestOptions?: RequestOptions) =>
|
||||
request<PermissionRulesOutput>(
|
||||
{
|
||||
method: "PUT",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/rules`,
|
||||
body: { permissions: input["permissions"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
file: {
|
||||
read: (input: FileReadInput, requestOptions?: RequestOptions) =>
|
||||
|
||||
@@ -551,28 +551,6 @@ export type InstructionEntryInfo = { key: InstructionEntryKey; value: JsonValue
|
||||
|
||||
export type InstructionEntrySnapshot = Array<{ key: InstructionEntryKey; value: JsonValue; removed: boolean }>
|
||||
|
||||
export type SessionCreated = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.created"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: {
|
||||
sessionID: string
|
||||
projectID: string
|
||||
location: LocationRef
|
||||
subpath?: string
|
||||
parentID?: string
|
||||
slug: string
|
||||
title?: string
|
||||
agent?: string
|
||||
model?: ModelRef
|
||||
metadata?: SessionMetadata
|
||||
version: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionAgentSelected = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -1651,24 +1629,6 @@ export type SessionInboxMove = {
|
||||
delivery: SessionInboxDelivery
|
||||
}
|
||||
|
||||
export type SessionInfo = {
|
||||
id: string
|
||||
parentID?: string
|
||||
fork?: { sessionID: string; boundary: SessionForkBoundary }
|
||||
projectID: string
|
||||
agent?: string
|
||||
model?: ModelRef
|
||||
cost: MoneyUSD
|
||||
tokens: TokenUsageInfo
|
||||
outcome?: "succeeded" | "failed" | "interrupted"
|
||||
time: { created: number; updated: number; idle?: number; viewed?: number; archived?: number }
|
||||
title?: string
|
||||
location: LocationRef
|
||||
subpath?: string
|
||||
metadata?: SessionMetadata
|
||||
revert?: SessionRevert
|
||||
}
|
||||
|
||||
export type SessionRevertStaged = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -1912,6 +1872,58 @@ export type AgentInfo = {
|
||||
permissions: PermissionRuleset
|
||||
}
|
||||
|
||||
export type SessionPermissionsUpdated = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.permissions.updated"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; permissions: PermissionRuleset }
|
||||
}
|
||||
|
||||
export type SessionInfo = {
|
||||
id: string
|
||||
parentID?: string
|
||||
fork?: { sessionID: string; boundary: SessionForkBoundary }
|
||||
projectID: string
|
||||
agent?: string
|
||||
model?: ModelRef
|
||||
cost: MoneyUSD
|
||||
tokens: TokenUsageInfo
|
||||
outcome?: "succeeded" | "failed" | "interrupted"
|
||||
time: { created: number; updated: number; idle?: number; viewed?: number; archived?: number }
|
||||
title?: string
|
||||
location: LocationRef
|
||||
subpath?: string
|
||||
metadata?: SessionMetadata
|
||||
permissions?: PermissionRuleset
|
||||
revert?: SessionRevert
|
||||
}
|
||||
|
||||
export type SessionCreated = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.created"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: {
|
||||
sessionID: string
|
||||
projectID: string
|
||||
location: LocationRef
|
||||
subpath?: string
|
||||
parentID?: string
|
||||
slug: string
|
||||
title?: string
|
||||
agent?: string
|
||||
model?: ModelRef
|
||||
metadata?: SessionMetadata
|
||||
permissions?: PermissionRuleset
|
||||
version: string
|
||||
}
|
||||
}
|
||||
|
||||
export type ConfigEntry =
|
||||
| {
|
||||
type: "document"
|
||||
@@ -2084,8 +2096,6 @@ export type ConfigEntry =
|
||||
| { type: "agents"; path: string }
|
||||
| { type: "claude"; path: string }
|
||||
|
||||
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
|
||||
|
||||
export type SessionInboxUser = {
|
||||
id: string
|
||||
sessionID: string
|
||||
@@ -2140,6 +2150,8 @@ export type FormFields = [FormField, ...Array<FormField>]
|
||||
|
||||
export type FormFields2 = [FormField1, ...Array<FormField1>]
|
||||
|
||||
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
|
||||
|
||||
export type SessionInboxInfo = SessionInboxUser | SessionInboxSynthetic | SessionInboxCompaction | SessionInboxMove
|
||||
|
||||
export type SessionInboxEnqueued = {
|
||||
@@ -2233,6 +2245,7 @@ export type SessionEventDurable =
|
||||
| SessionModelSelected
|
||||
| SessionMoved
|
||||
| SessionRenamed
|
||||
| SessionPermissionsUpdated
|
||||
| SessionViewed
|
||||
| SessionDeleted
|
||||
| SessionForked
|
||||
@@ -2292,6 +2305,7 @@ export type V2Event =
|
||||
| SessionModelSelected
|
||||
| SessionMoved
|
||||
| SessionRenamed
|
||||
| SessionPermissionsUpdated
|
||||
| SessionViewed
|
||||
| SessionUsageUpdated
|
||||
| SessionDeleted
|
||||
@@ -2804,6 +2818,11 @@ export type SessionCreateInput = {
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue } | null
|
||||
readonly permissions?: ReadonlyArray<{
|
||||
readonly action: string
|
||||
readonly resource: string
|
||||
readonly effect: "allow" | "deny" | "ask"
|
||||
}> | null
|
||||
}["id"]
|
||||
readonly title?: {
|
||||
readonly id?: string | null
|
||||
@@ -2812,6 +2831,11 @@ export type SessionCreateInput = {
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue } | null
|
||||
readonly permissions?: ReadonlyArray<{
|
||||
readonly action: string
|
||||
readonly resource: string
|
||||
readonly effect: "allow" | "deny" | "ask"
|
||||
}> | null
|
||||
}["title"]
|
||||
readonly agent?: {
|
||||
readonly id?: string | null
|
||||
@@ -2820,6 +2844,11 @@ export type SessionCreateInput = {
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue } | null
|
||||
readonly permissions?: ReadonlyArray<{
|
||||
readonly action: string
|
||||
readonly resource: string
|
||||
readonly effect: "allow" | "deny" | "ask"
|
||||
}> | null
|
||||
}["agent"]
|
||||
readonly model?: {
|
||||
readonly id?: string | null
|
||||
@@ -2828,6 +2857,11 @@ export type SessionCreateInput = {
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue } | null
|
||||
readonly permissions?: ReadonlyArray<{
|
||||
readonly action: string
|
||||
readonly resource: string
|
||||
readonly effect: "allow" | "deny" | "ask"
|
||||
}> | null
|
||||
}["model"]
|
||||
readonly location?: {
|
||||
readonly id?: string | null
|
||||
@@ -2836,6 +2870,11 @@ export type SessionCreateInput = {
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue } | null
|
||||
readonly permissions?: ReadonlyArray<{
|
||||
readonly action: string
|
||||
readonly resource: string
|
||||
readonly effect: "allow" | "deny" | "ask"
|
||||
}> | null
|
||||
}["location"]
|
||||
readonly metadata?: {
|
||||
readonly id?: string | null
|
||||
@@ -2844,7 +2883,25 @@ export type SessionCreateInput = {
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue } | null
|
||||
readonly permissions?: ReadonlyArray<{
|
||||
readonly action: string
|
||||
readonly resource: string
|
||||
readonly effect: "allow" | "deny" | "ask"
|
||||
}> | null
|
||||
}["metadata"]
|
||||
readonly permissions?: {
|
||||
readonly id?: string | null
|
||||
readonly title?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue } | null
|
||||
readonly permissions?: ReadonlyArray<{
|
||||
readonly action: string
|
||||
readonly resource: string
|
||||
readonly effect: "allow" | "deny" | "ask"
|
||||
}> | null
|
||||
}["permissions"]
|
||||
}
|
||||
|
||||
export type SessionCreateOutput = { data: SessionInfo }["data"]
|
||||
@@ -2882,6 +2939,11 @@ export type SessionImportInput = {
|
||||
readonly location: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly subpath?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly permissions?: ReadonlyArray<{
|
||||
readonly action: string
|
||||
readonly resource: string
|
||||
readonly effect: "allow" | "deny" | "ask"
|
||||
}>
|
||||
readonly revert?: {
|
||||
readonly messageID: string
|
||||
readonly partID?: string
|
||||
@@ -3187,6 +3249,11 @@ export type SessionImportInput = {
|
||||
readonly location: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly subpath?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly permissions?: ReadonlyArray<{
|
||||
readonly action: string
|
||||
readonly resource: string
|
||||
readonly effect: "allow" | "deny" | "ask"
|
||||
}>
|
||||
readonly revert?: {
|
||||
readonly messageID: string
|
||||
readonly partID?: string
|
||||
@@ -3492,6 +3559,11 @@ export type SessionImportInput = {
|
||||
readonly location: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly subpath?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly permissions?: ReadonlyArray<{
|
||||
readonly action: string
|
||||
readonly resource: string
|
||||
readonly effect: "allow" | "deny" | "ask"
|
||||
}>
|
||||
readonly revert?: {
|
||||
readonly messageID: string
|
||||
readonly partID?: string
|
||||
@@ -5753,6 +5825,19 @@ export type PermissionReplyInput = {
|
||||
|
||||
export type PermissionReplyOutput = void
|
||||
|
||||
export type PermissionRulesInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly permissions: {
|
||||
readonly permissions: ReadonlyArray<{
|
||||
readonly action: string
|
||||
readonly resource: string
|
||||
readonly effect: "allow" | "deny" | "ask"
|
||||
}>
|
||||
}["permissions"]
|
||||
}
|
||||
|
||||
export type PermissionRulesOutput = void
|
||||
|
||||
export type FileReadInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
|
||||
@@ -695,6 +695,10 @@ export function createData(config: CreateDataInput) {
|
||||
})
|
||||
return
|
||||
}
|
||||
case "session.permissions.updated":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "permissions", event.data.permissions)
|
||||
return
|
||||
case "session.moved": {
|
||||
const current = store.session.info[event.data.sessionID]
|
||||
if (current) {
|
||||
|
||||
@@ -5,9 +5,8 @@ import { coerceToString } from "./value.js"
|
||||
// WebIDL DOMString conversion: a missing argument is a TypeError, anything else stringifies.
|
||||
const base64 = (name: "atob" | "btoa") =>
|
||||
sync(name, (args, node) => {
|
||||
if (args.length === 0) {
|
||||
throw new InterpreterRuntimeError(`${name} requires 1 argument, but only 0 were provided.`, node).as("TypeError")
|
||||
}
|
||||
if (args.length === 0)
|
||||
throw new InterpreterRuntimeError(`${name} requires 1 argument (a string)`, node).as("TypeError")
|
||||
const input = coerceToString(args[0])
|
||||
try {
|
||||
return name === "atob" ? atob(input) : btoa(input)
|
||||
|
||||
+155
-341
@@ -1,248 +1,187 @@
|
||||
export * as AISDKNative from "./aisdk-native.js"
|
||||
|
||||
import { isRecord } from "@opencode/ai/utils/record"
|
||||
import { Effect, Option, Schema, Struct } from "effect"
|
||||
import { Provider } from "./provider.js"
|
||||
|
||||
export interface Mapping {
|
||||
readonly package: string
|
||||
readonly settings: Readonly<Record<string, unknown>>
|
||||
readonly settings: Provider.Settings
|
||||
readonly headers?: Readonly<Record<string, string>>
|
||||
readonly body?: Readonly<Record<string, unknown>>
|
||||
}
|
||||
|
||||
export interface MapInput {
|
||||
readonly packageName: string | undefined
|
||||
readonly settings: Readonly<Record<string, unknown>>
|
||||
readonly settings: Provider.Settings
|
||||
readonly modelID: string
|
||||
readonly providerID: string
|
||||
}
|
||||
|
||||
// A wrongly typed legacy value is dropped rather than failing the whole decode.
|
||||
const lenient = <S extends Schema.Top>(schema: S) =>
|
||||
Schema.optional(Schema.UndefinedOr(schema).pipe(Schema.catchDecoding(() => Effect.succeed(Option.some(undefined)))))
|
||||
|
||||
const Credentials = Schema.Struct({
|
||||
accessKeyId: Schema.String,
|
||||
secretAccessKey: Schema.String,
|
||||
sessionToken: lenient(Schema.String),
|
||||
region: lenient(Schema.String),
|
||||
})
|
||||
|
||||
/** AI SDK settings whose spelling differs from the native package. Everything else passes through. */
|
||||
const Legacy = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
apiKey: lenient(Schema.String),
|
||||
baseURL: lenient(Schema.String),
|
||||
headers: lenient(Schema.Record(Schema.String, Schema.String)),
|
||||
extraBody: lenient(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
useCompletionUrls: lenient(Schema.Boolean),
|
||||
// Bedrock
|
||||
auth: lenient(Schema.Literals(["bearer", "sigv4"])),
|
||||
bearerToken: lenient(Schema.String),
|
||||
endpoint: lenient(Schema.String),
|
||||
region: lenient(Schema.String),
|
||||
credentials: lenient(Credentials),
|
||||
accessKeyId: lenient(Schema.String),
|
||||
secretAccessKey: lenient(Schema.String),
|
||||
sessionToken: lenient(Schema.String),
|
||||
anthropicBeta: lenient(Schema.Array(Schema.String)),
|
||||
serviceTier: lenient(Schema.String),
|
||||
reasoningConfig: lenient(
|
||||
Schema.Struct({
|
||||
type: lenient(Schema.String),
|
||||
display: lenient(Schema.String),
|
||||
maxReasoningEffort: lenient(Schema.String),
|
||||
budgetTokens: lenient(Schema.Number),
|
||||
}),
|
||||
),
|
||||
additionalModelRequestFields: lenient(
|
||||
Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
anthropic_beta: lenient(Schema.Array(Schema.String)),
|
||||
output_config: lenient(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
reasoning: lenient(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
),
|
||||
),
|
||||
// OpenRouter
|
||||
appName: lenient(Schema.String),
|
||||
appUrl: lenient(Schema.String),
|
||||
api_keys: lenient(Schema.Record(Schema.String, Schema.String)),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
)
|
||||
type Legacy = typeof Legacy.Type
|
||||
const decode = Schema.decodeUnknownSync(Legacy)
|
||||
|
||||
/** Maps a legacy AI SDK package onto the native package that replaces it. */
|
||||
export function map(input: MapInput): Mapping | undefined {
|
||||
const baseSettings = mapBaseSettings(input.settings)
|
||||
switch (input.packageName) {
|
||||
const settings = decode(input.settings)
|
||||
const native = mapPackage(input.packageName, input.modelID, settings)
|
||||
if (!native) return
|
||||
const converse = native === "@opencode/ai/providers/amazon-bedrock"
|
||||
const mapped = {
|
||||
...Struct.omit(settings, ["headers", "extraBody", ...OPENROUTER_KEYS]),
|
||||
...(native === "@opencode/ai/providers/openai-compatible" ? { provider: input.providerID } : {}),
|
||||
}
|
||||
return {
|
||||
package: native,
|
||||
settings: native.startsWith("@opencode/ai/providers/amazon-bedrock") ? bedrockSettings(mapped, converse) : mapped,
|
||||
...(settings.headers === undefined ? {} : { headers: settings.headers }),
|
||||
...(settings.extraBody === undefined ? {} : { body: settings.extraBody }),
|
||||
...(converse ? bedrockRequest(input.modelID, settings) : {}),
|
||||
...(native === "@opencode/ai/providers/openrouter" ? openRouterRequest(settings) : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function mapPackage(packageName: string | undefined, modelID: string, settings: Legacy) {
|
||||
switch (packageName) {
|
||||
case "@ai-sdk/anthropic":
|
||||
return {
|
||||
package: "@opencode/ai/providers/anthropic",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(input.settings),
|
||||
...(typeof input.settings.authToken === "string" ? { authToken: input.settings.authToken } : {}),
|
||||
...mapProviderOptions(input.settings, ["apiKey", "authToken", "baseURL"]),
|
||||
},
|
||||
}
|
||||
case "@ai-sdk/amazon-bedrock":
|
||||
return {
|
||||
package: "@opencode/ai/providers/amazon-bedrock",
|
||||
settings: mapBedrockSettings(input.settings, baseSettings),
|
||||
...mapBedrockRequest(input),
|
||||
}
|
||||
case "@ai-sdk/amazon-bedrock/mantle":
|
||||
return mapBedrockMantle(input, baseSettings)
|
||||
case "@ai-sdk/azure":
|
||||
return {
|
||||
package: `@opencode/ai/providers/azure/${input.settings.useCompletionUrls === true ? "chat" : "responses"}`,
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(input.settings),
|
||||
...(typeof input.settings.resourceName === "string" ? { resourceName: input.settings.resourceName } : {}),
|
||||
...(typeof input.settings.apiVersion === "string" ? { apiVersion: input.settings.apiVersion } : {}),
|
||||
...(isStringRecord(input.settings.queryParams) ? { queryParams: input.settings.queryParams } : {}),
|
||||
...(typeof input.settings.useDeploymentBasedUrls === "boolean"
|
||||
? { useDeploymentBasedUrls: input.settings.useDeploymentBasedUrls }
|
||||
: {}),
|
||||
...mapOpenAIOptions(input.settings),
|
||||
},
|
||||
}
|
||||
case "@ai-sdk/cerebras":
|
||||
case "@ai-sdk/deepinfra":
|
||||
case "@ai-sdk/groq":
|
||||
case "@ai-sdk/togetherai":
|
||||
return {
|
||||
package: `@opencode/ai/providers/${input.packageName.slice("@ai-sdk/".length)}`,
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(input.settings),
|
||||
...mapProviderOptions(input.settings, ["apiKey", "baseURL", "fetch", "headers", "name"]),
|
||||
},
|
||||
...(isStringRecord(input.settings.headers) ? { headers: input.settings.headers } : {}),
|
||||
}
|
||||
case "@ai-sdk/google":
|
||||
return {
|
||||
package: "@opencode/ai/providers/google",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(input.settings),
|
||||
...mapGoogleOptions(input.settings),
|
||||
},
|
||||
}
|
||||
case "@ai-sdk/google-vertex":
|
||||
return {
|
||||
package: "@opencode/ai/providers/google-vertex",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...(typeof input.settings.accessToken === "string" ? { accessToken: input.settings.accessToken } : {}),
|
||||
...mapAPIKey(input.settings),
|
||||
...(typeof input.settings.location === "string" ? { location: input.settings.location } : {}),
|
||||
...(typeof input.settings.project === "string" ? { project: input.settings.project } : {}),
|
||||
...mapGoogleOptions(input.settings),
|
||||
},
|
||||
...(isStringRecord(input.settings.headers) ? { headers: input.settings.headers } : {}),
|
||||
}
|
||||
case "@ai-sdk/google-vertex/anthropic":
|
||||
return {
|
||||
package: "@opencode/ai/providers/google-vertex/messages",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...(typeof input.settings.accessToken === "string" ? { accessToken: input.settings.accessToken } : {}),
|
||||
...(typeof input.settings.location === "string" ? { location: input.settings.location } : {}),
|
||||
...(typeof input.settings.project === "string" ? { project: input.settings.project } : {}),
|
||||
...(isRecord(input.settings.thinking) || typeof input.settings.effort === "string"
|
||||
? {
|
||||
providerOptions: {
|
||||
...(isRecord(input.settings.thinking) ? { thinking: input.settings.thinking } : {}),
|
||||
...(typeof input.settings.effort === "string" ? { effort: input.settings.effort } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
...(isStringRecord(input.settings.headers) ? { headers: input.settings.headers } : {}),
|
||||
}
|
||||
case "@ai-sdk/groq":
|
||||
case "@ai-sdk/mistral":
|
||||
return {
|
||||
package: "@opencode/ai/providers/mistral",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(input.settings),
|
||||
...mapMistralOptions(input.settings),
|
||||
},
|
||||
...(isStringRecord(input.settings.headers) ? { headers: input.settings.headers } : {}),
|
||||
...(isRecord(input.settings.extraBody) ? { body: input.settings.extraBody } : {}),
|
||||
}
|
||||
case "@ai-sdk/openai":
|
||||
return {
|
||||
package: "@opencode/ai/providers/openai",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(input.settings),
|
||||
...(typeof input.settings.organization === "string" ? { organization: input.settings.organization } : {}),
|
||||
...(typeof input.settings.project === "string" ? { project: input.settings.project } : {}),
|
||||
...(isStringRecord(input.settings.queryParams) ? { queryParams: input.settings.queryParams } : {}),
|
||||
...mapProviderOptions(input.settings, ["apiKey", "baseURL", "organization", "project", "queryParams"]),
|
||||
},
|
||||
}
|
||||
case "@ai-sdk/openai-compatible":
|
||||
if (typeof input.settings.baseURL !== "string") return
|
||||
return {
|
||||
package: "@opencode/ai/providers/openai-compatible",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(input.settings),
|
||||
provider: input.providerID,
|
||||
...mapProviderOptions(input.settings, ["apiKey", "baseURL"]),
|
||||
},
|
||||
}
|
||||
case "@openrouter/ai-sdk-provider":
|
||||
return mapOpenRouter(input.settings, baseSettings)
|
||||
case "@ai-sdk/togetherai":
|
||||
case "@ai-sdk/xai":
|
||||
return {
|
||||
package: "@opencode/ai/providers/xai",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(input.settings),
|
||||
...mapXAIOptions(input.settings),
|
||||
},
|
||||
}
|
||||
case "@ai-sdk/amazon-bedrock":
|
||||
return `@opencode/ai/providers/${packageName.slice("@ai-sdk/".length)}`
|
||||
case "@ai-sdk/amazon-bedrock/mantle":
|
||||
return `@opencode/ai/providers/amazon-bedrock/mantle/${modelID.includes("gpt-oss") ? "chat" : "responses"}`
|
||||
case "@ai-sdk/azure":
|
||||
return `@opencode/ai/providers/azure/${settings.useCompletionUrls === true ? "chat" : "responses"}`
|
||||
case "@ai-sdk/google-vertex/anthropic":
|
||||
return "@opencode/ai/providers/google-vertex/messages"
|
||||
case "@ai-sdk/openai-compatible":
|
||||
return settings.baseURL === undefined ? undefined : "@opencode/ai/providers/openai-compatible"
|
||||
case "@openrouter/ai-sdk-provider":
|
||||
return "@opencode/ai/providers/openrouter"
|
||||
}
|
||||
}
|
||||
|
||||
function mapProviderOptions(settings: Readonly<Record<string, unknown>>, excluded: ReadonlyArray<string>) {
|
||||
const options = Object.fromEntries(Object.entries(settings).filter(([name]) => !excluded.includes(name)))
|
||||
if (Object.keys(options).length === 0) return {}
|
||||
return { providerOptions: options }
|
||||
}
|
||||
// AI SDK spellings the native Bedrock packages do not read.
|
||||
const BEDROCK_KEYS = [
|
||||
"bearerToken",
|
||||
"endpoint",
|
||||
"credentials",
|
||||
"credentialProvider",
|
||||
"accessKeyId",
|
||||
"secretAccessKey",
|
||||
"sessionToken",
|
||||
]
|
||||
// Request settings Converse takes in the body; translated by `bedrockRequest`.
|
||||
const CONVERSE_KEYS = ["additionalModelRequestFields", "reasoningConfig", "anthropicBeta", "serviceTier"]
|
||||
|
||||
function mapBedrockMantle(input: MapInput, baseSettings: Readonly<Record<string, unknown>>): Mapping | undefined {
|
||||
const settings = input.settings
|
||||
const chat = input.modelID.includes("gpt-oss")
|
||||
function bedrockSettings(settings: Legacy, converse: boolean) {
|
||||
const region = settings.region ?? settings.credentials?.region
|
||||
const credentials = settings.credentials ?? settings
|
||||
const baseURL = settings.baseURL ?? settings.endpoint
|
||||
return {
|
||||
package: `@opencode/ai/providers/amazon-bedrock/mantle/${chat ? "chat" : "responses"}`,
|
||||
settings: {
|
||||
...mapBedrockSettings(settings, baseSettings),
|
||||
...mapOpenAIOptions(settings),
|
||||
},
|
||||
...(isStringRecord(settings.headers) ? { headers: settings.headers } : {}),
|
||||
...Struct.omit(settings, converse ? [...BEDROCK_KEYS, ...CONVERSE_KEYS] : BEDROCK_KEYS),
|
||||
...(baseURL === undefined
|
||||
? {}
|
||||
: { baseURL: region === undefined ? baseURL : baseURL.replaceAll("${AWS_REGION}", region) }),
|
||||
...(settings.apiKey === undefined && settings.bearerToken !== undefined ? { apiKey: settings.bearerToken } : {}),
|
||||
...(region === undefined || credentials.accessKeyId === undefined || credentials.secretAccessKey === undefined
|
||||
? {}
|
||||
: {
|
||||
credentials: {
|
||||
region,
|
||||
accessKeyId: credentials.accessKeyId,
|
||||
secretAccessKey: credentials.secretAccessKey,
|
||||
...(credentials.sessionToken === undefined ? {} : { sessionToken: credentials.sessionToken }),
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function mapBedrockSettings(
|
||||
settings: Readonly<Record<string, unknown>>,
|
||||
baseSettings: Readonly<Record<string, unknown>>,
|
||||
) {
|
||||
const apiKey =
|
||||
typeof settings.apiKey === "string"
|
||||
? settings.apiKey
|
||||
: typeof settings.bearerToken === "string"
|
||||
? settings.bearerToken
|
||||
: undefined
|
||||
const region = bedrockRegion(settings)
|
||||
const credentials = mapBedrockCredentials(settings, region)
|
||||
return {
|
||||
...baseSettings,
|
||||
...(typeof baseSettings.baseURL === "string" && region !== undefined
|
||||
? { baseURL: baseSettings.baseURL.replaceAll("${AWS_REGION}", region) }
|
||||
: {}),
|
||||
...(typeof settings.baseURL !== "string" && typeof settings.endpoint === "string"
|
||||
? { baseURL: settings.endpoint }
|
||||
: {}),
|
||||
...(apiKey === undefined ? {} : { apiKey }),
|
||||
...(settings.auth === "bearer" || settings.auth === "sigv4" ? { auth: settings.auth } : {}),
|
||||
...(credentials === undefined ? {} : { credentials }),
|
||||
...(typeof settings.profile === "string" ? { profile: settings.profile } : {}),
|
||||
...(typeof settings.region === "string" ? { region: settings.region } : {}),
|
||||
...(typeof settings.topP === "number" ? { topP: settings.topP } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function mapBedrockRequest(input: MapInput): Pick<Mapping, "headers" | "body"> {
|
||||
const settings = input.settings
|
||||
const headers = isStringRecord(settings.headers) ? settings.headers : undefined
|
||||
const additional = isRecord(settings.additionalModelRequestFields) ? settings.additionalModelRequestFields : {}
|
||||
const reasoning = isRecord(settings.reasoningConfig) ? settings.reasoningConfig : undefined
|
||||
const anthropic = input.modelID.includes("anthropic")
|
||||
const openai = input.modelID.includes("openai.")
|
||||
// Converse passes OpenAI fields through verbatim. gpt-oss (Harmony) takes the
|
||||
// flat chat-completions `reasoning_effort`; GPT-5.6+ reject it and take the
|
||||
// Responses-style `reasoning.effort` instead.
|
||||
const harmony = input.modelID.includes("openai.gpt-oss")
|
||||
const effort = typeof reasoning?.maxReasoningEffort === "string" ? reasoning.maxReasoningEffort : undefined
|
||||
const type = typeof reasoning?.type === "string" ? reasoning.type : undefined
|
||||
const budget = typeof reasoning?.budgetTokens === "number" ? reasoning.budgetTokens : undefined
|
||||
const display = typeof reasoning?.display === "string" ? reasoning.display : undefined
|
||||
const betas = Array.isArray(settings.anthropicBeta)
|
||||
? settings.anthropicBeta.filter((item): item is string => typeof item === "string")
|
||||
: []
|
||||
const existingBetas = Array.isArray(additional.anthropic_beta)
|
||||
? additional.anthropic_beta.filter((item): item is string => typeof item === "string")
|
||||
: []
|
||||
function bedrockRequest(modelID: string, settings: Legacy): Pick<Mapping, "body"> {
|
||||
const additional = settings.additionalModelRequestFields ?? {}
|
||||
const reasoning = settings.reasoningConfig
|
||||
const anthropic = modelID.includes("anthropic")
|
||||
const openai = modelID.includes("openai.")
|
||||
// gpt-oss (Harmony) takes the flat chat-completions `reasoning_effort`; GPT-5.6+ take Responses-style `reasoning.effort`.
|
||||
const harmony = modelID.includes("openai.gpt-oss")
|
||||
const effort = reasoning?.maxReasoningEffort
|
||||
const type = reasoning?.type
|
||||
const budget = reasoning?.budgetTokens
|
||||
const display = reasoning?.display
|
||||
const betas = settings.anthropicBeta ?? []
|
||||
const fields = Provider.mergeOverlay(additional, {
|
||||
...(betas.length > 0 ? { anthropic_beta: [...existingBetas, ...betas] } : {}),
|
||||
...(betas.length > 0 ? { anthropic_beta: [...(additional.anthropic_beta ?? []), ...betas] } : {}),
|
||||
...(anthropic && type === "enabled" && budget !== undefined
|
||||
? { thinking: { type: "enabled", budget_tokens: budget } }
|
||||
: {}),
|
||||
...(anthropic && type === "adaptive"
|
||||
? { thinking: { type: "adaptive", ...(display === undefined ? {} : { display }) } }
|
||||
: {}),
|
||||
...(anthropic && effort !== undefined
|
||||
? {
|
||||
output_config: {
|
||||
...(isRecord(additional.output_config) ? additional.output_config : {}),
|
||||
effort,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(anthropic && effort !== undefined ? { output_config: { ...additional.output_config, effort } } : {}),
|
||||
...(!anthropic && openai && harmony && effort !== undefined ? { reasoning_effort: effort } : {}),
|
||||
...(!anthropic && openai && !harmony && effort !== undefined
|
||||
? { reasoning: { ...(isRecord(additional.reasoning) ? additional.reasoning : {}), effort } }
|
||||
? { reasoning: { ...additional.reasoning, effort } }
|
||||
: {}),
|
||||
...(!anthropic && !openai && effort !== undefined
|
||||
? {
|
||||
@@ -256,151 +195,26 @@ function mapBedrockRequest(input: MapInput): Pick<Mapping, "headers" | "body"> {
|
||||
})
|
||||
const body = {
|
||||
...(fields && Object.keys(fields).length > 0 ? { additionalModelRequestFields: fields } : {}),
|
||||
...(typeof settings.serviceTier === "string" ? { serviceTier: { type: settings.serviceTier } } : {}),
|
||||
}
|
||||
return {
|
||||
...(headers === undefined ? {} : { headers }),
|
||||
...(Object.keys(body).length === 0 ? {} : { body }),
|
||||
...(settings.serviceTier === undefined ? {} : { serviceTier: { type: settings.serviceTier } }),
|
||||
}
|
||||
return Object.keys(body).length === 0 ? {} : { body }
|
||||
}
|
||||
|
||||
function mapBedrockCredentials(settings: Readonly<Record<string, unknown>>, region: string | undefined) {
|
||||
const credentials = isRecord(settings.credentials) ? settings.credentials : settings
|
||||
if (
|
||||
region === undefined ||
|
||||
typeof credentials.accessKeyId !== "string" ||
|
||||
typeof credentials.secretAccessKey !== "string"
|
||||
)
|
||||
return undefined
|
||||
return {
|
||||
region,
|
||||
accessKeyId: credentials.accessKeyId,
|
||||
secretAccessKey: credentials.secretAccessKey,
|
||||
...(typeof credentials.sessionToken === "string" ? { sessionToken: credentials.sessionToken } : {}),
|
||||
}
|
||||
}
|
||||
// Constructor options the native OpenRouter package takes as headers, plus `compatibility`, which the
|
||||
// native package would otherwise forward to the request body.
|
||||
const OPENROUTER_KEYS = ["appName", "appUrl", "api_keys", "compatibility"] as const
|
||||
|
||||
function bedrockRegion(settings: Readonly<Record<string, unknown>>) {
|
||||
const credentials = isRecord(settings.credentials) ? settings.credentials : settings
|
||||
return typeof settings.region === "string"
|
||||
? settings.region
|
||||
: typeof credentials.region === "string"
|
||||
? credentials.region
|
||||
: undefined
|
||||
}
|
||||
|
||||
function mapOpenAIOptions(settings: Readonly<Record<string, unknown>>) {
|
||||
const options = {
|
||||
...(typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : {}),
|
||||
...(typeof settings.reasoningSummary === "string" ? { reasoningSummary: settings.reasoningSummary } : {}),
|
||||
...(Array.isArray(settings.include) ? { include: settings.include } : {}),
|
||||
...(typeof settings.store === "boolean" ? { store: settings.store } : {}),
|
||||
...(typeof settings.promptCacheKey === "string" ? { promptCacheKey: settings.promptCacheKey } : {}),
|
||||
...(typeof settings.textVerbosity === "string" ? { textVerbosity: settings.textVerbosity } : {}),
|
||||
...(typeof settings.serviceTier === "string" ? { serviceTier: settings.serviceTier } : {}),
|
||||
}
|
||||
if (Object.keys(options).length === 0) return {}
|
||||
return { providerOptions: options }
|
||||
}
|
||||
|
||||
function mapMistralOptions(settings: Readonly<Record<string, unknown>>) {
|
||||
const options = {
|
||||
...(typeof settings.safePrompt === "boolean" ? { safePrompt: settings.safePrompt } : {}),
|
||||
...(typeof settings.documentImageLimit === "number" ? { documentImageLimit: settings.documentImageLimit } : {}),
|
||||
...(typeof settings.documentPageLimit === "number" ? { documentPageLimit: settings.documentPageLimit } : {}),
|
||||
...(typeof settings.parallelToolCalls === "boolean" ? { parallelToolCalls: settings.parallelToolCalls } : {}),
|
||||
...(typeof settings.promptCacheKey === "string" ? { promptCacheKey: settings.promptCacheKey } : {}),
|
||||
...(typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : {}),
|
||||
...(settings.promptMode === "reasoning" ? { promptMode: settings.promptMode } : {}),
|
||||
}
|
||||
if (Object.keys(options).length === 0) return {}
|
||||
return { providerOptions: options }
|
||||
}
|
||||
|
||||
function mapBaseSettings(settings: Readonly<Record<string, unknown>>) {
|
||||
return {
|
||||
...(typeof settings.baseURL === "string" ? { baseURL: settings.baseURL } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function mapAPIKey(settings: Readonly<Record<string, unknown>>) {
|
||||
return typeof settings.apiKey === "string" ? { apiKey: settings.apiKey } : {}
|
||||
}
|
||||
|
||||
function mapGoogleOptions(settings: Readonly<Record<string, unknown>>) {
|
||||
const input = settings.thinkingConfig
|
||||
const thinkingConfig = {
|
||||
...(isRecord(input) && typeof input.thinkingBudget === "number" ? { thinkingBudget: input.thinkingBudget } : {}),
|
||||
...(isRecord(input) && typeof input.includeThoughts === "boolean"
|
||||
? { includeThoughts: input.includeThoughts }
|
||||
: {}),
|
||||
...(isRecord(input) && typeof input.thinkingLevel === "string" ? { thinkingLevel: input.thinkingLevel } : {}),
|
||||
}
|
||||
const options = {
|
||||
...(typeof settings.cachedContent === "string" ? { cachedContent: settings.cachedContent } : {}),
|
||||
...(isStringRecord(settings.labels) ? { labels: settings.labels } : {}),
|
||||
...(Array.isArray(settings.safetySettings) ? { safetySettings: settings.safetySettings } : {}),
|
||||
...(typeof settings.serviceTier === "string" ? { serviceTier: settings.serviceTier } : {}),
|
||||
...(Object.keys(thinkingConfig).length > 0 ? { thinkingConfig } : {}),
|
||||
}
|
||||
if (Object.keys(options).length === 0) return {}
|
||||
return { providerOptions: options }
|
||||
}
|
||||
|
||||
function mapOpenRouter(
|
||||
settings: Readonly<Record<string, unknown>>,
|
||||
baseSettings: Readonly<Record<string, unknown>>,
|
||||
): Mapping {
|
||||
function openRouterRequest(settings: Legacy): Pick<Mapping, "headers"> {
|
||||
const headers =
|
||||
Provider.mergeHeaders(
|
||||
{
|
||||
...(typeof settings.appName === "string" ? { "X-OpenRouter-Title": settings.appName } : {}),
|
||||
...(typeof settings.appUrl === "string" ? { "HTTP-Referer": settings.appUrl } : {}),
|
||||
...(isStringRecord(settings.api_keys) && Object.keys(settings.api_keys).length > 0
|
||||
? { "X-Provider-API-Keys": JSON.stringify(settings.api_keys) }
|
||||
: {}),
|
||||
...(settings.appName === undefined ? {} : { "X-OpenRouter-Title": settings.appName }),
|
||||
...(settings.appUrl === undefined ? {} : { "HTTP-Referer": settings.appUrl }),
|
||||
...(settings.api_keys === undefined || Object.keys(settings.api_keys).length === 0
|
||||
? {}
|
||||
: { "X-Provider-API-Keys": JSON.stringify(settings.api_keys) }),
|
||||
},
|
||||
isStringRecord(settings.headers) ? settings.headers : undefined,
|
||||
settings.headers,
|
||||
) ?? {}
|
||||
return {
|
||||
package: "@opencode/ai/providers/openrouter",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(settings),
|
||||
...mapOpenRouterOptions(settings),
|
||||
},
|
||||
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
||||
...(isRecord(settings.extraBody) ? { body: settings.extraBody } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function mapOpenRouterOptions(settings: Readonly<Record<string, unknown>>) {
|
||||
return mapProviderOptions(settings, [
|
||||
"apiKey",
|
||||
"api_keys",
|
||||
"appName",
|
||||
"appUrl",
|
||||
"authToken",
|
||||
"baseURL",
|
||||
"chunkTimeout",
|
||||
"compatibility",
|
||||
"extraBody",
|
||||
"fetch",
|
||||
"headers",
|
||||
"promptCacheKey",
|
||||
"timeout",
|
||||
])
|
||||
}
|
||||
|
||||
function isStringRecord(value: unknown): value is Readonly<Record<string, string>> {
|
||||
return isRecord(value) && Object.values(value).every((item) => typeof item === "string")
|
||||
}
|
||||
|
||||
function mapXAIOptions(settings: Readonly<Record<string, unknown>>) {
|
||||
const options = {
|
||||
...(typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : {}),
|
||||
...(typeof settings.store === "boolean" ? { store: settings.store } : {}),
|
||||
}
|
||||
if (Object.keys(options).length === 0) return {}
|
||||
return { providerOptions: options }
|
||||
return Object.keys(headers).length === 0 ? {} : { headers }
|
||||
}
|
||||
|
||||
+2
@@ -45,6 +45,7 @@ import m42 from "./migration/20260812181746_session_inbox.js"
|
||||
import m43 from "./migration/20260812213948_worktree.js"
|
||||
import m44 from "./migration/20260819222447_session_viewed_state.js"
|
||||
import m45 from "./migration/20260823191254_nullable_workspace_binding.js"
|
||||
import m46 from "./migration/20260910120000_clear_v1_session_permission.js"
|
||||
|
||||
export const migrations = [
|
||||
m00,
|
||||
@@ -93,4 +94,5 @@ export const migrations = [
|
||||
m43,
|
||||
m44,
|
||||
m45,
|
||||
m46,
|
||||
] satisfies DatabaseMigration.Migration[]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration.js"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260910120000_clear_v1_session_permission",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`UPDATE \`session_v2\` SET \`permission\` = NULL;`)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
@@ -600,7 +600,7 @@ export function run(options: Options = {}): Effect.Effect<RunResult, never, Data
|
||||
id, ${projectID}, workspace_id, parent_id, slug, directory, path, title, version, share_url,
|
||||
summary_additions, summary_deletions, summary_files, summary_diffs, metadata, cost,
|
||||
tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
|
||||
revert, permission, agent, model, time_created, time_updated, time_compacting, time_archived
|
||||
revert, NULL, agent, model, time_created, time_updated, time_compacting, time_archived
|
||||
FROM session
|
||||
WHERE id = ${nextID.id}
|
||||
`)
|
||||
|
||||
@@ -183,7 +183,7 @@ const resolveCatalogModel = Effect.fn("ModelResolver.resolveCatalogModel")(funct
|
||||
if (!native) return yield* unsupported(resolved)
|
||||
|
||||
const specifier = native
|
||||
const mapped = yield* prepareProviderSettings(resolved, mapping?.settings ?? configured)
|
||||
const mapped = yield* prepareProviderSettings(resolved, Provider.nativeSettings(mapping?.settings ?? configured))
|
||||
const module = yield* (dependencies?.loadPackage ?? Provider.loadPackage)(specifier).pipe(
|
||||
Effect.mapError(() => unsupported(resolved)),
|
||||
)
|
||||
|
||||
@@ -154,7 +154,7 @@ const layer = Layer.effect(
|
||||
const session = yield* sessions.get(sessionID)
|
||||
if (!session) return yield* new SessionErrors.NotFoundError({ sessionID })
|
||||
const agent = yield* agents.resolve(agentID ?? session.agent)
|
||||
return agent?.permissions ?? missingAgentPermissions
|
||||
return merge(agent?.permissions ?? missingAgentPermissions, session.permissions ?? [])
|
||||
})
|
||||
|
||||
function denied(input: Pick<Request, "action" | "resources">, rules: Permission.Ruleset) {
|
||||
|
||||
@@ -404,6 +404,7 @@ export const make = Effect.fn("PluginHost.make")(function* (
|
||||
: Effect.fail(new Error(`Permission request not found: ${input.requestID}`)),
|
||||
),
|
||||
),
|
||||
rules: sessions.setPermissions,
|
||||
},
|
||||
plugin: {
|
||||
list: () => response(plugin.list()),
|
||||
@@ -509,6 +510,8 @@ export const make = Effect.fn("PluginHost.make")(function* (
|
||||
title: input?.title,
|
||||
agent: input?.agent,
|
||||
model: input?.model,
|
||||
metadata: input?.metadata,
|
||||
permissions: input?.permissions,
|
||||
location:
|
||||
input?.location ?? Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }),
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as Provider from "./provider.js"
|
||||
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Effect, Schema, Struct } from "effect"
|
||||
import { Provider } from "@opencode/schema/provider"
|
||||
import type { ProviderPackageDefinition } from "@opencode/ai"
|
||||
import { isRecord } from "@opencode/ai/utils/record"
|
||||
@@ -103,6 +103,22 @@ export const loadPackage = Effect.fn("Provider.loadPackage")(function* (input: s
|
||||
return yield* importPackage(specifier, entrypoint)
|
||||
})
|
||||
|
||||
// opencode-only; handled in aisdk.ts.
|
||||
const TRANSPORT_KEYS = ["chunkTimeout", "fetch", "timeout"] as const
|
||||
// Credentials and request overlays that must not be duplicated into providerOptions.
|
||||
const PACKAGE_KEYS = ["accessToken", "apiKey", "authToken", "baseURL", "body", "headers"] as const
|
||||
|
||||
/**
|
||||
* opencode settings are flat, but `@opencode/ai` packages still read request options from a nested
|
||||
* `providerOptions`. Until that is flattened, hand the same settings to both places and let each side
|
||||
* pick the keys it knows.
|
||||
*/
|
||||
export function nativeSettings(settings: Settings): Settings {
|
||||
const flat = Struct.omit({ ...settings.providerOptions, ...settings }, ["providerOptions", ...TRANSPORT_KEYS])
|
||||
const providerOptions = Struct.omit(flat, PACKAGE_KEYS)
|
||||
return { ...flat, ...(Object.keys(providerOptions).length === 0 ? {} : { providerOptions }) }
|
||||
}
|
||||
|
||||
export function mergeOverlay(
|
||||
base: Readonly<Record<string, unknown>> | undefined,
|
||||
overlay: Readonly<Record<string, unknown>> | undefined,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { SessionMessageTable } from "./session/sql.js"
|
||||
import { SessionSchema } from "./session/schema.js"
|
||||
import { RelativePath } from "./schema.js"
|
||||
import { Agent } from "@opencode/schema/agent"
|
||||
import type { Permission } from "@opencode/schema/permission"
|
||||
import { App } from "./app.js"
|
||||
import { Slug } from "./util/slug.js"
|
||||
import path from "path"
|
||||
@@ -81,6 +82,7 @@ type CreateBaseInput = {
|
||||
agent?: Agent.ID
|
||||
model?: Model.Ref
|
||||
metadata?: SessionSchema.Metadata
|
||||
permissions?: Permission.Ruleset
|
||||
}
|
||||
type CreateInput = CreateBaseInput &
|
||||
({ location: Location.Ref; parentID?: never } | { parentID: SessionSchema.ID; location?: never })
|
||||
@@ -157,6 +159,10 @@ export interface Interface {
|
||||
readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: Agent.ID }) => Effect.Effect<void, NotFoundError>
|
||||
readonly switchModel: (input: { sessionID: SessionSchema.ID; model: Model.Ref }) => Effect.Effect<void, NotFoundError>
|
||||
readonly rename: (input: { sessionID: SessionSchema.ID; title: string }) => Effect.Effect<void, NotFoundError>
|
||||
readonly setPermissions: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
permissions: Permission.Ruleset
|
||||
}) => Effect.Effect<void, NotFoundError>
|
||||
readonly move: SessionMove.Interface["move"]
|
||||
readonly prompt: (
|
||||
input: Parameters<Session.Handle["prompt"]>[0] & { sessionID: SessionSchema.ID },
|
||||
@@ -248,9 +254,10 @@ const layer = Layer.effect(
|
||||
subpath: RelativePath.make(path.relative(project.directory, location.directory).replaceAll("\\", "/")),
|
||||
title: input.title,
|
||||
agent: input.agent,
|
||||
// Children inherit metadata the way they inherit location, so
|
||||
// host policies that read it treat the family uniformly.
|
||||
// Children inherit metadata and permissions the way they inherit
|
||||
// location, so host policies that read them treat the family uniformly.
|
||||
metadata: input.metadata ?? parent?.metadata,
|
||||
permissions: input.permissions ?? parent?.permissions,
|
||||
model: input.model
|
||||
? {
|
||||
id: Model.ID.make(input.model.id),
|
||||
@@ -387,6 +394,7 @@ const layer = Layer.effect(
|
||||
switchAgent: (input) => sessions.forSession(input.sessionID).switchAgent(input),
|
||||
switchModel: (input) => sessions.forSession(input.sessionID).switchModel(input),
|
||||
rename: (input) => sessions.forSession(input.sessionID).rename(input),
|
||||
setPermissions: (input) => sessions.forSession(input.sessionID).setPermissions(input),
|
||||
move: moves.move,
|
||||
compact: (input) => sessions.forSession(input.sessionID).compact(input),
|
||||
wait: (sessionID) => sessions.forSession(sessionID).wait(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * as SessionContext from "./context.js"
|
||||
|
||||
import { Model } from "@opencode/schema/model"
|
||||
import { Permission } from "../permission.js"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { Agent } from "../agent.js"
|
||||
import { Catalog } from "../catalog.js"
|
||||
@@ -129,7 +130,7 @@ const layer = Layer.effect(
|
||||
if (!agent.info) return yield* new AgentNotFoundError({ sessionID: session.id, agent: session.agent ?? agent.id })
|
||||
const loaded = yield* Effect.all(
|
||||
{
|
||||
tools: registry.snapshot(agent.info.permissions),
|
||||
tools: registry.snapshot(Permission.merge(agent.info.permissions, session.permissions ?? [])),
|
||||
builtins: builtins.load(sessionID),
|
||||
discovery: discovery.load(),
|
||||
skills: skillInstructions.load(agent),
|
||||
|
||||
@@ -50,6 +50,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In
|
||||
}),
|
||||
subpath: row.path ? RelativePath.make(row.path) : undefined,
|
||||
metadata: row.metadata ?? undefined,
|
||||
permissions: row.permission ?? undefined,
|
||||
revert: row.revert ? decodeRevert(row.revert) : undefined,
|
||||
outcome: row.idle_outcome ?? undefined,
|
||||
time: {
|
||||
|
||||
@@ -116,6 +116,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
)
|
||||
}),
|
||||
"session.renamed": () => Effect.void,
|
||||
"session.permissions.updated": () => Effect.void,
|
||||
"session.deleted": () => Effect.void,
|
||||
"session.forked": () => Effect.void,
|
||||
"session.inbox.delivered": () => Effect.void,
|
||||
|
||||
@@ -160,6 +160,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
||||
agent: parent.agent,
|
||||
model: parent.model,
|
||||
metadata: parent.metadata,
|
||||
permission: parent.permission,
|
||||
version: parent.version,
|
||||
cost: 0,
|
||||
tokens_input: 0,
|
||||
@@ -450,6 +451,7 @@ const layer = Layer.effectDiscard(
|
||||
agent: event.data.agent,
|
||||
model: event.data.model,
|
||||
metadata: event.data.metadata,
|
||||
permission: event.data.permissions,
|
||||
version: event.data.version,
|
||||
time_created: event.created,
|
||||
time_updated: event.created,
|
||||
@@ -571,6 +573,14 @@ const layer = Layer.effectDiscard(
|
||||
.run()
|
||||
.pipe(Effect.orDie),
|
||||
)
|
||||
yield* bus.project(SessionEvent.PermissionsUpdated, (event) =>
|
||||
db
|
||||
.update(SessionTable)
|
||||
.set({ permission: event.data.permissions, time_updated: event.created })
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie),
|
||||
)
|
||||
yield* bus.project(SessionEvent.Viewed, (event) => {
|
||||
const idle = event.data.idle
|
||||
return db
|
||||
|
||||
@@ -3,6 +3,7 @@ export * as Session from "./session.js"
|
||||
import { DateTime, Effect, Fiber, Scope } from "effect"
|
||||
import type { Agent } from "@opencode/schema/agent"
|
||||
import type { Model } from "@opencode/schema/model"
|
||||
import type { Permission } from "@opencode/schema/permission"
|
||||
import { Event } from "@opencode/schema/event"
|
||||
import { FSUtil } from "@opencode/util/fs-util"
|
||||
import { Bus } from "../bus.js"
|
||||
@@ -72,6 +73,13 @@ export const make = Effect.fn("Session.make")(function* () {
|
||||
yield* get(sessionID)
|
||||
yield* bus.publish(SessionEvent.Renamed, { sessionID, title: input.title })
|
||||
})
|
||||
const setPermissions = Effect.fn("Session.setPermissions")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
input: { permissions: Permission.Ruleset },
|
||||
) {
|
||||
yield* get(sessionID)
|
||||
yield* bus.publish(SessionEvent.PermissionsUpdated, { sessionID, permissions: input.permissions })
|
||||
})
|
||||
const switchAgent = Effect.fn("Session.switchAgent")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
input: { agent: Agent.ID },
|
||||
@@ -334,6 +342,7 @@ export const make = Effect.fn("Session.make")(function* () {
|
||||
message,
|
||||
view,
|
||||
rename,
|
||||
setPermissions,
|
||||
switchAgent,
|
||||
switchModel,
|
||||
inbox,
|
||||
@@ -356,6 +365,7 @@ export const make = Effect.fn("Session.make")(function* () {
|
||||
const message = operations.message.bind(undefined, sessionID)
|
||||
const view = operations.view.bind(undefined, sessionID)
|
||||
const rename = operations.rename.bind(undefined, sessionID)
|
||||
const setPermissions = operations.setPermissions.bind(undefined, sessionID)
|
||||
const switchAgent = operations.switchAgent.bind(undefined, sessionID)
|
||||
const switchModel = operations.switchModel.bind(undefined, sessionID)
|
||||
const inbox = operations.inbox.bind(undefined, sessionID)
|
||||
@@ -381,6 +391,7 @@ export const make = Effect.fn("Session.make")(function* () {
|
||||
message,
|
||||
view,
|
||||
rename,
|
||||
setPermissions,
|
||||
switchAgent,
|
||||
switchModel,
|
||||
inbox,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ProjectTable } from "../project/sql.js"
|
||||
import type { SessionMessage } from "./message.js"
|
||||
import type { SessionInbox } from "./inbox.js"
|
||||
import type { FileDiff } from "@opencode/schema/file-diff"
|
||||
import type { PermissionV1 } from "@opencode/schema/permission-v1"
|
||||
import type { Permission } from "@opencode/schema/permission"
|
||||
import type { Project } from "@opencode/schema/project"
|
||||
import type { SessionSchema } from "./schema.js"
|
||||
import type { Workspace } from "@opencode/schema/workspace"
|
||||
@@ -49,7 +49,7 @@ export const SessionTable = sqliteTable(
|
||||
tokens_cache_read: integer().notNull().default(0),
|
||||
tokens_cache_write: integer().notNull().default(0),
|
||||
revert: text({ mode: "json" }).$type<Session.Revert | RevertV1>(),
|
||||
permission: text({ mode: "json" }).$type<PermissionV1.Ruleset>(),
|
||||
permission: text({ mode: "json" }).$type<Permission.Ruleset>(),
|
||||
agent: text(),
|
||||
model: text({ mode: "json" }).$type<{
|
||||
id: string
|
||||
|
||||
@@ -103,6 +103,7 @@ const layer = Layer.effect(
|
||||
agent: input.data.info.agent,
|
||||
model: input.data.info.model,
|
||||
metadata: input.data.info.metadata,
|
||||
permissions: input.data.info.permissions,
|
||||
},
|
||||
{
|
||||
location: input.location,
|
||||
|
||||
@@ -99,8 +99,19 @@ export const layer = Layer.effect(
|
||||
},
|
||||
)
|
||||
const text = content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n")
|
||||
const output = () => {
|
||||
if (result.structured !== undefined) return result.structured
|
||||
if (text === "") return null
|
||||
// Agents assume JSON returned as text is already an object, so parse it when the server declares no schema.
|
||||
if (tool.outputSchema === undefined && (text.startsWith("{") || text.startsWith("["))) {
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {}
|
||||
}
|
||||
return text
|
||||
}
|
||||
return {
|
||||
output: result.structured ?? (text === "" ? null : text),
|
||||
output: output(),
|
||||
...(content.length === 0 ? {} : { content }),
|
||||
}
|
||||
}).pipe(
|
||||
|
||||
@@ -21,12 +21,10 @@ describe("AISDKNative", () => {
|
||||
settings: {
|
||||
apiKey: "secret",
|
||||
baseURL: "https://api.meta.ai/v1",
|
||||
providerOptions: {
|
||||
reasoningEffort: "xhigh",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
truncation: "auto",
|
||||
},
|
||||
reasoningEffort: "xhigh",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
truncation: "auto",
|
||||
organization: "org",
|
||||
},
|
||||
})
|
||||
@@ -35,7 +33,7 @@ describe("AISDKNative", () => {
|
||||
settings: {
|
||||
baseURL: "https://example.com/v1",
|
||||
provider: "test-provider",
|
||||
providerOptions: { reasoningEffort: "high" },
|
||||
reasoningEffort: "high",
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -53,10 +51,8 @@ describe("AISDKNative", () => {
|
||||
settings: {
|
||||
authToken: "token",
|
||||
baseURL: "https://anthropic.example/v1",
|
||||
providerOptions: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "high",
|
||||
},
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "high",
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -68,7 +64,6 @@ describe("AISDKNative", () => {
|
||||
apiKey: "secret",
|
||||
baseURL: `https://${name}.example/v1`,
|
||||
headers: { "x-provider": name },
|
||||
name: "custom-provider",
|
||||
reasoningEffort: "high",
|
||||
customOption: { enabled: true },
|
||||
}),
|
||||
@@ -77,7 +72,8 @@ describe("AISDKNative", () => {
|
||||
settings: {
|
||||
apiKey: "secret",
|
||||
baseURL: `https://${name}.example/v1`,
|
||||
providerOptions: { reasoningEffort: "high", customOption: { enabled: true } },
|
||||
reasoningEffort: "high",
|
||||
customOption: { enabled: true },
|
||||
},
|
||||
headers: { "x-provider": name },
|
||||
})
|
||||
@@ -101,10 +97,8 @@ describe("AISDKNative", () => {
|
||||
settings: {
|
||||
project: "project",
|
||||
location: "us-central1",
|
||||
providerOptions: {
|
||||
labels: { environment: "test" },
|
||||
thinkingConfig: { thinkingLevel: "high" },
|
||||
},
|
||||
labels: { environment: "test" },
|
||||
thinkingConfig: { thinkingLevel: "high" },
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -123,52 +117,25 @@ describe("AISDKNative", () => {
|
||||
promptCacheKey: "session-123",
|
||||
reasoningEffort: "high",
|
||||
promptMode: "reasoning",
|
||||
fetch: "ignored",
|
||||
generateId: "ignored",
|
||||
structuredOutputs: true,
|
||||
unsupported: true,
|
||||
}),
|
||||
).toEqual({
|
||||
package: "@opencode/ai/providers/mistral",
|
||||
settings: {
|
||||
apiKey: "secret",
|
||||
baseURL: "https://mistral.example/v1",
|
||||
providerOptions: {
|
||||
safePrompt: false,
|
||||
documentImageLimit: 4,
|
||||
documentPageLimit: 12,
|
||||
parallelToolCalls: false,
|
||||
promptCacheKey: "session-123",
|
||||
reasoningEffort: "high",
|
||||
promptMode: "reasoning",
|
||||
},
|
||||
safePrompt: false,
|
||||
documentImageLimit: 4,
|
||||
documentPageLimit: 12,
|
||||
parallelToolCalls: false,
|
||||
promptCacheKey: "session-123",
|
||||
reasoningEffort: "high",
|
||||
promptMode: "reasoning",
|
||||
},
|
||||
headers: { "x-provider": "mistral" },
|
||||
body: { custom: { enabled: true } },
|
||||
})
|
||||
})
|
||||
|
||||
test("omits invalid and runtime-only Mistral settings", () => {
|
||||
expect(
|
||||
map("@ai-sdk/mistral", {
|
||||
headers: { valid: "header", invalid: 1 },
|
||||
extraBody: "invalid",
|
||||
safePrompt: "false",
|
||||
documentImageLimit: "4",
|
||||
documentPageLimit: null,
|
||||
parallelToolCalls: 0,
|
||||
promptCacheKey: false,
|
||||
reasoningEffort: false,
|
||||
promptMode: "unsupported",
|
||||
fetch: "ignored",
|
||||
generateId: "ignored",
|
||||
}),
|
||||
).toEqual({
|
||||
package: "@opencode/ai/providers/mistral",
|
||||
settings: {},
|
||||
})
|
||||
})
|
||||
|
||||
test("maps both models.dev Bedrock packages to native providers", () => {
|
||||
expect(map("@ai-sdk/amazon-bedrock", { region: "us-east-1" })).toEqual({
|
||||
package: "@opencode/ai/providers/amazon-bedrock",
|
||||
@@ -197,7 +164,7 @@ describe("AISDKNative", () => {
|
||||
apiVersion: "2025-01-01-preview",
|
||||
queryParams: { feature: "enabled" },
|
||||
useDeploymentBasedUrls: true,
|
||||
providerOptions: { reasoningEffort: "high" },
|
||||
reasoningEffort: "high",
|
||||
},
|
||||
})
|
||||
expect(map("@ai-sdk/azure", { ...settings, useCompletionUrls: true }, "custom-deployment")?.package).toBe(
|
||||
@@ -259,9 +226,9 @@ describe("AISDKNative", () => {
|
||||
|
||||
// GPT-5.6+ reject `reasoning_effort` and take the Responses-style nested field.
|
||||
for (const modelID of ["global.openai.gpt-5.6-sol", "us.openai.gpt-5.6-sol", "us.openai.gpt-6-astra"]) {
|
||||
expect(
|
||||
map("@ai-sdk/amazon-bedrock", { reasoningConfig: { maxReasoningEffort: "none" } }, modelID)?.body,
|
||||
).toEqual({ additionalModelRequestFields: { reasoning: { effort: "none" } } })
|
||||
expect(map("@ai-sdk/amazon-bedrock", { reasoningConfig: { maxReasoningEffort: "none" } }, modelID)?.body).toEqual(
|
||||
{ additionalModelRequestFields: { reasoning: { effort: "none" } } },
|
||||
)
|
||||
}
|
||||
expect(
|
||||
map(
|
||||
@@ -292,11 +259,9 @@ describe("AISDKNative", () => {
|
||||
apiKey: "token",
|
||||
baseURL: "https://mantle.test/v1",
|
||||
region: "us-west-2",
|
||||
providerOptions: {
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
headers: { "x-test": "value" },
|
||||
})
|
||||
@@ -330,7 +295,6 @@ describe("AISDKNative", () => {
|
||||
},
|
||||
baseURL: "https://bedrock-mantle.${AWS_REGION}.api.aws/v1",
|
||||
credentialProvider: "ignored",
|
||||
fetch: "ignored",
|
||||
store: false,
|
||||
},
|
||||
"openai.gpt-oss-120b",
|
||||
@@ -345,7 +309,7 @@ describe("AISDKNative", () => {
|
||||
region: "eu-west-1",
|
||||
},
|
||||
baseURL: "https://bedrock-mantle.eu-west-1.api.aws/v1",
|
||||
providerOptions: { store: false },
|
||||
store: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -389,12 +353,10 @@ describe("AISDKNative", () => {
|
||||
).toEqual({
|
||||
package: "@opencode/ai/providers/openrouter",
|
||||
settings: {
|
||||
providerOptions: {
|
||||
models: ["anthropic/claude-sonnet-4.6"],
|
||||
provider: { only: ["anthropic"], require_parameters: true },
|
||||
reasoning: { effort: "high" },
|
||||
future_option: { enabled: true },
|
||||
},
|
||||
models: ["anthropic/claude-sonnet-4.6"],
|
||||
provider: { only: ["anthropic"], require_parameters: true },
|
||||
reasoning: { effort: "high" },
|
||||
future_option: { enabled: true },
|
||||
},
|
||||
headers: {
|
||||
"x-openrouter-title": "Configured",
|
||||
@@ -415,21 +377,18 @@ describe("AISDKNative", () => {
|
||||
thinkingBudget: 0,
|
||||
includeThoughts: false,
|
||||
thinkingLevel: "high",
|
||||
unknown: true,
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
package: "@opencode/ai/providers/google",
|
||||
settings: {
|
||||
providerOptions: {
|
||||
cachedContent: "cachedContents/example",
|
||||
safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
|
||||
serviceTier: "flex",
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 0,
|
||||
includeThoughts: false,
|
||||
thinkingLevel: "high",
|
||||
},
|
||||
cachedContent: "cachedContents/example",
|
||||
safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
|
||||
serviceTier: "flex",
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 0,
|
||||
includeThoughts: false,
|
||||
thinkingLevel: "high",
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -438,7 +397,7 @@ describe("AISDKNative", () => {
|
||||
test("maps Google thinking settings independently", () => {
|
||||
for (const thinkingConfig of [{ thinkingBudget: -1 }, { includeThoughts: true }, { thinkingLevel: "medium" }]) {
|
||||
expect(map("@ai-sdk/google", { thinkingConfig })).toMatchObject({
|
||||
settings: { providerOptions: { thinkingConfig } },
|
||||
settings: { thinkingConfig },
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -452,11 +411,9 @@ describe("AISDKNative", () => {
|
||||
}),
|
||||
).toMatchObject({
|
||||
settings: {
|
||||
providerOptions: {
|
||||
cachedContent: "cachedContents/example",
|
||||
safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
|
||||
serviceTier: "future-tier",
|
||||
},
|
||||
cachedContent: "cachedContents/example",
|
||||
safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
|
||||
serviceTier: "future-tier",
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -479,10 +436,8 @@ describe("AISDKNative", () => {
|
||||
baseURL: "https://vertex.example/v1",
|
||||
location: "eu",
|
||||
project: "vertex-project",
|
||||
providerOptions: {
|
||||
labels: { component: "opencode", environment: "test" },
|
||||
thinkingConfig: { thinkingLevel: "high" },
|
||||
},
|
||||
labels: { component: "opencode", environment: "test" },
|
||||
thinkingConfig: { thinkingLevel: "high" },
|
||||
},
|
||||
headers: { "x-test": "value" },
|
||||
})
|
||||
@@ -506,10 +461,8 @@ describe("AISDKNative", () => {
|
||||
baseURL: "https://vertex.example/v1",
|
||||
location: "eu",
|
||||
project: "vertex-project",
|
||||
providerOptions: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "high",
|
||||
},
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "high",
|
||||
},
|
||||
headers: { "x-test": "value" },
|
||||
})
|
||||
@@ -528,28 +481,9 @@ describe("AISDKNative", () => {
|
||||
settings: {
|
||||
apiKey: "secret",
|
||||
baseURL: "https://xai.example/v1",
|
||||
providerOptions: {
|
||||
reasoningEffort: "custom",
|
||||
store: true,
|
||||
},
|
||||
reasoningEffort: "custom",
|
||||
store: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("omits invalid and unsupported xAI settings", () => {
|
||||
expect(
|
||||
map("@ai-sdk/xai", {
|
||||
reasoningEffort: 10,
|
||||
store: "yes",
|
||||
include: ["unknown"],
|
||||
logprobs: true,
|
||||
topLogprobs: 8,
|
||||
previousResponseId: "response-id",
|
||||
searchParameters: { mode: "auto" },
|
||||
}),
|
||||
).toEqual({
|
||||
package: "@opencode/ai/providers/xai",
|
||||
settings: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -324,6 +324,32 @@ const mcp = Layer.mock(Mcp.Service, {
|
||||
description: "Status",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
}),
|
||||
new Mcp.Tool({
|
||||
server: Mcp.ServerName.make("demo"),
|
||||
name: "issues",
|
||||
description: "Returns JSON as text",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
}),
|
||||
new Mcp.Tool({
|
||||
server: Mcp.ServerName.make("demo"),
|
||||
name: "count",
|
||||
description: "Returns a number as text",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
}),
|
||||
new Mcp.Tool({
|
||||
server: Mcp.ServerName.make("demo"),
|
||||
name: "typed",
|
||||
description: "Declares a string output and returns JSON as text",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
outputSchema: { type: "string" },
|
||||
}),
|
||||
new Mcp.Tool({
|
||||
server: Mcp.ServerName.make("direct"),
|
||||
name: "issues",
|
||||
codemode: false,
|
||||
description: "Returns JSON as text",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
}),
|
||||
new Mcp.Tool({
|
||||
server: Mcp.ServerName.make("direct"),
|
||||
name: "lookup",
|
||||
@@ -374,6 +400,20 @@ const mcp = Layer.mock(Mcp.Service, {
|
||||
isError: false,
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
})
|
||||
if (input.name === "issues" || input.name === "typed")
|
||||
return new Mcp.ToolResult({
|
||||
server: Mcp.ServerName.make(input.server),
|
||||
tool: input.name,
|
||||
isError: false,
|
||||
content: [{ type: "text", text: '{"issues":[{"id":1}]}' }],
|
||||
})
|
||||
if (input.name === "count")
|
||||
return new Mcp.ToolResult({
|
||||
server: Mcp.ServerName.make(input.server),
|
||||
tool: input.name,
|
||||
isError: false,
|
||||
content: [{ type: "text", text: "42" }],
|
||||
})
|
||||
return new Mcp.ToolResult({
|
||||
server: Mcp.ServerName.make(input.server),
|
||||
tool: input.name,
|
||||
@@ -1943,6 +1983,7 @@ it.effect("advertises MCP output schemas to Code Mode", () =>
|
||||
|
||||
expect(toolSet.definitions.map((tool) => tool.name)).toEqual([
|
||||
"direct_fail",
|
||||
"direct_issues",
|
||||
"direct_lookup",
|
||||
"direct_media",
|
||||
"execute",
|
||||
@@ -2033,6 +2074,39 @@ it.effect("returns content-only MCP results through Code Mode", () =>
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("parses JSON text results from MCP tools without an output schema", () =>
|
||||
Effect.gen(function* () {
|
||||
assertion = yield* Deferred.make<Permission.AssertInput>()
|
||||
decision = Effect.void
|
||||
const registry = yield* Tool.Service
|
||||
const registration = yield* McpTool.Service
|
||||
yield* registration.flush
|
||||
const toolSet = yield* registry.snapshot()
|
||||
|
||||
const run = (code: string) =>
|
||||
toolSet
|
||||
.execute({
|
||||
sessionID: Session.ID.make("ses_mcp_json_text"),
|
||||
...toolIdentity,
|
||||
call: { type: "tool-call", id: `call_${code.length}`, name: "execute", input: { code } },
|
||||
})
|
||||
.pipe(Effect.map((execution) => execution.output.output))
|
||||
|
||||
expect(yield* run("return (await tools.demo.issues({})).issues[0].id")).toBe("1")
|
||||
expect(yield* run("return typeof (await tools.demo.count({}))")).toBe("string")
|
||||
expect(yield* run("return typeof (await tools.demo.typed({}))")).toBe("string")
|
||||
|
||||
// Outside Code Mode the content the model reads is the original text.
|
||||
expect(
|
||||
yield* toolSet.execute({
|
||||
sessionID: Session.ID.make("ses_mcp_json_text"),
|
||||
...toolIdentity,
|
||||
call: { type: "tool-call", id: "call_direct_issues", name: "direct_issues", input: {} },
|
||||
}),
|
||||
).toMatchObject({ output: { issues: [{ id: 1 }] }, content: [{ type: "text", text: '{"issues":[{"id":1}]}' }] })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("advertises MCP tools directly when Code Mode is disabled for the server", () =>
|
||||
Effect.gen(function* () {
|
||||
const registry = yield* Tool.Service
|
||||
|
||||
@@ -855,6 +855,7 @@ describe("ModelResolver", () => {
|
||||
expect(modelID).toBe("api-test-model")
|
||||
expect(settings).toEqual({
|
||||
region: "test",
|
||||
providerOptions: { region: "test" },
|
||||
headers: { "x-package": "header" },
|
||||
body: { custom: true },
|
||||
})
|
||||
@@ -1039,11 +1040,7 @@ describe("ModelResolver", () => {
|
||||
const packages = [
|
||||
["@ai-sdk/anthropic", "@opencode/ai/providers/anthropic", "api-model"],
|
||||
["@ai-sdk/amazon-bedrock", "@opencode/ai/providers/amazon-bedrock", "api-model"],
|
||||
[
|
||||
"@ai-sdk/amazon-bedrock/mantle",
|
||||
"@opencode/ai/providers/amazon-bedrock/mantle/chat",
|
||||
"openai.gpt-oss-120b",
|
||||
],
|
||||
["@ai-sdk/amazon-bedrock/mantle", "@opencode/ai/providers/amazon-bedrock/mantle/chat", "openai.gpt-oss-120b"],
|
||||
["@ai-sdk/azure", "@opencode/ai/providers/azure/responses", "api-model"],
|
||||
["@ai-sdk/cerebras", "@opencode/ai/providers/cerebras", "api-model"],
|
||||
["@ai-sdk/deepinfra", "@opencode/ai/providers/deepinfra", "api-model"],
|
||||
|
||||
@@ -224,6 +224,34 @@ describe("Permission", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("merges session rules after agent rules and before saved approvals", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup([{ action: "*", resource: "*", effect: "allow" }])
|
||||
const { db } = yield* Database.Service
|
||||
const service = yield* Permission.Service
|
||||
const setSession = (permission: Permission.Ruleset) =>
|
||||
db
|
||||
.update(SessionTable)
|
||||
.set({ permission })
|
||||
.where(eq(SessionTable.id, Session.ID.make("ses_test")))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
yield* setSession([{ action: "edit", resource: "/original/**", effect: "deny" }])
|
||||
expect(yield* service.ask(assertion({ action: "edit", resources: ["/original/src/index.ts"] }))).toMatchObject({
|
||||
effect: "deny",
|
||||
})
|
||||
|
||||
yield* setRules([])
|
||||
const saved = yield* PermissionSaved.Service
|
||||
yield* saved.add({ projectID: Project.ID.global, action: "bash", resources: ["pwd"] })
|
||||
yield* setSession([{ action: "bash", resource: "*", effect: "deny" }])
|
||||
expect(yield* service.ask(assertion({ action: "bash", resources: ["pwd"] }))).toMatchObject({ effect: "deny" })
|
||||
yield* setSession([{ action: "bash", resource: "*", effect: "ask" }])
|
||||
expect(yield* service.ask(assertion({ action: "bash", resources: ["pwd"] }))).toMatchObject({ effect: "allow" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses saved bash approvals while preserving configured deny precedence", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup()
|
||||
|
||||
@@ -108,6 +108,7 @@ export function host(overrides: Overrides = {}): Plugin.Context {
|
||||
list: () => Effect.die("unused permission.list"),
|
||||
get: () => Effect.die("unused permission.get"),
|
||||
reply: () => Effect.die("unused permission.reply"),
|
||||
rules: () => Effect.die("unused permission.rules"),
|
||||
},
|
||||
plugin: overrides.plugin ?? {
|
||||
list: () => Effect.die("unused plugin.list"),
|
||||
|
||||
@@ -27,4 +27,24 @@ describe("Provider", () => {
|
||||
expect(loaded.model).toBeFunction()
|
||||
}
|
||||
})
|
||||
|
||||
test("offers flat settings to native packages as both connection settings and request options", () => {
|
||||
expect(
|
||||
Provider.nativeSettings({
|
||||
apiKey: "secret",
|
||||
baseURL: "https://example.com/v1",
|
||||
region: "us-east-1",
|
||||
reasoningEffort: "high",
|
||||
chunkTimeout: 1000,
|
||||
providerOptions: { textVerbosity: "low" },
|
||||
}),
|
||||
).toEqual({
|
||||
apiKey: "secret",
|
||||
baseURL: "https://example.com/v1",
|
||||
region: "us-east-1",
|
||||
reasoningEffort: "high",
|
||||
textVerbosity: "low",
|
||||
providerOptions: { region: "us-east-1", reasoningEffort: "high", textVerbosity: "low" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -388,6 +388,32 @@ describe("Session.create", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("stores permission rules, inherits them through children and forks, and replaces them", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
const bus = yield* Bus.Service
|
||||
const { db } = yield* Database.Service
|
||||
const permissions = [{ action: "edit", resource: "/original/**", effect: "deny" as const }]
|
||||
|
||||
const created = yield* session.create({ location, permissions })
|
||||
expect(created.permissions).toEqual(permissions)
|
||||
expect((yield* session.create({ parentID: created.id })).permissions).toEqual(permissions)
|
||||
expect((yield* session.create({ parentID: created.id, permissions: [] })).permissions).toEqual([])
|
||||
|
||||
yield* session.prompt({ sessionID: created.id, text: "Fork context", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, created.id, "steer")
|
||||
const forked = yield* session.fork({ sessionID: created.id, boundary: { type: "through" } })
|
||||
expect(forked.permissions).toEqual(permissions)
|
||||
|
||||
const replaced = [{ action: "shell", resource: "*", effect: "ask" as const }]
|
||||
yield* session.setPermissions({ sessionID: created.id, permissions: replaced })
|
||||
expect((yield* session.get(created.id)).permissions).toEqual(replaced)
|
||||
expect(
|
||||
yield* session.setPermissions({ sessionID: Session.ID.create(), permissions: replaced }).pipe(Effect.flip),
|
||||
).toBeInstanceOf(Session.NotFoundError)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("inherits location from an existing parent when omitted", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
@@ -1330,7 +1356,12 @@ describe("SessionTransfer", () => {
|
||||
const transfer = yield* SessionTransfer.Service
|
||||
const bus = yield* Bus.Service
|
||||
const { db } = yield* Database.Service
|
||||
const template = yield* session.create({ location, title: "Exported", metadata: { channel: "C123" } })
|
||||
const template = yield* session.create({
|
||||
location,
|
||||
title: "Exported",
|
||||
metadata: { channel: "C123" },
|
||||
permissions: [{ action: "edit", resource: "*", effect: "deny" }],
|
||||
})
|
||||
const sessionID = Session.ID.create()
|
||||
const sourceMessageID = SessionMessage.ID.create()
|
||||
const errorMessageID = SessionMessage.ID.create()
|
||||
@@ -1376,7 +1407,13 @@ describe("SessionTransfer", () => {
|
||||
})
|
||||
const messages = yield* session.messages({ sessionID, order: "asc" })
|
||||
|
||||
expect(imported).toMatchObject({ id: sessionID, title: "Exported", location, metadata: { channel: "C123" } })
|
||||
expect(imported).toMatchObject({
|
||||
id: sessionID,
|
||||
title: "Exported",
|
||||
location,
|
||||
metadata: { channel: "C123" },
|
||||
permissions: [{ action: "edit", resource: "*", effect: "deny" }],
|
||||
})
|
||||
expect(imported.time).toMatchObject({
|
||||
updated: DateTime.makeUnsafe(1_000),
|
||||
idle: DateTime.makeUnsafe(200),
|
||||
|
||||
@@ -1944,7 +1944,16 @@ describe("SessionRunnerLLM", () => {
|
||||
expect(yield* entries.list(sessionID)).toEqual([{ key: "nullable", value: null }])
|
||||
})
|
||||
|
||||
scenario("rejects API instruction entries larger than 8KB", function* () {
|
||||
scenario("accepts API instruction entries up to 256 KiB", function* () {
|
||||
const entries = yield* InstructionEntry.Service
|
||||
const value = "x".repeat(InstructionEntry.MaxValueBytes - 2)
|
||||
|
||||
yield* entries.put({ sessionID, key: "large", value })
|
||||
|
||||
expect(yield* entries.list(sessionID)).toEqual([{ key: "large", value }])
|
||||
})
|
||||
|
||||
scenario("rejects API instruction entries larger than 256 KiB", function* () {
|
||||
const entries = yield* InstructionEntry.Service
|
||||
|
||||
const exit = yield* entries
|
||||
|
||||
@@ -19,6 +19,6 @@ export interface PermissionHooks {
|
||||
readonly evaluate: PermissionEvaluation
|
||||
}
|
||||
|
||||
export type PermissionDomain = Pick<PermissionApi<unknown>, "list" | "get" | "reply"> & {
|
||||
export type PermissionDomain = Pick<PermissionApi<unknown>, "list" | "get" | "reply" | "rules"> & {
|
||||
readonly hook: Hooks<PermissionHooks>
|
||||
}
|
||||
|
||||
@@ -438,6 +438,7 @@ export function fromPromise(plugin: Plugin) {
|
||||
list: adaptApiMethod(PermissionEndpoints["session.permission.list"], host.permission.list),
|
||||
get: adaptApiMethod(PermissionEndpoints["session.permission.get"], host.permission.get),
|
||||
reply: adaptApiMethod(PermissionEndpoints["session.permission.reply"], host.permission.reply),
|
||||
rules: adaptApiMethod(PermissionEndpoints["session.permission.rules"], host.permission.rules),
|
||||
},
|
||||
plugin: {
|
||||
list: adaptApiMethod(PluginEndpoints["plugin.list"], host.plugin.list),
|
||||
|
||||
@@ -19,6 +19,6 @@ export interface PermissionHooks {
|
||||
readonly evaluate: PermissionEvaluation
|
||||
}
|
||||
|
||||
export type PermissionDomain = Pick<PermissionApi, "list" | "get" | "reply"> & {
|
||||
export type PermissionDomain = Pick<PermissionApi, "list" | "get" | "reply" | "rules"> & {
|
||||
readonly hook: Hooks<PermissionHooks>
|
||||
}
|
||||
|
||||
@@ -132,4 +132,21 @@ export const makePermissionGroup = <
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.put("session.permission.rules", "/api/session/:sessionID/permission/rules", {
|
||||
params: { sessionID: Session.ID },
|
||||
payload: Schema.Struct({ permissions: Permission.Ruleset }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.permission.rules",
|
||||
summary: "Replace session permission rules",
|
||||
description:
|
||||
"Replace the session-scoped permission rules. Rules are evaluated after the agent's rules, and the last matching rule wins.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "permission", description: "Experimental permission routes." }))
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { Agent } from "@opencode/schema/agent"
|
||||
import { Skill } from "@opencode/schema/skill"
|
||||
import { Model } from "@opencode/schema/model"
|
||||
import { Permission } from "@opencode/schema/permission"
|
||||
import { Location } from "@opencode/schema/location"
|
||||
import { SessionEvent } from "@opencode/schema/session-event"
|
||||
import { EventLog } from "@opencode/schema/event-log"
|
||||
@@ -175,6 +176,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
model: Model.Ref.pipe(Schema.optional),
|
||||
location: Location.Ref.pipe(Schema.optional),
|
||||
metadata: Session.Metadata.pipe(Schema.optional),
|
||||
permissions: Permission.Ruleset.pipe(Schema.optional),
|
||||
}),
|
||||
success: Schema.Struct({ data: Session.Info }),
|
||||
}).annotateMerge(
|
||||
|
||||
@@ -27,7 +27,7 @@ export const Snapshot = Schema.Array(
|
||||
).annotate({ identifier: "InstructionEntry.Snapshot" })
|
||||
export type Snapshot = typeof Snapshot.Type
|
||||
|
||||
export const MaxValueBytes = 8 * 1024
|
||||
export const MaxValueBytes = 256 * 1024
|
||||
|
||||
export class ValueTooLargeError extends Schema.TaggedError<ValueTooLargeError>()(
|
||||
"InstructionEntryValueTooLargeError",
|
||||
|
||||
@@ -25,6 +25,7 @@ import { TokenUsage } from "./token-usage.js"
|
||||
import { SessionInbox } from "./session-inbox.js"
|
||||
import { Project } from "./project.js"
|
||||
import { SessionFork } from "./session-fork.js"
|
||||
import { Permission } from "./permission.js"
|
||||
|
||||
export { FileAttachment }
|
||||
|
||||
@@ -62,6 +63,7 @@ export const Created = Event.durable({
|
||||
model: Model.Ref.pipe(optional),
|
||||
/** Host-supplied annotations resolved at creation, including any inherited from a parent. */
|
||||
metadata: SessionMetadata.pipe(optional),
|
||||
permissions: Permission.Ruleset.pipe(optional),
|
||||
version: Schema.String,
|
||||
},
|
||||
})
|
||||
@@ -109,6 +111,16 @@ export const Renamed = Event.durable({
|
||||
})
|
||||
export type Renamed = typeof Renamed.Type
|
||||
|
||||
export const PermissionsUpdated = Event.durable({
|
||||
type: "session.permissions.updated",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
permissions: Permission.Ruleset,
|
||||
},
|
||||
})
|
||||
export type PermissionsUpdated = typeof PermissionsUpdated.Type
|
||||
|
||||
export const Viewed = Event.durable({
|
||||
type: "session.viewed",
|
||||
...options,
|
||||
@@ -634,6 +646,7 @@ export const Definitions = Event.inventory(
|
||||
ModelSelected,
|
||||
Moved,
|
||||
Renamed,
|
||||
PermissionsUpdated,
|
||||
Viewed,
|
||||
UsageUpdated,
|
||||
Deleted,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SessionEvent } from "./session-event.js"
|
||||
import { SessionID } from "./session-id.js"
|
||||
import { SessionMetadata } from "./session-metadata.js"
|
||||
import { Money } from "./money.js"
|
||||
import { Permission } from "./permission.js"
|
||||
import { TokenUsage } from "./token-usage.js"
|
||||
import { Revert } from "./session-revert.js"
|
||||
import { SessionFork } from "./session-fork.js"
|
||||
@@ -54,6 +55,8 @@ export const Info = Schema.Struct({
|
||||
location: Location.Ref,
|
||||
subpath: RelativePath.pipe(optional),
|
||||
metadata: Metadata.pipe(optional),
|
||||
/** Evaluated after the agent's rules; the last matching rule wins. */
|
||||
permissions: Permission.Ruleset.pipe(optional),
|
||||
revert: Revert.pipe(optional),
|
||||
}).annotate({ identifier: "Session.Info" })
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ describe("public event manifest", () => {
|
||||
"session.model.selected.1",
|
||||
"session.moved.1",
|
||||
"session.renamed.1",
|
||||
"session.permissions.updated.1",
|
||||
"session.viewed.1",
|
||||
"session.message.content.updated.1",
|
||||
"session.usage.recorded.1",
|
||||
|
||||
@@ -83,6 +83,15 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission",
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.permission.rules",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* sessions
|
||||
.setPermissions({ sessionID: ctx.params.sessionID, permissions: ctx.payload.permissions })
|
||||
.pipe(Effect.catchTag("Session.NotFoundError", missingSession))
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"permission.saved.list",
|
||||
Effect.fn(function* (ctx) {
|
||||
|
||||
@@ -120,6 +120,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
agent: ctx.payload.agent,
|
||||
model: ctx.payload.model,
|
||||
metadata: ctx.payload.metadata,
|
||||
permissions: ctx.payload.permissions,
|
||||
location: ctx.payload.location ?? { directory: AbsolutePath.make(process.cwd()) },
|
||||
})
|
||||
.pipe(Effect.orDie),
|
||||
|
||||
@@ -39,6 +39,7 @@ describe("inference stat normalization", () => {
|
||||
})
|
||||
|
||||
test("merges renamed models under their current name", () => {
|
||||
expect(statModel("deepseek-flash", "")).toBe("deepseek-v4.1-flash")
|
||||
expect(statModel("x-preview-f", "")).toBe("ox-alpha")
|
||||
expect(statModel("xiaomi/mimo-v2.5", "")).toBe("mimo-v2.5")
|
||||
expect(toModelAggregate(aggregate("x-preview-f", "openai"))).toMatchObject([
|
||||
|
||||
@@ -14,6 +14,7 @@ export const MODEL_AUTHOR_RULES = [
|
||||
] as const
|
||||
export const EXCLUDED_MODELS = new Set(["alpha-gpt-next"])
|
||||
export const MODEL_NAME_ALIASES: Record<string, string> = {
|
||||
"deepseek-flash": "deepseek-v4.1-flash",
|
||||
"x-preview-f": "ox-alpha",
|
||||
"xiaomi/mimo-v2.5": "mimo-v2.5",
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@ export function createClipboardAdapter(clipboard: CoreClipboardService): OwnedCl
|
||||
throw new Error(`Unexpected clipboard MIME type: ${result.representation.mimeType}`)
|
||||
},
|
||||
async write(text) {
|
||||
const result = await clipboard.writeText(text, {
|
||||
// OpenTUI rejects NUL before any destination; host clipboard text cannot contain it.
|
||||
const result = await clipboard.writeText(text.replaceAll("\0", ""), {
|
||||
destination: "all-available",
|
||||
selection: "clipboard",
|
||||
})
|
||||
|
||||
@@ -274,6 +274,7 @@ export const Definitions = {
|
||||
"permission.prompt.fullscreen": keybind("ctrl+f", "Toggle permission prompt fullscreen"),
|
||||
"plugins.toggle": keybind("return", "Toggle plugin"),
|
||||
"dialog.mcp.toggle": keybind("space", "Toggle MCP server"),
|
||||
"dialog.plugins.error": keybind("space", "View plugin error"),
|
||||
"dialog.plugins.install": keybind("shift+i", "Install plugin from plugin dialog"),
|
||||
"dialog.plugins.update": keybind("ctrl+u", "Update plugin from plugin dialog"),
|
||||
"dialog.plugins.check": keybind("ctrl+r", "Check for plugin updates from plugin dialog"),
|
||||
|
||||
@@ -223,6 +223,15 @@ export function PluginsDialog(props: {
|
||||
disabled: checking(),
|
||||
onTrigger: check,
|
||||
},
|
||||
{
|
||||
title: "view error",
|
||||
command: "dialog.plugins.error",
|
||||
hidden: !pluginError(focusedTui()),
|
||||
onTrigger: (option) => {
|
||||
const entry = entries().find((entry) => entry.key === option.value)
|
||||
if (pluginError(entry)) setDetail(entry)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: toggleTitle(),
|
||||
command: "plugins.toggle",
|
||||
@@ -239,7 +248,7 @@ export function PluginsDialog(props: {
|
||||
},
|
||||
]}
|
||||
footer={
|
||||
<Show when={pluginError(focusedEntry())}>
|
||||
<Show when={pluginError(focusedEntry()) && !focusedTui()}>
|
||||
<text>
|
||||
<span style={{ fg: props.context.theme.text.default }}>
|
||||
<b>enter</b>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
export type GroupNode<Entry, Kind extends string> =
|
||||
| { readonly type: "entry"; readonly entry: Entry; readonly size: 1 }
|
||||
| {
|
||||
readonly type: "group"
|
||||
readonly kind: Kind
|
||||
readonly children: readonly GroupNode<Entry, Kind>[]
|
||||
/** Number of descendant leaves, independent of disclosure state. */
|
||||
readonly size: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Group adjacent entries by their configured nesting paths. For example, a read
|
||||
* can use ["exploration"] today or ["activity", "exploration"] in Low.
|
||||
* Entries are opaque: message/part identity, visibility and live state remain
|
||||
* owned by the session projection. A path of [] creates a standalone leaf.
|
||||
*/
|
||||
export function groupEntries<Entry, Kind extends string>(
|
||||
entries: readonly Entry[],
|
||||
path: (entry: Entry) => readonly Kind[],
|
||||
): readonly GroupNode<Entry, Kind>[] {
|
||||
const result: BuildingNode<Entry, Kind>[] = []
|
||||
entries.forEach((entry) => {
|
||||
appendEntry(result, entry, path(entry))
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Only freshly constructed nodes are writable; the published tree is readonly.
|
||||
type BuildingNode<Entry, Kind extends string> =
|
||||
| { type: "entry"; entry: Entry; size: 1 }
|
||||
| { type: "group"; kind: Kind; children: BuildingNode<Entry, Kind>[]; size: number }
|
||||
|
||||
function appendEntry<Entry, Kind extends string>(
|
||||
nodes: BuildingNode<Entry, Kind>[],
|
||||
entry: Entry,
|
||||
path: readonly Kind[],
|
||||
depth = 0,
|
||||
) {
|
||||
const kind = path[depth]
|
||||
if (kind === undefined) {
|
||||
nodes.push({ type: "entry", entry, size: 1 })
|
||||
return
|
||||
}
|
||||
const previous = nodes.at(-1)
|
||||
if (previous?.type === "group" && previous.kind === kind) {
|
||||
previous.size++
|
||||
appendEntry(previous.children, entry, path, depth + 1)
|
||||
return
|
||||
}
|
||||
const children: BuildingNode<Entry, Kind>[] = []
|
||||
appendEntry(children, entry, path, depth + 1)
|
||||
nodes.push({ type: "group", kind, children, size: 1 })
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate ordered, disjoint chunks, recursively merging compatible groups
|
||||
* at their seam. Untouched subtrees retain their object identity.
|
||||
*
|
||||
* This is concatenation, not ingestion: callers must reconcile overlapping
|
||||
* pages/replayed message IDs before merging. Equal payloads may be distinct
|
||||
* entries and must not be silently deduplicated here.
|
||||
*/
|
||||
export function mergeGroups<Entry, Kind extends string>(
|
||||
left: readonly GroupNode<Entry, Kind>[],
|
||||
right: readonly GroupNode<Entry, Kind>[],
|
||||
): readonly GroupNode<Entry, Kind>[] {
|
||||
if (!left.length) return right
|
||||
if (!right.length) return left
|
||||
const a = left[left.length - 1]
|
||||
const b = right[0]
|
||||
if (a.type !== "group" || b.type !== "group" || a.kind !== b.kind) return [...left, ...right]
|
||||
return [
|
||||
...left.slice(0, -1),
|
||||
{
|
||||
type: "group",
|
||||
kind: a.kind,
|
||||
size: a.size + b.size,
|
||||
children: mergeGroups(a.children, b.children),
|
||||
},
|
||||
...right.slice(1),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Split at a depth-first leaf offset. Group headers count as zero. Cached sizes
|
||||
* skip whole subtrees; only the ancestors crossing the cut are reconstructed.
|
||||
* The returned halves can be seam-merged again without changing their meaning.
|
||||
*/
|
||||
export function splitGroups<Entry, Kind extends string>(
|
||||
nodes: readonly GroupNode<Entry, Kind>[],
|
||||
count: number,
|
||||
): readonly [readonly GroupNode<Entry, Kind>[], readonly GroupNode<Entry, Kind>[]] {
|
||||
if (!Number.isInteger(count) || count < 0) throw new RangeError("Group split requires a non-negative integer")
|
||||
if (count === 0) return [[], nodes]
|
||||
let offset = 0
|
||||
for (const [index, node] of nodes.entries()) {
|
||||
const end = offset + node.size
|
||||
if (count === end) return [nodes.slice(0, index + 1), nodes.slice(index + 1)]
|
||||
if (count < end) {
|
||||
if (node.type !== "group") throw new RangeError("Cannot split inside an entry")
|
||||
const size = count - offset
|
||||
const [left, right] = splitGroups(node.children, size)
|
||||
return [
|
||||
[...nodes.slice(0, index), { ...node, children: left, size }],
|
||||
[{ ...node, children: right, size: node.size - size }, ...nodes.slice(index + 1)],
|
||||
]
|
||||
}
|
||||
offset = end
|
||||
}
|
||||
throw new RangeError("Group split exceeds entry count")
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
batch,
|
||||
createContext,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
onMount,
|
||||
Show,
|
||||
Switch,
|
||||
useContext,
|
||||
type Accessor,
|
||||
} from "solid-js"
|
||||
import path from "node:path"
|
||||
@@ -29,7 +27,6 @@ import { createSyntaxStyleMemo, ThemeContextProvider, useTheme, useThemes } from
|
||||
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA, MouseEvent } from "@opentui/core"
|
||||
import { Prompt, type PromptRef } from "../../component/prompt"
|
||||
import type {
|
||||
ModelInfo,
|
||||
SessionMessageInfo,
|
||||
SessionMessageAssistant,
|
||||
SessionMessageAssistantReasoning,
|
||||
@@ -111,6 +108,9 @@ import { createDelayedPresence } from "../../util/delayed-presence"
|
||||
import { SessionLocationMissing } from "./location-missing"
|
||||
import { isRecord } from "../../util/record"
|
||||
import { createHistoryPrepend } from "./history"
|
||||
import { context, use, type PendingAction } from "./render-context"
|
||||
import { INLINE_TOOL_ICON_WIDTH, InlineToolRow, ReasoningPart, reasoningContent, TextPart } from "./message-parts"
|
||||
export { InlineToolRow } from "./message-parts"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
@@ -121,34 +121,6 @@ const BACKGROUND_TOOL_HINT_DELAY = 3_000
|
||||
// The tail comfortably overfills a tall viewport; older rows mount as the reader approaches them.
|
||||
const TRANSCRIPT_TAIL_ROWS = 40
|
||||
const TRANSCRIPT_BACKFILL_CHUNK = 60
|
||||
type PendingAction = "steer" | "queue" | "cancel"
|
||||
|
||||
const context = createContext<{
|
||||
/** Content width: terminal width minus vertical tabs, sidebar, and padding. */
|
||||
width: number
|
||||
/**
|
||||
* Shared reactive terminal size. Transcript-row components must read this
|
||||
* instead of calling useTerminalDimensions(), which registers one renderer
|
||||
* resize listener per mounted component and grows with transcript length.
|
||||
*/
|
||||
terminal: { width: number; height: number }
|
||||
sessionID: string
|
||||
thinkingMode: () => ThinkingMode
|
||||
markdownMode: () => "source" | "rendered"
|
||||
groupExploration: () => boolean
|
||||
diffWrapMode: () => "word" | "none"
|
||||
models: () => ModelInfo[]
|
||||
messageIndex: (messageID: string) => number | undefined
|
||||
config: ReturnType<typeof useConfig>["data"]
|
||||
mutatePending: (action: PendingAction, inboxID: string) => Promise<boolean>
|
||||
pendingDelivery: (inboxID: string) => SessionInbox.Delivery | undefined
|
||||
}>()
|
||||
|
||||
function use() {
|
||||
const ctx = useContext(context)
|
||||
if (!ctx) throw new Error("useContext must be used within a Session component")
|
||||
return ctx
|
||||
}
|
||||
|
||||
export function Session(props: {
|
||||
scrollRef?: (scroll: ScrollBoxRenderable | undefined) => void
|
||||
@@ -2462,160 +2434,6 @@ function AssistantRetry(props: { retry: SessionMessageAssistant["retry"] }) {
|
||||
)
|
||||
}
|
||||
|
||||
const INLINE_TOOL_ICON_WIDTH = 2
|
||||
|
||||
function ReasoningPart(props: {
|
||||
last: boolean
|
||||
part: SessionMessageAssistantReasoning
|
||||
message: SessionMessageAssistant
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
const { currentSyntax: syntax } = useThemes()
|
||||
const thinkingSyntax = createSyntaxStyleMemo(() => generateThinkingSyntax(syntax(), theme.text.subdued))
|
||||
const ctx = use()
|
||||
// Collapsed by default in hide mode: a single line throughout, so the
|
||||
// layout never shifts. Click to open the full markdown block, click to close.
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
|
||||
const content = createMemo(() => reasoningContent(props.part))
|
||||
const isDone = createMemo(
|
||||
() => props.part.time?.completed !== undefined || props.message.time.completed !== undefined,
|
||||
)
|
||||
const inMinimal = createMemo(() => ctx.thinkingMode() === "hide")
|
||||
const duration = createMemo(() => {
|
||||
const end = props.part.time?.completed ?? props.message.time.completed
|
||||
const start = props.part.time?.created ?? props.message.time.created
|
||||
return end === undefined ? 0 : Math.max(0, end - start)
|
||||
})
|
||||
const summary = createMemo(() => reasoningSummary(content()))
|
||||
const toggle = () => {
|
||||
if (!inMinimal()) return
|
||||
setExpanded((prev) => !prev)
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={content()}>
|
||||
<box paddingLeft={3} flexDirection="column" flexShrink={0}>
|
||||
<box
|
||||
border={!inMinimal() || expanded() ? ["left"] : undefined}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.raise(theme.background.default)}
|
||||
paddingLeft={!inMinimal() || expanded() ? 1 : 0}
|
||||
>
|
||||
<box onMouseUp={toggle}>
|
||||
<ReasoningHeader
|
||||
toggleable={inMinimal()}
|
||||
open={!inMinimal() || expanded()}
|
||||
done={isDone()}
|
||||
title={inMinimal() && !expanded() ? summary().title : null}
|
||||
duration={isDone() ? Locale.duration(duration()) : undefined}
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
<Show when={!inMinimal() || expanded()}>
|
||||
<box marginTop={1}>
|
||||
<box
|
||||
border={["left"]}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.raise(theme.background.default)}
|
||||
paddingLeft={inMinimal() ? 3 : 1}
|
||||
>
|
||||
<code
|
||||
filetype="markdown"
|
||||
drawUnstyledText={false}
|
||||
streaming={true}
|
||||
syntaxStyle={thinkingSyntax()}
|
||||
content={content()}
|
||||
conceal={ctx.markdownMode() === "rendered"}
|
||||
fg={theme.text.subdued}
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function reasoningContent(part: SessionMessageAssistantReasoning) {
|
||||
// OpenRouter encrypts some reasoning blocks; drop the placeholder.
|
||||
return part.text.replace("[REDACTED]", "").trim()
|
||||
}
|
||||
|
||||
function ReasoningHeader(props: {
|
||||
toggleable: boolean
|
||||
open: boolean
|
||||
done: boolean
|
||||
title: string | null
|
||||
duration?: string
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
const fg = () =>
|
||||
props.open
|
||||
? RGBA.fromValues(
|
||||
theme.text.feedback.warning.default.r,
|
||||
theme.text.feedback.warning.default.g,
|
||||
theme.text.feedback.warning.default.b,
|
||||
0.6,
|
||||
)
|
||||
: theme.text.feedback.warning.default
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={!props.done}>
|
||||
<box flexDirection="row">
|
||||
<Spinner color={fg()}>{props.title ? "Thinking: " + props.title : "Thinking"}</Spinner>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<text fg={fg()} wrapMode="none">
|
||||
<Show when={props.toggleable}>
|
||||
<span>{props.open ? "- " : "+ "}</span>
|
||||
</Show>
|
||||
<span>Thought</span>
|
||||
<Show when={props.title || props.duration}>
|
||||
<span>: </span>
|
||||
</Show>
|
||||
<Show when={props.title}>
|
||||
<span>{props.title}</span>
|
||||
</Show>
|
||||
<Show when={props.duration}>
|
||||
<span>
|
||||
{props.title ? " · " : ""}
|
||||
{props.duration}
|
||||
</span>
|
||||
</Show>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
function TextPart(props: { last: boolean; part: SessionMessageAssistantText; message: SessionMessageAssistant }) {
|
||||
const ctx = use()
|
||||
const theme = useTheme()
|
||||
const { currentSyntax: syntax } = useThemes()
|
||||
const plugins = usePlugin()
|
||||
return (
|
||||
<Show when={props.part.text.trim()}>
|
||||
<box paddingLeft={3} flexShrink={0}>
|
||||
{/* Configure custom nodes before parsing; apply content before streaming so completion keeps the final tokens. */}
|
||||
<markdown
|
||||
syntaxStyle={syntax()}
|
||||
renderNode={plugins.markdown()}
|
||||
content={props.part.text.trim()}
|
||||
streaming={props.message.time.completed === undefined}
|
||||
internalBlockMode="top-level"
|
||||
tableOptions={{ style: "grid", cellPaddingX: 1 }}
|
||||
conceal={ctx.markdownMode() === "rendered"}
|
||||
fg={theme.markdown.text}
|
||||
bg={theme.background.default}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
// Pending messages moved to individual tool pending functions
|
||||
|
||||
function ToolPart(props: { part: SessionMessageAssistantTool; images?: boolean }) {
|
||||
@@ -2918,101 +2736,6 @@ function InlineTool(props: {
|
||||
)
|
||||
}
|
||||
|
||||
export function InlineToolRow(props: {
|
||||
icon: string
|
||||
iconColor?: RGBA
|
||||
color?: RGBA
|
||||
errorColor?: RGBA
|
||||
failed?: boolean
|
||||
denied?: boolean
|
||||
error?: string
|
||||
errorExpanded?: boolean
|
||||
complete: unknown
|
||||
pending: string
|
||||
failure?: string
|
||||
spinner?: boolean
|
||||
status?: JSX.Element
|
||||
children: JSX.Element
|
||||
onMouseOver?: () => void
|
||||
onMouseOut?: () => void
|
||||
onMouseUp?: () => void
|
||||
}) {
|
||||
return (
|
||||
<box paddingLeft={3} onMouseOver={props.onMouseOver} onMouseOut={props.onMouseOut} onMouseUp={props.onMouseUp}>
|
||||
<Switch>
|
||||
<Match when={props.spinner}>
|
||||
<Show when={props.status} fallback={<Spinner color={props.color} children={props.children} />}>
|
||||
{(status) => (
|
||||
<box flexDirection="row" gap={1}>
|
||||
<Spinner color={props.color} />
|
||||
<InlineToolLabel color={props.color} status={status()}>
|
||||
{props.children}
|
||||
</InlineToolLabel>
|
||||
</box>
|
||||
)}
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Show fallback={<Spinner color={props.color}>{props.pending}</Spinner>} when={props.complete || props.failed}>
|
||||
<box flexDirection="row">
|
||||
<text
|
||||
width={INLINE_TOOL_ICON_WIDTH}
|
||||
fg={props.failed ? props.errorColor : (props.iconColor ?? props.color)}
|
||||
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
|
||||
>
|
||||
{props.icon}
|
||||
</text>
|
||||
<Show
|
||||
when={props.status}
|
||||
fallback={
|
||||
<text
|
||||
flexGrow={1}
|
||||
fg={props.failed ? props.errorColor : props.color}
|
||||
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
|
||||
>
|
||||
{props.failed && !props.complete ? (props.failure ?? props.children) : props.children}
|
||||
</text>
|
||||
}
|
||||
>
|
||||
{(status) => (
|
||||
<InlineToolLabel
|
||||
color={props.failed ? props.errorColor : props.color}
|
||||
denied={props.denied}
|
||||
status={status()}
|
||||
>
|
||||
{props.failed && !props.complete ? (props.failure ?? props.children) : props.children}
|
||||
</InlineToolLabel>
|
||||
)}
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={props.failed && props.errorExpanded}>
|
||||
<box paddingLeft={INLINE_TOOL_ICON_WIDTH}>
|
||||
<text fg={props.errorColor}>{props.error}</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function InlineToolLabel(props: { color?: RGBA; denied?: boolean; status: JSX.Element; children: JSX.Element }) {
|
||||
return (
|
||||
<box flexDirection="row" flexWrap="wrap" columnGap={1} flexGrow={1}>
|
||||
<text
|
||||
maxWidth="100%"
|
||||
flexShrink={0}
|
||||
fg={props.color}
|
||||
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
|
||||
>
|
||||
{props.children}
|
||||
</text>
|
||||
{props.status}
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusBadge(props: { children: string }) {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
import { createMemo, createSignal, Match, Show, Switch } from "solid-js"
|
||||
import { RGBA, TextAttributes } from "@opentui/core"
|
||||
import type { JSX } from "@opentui/solid"
|
||||
import type {
|
||||
SessionMessageAssistant,
|
||||
SessionMessageAssistantReasoning,
|
||||
SessionMessageAssistantText,
|
||||
} from "@opencode/client"
|
||||
import { Spinner } from "../../component/spinner"
|
||||
import { createSyntaxStyleMemo, useTheme, useThemes } from "../../context/theme"
|
||||
import { reasoningSummary } from "../../context/thinking"
|
||||
import { usePlugin } from "../../plugin/context"
|
||||
import { SplitBorder } from "../../ui/border"
|
||||
import { Locale } from "../../util/locale"
|
||||
import { use } from "./render-context"
|
||||
import { generateThinkingSyntax } from "./thinking-syntax"
|
||||
|
||||
export const INLINE_TOOL_ICON_WIDTH = 2
|
||||
|
||||
export function ReasoningPart(props: {
|
||||
last: boolean
|
||||
part: SessionMessageAssistantReasoning
|
||||
message: SessionMessageAssistant
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
const { currentSyntax: syntax } = useThemes()
|
||||
const thinkingSyntax = createSyntaxStyleMemo(() => generateThinkingSyntax(syntax(), theme.text.subdued))
|
||||
const ctx = use()
|
||||
// Collapsed by default in hide mode: a single line throughout, so the
|
||||
// layout never shifts. Click to open the full markdown block, click to close.
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
|
||||
const content = createMemo(() => reasoningContent(props.part))
|
||||
const isDone = createMemo(
|
||||
() => props.part.time?.completed !== undefined || props.message.time.completed !== undefined,
|
||||
)
|
||||
const inMinimal = createMemo(() => ctx.thinkingMode() === "hide")
|
||||
const duration = createMemo(() => {
|
||||
const end = props.part.time?.completed ?? props.message.time.completed
|
||||
const start = props.part.time?.created ?? props.message.time.created
|
||||
return end === undefined ? 0 : Math.max(0, end - start)
|
||||
})
|
||||
const summary = createMemo(() => reasoningSummary(content()))
|
||||
const toggle = () => {
|
||||
if (!inMinimal()) return
|
||||
setExpanded((prev) => !prev)
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={content()}>
|
||||
<box paddingLeft={3} flexDirection="column" flexShrink={0}>
|
||||
<box
|
||||
border={!inMinimal() || expanded() ? ["left"] : undefined}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.raise(theme.background.default)}
|
||||
paddingLeft={!inMinimal() || expanded() ? 1 : 0}
|
||||
>
|
||||
<box onMouseUp={toggle}>
|
||||
<ReasoningHeader
|
||||
toggleable={inMinimal()}
|
||||
open={!inMinimal() || expanded()}
|
||||
done={isDone()}
|
||||
title={inMinimal() && !expanded() ? summary().title : null}
|
||||
duration={isDone() ? Locale.duration(duration()) : undefined}
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
<Show when={!inMinimal() || expanded()}>
|
||||
<box marginTop={1}>
|
||||
<box
|
||||
border={["left"]}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.raise(theme.background.default)}
|
||||
paddingLeft={inMinimal() ? 3 : 1}
|
||||
>
|
||||
<code
|
||||
filetype="markdown"
|
||||
drawUnstyledText={false}
|
||||
streaming={true}
|
||||
syntaxStyle={thinkingSyntax()}
|
||||
content={content()}
|
||||
conceal={ctx.markdownMode() === "rendered"}
|
||||
fg={theme.text.subdued}
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
export function reasoningContent(part: SessionMessageAssistantReasoning) {
|
||||
// OpenRouter encrypts some reasoning blocks; drop the placeholder.
|
||||
return part.text.replace("[REDACTED]", "").trim()
|
||||
}
|
||||
|
||||
function ReasoningHeader(props: {
|
||||
toggleable: boolean
|
||||
open: boolean
|
||||
done: boolean
|
||||
title: string | null
|
||||
duration?: string
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
const fg = () =>
|
||||
props.open
|
||||
? RGBA.fromValues(
|
||||
theme.text.feedback.warning.default.r,
|
||||
theme.text.feedback.warning.default.g,
|
||||
theme.text.feedback.warning.default.b,
|
||||
0.6,
|
||||
)
|
||||
: theme.text.feedback.warning.default
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={!props.done}>
|
||||
<box flexDirection="row">
|
||||
<Spinner color={fg()}>{props.title ? "Thinking: " + props.title : "Thinking"}</Spinner>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<text fg={fg()} wrapMode="none">
|
||||
<Show when={props.toggleable}>
|
||||
<span>{props.open ? "- " : "+ "}</span>
|
||||
</Show>
|
||||
<span>Thought</span>
|
||||
<Show when={props.title || props.duration}>
|
||||
<span>: </span>
|
||||
</Show>
|
||||
<Show when={props.title}>
|
||||
<span>{props.title}</span>
|
||||
</Show>
|
||||
<Show when={props.duration}>
|
||||
<span>
|
||||
{props.title ? " · " : ""}
|
||||
{props.duration}
|
||||
</span>
|
||||
</Show>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
export function TextPart(props: {
|
||||
last: boolean
|
||||
part: SessionMessageAssistantText
|
||||
message: SessionMessageAssistant
|
||||
}) {
|
||||
const ctx = use()
|
||||
const theme = useTheme()
|
||||
const { currentSyntax: syntax } = useThemes()
|
||||
const plugins = usePlugin()
|
||||
return (
|
||||
<Show when={props.part.text.trim()}>
|
||||
<box paddingLeft={3} flexShrink={0}>
|
||||
{/* Configure custom nodes before parsing; apply content before streaming so completion keeps the final tokens. */}
|
||||
<markdown
|
||||
syntaxStyle={syntax()}
|
||||
renderNode={plugins.markdown()}
|
||||
content={props.part.text.trim()}
|
||||
streaming={props.message.time.completed === undefined}
|
||||
internalBlockMode="top-level"
|
||||
tableOptions={{ style: "grid", cellPaddingX: 1 }}
|
||||
conceal={ctx.markdownMode() === "rendered"}
|
||||
fg={theme.markdown.text}
|
||||
bg={theme.background.default}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
export function InlineToolRow(props: {
|
||||
icon: string
|
||||
iconColor?: RGBA
|
||||
color?: RGBA
|
||||
errorColor?: RGBA
|
||||
failed?: boolean
|
||||
denied?: boolean
|
||||
error?: string
|
||||
errorExpanded?: boolean
|
||||
complete: unknown
|
||||
pending: string
|
||||
failure?: string
|
||||
spinner?: boolean
|
||||
status?: JSX.Element
|
||||
children: JSX.Element
|
||||
onMouseOver?: () => void
|
||||
onMouseOut?: () => void
|
||||
onMouseUp?: () => void
|
||||
}) {
|
||||
return (
|
||||
<box paddingLeft={3} onMouseOver={props.onMouseOver} onMouseOut={props.onMouseOut} onMouseUp={props.onMouseUp}>
|
||||
<Switch>
|
||||
<Match when={props.spinner}>
|
||||
<Show when={props.status} fallback={<Spinner color={props.color} children={props.children} />}>
|
||||
{(status) => (
|
||||
<box flexDirection="row" gap={1}>
|
||||
<Spinner color={props.color} />
|
||||
<InlineToolLabel color={props.color} status={status()}>
|
||||
{props.children}
|
||||
</InlineToolLabel>
|
||||
</box>
|
||||
)}
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Show fallback={<Spinner color={props.color}>{props.pending}</Spinner>} when={props.complete || props.failed}>
|
||||
<box flexDirection="row">
|
||||
<text
|
||||
width={INLINE_TOOL_ICON_WIDTH}
|
||||
fg={props.failed ? props.errorColor : (props.iconColor ?? props.color)}
|
||||
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
|
||||
>
|
||||
{props.icon}
|
||||
</text>
|
||||
<Show
|
||||
when={props.status}
|
||||
fallback={
|
||||
<text
|
||||
flexGrow={1}
|
||||
fg={props.failed ? props.errorColor : props.color}
|
||||
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
|
||||
>
|
||||
{props.failed && !props.complete ? (props.failure ?? props.children) : props.children}
|
||||
</text>
|
||||
}
|
||||
>
|
||||
{(status) => (
|
||||
<InlineToolLabel
|
||||
color={props.failed ? props.errorColor : props.color}
|
||||
denied={props.denied}
|
||||
status={status()}
|
||||
>
|
||||
{props.failed && !props.complete ? (props.failure ?? props.children) : props.children}
|
||||
</InlineToolLabel>
|
||||
)}
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={props.failed && props.errorExpanded}>
|
||||
<box paddingLeft={INLINE_TOOL_ICON_WIDTH}>
|
||||
<text fg={props.errorColor}>{props.error}</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function InlineToolLabel(props: { color?: RGBA; denied?: boolean; status: JSX.Element; children: JSX.Element }) {
|
||||
return (
|
||||
<box flexDirection="row" flexWrap="wrap" columnGap={1} flexGrow={1}>
|
||||
<text
|
||||
maxWidth="100%"
|
||||
flexShrink={0}
|
||||
fg={props.color}
|
||||
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
|
||||
>
|
||||
{props.children}
|
||||
</text>
|
||||
{props.status}
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createContext, useContext } from "solid-js"
|
||||
import type { ModelInfo } from "@opencode/client"
|
||||
import type { SessionInbox } from "@opencode/schema/session-inbox"
|
||||
import type { useConfig } from "../../config"
|
||||
import type { ThinkingMode } from "../../context/thinking"
|
||||
|
||||
export type PendingAction = "steer" | "queue" | "cancel"
|
||||
|
||||
export const context = createContext<{
|
||||
/** Content width: terminal width minus vertical tabs, sidebar, and padding. */
|
||||
width: number
|
||||
/**
|
||||
* Shared reactive terminal size. Transcript-row components must read this
|
||||
* instead of calling useTerminalDimensions(), which registers one renderer
|
||||
* resize listener per mounted component and grows with transcript length.
|
||||
*/
|
||||
terminal: { width: number; height: number }
|
||||
sessionID: string
|
||||
thinkingMode: () => ThinkingMode
|
||||
markdownMode: () => "source" | "rendered"
|
||||
groupExploration: () => boolean
|
||||
diffWrapMode: () => "word" | "none"
|
||||
models: () => ModelInfo[]
|
||||
messageIndex: (messageID: string) => number | undefined
|
||||
config: ReturnType<typeof useConfig>["data"]
|
||||
mutatePending: (action: PendingAction, inboxID: string) => Promise<boolean>
|
||||
pendingDelivery: (inboxID: string) => SessionInbox.Delivery | undefined
|
||||
}>()
|
||||
|
||||
export function use() {
|
||||
const ctx = useContext(context)
|
||||
if (!ctx) throw new Error("useContext must be used within a Session component")
|
||||
return ctx
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { groupEntries, mergeGroups, splitGroups } from "../../../src/routes/session/grouping/tree"
|
||||
|
||||
const path = (entry: { path: string[] }) => entry.path
|
||||
const read = { id: "read", path: ["activity", "exploration"] }
|
||||
const search = { id: "search", path: ["activity", "exploration"] }
|
||||
const thought = { id: "thought", path: ["activity", "reasoning"] }
|
||||
const text = { id: "text", path: [] }
|
||||
const leaf = (entry: typeof read) => ({ type: "entry" as const, entry, size: 1 as const })
|
||||
|
||||
test("groups adjacent entries by path and counts leaves, not wrappers", () => {
|
||||
expect(groupEntries([read, search, thought, text, read], path)).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "activity",
|
||||
size: 3,
|
||||
children: [
|
||||
{ type: "group", kind: "exploration", size: 2, children: [leaf(read), leaf(search)] },
|
||||
{ type: "group", kind: "reasoning", size: 1, children: [leaf(thought)] },
|
||||
],
|
||||
},
|
||||
leaf(text),
|
||||
{
|
||||
type: "group",
|
||||
kind: "activity",
|
||||
size: 1,
|
||||
children: [{ type: "group", kind: "exploration", size: 1, children: [leaf(read)] }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("a direct child breaks a subgroup without ending the outer group", () => {
|
||||
const shell = { id: "shell", path: ["activity"] }
|
||||
expect(groupEntries([read, shell, search], path)).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "activity",
|
||||
size: 3,
|
||||
children: [
|
||||
{ type: "group", kind: "exploration", size: 1, children: [leaf(read)] },
|
||||
leaf(shell),
|
||||
{ type: "group", kind: "exploration", size: 1, children: [leaf(search)] },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("merges both grouping levels at a page seam without changing the inputs", () => {
|
||||
const left = groupEntries([text, read], path)
|
||||
const right = groupEntries([search, thought], path)
|
||||
const saved = structuredClone([left, right])
|
||||
const merged = mergeGroups(left, right)
|
||||
expect(merged).toEqual(groupEntries([text, read, search, thought], path))
|
||||
expect([left, right]).toEqual(saved)
|
||||
expect(merged[0]).toBe(left[0])
|
||||
})
|
||||
|
||||
test("splits at each leaf boundary and merges back to the original tree", () => {
|
||||
const entries = [read, search, thought, text]
|
||||
const tree = groupEntries(entries, path)
|
||||
for (let count = 0; count <= entries.length; count++) {
|
||||
const [left, right] = splitGroups(tree, count)
|
||||
expect(left).toEqual(groupEntries(entries.slice(0, count), path))
|
||||
expect(right).toEqual(groupEntries(entries.slice(count), path))
|
||||
expect(mergeGroups(left, right)).toEqual(tree)
|
||||
}
|
||||
})
|
||||
|
||||
test("handles empty chunks", () => {
|
||||
const tree = groupEntries([read], path)
|
||||
expect(groupEntries([], path)).toEqual([])
|
||||
expect(mergeGroups([], tree)).toBe(tree)
|
||||
expect(mergeGroups(tree, [])).toBe(tree)
|
||||
expect(splitGroups([], 0)).toEqual([[], []])
|
||||
})
|
||||
|
||||
test("rejects invalid split offsets", () => {
|
||||
const tree = groupEntries([read], path)
|
||||
for (const count of [-1, 0.5, 2, NaN]) {
|
||||
expect(() => splitGroups(tree, count)).toThrow(RangeError)
|
||||
}
|
||||
})
|
||||
@@ -13,6 +13,10 @@ import { ThemeProvider, useThemes } from "../../../src/context/theme"
|
||||
// the context back, so the context must load first exactly as it does in the app.
|
||||
import type { usePlugin } from "../../../src/plugin/context"
|
||||
import "../../../src/plugin/context"
|
||||
import { ClientProvider } from "../../../src/context/client"
|
||||
import { DataProvider } from "../../../src/context/data"
|
||||
import { LocationProvider } from "../../../src/context/location"
|
||||
import { RouteProvider } from "../../../src/context/route"
|
||||
import { PluginsDialog } from "../../../src/feature-plugins/system/plugins"
|
||||
import { DialogProvider } from "../../../src/ui/dialog"
|
||||
import { ToastProvider } from "../../../src/ui/toast"
|
||||
@@ -32,11 +36,19 @@ function packagePlugin(outdated: boolean): PluginInfo {
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPlugins(root: string, inventory: { list: PluginInfo[]; check: PluginInfo[] }) {
|
||||
async function renderPlugins(
|
||||
root: string,
|
||||
inventory: { list: PluginInfo[]; check: PluginInfo[] },
|
||||
tui?: {
|
||||
registered: { id: string; source: "builtin" | "external"; active: boolean }[]
|
||||
list: { target: string; id?: string; status: "active" | "inactive" | "failed"; error?: string }[]
|
||||
},
|
||||
) {
|
||||
const state = path.join(root, "state")
|
||||
await mkdir(state, { recursive: true })
|
||||
const requests: { path: string; body: unknown }[] = []
|
||||
const toasts: ToastOptions[] = []
|
||||
const activations: string[] = []
|
||||
const location = { directory: root, project: { id: "proj_test", directory: root, canonical: root } }
|
||||
const transport = createFetch(async (url, request) => {
|
||||
if (url.pathname === "/api/plugin") return json({ location, data: inventory.list })
|
||||
@@ -49,13 +61,14 @@ async function renderPlugins(root: string, inventory: { list: PluginInfo[]; chec
|
||||
return new Response(null, { status: 204 })
|
||||
}
|
||||
})
|
||||
const api = createApi(transport.fetch)
|
||||
|
||||
function Harness() {
|
||||
function Content() {
|
||||
onCleanup(Keymap.use().mode.push("modal"))
|
||||
const theme = useThemes().currentTokens()
|
||||
const context = {
|
||||
client: createApi(transport.fetch),
|
||||
client: api,
|
||||
data: { location: { default: () => ({ directory: root }) }, on: () => () => {} },
|
||||
get theme() {
|
||||
return theme
|
||||
@@ -66,9 +79,12 @@ async function renderPlugins(root: string, inventory: { list: PluginInfo[]; chec
|
||||
},
|
||||
} as unknown as Context
|
||||
const plugins = {
|
||||
registered: () => [],
|
||||
list: () => [],
|
||||
activate: async () => true,
|
||||
registered: () => tui?.registered ?? [],
|
||||
list: () => tui?.list ?? [],
|
||||
activate: async (id: string) => {
|
||||
activations.push(id)
|
||||
return true
|
||||
},
|
||||
deactivate: async () => true,
|
||||
} as unknown as ReturnType<typeof usePlugin>
|
||||
return <PluginsDialog context={context} plugins={plugins} />
|
||||
@@ -77,15 +93,23 @@ async function renderPlugins(root: string, inventory: { list: PluginInfo[]; chec
|
||||
return (
|
||||
<TestTuiContexts directory={root} paths={{ home: root, state, worktree: root }}>
|
||||
<ConfigProvider config={createTuiResolvedConfig()}>
|
||||
<Keymap.Provider>
|
||||
<ThemeProvider mode="dark" source={emptyThemeSource}>
|
||||
<ToastProvider>
|
||||
<DialogProvider>
|
||||
<Content />
|
||||
</DialogProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</Keymap.Provider>
|
||||
<RouteProvider initialRoute={{ type: "home" }}>
|
||||
<ClientProvider api={api}>
|
||||
<DataProvider directory={root}>
|
||||
<LocationProvider>
|
||||
<Keymap.Provider>
|
||||
<ThemeProvider mode="dark" source={emptyThemeSource}>
|
||||
<ToastProvider>
|
||||
<DialogProvider>
|
||||
<Content />
|
||||
</DialogProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</Keymap.Provider>
|
||||
</LocationProvider>
|
||||
</DataProvider>
|
||||
</ClientProvider>
|
||||
</RouteProvider>
|
||||
</ConfigProvider>
|
||||
</TestTuiContexts>
|
||||
)
|
||||
@@ -93,10 +117,46 @@ async function renderPlugins(root: string, inventory: { list: PluginInfo[]; chec
|
||||
|
||||
const app = await testRender(() => <Harness />, { width: 80, height: 20, kittyKeyboard: true })
|
||||
app.renderer.start()
|
||||
await app.waitForFrame((frame) => frame.includes("team.plugins") || frame.includes("local.plugin"))
|
||||
return { app, requests, toasts }
|
||||
const expected = tui?.list[0]?.id ?? inventory.list[0]?.id ?? "local.plugin"
|
||||
await app.waitForFrame((frame) => frame.includes(expected))
|
||||
return { app, requests, toasts, activations }
|
||||
}
|
||||
|
||||
test("failed TUI plugins keep enter to enable and use space to show the error", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const fixture = await renderPlugins(
|
||||
tmp.path,
|
||||
{ list: [], check: [] },
|
||||
{
|
||||
registered: [{ id: "broken.plugin", source: "external", active: false }],
|
||||
list: [
|
||||
{
|
||||
target: "./broken.ts",
|
||||
id: "broken.plugin",
|
||||
status: "failed",
|
||||
error: "Plugin setup failed",
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
try {
|
||||
await fixture.app.waitForFrame((frame) => frame.includes("broken.plugin") && frame.includes("view error"))
|
||||
expect(fixture.app.captureCharFrame()).toContain("enable")
|
||||
|
||||
fixture.app.mockInput.pressEnter()
|
||||
await fixture.app.waitFor(() => fixture.activations.length === 1)
|
||||
expect(fixture.activations).toEqual(["broken.plugin"])
|
||||
|
||||
fixture.app.mockInput.pressKey(" ")
|
||||
await fixture.app.waitForFrame(
|
||||
(frame) => frame.includes("TUI plugin error") && frame.includes("Plugin setup failed"),
|
||||
)
|
||||
} finally {
|
||||
fixture.app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("checking for updates refreshes the inventory and reveals the update action", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const fixture = await renderPlugins(tmp.path, { list: [packagePlugin(false)], check: [packagePlugin(true)] })
|
||||
|
||||
@@ -102,6 +102,19 @@ test("uses all available routes but skips the process host remotely", async () =
|
||||
expect(writes).toEqual({ host: 0, terminal: 1 })
|
||||
})
|
||||
|
||||
test("removes NUL characters before writing", async () => {
|
||||
const writes: string[] = []
|
||||
const clipboard = createClipboardAdapter(
|
||||
coreClipboard({
|
||||
onWrite: (text) => writes.push(text),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(await clipboard.write("before\0after")).toBeUndefined()
|
||||
expect(await clipboard.write("clean")).toBeUndefined()
|
||||
expect(writes).toEqual(["beforeafter", "clean"])
|
||||
})
|
||||
|
||||
test("rejects only when no clipboard route accepted the write", async () => {
|
||||
const writes: [string, ClipboardWriteOptions][] = []
|
||||
const failure = new Error("native clipboard failed")
|
||||
|
||||
@@ -2,85 +2,13 @@
|
||||
title: "Agents"
|
||||
---
|
||||
|
||||
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 available for direct use. 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. This is the default for a custom agent when `mode` is omitted. |
|
||||
| `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. |
|
||||
|
||||
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.
|
||||
|
||||
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`:
|
||||
Create a Markdown file to add a reusable agent. This example adds a read-only reviewer that the main agent can launch for code reviews:
|
||||
|
||||
```md title=".opencode/agents/reviewer.md"
|
||||
---
|
||||
description: Reviews changes without modifying files
|
||||
description: Reviews changes for correctness and regressions
|
||||
mode: subagent
|
||||
model: anthropic/claude-sonnet-4-5#high
|
||||
color: "#ff6b6b"
|
||||
steps: 8
|
||||
permissions:
|
||||
- action: edit
|
||||
resource: "*"
|
||||
@@ -90,70 +18,191 @@ permissions:
|
||||
effect: deny
|
||||
---
|
||||
|
||||
Review for correctness, security, regressions, and missing tests.
|
||||
List findings in severity order with file and line references.
|
||||
Review the current changes. List findings in severity order with file and line references.
|
||||
```
|
||||
|
||||
### JSON or JSONC
|
||||
Ask your primary agent to use it:
|
||||
|
||||
Use the `agents` field in any [OpenCode configuration file](/config):
|
||||
```text
|
||||
Use the reviewer subagent to review my current changes.
|
||||
```
|
||||
|
||||
An agent combines a system prompt, model preference, permissions, and display details into a named assistant profile.
|
||||
|
||||
## Locations
|
||||
|
||||
Save Markdown agents globally for all projects or inside a project:
|
||||
|
||||
```text
|
||||
~/.config/opencode/agents/<name>.md
|
||||
.opencode/agents/<name>.md
|
||||
```
|
||||
|
||||
OpenCode discovers project `.opencode` directories from the current directory up to the project root. A nested path becomes part of the agent ID:
|
||||
|
||||
```text
|
||||
.opencode/agents/team/reviewer.md → team/reviewer
|
||||
```
|
||||
|
||||
## Formats
|
||||
|
||||
### Markdown
|
||||
|
||||
Frontmatter accepts the same fields as an `agents` configuration entry. The Markdown body becomes the agent's `system` prompt:
|
||||
|
||||
```md title=".opencode/agents/explainer.md"
|
||||
---
|
||||
description: Explains code without changing it
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
Explain the relevant code with short examples. Do not edit files.
|
||||
```
|
||||
|
||||
### JSONC
|
||||
|
||||
Define agents under `agents` in any [OpenCode configuration file](/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": "#ff6b6b",
|
||||
"steps": 8,
|
||||
"description": "Reviews current changes",
|
||||
"mode": "subagent",
|
||||
"system": "Report findings in severity order.",
|
||||
"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.
|
||||
## Selection
|
||||
|
||||
## Options
|
||||
Set the primary agent used when a session has not selected one:
|
||||
|
||||
### `description`
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"default_agent": "writer",
|
||||
"agents": {
|
||||
"writer": { "mode": "primary" },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
The selected default must exist, be visible, and support primary use. Otherwise OpenCode uses `build`, then the first visible primary-capable agent. Changing this setting does not replace the agent stored on an existing session.
|
||||
|
||||
### `mode`
|
||||
## Modes
|
||||
|
||||
Accepts `primary`, `subagent`, or `all`. The default is `all`.
|
||||
|
||||
### `model`
|
||||
|
||||
Selects a model using `provider/model` with an optional `#variant`:
|
||||
Set `mode` according to where the agent should run:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agents": {
|
||||
"reviewer": {
|
||||
"model": "anthropic/claude-sonnet-4-5#high",
|
||||
"reviewer": { "mode": "subagent" },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
| Mode | Behavior |
|
||||
| --- | --- |
|
||||
| `primary` | Runs as the main agent for a session. This is the default for a new custom agent. |
|
||||
| `subagent` | Runs only in a child session through the `subagent` tool. |
|
||||
| `all` | Runs either as a primary agent or a subagent. |
|
||||
|
||||
Subagents run with fresh context in foreground or background child sessions. The parent agent's `subagent` permissions control which agents it may launch; the child uses its own configured permissions.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agents": {
|
||||
"orchestrator": {
|
||||
"permissions": [
|
||||
{ "action": "subagent", "resource": "*", "effect": "deny" },
|
||||
{ "action": "subagent", "resource": "reviewer", "effect": "allow" },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The equivalent expanded form is:
|
||||
## Builtins
|
||||
|
||||
OpenCode includes these visible agents:
|
||||
|
||||
| Agent | Mode | Purpose |
|
||||
| --- | --- | --- |
|
||||
| **Build** (`build`) | `primary` | Default coding agent. Tools are allowed by default; sensitive environment-file reads and access outside the workspace ask for approval. |
|
||||
| **Plan** (`plan`) | `primary` | Explores and plans without editing normal project files. It may write OpenCode plan files when asked, and shell commands remain permission-controlled. |
|
||||
| **General** (`general`) | `subagent` | Handles research and multi-step work with broad tool access, but cannot launch more subagents. |
|
||||
| **Explore** (`explore`) | `subagent` | Searches and reads code or web sources without editing files. |
|
||||
|
||||
Override a built-in by using the same ID:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agents": {
|
||||
"build": {
|
||||
"permissions": [
|
||||
{ "action": "shell", "resource": "git push *", "effect": "ask" },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Hidden `compaction`, `title`, and `summary` agents perform maintenance and cannot be selected directly. V2 has no built-in `scout` agent.
|
||||
|
||||
## Merging
|
||||
|
||||
Agent definitions merge in configuration order. Later scalar values replace earlier values, request maps merge by key, and permission rules append:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"permissions": [
|
||||
{ "action": "shell", "resource": "*", "effect": "ask" },
|
||||
],
|
||||
"agents": {
|
||||
"build": {
|
||||
"permissions": [
|
||||
{ "action": "shell", "resource": "git status", "effect": "allow" },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Global `permissions` apply before agent-specific rules, so later agent rules can refine them.
|
||||
|
||||
## Options
|
||||
|
||||
### Description
|
||||
|
||||
`description` explains the agent's purpose. Add it to subagents because OpenCode shows it to the model choosing which agent to launch:
|
||||
|
||||
```yaml
|
||||
description: Reviews database migrations for safety
|
||||
```
|
||||
|
||||
### Mode
|
||||
|
||||
`mode` accepts `primary`, `subagent`, or `all`. When omitted on a new custom agent, it defaults to `primary`:
|
||||
|
||||
```yaml
|
||||
mode: all
|
||||
```
|
||||
|
||||
### Model
|
||||
|
||||
`model` uses `provider/model` with an optional `#variant`:
|
||||
|
||||
```yaml
|
||||
model: anthropic/claude-sonnet-4-5#high
|
||||
```
|
||||
|
||||
JSON configuration also accepts the expanded form:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -169,84 +218,108 @@ The equivalent expanded form is:
|
||||
}
|
||||
```
|
||||
|
||||
This is the preferred model when the agent is activated. A child session uses
|
||||
its subagent's configured model, or inherits the parent session's model when
|
||||
none is configured. 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.
|
||||
- A subagent uses its configured model, or inherits the parent session's model when none is configured.
|
||||
- A session stores its selected model separately. Selecting a primary agent by ID does not change that model.
|
||||
|
||||
### `system`
|
||||
### 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:
|
||||
`system` sets the agent's system prompt. A non-empty value replaces the provider's base prompt for that agent:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agents": {
|
||||
"orchestrator": {
|
||||
"reviewer": { "system": "Review only. Do not modify files." },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Project instructions, skills, references, and other instruction sources are still added. In a Markdown agent, put this text in the document body instead of a `system` frontmatter field.
|
||||
|
||||
### Permissions
|
||||
|
||||
`permissions` is an ordered list of matching rules:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agents": {
|
||||
"reviewer": {
|
||||
"permissions": [
|
||||
{ "action": "subagent", "resource": "*", "effect": "deny" },
|
||||
{ "action": "subagent", "resource": "explore", "effect": "allow" },
|
||||
{ "action": "shell", "resource": "git *", "effect": "ask" },
|
||||
{ "action": "*", "resource": "*", "effect": "deny" },
|
||||
{ "action": "read", "resource": "src/**", "effect": "allow" },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Each rule has:
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `action` | Tool or permission action. Wildcards are supported. |
|
||||
| `resource` | Path, command, agent ID, or other value matched by the action. Wildcards are supported. |
|
||||
| `effect` | `allow`, `ask`, or `deny`. |
|
||||
|
||||
| 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, so put broad rules before exceptions. Common actions include:
|
||||
|
||||
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`.
|
||||
| Action | Covers |
|
||||
| --- | --- |
|
||||
| `shell` | Shell commands |
|
||||
| `edit` | Edit, write, and patch tools |
|
||||
| `subagent` | Child agents |
|
||||
| `read`, `glob`, `grep` | Local discovery tools |
|
||||
| `webfetch`, `websearch` | Web tools |
|
||||
| `skill` | Skill loading |
|
||||
|
||||
<Callout type="tip">
|
||||
Put broad wildcard rules first and exceptions afterward. For example, deny all subagents first, then allow `explore`.
|
||||
</Callout>
|
||||
For `read`, `edit`, and `external_directory` resources, OpenCode expands `~` and `$HOME`:
|
||||
|
||||
`~` and `$HOME` are expanded in filesystem resources for `read`, `edit`, and
|
||||
`external_directory`. Shell resources are raw command text and are not
|
||||
expanded.
|
||||
```jsonc
|
||||
{ "action": "read", "resource": "~/notes/**", "effect": "allow" }
|
||||
```
|
||||
|
||||
### `steps`
|
||||
Shell resources remain raw command text and do not expand those values.
|
||||
|
||||
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.
|
||||
### Steps
|
||||
|
||||
### `hidden`
|
||||
`steps` sets a positive maximum number of model steps:
|
||||
|
||||
When `true`, removes the agent from normal agent listings, interactive
|
||||
discovery, and the subagent catalog advertised to models. It is a visibility
|
||||
setting, not a security boundary.
|
||||
```yaml
|
||||
steps: 8
|
||||
```
|
||||
|
||||
### `color`
|
||||
On the final step, OpenCode removes tools and asks the model to summarize in text. New user input resets the allowance.
|
||||
|
||||
Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`.
|
||||
### Hidden
|
||||
|
||||
### `disabled`
|
||||
`hidden` removes an agent from normal listings, interactive discovery, and the subagent catalog:
|
||||
|
||||
When `true`, removes the agent definition at that point in configuration
|
||||
loading. This works for built-in and custom agents.
|
||||
```yaml
|
||||
hidden: true
|
||||
```
|
||||
|
||||
### `request`
|
||||
This controls visibility, not security. Use permissions to restrict behavior.
|
||||
|
||||
The V2 schema accepts per-agent request `headers` and JSON `body` overlays:
|
||||
### Color
|
||||
|
||||
`color` sets the agent's UI color using a six-digit hex value:
|
||||
|
||||
```yaml
|
||||
color: "#ff6b6b"
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
`disabled` removes a built-in or custom agent at that point in configuration loading:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agents": {
|
||||
"plan": { "disabled": true },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Request
|
||||
|
||||
`request` accepts per-agent header and JSON body overlays:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -262,8 +335,8 @@ The V2 schema accepts per-agent request `headers` and JSON `body` overlays:
|
||||
```
|
||||
|
||||
<Callout type="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.
|
||||
The V2 session runner preserves these values but does not yet send them with model requests. Configure active request
|
||||
settings on the provider, model, or model variant instead.
|
||||
</Callout>
|
||||
|
||||
Do not use legacy top-level fields such as `temperature`, `top_p`, `prompt`, `permission`, `tools`, `disable`, or `maxSteps` in new V2 agent configuration.
|
||||
|
||||
@@ -2,52 +2,78 @@
|
||||
title: "Attachments"
|
||||
---
|
||||
|
||||
Attachments add local context to a prompt as text or image media. Regardless
|
||||
of how a prompt is submitted, current V2 sessions make these attachment types
|
||||
visible to the model:
|
||||
## Attach
|
||||
|
||||
| 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 |
|
||||
Attach a local file, then ask OpenCode to use it in your prompt. In the desktop
|
||||
or web client, choose **Attach file**, paste a file, or drag it into the prompt.
|
||||
|
||||
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. A client accepting a file does not mean its contents are visible to
|
||||
the model.
|
||||
```text
|
||||
Summarize the attached README.md and list the required setup steps.
|
||||
```
|
||||
|
||||
Desktop file-picker selections can total up to 20 MiB. Other interfaces may
|
||||
apply lower client-side limits.
|
||||
|
||||
<Callout type="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.
|
||||
Choose a model with image input before attaching an image. OpenCode can pass supported images to the provider, but the
|
||||
provider and model still enforce their own formats, dimensions, file counts, and sizes. A text-only model may reject the
|
||||
request.
|
||||
</Callout>
|
||||
|
||||
## Add attachments
|
||||
## Syntax
|
||||
|
||||
Desktop and web clients provide **Attach file**, paste, and drag-and-drop controls for supported text and image files. The
|
||||
desktop file picker limits one selection to 20 MiB in total; the server also applies the per-attachment limit below.
|
||||
V2 prompt and command inputs describe an attachment with a `uri` and optional
|
||||
`name` and `description`:
|
||||
|
||||
Attachment controls and client-side limits depend on the interface. For programmatic submission, see the generated
|
||||
[API reference](/api).
|
||||
```json
|
||||
{
|
||||
"uri": "file:///home/me/project/src/server.ts",
|
||||
"name": "server.ts",
|
||||
"description": "HTTP server entrypoint"
|
||||
}
|
||||
```
|
||||
|
||||
V2 prompt and command inputs represent each attachment with a `uri` and
|
||||
optional `name` and `description`. Use an absolute `file:` URL for a file or
|
||||
directory available to the server, or an inline `data:` URL. For a text `file:`
|
||||
URL, optional positive `start` and `end` query parameters select one-based
|
||||
lines:
|
||||
Use an absolute `file:` URL for a file or directory that is available to the
|
||||
server. For text files, positive `start` and `end` parameters select one-based
|
||||
lines.
|
||||
|
||||
```text
|
||||
file:///home/me/project/src/server.ts?start=20&end=60
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
Use a `data:` URL to send content inline:
|
||||
|
||||
## Configure image processing
|
||||
```json
|
||||
{
|
||||
"uri": "data:text/plain;base64,SGVsbG8sIE9wZW5Db2RlIQ==",
|
||||
"name": "greeting.txt"
|
||||
}
|
||||
```
|
||||
|
||||
HTTP and HTTPS attachment URLs are not supported. See the generated
|
||||
[API reference](/api) for programmatic prompt submission.
|
||||
|
||||
## Formats
|
||||
|
||||
Current V2 sessions make these attachment types visible to the model:
|
||||
|
||||
| Input | Model receives | Example |
|
||||
| ----------------------- | -------------------------------------------------------------- | ------------------ |
|
||||
| UTF-8 text file | Filename and decoded text | `README.md` |
|
||||
| Directory | Non-recursive listing of immediate files and directories | `file:///home/me/` |
|
||||
| PNG, JPEG, GIF, or WebP | Image media | `diagram.png` |
|
||||
|
||||
SVG is treated as text. PDF, AVIF, BMP, audio, video, and other binary prompt
|
||||
attachments are not included in the model request. Convert an unsupported
|
||||
binary to text or a supported image first; for example, export a PDF page as
|
||||
`page-1.png` before attaching it.
|
||||
|
||||
OpenCode reads each attachment before admitting the prompt. It rejects invalid
|
||||
URLs, unreadable paths, paths other than files or directories, and decoded
|
||||
attachments over 20 MiB. Media type is detected from the bytes, so changing a
|
||||
filename or `data:` URL media type does not make an unsupported binary visible.
|
||||
|
||||
## Images
|
||||
|
||||
Configure image normalization in `opencode.json` or `opencode.jsonc`:
|
||||
|
||||
@@ -67,41 +93,70 @@ Configure image normalization in `opencode.json` or `opencode.jsonc`:
|
||||
|
||||
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. |
|
||||
| Field | Default | Behavior |
|
||||
| ------------------ | --------- | ------------------------------------------------------------------------- |
|
||||
| `auto_resize` | `true` | Resize an image over a configured limit; when `false`, reject the image. |
|
||||
| `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 bytes in the Base64-encoded image; must be a positive integer. |
|
||||
|
||||
<Callout type="note">
|
||||
These settings apply to supported image media attached directly to prompts and images produced by the built-in `read`
|
||||
tool. If the image resizer is unavailable, OpenCode passes the original image through unchanged.
|
||||
</Callout>
|
||||
For example, this rejects rather than resizes an image wider than 1200 pixels:
|
||||
|
||||
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.
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"media": {
|
||||
"image": {
|
||||
"auto_resize": false,
|
||||
"max_width": 1200,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
These settings apply both to supported images attached to prompts and to images
|
||||
returned by the built-in `read` tool.
|
||||
|
||||
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, OpenCode uses the
|
||||
original image instead, so these settings are processing limits rather than an upload or security boundary.
|
||||
## Processing
|
||||
|
||||
## Limits and provider behavior
|
||||
The `read` tool recognizes PNG, JPEG, GIF, and WebP by their contents and reads
|
||||
up to 20 MiB of source image data. It checks width, height, and Base64 length
|
||||
against the configured image limits.
|
||||
|
||||
- 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.
|
||||
With `auto_resize: true`, OpenCode preserves the aspect ratio and scales down
|
||||
to the dimension limits. It then tries progressively smaller PNG and JPEG
|
||||
encodings until the Base64 limit is met, so the output media type can change.
|
||||
|
||||
```text
|
||||
Input: 4000 × 2000 WebP
|
||||
Limits: 2000 × 2000
|
||||
Output: 2000 × 1000 PNG or JPEG
|
||||
```
|
||||
|
||||
If no encoding fits, processing fails. With `auto_resize: false`, an image that
|
||||
exceeds any limit fails without modification; an image that cannot be decoded
|
||||
also fails.
|
||||
|
||||
```text
|
||||
Input: 2400 × 1600 JPEG
|
||||
Limit: max_width = 2000, auto_resize = false
|
||||
Result: Image processing fails
|
||||
```
|
||||
|
||||
If the image resizer is unavailable, OpenCode passes the original image through
|
||||
unchanged. Image settings are therefore processing limits, not an upload or
|
||||
security boundary.
|
||||
|
||||
## Limits
|
||||
|
||||
| Limit | Value or behavior | Example |
|
||||
| ----------------------------- | ------------------------------------------------------------- | -------------------------------------------- |
|
||||
| Direct attachment | 20 MiB decoded per item; clients may impose lower limits | Two 12 MiB files pass the per-item limit |
|
||||
| Desktop picker selection | 20 MiB total | Two 12 MiB files exceed the selection limit |
|
||||
| `max_base64_bytes` | Encoded Base64 only, excluding the complete `data:` URL | `SGVsbG8=` counts as 8 bytes |
|
||||
| Provider image limits | Apply after OpenCode processing | A provider may reject an accepted image |
|
||||
| Text attachment model support | Does not require a multimodal model | `notes.txt` is inserted as prompt text |
|
||||
| `read` text limits | Uses separate paging and truncation limits | Read a large log in pages |
|
||||
|
||||
A client accepting a file does not guarantee that its contents reach the
|
||||
model. The attachment must use a model-visible format and satisfy both OpenCode
|
||||
and provider limits.
|
||||
|
||||
@@ -7,10 +7,6 @@ API. Use it when your application connects to an OpenCode server over the
|
||||
network. Its native types and methods are generated from the same contract as the
|
||||
[API reference](/api). Plugin RPC types come from imported RPC definitions.
|
||||
|
||||
<Callout type="warning">
|
||||
The V2 API and client are beta. Method names, inputs, and outputs may change before the stable release.
|
||||
</Callout>
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
|
||||
@@ -510,7 +510,7 @@ Expose the CLI plugin through `./tui`; add OpenTUI peers when the plugin renders
|
||||
"./tui": "./src/tui.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "beta"
|
||||
"@opencode/plugin": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.5.8",
|
||||
|
||||
@@ -1326,7 +1326,7 @@ entrypoint and declare both runtime dependencies.
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "beta",
|
||||
"@opencode/plugin": "latest",
|
||||
"effect": "4.0.0-rc.111"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,6 +656,16 @@ const request = await ctx.permission.get({ sessionID, requestID })
|
||||
await ctx.permission.reply({ sessionID, requestID, reply: "once" })
|
||||
```
|
||||
|
||||
Replace the session-scoped permission rules. They are evaluated after the agent's rules, and the
|
||||
last matching rule wins. Child sessions inherit the rules in effect when they are created.
|
||||
|
||||
```ts
|
||||
await ctx.permission.rules({
|
||||
sessionID,
|
||||
permissions: [{ action: "edit", resource: "/path/to/original/checkout/**", effect: "deny" }],
|
||||
})
|
||||
```
|
||||
|
||||
### Sessions
|
||||
|
||||
Create or read a session.
|
||||
@@ -1511,7 +1521,7 @@ manifest is:
|
||||
"./rpc": "./src/rpc.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "beta"
|
||||
"@opencode/plugin": "latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1521,9 +1531,8 @@ The `./rpc` export is optional; include it when publishing a shared
|
||||
without loading your implementation.
|
||||
|
||||
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.
|
||||
installed package, not only a workspace-linked copy. Publish a compatible
|
||||
plugin update when you adopt a newer API contract.
|
||||
|
||||
## Support V1
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Use `@opencode/sdk/workerd` inside a Cloudflare Durable Object. This profile use
|
||||
persists durable events for eviction recovery, and replaces unavailable local filesystem and process services.
|
||||
|
||||
```sh
|
||||
bun add @opencode/sdk@dev
|
||||
bun add @opencode/sdk@beta
|
||||
```
|
||||
|
||||
Hold one host for the lifetime of the Durable Object instance instead of creating one for every request.
|
||||
|
||||
@@ -6,7 +6,7 @@ title: "Effect"
|
||||
the owning Scope releases the router, Location services, fibers, and plugin registrations.
|
||||
|
||||
```sh
|
||||
bun add @opencode/sdk@dev effect
|
||||
bun add @opencode/sdk@beta effect
|
||||
```
|
||||
|
||||
## Create a host
|
||||
|
||||
@@ -9,10 +9,11 @@ network hop between the client and server.
|
||||
|
||||
For Cloudflare Durable Objects, see the [Cloudflare guide](/build/sdk/cloudflare).
|
||||
|
||||
<Callout type="warning">
|
||||
The V2 SDK is beta. Install the current preview with `bun add @opencode/sdk@dev`; its API may change before a
|
||||
stable release.
|
||||
</Callout>
|
||||
Install the SDK:
|
||||
|
||||
```sh
|
||||
bun add @opencode/sdk@beta
|
||||
```
|
||||
|
||||
## Create a host
|
||||
|
||||
|
||||
@@ -0,0 +1,548 @@
|
||||
---
|
||||
title: "Commands"
|
||||
description: "Reference for the opencode2 command line."
|
||||
---
|
||||
|
||||
Every command accepts `--help` for its full flag list, for example `opencode2 run --help`. Commands that talk to a server also accept `--standalone` to run a private server and `--server <url>` to target a specific one.
|
||||
|
||||
## run
|
||||
|
||||
`opencode2 run` sends a message and prints the reply without opening the interactive interface.
|
||||
|
||||
```bash
|
||||
$ opencode2 run "Explain this repository"
|
||||
```
|
||||
|
||||
Choose a model.
|
||||
|
||||
```bash
|
||||
$ opencode2 run --model anthropic/claude-sonnet-4-5 "Refactor parseToken"
|
||||
```
|
||||
|
||||
Continue the last session.
|
||||
|
||||
```bash
|
||||
$ opencode2 run --continue "Now handle the expired case"
|
||||
```
|
||||
|
||||
Emit newline-delimited JSON for scripts.
|
||||
|
||||
```bash
|
||||
$ opencode2 run --format json "List the TODO comments"
|
||||
```
|
||||
|
||||
Attach files to the message.
|
||||
|
||||
```bash
|
||||
$ opencode2 run --file src/server.ts --file src/client.ts "Review these for bugs"
|
||||
```
|
||||
|
||||
Run with a specific agent.
|
||||
|
||||
```bash
|
||||
$ opencode2 run --agent build "Fix the failing test"
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 run --help
|
||||
```
|
||||
|
||||
## mini
|
||||
|
||||
`opencode2 mini` starts the minimal interactive interface instead of the full-screen TUI.
|
||||
|
||||
```bash
|
||||
$ opencode2 mini
|
||||
```
|
||||
|
||||
Continue the last session.
|
||||
|
||||
```bash
|
||||
$ opencode2 mini --continue
|
||||
```
|
||||
|
||||
Start with a model and an initial prompt.
|
||||
|
||||
```bash
|
||||
$ opencode2 mini --model anthropic/claude-sonnet-4-5 --prompt "Summarize this repository"
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 mini --help
|
||||
```
|
||||
|
||||
## session
|
||||
|
||||
`opencode2 session` manages sessions.
|
||||
|
||||
```bash
|
||||
$ opencode2 session list
|
||||
```
|
||||
|
||||
Limit the list and print JSON.
|
||||
|
||||
```bash
|
||||
$ opencode2 session list --max-count 20 --format json
|
||||
```
|
||||
|
||||
Delete a session and its child sessions.
|
||||
|
||||
```bash
|
||||
$ opencode2 session delete ses_9c1b08
|
||||
```
|
||||
|
||||
Export session data as JSON.
|
||||
|
||||
```bash
|
||||
$ opencode2 session export ses_4f2a1c
|
||||
```
|
||||
|
||||
Redact sensitive transcript and file data when exporting.
|
||||
|
||||
```bash
|
||||
$ opencode2 session export ses_4f2a1c --sanitize
|
||||
```
|
||||
|
||||
Import session data from a JSON file or URL.
|
||||
|
||||
```bash
|
||||
$ opencode2 session import session.json
|
||||
```
|
||||
|
||||
Import into a specific directory.
|
||||
|
||||
```bash
|
||||
$ opencode2 session import session.json --directory ~/code/project
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 session --help
|
||||
```
|
||||
|
||||
## auth
|
||||
|
||||
`opencode2 auth` manages AI providers and credentials.
|
||||
|
||||
```bash
|
||||
$ opencode2 auth list
|
||||
```
|
||||
|
||||
List them as JSON.
|
||||
|
||||
```bash
|
||||
$ opencode2 auth list --format json
|
||||
```
|
||||
|
||||
Log in to a provider.
|
||||
|
||||
```bash
|
||||
$ opencode2 auth login anthropic
|
||||
```
|
||||
|
||||
Log in with a specific authentication method.
|
||||
|
||||
```bash
|
||||
$ opencode2 auth login anthropic --method api-key
|
||||
```
|
||||
|
||||
Log out of a saved account.
|
||||
|
||||
```bash
|
||||
$ opencode2 auth logout anthropic work
|
||||
```
|
||||
|
||||
Switch the active account for an integration.
|
||||
|
||||
```bash
|
||||
$ opencode2 auth switch anthropic work
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 auth --help
|
||||
```
|
||||
|
||||
## models
|
||||
|
||||
`opencode2 models` lists every available model.
|
||||
|
||||
```bash
|
||||
$ opencode2 models
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 models --help
|
||||
```
|
||||
|
||||
## mcp
|
||||
|
||||
`opencode2 mcp` manages MCP (Model Context Protocol) servers.
|
||||
|
||||
```bash
|
||||
$ opencode2 mcp list
|
||||
```
|
||||
|
||||
Add a remote server.
|
||||
|
||||
```bash
|
||||
$ opencode2 mcp add context7 --url https://mcp.context7.com/mcp
|
||||
```
|
||||
|
||||
Add a local server to the global config.
|
||||
|
||||
```bash
|
||||
$ opencode2 mcp add everything --global -- npx -y @modelcontextprotocol/server-everything
|
||||
```
|
||||
|
||||
Add a local server with an environment variable.
|
||||
|
||||
```bash
|
||||
$ opencode2 mcp add everything --env LOG_LEVEL=debug -- npx -y @modelcontextprotocol/server-everything
|
||||
```
|
||||
|
||||
Add a remote server with a header.
|
||||
|
||||
```bash
|
||||
$ opencode2 mcp add context7 --url https://mcp.context7.com/mcp --header CONTEXT7_API_KEY=secret
|
||||
```
|
||||
|
||||
Authenticate with an OAuth-capable remote server.
|
||||
|
||||
```bash
|
||||
$ opencode2 mcp auth sentry
|
||||
```
|
||||
|
||||
Remove stored OAuth credentials for a server.
|
||||
|
||||
```bash
|
||||
$ opencode2 mcp logout sentry
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 mcp --help
|
||||
```
|
||||
|
||||
## plugin
|
||||
|
||||
`opencode2 plugin` manages plugins.
|
||||
|
||||
```bash
|
||||
$ opencode2 plugin list
|
||||
```
|
||||
|
||||
Include built-in server plugins.
|
||||
|
||||
```bash
|
||||
$ opencode2 plugin list --builtin
|
||||
```
|
||||
|
||||
Install a plugin and add it to the global configuration.
|
||||
|
||||
```bash
|
||||
$ opencode2 plugin add @example/opencode-tui
|
||||
```
|
||||
|
||||
Check package plugins for updates.
|
||||
|
||||
```bash
|
||||
$ opencode2 plugin check
|
||||
```
|
||||
|
||||
Update package plugins.
|
||||
|
||||
```bash
|
||||
$ opencode2 plugin update
|
||||
```
|
||||
|
||||
Remove a plugin from global configuration.
|
||||
|
||||
```bash
|
||||
$ opencode2 plugin remove @example/opencode-tui
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 plugin --help
|
||||
```
|
||||
|
||||
## stats
|
||||
|
||||
`opencode2 stats` shows shareable usage statistics.
|
||||
|
||||
```bash
|
||||
$ opencode2 stats
|
||||
```
|
||||
|
||||
Show the last 7 days.
|
||||
|
||||
```bash
|
||||
$ opencode2 stats --days 7
|
||||
```
|
||||
|
||||
Show model usage.
|
||||
|
||||
```bash
|
||||
$ opencode2 stats --models
|
||||
```
|
||||
|
||||
Show cost and token details.
|
||||
|
||||
```bash
|
||||
$ opencode2 stats --cost
|
||||
```
|
||||
|
||||
Print JSON instead of a report.
|
||||
|
||||
```bash
|
||||
$ opencode2 stats --json
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 stats --help
|
||||
```
|
||||
|
||||
## serve
|
||||
|
||||
`opencode2 serve` starts the API and web server. See [Web](/cli/web).
|
||||
|
||||
```bash
|
||||
$ opencode2 serve
|
||||
```
|
||||
|
||||
Bind to all interfaces on a fixed port.
|
||||
|
||||
```bash
|
||||
$ opencode2 serve --hostname 0.0.0.0 --port 4096
|
||||
```
|
||||
|
||||
Allow a browser client from another origin.
|
||||
|
||||
```bash
|
||||
$ opencode2 serve --cors https://app.example.com
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 serve --help
|
||||
```
|
||||
|
||||
## pair
|
||||
|
||||
`opencode2 pair` shows server pairing information, including URLs, credentials, and a QR code.
|
||||
|
||||
```bash
|
||||
$ opencode2 pair
|
||||
```
|
||||
|
||||
Advertise an external URL in the QR code.
|
||||
|
||||
```bash
|
||||
$ opencode2 pair --url https://dev.example.com
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 pair --help
|
||||
```
|
||||
|
||||
## service
|
||||
|
||||
`opencode2 service` manages the background server. See [Web](/cli/web).
|
||||
|
||||
```bash
|
||||
$ opencode2 service start
|
||||
```
|
||||
|
||||
Restart it.
|
||||
|
||||
```bash
|
||||
$ opencode2 service restart
|
||||
```
|
||||
|
||||
Show its status.
|
||||
|
||||
```bash
|
||||
$ opencode2 service status
|
||||
```
|
||||
|
||||
Stop it.
|
||||
|
||||
```bash
|
||||
$ opencode2 service stop
|
||||
```
|
||||
|
||||
Read a setting.
|
||||
|
||||
```bash
|
||||
$ opencode2 service get hostname
|
||||
```
|
||||
|
||||
Set a setting.
|
||||
|
||||
```bash
|
||||
$ opencode2 service set hostname 0.0.0.0
|
||||
```
|
||||
|
||||
Allow an extra CORS origin.
|
||||
|
||||
```bash
|
||||
$ opencode2 service set cors https://app.example.com
|
||||
```
|
||||
|
||||
Pass an environment variable to the server process.
|
||||
|
||||
```bash
|
||||
$ opencode2 service set env OPENCODE_LOG_LEVEL DEBUG
|
||||
```
|
||||
|
||||
Reset a setting to its default.
|
||||
|
||||
```bash
|
||||
$ opencode2 service unset hostname
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 service --help
|
||||
```
|
||||
|
||||
## api
|
||||
|
||||
`opencode2 api` makes a request to the running server.
|
||||
|
||||
```bash
|
||||
$ opencode2 api GET /api/session
|
||||
```
|
||||
|
||||
Call an operation ID with a query parameter.
|
||||
|
||||
```bash
|
||||
$ opencode2 api v2.session.list --param limit=10
|
||||
```
|
||||
|
||||
Send a JSON body.
|
||||
|
||||
```bash
|
||||
$ opencode2 api v2.session.create --data '{"title": "New session"}'
|
||||
```
|
||||
|
||||
Add a request header.
|
||||
|
||||
```bash
|
||||
$ opencode2 api GET /api/session -H "accept: application/json"
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 api --help
|
||||
```
|
||||
|
||||
## acp
|
||||
|
||||
`opencode2 acp` starts an Agent Client Protocol server over stdin and stdout for editor integrations. It runs until the client closes the connection.
|
||||
|
||||
```bash
|
||||
$ opencode2 acp
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 acp --help
|
||||
```
|
||||
|
||||
## debug
|
||||
|
||||
`opencode2 debug` provides debugging and troubleshooting tools.
|
||||
|
||||
```bash
|
||||
$ opencode2 debug agents
|
||||
```
|
||||
|
||||
List configuration sources.
|
||||
|
||||
```bash
|
||||
$ opencode2 debug config
|
||||
```
|
||||
|
||||
Show global paths.
|
||||
|
||||
```bash
|
||||
$ opencode2 debug paths
|
||||
```
|
||||
|
||||
Print a single path.
|
||||
|
||||
```bash
|
||||
$ opencode2 debug paths db
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 debug --help
|
||||
```
|
||||
|
||||
## upgrade
|
||||
|
||||
`opencode2 upgrade` upgrades OpenCode to the latest or a specific version. Alias: `update`.
|
||||
|
||||
```bash
|
||||
$ opencode2 upgrade
|
||||
```
|
||||
|
||||
Upgrade to a specific version with a specific package manager.
|
||||
|
||||
```bash
|
||||
$ opencode2 upgrade 1.18.15 --method bun
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 upgrade --help
|
||||
```
|
||||
|
||||
## uninstall
|
||||
|
||||
`opencode2 uninstall` removes OpenCode and all related files.
|
||||
|
||||
```bash
|
||||
$ opencode2 uninstall
|
||||
```
|
||||
|
||||
Preview what would be removed.
|
||||
|
||||
```bash
|
||||
$ opencode2 uninstall --dry-run
|
||||
```
|
||||
|
||||
Keep configuration and session data.
|
||||
|
||||
```bash
|
||||
$ opencode2 uninstall --keep-config --keep-data
|
||||
```
|
||||
|
||||
View all subcommands and flags.
|
||||
|
||||
```bash
|
||||
$ opencode2 uninstall --help
|
||||
```
|
||||
@@ -1,3 +1,140 @@
|
||||
---
|
||||
title: "Providers"
|
||||
description: "Connect provider accounts and manage credentials from the CLI and TUI."
|
||||
---
|
||||
|
||||
Connect a provider in the TUI, then choose one of its models:
|
||||
|
||||
```text
|
||||
/connect
|
||||
/models
|
||||
```
|
||||
|
||||
`/connect` lists the integrations available from the current server and project. Select a provider, choose an
|
||||
authentication method when it offers more than one, and follow the prompts.
|
||||
|
||||
## TUI
|
||||
|
||||
The TUI supports API keys, OAuth, and provider authentication commands. OAuth opens an authorization URL or shows a
|
||||
code to enter; press `o` to open the URL and `c` to copy the authorization details.
|
||||
|
||||
```text
|
||||
/connect
|
||||
# Select OpenAI, then ChatGPT Pro/Plus (headless).
|
||||
```
|
||||
|
||||
Run `/connect` again to add another account. Selecting an already connected provider opens its account list, where you
|
||||
can activate, rename, or delete a saved account.
|
||||
|
||||
## CLI
|
||||
|
||||
Use `auth login` for the same provider methods without opening the TUI. With no provider argument, the command opens an
|
||||
interactive provider picker.
|
||||
|
||||
```bash
|
||||
opencode2 auth login
|
||||
```
|
||||
|
||||
Pass an integration ID or name to skip the first picker. Use `--method key` to select API-key entry explicitly.
|
||||
|
||||
```bash
|
||||
opencode2 auth login anthropic --method key
|
||||
```
|
||||
|
||||
Method IDs are provider-specific. Run the command without `--method` to see the available methods when a provider has
|
||||
more than one.
|
||||
|
||||
```bash
|
||||
opencode2 auth login openai
|
||||
```
|
||||
|
||||
API-key entry and provider forms require an interactive terminal. OAuth methods that ask you to paste an authorization
|
||||
code also require one.
|
||||
|
||||
## Methods
|
||||
|
||||
OpenCode receives provider credentials through four integration methods:
|
||||
|
||||
| Method | Behavior |
|
||||
| --- | --- |
|
||||
| API key | Prompts for a secret and saves it as a provider account. |
|
||||
| OAuth | Provides a browser URL, device code, or authorization-code prompt, then saves tokens and refreshes them when supported. |
|
||||
| Command | Runs a provider-supplied authentication command and saves its standard output as a key. |
|
||||
| Environment | Reads a supported variable from the server process without saving it. |
|
||||
|
||||
Providers can add forms to API-key and OAuth methods for required details such as an Azure resource name or a GitHub
|
||||
Enterprise domain. The CLI and TUI render those forms before starting authentication.
|
||||
|
||||
## Environment
|
||||
|
||||
Set a provider's supported environment variable on the server process that runs model requests. For a one-off private
|
||||
server, pass it when starting standalone mode.
|
||||
|
||||
```bash
|
||||
ANTHROPIC_API_KEY=sk-ant-... opencode2 --standalone
|
||||
```
|
||||
|
||||
For the shared background server, add the variable to its managed environment. This stops a running service; the next
|
||||
OpenCode command starts it with the new value.
|
||||
|
||||
```bash
|
||||
opencode2 service set env ANTHROPIC_API_KEY sk-ant-...
|
||||
opencode2 auth list
|
||||
```
|
||||
|
||||
Environment connections appear in `auth list` with type `environment`. They are not accounts: `auth logout` cannot
|
||||
remove them, so unset the variable to disconnect. A saved account takes precedence over an environment connection for
|
||||
the same integration.
|
||||
|
||||
```bash
|
||||
opencode2 auth list
|
||||
opencode2 service unset env ANTHROPIC_API_KEY
|
||||
```
|
||||
|
||||
Some cloud providers also use their native ambient credential chain instead of an API-key variable:
|
||||
|
||||
- Amazon Bedrock supports the AWS default credential chain, including profiles, access-key environments, web identity,
|
||||
and container credentials. It also supports `AWS_BEARER_TOKEN_BEDROCK`.
|
||||
- Google Vertex uses Application Default Credentials and a resolvable project. For example, authenticate with
|
||||
`gcloud auth application-default login` and set `GOOGLE_CLOUD_PROJECT`.
|
||||
- Azure exposes **Microsoft Entra ID (Azure CLI)** as a connect method when `az` is installed. Run `az login` first, then
|
||||
select that method in `/connect` or `auth login azure`.
|
||||
|
||||
```bash
|
||||
gcloud auth application-default login
|
||||
GOOGLE_CLOUD_PROJECT=my-project opencode2
|
||||
```
|
||||
|
||||
See [Providers](/providers) for provider-specific and server-side setup.
|
||||
|
||||
## Accounts
|
||||
|
||||
Each successful API-key, OAuth, or command login creates a saved account. The newest account becomes active; switch the
|
||||
active account by its label or credential ID.
|
||||
|
||||
```bash
|
||||
opencode2 auth list
|
||||
opencode2 auth switch anthropic work
|
||||
```
|
||||
|
||||
Remove a saved account with `auth logout`. Both commands open pickers when their arguments are omitted.
|
||||
|
||||
```bash
|
||||
opencode2 auth logout anthropic work
|
||||
```
|
||||
|
||||
In the TUI, `/connect` provides the same add, activate, rename, and delete operations for saved accounts.
|
||||
|
||||
## Storage
|
||||
|
||||
Saved API keys and OAuth tokens live in the server's SQLite database. For the local server, print its database path with:
|
||||
|
||||
```bash
|
||||
opencode2 debug paths db
|
||||
```
|
||||
|
||||
The usual release path is `~/.local/share/opencode/opencode.db`; `XDG_DATA_HOME`, the release channel, and `OPENCODE_DB`
|
||||
can change it. Do not edit the database to manage credentials; use `/connect` or the `auth` commands.
|
||||
|
||||
V2 imports supported credentials from the legacy `auth.json` in the OpenCode data directory during its database
|
||||
migration. New and updated credentials are stored in SQLite rather than written back to that file.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: "Web"
|
||||
description: "Run OpenCode in the browser."
|
||||
---
|
||||
|
||||
OpenCode ships with a web ui that is served from the same server that powers the
|
||||
TUI. It's available by default and password protected.
|
||||
|
||||
## Access
|
||||
|
||||
```bash
|
||||
$ opencode2 pair
|
||||
|
||||
URLs http://127.0.0.1:49374
|
||||
Username opencode
|
||||
Password ********
|
||||
```
|
||||
|
||||
By default the server runs on port 49374 and listens only on localhost. You can
|
||||
change this config with the `opencode2 service` command.
|
||||
|
||||
## Configure
|
||||
|
||||
Set any option with `opencode2 service set`:
|
||||
|
||||
```bash
|
||||
# Listen on every network interface
|
||||
$ opencode2 service set hostname 0.0.0.0
|
||||
|
||||
# Use a fixed port instead of the channel default
|
||||
$ opencode2 service set port 49374
|
||||
|
||||
# Replace the generated password
|
||||
$ opencode2 service set password "a-long-secret"
|
||||
|
||||
# Allow a web client served from another origin
|
||||
$ opencode2 service set cors https://app.example.com,https://other.example.com
|
||||
|
||||
# Pass an environment variable to the server process
|
||||
$ opencode2 service set env OPENCODE_LOG_LEVEL DEBUG
|
||||
```
|
||||
|
||||
Changing a setting stops the background server. To apply the new config
|
||||
|
||||
```bash
|
||||
$ opencode2 service start
|
||||
```
|
||||
|
||||
## Standalone
|
||||
|
||||
`opencode2 serve` runs the same server in the foreground instead of through the
|
||||
shared background service.
|
||||
|
||||
```bash
|
||||
$ opencode2 serve --hostname 0.0.0.0 --port 4096
|
||||
server listening on http://0.0.0.0:4096
|
||||
server password <password>
|
||||
```
|
||||
|
||||
Use it when you want to:
|
||||
|
||||
- Run OpenCode on a shared, always-on, or remote host, then connect clients with
|
||||
`opencode2 --server <url>`.
|
||||
- Control the hostname, port, and CORS origins for a single process.
|
||||
- Run under a supervisor like systemd, Docker, or another environment that expects
|
||||
a foreground process.
|
||||
- Keep a dedicated server instead of the shared background service.
|
||||
|
||||
|
||||
Connect a client to it with `--server`:
|
||||
|
||||
```bash
|
||||
$ opencode2 --server http://127.0.0.1:4096
|
||||
```
|
||||
@@ -2,52 +2,62 @@
|
||||
title: "Commands"
|
||||
---
|
||||
|
||||
Custom commands turn a named prompt template into a reusable command.
|
||||
Create `.opencode/commands/review.md` to turn a prompt into `/review`:
|
||||
|
||||
## Configure with Markdown
|
||||
|
||||
OpenCode discovers `.md` command files in `commands/` directories:
|
||||
|
||||
```text
|
||||
~/.config/opencode/commands/ # Global
|
||||
.opencode/commands/ # Project
|
||||
```md title=".opencode/commands/review.md"
|
||||
Review $ARGUMENTS for bugs and missing tests.
|
||||
```
|
||||
|
||||
Files may be nested; for example, `.opencode/commands/team/review.md` defines
|
||||
the command `team/review`. Files with other extensions, including `.mdx`, are not
|
||||
discovered.
|
||||
Run it from the TUI with a target:
|
||||
|
||||
```text
|
||||
/review src/auth.ts
|
||||
```
|
||||
|
||||
OpenCode submits `Review src/auth.ts for bugs and missing tests.` as a user prompt.
|
||||
|
||||
## Markdown
|
||||
|
||||
Put global commands in `~/.config/opencode/commands/` and project commands in `.opencode/commands/`.
|
||||
|
||||
```text
|
||||
~/.config/opencode/commands/review.md
|
||||
.opencode/commands/review.md
|
||||
```
|
||||
|
||||
Only `.md` files are discovered. Nested paths become command names with `/` separators:
|
||||
|
||||
```md title=".opencode/commands/team/review.md"
|
||||
Review $ARGUMENTS using the team's checklist.
|
||||
```
|
||||
|
||||
Run the nested command as `/team/review src/auth.ts`. The legacy singular directories `command/` are also discovered,
|
||||
but use `commands/` for new files.
|
||||
|
||||
Add YAML frontmatter when the command needs metadata. The trimmed Markdown body is always the prompt template.
|
||||
|
||||
```md title=".opencode/commands/review.md"
|
||||
---
|
||||
description: Review code for correctness and missing tests
|
||||
description: Review code for correctness
|
||||
agent: plan
|
||||
model: anthropic/claude-sonnet-4-5#high
|
||||
---
|
||||
|
||||
Review $ARGUMENTS. Report bugs first, then missing tests.
|
||||
Review $ARGUMENTS. Report bugs first.
|
||||
```
|
||||
|
||||
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.
|
||||
## JSON
|
||||
|
||||
## Configure with JSON
|
||||
|
||||
Add commands under the `commands` key in any OpenCode JSON or JSONC
|
||||
[configuration file](/config). Each entry's key is the command name and
|
||||
`template` is required.
|
||||
Define commands under `commands` in any OpenCode JSON or JSONC [configuration file](/config). Each command requires a
|
||||
`template`.
|
||||
|
||||
```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",
|
||||
"description": "Review code for correctness",
|
||||
"template": "Review $ARGUMENTS. Report bugs first.",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -55,107 +65,196 @@ Add commands under the `commands` key in any OpenCode JSON or JSONC
|
||||
|
||||
## 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 command listings and discovery. |
|
||||
| `agent` | No | Agent that runs the command. |
|
||||
| `model` | No | Model override in `provider/model` or `provider/model#variant` format. |
|
||||
| `subagent` | No | Run in a background child session, or use `false` to stay in the current session. |
|
||||
| `subtask` | No | Deprecated alias for `subagent`. |
|
||||
Markdown frontmatter and JSON entries accept the same fields, except that Markdown gets `template` from the file body.
|
||||
|
||||
The optional fields can be used in JSON or YAML frontmatter. Do not put
|
||||
`template` in frontmatter because the Markdown body always supplies it.
|
||||
| Field | Required | Behavior |
|
||||
| --- | --- | --- |
|
||||
| `template` | JSON only | Prompt template. |
|
||||
| `description` | No | Text shown in command lists and discovery. |
|
||||
| `agent` | No | Agent selected when the command runs. |
|
||||
| `model` | No | Model override in `provider/model` or `provider/model#variant` format. |
|
||||
| `subagent` | No | `true` runs in a background child session; `false` forces the current session. |
|
||||
| `subtask` | No | Deprecated alias for `subagent`. |
|
||||
|
||||
For example, this JSON command sets every current optional field:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"commands": {
|
||||
"audit": {
|
||||
"template": "Audit $ARGUMENTS.",
|
||||
"description": "Audit a package",
|
||||
"agent": "general",
|
||||
"model": "anthropic/claude-sonnet-4-5#high",
|
||||
"subagent": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Do not put `template` in Markdown frontmatter; the body supplies it.
|
||||
|
||||
## Arguments
|
||||
|
||||
Use `$ARGUMENTS` for the complete argument string:
|
||||
Use `$ARGUMENTS` for the complete argument string exactly as entered.
|
||||
|
||||
```md title=".opencode/commands/component.md"
|
||||
---
|
||||
description: Create a component
|
||||
---
|
||||
|
||||
Create a typed React component named $ARGUMENTS.
|
||||
```
|
||||
|
||||
Use `$1`, `$2`, and higher numbers for parsed positional arguments. Single and
|
||||
double quotes group text containing spaces and are removed during parsing.
|
||||
For `/component Account Settings`, the placeholder becomes `Account Settings`.
|
||||
|
||||
## Positions
|
||||
|
||||
Use `$1`, `$2`, and higher numbers for parsed positional arguments. Single or double quotes group words and are removed.
|
||||
|
||||
```md title=".opencode/commands/check.md"
|
||||
---
|
||||
description: Check one area with a specific focus
|
||||
---
|
||||
|
||||
Check $1. Focus on $2.
|
||||
```
|
||||
|
||||
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.
|
||||
For `/check src/auth.ts "error handling"`, `$1` becomes `src/auth.ts` and `$2` becomes `error handling`.
|
||||
|
||||
If a template contains neither positional placeholders nor `$ARGUMENTS`,
|
||||
OpenCode appends non-empty arguments to the template after a blank line.
|
||||
The highest-numbered placeholder consumes its argument and everything after it. Missing positions become empty strings.
|
||||
|
||||
## Shell interpolation
|
||||
```md title=".opencode/commands/compare.md"
|
||||
Compare $1 with $2.
|
||||
```
|
||||
|
||||
Wrap a shell command in `!` followed by backticks to insert its output before
|
||||
the prompt is submitted:
|
||||
For `/compare api stable branch`, OpenCode submits `Compare api with stable branch.`
|
||||
|
||||
## Fallback
|
||||
|
||||
When a template has no `$ARGUMENTS` or positional placeholder, OpenCode appends non-empty arguments after a blank line.
|
||||
|
||||
```md title=".opencode/commands/explain.md"
|
||||
Explain this code clearly.
|
||||
```
|
||||
|
||||
For `/explain src/cache.ts`, the prompt becomes:
|
||||
|
||||
```text
|
||||
Explain this code clearly.
|
||||
|
||||
src/cache.ts
|
||||
```
|
||||
|
||||
## Shell
|
||||
|
||||
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.
|
||||
OpenCode runs each block with the configured shell in the active project location and inserts its combined output.
|
||||
Argument placeholders are expanded first:
|
||||
|
||||
```md title=".opencode/commands/history.md"
|
||||
Summarize these commits:
|
||||
|
||||
!`git log --oneline -$1`
|
||||
```
|
||||
|
||||
For `/history 5`, the shell receives `git log --oneline -5`. Do not place untrusted arguments inside shell blocks.
|
||||
|
||||
<Callout type="warning">
|
||||
Shell interpolations run when the command is evaluated, outside the agent's tool permission flow. Only use commands
|
||||
from sources you trust.
|
||||
Shell blocks run when OpenCode evaluates the command, outside the agent's tool permission flow. Only use commands from
|
||||
sources you trust.
|
||||
</Callout>
|
||||
|
||||
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.
|
||||
## Attachments
|
||||
|
||||
## Agent, model, and execution
|
||||
Stored templates do not expand `@path`; it remains ordinary prompt text.
|
||||
|
||||
Commands evaluate their arguments and shell blocks before submitting a durable
|
||||
user prompt. Commands run in the current session unless background delegation
|
||||
is enabled as described below.
|
||||
```md title=".opencode/commands/readme.md"
|
||||
Review @README.md.
|
||||
```
|
||||
|
||||
For current-session commands, `agent` overrides the active agent when the command is 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 active at invocation.
|
||||
To attach a file, add it through the composer when invoking the command. OpenCode preserves those composer attachments
|
||||
when it submits the expanded prompt.
|
||||
|
||||
### Background subagents
|
||||
## Execution
|
||||
|
||||
Set `subagent: true` to run a command in a background child session. The parent
|
||||
keeps its agent and model, stays available for other work, and receives the
|
||||
child's result or failure when it finishes.
|
||||
Commands expand arguments and shell blocks before submitting a durable user prompt. They run in the current session by
|
||||
default.
|
||||
|
||||
```md title=".opencode/commands/review.md"
|
||||
```md title=".opencode/commands/plan.md"
|
||||
---
|
||||
description: Review changes in the background
|
||||
agent: plan
|
||||
---
|
||||
|
||||
Plan $ARGUMENTS.
|
||||
```
|
||||
|
||||
Running `/plan migration` switches the current session to `plan` before submitting the prompt.
|
||||
|
||||
Model selection follows these rules:
|
||||
|
||||
1. A command `model` overrides every other model.
|
||||
2. Otherwise, the selected command agent's configured model overrides the model active at invocation.
|
||||
3. Otherwise, the current session model remains active.
|
||||
|
||||
For example, this command selects both its agent and an explicit model:
|
||||
|
||||
```md title=".opencode/commands/design.md"
|
||||
---
|
||||
agent: plan
|
||||
model: openai/gpt-5#high
|
||||
---
|
||||
|
||||
Design $ARGUMENTS.
|
||||
```
|
||||
|
||||
## Background
|
||||
|
||||
Set `subagent: true` to run a command in a background child session.
|
||||
|
||||
```md title=".opencode/commands/audit.md"
|
||||
---
|
||||
description: Audit changes
|
||||
agent: general
|
||||
subagent: true
|
||||
---
|
||||
|
||||
Review $ARGUMENTS for bugs and missing tests.
|
||||
Audit $ARGUMENTS for bugs and missing tests.
|
||||
```
|
||||
|
||||
- `true` forces child execution, including for an agent with `mode: primary`.
|
||||
- `false` forces execution in the current session.
|
||||
- When omitted, a command targeting an agent with `mode: subagent` runs in the background.
|
||||
- The child uses the command's model override, then the selected agent's model, then the parent's model.
|
||||
- Legacy `subtask` is still accepted in JSON and Markdown. If both fields are present, `subagent` takes precedence.
|
||||
The parent stays available and keeps its agent and model. OpenCode sends the child's result or failure back to the parent
|
||||
when the child finishes.
|
||||
|
||||
| Value | Behavior |
|
||||
| --- | --- |
|
||||
| `true` | Always use a child, even when the selected agent has `mode: primary`. |
|
||||
| `false` | Always use the current session, even when the selected agent has `mode: subagent`. |
|
||||
| Omitted | Use a child only when the selected agent has `mode: subagent`. |
|
||||
|
||||
The child uses the command model, then the selected agent's model, then the parent's model. Legacy `subtask` remains
|
||||
accepted; if both fields are present, `subagent` wins.
|
||||
|
||||
```yaml
|
||||
subagent: false
|
||||
subtask: true
|
||||
```
|
||||
|
||||
This example runs in the current session because `subagent` takes precedence.
|
||||
|
||||
## Loading
|
||||
|
||||
Markdown and JSON commands share one registry. Later sources replace earlier commands with the same name.
|
||||
|
||||
```text
|
||||
~/.config/opencode/commands/review.md # Lower priority
|
||||
.opencode/commands/review.md # Replaces the global command
|
||||
```
|
||||
|
||||
Project sources take precedence over global sources, and nearer project sources take precedence over ancestor sources.
|
||||
A custom definition can also replace an earlier built-in command. OpenCode reloads command files and configuration changes
|
||||
automatically.
|
||||
|
||||
```md title=".opencode/commands/review.md"
|
||||
Review only the staged changes: !`git diff --cached`
|
||||
```
|
||||
|
||||
Saving this file updates `/review` without restarting OpenCode.
|
||||
|
||||
@@ -2,54 +2,118 @@
|
||||
title: "Compaction"
|
||||
---
|
||||
|
||||
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 gives a long session more context space by replacing its older active
|
||||
context with a generated checkpoint. Earlier session messages remain stored,
|
||||
but future model requests start from the latest completed checkpoint.
|
||||
|
||||
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.
|
||||
## Start
|
||||
|
||||
## Automatic compaction
|
||||
Automatic compaction is enabled by default. This minimal configuration keeps
|
||||
about 15,000 tokens of recent conversation beside the generated summary:
|
||||
|
||||
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:
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"compaction": {
|
||||
"auto": true,
|
||||
"keep": { "tokens": 15000 },
|
||||
"buffer": 20000,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Continue using the session normally. When its active context approaches the
|
||||
model limit, OpenCode creates a checkpoint and retries the pending work with
|
||||
more room. Compaction is lossy, so increase `keep.tokens` when exact recent
|
||||
details matter.
|
||||
|
||||
## Manual
|
||||
|
||||
Request compaction when you want a checkpoint before the automatic threshold.
|
||||
The server endpoint works even for a short history:
|
||||
|
||||
```sh
|
||||
curl -X POST http://localhost:4096/api/session/ses_example/compact \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
OpenCode stores the request before waking the session. The response confirms
|
||||
admission; it does not wait for the summary. Wait for the session or follow
|
||||
`session.compaction.*` events to learn when it finishes. See the generated
|
||||
[API reference](/api) for the full operation.
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "msg_example",
|
||||
"type": "compaction",
|
||||
"sessionID": "ses_example",
|
||||
"timeCreated": 1789056000000,
|
||||
"payload": {},
|
||||
"delivery": "steer"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Manual requests follow these rules:
|
||||
|
||||
| Rule | Behavior |
|
||||
| --- | --- |
|
||||
| Timing | Runs at the next safe model-step boundary. By default it runs before pending steered or queued prompts, even when a prompt was submitted first. |
|
||||
| Repeats | Requests made while one is pending merge into the pending request. |
|
||||
| Failure | Success or failure clears the wait point so pending prompts can continue. |
|
||||
| Automation | Works when `compaction.auto` is `false`. |
|
||||
|
||||
Supply an optional message `id` to make an exact retry idempotent:
|
||||
|
||||
```sh
|
||||
curl -X POST http://localhost:4096/api/session/ses_example/compact \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"id":"msg_compact_once"}'
|
||||
```
|
||||
|
||||
Reusing that ID for another record returns a conflict.
|
||||
|
||||
## Automatic
|
||||
|
||||
Before each model call, OpenCode estimates the final size of the system prompt,
|
||||
messages, and advertised tools. It starts compaction at this ceiling:
|
||||
|
||||
```text
|
||||
estimated tokens >= min(input limit - buffer, context limit - max(output reserve, buffer))
|
||||
```
|
||||
|
||||
The estimate uses the latest model response's input usage plus output and newer
|
||||
content. Without usage, it estimates text, media, instructions, and tools locally.
|
||||
The output reserve is capped at 32,000 tokens; an absent input limit does not
|
||||
constrain the ceiling. Successful compaction rebuilds the request without promoting
|
||||
input again or spending another agent step.
|
||||
For example, with a 128,000-token input limit and the default 20,000-token
|
||||
buffer, the input-limit side of the ceiling is 108,000 tokens.
|
||||
|
||||
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
|
||||
only when `auto` is enabled. A second overflow after recovery is returned as an error.
|
||||
```text
|
||||
128,000 - 20,000 = 108,000
|
||||
```
|
||||
|
||||
## Manual compaction
|
||||
The estimate follows these rules:
|
||||
|
||||
Manual compaction is available through session interfaces. See the generated [API reference](/api) for the server
|
||||
operation.
|
||||
- The latest model response's input usage is the starting point; output and
|
||||
newer content are then added.
|
||||
- Without provider usage, OpenCode estimates text, media, instructions, and
|
||||
tools locally.
|
||||
- The reserved model output is capped at 32,000 tokens.
|
||||
- A model without an input limit is constrained by its context limit instead.
|
||||
- A successful checkpoint rebuilds the same pending model step. It does not
|
||||
promote the input again or spend another agent step.
|
||||
|
||||
A manual request is durably admitted and wakes the session runner. It can
|
||||
compact short histories that would not trigger automatic compaction. By default,
|
||||
compaction runs at the next safe step boundary before pending steered or queued
|
||||
prompts, even if they were submitted first. Repeated requests while one is pending
|
||||
coalesce into that pending request. Whether compaction completes or fails, the
|
||||
barrier is then settled so pending prompts can proceed.
|
||||
OpenCode also recognizes provider errors classified as context overflow. If no
|
||||
assistant output or other retry evidence was produced, it can compact and retry
|
||||
that model step once:
|
||||
|
||||
The server operation returns the admitted compaction input; it does not wait
|
||||
for summary generation. Clients can 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.
|
||||
```text
|
||||
model call → context overflow → compact → retry same step once
|
||||
```
|
||||
|
||||
## Configuration
|
||||
This recovery requires `compaction.auto: true`. A second overflow is returned
|
||||
as an error.
|
||||
|
||||
## Settings
|
||||
|
||||
Add `compaction` to any [OpenCode configuration file](/config):
|
||||
|
||||
@@ -57,30 +121,27 @@ Add `compaction` to any [OpenCode configuration file](/config):
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"compaction": {
|
||||
"auto": true,
|
||||
"keep": {
|
||||
"tokens": 15000,
|
||||
},
|
||||
"buffer": 20000,
|
||||
"auto": false,
|
||||
"keep": { "tokens": 24000 },
|
||||
"buffer": 16000,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Default | V2 behavior |
|
||||
| ------------- | ------: | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `auto` | `true` | Enables preflight context-size checks and one-shot provider-overflow recovery. Disabling it does not affect manual compaction. |
|
||||
| `keep.tokens` | `15000` | Approximate number of tokens from the newest serialized conversation context to retain beside the summary. |
|
||||
| `buffer` | `20000` | Safety reserve below an explicit input limit. Without one, it is the minimum context reserve and the model output allowance wins when larger. |
|
||||
| Field | Default | Behavior |
|
||||
| --- | ---: | --- |
|
||||
| `auto` | `true` | Enables preflight checks and one provider-overflow recovery attempt. It does not control manual compaction. |
|
||||
| `keep.tokens` | `15000` | Approximate recent serialized context retained beside a local summary, or real user input retained for a provider checkpoint. |
|
||||
| `buffer` | `20000` | Safety margin below an explicit input limit. Without one, it is the minimum context reserve; a larger model output allowance wins. |
|
||||
|
||||
`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.
|
||||
`keep.tokens` and `buffer` accept non-negative integers. Larger
|
||||
`keep.tokens` preserves more recent detail but leaves less room for new work;
|
||||
larger `buffer` starts automatic compaction earlier.
|
||||
|
||||
## Provider compaction
|
||||
## Providers
|
||||
|
||||
By default, compaction generates a local text summary. To use the selected
|
||||
provider's native compaction operation for automatic and manual requests, set a
|
||||
provider policy. An individual model's policy replaces the entire provider policy:
|
||||
Local text summaries are the default. Use a provider policy to request the
|
||||
selected provider's native compaction for both automatic and manual requests:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -97,94 +158,140 @@ provider policy. An individual model's policy replaces the entire provider polic
|
||||
}
|
||||
```
|
||||
|
||||
- `threshold` is an optional positive integer in provider mode. Omit it to use the
|
||||
selected model's usable input ceiling above. A configured threshold is clamped
|
||||
to that ceiling. In this example, `gpt-5.4-mini` uses its own ceiling, not 120,000.
|
||||
- Scheduling uses the normal safe session step boundaries, not in-band provider
|
||||
context management. `compaction.auto: false` disables all new automatic work.
|
||||
- After installing a native checkpoint, automatic checks wait for a fresh model
|
||||
usage anchor. Encrypted checkpoint bytes are not a meaningful token count.
|
||||
- OpenAI Responses uses a streamed compaction trigger when the route supports it.
|
||||
Endpoint-only routes use their standalone compaction endpoint. Deployment/model
|
||||
support can vary. Transient provider failures retry under the same session retry
|
||||
policy and plugin hook as other requests; nothing is installed until a checkpoint
|
||||
is returned.
|
||||
- A known automatic context overflow uses local recovery over the durable original
|
||||
history, re-expanding native checkpoints. This applies both to ordinary model
|
||||
calls and native compaction rejection. If local recovery fails, the prior
|
||||
checkpoint remains intact and the error surfaces. Authentication, rate limits,
|
||||
cancellation, and other failures do not trigger local fallback. Manual native
|
||||
compaction also surfaces errors without fallback.
|
||||
- Unsupported routes are rejected during model resolution. Configure custom
|
||||
endpoints through provider/model `settings.baseURL`, not a `model.request` hook;
|
||||
native compaction rejects endpoint rewrites by that hook.
|
||||
- Trigger checkpoints retain whole, real user messages and attachments up to the
|
||||
`compaction.tokens` budget, including users retained across earlier native
|
||||
compactions. Synthetic guidance is not retained as user input. Endpoint results
|
||||
are stored as the provider returned them. Neither path fabricates a text summary.
|
||||
Keep `threshold` comfortably above `tokens` plus the system prompt and tools, or
|
||||
every step will compact again as soon as the next response reports usage.
|
||||
- Successful native checkpoints advance the instruction epoch and are replayed
|
||||
only with a matching provider, model, protocol, and endpoint. Switching to an
|
||||
incompatible route reuses an earlier compatible checkpoint or retained transcript.
|
||||
Disabling automatic compaction does not remove an installed checkpoint.
|
||||
A model policy replaces the entire provider policy. In this example,
|
||||
`gpt-5.4-mini` therefore uses its own usable input ceiling rather than inheriting
|
||||
the provider's `120000` threshold.
|
||||
|
||||
## Local checkpoint contents
|
||||
| Topic | Provider behavior |
|
||||
| --- | --- |
|
||||
| Threshold | `threshold` is an optional positive integer in provider mode. When omitted, OpenCode uses the model's usable input ceiling; when larger than that ceiling, it is clamped. |
|
||||
| Scheduling | Checkpoints run at normal safe step boundaries. `compaction.auto: false` disables all new automatic work. |
|
||||
| Usage | After a native checkpoint is installed, automatic checks wait for fresh model usage because encrypted checkpoint bytes cannot provide a meaningful token count. |
|
||||
| OpenAI | Responses routes use a streamed compaction trigger when supported; endpoint-only routes use the standalone compaction endpoint. Deployment and model support vary. |
|
||||
| Retries | Transient provider failures use the normal session retry policy and plugin hook. Nothing is installed until the provider returns a checkpoint. |
|
||||
| Routes | Unsupported routes fail during model resolution. Configure custom endpoints with provider or model `settings.baseURL`. Native compaction rejects endpoint rewrites made by a `model.request` hook. |
|
||||
| Replay | Native checkpoints are reused only with the same provider, model, protocol, and endpoint. An incompatible route falls back to an earlier compatible checkpoint or retained transcript. |
|
||||
|
||||
V2 uses the session's selected agent, model, and variant to generate the summary.
|
||||
The request reuses the normal instructions, tool definitions, and structured
|
||||
history prefix, then appends a user message requesting a checkpoint. Context
|
||||
hooks run as they do for normal session requests.
|
||||
Choose a threshold comfortably above `keep.tokens` plus the system prompt and
|
||||
tools. For example, avoid a 20,000-token threshold when the retained input alone
|
||||
is 15,000 tokens and the prompt and tools require another 8,000:
|
||||
|
||||
Compaction does not dispatch local tool calls or override tool choice. The
|
||||
summary must contain at least one heading from the requested template, such as
|
||||
`## Objective`. If it does not, V2 makes one additional request asking the model
|
||||
to fill in the template correctly. A second invalid response fails compaction.
|
||||
Provider-hosted tools remain subject to the selected provider's behavior.
|
||||
```text
|
||||
15,000 retained + 8,000 prompt and tools > 20,000 threshold
|
||||
```
|
||||
|
||||
The summary records the objective, requirements, decisions, completed and active
|
||||
work, blockers, next moves, relevant files, and additional context.
|
||||
Otherwise each fresh usage report can immediately trigger another checkpoint.
|
||||
Disabling automatic compaction does not remove a checkpoint already installed.
|
||||
|
||||
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.
|
||||
Streamed trigger checkpoints preserve whole, real user messages and attachments
|
||||
up to the `keep.tokens` budget, including user messages retained across earlier
|
||||
native checkpoints. Synthetic guidance is excluded. Standalone endpoint results
|
||||
are stored as returned; OpenCode does not fabricate a text summary for either
|
||||
native route.
|
||||
|
||||
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.
|
||||
On a recognized automatic context overflow, OpenCode attempts local recovery
|
||||
from the original stored history, expanding native checkpoints first:
|
||||
|
||||
## Compaction advances the instruction epoch
|
||||
```text
|
||||
native request rejected for overflow → rebuild stored history → local checkpoint
|
||||
```
|
||||
|
||||
Conversation compaction and instruction synchronization are separate. Before
|
||||
each physical model attempt, V2 compares live instruction sources with the
|
||||
latest admitted values, before delivering pending input for that attempt.
|
||||
Ordinary changes become durable value deltas. Later changes freeze their
|
||||
model-facing text when admitted and project it as chronological System messages;
|
||||
request assembly renders only the epoch baseline from stored values.
|
||||
This applies to ordinary model calls and rejected native compaction. If recovery
|
||||
fails, the previous checkpoint remains active and the error is returned.
|
||||
Authentication, rate-limit, cancellation, and other errors do not use local
|
||||
fallback. Manual native compaction also returns errors without fallback.
|
||||
|
||||
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 retains instruction
|
||||
state so destination changes become chronological updates. Committed revert
|
||||
clears instruction state so the next model attempt requires one complete source
|
||||
read. See [Instructions](/instructions) for source ordering and update behavior.
|
||||
## Checkpoints
|
||||
|
||||
## Current limitations
|
||||
For local compaction, OpenCode uses the session's selected agent, model, and
|
||||
variant. It sends the normal instructions, tools, and older history followed by
|
||||
a request for a structured checkpoint:
|
||||
|
||||
- Compaction requires a resolvable model. Automatic scheduling needs a positive
|
||||
catalog context limit; manual and overflow recovery do not.
|
||||
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.
|
||||
```md
|
||||
## Objective
|
||||
Finish the authentication migration.
|
||||
|
||||
V1 used additional tail-turn and pruning behavior. Those V1 details are only
|
||||
migration context; the settings and behavior on this page describe V2.
|
||||
## Next Move
|
||||
- Update the callback handler.
|
||||
- Verify the login flow.
|
||||
```
|
||||
|
||||
Context hooks run as they do for a normal session request. Local tool calls are
|
||||
not dispatched, and compaction does not override tool choice. Provider-hosted
|
||||
tools remain subject to provider behavior.
|
||||
|
||||
The summary must contain at least one requested heading such as `## Objective`.
|
||||
An invalid response gets one corrective request; a second invalid response fails
|
||||
compaction. The template covers:
|
||||
|
||||
- objective and requirements
|
||||
- decisions, completed work, and active work
|
||||
- blockers and next moves
|
||||
- relevant files and additional context
|
||||
|
||||
The newest serialized context up to `keep.tokens` is retained separately from
|
||||
the summary. For example, a large tool result is represented by a shortened
|
||||
record rather than copied exactly:
|
||||
|
||||
```text
|
||||
recent user message
|
||||
recent assistant message
|
||||
tool output (limited to 2,000 characters)
|
||||
attachment descriptor (embedded data omitted)
|
||||
```
|
||||
|
||||
On later local compactions, OpenCode updates the previous summary, carries its
|
||||
retained recent context forward, and then selects a new tail. Completed
|
||||
checkpoints appear to the model as historical conversation, not new
|
||||
instructions. Running and failed checkpoints are excluded from model context.
|
||||
|
||||
## Instructions
|
||||
|
||||
Instruction updates and conversation compaction are tracked separately. Before
|
||||
each actual provider attempt, OpenCode checks live instruction sources before it
|
||||
delivers pending input. Later changes appear to the model as chronological
|
||||
system messages, while request construction renders the stored baseline.
|
||||
|
||||
```text
|
||||
initial instructions → instruction update → completed checkpoint → new baseline
|
||||
```
|
||||
|
||||
When compaction completes, the instruction values already accepted at that exact
|
||||
point become the new baseline. Compaction does not reread sources or emit a new
|
||||
instruction update. Moving a session keeps this state, so instructions changed
|
||||
at the destination appear chronologically. A committed revert clears it, and
|
||||
the next model attempt performs one complete source read. See
|
||||
[Instructions](/instructions) for source ordering and update behavior.
|
||||
|
||||
## Limits
|
||||
|
||||
| Limit | Result |
|
||||
| --- | --- |
|
||||
| Model | Compaction requires a resolvable model. There is no separate compaction model or fallback model. |
|
||||
| Catalog | Automatic scheduling requires a positive catalog context limit; manual compaction and overflow recovery do not. |
|
||||
| Summary | Generation can fail when its prompt and output allowance do not fit, the model returns no summary, or the provider fails. |
|
||||
| History | Automatic and overflow compaction require older context that can be replaced. An overflow can remain when there is no compressible history or fixed instructions and tool schemas dominate the request. |
|
||||
| Recovery | Overflow recovery retries only once per model step. Heuristic token estimates cannot prevent every provider-specific overflow. |
|
||||
| Storage | Earlier messages remain stored even when excluded from active model context. |
|
||||
|
||||
For example, compaction cannot create room when almost the entire request is a
|
||||
fixed system prompt and tool schemas:
|
||||
|
||||
```text
|
||||
128k context = 120k fixed instructions and tools + 8k conversation
|
||||
```
|
||||
|
||||
## Migration
|
||||
|
||||
V1 also used tail-turn and pruning behavior. V2 instead uses checkpoint-based
|
||||
compaction and `compaction.keep.tokens`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"compaction": {
|
||||
"keep": { "tokens": 15000 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The settings and behavior on this page apply to V2.
|
||||
|
||||
@@ -2,56 +2,51 @@
|
||||
title: "Config"
|
||||
---
|
||||
|
||||
You shouldn't have to configure OpenCode manually. Ask OpenCode to update its configuration for you.
|
||||
|
||||
## Format
|
||||
|
||||
OpenCode supports both **JSON** and **JSONC** (JSON with Comments) configuration files.
|
||||
Create `opencode.jsonc` in your project to configure OpenCode. Add the schema for editor validation, then set only the options you need.
|
||||
|
||||
```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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
}
|
||||
```
|
||||
|
||||
You can also ask OpenCode to update this file for you.
|
||||
|
||||
## Format
|
||||
|
||||
OpenCode supports JSON and JSONC. Use JSONC when you want comments or trailing commas.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
// Use this model by default.
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
}
|
||||
```
|
||||
|
||||
## Locations
|
||||
|
||||
OpenCode loads global configuration from:
|
||||
Put settings for every project in the global configuration:
|
||||
|
||||
```text
|
||||
~/.config/opencode/opencode.json(c)
|
||||
```
|
||||
|
||||
Project-specific configuration can use either form:
|
||||
Put project settings in either of these files:
|
||||
|
||||
```text
|
||||
/home/user/projects/my-app/opencode.json(c)
|
||||
/home/user/projects/my-app/.opencode/opencode.json(c)
|
||||
```
|
||||
|
||||
During ordinary project discovery, OpenCode searches for configuration files
|
||||
from the current Location directory through every ancestor to the filesystem
|
||||
root, including directories above the detected project or repository root. It
|
||||
merges direct `opencode.json(c)` files from the farthest ancestor toward the
|
||||
current directory, then does the same for files inside `.opencode` directories.
|
||||
A discovered `.opencode` config therefore overrides every discovered direct
|
||||
config, even when the direct config is closer to the current directory. Avoid
|
||||
mixing the two forms across one directory hierarchy unless this precedence is
|
||||
intentional.
|
||||
OpenCode searches from the current directory to the filesystem root. It first
|
||||
merges direct `opencode.json(c)` files from the farthest directory to the
|
||||
closest, then merges files inside `.opencode` directories in the same order.
|
||||
This means every discovered `.opencode` config overrides every direct config.
|
||||
Use one form throughout a directory tree unless you need that behavior.
|
||||
|
||||
For example, consider a monorepo with OpenCode started from
|
||||
`/home/user/projects/acme/packages/web`:
|
||||
For example, start OpenCode from `/home/user/projects/acme/packages/web`:
|
||||
|
||||
```text
|
||||
~/.config/opencode/opencode.json
|
||||
@@ -70,9 +65,8 @@ OpenCode applies these files from lowest to highest precedence:
|
||||
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.
|
||||
The package config overrides matching settings from the repository config,
|
||||
which overrides the global config. Settings that do not conflict are preserved.
|
||||
|
||||
## Schema
|
||||
|
||||
@@ -114,7 +108,7 @@ does not retain a `#variant`; agent and command model references can select one.
|
||||
|
||||
See the [models guide](/models) for model selection and local models.
|
||||
|
||||
### Default agent
|
||||
### Agent
|
||||
|
||||
Choose the primary agent used when a session does not select one explicitly.
|
||||
|
||||
@@ -144,8 +138,8 @@ Project-level values are ignored.
|
||||
|
||||
### Sharing
|
||||
|
||||
Set the intended session sharing policy. V2 accepts this field, but session
|
||||
sharing is not implemented yet.
|
||||
Set the session sharing policy. OpenCode accepts this field, but session sharing
|
||||
is not supported yet.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -157,8 +151,8 @@ See the [sharing guide](/sharing) for more details.
|
||||
|
||||
### Username
|
||||
|
||||
Set a username for future display behavior. V2 accepts this field but does not
|
||||
currently display it in conversations.
|
||||
Set a username. OpenCode accepts this field but does not display it in
|
||||
conversations.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -231,39 +225,16 @@ Ignore files and directories that should not trigger filesystem updates.
|
||||
|
||||
### Formatter
|
||||
|
||||
Define formatter settings for compatibility and future use. V2 accepts this
|
||||
field, but it does not run formatters yet.
|
||||
Format files after the `write`, `edit`, or `patch` tools change them. Set
|
||||
`formatter` to `true` to enable available built-in formatters.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"formatter": {
|
||||
"prettier": {
|
||||
"command": ["bunx", "prettier", "--write", "$FILE"],
|
||||
"extensions": [".js", ".ts", ".tsx"],
|
||||
},
|
||||
},
|
||||
"formatter": true,
|
||||
}
|
||||
```
|
||||
|
||||
See the [formatters guide](/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](/lsp) for accepted fields and current limitations.
|
||||
See the [formatters guide](/formatters) for built-ins and custom formatters.
|
||||
|
||||
### Media
|
||||
|
||||
@@ -285,7 +256,7 @@ before they are sent to a model.
|
||||
|
||||
See the [attachments guide](/attachments) for image processing and limits.
|
||||
|
||||
### Tool output
|
||||
### Output
|
||||
|
||||
Set the maximum number of lines and bytes retained from a tool result.
|
||||
|
||||
@@ -298,10 +269,10 @@ Set the maximum number of lines and bytes retained from a tool result.
|
||||
}
|
||||
```
|
||||
|
||||
### Web search
|
||||
### Search
|
||||
|
||||
Use `"random"` to randomly choose a search provider for each session and keep using it until it
|
||||
returns HTTP 429. OpenCode then retries the query with another available provider.
|
||||
Choose how OpenCode searches the web. Use `"random"` to select an available
|
||||
provider automatically.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -311,12 +282,8 @@ returns HTTP 429. OpenCode then retries the query with another available provide
|
||||
}
|
||||
```
|
||||
|
||||
- Rate-limited providers cool down for `Retry-After`, or 60 seconds if it is missing or invalid.
|
||||
- When every provider is cooling down, the search fails without waiting.
|
||||
- Each session remembers its preferred provider; cooldowns are shared within a Location.
|
||||
- State is kept in memory. Moving a session or restarting its Location services resets its preference.
|
||||
- API and plugin queries without session context share a Location-level preference.
|
||||
- Set `provider` to a provider ID to disable automatic switching, or set `websearch` to `false` to disable search.
|
||||
See the [websearch guide](/websearch) for providers, credentials, selection,
|
||||
rate limits, and disabling search.
|
||||
|
||||
### MCP
|
||||
|
||||
@@ -378,7 +345,7 @@ Top-level `compaction.auto: false` disables new automatic compaction without
|
||||
discarding installed checkpoints. See the [compaction guide](/compaction) for
|
||||
budgeting and overflow recovery.
|
||||
|
||||
### Session warming
|
||||
### Warming
|
||||
|
||||
Keep recently active model sessions warm with periodic transient requests.
|
||||
Warming is disabled by default; set it to `true` to use the four-minute idle
|
||||
@@ -394,7 +361,7 @@ interval and 30-minute active window.
|
||||
}
|
||||
```
|
||||
|
||||
See the [session warming guide](/warming) for request behavior, customization,
|
||||
See the [warming guide](/warming) for request behavior, customization,
|
||||
and cost considerations.
|
||||
|
||||
### Skills
|
||||
@@ -428,8 +395,8 @@ See the [commands guide](/commands) for arguments, models, agents, and file-base
|
||||
|
||||
### 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.
|
||||
Declare additional instruction files, globs, or URLs. OpenCode accepts this
|
||||
field but does not load its entries; use `AGENTS.md` for instructions.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: "Websearch"
|
||||
description: "Hosted websearch for OpenCode through Console."
|
||||
---
|
||||
|
||||
Console provides hosted Websearch to connected OpenCode v2 users. A workspace owner or admin enables it once for the workspace, without giving members a separate search provider key.
|
||||
|
||||
Each successful search costs **$0.01** and counts toward the workspace balance and member spending limits.
|
||||
|
||||
## Enable
|
||||
|
||||
1. Sign in to the [Console](https://console.opencode.ai) as a workspace owner or admin.
|
||||
2. Open **Settings** > **General**.
|
||||
3. Turn on **Hosted web search**.
|
||||
|
||||
Each member then connects OpenCode to the workspace with `/connect`.
|
||||
|
||||
```text
|
||||
/connect
|
||||
```
|
||||
|
||||
OpenCode loads **OpenCode Web Search** from the workspace's managed configuration. Members do not need to set `websearch.provider` or add a provider API key.
|
||||
|
||||
## Search
|
||||
|
||||
Ask OpenCode for current information in a prompt. Searches run from OpenCode rather than from an interactive page in Console.
|
||||
|
||||
```text
|
||||
Find the latest Bun release and summarize the changes with source links.
|
||||
```
|
||||
|
||||
The hosted provider searches the public web and returns up to eight results. OpenCode uses the results to answer the prompt and cite sources.
|
||||
|
||||
See the [Websearch guide](/websearch) to configure permissions or disable the tool.
|
||||
|
||||
## Billing
|
||||
|
||||
Console charges **$0.01** after a search returns a valid result set. Failed and rate-limited searches are not charged.
|
||||
|
||||
Websearch appears separately from model usage in **Usage**, **My Activity**, member activity, invoices, and CSV exports. Workspace and member monthly spending limits include Websearch charges.
|
||||
|
||||
## Privacy
|
||||
|
||||
Hosted Websearch has zero data retention. Console and its hosted search provider process each query and its results without storing their contents.
|
||||
@@ -2,91 +2,200 @@
|
||||
title: "Formatters"
|
||||
---
|
||||
|
||||
OpenCode V2 accepts formatter configuration, but it does not yet include a
|
||||
formatter runtime. File writes and edits are not automatically formatted.
|
||||
OpenCode can format files after its `write`, `edit`, or `patch` tools change
|
||||
them. Formatters are disabled by default, so enable them in your configuration:
|
||||
|
||||
<Callout type="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.
|
||||
</Callout>
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"formatter": true,
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
## Enable
|
||||
|
||||
The `formatter` field accepts a boolean or an object keyed by formatter name:
|
||||
Set `formatter` to `true` to enable every built-in formatter. OpenCode runs a
|
||||
built-in only when its executable and any project-specific requirements are
|
||||
available.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"formatter": true,
|
||||
}
|
||||
```
|
||||
|
||||
An object also enables the built-ins and lets you override them or add custom
|
||||
formatters. An empty object is therefore equivalent to `true`.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"formatter": {},
|
||||
}
|
||||
```
|
||||
|
||||
## Builtins
|
||||
|
||||
OpenCode includes these formatter definitions. Most require the named command
|
||||
to be available; definitions with extra detection rules list them below.
|
||||
|
||||
| Formatter | Extensions | Requirement |
|
||||
| --- | --- | --- |
|
||||
| `gofmt` | `.go` | `gofmt` command |
|
||||
| `mix` | `.ex`, `.exs`, `.eex`, `.heex`, `.leex`, `.neex`, `.sface` | `mix` command |
|
||||
| `oxfmt` | `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.mts`, `.cts` | `oxfmt` dependency in `package.json` |
|
||||
| `prettier` | `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.mts`, `.cts`, `.html`, `.htm`, `.css`, `.scss`, `.sass`, `.less`, `.vue`, `.svelte`, `.json`, `.jsonc`, `.yaml`, `.yml`, `.toml`, `.xml`, `.md`, `.mdx`, `.graphql`, `.gql` | `prettier` dependency in `package.json` |
|
||||
| `biome` | Same extensions as `prettier` above | `biome.json` or `biome.jsonc` and an installed `@biomejs/biome` binary |
|
||||
| `zig` | `.zig`, `.zon` | `zig` command |
|
||||
| `clang-format` | `.c`, `.cc`, `.cpp`, `.cxx`, `.c++`, `.h`, `.hh`, `.hpp`, `.hxx`, `.h++`, `.ino`, `.C`, `.H` | `clang-format` command and `.clang-format` |
|
||||
| `ktlint` | `.kt`, `.kts` | `ktlint` command |
|
||||
| `ruff` | `.py`, `.pyi` | `ruff` command and a Ruff config or dependency declaration |
|
||||
| `air` | `.R` | `air` command that identifies itself as the R formatter |
|
||||
| `uv` | `.py`, `.pyi` | `uv` command with `uv format` support |
|
||||
| `rubocop` | `.rb`, `.rake`, `.gemspec`, `.ru` | `rubocop` command |
|
||||
| `standardrb` | `.rb`, `.rake`, `.gemspec`, `.ru` | `standardrb` command |
|
||||
| `htmlbeautifier` | `.erb` | `htmlbeautifier` command |
|
||||
| `dart` | `.dart` | `dart` command |
|
||||
| `ocamlformat` | `.ml`, `.mli` | `ocamlformat` command and `.ocamlformat` |
|
||||
| `terraform` | `.tf`, `.tfvars` | `terraform` command |
|
||||
| `latexindent` | `.tex` | `latexindent` command |
|
||||
| `gleam` | `.gleam` | `gleam` command |
|
||||
| `shfmt` | `.sh`, `.bash` | `shfmt` command |
|
||||
| `nixfmt` | `.nix` | `nixfmt` command |
|
||||
| `rustfmt` | `.rs` | `rustfmt` command |
|
||||
| `pint` | `.php` | `laravel/pint` in `composer.json` |
|
||||
| `ormolu` | `.hs` | `ormolu` command |
|
||||
| `cljfmt` | `.clj`, `.cljs`, `.cljc`, `.edn` | `cljfmt` command |
|
||||
| `dfmt` | `.d` | `dfmt` command |
|
||||
|
||||
For example, enabling built-ins lets OpenCode discover and run a project-local
|
||||
Prettier dependency for matching files:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"formatter": true,
|
||||
}
|
||||
```
|
||||
|
||||
## Customize
|
||||
|
||||
Add a named entry to change a built-in or define a custom formatter. A custom
|
||||
formatter needs both `command` and `extensions` to run.
|
||||
|
||||
```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"],
|
||||
"extensions": [".js", ".ts"],
|
||||
},
|
||||
"deno-markdown": {
|
||||
"command": ["deno", "fmt", "$FILE"],
|
||||
"extensions": [".md"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
This example is valid V2 configuration, but V2 does not currently execute the
|
||||
command.
|
||||
| Field | Type | Behavior |
|
||||
| --- | --- | --- |
|
||||
| `disabled` | `boolean` | Removes the named formatter when `true`. |
|
||||
| `command` | `string[]` | Replaces the built-in command or defines a custom command. |
|
||||
| `environment` | `Record<string, string>` | Adds environment variables while preserving the parent environment. |
|
||||
| `extensions` | `string[]` | Replaces the built-in extension list or defines the custom list. Include the leading dot. |
|
||||
|
||||
Each named formatter entry supports these optional fields:
|
||||
All fields are optional. A built-in entry inherits omitted values, while a new
|
||||
entry without a command or extensions cannot run.
|
||||
|
||||
| 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.
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"formatter": false,
|
||||
"formatter": {
|
||||
"prettier": {
|
||||
"extensions": [".md", ".mdx"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```jsonc
|
||||
// Reserved for enabling all built-ins once a V2 runtime provides them.
|
||||
{
|
||||
"formatter": true,
|
||||
}
|
||||
```
|
||||
## Commands
|
||||
|
||||
`command` is an argument array, not a shell command string. OpenCode replaces
|
||||
`$FILE` with the file's absolute path and runs the command from the active
|
||||
project directory.
|
||||
|
||||
```jsonc
|
||||
// Configure named entries or mark one as disabled.
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"formatter": {
|
||||
"prettier": { "disabled": true },
|
||||
"custom": {
|
||||
"command": ["custom-fmt", "$FILE"],
|
||||
"command": ["custom-fmt", "--write", "$FILE"],
|
||||
"extensions": [".custom"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Matching
|
||||
|
||||
OpenCode compares the file's final extension with `extensions`. Matching is
|
||||
case-sensitive, and compound entries such as `.part.md` do not match
|
||||
`notes.part.md` because its final extension is `.md`.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"formatter": {
|
||||
"markdown": {
|
||||
"command": ["deno", "fmt", "$FILE"],
|
||||
"extensions": [".md"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
When several formatters match, OpenCode tries them in registered order and
|
||||
stops after the first successful command. Built-ins retain their built-in
|
||||
order; custom entries follow in object order. If one exits unsuccessfully,
|
||||
OpenCode logs the failure and tries the next match.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"formatter": {
|
||||
"preferred": {
|
||||
"command": ["preferred-fmt", "$FILE"],
|
||||
"extensions": [".foo"],
|
||||
},
|
||||
"fallback": {
|
||||
"command": ["fallback-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.
|
||||
## Disable
|
||||
|
||||
## Commands and placeholders
|
||||
Omit `formatter` or set it to `false` to disable all formatting. An explicit
|
||||
`false` can override a lower-priority configuration that enabled formatters.
|
||||
|
||||
`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.
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"formatter": false,
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
To disable one built-in while leaving the others enabled, mark its named entry
|
||||
as disabled.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"formatter": {
|
||||
"prettier": {
|
||||
"disabled": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
title: "Intro"
|
||||
---
|
||||
|
||||
These docs are for the beta version of OpenCode, which will become OpenCode 2.0. The beta is still changing: things may
|
||||
break, and APIs, configuration, and plugin APIs may change.
|
||||
These docs describe OpenCode 2 and its released APIs, configuration, and plugin system.
|
||||
|
||||
OpenCode 2 installs and runs as `opencode2`. It does not replace OpenCode 1's `opencode` binary, so you can keep both
|
||||
versions installed and run them side by side.
|
||||
@@ -27,7 +26,7 @@ commands above explicitly allow that script to run.
|
||||
On Arch Linux, install [`opencode-beta`](https://aur.archlinux.org/packages/opencode-beta) from the AUR with `paru`.
|
||||
It provides the `opencode2` command; manage updates through your AUR helper.
|
||||
|
||||
Homebrew, Windows package managers, Docker, and standalone binaries are not supported during the beta.
|
||||
Homebrew, Windows package managers, Docker, and standalone binaries are not supported in V2.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,126 +2,111 @@
|
||||
title: "Instructions"
|
||||
---
|
||||
|
||||
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.
|
||||
Add an `AGENTS.md` file to give OpenCode persistent project guidance. Use it for build commands, architecture notes, code conventions, and verification requirements.
|
||||
|
||||
## AGENTS.md
|
||||
```md title="AGENTS.md"
|
||||
# Project instructions
|
||||
|
||||
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 home
|
||||
directory when the Location is inside it. For Locations outside home, the
|
||||
scan stops at 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
|
||||
- Run `bun typecheck` after changing TypeScript.
|
||||
- Keep database queries in `src/database`.
|
||||
- Do not edit generated files directly.
|
||||
```
|
||||
|
||||
The files are combined rather than selecting a single winner. They are rendered
|
||||
in this order: global, then files from the Location toward home or the project
|
||||
root. OpenCode does not resolve conflicts between their contents, so keep broad
|
||||
guidance global and put scoped guidance in the relevant directory.
|
||||
Commit project instruction files so everyone working in the repository receives the same guidance.
|
||||
|
||||
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.
|
||||
## Scope
|
||||
|
||||
Place `AGENTS.md` in the directory where its guidance should apply. OpenCode loads the global file followed by every `AGENTS.md` from the current workspace directory toward the home directory. For workspaces outside the home directory, it stops at the project root.
|
||||
|
||||
```text
|
||||
~/.config/opencode/AGENTS.md
|
||||
~/code/my-project/AGENTS.md
|
||||
~/code/my-project/packages/AGENTS.md
|
||||
~/code/my-project/packages/web/AGENTS.md ← current workspace
|
||||
```
|
||||
|
||||
In this example, all four files are loaded. They are combined in this order:
|
||||
|
||||
```text
|
||||
~/.config/opencode/AGENTS.md
|
||||
packages/web/AGENTS.md
|
||||
packages/AGENTS.md
|
||||
AGENTS.md
|
||||
```
|
||||
|
||||
Keep guidance that applies everywhere in the global file. Put repository-wide guidance at the project root and more specific guidance closer to the code it covers. OpenCode combines the files and does not resolve conflicts between them.
|
||||
|
||||
If the workspace is outside the project root, only the global file is loaded. Set `OPENCODE_DISABLE_PROJECT_CONFIG=1` to skip project `AGENTS.md` discovery without disabling the global file.
|
||||
|
||||
<Callout type="note">
|
||||
Current V2 discovery only recognizes `AGENTS.md`. The `CLAUDE.md` fallback and related precedence described by older
|
||||
OpenCode documentation do not apply.
|
||||
OpenCode V2 recognizes `AGENTS.md` only. It does not use `CLAUDE.md` as a fallback.
|
||||
</Callout>
|
||||
|
||||
### Nested instructions
|
||||
## Discovery
|
||||
|
||||
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.
|
||||
Instruction files below the workspace are discovered as the agent explores the project. Reading a file or listing a directory loads any `AGENTS.md` files between that target and the workspace.
|
||||
|
||||
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.
|
||||
```text
|
||||
my-project/ ← current workspace
|
||||
├── AGENTS.md loaded initially
|
||||
└── packages/
|
||||
└── web/
|
||||
├── AGENTS.md loaded when this area is read
|
||||
└── src/
|
||||
└── app.ts read target
|
||||
```
|
||||
|
||||
## Config entries
|
||||
Nested files are loaded nearest-first and deduplicated while their instruction entry remains in model-visible history. Reading the same area again does not normally inject them again. If compaction or a revert removes that entry, a later read can load the file again.
|
||||
|
||||
The V2 config schema accepts an `instructions` array of strings:
|
||||
Edits to a nested file are not detected automatically after it loads. Start a new session when updated text must apply immediately.
|
||||
|
||||
## Ordering
|
||||
|
||||
The selected agent or provider system prompt is sent first. OpenCode then assembles initial instructions in this order:
|
||||
|
||||
```text
|
||||
1. Agent or provider system prompt
|
||||
2. Built-in environment and date context
|
||||
3. Code Mode tool guidance, when enabled
|
||||
4. Global and project AGENTS.md files
|
||||
5. Available skill, reference, and MCP guidance
|
||||
6. Session-specific instruction entries supplied through the API
|
||||
```
|
||||
|
||||
These sources are combined rather than used as overrides. Nested `AGENTS.md` files discovered later are added to session history in discovery order.
|
||||
|
||||
## Updates
|
||||
|
||||
Edit a global or upward-discovered `AGENTS.md` while a session is running to update its guidance.
|
||||
|
||||
```bash
|
||||
$ printf '\n- Run the integration suite before committing.\n' >> AGENTS.md
|
||||
```
|
||||
|
||||
Before the next model request, OpenCode detects the change and adds an instruction update before delivering pending input:
|
||||
|
||||
```text
|
||||
AGENTS.md changes
|
||||
→ instruction update
|
||||
→ next prompt
|
||||
```
|
||||
|
||||
- Removing every ambient `AGENTS.md` tells the session that the previous ambient instructions no longer apply.
|
||||
- A temporary read failure preserves the last known instructions instead of treating them as deleted.
|
||||
- Moving a session retains its instruction state, so guidance at the destination is introduced as an update.
|
||||
- Committing a session revert clears its instruction state and reloads instructions before the next prompt.
|
||||
|
||||
Instruction values remain privileged. Clients can see which sources changed, but not their contents.
|
||||
|
||||
## Configuration
|
||||
|
||||
The V2 config schema accepts an `instructions` array, but V2 does not currently resolve its files, glob patterns, or URLs. This configuration does not add instructions to the model yet:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"instructions": ["CONTRIBUTING.md", "docs/guidelines/*.md", "https://example.com/shared-instructions.md"],
|
||||
"instructions": ["CONTRIBUTING.md", "docs/guidelines/*.md", "https://example.com/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.
|
||||
|
||||
<Callout type="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.
|
||||
</Callout>
|
||||
|
||||
See [Config](/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 each physical model attempt, V2 compares live instruction sources with
|
||||
the latest admitted source values. This comparison happens before pending input
|
||||
is delivered for that attempt:
|
||||
|
||||
- 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 retains instruction state, so destination changes become
|
||||
chronological updates. Committing a revert clears instruction state; the next
|
||||
model attempt requires one complete source read before delivering input.
|
||||
|
||||
The durable event stores changed source keys and value hashes. Initial baseline
|
||||
events contain no rendered prose. Later changes render once when admitted and
|
||||
freeze that optional text in the event, which projects it as a chronological
|
||||
System message. During request assembly, OpenCode renders the epoch's initial
|
||||
values and reuses projected update messages verbatim. Clients see changed keys
|
||||
but never the privileged value bodies.
|
||||
Use `AGENTS.md` for active V2 instructions. See [Config](/config) for configuration locations and precedence.
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
---
|
||||
title: "LSP"
|
||||
---
|
||||
|
||||
Language Server Protocol (LSP) integrations can provide code diagnostics,
|
||||
symbols, definitions, references, and other language-aware context.
|
||||
|
||||
<Callout type="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.
|
||||
</Callout>
|
||||
|
||||
## 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.
|
||||
@@ -2,11 +2,32 @@
|
||||
title: "MCP servers"
|
||||
---
|
||||
|
||||
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.
|
||||
OpenCode connects to [Model Context Protocol](https://modelcontextprotocol.io/) servers and exposes capabilities such as tools, prompts, resources, and instructions. MCP tools consume model context, so add only the servers you need.
|
||||
|
||||
## Configure servers
|
||||
## Setup
|
||||
|
||||
Define each server by a unique name under `mcp.servers` in your [OpenCode configuration](/config). V2 does not place server names directly under `mcp`.
|
||||
Add a remote server from the project that should use it, then check its connection:
|
||||
|
||||
```sh
|
||||
opencode2 mcp add context7 --url https://mcp.context7.com/mcp
|
||||
opencode2 mcp list
|
||||
```
|
||||
|
||||
The command writes the server to the project [configuration](/config). Add `--global` to make it available in every project:
|
||||
|
||||
```sh
|
||||
opencode2 mcp add context7 --global --url https://mcp.context7.com/mcp
|
||||
```
|
||||
|
||||
Remote servers use OAuth by default. If the list shows `needs authentication`, open OpenCode, run `/mcps`, select the server, and sign in. A connected server is ready for an agent to use:
|
||||
|
||||
```text
|
||||
✓ context7 connected
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
To configure a server by hand, give it a unique name under `mcp.servers`. V2 does not place server names directly under `mcp`.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -22,7 +43,7 @@ Define each server by a unique name under `mcp.servers` in your [OpenCode config
|
||||
}
|
||||
```
|
||||
|
||||
Servers connect automatically unless `disabled` is `true`. There is no V2 `enabled` field.
|
||||
Servers connect automatically. Use `disabled`, not an `enabled` field, to keep one configured without connecting it:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -38,11 +59,30 @@ Servers connect automatically unless `disabled` is `true`. There is no V2 `enabl
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
A higher-precedence project config replaces the entire server object with the same name. Use different names for separate connections or accounts; otherwise repeat every required field in the override:
|
||||
|
||||
## Local servers
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"my-server": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.example.com/mcp",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
A local server is a command that OpenCode starts using the MCP stdio transport.
|
||||
## Local
|
||||
|
||||
A local server is a command that OpenCode starts over the MCP stdio transport. Add one with a command after `--`:
|
||||
|
||||
```sh
|
||||
opencode2 mcp add everything -- npx -y @modelcontextprotocol/server-everything
|
||||
```
|
||||
|
||||
Use configuration for process options such as a working directory or environment variables:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -63,21 +103,35 @@ A local server is a command that OpenCode starts using the MCP stdio transport.
|
||||
}
|
||||
```
|
||||
|
||||
| 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`. |
|
||||
| `codemode` | No | Set to `false` to expose the server's tools directly to the model instead of through Code Mode. Defaults to `true`. |
|
||||
| `timeout` | No | Per-server timeout overrides. |
|
||||
| Field | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `type` | Yes | Must be `"local"`. |
|
||||
| `command` | Yes | Executable followed by its arguments. |
|
||||
| `cwd` | No | Process directory. Relative paths resolve from the workspace, which is also the default. |
|
||||
| `environment` | No | String variables added to OpenCode's inherited process environment. |
|
||||
| `disabled` | No | Prevents connection when `true`. Defaults to `false`. |
|
||||
| `codemode` | No | Set to `false` to expose tools directly instead of through Code Mode. Defaults to `true`. |
|
||||
| `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.
|
||||
Use `{env:NAME}` for environment substitution. Shell expressions such as `$NAME` are not expanded in JSON strings:
|
||||
|
||||
## Remote servers
|
||||
```jsonc
|
||||
{
|
||||
"environment": {
|
||||
"MCP_API_KEY": "{env:MCP_API_KEY}",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
A remote server uses the MCP Streamable HTTP transport. Its `url` must be a valid absolute URL.
|
||||
## Remote
|
||||
|
||||
A remote server uses the MCP Streamable HTTP transport and requires an absolute URL:
|
||||
|
||||
```sh
|
||||
opencode2 mcp add context7 --url https://mcp.context7.com/mcp
|
||||
```
|
||||
|
||||
Use configuration when the server needs headers or other options. Store secrets in environment variables rather than in the file:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -97,23 +151,32 @@ A remote server uses the MCP Streamable HTTP transport. Its `url` must be a vali
|
||||
}
|
||||
```
|
||||
|
||||
| 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`. |
|
||||
| `codemode` | No | Set to `false` to expose the server's tools directly to the model instead of through Code Mode. Defaults to `true`. |
|
||||
| `timeout` | No | Per-server timeout overrides. |
|
||||
| Field | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `type` | Yes | Must be `"remote"`. |
|
||||
| `url` | Yes | Absolute Streamable HTTP endpoint. |
|
||||
| `headers` | No | String HTTP headers sent to the endpoint. |
|
||||
| `oauth` | No | OAuth settings, or `false` to disable OAuth. |
|
||||
| `disabled` | No | Prevents connection when `true`. Defaults to `false`. |
|
||||
| `codemode` | No | Set to `false` to expose tools directly instead of through Code Mode. Defaults to `true`. |
|
||||
| `timeout` | No | Per-server timeout overrides. |
|
||||
|
||||
Use `oauth: false` for a server that exclusively uses an API key or another header-based credential.
|
||||
Use `oauth: false` only when the server exclusively uses an API key or another header credential:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"type": "remote",
|
||||
"url": "https://mcp.example.com/mcp",
|
||||
"oauth": false,
|
||||
"headers": { "Authorization": "Bearer {env:MCP_API_KEY}" },
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
OAuth is enabled for remote servers unless `oauth` is `false`. OpenCode discovers the authorization server, uses PKCE, refreshes tokens, and attempts dynamic client registration when supported; credentials stay outside project configuration.
|
||||
|
||||
For a server that supports dynamic client registration, only the remote server is required:
|
||||
For dynamic registration, configure only the server URL:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -129,10 +192,13 @@ For a server that supports dynamic client registration, only the remote server i
|
||||
}
|
||||
```
|
||||
|
||||
When a server reports that it needs authentication, start its OAuth flow using
|
||||
an MCP management interface and complete authorization in the browser.
|
||||
If the server needs authentication, run `/mcps`, select it, and complete authorization in the browser. The CLI can start the same flow:
|
||||
|
||||
If the provider issued client credentials, configure them using V2's snake_case field names:
|
||||
```sh
|
||||
opencode2 mcp auth sentry
|
||||
```
|
||||
|
||||
When a provider gives you client credentials, use V2's snake_case OAuth fields:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -155,17 +221,23 @@ If the provider issued client credentials, configure them using V2's snake_case
|
||||
}
|
||||
```
|
||||
|
||||
| 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. |
|
||||
| Field | Description |
|
||||
| --- | --- |
|
||||
| `client_id` | Pre-registered client ID. Omit it to attempt dynamic registration. |
|
||||
| `client_secret` | 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 the default. |
|
||||
| `redirect_uri` | Pre-registered loopback URI whose path and port reach the local callback listener. |
|
||||
|
||||
Remove stored OAuth credentials when you need to sign in again or switch accounts:
|
||||
|
||||
```sh
|
||||
opencode2 mcp logout sentry
|
||||
```
|
||||
|
||||
## Timeouts
|
||||
|
||||
Timeouts are positive integer milliseconds. Configure defaults under `mcp.timeout`; a server's `timeout` fields override matching defaults.
|
||||
Timeouts are positive integer milliseconds. Set defaults under `mcp.timeout`; a server's `timeout` object overrides matching defaults.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -189,19 +261,37 @@ Timeouts are positive integer milliseconds. Configure defaults under `mcp.timeou
|
||||
}
|
||||
```
|
||||
|
||||
| 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. |
|
||||
| Timeout | Default | Applies to |
|
||||
| --- | --- | --- |
|
||||
| `startup` | 30 seconds | Transport connection and server initialization. |
|
||||
| `catalog` | 30 seconds | Listing tools, prompts, resources, and resource templates. |
|
||||
| `execution` | 12 hours | Tool calls, prompt retrieval, and resource reads. |
|
||||
|
||||
## Names and permissions
|
||||
## Names
|
||||
|
||||
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 become available as commands named `<server>:<prompt>` using the same normalization.
|
||||
OpenCode names a tool `<server>_<tool>`. It replaces characters other than letters, numbers, `_`, and `-` with `_`:
|
||||
|
||||
Choose short server names that remain unique after normalization. Under the default Code Mode, MCP tools are grouped by the normalized server name.
|
||||
```text
|
||||
server: context 7
|
||||
tool: resolve.library/id
|
||||
name: context_7_resolve_library_id
|
||||
```
|
||||
|
||||
Set `codemode` to `false` on a server when its tools should remain on the provider's native tool list:
|
||||
MCP prompts become commands named `<server>:<prompt>` with the same normalization. For example:
|
||||
|
||||
```text
|
||||
/context_7:find_docs
|
||||
```
|
||||
|
||||
Choose short server names that remain unique after normalization. Under the default Code Mode, tools are grouped by the normalized server name:
|
||||
|
||||
```text
|
||||
tools.context_7.resolve_library_id(...)
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
Code Mode is the default. Set `codemode` to `false` when a server's tools must stay on the provider's native tool list:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -217,7 +307,7 @@ Set `codemode` to `false` on a server when its tools should remain on the provid
|
||||
}
|
||||
```
|
||||
|
||||
Use permission actions to hide or deny a server's tools without stopping its connection:
|
||||
Use permission actions to hide or deny tools without disconnecting their server. Match the normalized `<server>_<tool>` name:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -231,21 +321,68 @@ Use permission actions to hide or deny a server's tools without stopping its con
|
||||
}
|
||||
```
|
||||
|
||||
## Session context
|
||||
## Context
|
||||
|
||||
When OpenCode invokes an MCP tool on behalf of a session, it includes the invoking
|
||||
session's ID in `CallToolRequest.params._meta.sessionID`. This applies to direct tool
|
||||
calls and Code Mode over both stdio and Streamable HTTP.
|
||||
For calls made on behalf of a session, OpenCode sends the session ID in `CallToolRequest.params._meta.sessionID`. This applies to direct tools and Code Mode over stdio and Streamable HTTP:
|
||||
|
||||
The ID is request metadata, not a tool argument, so it does not appear in the
|
||||
model-visible tool schema. Treat it as an opaque correlation value: it identifies the
|
||||
invoking OpenCode session rather than the MCP transport session, can be absent for
|
||||
calls without session context, and must not be used by itself for authentication or
|
||||
authorization. Remote MCP servers receive the raw ID and may log or retain it.
|
||||
```json
|
||||
{
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "lookup",
|
||||
"arguments": { "query": "example" },
|
||||
"_meta": { "sessionID": "ses_..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Manage servers
|
||||
The ID is request metadata, not a tool argument, so it is absent from the model-visible schema. Treat it as an opaque correlation value:
|
||||
|
||||
OpenCode interfaces can add servers to project or global configuration, list
|
||||
configured servers and their connection status, authenticate remote servers,
|
||||
and remove stored OAuth credentials. Edit configuration directly for OAuth client settings, timeouts, working
|
||||
directories, or enablement.
|
||||
| Rule | Behavior |
|
||||
| --- | --- |
|
||||
| Identity | It identifies the invoking OpenCode session, not the MCP transport session. |
|
||||
| Presence | It can be absent for calls without session context. |
|
||||
| Security | Do not use it by itself for authentication or authorization. |
|
||||
| Privacy | Remote servers receive the raw ID and may log or retain it. |
|
||||
|
||||
## Management
|
||||
|
||||
List servers and their current connection state from any project:
|
||||
|
||||
```sh
|
||||
opencode2 mcp list
|
||||
```
|
||||
|
||||
Use `/mcps` in OpenCode to view, connect, disconnect, or authenticate servers. Use the CLI to add servers and manage OAuth credentials:
|
||||
|
||||
```sh
|
||||
opencode2 mcp add sentry --url https://mcp.sentry.dev/mcp
|
||||
opencode2 mcp auth sentry
|
||||
opencode2 mcp logout sentry
|
||||
```
|
||||
|
||||
To remove a server, delete its entry from the project or global configuration where it was added:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Edit configuration directly for OAuth client settings, timeouts, working directories, or persistent enablement:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"sentry": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.sentry.dev/mcp",
|
||||
"disabled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -20,20 +20,16 @@ schema never had a V2 equivalent and are intentionally ignored; these are listed
|
||||
|
||||
Existing supported server config fields, agent definitions, command definitions, skills, and other files in `.opencode/`
|
||||
should continue to work without changes. If supported behavior described in this guide stops working in V2, treat it as a
|
||||
beta compatibility bug rather than an expected migration requirement.
|
||||
compatibility bug rather than an expected migration requirement.
|
||||
|
||||
<Callout type="tip">
|
||||
If supported V1 functionality does not work in V2, follow the issue-reporting guidance in
|
||||
[Troubleshooting](/troubleshooting) and file a compatibility issue.
|
||||
</Callout>
|
||||
|
||||
<Callout type="warning">
|
||||
OpenCode 2.0 is in beta. Features may break unintentionally, and the server and plugin APIs may continue to change.
|
||||
</Callout>
|
||||
## Install V2
|
||||
|
||||
## Install the beta
|
||||
|
||||
The V2 terminal client is published on the `beta` distribution tag. See the [terminal startup guide](/cli).
|
||||
Install the V2 terminal client with the [terminal startup guide](/cli).
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -546,24 +542,20 @@ Moving a file between these directories does not migrate its implementation.
|
||||
|
||||
<Callout type="warning">V1 plugins will not work in V2.</Callout>
|
||||
|
||||
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](/build/plugins).
|
||||
The config entry can be translated automatically, but plugin implementation code must be ported to the released V2 API.
|
||||
Use the [Plugins guide](/build/plugins) to replace V1 hooks and entrypoints. Related local modules and dependencies can
|
||||
remain with the plugin while you port its implementation.
|
||||
|
||||
## 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/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](/api) for the current endpoints,
|
||||
request types, and responses.
|
||||
Use the `@opencode/client` package to access the released V2 API. See the generated [API reference](/api) for its
|
||||
endpoints, request types, and responses.
|
||||
|
||||
## Verify your setup
|
||||
|
||||
Verify your model, provider credentials, agents, permissions, MCP servers, and plugins in a project 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
|
||||
Verify your model, provider credentials, agents, permissions, MCP servers, and plugins in a project before relying on V2
|
||||
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.
|
||||
|
||||
@@ -2,34 +2,44 @@
|
||||
title: "Models"
|
||||
---
|
||||
|
||||
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 are available for selection.
|
||||
Choose a model for the current session with `/models`:
|
||||
|
||||
Configure provider availability in [Providers](/providers).
|
||||
```text
|
||||
/models
|
||||
```
|
||||
|
||||
## Choose a model
|
||||
OpenCode lists models from [models.dev](https://models.dev), provider integrations, and your configuration. The selector
|
||||
only shows enabled models whose provider is available in the current project. Connect providers in
|
||||
[Providers](/providers).
|
||||
|
||||
Clients can select any model available from providers connected to the current project. Selecting a model updates the
|
||||
current session without changing configuration. Use an available catalog entry rather than guessing a provider or model
|
||||
name.
|
||||
## Select
|
||||
|
||||
## Per-run model
|
||||
Pick an entry from `/models` instead of guessing its provider or model ID. The selection applies to the current session
|
||||
and does not change your configuration.
|
||||
|
||||
Command-line runs can select a model without changing the configured default.
|
||||
```text
|
||||
anthropic/claude-sonnet-4-5
|
||||
```
|
||||
|
||||
Agents and commands can also select their own model. See [Agents](/agents) and [Commands](/commands).
|
||||
Availability is project-specific:
|
||||
|
||||
## Variants
|
||||
- The model must be enabled.
|
||||
- Its provider must be available in the current project.
|
||||
- Credentials and configuration from another project do not carry over automatically.
|
||||
|
||||
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. Clients should present only the variants available for the selected model.
|
||||
## Runs
|
||||
|
||||
## Configure
|
||||
Use `--model` to choose a model for one command-line run without changing the configured default:
|
||||
|
||||
### Default model
|
||||
```bash
|
||||
opencode2 run --model anthropic/claude-sonnet-4-5 "Refactor parseToken"
|
||||
```
|
||||
|
||||
Set `model` in `opencode.json` or `opencode.jsonc`:
|
||||
Agents and commands can also choose their own model. See [Agents](/agents) and [Commands](/commands).
|
||||
|
||||
## Defaults
|
||||
|
||||
Set `model` in `opencode.json` or `opencode.jsonc` to choose the default for new work:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -38,87 +48,28 @@ Set `model` in `opencode.json` or `opencode.jsonc`:
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
The configured model becomes the catalog default when it is enabled and its provider is available. Otherwise, OpenCode
|
||||
falls back to the newest available supported model.
|
||||
|
||||
- A model already selected for a session takes precedence over the configured default.
|
||||
- Switching a session's model does not rewrite the config file.
|
||||
- The root `model` currently retains only the default provider and model, not a variant.
|
||||
|
||||
See [Config](/config) for configuration locations and precedence.
|
||||
|
||||
### Model settings
|
||||
## Variants
|
||||
|
||||
Provider and model entries can supply three kinds of request configuration:
|
||||
Variants are named options for one model, often used for reasoning effort or token budgets. Add `#variant` when selecting
|
||||
one for a run, session, agent, or command:
|
||||
|
||||
- `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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```bash
|
||||
opencode2 run --model openai/gpt-5.2#high "Review this migration plan"
|
||||
```
|
||||
|
||||
Here `openai/coding-default` is the selectable catalog reference, while `gpt-5.2` is sent to the provider. A model that is
|
||||
not already in the catalog receives fallback metadata:
|
||||
Variant names come from the selected model's current catalog metadata. Names such as `low`, `high`, and `max` are not
|
||||
available for every model, and an unknown variant produces a model-resolution error.
|
||||
|
||||
- Tool support, text and image input, and text output.
|
||||
- A 200,000-token context limit and 32,000-token output limit. The input limit remains unspecified.
|
||||
|
||||
These values are assumptions, not model discovery. Configure accurate `capabilities` and `limit` values whenever they are
|
||||
known; explicit values override the fallbacks. Set `disabled: true` on a model entry to hide it from the available
|
||||
catalog.
|
||||
|
||||
OpenAI-compatible models that stream reasoning through a custom assistant-message field can set
|
||||
`compatibility.reasoningField`:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"providers": {
|
||||
"local": {
|
||||
"models": {
|
||||
"reasoner": {
|
||||
"compatibility": {
|
||||
"reasoningField": "reasoning_content",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
OpenCode recognizes `reasoning`, `reasoning_content`, and `reasoning_text`, and accepts any provider-specific string. It
|
||||
reads streamed reasoning from this field and includes the field when replaying assistant messages to the model.
|
||||
|
||||
### Custom variants
|
||||
|
||||
Add a variant, or override a catalog variant with the same ID, under the model's `variants` array:
|
||||
Define new variants, or replace catalog variants with the same ID, in the model's `variants` array:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -152,15 +103,143 @@ Add a variant, or override a catalog variant with the same ID, under the model's
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
Each variant can contain `settings`, `headers`, and `body`. Its values are applied after provider and model values.
|
||||
|
||||
### Local models
|
||||
## Options
|
||||
|
||||
#### Ollama
|
||||
Provider and model entries can customize requests with `settings`, `headers`, and `body`:
|
||||
|
||||
OpenCode automatically discovers language models from an Ollama server listening on its default address,
|
||||
`http://127.0.0.1:11434`. Discovered models use the `ollama` provider ID and Ollama's model name:
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"providers": {
|
||||
"openai": {
|
||||
"settings": {
|
||||
"baseURL": "https://api.example.com/v1",
|
||||
},
|
||||
"headers": {
|
||||
"X-Team": "platform",
|
||||
},
|
||||
"models": {
|
||||
"gpt-5.2": {
|
||||
"settings": {
|
||||
"reasoningEffort": "high",
|
||||
},
|
||||
"body": {
|
||||
"store": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Purpose |
|
||||
| ---------- | ------------------------------------------------------------------ |
|
||||
| `settings` | Provider-package options such as `baseURL` or `reasoningEffort`. |
|
||||
| `headers` | Additional HTTP request headers. |
|
||||
| `body` | Provider-specific request body fields. |
|
||||
|
||||
OpenCode applies provider values first, model values second, and the selected variant last.
|
||||
|
||||
- Nested `settings` and `body` objects are merged.
|
||||
- Later arrays and scalar values replace earlier values.
|
||||
- Header names are matched case-insensitively.
|
||||
- Options are provider-specific and may be ignored or rejected by another provider package.
|
||||
|
||||
## Aliases
|
||||
|
||||
Use `modelID` to give an API model a friendlier catalog ID:
|
||||
|
||||
```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 reference and `gpt-5.2` is sent to the provider. For a model that is not
|
||||
already in the catalog, OpenCode assumes:
|
||||
|
||||
- Tool support, text and image input, and text output.
|
||||
- A 200,000-token context limit and 32,000-token output limit.
|
||||
- An unspecified input limit.
|
||||
|
||||
These are fallback assumptions, not detected capabilities. Set accurate `capabilities` and `limit` values when known;
|
||||
explicit values replace the fallbacks. Add `disabled: true` to hide a model from the available catalog:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"providers": {
|
||||
"openai": {
|
||||
"models": {
|
||||
"legacy-model": {
|
||||
"disabled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Reasoning
|
||||
|
||||
For an OpenAI-compatible model that streams reasoning in a custom assistant-message field, set
|
||||
`compatibility.reasoningField`:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"providers": {
|
||||
"local": {
|
||||
"models": {
|
||||
"reasoner": {
|
||||
"compatibility": {
|
||||
"reasoningField": "reasoning_content",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
OpenCode recognizes `reasoning`, `reasoning_content`, and `reasoning_text`, and also accepts another provider-specific
|
||||
string. It reads streamed reasoning from that field and restores the field when sending prior assistant messages back to
|
||||
the model.
|
||||
|
||||
## Local
|
||||
|
||||
OpenCode automatically discovers models from Ollama, LM Studio, and vLLM at their default local addresses. You can also
|
||||
configure any OpenAI-compatible server manually.
|
||||
|
||||
```text
|
||||
ollama/gemma3:4b
|
||||
lmstudio/google/gemma-4-26b-a4b
|
||||
vllm/Qwen/Qwen3-Coder-30B-A3B-Instruct
|
||||
```
|
||||
|
||||
### Ollama
|
||||
|
||||
With Ollama listening at `http://127.0.0.1:11434`, select a discovered model with the `ollama` provider ID:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -170,15 +249,13 @@ OpenCode automatically discovers language models from an Ollama server listening
|
||||
```
|
||||
|
||||
OpenCode refreshes the inventory in the background and reads context, vision, and tool-use capabilities from Ollama.
|
||||
Embedding-only models are excluded because they cannot drive a session. Disable discovery with
|
||||
`"plugins": ["-opencode.provider.ollama"]`.
|
||||
Embedding-only models are excluded.
|
||||
|
||||
For a different host or port, configure Ollama's OpenAI-compatible base URL. Models are still discovered through the
|
||||
native Ollama API at the same path prefix:
|
||||
For another host or port, set Ollama's OpenAI-compatible URL. Discovery still uses the native Ollama API at the same
|
||||
path prefix:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"settings": {
|
||||
@@ -190,12 +267,13 @@ native Ollama API at the same path prefix:
|
||||
}
|
||||
```
|
||||
|
||||
Omit `apiKey` when the Ollama endpoint does not require bearer authentication.
|
||||
- Omit `apiKey` when the endpoint does not require bearer authentication.
|
||||
- Disable discovery with `"plugins": ["-opencode.provider.ollama"]`.
|
||||
|
||||
#### LM Studio
|
||||
### LMStudio
|
||||
|
||||
OpenCode automatically discovers language models from an unauthenticated LM Studio server listening on its default
|
||||
address, `http://127.0.0.1:1234`. Discovered models use the `lmstudio` provider ID and LM Studio's model key:
|
||||
With an unauthenticated LM Studio server at `http://127.0.0.1:1234`, select a discovered model with the `lmstudio`
|
||||
provider ID:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -204,15 +282,13 @@ address, `http://127.0.0.1:1234`. Discovered models use the `lmstudio` provider
|
||||
}
|
||||
```
|
||||
|
||||
OpenCode refreshes the inventory in the background and reads context, vision, and tool-use capabilities from LM
|
||||
Studio. Embedding models are excluded because they cannot drive a session. Disable discovery with
|
||||
`"plugins": ["-opencode.provider.lmstudio"]`.
|
||||
OpenCode refreshes the inventory in the background and reads context, vision, and tool-use capabilities from LM Studio.
|
||||
Embedding models are excluded.
|
||||
|
||||
For a different host or port, configure the OpenAI-compatible base URL. Models are still discovered automatically:
|
||||
For another host or port, set the OpenAI-compatible URL. Models are still discovered automatically:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"providers": {
|
||||
"lmstudio": {
|
||||
"settings": {
|
||||
@@ -224,12 +300,12 @@ For a different host or port, configure the OpenAI-compatible base URL. Models a
|
||||
}
|
||||
```
|
||||
|
||||
Omit `apiKey` when LM Studio authentication is disabled.
|
||||
- Omit `apiKey` when LM Studio authentication is disabled.
|
||||
- Disable discovery with `"plugins": ["-opencode.provider.lmstudio"]`.
|
||||
|
||||
#### vLLM
|
||||
### vLLM
|
||||
|
||||
OpenCode automatically discovers models from a vLLM server listening on its default address, `http://127.0.0.1:8000`.
|
||||
Discovered models use the `vllm` provider ID and the model ID reported by vLLM:
|
||||
With vLLM listening at `http://127.0.0.1:8000`, select a discovered model with the `vllm` provider ID:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -238,17 +314,16 @@ Discovered models use the `vllm` provider ID and the model ID reported by vLLM:
|
||||
}
|
||||
```
|
||||
|
||||
OpenCode checks vLLM's `/health` endpoint and refreshes `/v1/models` in the background. It uses the reported
|
||||
`max_model_len` as the context limit and only includes model cards owned by `vllm`. Discovered vLLM models advertise
|
||||
text input and output, but not vision or tools. Tool calling is conservative because vLLM enables it with server-level
|
||||
flags such as `--enable-auto-tool-choice` and `--tool-call-parser`, which model discovery does not report. Disable
|
||||
discovery with `"plugins": ["-opencode.provider.vllm"]`.
|
||||
OpenCode checks `/health`, refreshes `/v1/models` in the background, uses the reported `max_model_len` as the context
|
||||
limit, and includes only model cards owned by `vllm`.
|
||||
|
||||
For a different endpoint or an authenticated server, configure its OpenAI-compatible base URL:
|
||||
Discovered models advertise text input and output, but not vision or tools. vLLM enables tool calling with server flags
|
||||
such as `--enable-auto-tool-choice` and `--tool-call-parser`, which discovery does not report.
|
||||
|
||||
For another endpoint or an authenticated server, set its OpenAI-compatible URL:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"providers": {
|
||||
"vllm": {
|
||||
"settings": {
|
||||
@@ -260,10 +335,14 @@ For a different endpoint or an authenticated server, configure its OpenAI-compat
|
||||
}
|
||||
```
|
||||
|
||||
Omit `apiKey` when authentication is disabled. Path-prefixed proxy URLs are supported; for example,
|
||||
`https://example.com/vllm/v1` checks `/vllm/health` and discovers `/vllm/v1/models`.
|
||||
- Omit `apiKey` when authentication is disabled.
|
||||
- Disable discovery with `"plugins": ["-opencode.provider.vllm"]`.
|
||||
- Path-prefixed proxies are supported. For example, `https://example.com/vllm/v1` checks `/vllm/health` and discovers
|
||||
`/vllm/v1/models`.
|
||||
|
||||
For an OpenAI-compatible server, define a provider package, endpoint, and at least one model:
|
||||
### Compatible
|
||||
|
||||
For another OpenAI-compatible server, define its provider package, endpoint, and at least one model:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -295,14 +374,13 @@ For an OpenAI-compatible server, define a provider package, endpoint, and at lea
|
||||
}
|
||||
```
|
||||
|
||||
Use the server's real model name, limits, modalities, and tool support. OpenCode applies the custom-model capability
|
||||
defaults described above but cannot infer the server's actual limits or whether those defaults are accurate. 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.
|
||||
Use the server's real model name, limits, input and output types, and tool support. OpenCode cannot detect whether the
|
||||
custom-model fallback values are accurate. If the endpoint requires a key, add `apiKey` to `settings` with an environment
|
||||
substitution such as `"apiKey": "{env:LOCAL_API_KEY}"`; do not commit secrets.
|
||||
|
||||
### Model references
|
||||
## References
|
||||
|
||||
Configuration fields and model-selection inputs identify a model as `provider/model`, with an optional `#variant`:
|
||||
Model selectors use `provider/model` with an optional `#variant`:
|
||||
|
||||
```text
|
||||
openai/gpt-5.2
|
||||
@@ -310,10 +388,15 @@ 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 `#`.
|
||||
| Part | Rule |
|
||||
| ---------- | ---------------------------------------------------------- |
|
||||
| Provider | Ends at the first `/`; cannot contain `/` or `#`. |
|
||||
| Model | May contain additional `/` characters; cannot contain `#`. |
|
||||
| Variant | Follows `#` when present. |
|
||||
| Casing | Provider and model IDs are case-sensitive. |
|
||||
|
||||
The expanded config form is equivalent when generated or programmatic configuration is more convenient:
|
||||
Use catalog IDs rather than provider display names. Root, agent, and command `model` fields accept the string form above
|
||||
or an expanded object:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -324,15 +407,23 @@ The expanded config form is equivalent when generated or programmatic configurat
|
||||
}
|
||||
```
|
||||
|
||||
Root, agent, and command `model` fields accept both forms. Use IDs from the available catalog, not provider display names.
|
||||
The expanded form is useful for generated or programmatic configuration.
|
||||
|
||||
### Caveats
|
||||
## 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 for the session, run, agent, or command instead.
|
||||
- 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.
|
||||
Keep these rules in mind when configuring models:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"model": "openai/gpt-5.2"
|
||||
}
|
||||
```
|
||||
|
||||
- A selector object uses `model`; a provider catalog entry uses `modelID` for the ID sent to the provider.
|
||||
- Although the root selection shape accepts a variant, the V2 catalog default does not retain it. Select variants for a
|
||||
session, run, agent, or command instead.
|
||||
- Model options are provider-specific. Another provider package may ignore or reject them.
|
||||
- Catalog data, credentials, and configuration are location-scoped. A model available in one project may be unavailable
|
||||
in another.
|
||||
- Configuration files normally reload automatically. A model request already in progress keeps the settings it started
|
||||
with.
|
||||
|
||||
@@ -2,135 +2,180 @@
|
||||
title: "Permissions"
|
||||
---
|
||||
|
||||
Permissions control whether an agent may perform an action on a resource. V2
|
||||
configuration uses the `permissions` field and an ordered array of rules.
|
||||
Permissions control whether an agent may perform an action on a resource.
|
||||
|
||||
<Callout type="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`.
|
||||
</Callout>
|
||||
## Configure
|
||||
|
||||
## Rule schema
|
||||
|
||||
Each rule has three required string fields:
|
||||
A common setup is to ask before running shell commands, allow routine Git
|
||||
inspection, and always block pushes. Add these ordered rules to `opencode.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"permissions": [
|
||||
{ "action": "*", "resource": "*", "effect": "ask" },
|
||||
{ "action": "shell", "resource": "*", "effect": "ask" },
|
||||
{ "action": "shell", "resource": "git status *", "effect": "allow" },
|
||||
{ "action": "shell", "resource": "git diff *", "effect": "allow" },
|
||||
{ "action": "shell", "resource": "git push *", "effect": "deny" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
The last matching rule wins, so the specific exceptions follow the broad rule.
|
||||
|
||||
<Callout type="warning">
|
||||
V1 uses different field and action names. In V2, use `permissions`, `shell`, and `subagent` instead of `permission`,
|
||||
`bash`, and `task`.
|
||||
</Callout>
|
||||
|
||||
## Rules
|
||||
|
||||
Each rule requires three string fields:
|
||||
|
||||
| Field | Meaning |
|
||||
| ---------- | -------------------------------------------------------------------------- |
|
||||
| `action` | Tool permission action |
|
||||
| `resource` | Value being used, such as a path, command, URL, query, skill ID, or agent ID |
|
||||
| `effect` | `allow`, `deny`, or `ask` |
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"permissions": [
|
||||
{ "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" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
| Effect | Result |
|
||||
| ------- | ----------------------------------- |
|
||||
| `allow` | Continue without prompting |
|
||||
| `deny` | Block the operation |
|
||||
| `ask` | Wait for a decision from the client |
|
||||
|
||||
If no rule matches, OpenCode uses `ask`.
|
||||
|
||||
## Matching
|
||||
|
||||
Actions and resources use simple whole-value wildcards:
|
||||
|
||||
| Pattern | Match |
|
||||
| ------- | ------------------------------------------ |
|
||||
| `*` | Zero or more characters, including `/` |
|
||||
| `?` | Exactly one character |
|
||||
| Other | The literal character |
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"permissions": [
|
||||
{ "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"`.
|
||||
This pattern matches the entire normalized path. Backslashes are normalized to
|
||||
slashes, and matching is case-insensitive on Windows.
|
||||
|
||||
`allow` proceeds without prompting, `deny` blocks the operation, and `ask`
|
||||
waits for a user decision. If no rule matches, the result is `ask`.
|
||||
A shell pattern ending in ` *` also matches the command without arguments:
|
||||
|
||||
## Matching and order
|
||||
```jsonc
|
||||
{ "action": "shell", "resource": "git status *", "effect": "allow" }
|
||||
```
|
||||
|
||||
Both `action` and `resource` support simple wildcards:
|
||||
This matches both `git status` and `git status --short`.
|
||||
|
||||
- `*` matches zero or more characters, including `/`.
|
||||
- `?` matches exactly one character.
|
||||
- All other characters are literal.
|
||||
OpenCode combines rules in order and uses the last match. Lower-priority
|
||||
configuration is loaded first, global rules are appended next, and agent rules
|
||||
are appended last.
|
||||
|
||||
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`.
|
||||
```jsonc
|
||||
{
|
||||
"permissions": [
|
||||
{ "action": "read", "resource": "*", "effect": "allow" },
|
||||
{ "action": "read", "resource": "secrets/*", "effect": "deny" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
Operations may check several resources, such as a patch that touches multiple
|
||||
files. Any `deny` denies the operation; otherwise any `ask` asks; otherwise the
|
||||
operation is allowed.
|
||||
|
||||
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
|
||||
|
||||
## Actions and resources
|
||||
V2 action names are strings, so plugins may define more actions. Built-in tools
|
||||
currently use these actions and resources:
|
||||
|
||||
V2 action names are strings, so plugins may introduce additional actions. The
|
||||
current built-in actions use these resources:
|
||||
| Action | Resource |
|
||||
| -------------------- | --------------------------------------------------------------------------- |
|
||||
| `read` | Location-relative internal path or canonical absolute external path |
|
||||
| `edit` | Target path for `edit`, `write`, and `patch` |
|
||||
| `glob` | Requested glob pattern |
|
||||
| `grep` | Requested regular expression, not the search path |
|
||||
| `shell` | Scanner-produced command string; compound commands may produce several |
|
||||
| `subagent` | Target agent ID |
|
||||
| `skill` | Skill ID |
|
||||
| `question` | `*` |
|
||||
| `webfetch` | Requested URL |
|
||||
| `websearch` | Search query |
|
||||
| `external_directory` | Canonical external directory boundary, normally ending in `/*` |
|
||||
| `<server>_<tool>` | `*` for an MCP tool; unsupported characters in both names become `_` |
|
||||
| `execute` | `*`; controls Code Mode availability, while nested tools enforce their rules |
|
||||
|
||||
| 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 |
|
||||
For example, allow one skill and deny all other skills:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"permissions": [
|
||||
{ "action": "skill", "resource": "*", "effect": "deny" },
|
||||
{ "action": "skill", "resource": "effect", "effect": "allow" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
`doom_loop` and `lsp` are not current V2 Core permission actions.
|
||||
|
||||
## External directories
|
||||
## Directories
|
||||
|
||||
A path outside both the active Location and its non-root project worktree
|
||||
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.
|
||||
A path outside both the active Location and its non-root project worktree needs
|
||||
`external_directory` approval before its `read` or `edit` approval.
|
||||
|
||||
```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",
|
||||
},
|
||||
{ "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.
|
||||
This applies to external paths used by `read`, `edit`, `write`, and `patch`.
|
||||
Shell checks its external working directory and directories inferred by its
|
||||
scanner before checking `shell` resources.
|
||||
|
||||
For `external_directory`, `read`, and `edit`, a leading `~`, `~/`, `$HOME`, or
|
||||
`$HOME/` is expanded when configuration loads:
|
||||
|
||||
```jsonc
|
||||
{ "action": "read", "resource": "$HOME/reference/*", "effect": "allow" }
|
||||
```
|
||||
|
||||
Shell resources remain raw command text and are not home-expanded.
|
||||
|
||||
<Callout type="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.
|
||||
`shell` runs with the host user's filesystem, process, and network authority. Directory inference from command text is
|
||||
best effort, so prefer a narrow shell allowlist instead of patterns intended to recognize every dangerous command.
|
||||
</Callout>
|
||||
|
||||
Relative mutation paths may traverse outside the active Location while
|
||||
remaining inside its project worktree. Paths outside both boundaries require
|
||||
`external_directory` approval. Explicit external paths are canonicalized before
|
||||
matching, so authorize only trusted directory boundaries.
|
||||
Relative mutation paths may leave the active Location while remaining inside
|
||||
its project worktree. Explicit external paths are canonicalized before matching,
|
||||
so authorize only trusted directory boundaries.
|
||||
|
||||
## Experimental shell scanner
|
||||
## Scanner
|
||||
|
||||
Set `experimental.portable_shell_scanner` to `true` to test the portable shell
|
||||
permission scanner. The default remains the tree-sitter scanner.
|
||||
Enable the experimental portable shell scanner with:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -141,60 +186,55 @@ permission scanner. The default remains the tree-sitter scanner.
|
||||
}
|
||||
```
|
||||
|
||||
When enabled, shell commands are analyzed only by the portable scanner.
|
||||
Tree-sitter is not used as a fallback or a second opinion. If the scanner
|
||||
cannot analyze a command, the shell tool reports a scanner error rather than
|
||||
silently retrying with Tree-sitter. These failures are experimental parser
|
||||
gaps to fix, not permission denials.
|
||||
The portable scanner replaces the default tree-sitter scanner; tree-sitter is
|
||||
not a fallback or second opinion. A command the portable scanner cannot analyze
|
||||
returns a scanner error, not a permission denial.
|
||||
|
||||
The flag changes parser selection, not permission policy. Existing rules,
|
||||
saved approval patterns, and best-effort directory inference continue to apply.
|
||||
There is no additional approval mode or blanket unknown-directory restriction.
|
||||
With the flag disabled, the existing Tree-sitter path is unchanged.
|
||||
The flag changes only parser selection. Existing permission rules, saved
|
||||
approval patterns, and best-effort directory inference still apply. There is no
|
||||
extra approval mode or blanket restriction for unknown directories.
|
||||
|
||||
## Defaults
|
||||
|
||||
Every agent, including custom agents, starts with ordered defaults that allow
|
||||
tools, ask for external directories, ask for `.env` reads, and allow
|
||||
`.env.example` reads. Shipped agents then add their own policies:
|
||||
|
||||
| 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:
|
||||
Every agent, including custom agents, starts with this ordered base policy:
|
||||
|
||||
```jsonc
|
||||
[
|
||||
{ "action": "read", "resource": "*", "effect": "allow" },
|
||||
{ "action": "*", "resource": "*", "effect": "allow" },
|
||||
{ "action": "external_directory", "resource": "*", "effect": "ask" },
|
||||
{ "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, shell-output, temporary, and
|
||||
global configuration directories. These exceptions apply only to the
|
||||
external-directory boundary for every agent; the underlying action still uses
|
||||
its own permission rules. The environment instructions identify the temporary
|
||||
directory available for work outside the workspace. Later global and
|
||||
agent-specific rules can override these defaults.
|
||||
Shipped agents append these policies:
|
||||
|
||||
## Agent overrides
|
||||
| Agent | Additional policy |
|
||||
| ------------ | ------------------------------------------------------------------------------ |
|
||||
| `build` | Allows questions |
|
||||
| `plan` | Allows questions; denies edits except files under `~/.opencode/plan` |
|
||||
| `general` | Denies questions and launching subagents |
|
||||
| `explore` | Denies everything except reads, globs, grep, web fetches, and web searches; asks for external directories and `.env` reads |
|
||||
| `title` | Denies all actions |
|
||||
| `summary` | Denies all actions |
|
||||
| `compaction` | Keeps the base policy |
|
||||
|
||||
Configure shared policy at the top level and append narrower rules to a named
|
||||
agent under `agents.<id>.permissions`:
|
||||
OpenCode also allows external-directory access to its managed tool-output,
|
||||
shell-output, temporary, and global configuration directories. The underlying
|
||||
`read`, `edit`, or other action still uses its own rules. Later global and agent
|
||||
rules can override these defaults.
|
||||
|
||||
## Agents
|
||||
|
||||
Put shared rules at the top level and narrower rules 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": {
|
||||
@@ -203,33 +243,37 @@ agent under `agents.<id>.permissions`:
|
||||
"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.
|
||||
Agent rules are appended after global rules; they do not replace the global
|
||||
array. A custom subagent uses its own permissions, not a subset of its parent's
|
||||
permissions.
|
||||
|
||||
## Approval choices
|
||||
## Approvals
|
||||
|
||||
When an `ask` rule matches, clients can reply with:
|
||||
When a rule resolves to `ask`, 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.
|
||||
| Choice | Reply | Result |
|
||||
| ------------ | ---------- | -------------------------------------------------------------------- |
|
||||
| Allow once | `once` | Approve only the pending request |
|
||||
| Allow always | `always` | Approve it and save the tool's proposed patterns for the project |
|
||||
| Reject | `reject` | Reject it and every other pending permission request in that session |
|
||||
|
||||
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 command prefixes, and skills and subagents propose their
|
||||
IDs. Review the confirmation carefully and remove saved approvals that are no
|
||||
longer needed.
|
||||
For example, choosing **Allow always** for `git status --short` may save a
|
||||
shell prefix that covers later `git status` commands:
|
||||
|
||||
Non-interactive clients must decide how to handle requests that require approval; explicit `deny` rules remain enforced.
|
||||
```text
|
||||
shell: git status * → allow
|
||||
```
|
||||
|
||||
Saved approvals are durable, project-scoped `allow` rules. They never override
|
||||
a configured `deny`. Tools choose the proposed saved pattern: some propose `*`,
|
||||
shell proposes command prefixes, and skills and subagents propose their IDs.
|
||||
Review broad approvals and remove those no longer needed.
|
||||
|
||||
Clients may attach feedback when rejecting. Non-interactive clients must decide
|
||||
how to handle approval requests; configured `deny` rules always remain enforced.
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
title: "Plugins"
|
||||
---
|
||||
|
||||
Load published packages, versioned packages, scoped packages, local plugin directories, or configured plugins from
|
||||
`opencode.json(c)`.
|
||||
Add published packages, versioned packages, scoped packages, or local plugin directories to `opencode.json(c)`.
|
||||
|
||||
## Configure
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -36,6 +37,8 @@ from lowest to highest precedence instead of replacing one another.
|
||||
./.opencode/opencode.jsonc
|
||||
```
|
||||
|
||||
## Discover
|
||||
|
||||
OpenCode also loads direct `.ts` and `.js` files and immediate plugin package directories from every discovered
|
||||
`.opencode/plugins/` directory.
|
||||
|
||||
@@ -62,6 +65,8 @@ explicitly or move it under `.opencode/`.
|
||||
}
|
||||
```
|
||||
|
||||
## Control
|
||||
|
||||
Plugin entries are processed in order. Prefix an ID or wildcard with `-` to disable it, use `*` for every plugin, and
|
||||
use `.*` to match an ID prefix. A later ID re-enables a plugin.
|
||||
|
||||
@@ -71,7 +76,9 @@ use `.*` to match an ID prefix. A later ID re-enables a plugin.
|
||||
}
|
||||
```
|
||||
|
||||
Install, inspect, list, or remove global package plugins with the CLI.
|
||||
## Manage
|
||||
|
||||
Install, list, check, update, or remove global package plugins with the CLI.
|
||||
|
||||
```sh
|
||||
opencode2 plugin add opencode-acme-plugin@1.2.0
|
||||
@@ -91,7 +98,7 @@ Git repositories can use hosted shortcuts, HTTPS, or SSH, including private repo
|
||||
Git credentials.
|
||||
|
||||
```sh
|
||||
opencode2 plugin add @acme/opencode-plugin@beta
|
||||
opencode2 plugin add @acme/opencode-plugin@latest
|
||||
opencode2 plugin add github:acme/opencode-plugin
|
||||
opencode2 plugin add git+ssh://git@github.com/acme/opencode-plugin.git#main
|
||||
opencode2 plugin add 'github:acme/plugins#main::path:packages/opencode-plugin'
|
||||
@@ -100,6 +107,8 @@ opencode2 plugin add 'github:acme/plugins#main::path:packages/opencode-plugin'
|
||||
Branches, tags, complete commit hashes, and npm's `::path:` repository-subdirectory selectors are supported. Configure
|
||||
local paths directly; tarball and npm alias targets are not accepted by `plugin add`.
|
||||
|
||||
## Reload
|
||||
|
||||
Changes under watched config directories reload automatically. Server startup loads cached package plugins immediately,
|
||||
installs missing packages in the background, and checks unpinned npm and Git plugins for updates without changing the
|
||||
installed package. Exact npm versions and full Git commit hashes stay pinned. Changes to unwatched local dependencies may
|
||||
@@ -110,6 +119,8 @@ touch .opencode/plugins/concise/index.ts
|
||||
opencode2 service restart
|
||||
```
|
||||
|
||||
## Terminal
|
||||
|
||||
CLI-only plugins are configured separately and remain active when connected to a remote server.
|
||||
|
||||
```json title="cli.json"
|
||||
|
||||
@@ -2,8 +2,35 @@
|
||||
title: "Providers"
|
||||
---
|
||||
|
||||
OpenCode includes a built-in catalog of providers and models from [models.dev](https://models.dev). You can also add a
|
||||
custom provider to your configuration.
|
||||
OpenCode includes a provider and model catalog from [models.dev](https://models.dev). For most providers, connect an
|
||||
account first, then select a model.
|
||||
|
||||
## Setup
|
||||
|
||||
Run `/connect`, choose a provider, and enter its credentials. Then run `/models` to select one of its models.
|
||||
|
||||
```text
|
||||
/connect
|
||||
/models
|
||||
```
|
||||
|
||||
## Go
|
||||
|
||||
[OpenCode Go](/console/go) is an optional subscription for coding models tested by the OpenCode team. Subscribe in the
|
||||
[console](https://console.opencode.ai), copy your API key, then connect it as **OpenCode Go**.
|
||||
|
||||
```text
|
||||
/connect
|
||||
# Select OpenCode Go, then paste your API key.
|
||||
/models
|
||||
```
|
||||
|
||||
See the [Go guide](/console/go) for usage limits, endpoints, and privacy details.
|
||||
|
||||
## Custom
|
||||
|
||||
Add a provider when its API is not already in the catalog. This OpenAI-compatible example defines the credential,
|
||||
runtime package, endpoint, and first model together.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -26,121 +53,25 @@ custom provider to your configuration.
|
||||
}
|
||||
```
|
||||
|
||||
The `providers` object is keyed by provider ID. Each provider accepts these fields:
|
||||
The `providers` object is keyed by the provider ID used in model references, such as `acme/qwen3-coder`.
|
||||
|
||||
| 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. |
|
||||
|
||||
## OpenCode Go
|
||||
|
||||
[OpenCode Go](/console/go) is an optional subscription that provides access to coding models tested by the OpenCode team.
|
||||
Subscribe in the [console](https://console.opencode.ai), copy your API key, then run `/connect` in the TUI and select
|
||||
**OpenCode Go**:
|
||||
|
||||
```text
|
||||
/connect
|
||||
```
|
||||
|
||||
Paste your API key, then run `/models` to select a Go model. See the [Go guide](/console/go) for setup, usage limits,
|
||||
endpoints, and privacy details.
|
||||
|
||||
## Azure OpenAI and Microsoft Foundry
|
||||
|
||||
Azure supports either an API key or your existing Microsoft Entra ID session from the Azure CLI.
|
||||
|
||||
1. Install the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) and sign in:
|
||||
|
||||
```bash
|
||||
az login
|
||||
```
|
||||
|
||||
If the resource belongs to another tenant or subscription, select them first:
|
||||
|
||||
```bash
|
||||
az login --tenant TENANT_ID
|
||||
az account set --subscription NAME_OR_ID
|
||||
```
|
||||
|
||||
2. Find your Azure resource name in the [Azure portal](https://portal.azure.com/) or
|
||||
[Microsoft Foundry](https://ai.azure.com/): open the Azure OpenAI or Foundry resource and copy its **Resource name**.
|
||||
It is also the first part of the endpoint: `my-models` in `https://my-models.openai.azure.com/` or
|
||||
`https://my-models.services.ai.azure.com/`.
|
||||
|
||||
If your identity can list resources, the Azure CLI can display the names and resource groups:
|
||||
|
||||
```bash
|
||||
az cognitiveservices account list \
|
||||
--query "[].{name:name,resourceGroup:resourceGroup}" \
|
||||
--output table
|
||||
```
|
||||
|
||||
3. In OpenCode, run `/connect`, select **Azure**, and choose **Microsoft Entra ID (Azure CLI)**. Enter the Azure resource
|
||||
name when prompted. If `AZURE_RESOURCE_NAME` is already set, OpenCode uses it without prompting.
|
||||
|
||||
4. Select a deployed model with `/models`.
|
||||
|
||||
OpenCode does not query Azure management APIs or discover deployments. Select a model whose catalog name matches your
|
||||
deployment, or configure the deployment name explicitly:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"providers": {
|
||||
"azure": {
|
||||
"models": {
|
||||
"gpt-5-mini": {
|
||||
"modelID": "gpt-production",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Your identity needs the **Cognitive Services OpenAI User** role for Azure OpenAI models or the **Cognitive Services User**
|
||||
role for other Foundry models. If a request fails because the token belongs to another tenant, sign in again with
|
||||
`az login --tenant TENANT_ID`.
|
||||
|
||||
## WebSocket transport
|
||||
|
||||
OpenAI, xAI, and supported Azure Responses models keep one WebSocket connection open per session and send each step
|
||||
over it instead of opening a new HTTP request. While the request prefix is unchanged, consecutive steps only transmit
|
||||
what was added since the previous response, which cuts upload volume on long sessions. OpenAI provider compaction runs
|
||||
over the same connection. xAI continues a chain only from stored responses, so with its default `store: false` each step
|
||||
is sent in full over the reused connection.
|
||||
|
||||
The connection is transparent. When the provider closes the socket, the next step reconnects; when a connection cannot
|
||||
be opened at all, the session continues over HTTP. Plugins that register `http.request` or `http.response` hooks for a
|
||||
provider keep it on HTTP so the hooks observe every request.
|
||||
|
||||
WebSockets are opt-in per built-in provider. Set `websocket: true` to enable a supported provider or model, or `false`
|
||||
to disable a built-in opt-in; a model policy overrides the provider policy:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"providers": {
|
||||
"openai": {
|
||||
"websocket": false,
|
||||
"models": {
|
||||
"gpt-5.5": { "websocket": true },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
| Field | Purpose |
|
||||
| --- | --- |
|
||||
| `name` | Display name. |
|
||||
| `env` | Ordered environment variable names that can provide the credential. |
|
||||
| `package` | Runtime provider package. |
|
||||
| `canonical` | Built-in provider ID whose catalog defaults this provider inherits. |
|
||||
| `settings` | JSON options passed to the runtime package. |
|
||||
| `headers` | String-valued HTTP headers added to requests. |
|
||||
| `body` | JSON fields merged into request bodies. |
|
||||
| `models` | Models to add or override, keyed by the OpenCode model ID. |
|
||||
| `websocket` | Provider-level WebSocket policy for supported routes. |
|
||||
| `compaction` | Local or provider [compaction](/compaction) policy. |
|
||||
|
||||
## Endpoint
|
||||
|
||||
Override `settings.baseURL` to send an existing provider through a proxy or compatible endpoint. Its existing package,
|
||||
models, and connection continue to apply:
|
||||
Override `settings.baseURL` to send a catalog provider through a proxy or compatible endpoint. Its existing package,
|
||||
models, and connection still apply.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -157,10 +88,9 @@ models, and connection continue to apply:
|
||||
|
||||
`settings` is package-specific. A field only has an effect when the selected package supports it.
|
||||
|
||||
## Headers and body
|
||||
## Requests
|
||||
|
||||
Use `headers` to add HTTP headers to provider requests. Use `body` to merge additional JSON fields into each request
|
||||
body:
|
||||
Use `headers` for additional HTTP headers and `body` for JSON fields that should be merged into every request body.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -180,10 +110,33 @@ body:
|
||||
}
|
||||
```
|
||||
|
||||
## Package
|
||||
Both fields can also be set on a model or variant when only part of a provider's traffic needs the override.
|
||||
|
||||
The `package` field selects the runtime used to communicate with a provider. For an OpenAI-compatible API, use the
|
||||
built-in compatible package:
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"providers": {
|
||||
"openai": {
|
||||
"models": {
|
||||
"gpt-5.2": {
|
||||
"headers": { "X-Model-Tier": "coding" },
|
||||
"variants": [
|
||||
{
|
||||
"id": "batch",
|
||||
"body": { "service_tier": "flex" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Packages
|
||||
|
||||
The `package` field selects the runtime that communicates with a provider. Use the compatible runtime for APIs that
|
||||
implement the OpenAI request format.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -195,16 +148,14 @@ built-in compatible package:
|
||||
"baseURL": "https://llm.acme.example/v1",
|
||||
},
|
||||
"models": {
|
||||
"qwen3-coder": {
|
||||
"name": "Qwen 3 Coder",
|
||||
},
|
||||
"qwen3-coder": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Native package options:
|
||||
Native package options include:
|
||||
|
||||
- `@opencode/ai/providers/openai`
|
||||
- `@opencode/ai/providers/openai/chat`
|
||||
@@ -231,12 +182,22 @@ Native package options:
|
||||
|
||||
You can also use an npm package such as `@acme/opencode-provider` or an absolute `file://` URL for a local package.
|
||||
|
||||
Use `settings` for options supported by the selected package.
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"providers": {
|
||||
"acme": {
|
||||
"package": "@acme/opencode-provider",
|
||||
"models": { "acme-coder": {} },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 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:
|
||||
Add or override models in a provider's `models` map. The map key is the model ID used by OpenCode; `modelID` changes the
|
||||
ID sent to the provider.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -255,4 +216,116 @@ the provider:
|
||||
}
|
||||
```
|
||||
|
||||
See [Models](/models) for model selection, defaults, capabilities, limits, costs, and variants.
|
||||
| Field | Purpose |
|
||||
| --- | --- |
|
||||
| `modelID` | Model or deployment ID sent to the provider. |
|
||||
| `name` | Display name. |
|
||||
| `family` | Model family used for grouping related models. |
|
||||
| `package` | Runtime override for this model. |
|
||||
| `settings` | Package-specific JSON settings. |
|
||||
| `headers` | Additional string-valued request headers. |
|
||||
| `body` | Additional JSON request body fields. |
|
||||
| `capabilities` | Tool support plus accepted input and output media types. |
|
||||
| `compatibility` | Request and response compatibility overrides. |
|
||||
| `variants` | Named variants with their own `settings`, `headers`, and `body`. |
|
||||
| `cost` | Input, output, and optional cache pricing per million tokens. |
|
||||
| `limit` | Context, input, and output token limits. |
|
||||
| `disabled` | Removes the model from selection when `true`. |
|
||||
| `websocket` | Model-level WebSocket policy; overrides the provider policy. |
|
||||
| `compaction` | Model-level [compaction](/compaction) policy; overrides the provider policy. |
|
||||
|
||||
See [Models](/models) for selection, defaults, capabilities, limits, costs, and variants.
|
||||
|
||||
## Azure
|
||||
|
||||
Azure supports either an API key or the Microsoft Entra ID session from the Azure CLI. To use Entra ID, install the
|
||||
[Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) and sign in before connecting.
|
||||
|
||||
```bash
|
||||
az login
|
||||
```
|
||||
|
||||
For a resource in another tenant or subscription, select both explicitly.
|
||||
|
||||
```bash
|
||||
az login --tenant TENANT_ID
|
||||
az account set --subscription NAME_OR_ID
|
||||
```
|
||||
|
||||
Find the **Resource name** in the [Azure portal](https://portal.azure.com/) or
|
||||
[Microsoft Foundry](https://ai.azure.com/). It is also the first part of endpoints such as
|
||||
`https://my-models.openai.azure.com/` and `https://my-models.services.ai.azure.com/`.
|
||||
|
||||
```bash
|
||||
az cognitiveservices account list \
|
||||
--query "[].{name:name,resourceGroup:resourceGroup}" \
|
||||
--output table
|
||||
```
|
||||
|
||||
In OpenCode, select **Azure**, then **Microsoft Entra ID (Azure CLI)**. Enter the resource name when prompted;
|
||||
`AZURE_RESOURCE_NAME` skips that prompt when already set.
|
||||
|
||||
```text
|
||||
/connect
|
||||
/models
|
||||
```
|
||||
|
||||
OpenCode does not query Azure management APIs or discover deployments. If a deployment does not match its catalog
|
||||
model name, map an OpenCode model ID to the deployment with `modelID`.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"providers": {
|
||||
"azure": {
|
||||
"models": {
|
||||
"gpt-5-mini": {
|
||||
"modelID": "gpt-production",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Your identity needs one of these roles:
|
||||
|
||||
- **Cognitive Services OpenAI User** for Azure OpenAI models.
|
||||
- **Cognitive Services User** for other Foundry models.
|
||||
|
||||
If a request uses a token from the wrong tenant, sign in again with the required tenant.
|
||||
|
||||
```bash
|
||||
az login --tenant TENANT_ID
|
||||
```
|
||||
|
||||
## WebSockets
|
||||
|
||||
OpenAI, xAI, and supported Azure Responses models can keep one WebSocket connection open per session. Consecutive steps
|
||||
reuse the unchanged request prefix and only send content added since the previous response, reducing uploads in long
|
||||
sessions.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"providers": {
|
||||
"openai": {
|
||||
"websocket": false,
|
||||
"models": {
|
||||
"gpt-5.5": { "websocket": true },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
WebSocket behavior follows these rules:
|
||||
|
||||
- Built-in providers opt supported models in according to their own policy.
|
||||
- `websocket: true` enables a supported provider or model; `false` disables it.
|
||||
- A model value overrides its provider value.
|
||||
- OpenAI provider compaction uses the same connection.
|
||||
- xAI continues from stored responses only. With its default `store: false`, each step is sent in full over the reused
|
||||
connection.
|
||||
- A closed socket reconnects on the next step. If the connection cannot open, the session continues over HTTP.
|
||||
- Provider plugins with `http.request` or `http.response` hooks stay on HTTP so each request remains observable.
|
||||
|
||||
@@ -3,10 +3,8 @@ title: "References"
|
||||
---
|
||||
|
||||
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`:
|
||||
project. Add one in `opencode.json` or `opencode.jsonc`, then attach its alias in
|
||||
your client when you need it.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -16,16 +14,20 @@ Configure references by alias in `opencode.json` or `opencode.jsonc`:
|
||||
"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
|
||||
After attaching `docs`, ask the agent to inspect the relevant path:
|
||||
|
||||
```text
|
||||
Read the attached docs reference and summarize api/authentication.md.
|
||||
```
|
||||
|
||||
Use references for documentation, shared libraries, examples, or source from
|
||||
another repository.
|
||||
|
||||
## Local
|
||||
|
||||
Use `path` for a local directory:
|
||||
|
||||
@@ -40,11 +42,13 @@ Use `path` for a local directory:
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
Paths behave as follows:
|
||||
|
||||
The string shorthand is useful when no other fields are needed:
|
||||
- Relative paths resolve from the directory containing the config file.
|
||||
- Absolute paths are supported.
|
||||
- Home-relative paths such as `~/docs` are supported.
|
||||
|
||||
Use the string shorthand when no other fields are needed:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -56,14 +60,13 @@ The string shorthand is useful when no other fields are needed:
|
||||
```
|
||||
|
||||
<Callout type="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.
|
||||
A shorthand string is a local path only when it starts with `.`, `/`, or `~`. Use `./docs`, not `docs`; a bare `docs`
|
||||
value is interpreted as a Git repository.
|
||||
</Callout>
|
||||
|
||||
## Git repositories
|
||||
## Git
|
||||
|
||||
Use `repository` for a remote Git repository. GitHub `owner/repo` shorthand,
|
||||
Git URLs, host/path forms, and SCP-style remotes are supported.
|
||||
Use `repository` for a remote Git repository:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -80,12 +83,16 @@ Git URLs, host/path forms, and SCP-style remotes are supported.
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
Supported remote forms include:
|
||||
|
||||
Git references also support shorthand:
|
||||
- GitHub `owner/repo` shorthand
|
||||
- Git URLs
|
||||
- Host/path forms
|
||||
- SCP-style remotes
|
||||
|
||||
Local `file:` repositories are not supported.
|
||||
|
||||
Use the string shorthand to follow the remote's default branch:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -96,47 +103,114 @@ Git references also support shorthand:
|
||||
}
|
||||
```
|
||||
|
||||
### Cloning and storage
|
||||
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 `..`.
|
||||
|
||||
OpenCode normalizes a remote and stores one checkout per remote and branch
|
||||
under its global data directory. Without an explicit branch, the checkout is
|
||||
stored at `opencode/repos/<host>/<repository-path>`. On a typical Linux
|
||||
installation, for example, `Effect-TS/effect` is stored at:
|
||||
```jsonc
|
||||
{
|
||||
"references": {
|
||||
"sdk": {
|
||||
"repository": "example/sdk",
|
||||
"branch": "release/v2.1",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Storage
|
||||
|
||||
OpenCode normalizes each remote and stores one checkout per remote and branch
|
||||
under its global data directory. Without an explicit branch, the path uses this
|
||||
shape:
|
||||
|
||||
```text
|
||||
opencode/repos/<host>/<repository-path>
|
||||
```
|
||||
|
||||
On a typical Linux installation, `Effect-TS/effect` is stored at:
|
||||
|
||||
```text
|
||||
~/.local/share/opencode/repos/github.com/Effect-TS/effect
|
||||
```
|
||||
|
||||
An explicit branch adds an encoded `@<branch>` suffix to the checkout path.
|
||||
An explicit branch adds an encoded `@<branch>` suffix:
|
||||
|
||||
Missing repositories are cloned. When references load or reload, and after a
|
||||
new user prompt is admitted in their Location, OpenCode checks them in the
|
||||
background. An existing checkout is eligible when its last refresh attempt was
|
||||
at least 24 hours ago, or no attempt has been recorded. A refresh fetches and
|
||||
resets to the requested branch, or to the remote default branch when `branch`
|
||||
is omitted.
|
||||
```text
|
||||
~/.local/share/opencode/repos/github.com/Effect-TS/effect@main
|
||||
```
|
||||
|
||||
Refresh timestamps persist across service restarts and are shared by Locations
|
||||
using the same checkout. Failed refresh attempts are logged and remain subject
|
||||
to the 24-hour limit. There is no periodic polling while a Location is unused.
|
||||
## Refresh
|
||||
|
||||
Prompts do not wait for background refreshes. An attachment can therefore
|
||||
contain older content even if a later tool read sees the updated checkout.
|
||||
Initial cloning is also asynchronous, so a new reference can appear before its
|
||||
checkout is ready. Clone and refresh failures do not stop other references
|
||||
from loading.
|
||||
Missing repositories are cloned asynchronously. Existing checkouts are checked
|
||||
in the background when references load or reload and after a new user prompt is
|
||||
admitted in their Location.
|
||||
|
||||
```text
|
||||
Prompt admitted → refresh check starts in background → agent continues
|
||||
```
|
||||
|
||||
A checkout is eligible when its last refresh attempt was at least 24 hours ago,
|
||||
or when no attempt has been recorded. A refresh fetches and resets to the
|
||||
configured branch or the remote's default branch.
|
||||
|
||||
```text
|
||||
Last attempt: 25 hours ago → eligible
|
||||
Last attempt: 2 hours ago → skipped
|
||||
```
|
||||
|
||||
Refresh behavior follows these rules:
|
||||
|
||||
- Timestamps persist across service restarts.
|
||||
- Locations using the same checkout share its timestamp.
|
||||
- Failed attempts are logged and remain subject to the 24-hour limit.
|
||||
- There is no periodic polling while a Location is unused.
|
||||
- Clone or refresh failures do not stop other references from loading.
|
||||
|
||||
Prompts do not wait for cloning or refreshing. An attachment can contain older
|
||||
content even if a later tool read sees the updated checkout, and a new reference
|
||||
can appear before its checkout is ready.
|
||||
|
||||
```text
|
||||
Attachment created → cached content
|
||||
Background refresh completes → later tool read sees newer content
|
||||
```
|
||||
|
||||
<Callout type="warning">
|
||||
Cached checkouts are shared and can update while an agent is using them. Avoid editing cached checkouts because a
|
||||
refresh resets them.
|
||||
</Callout>
|
||||
|
||||
## Description and visibility
|
||||
## Guidance
|
||||
|
||||
`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 to clients but are not advertised
|
||||
automatically.
|
||||
description appear in agent instructions with their alias and resolved path.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"references": {
|
||||
"docs": {
|
||||
"path": "../docs",
|
||||
"description": "Use for product behavior and terminology",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
References without a description remain available to clients but are not
|
||||
advertised automatically.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"references": {
|
||||
"archive": {
|
||||
"path": "../archive",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Visibility
|
||||
|
||||
Set `hidden` to `true` to remove a reference from interactive client selectors:
|
||||
|
||||
@@ -152,20 +226,32 @@ Set `hidden` to `true` to remove a reference from interactive client selectors:
|
||||
}
|
||||
```
|
||||
|
||||
`hidden` controls only interactive visibility. It does not remove the
|
||||
reference from the reference API or agent instructions when a description is
|
||||
present.
|
||||
`hidden` affects only interactive visibility. The reference remains in the
|
||||
reference API and, when it has a description, in agent instructions.
|
||||
|
||||
## Use references
|
||||
## Usage
|
||||
|
||||
Clients can attach a reference by its root alias. The attachment provides a
|
||||
non-recursive listing of the root's immediate files and directories. Ask the
|
||||
agent to inspect a particular path when more detail is needed.
|
||||
Clients attach a reference by its root alias. The attachment contains a
|
||||
non-recursive listing of the root's immediate files and directories.
|
||||
|
||||
```text
|
||||
Attached alias: docs
|
||||
Ask: Inspect docs/guides/deployment.md and explain the deployment steps.
|
||||
```
|
||||
|
||||
Ask the agent to inspect a specific path when you need content below the root.
|
||||
|
||||
## Permissions
|
||||
|
||||
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.
|
||||
Location still follows normal tool rules and the `external_directory`
|
||||
permission; editing also requires the applicable edit permission.
|
||||
|
||||
```text
|
||||
Read ../product-docs/api.md
|
||||
```
|
||||
|
||||
The request succeeds only when the active permissions allow that external read.
|
||||
|
||||
## Fields
|
||||
|
||||
@@ -177,5 +263,28 @@ applicable edit permission.
|
||||
| `description` | Optional | Optional | Guidance describing when agents should use it |
|
||||
| `hidden` | Optional | Optional | Hide it from interactive client selectors |
|
||||
|
||||
A complete Git entry can use every Git-compatible field:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"references": {
|
||||
"sdk": {
|
||||
"repository": "example/sdk",
|
||||
"branch": "main",
|
||||
"description": "Use for SDK implementation details",
|
||||
"hidden": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Aliases
|
||||
|
||||
An alias cannot be empty or contain `/`, `\`, whitespace, a backtick, or a
|
||||
comma.
|
||||
|
||||
```text
|
||||
Valid: docs
|
||||
Invalid: product docs
|
||||
Invalid: docs/api
|
||||
```
|
||||
|
||||
@@ -1,35 +1,5 @@
|
||||
---
|
||||
title: "Session sharing"
|
||||
title: "Sharing"
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
There is no functional share or unshare server API endpoint.
|
||||
|
||||
## 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.
|
||||
OpenCode V2 does not support session sharing yet.
|
||||
|
||||
@@ -2,23 +2,7 @@
|
||||
title: "Skills"
|
||||
---
|
||||
|
||||
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
|
||||
```
|
||||
Create a skill to give an agent reusable instructions for a specific task. Put a `SKILL.md` file in `.opencode/skills/<skill-id>` and describe when to use it in `description`.
|
||||
|
||||
```markdown title=".opencode/skills/git-release/SKILL.md"
|
||||
---
|
||||
@@ -34,11 +18,27 @@ description: Prepare release notes, version bumps, and GitHub releases
|
||||
4. Run `scripts/changelog.ts` only after the user approves the version.
|
||||
```
|
||||
|
||||
Paths in a skill are relative to the directory containing `SKILL.md`.
|
||||
OpenCode advertises this skill when it is relevant. The agent can then load its instructions with the `skill` tool instead of adding every skill to every prompt.
|
||||
|
||||
## Create
|
||||
|
||||
Keep related scripts, references, and templates beside `SKILL.md`:
|
||||
|
||||
```text
|
||||
.opencode/skills/
|
||||
└── git-release/
|
||||
├── SKILL.md
|
||||
├── scripts/
|
||||
│ └── changelog.ts
|
||||
└── references/
|
||||
└── release-policy.md
|
||||
```
|
||||
|
||||
Paths written inside the skill are relative to the directory containing `SKILL.md`. The directory form is recommended because it gives supporting files a private base directory.
|
||||
|
||||
## Discovery
|
||||
|
||||
OpenCode automatically adds the following source directories:
|
||||
OpenCode automatically searches these locations:
|
||||
|
||||
| Scope | Sources |
|
||||
| --------------------- | -------------------------------------- |
|
||||
@@ -47,21 +47,23 @@ OpenCode automatically adds the following source directories:
|
||||
| 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.
|
||||
For project sources, OpenCode searches from the current directory up to the project root and includes matching directories at every level.
|
||||
|
||||
Within each source directory, OpenCode discovers:
|
||||
Each source can contain either form:
|
||||
|
||||
- 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`
|
||||
```text
|
||||
skills/
|
||||
├── review.md
|
||||
└── git-release/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
The directory form is recommended because it gives the skill a private base
|
||||
directory for supporting files.
|
||||
- Markdown files must be at the source root, such as `skills/review.md`.
|
||||
- Files named exactly `SKILL.md` can be at any depth, such as `skills/git-release/SKILL.md`.
|
||||
|
||||
## Configure sources
|
||||
## Sources
|
||||
|
||||
Use the `skills` array in any `opencode.json` or `opencode.jsonc` to add local
|
||||
directories or HTTP catalogs:
|
||||
Add more local directories or HTTP catalogs with the `skills` array in any `opencode.json` or `opencode.jsonc`:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -75,17 +77,18 @@ directories or HTTP catalogs:
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
| Value | Resolution |
|
||||
| ----- | ---------- |
|
||||
| Relative path | From the active OpenCode working directory, not the config file |
|
||||
| `~/` path | From the current user's home directory |
|
||||
| Absolute path | Used as written |
|
||||
| `http://` or `https://` URL | Loaded as an HTTP catalog |
|
||||
|
||||
Every discovered config document contributes its `skills` entries; the arrays
|
||||
are additive rather than replacing one another.
|
||||
Every discovered config file contributes its entries. `skills` arrays are combined rather than replaced.
|
||||
|
||||
### HTTP catalogs
|
||||
## Catalogs
|
||||
|
||||
An HTTP source is a base URL containing an `index.json`:
|
||||
An HTTP catalog is a base URL with an `index.json` file:
|
||||
|
||||
```json title="index.json"
|
||||
{
|
||||
@@ -99,41 +102,47 @@ An HTTP source is a base URL containing an `index.json`:
|
||||
}
|
||||
```
|
||||
|
||||
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`.
|
||||
For that entry, OpenCode downloads each file from `<base-url>/git-release/<file>`.
|
||||
|
||||
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.
|
||||
| Rule | Requirement |
|
||||
| ---- | ----------- |
|
||||
| Paths | Must be safe, relative, and same-origin |
|
||||
| Entry file | Include `SKILL.md` or `<name>.md`, such as `git-release.md` |
|
||||
| Updates | Increment `version` when files change so OpenCode refreshes its cache |
|
||||
|
||||
Prefer the named Markdown form in an HTTP catalog. Each downloaded skill directory becomes a source root, so `git-release.md` has the ID `git-release`. A root-level `SKILL.md` currently has the literal ID `SKILL` in V2.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
V2 reads these fields:
|
||||
Use frontmatter to name the skill and control where it appears:
|
||||
|
||||
| Field | Purpose |
|
||||
```markdown title="SKILL.md"
|
||||
---
|
||||
name: Git Release
|
||||
description: Prepare a repository release
|
||||
slash: true
|
||||
metadata:
|
||||
opencode/autoinvoke: false
|
||||
---
|
||||
|
||||
Prepare the changelog, version bump, tag, and release notes.
|
||||
```
|
||||
|
||||
| Field | Behavior |
|
||||
| ------------------------------ | ------------------------------------------------------------------ |
|
||||
| `name` | Display name; defaults to the path-derived ID |
|
||||
| `description` | Summary used for model-facing discovery |
|
||||
| `description` | Summary used to show the skill to the model |
|
||||
| `slash` | Set to `false` to hide the skill from interactive command catalogs |
|
||||
| `metadata.opencode/slash` | Boolean or `"true"`/`"false"`; overrides `slash` |
|
||||
| `metadata.opencode/autoinvoke` | Set to `false` to omit the skill from model-facing discovery |
|
||||
| `metadata.opencode/autoinvoke` | Set to `false` to omit the skill from the model's available list |
|
||||
|
||||
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.
|
||||
All frontmatter is optional at runtime. Add a clear `description` when the model should discover the skill; skills without one are not advertised.
|
||||
|
||||
`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.
|
||||
`opencode/autoinvoke: false` only hides the skill from the model's available list. The skill remains registered and can still be loaded explicitly by ID. V2 accepts portability fields such as `license` and `compatibility` but does not interpret them.
|
||||
|
||||
## IDs and validation
|
||||
## IDs
|
||||
|
||||
The skill ID comes from its path, not its frontmatter:
|
||||
The file path determines the skill ID. The frontmatter `name` is only a display label.
|
||||
|
||||
| File | ID |
|
||||
| --------------------------------- | ------------- |
|
||||
@@ -141,54 +150,58 @@ The skill ID comes from its path, not its frontmatter:
|
||||
| `<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:
|
||||
IDs are exact and case-sensitive. For portable 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]+)*$
|
||||
```
|
||||
|
||||
V2 currently does not enforce this pattern, the 1–64 character recommendation, a match between `name` and the directory, or a maximum description length.
|
||||
|
||||
## 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:
|
||||
Skills are selected by ID. If two sources define `git-release`, the source registered later supplies the skill that OpenCode loads:
|
||||
|
||||
```text
|
||||
~/.config/opencode/skills/git-release/SKILL.md ← lower precedence
|
||||
.opencode/skills/git-release/SKILL.md ← loaded
|
||||
```
|
||||
|
||||
Sources are registered from lower to higher precedence:
|
||||
|
||||
1. Built-in skills
|
||||
2. `.claude/skills` sources, global first and then from the farthest ancestor toward the current directory
|
||||
3. `.agents/skills` sources, global first and then from the farthest ancestor toward the current directory
|
||||
2. `.claude/skills`, global first and then from the farthest ancestor toward the current directory
|
||||
3. `.agents/skills`, global first and then from the farthest ancestor toward the current directory
|
||||
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.
|
||||
Avoid duplicate IDs unless you intend to override an earlier skill.
|
||||
|
||||
## Runtime loading
|
||||
## 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.
|
||||
At each model step, OpenCode lists permitted skills that have a description and do not set `opencode/autoinvoke` to `false`. The list includes only the ID, name, and description, not the full Markdown body.
|
||||
|
||||
When the model calls the `skill` tool with an exact ID, OpenCode:
|
||||
The model loads a skill by calling the `skill` tool with its exact ID:
|
||||
|
||||
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
|
||||
```json title="skill tool input"
|
||||
{
|
||||
"id": "git-release"
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
OpenCode then:
|
||||
|
||||
1. Selects the current definition for that ID.
|
||||
2. Checks the selected agent's `skill` permission.
|
||||
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 reads them when the skill directs it to do so. The file sample is available for directory-based `SKILL.md` skills; flat Markdown skills do not receive a neighboring file list.
|
||||
|
||||
## 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:
|
||||
Use the `skill` action and the skill ID as the resource. Rules run in order, and the last matching rule wins:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -200,18 +213,28 @@ are evaluated in order, with the last matching rule winning:
|
||||
}
|
||||
```
|
||||
|
||||
`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.
|
||||
| Effect | Behavior |
|
||||
| ------ | -------- |
|
||||
| `allow` | Advertises and loads matching skills without approval |
|
||||
| `ask` | Advertises matching skills and asks before loading them |
|
||||
| `deny` | Hides matching skills from the model and rejects loading |
|
||||
|
||||
Place the same rules under `agents.<id>.permissions` to apply them only to one agent.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For example, this skill's ID is `release`, not its frontmatter name `Git Release`:
|
||||
|
||||
```text
|
||||
.opencode/skills/release/SKILL.md
|
||||
└─ ID: release
|
||||
```
|
||||
|
||||
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.
|
||||
2. Check the path-derived, case-sensitive ID rather than the frontmatter `name`.
|
||||
3. Add a `description` if the model should discover the skill.
|
||||
4. Check `opencode/autoinvoke` and the selected agent's `skill` permissions.
|
||||
5. Look for a later source defining the same ID.
|
||||
5. Look for a later source that defines the same ID.
|
||||
6. For HTTP catalogs, verify `index.json`, same-origin file paths, and a changed `version`.
|
||||
|
||||
@@ -2,13 +2,104 @@
|
||||
title: "Snapshots"
|
||||
---
|
||||
|
||||
OpenCode snapshots let clients roll back conversation history and related file changes. They are a convenience for
|
||||
revising recent work, not a replacement for Git commits or backups.
|
||||
Snapshots let you undo recent conversation and file changes together. Use them to revise recent work, but keep Git commits
|
||||
or backups for anything important.
|
||||
|
||||
## Configuration
|
||||
## Workflow
|
||||
|
||||
Snapshots are enabled by default. Set `snapshots` to `false` in your [configuration](/config#snapshots) to stop capturing
|
||||
filesystem state:
|
||||
Run `/undo` after a response you want to revise. OpenCode stages the rollback, restores the affected files, and puts the
|
||||
removed prompt back in the composer so you can edit it.
|
||||
|
||||
```text
|
||||
/undo
|
||||
|
||||
# Edit the restored prompt, then submit it again.
|
||||
Add validation without changing the public API.
|
||||
```
|
||||
|
||||
Submitting the edited prompt commits the staged rollback. Before submitting, run `/redo` to cancel it and restore the
|
||||
conversation and files you had before `/undo`.
|
||||
|
||||
```text
|
||||
/redo
|
||||
```
|
||||
|
||||
The terminal also provides default shortcuts:
|
||||
|
||||
| Action | Command | Shortcut |
|
||||
| --- | --- | --- |
|
||||
| Stage the previous message rollback | `/undo` | `<leader>u` |
|
||||
| Cancel the staged rollback | `/redo` | `<leader>r` |
|
||||
|
||||
You can also select an earlier message and choose **Revert** to stage a rollback to that point.
|
||||
|
||||
## Capture
|
||||
|
||||
For each model step, OpenCode attempts a snapshot immediately before the model call and another when the step reaches a
|
||||
recorded success or failure. The assistant message records which paths changed between those snapshots.
|
||||
|
||||
```text
|
||||
before model step ── snapshot A
|
||||
model edits src/api.ts
|
||||
step finishes ────── snapshot B ── changed: src/api.ts
|
||||
```
|
||||
|
||||
Capture is best effort. If either snapshot is unavailable, the conversation continues, but that step may not have file
|
||||
changes available to restore.
|
||||
|
||||
## Scope
|
||||
|
||||
Snapshots cover the session's active directory inside a Git worktree. For example, a session opened in `packages/app`
|
||||
captures eligible files below `packages/app`, not changes elsewhere in the repository.
|
||||
|
||||
| Content | Captured | Example |
|
||||
| --- | --- | --- |
|
||||
| Tracked files in the active directory | Yes | `packages/app/src/app.tsx` |
|
||||
| Non-ignored untracked files up to 2 MiB each | Yes | `packages/app/notes.txt` |
|
||||
| Git-ignored files | No | `packages/app/dist/app.js` |
|
||||
| Individual untracked files larger than 2 MiB | No | `packages/app/debug.log` |
|
||||
| Files outside the active directory | No | `packages/server/src/index.ts` |
|
||||
| Git metadata and other side effects | No | commits, branches, databases, or processes |
|
||||
|
||||
Filesystem snapshots require a Git repository. Outside one, `/undo` can still roll back conversation history, but it
|
||||
cannot restore files.
|
||||
|
||||
## Restore
|
||||
|
||||
A rollback restores only paths attributed to assistant steps after the selected conversation boundary. Each path returns
|
||||
to its state before the first reverted step; a file created by those steps is removed if it did not exist then.
|
||||
|
||||
| Timeline | `src/api.ts` | `src/new.ts` |
|
||||
| --- | --- | --- |
|
||||
| Selected boundary | version A | missing |
|
||||
| Later assistant steps | version B | created |
|
||||
| After rollback | version A | removed |
|
||||
|
||||
Paths not attributed to those steps are left alone. Current edits to an affected path can be overwritten, so review the
|
||||
restored summary and `git diff` before continuing.
|
||||
|
||||
```sh
|
||||
git diff --stat
|
||||
git diff
|
||||
```
|
||||
|
||||
## Storage
|
||||
|
||||
OpenCode stores snapshots locally in a separate internal Git object database under its data directory. Snapshot capture
|
||||
does not create commits, move branches, or intentionally change your repository's index.
|
||||
|
||||
```sh
|
||||
# Your repository remains an ordinary dirty worktree after capture.
|
||||
git status --short
|
||||
```
|
||||
|
||||
Snapshot objects can contain the complete contents of tracked and eligible untracked files. Treat the OpenCode data
|
||||
directory as sensitive local data.
|
||||
|
||||
## Settings
|
||||
|
||||
Snapshots are enabled by default. Set `snapshots` to `false` in your [configuration](/config#snapshots) to stop future
|
||||
filesystem capture.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -17,47 +108,20 @@ filesystem state:
|
||||
}
|
||||
```
|
||||
|
||||
Filesystem snapshots require a Git repository. With snapshots disabled, unavailable, or missing, a client can still
|
||||
request a conversation rollback, but there is no captured file state to restore. Disabling snapshots does not delete
|
||||
snapshots that were already stored.
|
||||
Disabling snapshots does not delete snapshots already stored. Conversation rollback remains available, but without a
|
||||
captured filesystem state it cannot restore related file changes.
|
||||
|
||||
## What is captured
|
||||
## Safety
|
||||
|
||||
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 are a convenience, not a transaction or backup:
|
||||
|
||||
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.
|
||||
- Shell commands can change services, processes, network resources, databases, Git state, ignored output, or files outside
|
||||
the active directory. Restoring a snapshot does not reverse those effects.
|
||||
- Interrupted or failed work can leave changes when capture fails or no usable snapshot pair exists.
|
||||
- Restoring affected paths overwrites their current contents. Cancelling a staged rollback can also overwrite later edits.
|
||||
- OpenCode rejects rollback operations while the session is running, but external editors and commands can still change
|
||||
the worktree between capture and restoration.
|
||||
- Committing a rollback removes messages from the active conversation view, not from durable session history or existing
|
||||
snapshot storage. It is not secure erasure.
|
||||
|
||||
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.
|
||||
|
||||
## Restoration
|
||||
|
||||
A restoration selects a conversation boundary and restores only paths attributed to cleanly completed assistant steps
|
||||
after it. Each affected path returns to its state before the first restored step, and a file created by those steps is
|
||||
removed when it did not exist in the earlier snapshot.
|
||||
|
||||
Clients may stage a restoration before committing it so the previous conversation and filesystem state can still be
|
||||
recovered.
|
||||
|
||||
## 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. Snapshot restoration does not reverse those side effects.
|
||||
- Restoring a snapshot overwrites the current contents of affected paths. Recovering the pre-restoration state can also
|
||||
overwrite edits made after the restoration was staged.
|
||||
- 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.
|
||||
- Snapshot restoration is not secure erasure. Committing a revert removes messages from the active projection, not from
|
||||
durable session history or existing snapshot storage.
|
||||
|
||||
Review the restored file summary and your Git diff before continuing. Commit or back up important work independently
|
||||
before restoring snapshots on a dirty worktree.
|
||||
Commit or back up important work before using `/undo` on a dirty worktree.
|
||||
|
||||
@@ -2,8 +2,268 @@
|
||||
title: "Themes"
|
||||
---
|
||||
|
||||
Themes are currently a terminal-client capability rather than a shared OpenCode configuration feature. The terminal
|
||||
client includes built-in light and dark themes, supports terminal-aware color modes, and owns theme selection and custom
|
||||
theme loading.
|
||||
Choose a theme from the terminal UI by pressing `Ctrl+P`, selecting **Open settings**, and opening **Theme**. You can also
|
||||
set it directly in your global CLI config:
|
||||
|
||||
See [CLI config](/cli/config) for terminal-client theme settings.
|
||||
```json title="~/.config/opencode/cli.json"
|
||||
{
|
||||
"theme": {
|
||||
"name": "tokyonight",
|
||||
"mode": "system"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Themes belong to the terminal client. Put theme selection in `cli.json`, not `opencode.json`. See
|
||||
[CLI config](/cli/config) for the config location and other terminal settings.
|
||||
|
||||
## Modes
|
||||
|
||||
Use `system` to follow your terminal's appearance, or lock the theme to one mode:
|
||||
|
||||
```json title="~/.config/opencode/cli.json"
|
||||
{
|
||||
"theme": {
|
||||
"name": "opencode",
|
||||
"mode": "dark"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Value | Behavior |
|
||||
| -------- | --------------------------------------------- |
|
||||
| `system` | Follows the light or dark mode of the terminal. |
|
||||
| `dark` | Always uses the dark theme. |
|
||||
| `light` | Always uses the light theme. |
|
||||
|
||||
If a custom theme provides only one mode, OpenCode uses that mode when the other is requested.
|
||||
|
||||
## Builtins
|
||||
|
||||
Select a built-in theme by its name:
|
||||
|
||||
```json title="~/.config/opencode/cli.json"
|
||||
{
|
||||
"theme": {
|
||||
"name": "catppuccin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Available themes include:
|
||||
|
||||
- `aura`, `ayu`, `carbonfox`, `cobalt2`, `cursor`, `dracula`, `everforest`, `flexoki`
|
||||
- `catppuccin`, `catppuccin-frappe`, `catppuccin-macchiato`
|
||||
- `github`, `gruvbox`, `kanagawa`, `material`, `matrix`, `mercury`, `monokai`, `nightowl`, `nord`
|
||||
- `one-dark`, `opencode`, `orng`, `lucent-orng`, `osaka-jade`, `palenight`, `rosepine`
|
||||
- `solarized`, `synthwave84`, `tokyonight`, `vercel`, `vesper`, `zenburn`
|
||||
|
||||
The `system` theme is available when OpenCode can read your terminal palette.
|
||||
|
||||
## Files
|
||||
|
||||
To create a global theme named `ocean`, save this file:
|
||||
|
||||
```json title="~/.config/opencode/themes/ocean.json"
|
||||
{
|
||||
"version": 2,
|
||||
"dark": {
|
||||
"hue": {
|
||||
"accent": "$hue.cyan"
|
||||
},
|
||||
"text": {
|
||||
"default": "#d8f3ff"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then select `ocean` in settings or set `theme.name` to `ocean` in `cli.json`. The filename, without `.json`, is the
|
||||
theme name.
|
||||
|
||||
| Scope | Directory |
|
||||
| ------- | ------------------------------------------ |
|
||||
| Global | `~/.config/opencode/themes/` |
|
||||
| Project | `.opencode/themes/` in the project tree |
|
||||
|
||||
OpenCode reads `.json` files only. A theme closer to the current directory replaces a global or parent theme with the
|
||||
same name.
|
||||
|
||||
## Format
|
||||
|
||||
Every V2 theme declares `version: 2` and at least one of `light` or `dark`:
|
||||
|
||||
```json title="themes/ocean.json"
|
||||
{
|
||||
"version": 2,
|
||||
"light": {
|
||||
"text": {
|
||||
"default": "#16324f"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"text": {
|
||||
"default": "#d8f3ff"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Custom themes inherit unspecified values from the built-in OpenCode theme. To base one mode on the other, add
|
||||
`mergeMode: true` to the mode containing the overrides:
|
||||
|
||||
```json title="themes/ocean.json"
|
||||
{
|
||||
"version": 2,
|
||||
"light": {
|
||||
"text": {
|
||||
"default": "#16324f"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"mergeMode": true,
|
||||
"text": {
|
||||
"default": "#d8f3ff"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Only one mode can use `mergeMode`, and the other mode must be present.
|
||||
|
||||
## Colors
|
||||
|
||||
Use a hex color, `transparent`, or a reference to another theme color:
|
||||
|
||||
```json title="themes/ocean.json"
|
||||
{
|
||||
"version": 2,
|
||||
"dark": {
|
||||
"text": {
|
||||
"default": "#d8f3ff",
|
||||
"subdued": "$hue.neutral.400"
|
||||
},
|
||||
"background": {
|
||||
"default": "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hex values may use 3, 4, 6, or 8 digits. References begin with `$`; hue references use the form
|
||||
`$hue.<name>.<step>`.
|
||||
|
||||
## Hues
|
||||
|
||||
Override a hue alias to change a family of related colors at once:
|
||||
|
||||
```json title="themes/ocean.json"
|
||||
{
|
||||
"version": 2,
|
||||
"dark": {
|
||||
"hue": {
|
||||
"accent": "$hue.cyan",
|
||||
"interactive": "$hue.blue"
|
||||
},
|
||||
"categorical": ["accent", "purple", "green"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Kind | Values |
|
||||
| ------- | -------------------------------------------------------------------- |
|
||||
| Base | `gray`, `red`, `orange`, `yellow`, `green`, `cyan`, `blue`, `purple` |
|
||||
| Alias | `accent`, `interactive`, `neutral` |
|
||||
| Step | `100`, `200`, `300`, `400`, `500`, `600`, `700`, `800`, `900` |
|
||||
|
||||
`categorical` sets the ordered hues used to distinguish agents and other repeated items. It must contain at least one
|
||||
base hue or alias.
|
||||
|
||||
## States
|
||||
|
||||
Use state keys to change interactive colors. Unspecified states use the nearest default value:
|
||||
|
||||
```json title="themes/ocean.json"
|
||||
{
|
||||
"version": 2,
|
||||
"dark": {
|
||||
"text": {
|
||||
"action": {
|
||||
"primary": {
|
||||
"default": "$hue.interactive.400",
|
||||
"$hovered": "$hue.interactive.300",
|
||||
"$disabled": "$hue.neutral.600"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Group | Values |
|
||||
| --------- | --------------------------------------------------------- |
|
||||
| Actions | `primary`, `secondary`, `destructive` |
|
||||
| States | `$hovered`, `$focused`, `$pressed`, `$selected`, `$disabled` |
|
||||
|
||||
Form fields use the same state keys directly under `text.formfield` or `background.formfield`.
|
||||
|
||||
## Tokens
|
||||
|
||||
Override only the token groups your theme needs:
|
||||
|
||||
```json title="themes/ocean.json"
|
||||
{
|
||||
"version": 2,
|
||||
"dark": {
|
||||
"border": {
|
||||
"default": "$hue.neutral.700"
|
||||
},
|
||||
"syntax": {
|
||||
"comment": "$hue.neutral.500",
|
||||
"keyword": "$hue.accent.400"
|
||||
},
|
||||
"diff": {
|
||||
"text": {
|
||||
"added": "$hue.green.400",
|
||||
"removed": "$hue.red.400"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Group | Purpose |
|
||||
| ------------ | ------------------------------------------------------------- |
|
||||
| `text` | Default, subdued, action, form-field, status, and feedback text |
|
||||
| `background` | Default, surfaces, actions, form fields, and feedback fills |
|
||||
| `border` | Borders |
|
||||
| `scrollbar` | Scrollbars |
|
||||
| `diff` | Added, removed, context, highlight, and line-number colors |
|
||||
| `syntax` | Source-code highlighting |
|
||||
| `markdown` | Markdown elements |
|
||||
|
||||
Syntax keys are `comment`, `keyword`, `function`, `variable`, `string`, `number`, `type`, `operator`, and
|
||||
`punctuation`. Markdown keys are `text`, `heading`, `link`, `linkText`, `code`, `blockQuote`, `emphasis`, `strong`,
|
||||
`horizontalRule`, `listItem`, `listEnumeration`, `image`, `imageText`, and `codeBlock`.
|
||||
|
||||
## Contexts
|
||||
|
||||
Use context overrides for elevated panels and overlays while keeping the base theme elsewhere:
|
||||
|
||||
```json title="themes/ocean.json"
|
||||
{
|
||||
"version": 2,
|
||||
"dark": {
|
||||
"background": {
|
||||
"default": "#071521"
|
||||
},
|
||||
"@context:overlay": {
|
||||
"background": {
|
||||
"default": "#102a3c"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The available keys are `@context:elevated` and `@context:overlay`. Each accepts the same token groups as the base mode.
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
---
|
||||
title: "Session warming"
|
||||
title: "Warming"
|
||||
---
|
||||
|
||||
Session warming sends periodic model requests for recently active sessions.
|
||||
This can preserve provider-side prompt caches or other short-lived session
|
||||
state while you pause between prompts.
|
||||
Session warming sends periodic model requests to preserve provider-side prompt
|
||||
caches or other short-lived state while you pause between prompts. It is
|
||||
disabled by default.
|
||||
|
||||
Warming is disabled by default. Enable it with the default settings in any
|
||||
[OpenCode configuration file](/config):
|
||||
Enable warming in any [OpenCode configuration file](/config):
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -16,60 +15,126 @@ Warming is disabled by default. Enable it with the default settings in any
|
||||
}
|
||||
```
|
||||
|
||||
With the defaults, OpenCode sends a warming request after a session has made no
|
||||
model request for four minutes. It repeats this while the session remains idle,
|
||||
but stops 30 minutes after the last non-warming request. New model activity
|
||||
starts a new 30-minute window.
|
||||
With this configuration, OpenCode warms a recently active session after four
|
||||
minutes without a model request. It continues every four minutes until 30
|
||||
minutes have passed since the latest non-warming request.
|
||||
|
||||
## Configuration
|
||||
## Options
|
||||
|
||||
Use the object form to customize the prompt, idle interval, or active duration:
|
||||
Use the object form to change the warming prompt, idle interval, or active
|
||||
window:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"warming": {
|
||||
"prompt": "Do not perform any work. Reply with exactly: OK",
|
||||
"interval": "4 minutes",
|
||||
"duration": "30 minutes",
|
||||
"interval": "5 minutes",
|
||||
"duration": "1 hour",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Default | Description |
|
||||
| ---------- | ---------------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| `prompt` | Keep-alive instruction | Prompt sent in each warming request. The default instructs the model to do no work and reply with `OK`. |
|
||||
| `interval` | `"4 minutes"` | Idle time between warming requests. |
|
||||
| `duration` | `"30 minutes"` | Maximum warming window after the latest non-warming model request. |
|
||||
| Field | Default | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `prompt` | Keep-alive instruction | Appended as a transient user message for each warming request. The default asks the model to do no work, use no tools, and reply with `OK`. |
|
||||
| `interval` | `"4 minutes"` | Idle time before the next warming request. |
|
||||
| `duration` | `"30 minutes"` | Warming window measured from the latest non-warming model request. |
|
||||
|
||||
`interval` and `duration` accept duration strings such as `"30 seconds"`,
|
||||
`"4 minutes"`, or `"1 hour"`. Both must be finite and greater than zero.
|
||||
Omitted object fields keep their defaults. For example, this changes only the
|
||||
interval:
|
||||
|
||||
To disable warming explicitly:
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"warming": {
|
||||
"interval": "2 minutes",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```jsonc
|
||||
## Durations
|
||||
|
||||
`interval` and `duration` accept duration strings. Both must resolve to finite
|
||||
values greater than zero; otherwise OpenCode skips warming and logs a warning.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"warming": {
|
||||
"interval": "30 seconds",
|
||||
"duration": "1 hour",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Timing
|
||||
|
||||
Warming starts after a non-warming model request. After a warming request
|
||||
finishes, the next idle interval starts, but the active window is not extended.
|
||||
|
||||
```text
|
||||
00:00 normal model request
|
||||
00:04 warming request
|
||||
00:08 warming request
|
||||
00:30 warming stops
|
||||
```
|
||||
|
||||
Any new non-warming model activity resets both timers. If the interval is as
|
||||
long as or longer than the duration, the active window expires before a warming
|
||||
request is sent.
|
||||
|
||||
```text
|
||||
00:00 normal model request
|
||||
00:20 normal model request; window restarts
|
||||
00:24 warming request
|
||||
00:50 warming stops
|
||||
```
|
||||
|
||||
## Requests
|
||||
|
||||
A warming request uses the session's current model, agent, instructions, and
|
||||
conversation context. OpenCode appends the configured prompt transiently,
|
||||
makes one model call, and discards the response.
|
||||
|
||||
```text
|
||||
[current session context]
|
||||
[transient user message: keep-alive prompt]
|
||||
→ one model response, discarded
|
||||
```
|
||||
|
||||
The request does not admit input, add messages to history, or mutate durable
|
||||
session state. OpenCode does not dispatch local tool calls or continue a tool
|
||||
loop; the default prompt also tells the model not to use tools.
|
||||
|
||||
## Failures
|
||||
|
||||
Warming failures are logged without failing or changing the session. OpenCode
|
||||
waits until the next interval before trying again, while the original active
|
||||
window continues to count down.
|
||||
|
||||
```text
|
||||
00:04 warming request fails; session is unchanged
|
||||
00:08 next warming attempt
|
||||
```
|
||||
|
||||
## Costs
|
||||
|
||||
Warming requests are real provider requests. They can consume tokens, incur
|
||||
costs, count against rate limits, and fail like other model requests.
|
||||
|
||||
- Shorter intervals increase request volume.
|
||||
- Longer durations allow more warming requests.
|
||||
- Enable warming only when its provider-side benefit is worth the added usage.
|
||||
|
||||
For example, the defaults allow up to seven warming attempts during one
|
||||
uninterrupted 30-minute window: at minutes 4, 8, 12, 16, 20, 24, and 28.
|
||||
|
||||
## Disabling
|
||||
|
||||
Set `warming` to `false` to disable it explicitly. Omitting the field also
|
||||
leaves warming disabled.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"warming": false,
|
||||
}
|
||||
```
|
||||
|
||||
## Request behavior
|
||||
|
||||
A warming request uses the session's current model, agent, instructions, and
|
||||
conversation context. Tools are disabled. The configured prompt is appended as
|
||||
a transient user message, and the response is discarded.
|
||||
|
||||
Warming does not admit input, add messages to session history, or otherwise
|
||||
mutate durable session state. A warming request resets the idle interval but
|
||||
does not extend the active duration; without new model activity, warming still
|
||||
ends when the configured duration expires.
|
||||
|
||||
## Costs and limits
|
||||
|
||||
Warming requests are real provider requests. They can consume tokens, incur
|
||||
costs, count against rate limits, and fail for the same reasons as other model
|
||||
requests. OpenCode logs warming failures without failing or changing the
|
||||
session, then waits for the next interval before trying again.
|
||||
|
||||
Enable warming only when the provider-side benefit is worth the additional
|
||||
requests. A shorter interval or longer duration increases request volume.
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: "Websearch"
|
||||
---
|
||||
|
||||
Ask OpenCode for current information and it can search the web, read the results, and include source links in its response.
|
||||
|
||||
```text
|
||||
Find the latest Bun release and summarize the changes.
|
||||
```
|
||||
|
||||
The first search asks you to allow web search and select a provider. OpenCode remembers your choice for later sessions.
|
||||
|
||||
## Providers
|
||||
|
||||
OpenCode includes four search providers:
|
||||
|
||||
| Provider | ID | Environment variable |
|
||||
| --- | --- | --- |
|
||||
| Exa | `exa` | `EXA_API_KEY` |
|
||||
| Firecrawl | `firecrawl` | `FIRECRAWL_API_KEY` |
|
||||
| Parallel | `parallel` | `PARALLEL_API_KEY` |
|
||||
| Tavily | `tavily` | `TAVILY_API_KEY` |
|
||||
|
||||
Connect an account from the TUI with `/connect`, or set the provider's environment variable before starting OpenCode.
|
||||
|
||||
```bash
|
||||
$ TAVILY_API_KEY=your-key opencode2
|
||||
```
|
||||
|
||||
## Selection
|
||||
|
||||
Set a provider in `opencode.jsonc` to skip the selection prompt.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"websearch": {
|
||||
"provider": "tavily",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use `"random"` to choose an available provider automatically. Each session keeps using its selected provider until that provider is rate limited.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"websearch": {
|
||||
"provider": "random",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Limits
|
||||
|
||||
When a provider returns HTTP 429, random selection retries the search with another available provider. The rate-limited provider waits for its `Retry-After` period, or 60 seconds when the response does not include a valid period.
|
||||
|
||||
```text
|
||||
Exa returns HTTP 429
|
||||
→ Exa enters cooldown
|
||||
→ OpenCode retries with another provider
|
||||
```
|
||||
|
||||
If every provider is cooling down, the search fails immediately. Moving a session or restarting its server clears its remembered provider; cooldowns are shared by sessions in the same workspace.
|
||||
|
||||
## Permissions
|
||||
|
||||
Web searches use the `websearch` permission action and the search query as the resource. Ask before searches that are not explicitly allowed.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"permissions": [
|
||||
{
|
||||
"action": "websearch",
|
||||
"resource": "*",
|
||||
"effect": "ask",
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
See [Permissions](/permissions) for rule order and matching.
|
||||
|
||||
## Disable
|
||||
|
||||
Set `websearch` to `false` to remove the web search tool from model requests.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"websearch": false,
|
||||
}
|
||||
```
|
||||
@@ -30,7 +30,6 @@ export const docsSections: DocsSection[] = [
|
||||
{
|
||||
title: "Configure",
|
||||
items: [
|
||||
{ title: "LSP", slug: "lsp" },
|
||||
{ title: "Agents", slug: "agents" },
|
||||
{ title: "Models", slug: "models" },
|
||||
{ title: "Skills", slug: "skills" },
|
||||
@@ -38,6 +37,7 @@ export const docsSections: DocsSection[] = [
|
||||
{ title: "Commands", slug: "commands" },
|
||||
{ title: "Plugins", slug: "plugins" },
|
||||
{ title: "Providers", slug: "providers" },
|
||||
{ title: "Websearch", slug: "websearch" },
|
||||
{ title: "Snapshots", slug: "snapshots" },
|
||||
{ title: "Compaction", slug: "compaction" },
|
||||
{ title: "Formatters", slug: "formatters" },
|
||||
@@ -46,8 +46,8 @@ export const docsSections: DocsSection[] = [
|
||||
{ title: "MCP servers", slug: "mcp-servers" },
|
||||
{ title: "Permissions", slug: "permissions" },
|
||||
{ title: "Instructions", slug: "instructions" },
|
||||
{ title: "Session sharing", slug: "sharing" },
|
||||
{ title: "Session warming", slug: "warming" },
|
||||
{ title: "Sharing", slug: "sharing" },
|
||||
{ title: "Warming", slug: "warming" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -67,19 +67,14 @@ export const docsSections: DocsSection[] = [
|
||||
items: [
|
||||
{ title: "Intro", slug: "cli" },
|
||||
{ title: "Config", slug: "cli/config" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Configure",
|
||||
items: [
|
||||
{ title: "Web", slug: "cli/web" },
|
||||
{ title: "Providers", slug: "cli/providers" },
|
||||
{ title: "Commands", slug: "cli/commands" },
|
||||
{ title: "Theme", slug: "cli/theme" },
|
||||
{ title: "Plugins", slug: "cli/plugins" },
|
||||
{ title: "Keybinds", slug: "cli/keybinds" },
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [{ title: "Providers", slug: "cli/providers" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -140,6 +135,7 @@ export const docsSections: DocsSection[] = [
|
||||
items: [
|
||||
{ title: "Intro", slug: "console" },
|
||||
{ title: "Models", slug: "console/models" },
|
||||
{ title: "Websearch", slug: "console/websearch" },
|
||||
{ title: "Go", slug: "console/go" },
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user