Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
3 changes: 3 additions & 0 deletions packages/opencode/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,9 @@ export namespace ACP {
const defaultAgentName = await AgentModule.defaultAgent()
const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id

// Persist the default mode so prompt() uses it immediately
this.sessionManager.setMode(sessionId, currentModeId)

const mcpServers: Record<string, Config.Mcp> = {}
for (const server of params.mcpServers) {
if ("type" in server) {
Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,9 @@ export namespace Agent {
}

export async function defaultAgent() {
return state().then((x) => Object.keys(x)[0])
const agents = await list()
const primaryVisible = agents.find((a) => a.mode !== "subagent" && a.hidden !== true)
return primaryVisible?.name || "build"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should prolly throw an error if no primary agent is available... Thoughts?

Copy link
Copy Markdown
Contributor Author

@assagman assagman Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm currently thinking about it. Maybe you're right.

TUI throws unhandled exception here: https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/cli/cmd/tui/context/local.tsx#L41-L41

(when no primary visible agent found)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @rekram1-node , I've updated the logic as throwing errors for invalid cases:

  1. cfg.default_agent is set:
    a. cfg.default_agent is not found in state
    b. cfg.default_agent is a subagent
    c. cfg.default_agent is hidden
  2. cfg.default_agent is not set and all of them either subagent or hidden -> no primary visible agent found

}

export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) {
Expand Down
132 changes: 132 additions & 0 deletions packages/opencode/test/agent/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,3 +512,135 @@ test("explicit Truncate.DIR deny is respected", async () => {
},
})
})

test("defaultAgent returns build when no default_agent config", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.defaultAgent()
expect(agent).toBe("build")
},
})
})

test("defaultAgent respects default_agent config set to plan", async () => {
await using tmp = await tmpdir({
config: {
default_agent: "plan",
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.defaultAgent()
expect(agent).toBe("plan")
},
})
})

test("defaultAgent respects default_agent config set to custom agent with mode all", async () => {
await using tmp = await tmpdir({
config: {
default_agent: "my_custom",
agent: {
my_custom: {
description: "My custom agent",
},
},
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.defaultAgent()
expect(agent).toBe("my_custom")
},
})
})

test("defaultAgent falls back when default_agent points to subagent", async () => {
await using tmp = await tmpdir({
config: {
default_agent: "explore",
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.defaultAgent()
// explore is a subagent, so it should fall back to first primary-capable agent
expect(agent).not.toBe("explore")
expect(agent).toBe("build")
},
})
})

test("defaultAgent falls back when default_agent points to hidden agent", async () => {
await using tmp = await tmpdir({
config: {
default_agent: "compaction",
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.defaultAgent()
// compaction is hidden, so it should fall back to first primary-capable agent
expect(agent).not.toBe("compaction")
expect(agent).toBe("build")
},
})
})

test("defaultAgent falls back when default_agent points to non-existent agent", async () => {
await using tmp = await tmpdir({
config: {
default_agent: "does_not_exist",
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.defaultAgent()
expect(agent).toBe("build")
},
})
})

test("defaultAgent returns plan when build is disabled and default_agent not set", async () => {
await using tmp = await tmpdir({
config: {
agent: {
build: { disable: true },
},
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.defaultAgent()
// build is disabled, so it should return plan (next primary agent)
expect(agent).toBe("plan")
},
})
})

test("defaultAgent returns first primary-capable agent when all natives are disabled", async () => {
await using tmp = await tmpdir({
config: {
agent: {
build: { disable: true },
plan: { disable: true },
},
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.defaultAgent()
// build and plan are disabled, so it should return general
expect(agent).toBe("build")
},
})
})