mirror of
https://github.com/aljazceru/opencode.git
synced 2026-01-06 17:34:58 +01:00
Merge agent and mode into one (#1689)
The concept of mode has been deprecated, there is now only the agent field in the config. An agent can be cycled through as your primary agent with <tab> or you can spawn a subagent by @ mentioning it. if you include a description of when to use it, the primary agent will try to automatically use it Full docs here: https://opencode.ai/docs/agents/
This commit is contained in:
@@ -38,6 +38,13 @@
|
||||
"@openauthjs/openauth": "0.4.3",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opentelemetry/auto-instrumentations-node": "0.62.0",
|
||||
"@opentelemetry/exporter-jaeger": "2.0.1",
|
||||
"@opentelemetry/exporter-otlp-http": "0.26.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.203.0",
|
||||
"@opentelemetry/instrumentation-fetch": "0.203.0",
|
||||
"@opentelemetry/sdk-node": "0.203.0",
|
||||
"@opentelemetry/sdk-trace-node": "2.0.1",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"ai": "catalog:",
|
||||
|
||||
@@ -10,13 +10,16 @@ export namespace Agent {
|
||||
export const Info = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]),
|
||||
topP: z.number().optional(),
|
||||
temperature: z.number().optional(),
|
||||
model: z
|
||||
.object({
|
||||
modelID: z.string(),
|
||||
providerID: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
description: z.string(),
|
||||
prompt: z.string().optional(),
|
||||
tools: z.record(z.boolean()),
|
||||
})
|
||||
@@ -24,6 +27,7 @@ export namespace Agent {
|
||||
ref: "Agent",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
const state = App.state("agent", async () => {
|
||||
const cfg = await Config.get()
|
||||
const result: Record<string, Info> = {
|
||||
@@ -35,6 +39,21 @@ export namespace Agent {
|
||||
todoread: false,
|
||||
todowrite: false,
|
||||
},
|
||||
mode: "subagent",
|
||||
},
|
||||
build: {
|
||||
name: "build",
|
||||
tools: {},
|
||||
mode: "primary",
|
||||
},
|
||||
plan: {
|
||||
name: "plan",
|
||||
tools: {
|
||||
write: false,
|
||||
edit: false,
|
||||
patch: false,
|
||||
},
|
||||
mode: "primary",
|
||||
},
|
||||
}
|
||||
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
|
||||
@@ -46,14 +65,10 @@ export namespace Agent {
|
||||
if (!item)
|
||||
item = result[key] = {
|
||||
name: key,
|
||||
description: "",
|
||||
tools: {
|
||||
todowrite: false,
|
||||
todoread: false,
|
||||
},
|
||||
mode: "all",
|
||||
tools: {},
|
||||
}
|
||||
const model = value.model ?? cfg.model
|
||||
if (model) item.model = Provider.parseModel(model)
|
||||
if (value.model) item.model = Provider.parseModel(value.model)
|
||||
if (value.prompt) item.prompt = value.prompt
|
||||
if (value.tools)
|
||||
item.tools = {
|
||||
@@ -61,6 +76,9 @@ export namespace Agent {
|
||||
...value.tools,
|
||||
}
|
||||
if (value.description) item.description = value.description
|
||||
if (value.temperature != undefined) item.temperature = value.temperature
|
||||
if (value.top_p != undefined) item.topP = value.top_p
|
||||
if (value.mode) item.mode = value.mode
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
@@ -641,7 +641,7 @@ export const GithubRunCommand = cmd({
|
||||
messageID: Identifier.ascending("message"),
|
||||
providerID,
|
||||
modelID,
|
||||
mode: "build",
|
||||
agent: "build",
|
||||
parts: [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
|
||||
@@ -8,8 +8,8 @@ import { Flag } from "../../flag/flag"
|
||||
import { Config } from "../../config/config"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { MessageV2 } from "../../session/message-v2"
|
||||
import { Mode } from "../../session/mode"
|
||||
import { Identifier } from "../../id/id"
|
||||
import { Agent } from "../../agent/agent"
|
||||
|
||||
const TOOL: Record<string, [string, string]> = {
|
||||
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
@@ -54,9 +54,9 @@ export const RunCommand = cmd({
|
||||
alias: ["m"],
|
||||
describe: "model to use in the format of provider/model",
|
||||
})
|
||||
.option("mode", {
|
||||
.option("agent", {
|
||||
type: "string",
|
||||
describe: "mode to use",
|
||||
describe: "agent to use",
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
@@ -103,8 +103,19 @@ export const RunCommand = cmd({
|
||||
}
|
||||
UI.empty()
|
||||
|
||||
const mode = args.mode ? await Mode.get(args.mode) : await Mode.list().then((x) => x[0])
|
||||
const { providerID, modelID } = args.model ? Provider.parseModel(args.model) : mode.model ?? await Provider.defaultModel()
|
||||
const agent = await (async () => {
|
||||
if (args.agent) return Agent.get(args.agent)
|
||||
const build = Agent.get("build")
|
||||
if (build) return build
|
||||
return Agent.list().then((x) => x[0])
|
||||
})()
|
||||
|
||||
const { providerID, modelID } = await (() => {
|
||||
if (args.model) return Provider.parseModel(args.model)
|
||||
if (agent.model) return agent.model
|
||||
return Provider.defaultModel()
|
||||
})()
|
||||
|
||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "@ ", UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`)
|
||||
UI.empty()
|
||||
|
||||
@@ -157,14 +168,17 @@ export const RunCommand = cmd({
|
||||
UI.error(err)
|
||||
})
|
||||
|
||||
|
||||
const messageID = Identifier.ascending("message")
|
||||
const result = await Session.chat({
|
||||
sessionID: session.id,
|
||||
messageID,
|
||||
providerID,
|
||||
modelID,
|
||||
mode: mode.name,
|
||||
...(agent.model
|
||||
? agent.model
|
||||
: {
|
||||
providerID,
|
||||
modelID,
|
||||
}),
|
||||
agent: agent.name,
|
||||
parts: [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
|
||||
@@ -11,8 +11,8 @@ import { Config } from "../../config/config"
|
||||
import { Bus } from "../../bus"
|
||||
import { Log } from "../../util/log"
|
||||
import { FileWatcher } from "../../file/watch"
|
||||
import { Mode } from "../../session/mode"
|
||||
import { Ide } from "../../ide"
|
||||
import { Agent } from "../../agent/agent"
|
||||
|
||||
declare global {
|
||||
const OPENCODE_TUI_PATH: string
|
||||
@@ -115,7 +115,7 @@ export const TuiCommand = cmd({
|
||||
CGO_ENABLED: "0",
|
||||
OPENCODE_SERVER: server.url.toString(),
|
||||
OPENCODE_APP_INFO: JSON.stringify(app),
|
||||
OPENCODE_MODES: JSON.stringify(await Mode.list()),
|
||||
OPENCODE_AGENTS: JSON.stringify(await Agent.list()),
|
||||
},
|
||||
onExit: () => {
|
||||
server.stop()
|
||||
|
||||
@@ -83,7 +83,7 @@ export namespace Config {
|
||||
...md.data,
|
||||
prompt: md.content.trim(),
|
||||
}
|
||||
const parsed = Mode.safeParse(config)
|
||||
const parsed = Agent.safeParse(config)
|
||||
if (parsed.success) {
|
||||
result.mode = mergeDeep(result.mode, {
|
||||
[config.name]: parsed.data,
|
||||
@@ -92,6 +92,15 @@ export namespace Config {
|
||||
}
|
||||
throw new InvalidError({ path: item }, { cause: parsed.error })
|
||||
}
|
||||
// Migrate deprecated mode field to agent field
|
||||
for (const [name, mode] of Object.entries(result.mode)) {
|
||||
result.agent = mergeDeep(result.agent ?? {}, {
|
||||
[name]: {
|
||||
...mode,
|
||||
mode: "primary" as const,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
result.plugin = result.plugin || []
|
||||
result.plugin.push(
|
||||
@@ -108,6 +117,12 @@ export namespace Config {
|
||||
if (result.keybinds?.messages_revert && !result.keybinds.messages_undo) {
|
||||
result.keybinds.messages_undo = result.keybinds.messages_revert
|
||||
}
|
||||
if (result.keybinds?.switch_mode && !result.keybinds.switch_agent) {
|
||||
result.keybinds.switch_agent = result.keybinds.switch_mode
|
||||
}
|
||||
if (result.keybinds?.switch_mode_reverse && !result.keybinds.switch_agent_reverse) {
|
||||
result.keybinds.switch_agent_reverse = result.keybinds.switch_mode_reverse
|
||||
}
|
||||
|
||||
if (!result.username) {
|
||||
const os = await import("os")
|
||||
@@ -149,7 +164,7 @@ export namespace Config {
|
||||
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
||||
export type Mcp = z.infer<typeof Mcp>
|
||||
|
||||
export const Mode = z
|
||||
export const Agent = z
|
||||
.object({
|
||||
model: z.string().optional(),
|
||||
temperature: z.number().optional(),
|
||||
@@ -157,24 +172,26 @@ export namespace Config {
|
||||
prompt: z.string().optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
disable: z.boolean().optional(),
|
||||
description: z.string().optional().describe("Description of when to use the agent"),
|
||||
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "ModeConfig",
|
||||
ref: "AgentConfig",
|
||||
})
|
||||
export type Mode = z.infer<typeof Mode>
|
||||
|
||||
export const Agent = Mode.extend({
|
||||
description: z.string(),
|
||||
}).openapi({
|
||||
ref: "AgentConfig",
|
||||
})
|
||||
export type Agent = z.infer<typeof Agent>
|
||||
|
||||
export const Keybinds = z
|
||||
.object({
|
||||
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
|
||||
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
|
||||
switch_mode: z.string().optional().default("tab").describe("Next mode"),
|
||||
switch_mode_reverse: z.string().optional().default("shift+tab").describe("Previous Mode"),
|
||||
switch_mode: z.string().optional().default("none").describe("@deprecated use switch_agent. Next mode"),
|
||||
switch_mode_reverse: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("none")
|
||||
.describe("@deprecated use switch_agent_reverse. Previous mode"),
|
||||
switch_agent: z.string().optional().default("tab").describe("Next agent"),
|
||||
switch_agent_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
|
||||
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
|
||||
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
|
||||
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
|
||||
@@ -257,19 +274,21 @@ export namespace Config {
|
||||
.describe("Custom username to display in conversations instead of system username"),
|
||||
mode: z
|
||||
.object({
|
||||
build: Mode.optional(),
|
||||
plan: Mode.optional(),
|
||||
build: Agent.optional(),
|
||||
plan: Agent.optional(),
|
||||
})
|
||||
.catchall(Mode)
|
||||
.catchall(Agent)
|
||||
.optional()
|
||||
.describe("Modes configuration, see https://opencode.ai/docs/modes"),
|
||||
.describe("@deprecated Use `agent` field instead."),
|
||||
agent: z
|
||||
.object({
|
||||
plan: Agent.optional(),
|
||||
build: Agent.optional(),
|
||||
general: Agent.optional(),
|
||||
})
|
||||
.catchall(Agent)
|
||||
.optional()
|
||||
.describe("Modes configuration, see https://opencode.ai/docs/modes"),
|
||||
.describe("Agent configuration, see https://opencode.ai/docs/agent"),
|
||||
provider: z
|
||||
.record(
|
||||
ModelsDev.Provider.partial()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import "zod-openapi/extend"
|
||||
import { Trace } from "./trace"
|
||||
Trace.init()
|
||||
import yargs from "yargs"
|
||||
import { hideBin } from "yargs/helpers"
|
||||
import { RunCommand } from "./cli/cmd/run"
|
||||
@@ -18,9 +20,6 @@ import { DebugCommand } from "./cli/cmd/debug"
|
||||
import { StatsCommand } from "./cli/cmd/stats"
|
||||
import { McpCommand } from "./cli/cmd/mcp"
|
||||
import { GithubCommand } from "./cli/cmd/github"
|
||||
import { Trace } from "./trace"
|
||||
|
||||
Trace.init()
|
||||
|
||||
const cancel = new AbortController()
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ import { Config } from "../config/config"
|
||||
import { File } from "../file"
|
||||
import { LSP } from "../lsp"
|
||||
import { MessageV2 } from "../session/message-v2"
|
||||
import { Mode } from "../session/mode"
|
||||
import { callTui, TuiRoute } from "./tui"
|
||||
import { Permission } from "../permission"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { Agent } from "../agent/agent"
|
||||
|
||||
const ERRORS = {
|
||||
400: {
|
||||
@@ -872,23 +872,23 @@ export namespace Server {
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/mode",
|
||||
"/agent",
|
||||
describeRoute({
|
||||
description: "List all modes",
|
||||
operationId: "app.modes",
|
||||
description: "List all agents",
|
||||
operationId: "app.agents",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of modes",
|
||||
description: "List of agents",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Mode.Info.array()),
|
||||
schema: resolver(Agent.Info.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const modes = await Mode.list()
|
||||
const modes = await Agent.list()
|
||||
return c.json(modes)
|
||||
},
|
||||
)
|
||||
@@ -1027,7 +1027,7 @@ export namespace Server {
|
||||
.post(
|
||||
"/tui/execute-command",
|
||||
describeRoute({
|
||||
description: "Execute a TUI command (e.g. switch_mode)",
|
||||
description: "Execute a TUI command (e.g. switch_agent)",
|
||||
operationId: "tui.executeCommand",
|
||||
responses: {
|
||||
200: {
|
||||
|
||||
@@ -36,12 +36,12 @@ import { NamedError } from "../util/error"
|
||||
import { SystemPrompt } from "./system"
|
||||
import { FileTime } from "../file/time"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { Mode } from "./mode"
|
||||
import { LSP } from "../lsp"
|
||||
import { ReadTool } from "../tool/read"
|
||||
import { mergeDeep, pipe, splitWhen } from "remeda"
|
||||
import { ToolRegistry } from "../tool/registry"
|
||||
import { Plugin } from "../plugin"
|
||||
import { Agent } from "../agent/agent"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -357,7 +357,7 @@ export namespace Session {
|
||||
messageID: Identifier.schema("message").optional(),
|
||||
providerID: z.string(),
|
||||
modelID: z.string(),
|
||||
mode: z.string().optional(),
|
||||
agent: z.string().optional(),
|
||||
system: z.string().optional(),
|
||||
tools: z.record(z.boolean()).optional(),
|
||||
parts: z.array(
|
||||
@@ -382,6 +382,16 @@ export namespace Session {
|
||||
.openapi({
|
||||
ref: "FilePartInput",
|
||||
}),
|
||||
MessageV2.AgentPart.omit({
|
||||
messageID: true,
|
||||
sessionID: true,
|
||||
})
|
||||
.partial({
|
||||
id: true,
|
||||
})
|
||||
.openapi({
|
||||
ref: "AgentPartInput",
|
||||
}),
|
||||
]),
|
||||
),
|
||||
})
|
||||
@@ -393,7 +403,7 @@ export namespace Session {
|
||||
const l = log.clone().tag("session", input.sessionID)
|
||||
l.info("chatting")
|
||||
|
||||
const inputMode = input.mode ?? "build"
|
||||
const inputAgent = input.agent ?? "build"
|
||||
|
||||
// Process revert cleanup first, before creating new messages
|
||||
const session = await get(input.sessionID)
|
||||
@@ -566,6 +576,28 @@ export namespace Session {
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (part.type === "agent") {
|
||||
return [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
...part,
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
},
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "text",
|
||||
synthetic: true,
|
||||
text:
|
||||
"Use the above message and context to generate a prompt and call the task tool with subagent: " +
|
||||
part.name,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
@@ -576,7 +608,7 @@ export namespace Session {
|
||||
]
|
||||
}),
|
||||
).then((x) => x.flat())
|
||||
if (inputMode === "plan")
|
||||
if (inputAgent === "plan")
|
||||
userParts.push({
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
@@ -683,12 +715,12 @@ export namespace Session {
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const mode = await Mode.get(inputMode)
|
||||
const agent = await Agent.get(inputAgent)
|
||||
let system = SystemPrompt.header(input.providerID)
|
||||
system.push(
|
||||
...(() => {
|
||||
if (input.system) return [input.system]
|
||||
if (mode.prompt) return [mode.prompt]
|
||||
if (agent.prompt) return [agent.prompt]
|
||||
return SystemPrompt.provider(input.modelID)
|
||||
})(),
|
||||
)
|
||||
@@ -702,7 +734,7 @@ export namespace Session {
|
||||
id: Identifier.ascending("message"),
|
||||
role: "assistant",
|
||||
system,
|
||||
mode: inputMode,
|
||||
mode: inputAgent,
|
||||
path: {
|
||||
cwd: app.path.cwd,
|
||||
root: app.path.root,
|
||||
@@ -727,7 +759,7 @@ export namespace Session {
|
||||
const processor = createProcessor(assistantMsg, model.info)
|
||||
|
||||
const enabledTools = pipe(
|
||||
mode.tools,
|
||||
agent.tools,
|
||||
mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID)),
|
||||
mergeDeep(input.tools ?? {}),
|
||||
)
|
||||
@@ -818,9 +850,9 @@ export namespace Session {
|
||||
|
||||
const params = {
|
||||
temperature: model.info.temperature
|
||||
? (mode.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
|
||||
? (agent.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
|
||||
: undefined,
|
||||
topP: mode.topP ?? ProviderTransform.topP(input.providerID, input.modelID),
|
||||
topP: agent.topP ?? ProviderTransform.topP(input.providerID, input.modelID),
|
||||
}
|
||||
await Plugin.trigger(
|
||||
"chat.params",
|
||||
@@ -871,7 +903,7 @@ export namespace Session {
|
||||
},
|
||||
modelID: input.modelID,
|
||||
providerID: input.providerID,
|
||||
mode: inputMode,
|
||||
mode: inputAgent,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
|
||||
@@ -172,6 +172,21 @@ export namespace MessageV2 {
|
||||
})
|
||||
export type FilePart = z.infer<typeof FilePart>
|
||||
|
||||
export const AgentPart = PartBase.extend({
|
||||
type: z.literal("agent"),
|
||||
name: z.string(),
|
||||
source: z
|
||||
.object({
|
||||
value: z.string(),
|
||||
start: z.number().int(),
|
||||
end: z.number().int(),
|
||||
})
|
||||
.optional(),
|
||||
}).openapi({
|
||||
ref: "AgentPart",
|
||||
})
|
||||
export type AgentPart = z.infer<typeof AgentPart>
|
||||
|
||||
export const StepStartPart = PartBase.extend({
|
||||
type: z.literal("step-start"),
|
||||
}).openapi({
|
||||
@@ -212,7 +227,16 @@ export namespace MessageV2 {
|
||||
export type User = z.infer<typeof User>
|
||||
|
||||
export const Part = z
|
||||
.discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart, PatchPart])
|
||||
.discriminatedUnion("type", [
|
||||
TextPart,
|
||||
FilePart,
|
||||
ToolPart,
|
||||
StepStartPart,
|
||||
StepFinishPart,
|
||||
SnapshotPart,
|
||||
PatchPart,
|
||||
AgentPart,
|
||||
])
|
||||
.openapi({
|
||||
ref: "Part",
|
||||
})
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { App } from "../app/app"
|
||||
import { Config } from "../config/config"
|
||||
import z from "zod"
|
||||
import { Provider } from "../provider/provider"
|
||||
|
||||
export namespace Mode {
|
||||
export const Info = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
temperature: z.number().optional(),
|
||||
topP: z.number().optional(),
|
||||
model: z
|
||||
.object({
|
||||
modelID: z.string(),
|
||||
providerID: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
prompt: z.string().optional(),
|
||||
tools: z.record(z.boolean()),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Mode",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
const state = App.state("mode", async () => {
|
||||
const cfg = await Config.get()
|
||||
const model = cfg.model ? Provider.parseModel(cfg.model) : undefined
|
||||
const result: Record<string, Info> = {
|
||||
build: {
|
||||
model,
|
||||
name: "build",
|
||||
tools: {},
|
||||
},
|
||||
plan: {
|
||||
name: "plan",
|
||||
model,
|
||||
tools: {
|
||||
write: false,
|
||||
edit: false,
|
||||
patch: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
for (const [key, value] of Object.entries(cfg.mode ?? {})) {
|
||||
if (value.disable) continue
|
||||
let item = result[key]
|
||||
if (!item)
|
||||
item = result[key] = {
|
||||
name: key,
|
||||
tools: {},
|
||||
}
|
||||
item.name = key
|
||||
if (value.model) item.model = Provider.parseModel(value.model)
|
||||
if (value.prompt) item.prompt = value.prompt
|
||||
if (value.temperature != undefined) item.temperature = value.temperature
|
||||
if (value.top_p != undefined) item.topP = value.top_p
|
||||
if (value.tools)
|
||||
item.tools = {
|
||||
...value.tools,
|
||||
...item.tools,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
export async function get(mode: string) {
|
||||
return state().then((x) => x[mode])
|
||||
}
|
||||
|
||||
export async function list() {
|
||||
return state().then((x) => Object.values(x))
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,13 @@ import { Identifier } from "../id/id"
|
||||
import { Agent } from "../agent/agent"
|
||||
|
||||
export const TaskTool = Tool.define("task", async () => {
|
||||
const agents = await Agent.list()
|
||||
const description = DESCRIPTION.replace("{agents}", agents.map((a) => `- ${a.name}: ${a.description}`).join("\n"))
|
||||
const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary"))
|
||||
const description = DESCRIPTION.replace(
|
||||
"{agents}",
|
||||
agents
|
||||
.map((a) => `- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`)
|
||||
.join("\n"),
|
||||
)
|
||||
return {
|
||||
description,
|
||||
parameters: z.object({
|
||||
@@ -51,11 +56,12 @@ export const TaskTool = Tool.define("task", async () => {
|
||||
sessionID: session.id,
|
||||
modelID: model.modelID,
|
||||
providerID: model.providerID,
|
||||
mode: msg.info.mode,
|
||||
system: agent.prompt,
|
||||
agent: agent.name,
|
||||
tools: {
|
||||
...agent.tools,
|
||||
todowrite: false,
|
||||
todoread: false,
|
||||
task: false,
|
||||
...agent.tools,
|
||||
},
|
||||
parts: [
|
||||
{
|
||||
|
||||
@@ -1,53 +1,17 @@
|
||||
import { Global } from "../global"
|
||||
import { Installation } from "../installation"
|
||||
import path from "path"
|
||||
import { NodeSDK } from "@opentelemetry/sdk-node"
|
||||
import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch"
|
||||
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
|
||||
|
||||
export namespace Trace {
|
||||
export function init() {
|
||||
if (!Installation.isDev()) return
|
||||
const writer = Bun.file(path.join(Global.Path.data, "log", "fetch.log")).writer()
|
||||
const sdk = new NodeSDK({
|
||||
serviceName: "opencode",
|
||||
instrumentations: [new FetchInstrumentation()],
|
||||
traceExporter: new OTLPTraceExporter({
|
||||
url: "http://localhost:4318/v1/traces", // or your OTLP endpoint
|
||||
}),
|
||||
})
|
||||
|
||||
const originalFetch = globalThis.fetch
|
||||
// @ts-expect-error
|
||||
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url
|
||||
const method = init?.method || "GET"
|
||||
|
||||
const urlObj = new URL(url)
|
||||
|
||||
writer.write(`\n${method} ${urlObj.pathname}${urlObj.search} HTTP/1.1\n`)
|
||||
writer.write(`Host: ${urlObj.host}\n`)
|
||||
|
||||
if (init?.headers) {
|
||||
if (init.headers instanceof Headers) {
|
||||
init.headers.forEach((value, key) => {
|
||||
writer.write(`${key}: ${value}\n`)
|
||||
})
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(init.headers)) {
|
||||
writer.write(`${key}: ${value}\n`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (init?.body) {
|
||||
writer.write(`\n${init.body}`)
|
||||
}
|
||||
writer.flush()
|
||||
const response = await originalFetch(input, init)
|
||||
const clonedResponse = response.clone()
|
||||
writer.write(`\nHTTP/1.1 ${response.status} ${response.statusText}\n`)
|
||||
response.headers.forEach((value, key) => {
|
||||
writer.write(`${key}: ${value}\n`)
|
||||
})
|
||||
if (clonedResponse.body) {
|
||||
clonedResponse.text().then(async (x) => {
|
||||
writer.write(`\n${x}\n`)
|
||||
})
|
||||
}
|
||||
writer.flush()
|
||||
|
||||
return response
|
||||
}
|
||||
sdk.start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configured_endpoints: 34
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-52fd0b61e84fdc1cdd31ec12e1600510e9dd2f9d4fb20c2315b4975cb763ee98.yml
|
||||
openapi_spec_hash: e851b8d5a2412f5fc9be82ab88ebdfde
|
||||
config_hash: 11a6f0803eb407367c3f677d3e524c37
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-da1c340135c3dd6b1edb4e00e7039d2ac54d59271683a8b6ed528e51137ce41a.yml
|
||||
openapi_spec_hash: 0cdd9b6273d72f5a6f484a2999ff0632
|
||||
config_hash: 7581d5948150d4ef7dd7b13d0845dbeb
|
||||
|
||||
@@ -18,18 +18,18 @@ Methods:
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Agent">Agent</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Mode">Mode</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Model">Model</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Provider">Provider</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppProvidersResponse">AppProvidersResponse</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /agent">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Agents">Agents</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Agent">Agent</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /app">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /app/init">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /log">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Log">Log</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppLogParams">AppLogParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /mode">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Modes">Modes</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Mode">Mode</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /config/providers">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Providers">Providers</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppProvidersResponse">AppProvidersResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# Find
|
||||
@@ -65,7 +65,6 @@ Response Types:
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#KeybindsConfig">KeybindsConfig</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#McpLocalConfig">McpLocalConfig</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#McpRemoteConfig">McpRemoteConfig</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ModeConfig">ModeConfig</a>
|
||||
|
||||
Methods:
|
||||
|
||||
@@ -75,6 +74,7 @@ Methods:
|
||||
|
||||
Params Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AgentPartInputParam">AgentPartInputParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartInputParam">FilePartInputParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSourceUnionParam">FilePartSourceUnionParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSourceTextParam">FilePartSourceTextParam</a>
|
||||
@@ -84,6 +84,7 @@ Params Types:
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AgentPart">AgentPart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePart">FilePart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSource">FilePartSource</a>
|
||||
|
||||
@@ -31,6 +31,14 @@ func NewAppService(opts ...option.RequestOption) (r *AppService) {
|
||||
return
|
||||
}
|
||||
|
||||
// List all agents
|
||||
func (r *AppService) Agents(ctx context.Context, opts ...option.RequestOption) (res *[]Agent, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "agent"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
// Get app info
|
||||
func (r *AppService) Get(ctx context.Context, opts ...option.RequestOption) (res *App, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
@@ -55,14 +63,6 @@ func (r *AppService) Log(ctx context.Context, body AppLogParams, opts ...option.
|
||||
return
|
||||
}
|
||||
|
||||
// List all modes
|
||||
func (r *AppService) Modes(ctx context.Context, opts ...option.RequestOption) (res *[]Mode, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "mode"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
// List all providers
|
||||
func (r *AppService) Providers(ctx context.Context, opts ...option.RequestOption) (res *AppProvidersResponse, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
@@ -71,6 +71,78 @@ func (r *AppService) Providers(ctx context.Context, opts ...option.RequestOption
|
||||
return
|
||||
}
|
||||
|
||||
type Agent struct {
|
||||
Mode AgentMode `json:"mode,required"`
|
||||
Name string `json:"name,required"`
|
||||
Tools map[string]bool `json:"tools,required"`
|
||||
Description string `json:"description"`
|
||||
Model AgentModel `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
TopP float64 `json:"topP"`
|
||||
JSON agentJSON `json:"-"`
|
||||
}
|
||||
|
||||
// agentJSON contains the JSON metadata for the struct [Agent]
|
||||
type agentJSON struct {
|
||||
Mode apijson.Field
|
||||
Name apijson.Field
|
||||
Tools apijson.Field
|
||||
Description apijson.Field
|
||||
Model apijson.Field
|
||||
Prompt apijson.Field
|
||||
Temperature apijson.Field
|
||||
TopP apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *Agent) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r agentJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type AgentMode string
|
||||
|
||||
const (
|
||||
AgentModeSubagent AgentMode = "subagent"
|
||||
AgentModePrimary AgentMode = "primary"
|
||||
AgentModeAll AgentMode = "all"
|
||||
)
|
||||
|
||||
func (r AgentMode) IsKnown() bool {
|
||||
switch r {
|
||||
case AgentModeSubagent, AgentModePrimary, AgentModeAll:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type AgentModel struct {
|
||||
ModelID string `json:"modelID,required"`
|
||||
ProviderID string `json:"providerID,required"`
|
||||
JSON agentModelJSON `json:"-"`
|
||||
}
|
||||
|
||||
// agentModelJSON contains the JSON metadata for the struct [AgentModel]
|
||||
type agentModelJSON struct {
|
||||
ModelID apijson.Field
|
||||
ProviderID apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *AgentModel) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r agentModelJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type App struct {
|
||||
Git bool `json:"git,required"`
|
||||
Hostname string `json:"hostname,required"`
|
||||
@@ -145,58 +217,6 @@ func (r appTimeJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type Mode struct {
|
||||
Name string `json:"name,required"`
|
||||
Tools map[string]bool `json:"tools,required"`
|
||||
Model ModeModel `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
TopP float64 `json:"topP"`
|
||||
JSON modeJSON `json:"-"`
|
||||
}
|
||||
|
||||
// modeJSON contains the JSON metadata for the struct [Mode]
|
||||
type modeJSON struct {
|
||||
Name apijson.Field
|
||||
Tools apijson.Field
|
||||
Model apijson.Field
|
||||
Prompt apijson.Field
|
||||
Temperature apijson.Field
|
||||
TopP apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *Mode) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r modeJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ModeModel struct {
|
||||
ModelID string `json:"modelID,required"`
|
||||
ProviderID string `json:"providerID,required"`
|
||||
JSON modeModelJSON `json:"-"`
|
||||
}
|
||||
|
||||
// modeModelJSON contains the JSON metadata for the struct [ModeModel]
|
||||
type modeModelJSON struct {
|
||||
ModelID apijson.Field
|
||||
ProviderID apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ModeModel) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r modeModelJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
ID string `json:"id,required"`
|
||||
Attachment bool `json:"attachment,required"`
|
||||
|
||||
@@ -13,6 +13,28 @@ import (
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
)
|
||||
|
||||
func TestAppAgents(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.App.Agents(context.TODO())
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppGet(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
@@ -86,28 +108,6 @@ func TestAppLogWithOptionalParams(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppModes(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.App.Modes(context.TODO())
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppProviders(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
|
||||
@@ -43,7 +43,7 @@ func (r *ConfigService) Get(ctx context.Context, opts ...option.RequestOption) (
|
||||
type Config struct {
|
||||
// JSON schema reference for configuration validation
|
||||
Schema string `json:"$schema"`
|
||||
// Modes configuration, see https://opencode.ai/docs/modes
|
||||
// Agent configuration, see https://opencode.ai/docs/agent
|
||||
Agent ConfigAgent `json:"agent"`
|
||||
// @deprecated Use 'share' field instead. Share newly created sessions
|
||||
// automatically
|
||||
@@ -63,7 +63,7 @@ type Config struct {
|
||||
Lsp map[string]ConfigLsp `json:"lsp"`
|
||||
// MCP (Model Context Protocol) server configurations
|
||||
Mcp map[string]ConfigMcp `json:"mcp"`
|
||||
// Modes configuration, see https://opencode.ai/docs/modes
|
||||
// @deprecated Use `agent` field instead.
|
||||
Mode ConfigMode `json:"mode"`
|
||||
// Model to use in the format of provider/model, eg anthropic/claude-2
|
||||
Model string `json:"model"`
|
||||
@@ -119,16 +119,20 @@ func (r configJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
// Modes configuration, see https://opencode.ai/docs/modes
|
||||
// Agent configuration, see https://opencode.ai/docs/agent
|
||||
type ConfigAgent struct {
|
||||
Build ConfigAgentBuild `json:"build"`
|
||||
General ConfigAgentGeneral `json:"general"`
|
||||
Plan ConfigAgentPlan `json:"plan"`
|
||||
ExtraFields map[string]ConfigAgent `json:"-,extras"`
|
||||
JSON configAgentJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configAgentJSON contains the JSON metadata for the struct [ConfigAgent]
|
||||
type configAgentJSON struct {
|
||||
Build apijson.Field
|
||||
General apijson.Field
|
||||
Plan apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
@@ -141,16 +145,82 @@ func (r configAgentJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigAgentBuild struct {
|
||||
// Description of when to use the agent
|
||||
Description string `json:"description"`
|
||||
Disable bool `json:"disable"`
|
||||
Mode ConfigAgentBuildMode `json:"mode"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Tools map[string]bool `json:"tools"`
|
||||
TopP float64 `json:"top_p"`
|
||||
JSON configAgentBuildJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configAgentBuildJSON contains the JSON metadata for the struct
|
||||
// [ConfigAgentBuild]
|
||||
type configAgentBuildJSON struct {
|
||||
Description apijson.Field
|
||||
Disable apijson.Field
|
||||
Mode apijson.Field
|
||||
Model apijson.Field
|
||||
Prompt apijson.Field
|
||||
Temperature apijson.Field
|
||||
Tools apijson.Field
|
||||
TopP apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigAgentBuild) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configAgentBuildJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigAgentBuildMode string
|
||||
|
||||
const (
|
||||
ConfigAgentBuildModeSubagent ConfigAgentBuildMode = "subagent"
|
||||
ConfigAgentBuildModePrimary ConfigAgentBuildMode = "primary"
|
||||
ConfigAgentBuildModeAll ConfigAgentBuildMode = "all"
|
||||
)
|
||||
|
||||
func (r ConfigAgentBuildMode) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigAgentBuildModeSubagent, ConfigAgentBuildModePrimary, ConfigAgentBuildModeAll:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ConfigAgentGeneral struct {
|
||||
Description string `json:"description,required"`
|
||||
// Description of when to use the agent
|
||||
Description string `json:"description"`
|
||||
Disable bool `json:"disable"`
|
||||
Mode ConfigAgentGeneralMode `json:"mode"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Tools map[string]bool `json:"tools"`
|
||||
TopP float64 `json:"top_p"`
|
||||
JSON configAgentGeneralJSON `json:"-"`
|
||||
ModeConfig
|
||||
}
|
||||
|
||||
// configAgentGeneralJSON contains the JSON metadata for the struct
|
||||
// [ConfigAgentGeneral]
|
||||
type configAgentGeneralJSON struct {
|
||||
Description apijson.Field
|
||||
Disable apijson.Field
|
||||
Mode apijson.Field
|
||||
Model apijson.Field
|
||||
Prompt apijson.Field
|
||||
Temperature apijson.Field
|
||||
Tools apijson.Field
|
||||
TopP apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
@@ -163,6 +233,73 @@ func (r configAgentGeneralJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigAgentGeneralMode string
|
||||
|
||||
const (
|
||||
ConfigAgentGeneralModeSubagent ConfigAgentGeneralMode = "subagent"
|
||||
ConfigAgentGeneralModePrimary ConfigAgentGeneralMode = "primary"
|
||||
ConfigAgentGeneralModeAll ConfigAgentGeneralMode = "all"
|
||||
)
|
||||
|
||||
func (r ConfigAgentGeneralMode) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigAgentGeneralModeSubagent, ConfigAgentGeneralModePrimary, ConfigAgentGeneralModeAll:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ConfigAgentPlan struct {
|
||||
// Description of when to use the agent
|
||||
Description string `json:"description"`
|
||||
Disable bool `json:"disable"`
|
||||
Mode ConfigAgentPlanMode `json:"mode"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Tools map[string]bool `json:"tools"`
|
||||
TopP float64 `json:"top_p"`
|
||||
JSON configAgentPlanJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configAgentPlanJSON contains the JSON metadata for the struct [ConfigAgentPlan]
|
||||
type configAgentPlanJSON struct {
|
||||
Description apijson.Field
|
||||
Disable apijson.Field
|
||||
Mode apijson.Field
|
||||
Model apijson.Field
|
||||
Prompt apijson.Field
|
||||
Temperature apijson.Field
|
||||
Tools apijson.Field
|
||||
TopP apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigAgentPlan) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configAgentPlanJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigAgentPlanMode string
|
||||
|
||||
const (
|
||||
ConfigAgentPlanModeSubagent ConfigAgentPlanMode = "subagent"
|
||||
ConfigAgentPlanModePrimary ConfigAgentPlanMode = "primary"
|
||||
ConfigAgentPlanModeAll ConfigAgentPlanMode = "all"
|
||||
)
|
||||
|
||||
func (r ConfigAgentPlanMode) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigAgentPlanModeSubagent, ConfigAgentPlanModePrimary, ConfigAgentPlanModeAll:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ConfigExperimental struct {
|
||||
Hook ConfigExperimentalHook `json:"hook"`
|
||||
JSON configExperimentalJSON `json:"-"`
|
||||
@@ -516,11 +653,11 @@ func (r ConfigMcpType) IsKnown() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Modes configuration, see https://opencode.ai/docs/modes
|
||||
// @deprecated Use `agent` field instead.
|
||||
type ConfigMode struct {
|
||||
Build ModeConfig `json:"build"`
|
||||
Plan ModeConfig `json:"plan"`
|
||||
ExtraFields map[string]ModeConfig `json:"-,extras"`
|
||||
Build ConfigModeBuild `json:"build"`
|
||||
Plan ConfigModePlan `json:"plan"`
|
||||
ExtraFields map[string]ConfigMode `json:"-,extras"`
|
||||
JSON configModeJSON `json:"-"`
|
||||
}
|
||||
|
||||
@@ -540,6 +677,108 @@ func (r configModeJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigModeBuild struct {
|
||||
// Description of when to use the agent
|
||||
Description string `json:"description"`
|
||||
Disable bool `json:"disable"`
|
||||
Mode ConfigModeBuildMode `json:"mode"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Tools map[string]bool `json:"tools"`
|
||||
TopP float64 `json:"top_p"`
|
||||
JSON configModeBuildJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configModeBuildJSON contains the JSON metadata for the struct [ConfigModeBuild]
|
||||
type configModeBuildJSON struct {
|
||||
Description apijson.Field
|
||||
Disable apijson.Field
|
||||
Mode apijson.Field
|
||||
Model apijson.Field
|
||||
Prompt apijson.Field
|
||||
Temperature apijson.Field
|
||||
Tools apijson.Field
|
||||
TopP apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigModeBuild) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configModeBuildJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigModeBuildMode string
|
||||
|
||||
const (
|
||||
ConfigModeBuildModeSubagent ConfigModeBuildMode = "subagent"
|
||||
ConfigModeBuildModePrimary ConfigModeBuildMode = "primary"
|
||||
ConfigModeBuildModeAll ConfigModeBuildMode = "all"
|
||||
)
|
||||
|
||||
func (r ConfigModeBuildMode) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigModeBuildModeSubagent, ConfigModeBuildModePrimary, ConfigModeBuildModeAll:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ConfigModePlan struct {
|
||||
// Description of when to use the agent
|
||||
Description string `json:"description"`
|
||||
Disable bool `json:"disable"`
|
||||
Mode ConfigModePlanMode `json:"mode"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Tools map[string]bool `json:"tools"`
|
||||
TopP float64 `json:"top_p"`
|
||||
JSON configModePlanJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configModePlanJSON contains the JSON metadata for the struct [ConfigModePlan]
|
||||
type configModePlanJSON struct {
|
||||
Description apijson.Field
|
||||
Disable apijson.Field
|
||||
Mode apijson.Field
|
||||
Model apijson.Field
|
||||
Prompt apijson.Field
|
||||
Temperature apijson.Field
|
||||
Tools apijson.Field
|
||||
TopP apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigModePlan) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configModePlanJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigModePlanMode string
|
||||
|
||||
const (
|
||||
ConfigModePlanModeSubagent ConfigModePlanMode = "subagent"
|
||||
ConfigModePlanModePrimary ConfigModePlanMode = "primary"
|
||||
ConfigModePlanModeAll ConfigModePlanMode = "all"
|
||||
)
|
||||
|
||||
func (r ConfigModePlanMode) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigModePlanModeSubagent, ConfigModePlanModePrimary, ConfigModePlanModeAll:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ConfigPermission struct {
|
||||
Bash ConfigPermissionBashUnion `json:"bash"`
|
||||
Edit ConfigPermissionEdit `json:"edit"`
|
||||
@@ -588,11 +827,12 @@ type ConfigPermissionBashString string
|
||||
const (
|
||||
ConfigPermissionBashStringAsk ConfigPermissionBashString = "ask"
|
||||
ConfigPermissionBashStringAllow ConfigPermissionBashString = "allow"
|
||||
ConfigPermissionBashStringDeny ConfigPermissionBashString = "deny"
|
||||
)
|
||||
|
||||
func (r ConfigPermissionBashString) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigPermissionBashStringAsk, ConfigPermissionBashStringAllow:
|
||||
case ConfigPermissionBashStringAsk, ConfigPermissionBashStringAllow, ConfigPermissionBashStringDeny:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -609,11 +849,12 @@ type ConfigPermissionBashMapItem string
|
||||
const (
|
||||
ConfigPermissionBashMapAsk ConfigPermissionBashMapItem = "ask"
|
||||
ConfigPermissionBashMapAllow ConfigPermissionBashMapItem = "allow"
|
||||
ConfigPermissionBashMapDeny ConfigPermissionBashMapItem = "deny"
|
||||
)
|
||||
|
||||
func (r ConfigPermissionBashMapItem) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigPermissionBashMapAsk, ConfigPermissionBashMapAllow:
|
||||
case ConfigPermissionBashMapAsk, ConfigPermissionBashMapAllow, ConfigPermissionBashMapDeny:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -624,11 +865,12 @@ type ConfigPermissionEdit string
|
||||
const (
|
||||
ConfigPermissionEditAsk ConfigPermissionEdit = "ask"
|
||||
ConfigPermissionEditAllow ConfigPermissionEdit = "allow"
|
||||
ConfigPermissionEditDeny ConfigPermissionEdit = "deny"
|
||||
)
|
||||
|
||||
func (r ConfigPermissionEdit) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigPermissionEditAsk, ConfigPermissionEditAllow:
|
||||
case ConfigPermissionEditAsk, ConfigPermissionEditAllow, ConfigPermissionEditDeny:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -866,9 +1108,13 @@ type KeybindsConfig struct {
|
||||
SessionShare string `json:"session_share,required"`
|
||||
// Unshare current session
|
||||
SessionUnshare string `json:"session_unshare,required"`
|
||||
// Next mode
|
||||
// Next agent
|
||||
SwitchAgent string `json:"switch_agent,required"`
|
||||
// Previous agent
|
||||
SwitchAgentReverse string `json:"switch_agent_reverse,required"`
|
||||
// @deprecated use switch_agent. Next mode
|
||||
SwitchMode string `json:"switch_mode,required"`
|
||||
// Previous Mode
|
||||
// @deprecated use switch_agent_reverse. Previous mode
|
||||
SwitchModeReverse string `json:"switch_mode_reverse,required"`
|
||||
// List available themes
|
||||
ThemeList string `json:"theme_list,required"`
|
||||
@@ -913,6 +1159,8 @@ type keybindsConfigJSON struct {
|
||||
SessionNew apijson.Field
|
||||
SessionShare apijson.Field
|
||||
SessionUnshare apijson.Field
|
||||
SwitchAgent apijson.Field
|
||||
SwitchAgentReverse apijson.Field
|
||||
SwitchMode apijson.Field
|
||||
SwitchModeReverse apijson.Field
|
||||
ThemeList apijson.Field
|
||||
@@ -1022,33 +1270,3 @@ func (r McpRemoteConfigType) IsKnown() bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ModeConfig struct {
|
||||
Disable bool `json:"disable"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Tools map[string]bool `json:"tools"`
|
||||
TopP float64 `json:"top_p"`
|
||||
JSON modeConfigJSON `json:"-"`
|
||||
}
|
||||
|
||||
// modeConfigJSON contains the JSON metadata for the struct [ModeConfig]
|
||||
type modeConfigJSON struct {
|
||||
Disable apijson.Field
|
||||
Model apijson.Field
|
||||
Prompt apijson.Field
|
||||
Temperature apijson.Field
|
||||
Tools apijson.Field
|
||||
TopP apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ModeConfig) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r modeConfigJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
@@ -133,11 +133,13 @@ func NewRequestConfig(ctx context.Context, method string, u string, body interfa
|
||||
// Fallback to json serialization if none of the serialization functions that we expect
|
||||
// to see is present.
|
||||
if body != nil && !hasSerializationFunc {
|
||||
content, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
buf := new(bytes.Buffer)
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(true)
|
||||
if err := enc.Encode(body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader = bytes.NewBuffer(content)
|
||||
reader = buf
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, u, nil)
|
||||
|
||||
@@ -190,6 +190,113 @@ func (r *SessionService) Unshare(ctx context.Context, id string, opts ...option.
|
||||
return
|
||||
}
|
||||
|
||||
type AgentPart struct {
|
||||
ID string `json:"id,required"`
|
||||
MessageID string `json:"messageID,required"`
|
||||
Name string `json:"name,required"`
|
||||
SessionID string `json:"sessionID,required"`
|
||||
Type AgentPartType `json:"type,required"`
|
||||
Source AgentPartSource `json:"source"`
|
||||
JSON agentPartJSON `json:"-"`
|
||||
}
|
||||
|
||||
// agentPartJSON contains the JSON metadata for the struct [AgentPart]
|
||||
type agentPartJSON struct {
|
||||
ID apijson.Field
|
||||
MessageID apijson.Field
|
||||
Name apijson.Field
|
||||
SessionID apijson.Field
|
||||
Type apijson.Field
|
||||
Source apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *AgentPart) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r agentPartJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
func (r AgentPart) implementsPart() {}
|
||||
|
||||
type AgentPartType string
|
||||
|
||||
const (
|
||||
AgentPartTypeAgent AgentPartType = "agent"
|
||||
)
|
||||
|
||||
func (r AgentPartType) IsKnown() bool {
|
||||
switch r {
|
||||
case AgentPartTypeAgent:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type AgentPartSource struct {
|
||||
End int64 `json:"end,required"`
|
||||
Start int64 `json:"start,required"`
|
||||
Value string `json:"value,required"`
|
||||
JSON agentPartSourceJSON `json:"-"`
|
||||
}
|
||||
|
||||
// agentPartSourceJSON contains the JSON metadata for the struct [AgentPartSource]
|
||||
type agentPartSourceJSON struct {
|
||||
End apijson.Field
|
||||
Start apijson.Field
|
||||
Value apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *AgentPartSource) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r agentPartSourceJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type AgentPartInputParam struct {
|
||||
Name param.Field[string] `json:"name,required"`
|
||||
Type param.Field[AgentPartInputType] `json:"type,required"`
|
||||
ID param.Field[string] `json:"id"`
|
||||
Source param.Field[AgentPartInputSourceParam] `json:"source"`
|
||||
}
|
||||
|
||||
func (r AgentPartInputParam) MarshalJSON() (data []byte, err error) {
|
||||
return apijson.MarshalRoot(r)
|
||||
}
|
||||
|
||||
func (r AgentPartInputParam) implementsSessionChatParamsPartUnion() {}
|
||||
|
||||
type AgentPartInputType string
|
||||
|
||||
const (
|
||||
AgentPartInputTypeAgent AgentPartInputType = "agent"
|
||||
)
|
||||
|
||||
func (r AgentPartInputType) IsKnown() bool {
|
||||
switch r {
|
||||
case AgentPartInputTypeAgent:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type AgentPartInputSourceParam struct {
|
||||
End param.Field[int64] `json:"end,required"`
|
||||
Start param.Field[int64] `json:"start,required"`
|
||||
Value param.Field[string] `json:"value,required"`
|
||||
}
|
||||
|
||||
func (r AgentPartInputSourceParam) MarshalJSON() (data []byte, err error) {
|
||||
return apijson.MarshalRoot(r)
|
||||
}
|
||||
|
||||
type AssistantMessage struct {
|
||||
ID string `json:"id,required"`
|
||||
Cost float64 `json:"cost,required"`
|
||||
@@ -855,11 +962,13 @@ type Part struct {
|
||||
Cost float64 `json:"cost"`
|
||||
Filename string `json:"filename"`
|
||||
// This field can have the runtime type of [[]string].
|
||||
Files interface{} `json:"files"`
|
||||
Hash string `json:"hash"`
|
||||
Mime string `json:"mime"`
|
||||
Snapshot string `json:"snapshot"`
|
||||
Source FilePartSource `json:"source"`
|
||||
Files interface{} `json:"files"`
|
||||
Hash string `json:"hash"`
|
||||
Mime string `json:"mime"`
|
||||
Name string `json:"name"`
|
||||
Snapshot string `json:"snapshot"`
|
||||
// This field can have the runtime type of [FilePartSource], [AgentPartSource].
|
||||
Source interface{} `json:"source"`
|
||||
// This field can have the runtime type of [ToolPartState].
|
||||
State interface{} `json:"state"`
|
||||
Synthetic bool `json:"synthetic"`
|
||||
@@ -886,6 +995,7 @@ type partJSON struct {
|
||||
Files apijson.Field
|
||||
Hash apijson.Field
|
||||
Mime apijson.Field
|
||||
Name apijson.Field
|
||||
Snapshot apijson.Field
|
||||
Source apijson.Field
|
||||
State apijson.Field
|
||||
@@ -916,13 +1026,13 @@ func (r *Part) UnmarshalJSON(data []byte) (err error) {
|
||||
// for more type safety.
|
||||
//
|
||||
// Possible runtime types of the union are [TextPart], [FilePart], [ToolPart],
|
||||
// [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart].
|
||||
// [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart], [AgentPart].
|
||||
func (r Part) AsUnion() PartUnion {
|
||||
return r.union
|
||||
}
|
||||
|
||||
// Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart],
|
||||
// [StepFinishPart], [SnapshotPart] or [PartPatchPart].
|
||||
// [StepFinishPart], [SnapshotPart], [PartPatchPart] or [AgentPart].
|
||||
type PartUnion interface {
|
||||
implementsPart()
|
||||
}
|
||||
@@ -966,6 +1076,11 @@ func init() {
|
||||
Type: reflect.TypeOf(PartPatchPart{}),
|
||||
DiscriminatorValue: "patch",
|
||||
},
|
||||
apijson.UnionVariant{
|
||||
TypeFilter: gjson.JSON,
|
||||
Type: reflect.TypeOf(AgentPart{}),
|
||||
DiscriminatorValue: "agent",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1025,11 +1140,12 @@ const (
|
||||
PartTypeStepFinish PartType = "step-finish"
|
||||
PartTypeSnapshot PartType = "snapshot"
|
||||
PartTypePatch PartType = "patch"
|
||||
PartTypeAgent PartType = "agent"
|
||||
)
|
||||
|
||||
func (r PartType) IsKnown() bool {
|
||||
switch r {
|
||||
case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot, PartTypePatch:
|
||||
case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot, PartTypePatch, PartTypeAgent:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -2080,8 +2196,8 @@ type SessionChatParams struct {
|
||||
ModelID param.Field[string] `json:"modelID,required"`
|
||||
Parts param.Field[[]SessionChatParamsPartUnion] `json:"parts,required"`
|
||||
ProviderID param.Field[string] `json:"providerID,required"`
|
||||
Agent param.Field[string] `json:"agent"`
|
||||
MessageID param.Field[string] `json:"messageID"`
|
||||
Mode param.Field[string] `json:"mode"`
|
||||
System param.Field[string] `json:"system"`
|
||||
Tools param.Field[map[string]bool] `json:"tools"`
|
||||
}
|
||||
@@ -2095,7 +2211,8 @@ type SessionChatParamsPart struct {
|
||||
ID param.Field[string] `json:"id"`
|
||||
Filename param.Field[string] `json:"filename"`
|
||||
Mime param.Field[string] `json:"mime"`
|
||||
Source param.Field[FilePartSourceUnionParam] `json:"source"`
|
||||
Name param.Field[string] `json:"name"`
|
||||
Source param.Field[interface{}] `json:"source"`
|
||||
Synthetic param.Field[bool] `json:"synthetic"`
|
||||
Text param.Field[string] `json:"text"`
|
||||
Time param.Field[interface{}] `json:"time"`
|
||||
@@ -2108,7 +2225,7 @@ func (r SessionChatParamsPart) MarshalJSON() (data []byte, err error) {
|
||||
|
||||
func (r SessionChatParamsPart) implementsSessionChatParamsPartUnion() {}
|
||||
|
||||
// Satisfied by [TextPartInputParam], [FilePartInputParam],
|
||||
// Satisfied by [TextPartInputParam], [FilePartInputParam], [AgentPartInputParam],
|
||||
// [SessionChatParamsPart].
|
||||
type SessionChatParamsPartUnion interface {
|
||||
implementsSessionChatParamsPartUnion()
|
||||
@@ -2117,13 +2234,14 @@ type SessionChatParamsPartUnion interface {
|
||||
type SessionChatParamsPartsType string
|
||||
|
||||
const (
|
||||
SessionChatParamsPartsTypeText SessionChatParamsPartsType = "text"
|
||||
SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file"
|
||||
SessionChatParamsPartsTypeText SessionChatParamsPartsType = "text"
|
||||
SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file"
|
||||
SessionChatParamsPartsTypeAgent SessionChatParamsPartsType = "agent"
|
||||
)
|
||||
|
||||
func (r SessionChatParamsPartsType) IsKnown() bool {
|
||||
switch r {
|
||||
case SessionChatParamsPartsTypeText, SessionChatParamsPartsTypeFile:
|
||||
case SessionChatParamsPartsTypeText, SessionChatParamsPartsTypeFile, SessionChatParamsPartsTypeAgent:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -129,8 +129,8 @@ func TestSessionChatWithOptionalParams(t *testing.T) {
|
||||
}),
|
||||
}}),
|
||||
ProviderID: opencode.F("providerID"),
|
||||
Agent: opencode.F("agent"),
|
||||
MessageID: opencode.F("msg"),
|
||||
Mode: opencode.F("mode"),
|
||||
System: opencode.F("system"),
|
||||
Tools: opencode.F(map[string]bool{
|
||||
"foo": true,
|
||||
|
||||
@@ -47,7 +47,7 @@ func (r *TuiService) ClearPrompt(ctx context.Context, opts ...option.RequestOpti
|
||||
return
|
||||
}
|
||||
|
||||
// Execute a TUI command (e.g. switch_mode)
|
||||
// Execute a TUI command (e.g. switch_agent)
|
||||
func (r *TuiService) ExecuteCommand(ctx context.Context, body TuiExecuteCommandParams, opts ...option.RequestOption) (res *bool, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "tui/execute-command"
|
||||
|
||||
@@ -51,12 +51,12 @@ resources:
|
||||
logLevel: LogLevel
|
||||
provider: Provider
|
||||
model: Model
|
||||
mode: Mode
|
||||
agent: Agent
|
||||
methods:
|
||||
get: get /app
|
||||
init: post /app/init
|
||||
log: post /log
|
||||
modes: get /mode
|
||||
agents: get /agent
|
||||
providers: get /config/providers
|
||||
|
||||
find:
|
||||
@@ -99,6 +99,8 @@ resources:
|
||||
fileSource: FileSource
|
||||
symbolSource: SymbolSource
|
||||
toolPart: ToolPart
|
||||
agentPart: AgentPart
|
||||
agentPartInput: AgentPartInput
|
||||
stepStartPart: StepStartPart
|
||||
stepFinishPart: StepFinishPart
|
||||
snapshotPart: SnapshotPart
|
||||
|
||||
@@ -31,7 +31,7 @@ func main() {
|
||||
|
||||
var model *string = flag.String("model", "", "model to begin with")
|
||||
var prompt *string = flag.String("prompt", "", "prompt to begin with")
|
||||
var mode *string = flag.String("mode", "", "mode to begin with")
|
||||
var agent *string = flag.String("agent", "", "agent to begin with")
|
||||
flag.Parse()
|
||||
|
||||
url := os.Getenv("OPENCODE_SERVER")
|
||||
@@ -44,9 +44,9 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
modesStr := os.Getenv("OPENCODE_MODES")
|
||||
var modes []opencode.Mode
|
||||
err = json.Unmarshal([]byte(modesStr), &modes)
|
||||
agentsStr := os.Getenv("OPENCODE_AGENTS")
|
||||
var agents []opencode.Agent
|
||||
err = json.Unmarshal([]byte(agentsStr), &agents)
|
||||
if err != nil {
|
||||
slog.Error("Failed to unmarshal modes", "error", err)
|
||||
os.Exit(1)
|
||||
@@ -86,7 +86,7 @@ func main() {
|
||||
logger := slog.New(apiHandler)
|
||||
slog.SetDefault(logger)
|
||||
|
||||
slog.Debug("TUI launched", "app", appInfoStr, "modes", modesStr, "url", url)
|
||||
slog.Debug("TUI launched", "app", appInfoStr, "modes", agentsStr, "url", url)
|
||||
|
||||
go func() {
|
||||
err = clipboard.Init()
|
||||
@@ -96,7 +96,7 @@ func main() {
|
||||
}()
|
||||
|
||||
// Create main context for the application
|
||||
app_, err := app.New(ctx, version, appInfo, modes, httpClient, model, prompt, mode)
|
||||
app_, err := app.New(ctx, version, appInfo, agents, httpClient, model, prompt, agent)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"log/slog"
|
||||
@@ -27,15 +28,14 @@ type Message struct {
|
||||
|
||||
type App struct {
|
||||
Info opencode.App
|
||||
Modes []opencode.Mode
|
||||
Agents []opencode.Agent
|
||||
Providers []opencode.Provider
|
||||
Version string
|
||||
StatePath string
|
||||
Config *opencode.Config
|
||||
Client *opencode.Client
|
||||
State *State
|
||||
ModeIndex int
|
||||
Mode *opencode.Mode
|
||||
AgentIndex int
|
||||
Provider *opencode.Provider
|
||||
Model *opencode.Model
|
||||
Session *opencode.Session
|
||||
@@ -45,11 +45,15 @@ type App struct {
|
||||
Commands commands.CommandRegistry
|
||||
InitialModel *string
|
||||
InitialPrompt *string
|
||||
IntitialMode *string
|
||||
InitialAgent *string
|
||||
compactCancel context.CancelFunc
|
||||
IsLeaderSequence bool
|
||||
}
|
||||
|
||||
func (a *App) Agent() *opencode.Agent {
|
||||
return &a.Agents[a.AgentIndex]
|
||||
}
|
||||
|
||||
type SessionCreatedMsg = struct {
|
||||
Session *opencode.Session
|
||||
}
|
||||
@@ -83,11 +87,11 @@ func New(
|
||||
ctx context.Context,
|
||||
version string,
|
||||
appInfo opencode.App,
|
||||
modes []opencode.Mode,
|
||||
agents []opencode.Agent,
|
||||
httpClient *opencode.Client,
|
||||
initialModel *string,
|
||||
initialPrompt *string,
|
||||
initialMode *string,
|
||||
initialAgent *string,
|
||||
) (*App, error) {
|
||||
util.RootPath = appInfo.Path.Root
|
||||
util.CwdPath = appInfo.Path.Cwd
|
||||
@@ -108,8 +112,8 @@ func New(
|
||||
SaveState(appStatePath, appState)
|
||||
}
|
||||
|
||||
if appState.ModeModel == nil {
|
||||
appState.ModeModel = make(map[string]ModeModel)
|
||||
if appState.AgentModel == nil {
|
||||
appState.AgentModel = make(map[string]AgentModel)
|
||||
}
|
||||
|
||||
if configInfo.Theme != "" {
|
||||
@@ -121,27 +125,29 @@ func New(
|
||||
appState.Theme = themeEnv
|
||||
}
|
||||
|
||||
var modeIndex int
|
||||
var mode *opencode.Mode
|
||||
agentIndex := slices.IndexFunc(agents, func(a opencode.Agent) bool {
|
||||
return a.Mode != "subagent"
|
||||
})
|
||||
var agent *opencode.Agent
|
||||
modeName := "build"
|
||||
if appState.Mode != "" {
|
||||
modeName = appState.Mode
|
||||
if appState.Agent != "" {
|
||||
modeName = appState.Agent
|
||||
}
|
||||
if initialMode != nil && *initialMode != "" {
|
||||
modeName = *initialMode
|
||||
if initialAgent != nil && *initialAgent != "" {
|
||||
modeName = *initialAgent
|
||||
}
|
||||
for i, m := range modes {
|
||||
for i, m := range agents {
|
||||
if m.Name == modeName {
|
||||
modeIndex = i
|
||||
agentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
mode = &modes[modeIndex]
|
||||
agent = &agents[agentIndex]
|
||||
|
||||
if mode.Model.ModelID != "" {
|
||||
appState.ModeModel[mode.Name] = ModeModel{
|
||||
ProviderID: mode.Model.ProviderID,
|
||||
ModelID: mode.Model.ModelID,
|
||||
if agent.Model.ModelID != "" {
|
||||
appState.AgentModel[agent.Name] = AgentModel{
|
||||
ProviderID: agent.Model.ProviderID,
|
||||
ModelID: agent.Model.ModelID,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,20 +173,19 @@ func New(
|
||||
|
||||
app := &App{
|
||||
Info: appInfo,
|
||||
Modes: modes,
|
||||
Agents: agents,
|
||||
Version: version,
|
||||
StatePath: appStatePath,
|
||||
Config: configInfo,
|
||||
State: appState,
|
||||
Client: httpClient,
|
||||
ModeIndex: modeIndex,
|
||||
Mode: mode,
|
||||
AgentIndex: agentIndex,
|
||||
Session: &opencode.Session{},
|
||||
Messages: []Message{},
|
||||
Commands: commands.LoadFromConfig(configInfo),
|
||||
InitialModel: initialModel,
|
||||
InitialPrompt: initialPrompt,
|
||||
IntitialMode: initialMode,
|
||||
InitialAgent: initialAgent,
|
||||
}
|
||||
|
||||
return app, nil
|
||||
@@ -222,22 +227,24 @@ func SetClipboard(text string) tea.Cmd {
|
||||
|
||||
func (a *App) cycleMode(forward bool) (*App, tea.Cmd) {
|
||||
if forward {
|
||||
a.ModeIndex++
|
||||
if a.ModeIndex >= len(a.Modes) {
|
||||
a.ModeIndex = 0
|
||||
a.AgentIndex++
|
||||
if a.AgentIndex >= len(a.Agents) {
|
||||
a.AgentIndex = 0
|
||||
}
|
||||
} else {
|
||||
a.ModeIndex--
|
||||
if a.ModeIndex < 0 {
|
||||
a.ModeIndex = len(a.Modes) - 1
|
||||
a.AgentIndex--
|
||||
if a.AgentIndex < 0 {
|
||||
a.AgentIndex = len(a.Agents) - 1
|
||||
}
|
||||
}
|
||||
a.Mode = &a.Modes[a.ModeIndex]
|
||||
if a.Agent().Mode == "subagent" {
|
||||
return a.cycleMode(forward)
|
||||
}
|
||||
|
||||
modelID := a.Mode.Model.ModelID
|
||||
providerID := a.Mode.Model.ProviderID
|
||||
modelID := a.Agent().Model.ModelID
|
||||
providerID := a.Agent().Model.ProviderID
|
||||
if modelID == "" {
|
||||
if model, ok := a.State.ModeModel[a.Mode.Name]; ok {
|
||||
if model, ok := a.State.AgentModel[a.Agent().Name]; ok {
|
||||
modelID = model.ModelID
|
||||
providerID = model.ProviderID
|
||||
}
|
||||
@@ -258,20 +265,23 @@ func (a *App) cycleMode(forward bool) (*App, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
a.State.Mode = a.Mode.Name
|
||||
a.State.Agent = a.Agent().Name
|
||||
return a, a.SaveState()
|
||||
}
|
||||
|
||||
func (a *App) SwitchMode() (*App, tea.Cmd) {
|
||||
func (a *App) SwitchAgent() (*App, tea.Cmd) {
|
||||
return a.cycleMode(true)
|
||||
}
|
||||
|
||||
func (a *App) SwitchModeReverse() (*App, tea.Cmd) {
|
||||
func (a *App) SwitchAgentReverse() (*App, tea.Cmd) {
|
||||
return a.cycleMode(false)
|
||||
}
|
||||
|
||||
// findModelByFullID finds a model by its full ID in the format "provider/model"
|
||||
func findModelByFullID(providers []opencode.Provider, fullModelID string) (*opencode.Provider, *opencode.Model) {
|
||||
func findModelByFullID(
|
||||
providers []opencode.Provider,
|
||||
fullModelID string,
|
||||
) (*opencode.Provider, *opencode.Model) {
|
||||
modelParts := strings.SplitN(fullModelID, "/", 2)
|
||||
if len(modelParts) < 2 {
|
||||
return nil, nil
|
||||
@@ -284,7 +294,10 @@ func findModelByFullID(providers []opencode.Provider, fullModelID string) (*open
|
||||
}
|
||||
|
||||
// findModelByProviderAndModelID finds a model by provider ID and model ID
|
||||
func findModelByProviderAndModelID(providers []opencode.Provider, providerID, modelID string) (*opencode.Provider, *opencode.Model) {
|
||||
func findModelByProviderAndModelID(
|
||||
providers []opencode.Provider,
|
||||
providerID, modelID string,
|
||||
) (*opencode.Provider, *opencode.Model) {
|
||||
for _, provider := range providers {
|
||||
if provider.ID != providerID {
|
||||
continue
|
||||
@@ -330,7 +343,7 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
a.Providers = providers
|
||||
|
||||
// retains backwards compatibility with old state format
|
||||
if model, ok := a.State.ModeModel[a.State.Mode]; ok {
|
||||
if model, ok := a.State.AgentModel[a.State.Agent]; ok {
|
||||
a.State.Provider = model.ProviderID
|
||||
a.State.Model = model.ModelID
|
||||
}
|
||||
@@ -340,10 +353,17 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
|
||||
// Priority 1: Command line --model flag (InitialModel)
|
||||
if a.InitialModel != nil && *a.InitialModel != "" {
|
||||
if provider, model := findModelByFullID(providers, *a.InitialModel); provider != nil && model != nil {
|
||||
if provider, model := findModelByFullID(providers, *a.InitialModel); provider != nil &&
|
||||
model != nil {
|
||||
selectedProvider = provider
|
||||
selectedModel = model
|
||||
slog.Debug("Selected model from command line", "provider", provider.ID, "model", model.ID)
|
||||
slog.Debug(
|
||||
"Selected model from command line",
|
||||
"provider",
|
||||
provider.ID,
|
||||
"model",
|
||||
model.ID,
|
||||
)
|
||||
} else {
|
||||
slog.Debug("Command line model not found", "model", *a.InitialModel)
|
||||
}
|
||||
@@ -351,7 +371,8 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
|
||||
// Priority 2: Config file model setting
|
||||
if selectedProvider == nil && a.Config.Model != "" {
|
||||
if provider, model := findModelByFullID(providers, a.Config.Model); provider != nil && model != nil {
|
||||
if provider, model := findModelByFullID(providers, a.Config.Model); provider != nil &&
|
||||
model != nil {
|
||||
selectedProvider = provider
|
||||
selectedModel = model
|
||||
slog.Debug("Selected model from config", "provider", provider.ID, "model", model.ID)
|
||||
@@ -363,10 +384,17 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
// Priority 3: Recent model usage (most recently used model)
|
||||
if selectedProvider == nil && len(a.State.RecentlyUsedModels) > 0 {
|
||||
recentUsage := a.State.RecentlyUsedModels[0] // Most recent is first
|
||||
if provider, model := findModelByProviderAndModelID(providers, recentUsage.ProviderID, recentUsage.ModelID); provider != nil && model != nil {
|
||||
if provider, model := findModelByProviderAndModelID(providers, recentUsage.ProviderID, recentUsage.ModelID); provider != nil &&
|
||||
model != nil {
|
||||
selectedProvider = provider
|
||||
selectedModel = model
|
||||
slog.Debug("Selected model from recent usage", "provider", provider.ID, "model", model.ID)
|
||||
slog.Debug(
|
||||
"Selected model from recent usage",
|
||||
"provider",
|
||||
provider.ID,
|
||||
"model",
|
||||
model.ID,
|
||||
)
|
||||
} else {
|
||||
slog.Debug("Recent model not found", "provider", recentUsage.ProviderID, "model", recentUsage.ModelID)
|
||||
}
|
||||
@@ -374,7 +402,8 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
|
||||
// Priority 4: State-based model (backwards compatibility)
|
||||
if selectedProvider == nil && a.State.Provider != "" && a.State.Model != "" {
|
||||
if provider, model := findModelByProviderAndModelID(providers, a.State.Provider, a.State.Model); provider != nil && model != nil {
|
||||
if provider, model := findModelByProviderAndModelID(providers, a.State.Provider, a.State.Model); provider != nil &&
|
||||
model != nil {
|
||||
selectedProvider = provider
|
||||
selectedModel = model
|
||||
slog.Debug("Selected model from state", "provider", provider.ID, "model", model.ID)
|
||||
@@ -390,7 +419,13 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
if model := getDefaultModel(providersResponse, *provider); model != nil {
|
||||
selectedProvider = provider
|
||||
selectedModel = model
|
||||
slog.Debug("Selected model from internal priority (Anthropic)", "provider", provider.ID, "model", model.ID)
|
||||
slog.Debug(
|
||||
"Selected model from internal priority (Anthropic)",
|
||||
"provider",
|
||||
provider.ID,
|
||||
"model",
|
||||
model.ID,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,7 +435,13 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
if model := getDefaultModel(providersResponse, *provider); model != nil {
|
||||
selectedProvider = provider
|
||||
selectedModel = model
|
||||
slog.Debug("Selected model from fallback (first available)", "provider", provider.ID, "model", model.ID)
|
||||
slog.Debug(
|
||||
"Selected model from fallback (first available)",
|
||||
"provider",
|
||||
provider.ID,
|
||||
"model",
|
||||
model.ID,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -552,7 +593,7 @@ func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) {
|
||||
_, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{
|
||||
ProviderID: opencode.F(a.Provider.ID),
|
||||
ModelID: opencode.F(a.Model.ID),
|
||||
Mode: opencode.F(a.Mode.Name),
|
||||
Agent: opencode.F(a.Agent().Name),
|
||||
MessageID: opencode.F(messageID),
|
||||
Parts: opencode.F(message.ToSessionChatParams()),
|
||||
})
|
||||
|
||||
@@ -55,6 +55,22 @@ func (p Prompt) ToMessage(
|
||||
Text: text,
|
||||
}}
|
||||
for _, attachment := range p.Attachments {
|
||||
if attachment.Type == "agent" {
|
||||
source, _ := attachment.GetAgentSource()
|
||||
parts = append(parts, opencode.AgentPart{
|
||||
ID: id.Ascending(id.Part),
|
||||
MessageID: messageID,
|
||||
SessionID: sessionID,
|
||||
Name: source.Name,
|
||||
Source: opencode.AgentPartSource{
|
||||
Value: attachment.Display,
|
||||
Start: int64(attachment.StartIndex),
|
||||
End: int64(attachment.EndIndex),
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
text := opencode.FilePartSourceText{
|
||||
Start: int64(attachment.StartIndex),
|
||||
End: int64(attachment.EndIndex),
|
||||
@@ -122,6 +138,17 @@ func (m Message) ToPrompt() (*Prompt, error) {
|
||||
continue
|
||||
}
|
||||
text += p.Text + " "
|
||||
case opencode.AgentPart:
|
||||
attachments = append(attachments, &attachment.Attachment{
|
||||
ID: p.ID,
|
||||
Type: "agent",
|
||||
Display: p.Source.Value,
|
||||
StartIndex: int(p.Source.Start),
|
||||
EndIndex: int(p.Source.End),
|
||||
Source: &attachment.AgentSource{
|
||||
Name: p.Name,
|
||||
},
|
||||
})
|
||||
case opencode.FilePart:
|
||||
switch p.Source.Type {
|
||||
case "file":
|
||||
@@ -236,68 +263,18 @@ func (m Message) ToSessionChatParams() []opencode.SessionChatParamsPartUnion {
|
||||
Filename: opencode.F(p.Filename),
|
||||
Source: opencode.F(source),
|
||||
})
|
||||
case opencode.AgentPart:
|
||||
parts = append(parts, opencode.AgentPartInputParam{
|
||||
ID: opencode.F(p.ID),
|
||||
Type: opencode.F(opencode.AgentPartInputTypeAgent),
|
||||
Name: opencode.F(p.Name),
|
||||
Source: opencode.F(opencode.AgentPartInputSourceParam{
|
||||
Value: opencode.F(p.Source.Value),
|
||||
Start: opencode.F(p.Source.Start),
|
||||
End: opencode.F(p.Source.End),
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func (p Prompt) ToSessionChatParams() []opencode.SessionChatParamsPartUnion {
|
||||
parts := []opencode.SessionChatParamsPartUnion{
|
||||
opencode.TextPartInputParam{
|
||||
Type: opencode.F(opencode.TextPartInputTypeText),
|
||||
Text: opencode.F(p.Text),
|
||||
},
|
||||
}
|
||||
for _, att := range p.Attachments {
|
||||
filePart := opencode.FilePartInputParam{
|
||||
Type: opencode.F(opencode.FilePartInputTypeFile),
|
||||
Mime: opencode.F(att.MediaType),
|
||||
URL: opencode.F(att.URL),
|
||||
Filename: opencode.F(att.Filename),
|
||||
}
|
||||
switch att.Type {
|
||||
case "file":
|
||||
if fs, ok := att.GetFileSource(); ok {
|
||||
filePart.Source = opencode.F(
|
||||
opencode.FilePartSourceUnionParam(opencode.FileSourceParam{
|
||||
Type: opencode.F(opencode.FileSourceTypeFile),
|
||||
Path: opencode.F(fs.Path),
|
||||
Text: opencode.F(opencode.FilePartSourceTextParam{
|
||||
Start: opencode.F(int64(att.StartIndex)),
|
||||
End: opencode.F(int64(att.EndIndex)),
|
||||
Value: opencode.F(att.Display),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
case "symbol":
|
||||
if ss, ok := att.GetSymbolSource(); ok {
|
||||
filePart.Source = opencode.F(
|
||||
opencode.FilePartSourceUnionParam(opencode.SymbolSourceParam{
|
||||
Type: opencode.F(opencode.SymbolSourceTypeSymbol),
|
||||
Path: opencode.F(ss.Path),
|
||||
Name: opencode.F(ss.Name),
|
||||
Kind: opencode.F(int64(ss.Kind)),
|
||||
Range: opencode.F(opencode.SymbolSourceRangeParam{
|
||||
Start: opencode.F(opencode.SymbolSourceRangeStartParam{
|
||||
Line: opencode.F(float64(ss.Range.Start.Line)),
|
||||
Character: opencode.F(float64(ss.Range.Start.Char)),
|
||||
}),
|
||||
End: opencode.F(opencode.SymbolSourceRangeEndParam{
|
||||
Line: opencode.F(float64(ss.Range.End.Line)),
|
||||
Character: opencode.F(float64(ss.Range.End.Char)),
|
||||
}),
|
||||
}),
|
||||
Text: opencode.F(opencode.FilePartSourceTextParam{
|
||||
Start: opencode.F(int64(att.StartIndex)),
|
||||
End: opencode.F(int64(att.EndIndex)),
|
||||
Value: opencode.F(att.Display),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
parts = append(parts, filePart)
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
@@ -16,29 +16,29 @@ type ModelUsage struct {
|
||||
LastUsed time.Time `toml:"last_used"`
|
||||
}
|
||||
|
||||
type ModeModel struct {
|
||||
type AgentModel struct {
|
||||
ProviderID string `toml:"provider_id"`
|
||||
ModelID string `toml:"model_id"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Theme string `toml:"theme"`
|
||||
ScrollSpeed *int `toml:"scroll_speed"`
|
||||
ModeModel map[string]ModeModel `toml:"mode_model"`
|
||||
Provider string `toml:"provider"`
|
||||
Model string `toml:"model"`
|
||||
Mode string `toml:"mode"`
|
||||
RecentlyUsedModels []ModelUsage `toml:"recently_used_models"`
|
||||
MessagesRight bool `toml:"messages_right"`
|
||||
SplitDiff bool `toml:"split_diff"`
|
||||
MessageHistory []Prompt `toml:"message_history"`
|
||||
Theme string `toml:"theme"`
|
||||
ScrollSpeed *int `toml:"scroll_speed"`
|
||||
AgentModel map[string]AgentModel `toml:"agent_model"`
|
||||
Provider string `toml:"provider"`
|
||||
Model string `toml:"model"`
|
||||
Agent string `toml:"agent"`
|
||||
RecentlyUsedModels []ModelUsage `toml:"recently_used_models"`
|
||||
MessagesRight bool `toml:"messages_right"`
|
||||
SplitDiff bool `toml:"split_diff"`
|
||||
MessageHistory []Prompt `toml:"message_history"`
|
||||
}
|
||||
|
||||
func NewState() *State {
|
||||
return &State{
|
||||
Theme: "opencode",
|
||||
Mode: "build",
|
||||
ModeModel: make(map[string]ModeModel),
|
||||
Agent: "build",
|
||||
AgentModel: make(map[string]AgentModel),
|
||||
RecentlyUsedModels: make([]ModelUsage, 0),
|
||||
MessageHistory: make([]Prompt, 0),
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ type SymbolRange struct {
|
||||
End Position `toml:"end"`
|
||||
}
|
||||
|
||||
type AgentSource struct {
|
||||
Name string `toml:"name"`
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
Line int `toml:"line"`
|
||||
Char int `toml:"char"`
|
||||
@@ -76,6 +80,15 @@ func (a *Attachment) GetSymbolSource() (*SymbolSource, bool) {
|
||||
return ss, ok
|
||||
}
|
||||
|
||||
// GetAgentSource returns the source as AgentSource if the attachment is an agent type
|
||||
func (a *Attachment) GetAgentSource() (*AgentSource, bool) {
|
||||
if a.Type != "agent" {
|
||||
return nil, false
|
||||
}
|
||||
as, ok := a.Source.(*AgentSource)
|
||||
return as, ok
|
||||
}
|
||||
|
||||
// FromMap creates a TextSource from a map[string]any
|
||||
func (ts *TextSource) FromMap(sourceMap map[string]any) {
|
||||
if value, ok := sourceMap["value"].(string); ok {
|
||||
@@ -128,6 +141,13 @@ func (ss *SymbolSource) FromMap(sourceMap map[string]any) {
|
||||
}
|
||||
}
|
||||
|
||||
// FromMap creates an AgentSource from a map[string]any
|
||||
func (as *AgentSource) FromMap(sourceMap map[string]any) {
|
||||
if name, ok := sourceMap["name"].(string); ok {
|
||||
as.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
// RestoreSourceType converts a map[string]any source back to the proper type
|
||||
func (a *Attachment) RestoreSourceType() {
|
||||
if a.Source == nil {
|
||||
@@ -149,6 +169,10 @@ func (a *Attachment) RestoreSourceType() {
|
||||
ss := &SymbolSource{}
|
||||
ss.FromMap(sourceMap)
|
||||
a.Source = ss
|
||||
case "agent":
|
||||
as := &AgentSource{}
|
||||
as.FromMap(sourceMap)
|
||||
a.Source = as
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,8 +107,8 @@ func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
|
||||
|
||||
const (
|
||||
AppHelpCommand CommandName = "app_help"
|
||||
SwitchModeCommand CommandName = "switch_mode"
|
||||
SwitchModeReverseCommand CommandName = "switch_mode_reverse"
|
||||
SwitchAgentCommand CommandName = "switch_agent"
|
||||
SwitchAgentReverseCommand CommandName = "switch_agent_reverse"
|
||||
EditorOpenCommand CommandName = "editor_open"
|
||||
SessionNewCommand CommandName = "session_new"
|
||||
SessionListCommand CommandName = "session_list"
|
||||
@@ -181,13 +181,13 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
Trigger: []string{"help"},
|
||||
},
|
||||
{
|
||||
Name: SwitchModeCommand,
|
||||
Description: "next mode",
|
||||
Name: SwitchAgentCommand,
|
||||
Description: "next agent",
|
||||
Keybindings: parseBindings("tab"),
|
||||
},
|
||||
{
|
||||
Name: SwitchModeReverseCommand,
|
||||
Description: "previous mode",
|
||||
Name: SwitchAgentReverseCommand,
|
||||
Description: "previous agent",
|
||||
Keybindings: parseBindings("shift+tab"),
|
||||
},
|
||||
{
|
||||
|
||||
74
packages/tui/internal/completions/agents.go
Normal file
74
packages/tui/internal/completions/agents.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package completions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
type agentsContextGroup struct {
|
||||
app *app.App
|
||||
}
|
||||
|
||||
func (cg *agentsContextGroup) GetId() string {
|
||||
return "agents"
|
||||
}
|
||||
|
||||
func (cg *agentsContextGroup) GetEmptyMessage() string {
|
||||
return "no matching agents"
|
||||
}
|
||||
|
||||
func (cg *agentsContextGroup) GetChildEntries(
|
||||
query string,
|
||||
) ([]CompletionSuggestion, error) {
|
||||
items := make([]CompletionSuggestion, 0)
|
||||
|
||||
query = strings.TrimSpace(query)
|
||||
|
||||
agents, err := cg.app.Client.App.Agents(
|
||||
context.Background(),
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get agent list", "error", err)
|
||||
return items, err
|
||||
}
|
||||
if agents == nil {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
for _, agent := range *agents {
|
||||
if query != "" && !strings.Contains(strings.ToLower(agent.Name), strings.ToLower(query)) {
|
||||
continue
|
||||
}
|
||||
if agent.Mode == opencode.AgentModePrimary || agent.Name == "general" {
|
||||
continue
|
||||
}
|
||||
|
||||
displayFunc := func(s styles.Style) string {
|
||||
t := theme.CurrentTheme()
|
||||
muted := s.Foreground(t.TextMuted()).Render
|
||||
return s.Render(agent.Name) + muted(" (agent)")
|
||||
}
|
||||
|
||||
item := CompletionSuggestion{
|
||||
Display: displayFunc,
|
||||
Value: agent.Name,
|
||||
ProviderID: cg.GetId(),
|
||||
RawData: agent,
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func NewAgentsContextGroup(app *app.App) CompletionProvider {
|
||||
return &agentsContextGroup{
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
@@ -288,6 +288,31 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.textarea.InsertAttachment(attachment)
|
||||
m.textarea.InsertString(" ")
|
||||
return m, nil
|
||||
case "agents":
|
||||
atIndex := m.textarea.LastRuneIndex('@')
|
||||
if atIndex == -1 {
|
||||
// Should not happen, but as a fallback, just insert.
|
||||
m.textarea.InsertString(msg.Item.Value + " ")
|
||||
return m, nil
|
||||
}
|
||||
|
||||
cursorCol := m.textarea.CursorColumn()
|
||||
m.textarea.ReplaceRange(atIndex, cursorCol, "")
|
||||
|
||||
name := msg.Item.Value
|
||||
attachment := &attachment.Attachment{
|
||||
ID: uuid.NewString(),
|
||||
Type: "agent",
|
||||
Display: "@" + name,
|
||||
Source: &attachment.AgentSource{
|
||||
Name: name,
|
||||
},
|
||||
}
|
||||
|
||||
m.textarea.InsertAttachment(attachment)
|
||||
m.textarea.InsertString(" ")
|
||||
return m, nil
|
||||
|
||||
default:
|
||||
slog.Debug("Unknown provider", "provider", msg.Item.ProviderID)
|
||||
return m, nil
|
||||
|
||||
@@ -209,6 +209,7 @@ func renderText(
|
||||
width int,
|
||||
extra string,
|
||||
fileParts []opencode.FilePart,
|
||||
agentParts []opencode.AgentPart,
|
||||
toolCalls ...opencode.ToolPart,
|
||||
) string {
|
||||
t := theme.CurrentTheme()
|
||||
@@ -229,9 +230,47 @@ func renderText(
|
||||
|
||||
// Apply highlighting to filenames and base style to rest of text BEFORE wrapping
|
||||
textLen := int64(len(text))
|
||||
|
||||
// Collect all parts to highlight (both file and agent parts)
|
||||
type highlightPart struct {
|
||||
start int64
|
||||
end int64
|
||||
color compat.AdaptiveColor
|
||||
}
|
||||
var highlights []highlightPart
|
||||
|
||||
// Add file parts with secondary color
|
||||
for _, filePart := range fileParts {
|
||||
highlight := base.Foreground(t.Secondary())
|
||||
start, end := filePart.Source.Text.Start, filePart.Source.Text.End
|
||||
highlights = append(highlights, highlightPart{
|
||||
start: filePart.Source.Text.Start,
|
||||
end: filePart.Source.Text.End,
|
||||
color: t.Secondary(),
|
||||
})
|
||||
}
|
||||
|
||||
// Add agent parts with secondary color (same as file parts)
|
||||
for _, agentPart := range agentParts {
|
||||
highlights = append(highlights, highlightPart{
|
||||
start: agentPart.Source.Start,
|
||||
end: agentPart.Source.End,
|
||||
color: t.Secondary(),
|
||||
})
|
||||
}
|
||||
|
||||
// Sort highlights by start position
|
||||
slices.SortFunc(highlights, func(a, b highlightPart) int {
|
||||
if a.start < b.start {
|
||||
return -1
|
||||
}
|
||||
if a.start > b.start {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
for _, part := range highlights {
|
||||
highlight := base.Foreground(part.color)
|
||||
start, end := part.start, part.end
|
||||
|
||||
if end > textLen {
|
||||
end = textLen
|
||||
|
||||
@@ -300,12 +300,17 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
}
|
||||
remainingParts := message.Parts[partIndex+1:]
|
||||
fileParts := make([]opencode.FilePart, 0)
|
||||
agentParts := make([]opencode.AgentPart, 0)
|
||||
for _, part := range remainingParts {
|
||||
switch part := part.(type) {
|
||||
case opencode.FilePart:
|
||||
if part.Source.Text.Start >= 0 && part.Source.Text.End >= part.Source.Text.Start {
|
||||
fileParts = append(fileParts, part)
|
||||
}
|
||||
case opencode.AgentPart:
|
||||
if part.Source.Start >= 0 && part.Source.End >= part.Source.Start {
|
||||
agentParts = append(agentParts, part)
|
||||
}
|
||||
}
|
||||
}
|
||||
flexItems := []layout.FlexItem{}
|
||||
@@ -355,6 +360,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
width,
|
||||
files,
|
||||
fileParts,
|
||||
agentParts,
|
||||
)
|
||||
content = lipgloss.PlaceHorizontal(
|
||||
m.width,
|
||||
@@ -433,6 +439,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
width,
|
||||
"",
|
||||
[]opencode.FilePart{},
|
||||
[]opencode.AgentPart{},
|
||||
toolCallParts...,
|
||||
)
|
||||
content = lipgloss.PlaceHorizontal(
|
||||
@@ -453,6 +460,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
width,
|
||||
"",
|
||||
[]opencode.FilePart{},
|
||||
[]opencode.AgentPart{},
|
||||
toolCallParts...,
|
||||
)
|
||||
content = lipgloss.PlaceHorizontal(
|
||||
|
||||
@@ -66,11 +66,16 @@ func (c *completionDialogComponent) Init() tea.Cmd {
|
||||
|
||||
func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
allItems := make([]completions.CompletionSuggestion, 0)
|
||||
// Collect results from all providers and preserve provider order
|
||||
type providerItems struct {
|
||||
idx int
|
||||
items []completions.CompletionSuggestion
|
||||
}
|
||||
|
||||
itemsByProvider := make([]providerItems, 0, len(c.providers))
|
||||
providersWithResults := 0
|
||||
|
||||
// Collect results from all providers
|
||||
for _, provider := range c.providers {
|
||||
for idx, provider := range c.providers {
|
||||
items, err := provider.GetChildEntries(query)
|
||||
if err != nil {
|
||||
slog.Error(
|
||||
@@ -84,33 +89,46 @@ func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
|
||||
}
|
||||
if len(items) > 0 {
|
||||
providersWithResults++
|
||||
allItems = append(allItems, items...)
|
||||
itemsByProvider = append(itemsByProvider, providerItems{idx: idx, items: items})
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a query, use fuzzy ranking to sort results
|
||||
if query != "" && providersWithResults > 1 {
|
||||
// If there's a query, fuzzy-rank within each provider, then concatenate by provider order
|
||||
if query != "" && providersWithResults > 0 {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.NewStyle().Background(t.BackgroundElement())
|
||||
// Create a slice of display values for fuzzy matching
|
||||
displayValues := make([]string, len(allItems))
|
||||
for i, item := range allItems {
|
||||
displayValues[i] = item.Display(baseStyle)
|
||||
|
||||
// Ensure stable provider order just in case
|
||||
sort.SliceStable(itemsByProvider, func(i, j int) bool { return itemsByProvider[i].idx < itemsByProvider[j].idx })
|
||||
|
||||
final := make([]completions.CompletionSuggestion, 0)
|
||||
for _, entry := range itemsByProvider {
|
||||
// Build display values for fuzzy matching within this provider
|
||||
displayValues := make([]string, len(entry.items))
|
||||
for i, item := range entry.items {
|
||||
displayValues[i] = item.Display(baseStyle)
|
||||
}
|
||||
|
||||
matches := fuzzy.RankFindFold(query, displayValues)
|
||||
sort.Sort(matches)
|
||||
|
||||
// Reorder items for this provider based on fuzzy ranking
|
||||
ranked := make([]completions.CompletionSuggestion, 0, len(matches))
|
||||
for _, m := range matches {
|
||||
ranked = append(ranked, entry.items[m.OriginalIndex])
|
||||
}
|
||||
final = append(final, ranked...)
|
||||
}
|
||||
|
||||
matches := fuzzy.RankFindFold(query, displayValues)
|
||||
sort.Sort(matches)
|
||||
|
||||
// Reorder items based on fuzzy ranking
|
||||
rankedItems := make([]completions.CompletionSuggestion, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
rankedItems = append(rankedItems, allItems[match.OriginalIndex])
|
||||
}
|
||||
|
||||
return rankedItems
|
||||
return final
|
||||
}
|
||||
|
||||
return allItems
|
||||
// No query or no results: just concatenate in provider order
|
||||
all := make([]completions.CompletionSuggestion, 0)
|
||||
for _, entry := range itemsByProvider {
|
||||
all = append(all, entry.items...)
|
||||
}
|
||||
return all
|
||||
}
|
||||
}
|
||||
func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
@@ -121,7 +121,7 @@ func (m *statusComponent) View() string {
|
||||
|
||||
var modeBackground compat.AdaptiveColor
|
||||
var modeForeground compat.AdaptiveColor
|
||||
switch m.app.ModeIndex {
|
||||
switch m.app.AgentIndex {
|
||||
case 0:
|
||||
modeBackground = t.BackgroundElement()
|
||||
modeForeground = t.TextMuted()
|
||||
@@ -148,31 +148,31 @@ func (m *statusComponent) View() string {
|
||||
modeForeground = t.BackgroundPanel()
|
||||
}
|
||||
|
||||
command := m.app.Commands[commands.SwitchModeCommand]
|
||||
command := m.app.Commands[commands.SwitchAgentCommand]
|
||||
kb := command.Keybindings[0]
|
||||
key := kb.Key
|
||||
if kb.RequiresLeader {
|
||||
key = m.app.Config.Keybinds.Leader + " " + kb.Key
|
||||
}
|
||||
|
||||
modeStyle := styles.NewStyle().Background(modeBackground).Foreground(modeForeground)
|
||||
modeNameStyle := modeStyle.Bold(true).Render
|
||||
modeDescStyle := modeStyle.Render
|
||||
mode := modeNameStyle(strings.ToUpper(m.app.Mode.Name)) + modeDescStyle(" MODE")
|
||||
mode = modeStyle.
|
||||
agentStyle := styles.NewStyle().Background(modeBackground).Foreground(modeForeground)
|
||||
agentNameStyle := agentStyle.Bold(true).Render
|
||||
agentDescStyle := agentStyle.Render
|
||||
agent := agentNameStyle(strings.ToUpper(m.app.Agent().Name)) + agentDescStyle(" AGENT")
|
||||
agent = agentStyle.
|
||||
Padding(0, 1).
|
||||
BorderLeft(true).
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
BorderForeground(modeBackground).
|
||||
BorderBackground(t.BackgroundPanel()).
|
||||
Render(mode)
|
||||
Render(agent)
|
||||
|
||||
faintStyle := styles.NewStyle().
|
||||
Faint(true).
|
||||
Background(t.BackgroundPanel()).
|
||||
Foreground(t.TextMuted())
|
||||
mode = faintStyle.Render(key+" ") + mode
|
||||
modeWidth := lipgloss.Width(mode)
|
||||
agent = faintStyle.Render(key+" ") + agent
|
||||
modeWidth := lipgloss.Width(agent)
|
||||
|
||||
availableWidth := m.width - logoWidth - modeWidth
|
||||
branchSuffix := ""
|
||||
@@ -206,7 +206,7 @@ func (m *statusComponent) View() string {
|
||||
View: logo + cwd,
|
||||
},
|
||||
layout.FlexItem{
|
||||
View: mode,
|
||||
View: agent,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ type Model struct {
|
||||
commandProvider completions.CompletionProvider
|
||||
fileProvider completions.CompletionProvider
|
||||
symbolsProvider completions.CompletionProvider
|
||||
agentsProvider completions.CompletionProvider
|
||||
showCompletionDialog bool
|
||||
leaderBinding *key.Binding
|
||||
toastManager *toast.ToastManager
|
||||
@@ -211,8 +212,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
a.editor = updated.(chat.EditorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
// Set both file and symbols providers for @ completion
|
||||
a.completions = dialog.NewCompletionDialogComponent("@", a.fileProvider, a.symbolsProvider)
|
||||
// Set file, symbols, and agents providers for @ completion
|
||||
a.completions = dialog.NewCompletionDialogComponent("@", a.agentsProvider, a.fileProvider, a.symbolsProvider)
|
||||
updated, cmd = a.completions.Update(msg)
|
||||
a.completions = updated.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, cmd)
|
||||
@@ -585,7 +586,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case app.ModelSelectedMsg:
|
||||
a.app.Provider = &msg.Provider
|
||||
a.app.Model = &msg.Model
|
||||
a.app.State.ModeModel[a.app.Mode.Name] = app.ModeModel{
|
||||
a.app.State.AgentModel[a.app.Agent().Name] = app.AgentModel{
|
||||
ProviderID: msg.Provider.ID,
|
||||
ModelID: msg.Model.ID,
|
||||
}
|
||||
@@ -951,12 +952,12 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
||||
case commands.AppHelpCommand:
|
||||
helpDialog := dialog.NewHelpDialog(a.app)
|
||||
a.modal = helpDialog
|
||||
case commands.SwitchModeCommand:
|
||||
updated, cmd := a.app.SwitchMode()
|
||||
case commands.SwitchAgentCommand:
|
||||
updated, cmd := a.app.SwitchAgent()
|
||||
a.app = updated
|
||||
cmds = append(cmds, cmd)
|
||||
case commands.SwitchModeReverseCommand:
|
||||
updated, cmd := a.app.SwitchModeReverse()
|
||||
case commands.SwitchAgentReverseCommand:
|
||||
updated, cmd := a.app.SwitchAgentReverse()
|
||||
a.app = updated
|
||||
cmds = append(cmds, cmd)
|
||||
case commands.EditorOpenCommand:
|
||||
@@ -1220,6 +1221,7 @@ func NewModel(app *app.App) tea.Model {
|
||||
commandProvider := completions.NewCommandCompletionProvider(app)
|
||||
fileProvider := completions.NewFileContextGroup(app)
|
||||
symbolsProvider := completions.NewSymbolsContextGroup(app)
|
||||
agentsProvider := completions.NewAgentsContextGroup(app)
|
||||
|
||||
messages := chat.NewMessagesComponent(app)
|
||||
editor := chat.NewEditorComponent(app)
|
||||
@@ -1240,6 +1242,7 @@ func NewModel(app *app.App) tea.Model {
|
||||
commandProvider: commandProvider,
|
||||
fileProvider: fileProvider,
|
||||
symbolsProvider: symbolsProvider,
|
||||
agentsProvider: agentsProvider,
|
||||
leaderBinding: leaderBinding,
|
||||
showCompletionDialog: false,
|
||||
toastManager: toast.NewToastManager(),
|
||||
|
||||
@@ -5,6 +5,52 @@ description: Configure and use specialized agents in opencode.
|
||||
|
||||
Agents are specialized AI assistants that can be configured for specific tasks and workflows. They allow you to create focused tools with custom prompts, models, and tool access.
|
||||
|
||||
:::tip
|
||||
Use the plan agent to analyze code and review suggestions without making any code changes.
|
||||
:::
|
||||
|
||||
You can switch between agents during a session or configure them in your config file.
|
||||
|
||||
---
|
||||
|
||||
## Agent Types
|
||||
|
||||
opencode has two types of agents:
|
||||
|
||||
### Primary Agents
|
||||
|
||||
Primary agents are the main assistants you interact with directly. You can cycle through them using the **Tab** key (or your configured `switch_agent` keybind). These agents handle your main conversation and can access all configured tools.
|
||||
|
||||
**Built-in Primary Agents:**
|
||||
|
||||
- **Build** - The default agent with all tools enabled. Standard for development work where you need full access to file operations and system commands.
|
||||
- **Plan** - A restricted agent for planning and analysis. Has `write`, `edit`, `patch`, and `bash` tools disabled by default. Useful for analyzing code and suggesting changes without making modifications.
|
||||
|
||||
### Subagents
|
||||
|
||||
Subagents are specialized assistants that primary agents can invoke for specific tasks. You can also manually invoke them by **@ mentioning** them in your messages (e.g., `@general help me search for this function`).
|
||||
|
||||
**Built-in Subagents:**
|
||||
|
||||
- **General** - General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. Use when searching for keywords or files and you're not confident you'll find the right match in the first few tries.
|
||||
|
||||
---
|
||||
|
||||
## Using Agents
|
||||
|
||||
### Switching Primary Agents
|
||||
|
||||
Use the **Tab** key to cycle through available primary agents during a session. You can also use your configured `switch_agent` keybind.
|
||||
|
||||
### Invoking Subagents
|
||||
|
||||
- **Automatic**: Primary agents will automatically use subagents for specialized tasks based on their descriptions
|
||||
- **Manual**: @ mention a subagent in your message: `@general search for authentication code`
|
||||
|
||||
See also: [Formatters](/docs/formatters) for information about code formatting configuration.
|
||||
|
||||
---
|
||||
|
||||
## Creating Agents
|
||||
|
||||
You can create new agents using the `opencode agent create` command. This interactive command will:
|
||||
@@ -21,39 +67,46 @@ opencode agent create
|
||||
|
||||
The command will guide you through the process and automatically generate a well-structured agent based on your requirements.
|
||||
|
||||
## Built-in Agents
|
||||
|
||||
opencode comes with a built-in `general` agent:
|
||||
|
||||
- **general** - General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. Use this when searching for keywords or files and you're not confident you'll find the right match in the first few tries.
|
||||
|
||||
## Configuration
|
||||
|
||||
Agents can be configured in your `opencode.json` config file or as markdown files.
|
||||
You can customize the built-in agents or create your own through configuration. Agents can be configured in two ways:
|
||||
|
||||
### JSON Configuration
|
||||
|
||||
Configure agents in your `opencode.json` config file:
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"agent": {
|
||||
"build": {
|
||||
"mode": "primary",
|
||||
"model": "anthropic/claude-sonnet-4-20250514",
|
||||
"prompt": "{file:./prompts/build.txt}",
|
||||
"tools": {
|
||||
"write": true,
|
||||
"edit": true,
|
||||
"bash": true
|
||||
}
|
||||
},
|
||||
"plan": {
|
||||
"mode": "primary",
|
||||
"model": "anthropic/claude-haiku-4-20250514",
|
||||
"tools": {
|
||||
"write": false,
|
||||
"edit": false,
|
||||
"bash": false
|
||||
}
|
||||
},
|
||||
"code-reviewer": {
|
||||
"description": "Reviews code for best practices and potential issues",
|
||||
"mode": "subagent",
|
||||
"model": "anthropic/claude-sonnet-4-20250514",
|
||||
"prompt": "You are a code reviewer. Focus on security, performance, and maintainability.",
|
||||
"tools": {
|
||||
"write": false,
|
||||
"edit": false
|
||||
}
|
||||
},
|
||||
"test-writer": {
|
||||
"description": "Specialized agent for writing comprehensive tests",
|
||||
"prompt": "You are a test writing specialist. Write thorough, maintainable tests.",
|
||||
"tools": {
|
||||
"bash": true,
|
||||
"read": true,
|
||||
"write": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,25 +119,113 @@ You can also define agents using markdown files. Place them in:
|
||||
- Global: `~/.config/opencode/agent/`
|
||||
- Project: `.opencode/agent/`
|
||||
|
||||
```markdown title="~/.config/opencode/agent/code-reviewer.md"
|
||||
```markdown title="~/.config/opencode/agent/review.md"
|
||||
---
|
||||
description: Reviews code for best practices and potential issues
|
||||
description: Reviews code for quality and best practices
|
||||
mode: subagent
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
temperature: 0.1
|
||||
tools:
|
||||
write: false
|
||||
edit: false
|
||||
bash: false
|
||||
---
|
||||
|
||||
You are a code reviewer with expertise in security, performance, and maintainability.
|
||||
You are in code review mode. Focus on:
|
||||
|
||||
Focus on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs and edge cases
|
||||
- Performance implications
|
||||
- Security considerations
|
||||
|
||||
- Security vulnerabilities
|
||||
- Performance bottlenecks
|
||||
- Code maintainability
|
||||
- Best practices adherence
|
||||
Provide constructive feedback without making direct changes.
|
||||
```
|
||||
|
||||
The markdown file name becomes the agent name (e.g., `review.md` creates a `review` agent).
|
||||
|
||||
Let's look at these configuration options in detail.
|
||||
|
||||
---
|
||||
|
||||
### Model
|
||||
|
||||
Use the `model` config to override the default model for this agent. Useful for using different models optimized for different tasks. For example, a faster model for planning, a more capable model for implementation.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"agent": {
|
||||
"plan": {
|
||||
"model": "anthropic/claude-haiku-4-20250514"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Temperature
|
||||
|
||||
Control the randomness and creativity of the AI's responses with the `temperature` config. Lower values make responses more focused and deterministic, while higher values increase creativity and variability.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"agent": {
|
||||
"plan": {
|
||||
"temperature": 0.1
|
||||
},
|
||||
"creative": {
|
||||
"temperature": 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Temperature values typically range from 0.0 to 1.0:
|
||||
|
||||
- **0.0-0.2**: Very focused and deterministic responses, ideal for code analysis and planning
|
||||
- **0.3-0.5**: Balanced responses with some creativity, good for general development tasks
|
||||
- **0.6-1.0**: More creative and varied responses, useful for brainstorming and exploration
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"agent": {
|
||||
"analyze": {
|
||||
"temperature": 0.1,
|
||||
"prompt": "{file:./prompts/analysis.txt}"
|
||||
},
|
||||
"build": {
|
||||
"temperature": 0.3
|
||||
},
|
||||
"brainstorm": {
|
||||
"temperature": 0.7,
|
||||
"prompt": "{file:./prompts/creative.txt}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If no temperature is specified, opencode uses model-specific defaults (typically 0 for most models, 0.55 for Qwen models).
|
||||
|
||||
---
|
||||
|
||||
### Prompt
|
||||
|
||||
Specify a custom system prompt file for this agent with the `prompt` config. The prompt file should contain instructions specific to the agent's purpose.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"agent": {
|
||||
"review": {
|
||||
"prompt": "{file:./prompts/code-review.txt}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This path is relative to where the config file is located. So this works for both the global opencode config and the project specific config.
|
||||
|
||||
---
|
||||
|
||||
## Agent Properties
|
||||
|
||||
### Required
|
||||
@@ -96,39 +237,66 @@ Focus on:
|
||||
- **model** - Specific model to use (defaults to your configured model)
|
||||
- **prompt** - Custom system prompt for the agent
|
||||
- **tools** - Object specifying which tools the agent can access (true/false for each tool)
|
||||
- **temperature** - Control response randomness (0.0-1.0)
|
||||
- **mode** - Agent type: `"primary"` (can be cycled with Tab), `"subagent"` (invoked by @ mention), or `"all"` (both)
|
||||
- **disable** - Set to true to disable the agent
|
||||
|
||||
## Tool Access
|
||||
### Tools
|
||||
|
||||
By default, agents inherit the same tool access as the main assistant. You can restrict or enable specific tools:
|
||||
Control which tools are available in this agent with the `tools` config. You can enable or disable specific tools by setting them to `true` or `false`.
|
||||
|
||||
```json
|
||||
{
|
||||
"agent": {
|
||||
"readonly-agent": {
|
||||
"description": "Read-only agent for analysis",
|
||||
"readonly": {
|
||||
"tools": {
|
||||
"write": false,
|
||||
"edit": false,
|
||||
"bash": false
|
||||
"bash": false,
|
||||
"read": true,
|
||||
"grep": true,
|
||||
"glob": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Common tools you might want to control:
|
||||
If no tools are specified, all tools are enabled by default.
|
||||
|
||||
- `write` - Create new files
|
||||
- `edit` - Modify existing files
|
||||
- `bash` - Execute shell commands
|
||||
- `read` - Read files
|
||||
- `glob` - Search for files
|
||||
- `grep` - Search file contents
|
||||
---
|
||||
|
||||
## Using Agents
|
||||
#### Available tools
|
||||
|
||||
Agents are automatically available through the Task tool when configured. The main assistant will use them for specialized tasks based on their descriptions.
|
||||
Here are all the tools can be controlled through the agent config.
|
||||
|
||||
| Tool | Description |
|
||||
| ----------- | ----------------------- |
|
||||
| `bash` | Execute shell commands |
|
||||
| `edit` | Modify existing files |
|
||||
| `write` | Create new files |
|
||||
| `read` | Read file contents |
|
||||
| `grep` | Search file contents |
|
||||
| `glob` | Find files by pattern |
|
||||
| `list` | List directory contents |
|
||||
| `patch` | Apply patches to files |
|
||||
| `todowrite` | Manage todo lists |
|
||||
| `todoread` | Read todo lists |
|
||||
| `webfetch` | Fetch web content |
|
||||
|
||||
---
|
||||
|
||||
## Tool Access
|
||||
|
||||
By default, agents inherit the same tool access as the main assistant. You can restrict or enable specific tools as shown in the Tools section above.
|
||||
|
||||
## Agent Modes
|
||||
|
||||
The `mode` property determines how an agent can be used:
|
||||
|
||||
- **`"primary"`** - Can be cycled through with Tab key as your main assistant
|
||||
- **`"subagent"`** - Can be invoked by @ mentioning or automatically by primary agents
|
||||
- **`"all"`** - Can be used both as primary and subagent (default for custom agents)
|
||||
|
||||
## Best Practices
|
||||
|
||||
@@ -138,6 +306,97 @@ Agents are automatically available through the Task tool when configured. The ma
|
||||
4. **Consistent naming** - Use descriptive, consistent names for your agents
|
||||
5. **Project-specific agents** - Use `.opencode/agent/` for project-specific workflows
|
||||
|
||||
## Custom Agents
|
||||
|
||||
You can create your own custom agents by adding them to the configuration. Here are examples using both approaches:
|
||||
|
||||
### Using JSON configuration
|
||||
|
||||
```json title="opencode.json" {4-14}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"agent": {
|
||||
"docs": {
|
||||
"prompt": "{file:./prompts/documentation.txt}",
|
||||
"tools": {
|
||||
"write": true,
|
||||
"edit": true,
|
||||
"bash": false,
|
||||
"read": true,
|
||||
"grep": true,
|
||||
"glob": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using markdown files
|
||||
|
||||
Create agent files in `.opencode/agent/` for project-specific agents or `~/.config/opencode/agent/` for global agents:
|
||||
|
||||
```markdown title=".opencode/agent/debug.md"
|
||||
---
|
||||
temperature: 0.1
|
||||
tools:
|
||||
bash: true
|
||||
read: true
|
||||
grep: true
|
||||
write: false
|
||||
edit: false
|
||||
---
|
||||
|
||||
You are in debug mode. Your primary goal is to help investigate and diagnose issues.
|
||||
|
||||
Focus on:
|
||||
|
||||
- Understanding the problem through careful analysis
|
||||
- Using bash commands to inspect system state
|
||||
- Reading relevant files and logs
|
||||
- Searching for patterns and anomalies
|
||||
- Providing clear explanations of findings
|
||||
|
||||
Do not make any changes to files. Only investigate and report.
|
||||
```
|
||||
|
||||
```markdown title="~/.config/opencode/agent/refactor.md"
|
||||
---
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
temperature: 0.2
|
||||
tools:
|
||||
edit: true
|
||||
read: true
|
||||
grep: true
|
||||
glob: true
|
||||
---
|
||||
|
||||
You are in refactoring mode. Focus on improving code quality without changing functionality.
|
||||
|
||||
Priorities:
|
||||
|
||||
- Improve code readability and maintainability
|
||||
- Apply consistent naming conventions
|
||||
- Reduce code duplication
|
||||
- Optimize performance where appropriate
|
||||
- Ensure all tests continue to pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Use cases
|
||||
|
||||
Here are some common use cases for different agents.
|
||||
|
||||
- **Build agent**: Full development work with all tools enabled
|
||||
- **Plan agent**: Analysis and planning without making changes
|
||||
- **Review agent**: Code review with read-only access plus documentation tools
|
||||
- **Debug agent**: Focused on investigation with bash and read tools enabled
|
||||
- **Docs agent**: Documentation writing with file operations but no system commands
|
||||
|
||||
You might also find different models are good for different use cases.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Documentation Agent
|
||||
@@ -145,6 +404,7 @@ Agents are automatically available through the Task tool when configured. The ma
|
||||
```markdown title="~/.config/opencode/agent/docs-writer.md"
|
||||
---
|
||||
description: Writes and maintains project documentation
|
||||
mode: subagent
|
||||
tools:
|
||||
bash: false
|
||||
---
|
||||
@@ -164,6 +424,7 @@ Focus on:
|
||||
```markdown title="~/.config/opencode/agent/security-auditor.md"
|
||||
---
|
||||
description: Performs security audits and identifies vulnerabilities
|
||||
mode: subagent
|
||||
tools:
|
||||
write: false
|
||||
edit: false
|
||||
|
||||
@@ -11,7 +11,7 @@ opencode has a list of keybinds that you can customize through the opencode conf
|
||||
"keybinds": {
|
||||
"leader": "ctrl+x",
|
||||
"app_help": "<leader>h",
|
||||
"switch_mode": "tab",
|
||||
"switch_agent": "tab",
|
||||
|
||||
"editor_open": "<leader>e",
|
||||
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
---
|
||||
title: Modes
|
||||
description: Different modes for different use cases.
|
||||
---
|
||||
|
||||
Modes in opencode allow you to customize the behavior, tools, and prompts for different use cases.
|
||||
|
||||
It comes with two built-in modes: **build** and **plan**. You can customize
|
||||
these or configure your own through the opencode config.
|
||||
|
||||
:::tip
|
||||
Use the plan mode to analyze code and review suggestions without making any code
|
||||
changes.
|
||||
:::
|
||||
|
||||
You can switch between modes during a session or configure them in your config file.
|
||||
|
||||
---
|
||||
|
||||
## Built-in
|
||||
|
||||
opencode comes with two built-in modes.
|
||||
|
||||
---
|
||||
|
||||
### Build
|
||||
|
||||
Build is the **default** mode with all tools enabled. This is the standard mode for development work where you need full access to file operations and system commands.
|
||||
|
||||
---
|
||||
|
||||
### Plan
|
||||
|
||||
A restricted mode designed for planning and analysis. In plan mode, the following tools are disabled by default:
|
||||
|
||||
- `write` - Cannot create new files
|
||||
- `edit` - Cannot modify existing files
|
||||
- `patch` - Cannot apply patches
|
||||
- `bash` - Cannot execute shell commands
|
||||
|
||||
This mode is useful when you want the AI to analyze code, suggest changes, or create plans without making any actual modifications to your codebase.
|
||||
|
||||
---
|
||||
|
||||
## Switching
|
||||
|
||||
You can switch between modes during a session using the _Tab_ key. Or your configured `switch_mode` keybind.
|
||||
|
||||
See also: [Formatters](/docs/formatters) for information about code formatting configuration.
|
||||
|
||||
---
|
||||
|
||||
## Configure
|
||||
|
||||
You can customize the built-in modes or create your own through configuration. Modes can be configured in two ways:
|
||||
|
||||
### JSON Configuration
|
||||
|
||||
Configure modes in your `opencode.json` config file:
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mode": {
|
||||
"build": {
|
||||
"model": "anthropic/claude-sonnet-4-20250514",
|
||||
"prompt": "{file:./prompts/build.txt}",
|
||||
"tools": {
|
||||
"write": true,
|
||||
"edit": true,
|
||||
"bash": true
|
||||
}
|
||||
},
|
||||
"plan": {
|
||||
"model": "anthropic/claude-haiku-4-20250514",
|
||||
"tools": {
|
||||
"write": false,
|
||||
"edit": false,
|
||||
"bash": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Markdown Configuration
|
||||
|
||||
You can also define modes using markdown files. Place them in:
|
||||
|
||||
- Global: `~/.config/opencode/mode/`
|
||||
- Project: `.opencode/mode/`
|
||||
|
||||
```markdown title="~/.config/opencode/mode/review.md"
|
||||
---
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
temperature: 0.1
|
||||
tools:
|
||||
write: false
|
||||
edit: false
|
||||
bash: false
|
||||
---
|
||||
|
||||
You are in code review mode. Focus on:
|
||||
|
||||
- Code quality and best practices
|
||||
- Potential bugs and edge cases
|
||||
- Performance implications
|
||||
- Security considerations
|
||||
|
||||
Provide constructive feedback without making direct changes.
|
||||
```
|
||||
|
||||
The markdown file name becomes the mode name (e.g., `review.md` creates a `review` mode).
|
||||
|
||||
Let's look at these configuration options in detail.
|
||||
|
||||
---
|
||||
|
||||
### Model
|
||||
|
||||
Use the `model` config to override the default model for this mode. Useful for using different models optimized for different tasks. For example, a faster model for planning, a more capable model for implementation.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"mode": {
|
||||
"plan": {
|
||||
"model": "anthropic/claude-haiku-4-20250514"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Temperature
|
||||
|
||||
Control the randomness and creativity of the AI's responses with the `temperature` config. Lower values make responses more focused and deterministic, while higher values increase creativity and variability.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"mode": {
|
||||
"plan": {
|
||||
"temperature": 0.1
|
||||
},
|
||||
"creative": {
|
||||
"temperature": 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Temperature values typically range from 0.0 to 1.0:
|
||||
|
||||
- **0.0-0.2**: Very focused and deterministic responses, ideal for code analysis and planning
|
||||
- **0.3-0.5**: Balanced responses with some creativity, good for general development tasks
|
||||
- **0.6-1.0**: More creative and varied responses, useful for brainstorming and exploration
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"mode": {
|
||||
"analyze": {
|
||||
"temperature": 0.1,
|
||||
"prompt": "{file:./prompts/analysis.txt}"
|
||||
},
|
||||
"build": {
|
||||
"temperature": 0.3
|
||||
},
|
||||
"brainstorm": {
|
||||
"temperature": 0.7,
|
||||
"prompt": "{file:./prompts/creative.txt}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If no temperature is specified, opencode uses model-specific defaults (typically 0 for most models, 0.55 for Qwen models).
|
||||
|
||||
---
|
||||
|
||||
### Prompt
|
||||
|
||||
Specify a custom system prompt file for this mode with the `prompt` config. The prompt file should contain instructions specific to the mode's purpose.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"mode": {
|
||||
"review": {
|
||||
"prompt": "{file:./prompts/code-review.txt}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This path is relative to where the config file is located. So this works for
|
||||
both the global opencode config and the project specific config.
|
||||
|
||||
---
|
||||
|
||||
### Tools
|
||||
|
||||
Control which tools are available in this mode with the `tools` config. You can enable or disable specific tools by setting them to `true` or `false`.
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": {
|
||||
"readonly": {
|
||||
"tools": {
|
||||
"write": false,
|
||||
"edit": false,
|
||||
"bash": false,
|
||||
"read": true,
|
||||
"grep": true,
|
||||
"glob": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If no tools are specified, all tools are enabled by default.
|
||||
|
||||
---
|
||||
|
||||
#### Available tools
|
||||
|
||||
Here are all the tools can be controlled through the mode config.
|
||||
|
||||
| Tool | Description |
|
||||
| ----------- | ----------------------- |
|
||||
| `bash` | Execute shell commands |
|
||||
| `edit` | Modify existing files |
|
||||
| `write` | Create new files |
|
||||
| `read` | Read file contents |
|
||||
| `grep` | Search file contents |
|
||||
| `glob` | Find files by pattern |
|
||||
| `list` | List directory contents |
|
||||
| `patch` | Apply patches to files |
|
||||
| `todowrite` | Manage todo lists |
|
||||
| `todoread` | Read todo lists |
|
||||
| `webfetch` | Fetch web content |
|
||||
|
||||
---
|
||||
|
||||
## Custom modes
|
||||
|
||||
You can create your own custom modes by adding them to the configuration. Here are examples using both approaches:
|
||||
|
||||
### Using JSON configuration
|
||||
|
||||
```json title="opencode.json" {4-14}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mode": {
|
||||
"docs": {
|
||||
"prompt": "{file:./prompts/documentation.txt}",
|
||||
"tools": {
|
||||
"write": true,
|
||||
"edit": true,
|
||||
"bash": false,
|
||||
"read": true,
|
||||
"grep": true,
|
||||
"glob": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using markdown files
|
||||
|
||||
Create mode files in `.opencode/mode/` for project-specific modes or `~/.config/opencode/mode/` for global modes:
|
||||
|
||||
```markdown title=".opencode/mode/debug.md"
|
||||
---
|
||||
temperature: 0.1
|
||||
tools:
|
||||
bash: true
|
||||
read: true
|
||||
grep: true
|
||||
write: false
|
||||
edit: false
|
||||
---
|
||||
|
||||
You are in debug mode. Your primary goal is to help investigate and diagnose issues.
|
||||
|
||||
Focus on:
|
||||
|
||||
- Understanding the problem through careful analysis
|
||||
- Using bash commands to inspect system state
|
||||
- Reading relevant files and logs
|
||||
- Searching for patterns and anomalies
|
||||
- Providing clear explanations of findings
|
||||
|
||||
Do not make any changes to files. Only investigate and report.
|
||||
```
|
||||
|
||||
```markdown title="~/.config/opencode/mode/refactor.md"
|
||||
---
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
temperature: 0.2
|
||||
tools:
|
||||
edit: true
|
||||
read: true
|
||||
grep: true
|
||||
glob: true
|
||||
---
|
||||
|
||||
You are in refactoring mode. Focus on improving code quality without changing functionality.
|
||||
|
||||
Priorities:
|
||||
|
||||
- Improve code readability and maintainability
|
||||
- Apply consistent naming conventions
|
||||
- Reduce code duplication
|
||||
- Optimize performance where appropriate
|
||||
- Ensure all tests continue to pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Use cases
|
||||
|
||||
Here are some common use cases for different modes.
|
||||
|
||||
- **Build mode**: Full development work with all tools enabled
|
||||
- **Plan mode**: Analysis and planning without making changes
|
||||
- **Review mode**: Code review with read-only access plus documentation tools
|
||||
- **Debug mode**: Focused on investigation with bash and read tools enabled
|
||||
- **Docs mode**: Documentation writing with file operations but no system commands
|
||||
|
||||
You might also find different models are good for different use cases.
|
||||
Reference in New Issue
Block a user