Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/desktop/src/context/local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,20 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({

const agent = (() => {
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent"))
const defaultAgent = createMemo(() => list().find((x) => x.default)!)

const [store, setStore] = createStore<{
current: string
}>({
current: list()[0].name,
current: defaultAgent().name,
})
return {
list,
current() {
return list().find((x) => x.name === store.current)!
},
set(name: string | undefined) {
setStore("current", name ?? list()[0].name)
setStore("current", name ?? defaultAgent().name)
},
move(direction: 1 | -1) {
let next = list().findIndex((x) => x.name === store.current) + direction
Expand Down
19 changes: 9 additions & 10 deletions packages/opencode/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,15 +697,13 @@ export namespace ACP {
description: "compact the session",
})

const availableModes = agents
.filter((agent) => agent.mode !== "subagent")
.map((agent) => ({
id: agent.name,
name: agent.name,
description: agent.description,
}))

const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
const availableAgents = agents.filter((agent) => agent.mode !== "subagent")
const availableModes = availableAgents.map((agent) => ({
id: agent.name,
name: agent.name,
description: agent.description,
}))
const currentModeId = availableAgents.find((agent) => agent.default)?.name!

const mcpServers: Record<string, Config.Mcp> = {}
for (const server of params.mcpServers) {
Expand Down Expand Up @@ -801,13 +799,14 @@ export namespace ACP {
const sessionID = params.sessionId
const session = this.sessionManager.get(sessionID)
const directory = session.cwd
const globalCfg = await Config.get()

const current = session.model
const model = current ?? (await defaultModel(this.config, directory))
if (!current) {
this.sessionManager.setModel(session.id, model)
}
const agent = session.modeId ?? "build"
const agent = session.modeId ?? globalCfg.default_agent!

const parts: Array<
{ type: "text"; text: string } | { type: "file"; url: string; filename: string; mime: string }
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export namespace Agent {
name: z.string(),
description: z.string().optional(),
mode: z.enum(["subagent", "primary", "all"]),
default: z.boolean().optional(),
builtIn: z.boolean(),
topP: z.number().optional(),
temperature: z.number().optional(),
Expand Down Expand Up @@ -224,6 +225,8 @@ export namespace Agent {
if (permission ?? cfg.permission) {
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
}

if (key === cfg.default_agent) result[key].default = true
}
return result
})
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ const AgentListCommand = cmd({
})

for (const agent of sortedAgents) {
process.stdout.write(`${agent.name} (${agent.mode})${EOL}`)
process.stdout.write(`${agent.name} (${agent.mode}${agent.default ? ", default" : ""})${EOL}`)
}
},
})
Expand Down
6 changes: 4 additions & 2 deletions packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { select } from "@clack/prompts"
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2"
import { Server } from "../../server/server"
import { Provider } from "../../provider/provider"
import { Config } from "@/config/config"

const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
Expand Down Expand Up @@ -221,10 +222,11 @@ export const RunCommand = cmd({
}
})()

const cfg = await Config.get()
if (args.command) {
await sdk.session.command({
sessionID,
agent: args.agent || "build",
agent: args.agent || cfg.default_agent!,
model: args.model,
command: args.command,
arguments: message,
Expand All @@ -233,7 +235,7 @@ export const RunCommand = cmd({
const modelParam = args.model ? Provider.parseModel(args.model) : undefined
await sdk.session.prompt({
sessionID,
agent: args.agent || "build",
agent: args.agent || cfg.default_agent!,
model: modelParam,
parts: [...fileParts, { type: "text", text: message }],
})
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/tui/context/local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const [agentStore, setAgentStore] = createStore<{
current: string
}>({
current: agents()[0].name,
current: agents().find((x) => x.default)!.name,
})
const { theme } = useTheme()
const colors = createMemo(() => [
Expand Down
33 changes: 28 additions & 5 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@ export namespace Config {
return merged
}

function firstPrimaryAgent(agents: Record<string, Agent>): string {
const build = agents["build"]
if (build && !build.disable) return "build"

for (const [name, config] of Object.entries(agents)) {
if (config.mode !== "subagent" && !config.disable) {
return name
}
}

throw new InvalidError({
path: path.join(Instance.directory, "opencode.jsonc"),
message:
"No available primary agent found. Please ensure at least one primary agent is configured and not disabled.",
})
}

export const state = Instance.state(async () => {
const auth = await Auth.all()
let result = await global()
Expand Down Expand Up @@ -127,13 +144,18 @@ export namespace Config {
result.share = "auto"
}

// Handle migration from autoshare to share field
if (result.autoshare === true && !result.share) {
result.share = "auto"
}

if (!result.keybinds) result.keybinds = Info.shape.keybinds.parse({})

if (
!result.default_agent || // not configured
!(result.default_agent in result.agent) || // not exist
result.agent[result.default_agent]?.disable || // disabled
result.agent[result.default_agent]?.mode === "subagent" // not primary
) {
log.warn(`default agent not available, fallback to the first primary agent`)
result.default_agent = firstPrimaryAgent(result.agent)
}

return {
config: result,
directories,
Expand Down Expand Up @@ -666,6 +688,7 @@ export namespace Config {
.catchall(Agent)
.optional()
.describe("@deprecated Use `agent` field instead."),
default_agent: z.string().optional().describe("Default agent to use when no specific agent is specified"),
agent: z
.object({
plan: Agent.optional(),
Expand Down
6 changes: 4 additions & 2 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1041,11 +1041,13 @@ export namespace Server {
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
const msgs = await Session.messages({ sessionID })
let currentAgent = "build"
const cfg = await Config.get()

let currentAgent = cfg.default_agent!
for (let i = msgs.length - 1; i >= 0; i--) {
const info = msgs[i].info
if (info.role === "user") {
currentAgent = info.agent || "build"
currentAgent = info.agent
break
}
}
Expand Down
6 changes: 4 additions & 2 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -845,7 +845,8 @@ export namespace SessionPrompt {
}

async function createUserMessage(input: PromptInput) {
const agent = await Agent.get(input.agent ?? "build")
const cfg = await Config.get()
const agent = await Agent.get(input.agent ?? cfg.default_agent!)
const info: MessageV2.Info = {
id: input.messageID ?? Identifier.ascending("message"),
role: "user",
Expand Down Expand Up @@ -1375,7 +1376,8 @@ export namespace SessionPrompt {
export async function command(input: CommandInput) {
log.info("command", input)
const command = await Command.get(input.command)
const agentName = command.agent ?? input.agent ?? "build"
const cfg = await Config.get()
const agentName = command.agent ?? input.agent ?? cfg.default_agent!

const raw = input.arguments.match(argsRegex) ?? []
const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))
Expand Down
85 changes: 85 additions & 0 deletions packages/opencode/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,3 +501,88 @@ test("deduplicates duplicate plugins from global and local configs", async () =>
},
})
})

test("uses specified default_agent when provided", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
default_agent: "custom",
agent: {
build: {
model: "test/model",
description: "build agent",
},
custom: {
model: "test/model",
description: "custom agent",
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.default_agent).toBe("custom")
},
})
})

test("falls back when default_agent is disabled", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
default_agent: "build",
agent: {
build: {
model: "test/model",
description: "build agent",
disable: true,
},
custom: {
model: "test/model",
description: "custom agent",
mode: "primary",
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.default_agent).toBe("custom")
},
})
})

test("falls back when configured default_agent does not exist", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
default_agent: "nonexistent-agent",
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.default_agent).toBe("build")
},
})
})
5 changes: 5 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1404,6 +1404,10 @@ export type Config = {
plan?: AgentConfig
[key: string]: AgentConfig | undefined
}
/**
* Default agent to use when no specific agent is specified
*/
default_agent?: string
/**
* Agent configuration, see https://opencode.ai/docs/agent
*/
Expand Down Expand Up @@ -1734,6 +1738,7 @@ export type Agent = {
name: string
description?: string
mode: "subagent" | "primary" | "all"
default?: boolean
builtIn: boolean
topP?: number
temperature?: number
Expand Down