From cd9645d5906e2b5fad3fc84e89dae41f95a84a61 Mon Sep 17 00:00:00 2001 From: LCubero Date: Mon, 27 Apr 2026 17:26:55 -0600 Subject: [PATCH] feat(agent): add order field for configurable agent cycling order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an optional `order` field (PositiveInt) to agent config so users and plugins can control Tab cycling order. Sorting in Agent.list() becomes: 1. Default agent first (unchanged) 2. By order ascending (new) 3. By name ascending (unchanged fallback) Fully backward-compatible — agents without `order` behave as before. Closes #7372 --- packages/opencode/src/agent/agent.ts | 3 + packages/opencode/src/config/agent.ts | 5 ++ packages/opencode/test/agent/agent.test.ts | 76 +++++++++++++++++++++- 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 5e839ead5c88..0b1f8adc0603 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -33,6 +33,7 @@ export const Info = Schema.Struct({ hidden: Schema.optional(Schema.Boolean), topP: Schema.optional(Schema.Number), temperature: Schema.optional(Schema.Number), + order: Schema.optional(Schema.Number), color: Schema.optional(Schema.String), permission: Permission.Ruleset, model: Schema.optional( @@ -256,6 +257,7 @@ export const layer = Layer.effect( item.color = value.color ?? item.color item.hidden = value.hidden ?? item.hidden item.name = value.name ?? item.name + item.order = value.order ?? item.order item.steps = value.steps ?? item.steps item.options = mergeDeep(item.options, value.options ?? {}) item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {})) @@ -288,6 +290,7 @@ export const layer = Layer.effect( values(), sortBy( [(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"], + [(x) => x.order ?? Infinity, "asc"], [(x) => x.name, "asc"], ), ) diff --git a/packages/opencode/src/config/agent.ts b/packages/opencode/src/config/agent.ts index e673edbad4c9..66414e341a67 100644 --- a/packages/opencode/src/config/agent.ts +++ b/packages/opencode/src/config/agent.ts @@ -35,6 +35,10 @@ const AgentSchema = Schema.StructWithRest( disable: Schema.optional(Schema.Boolean), description: Schema.optional(Schema.String).annotate({ description: "Description of when to use the agent" }), mode: Schema.optional(Schema.Literals(["subagent", "primary", "all"])), + order: Schema.optional(PositiveInt).annotate({ + description: + "Sorting order for agent cycling (Tab). Lower values appear first. Agents without order are sorted alphabetically after ordered agents.", + }), hidden: Schema.optional(Schema.Boolean).annotate({ description: "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)", }), @@ -60,6 +64,7 @@ const KNOWN_KEYS = new Set([ "temperature", "top_p", "mode", + "order", "hidden", "color", "steps", diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index ec384709da10..f98a8c53cf8a 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -391,7 +391,7 @@ test("multiple custom agents can be defined", async () => { }) }) -test("Agent.list keeps the default agent first and sorts the rest by name", async () => { +test("Agent.list keeps the default agent first and sorts the rest by name when no order is set", async () => { await using tmp = await tmpdir({ config: { default_agent: "plan", @@ -417,6 +417,80 @@ test("Agent.list keeps the default agent first and sorts the rest by name", asyn }) }) +test("Agent.list respects order field for sorting", async () => { + await using tmp = await tmpdir({ + config: { + default_agent: "build", + agent: { + build: { + description: "Build agent", + mode: "primary", + order: 2, + }, + alpha: { + description: "Alpha", + mode: "primary", + order: 1, + }, + zebra: { + description: "Zebra", + mode: "primary", + order: 3, + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const names = (await load(tmp.path, (svc) => svc.list())).map((a) => a.name) + // Default agent ("build") always first regardless of order + expect(names[0]).toBe("build") + // Remaining sorted by order ascending: alpha (1) → zebra (3) + expect(names[1]).toBe("alpha") + expect(names[2]).toBe("zebra") + }, + }) +}) + +test("Agent.list sorts agents without order after ordered agents", async () => { + await using tmp = await tmpdir({ + config: { + agent: { + zebra: { + description: "Zebra", + mode: "primary", + }, + alpha: { + description: "Alpha", + mode: "primary", + order: 1, + }, + beta: { + description: "Beta", + mode: "primary", + order: 2, + }, + gamma: { + description: "Gamma", + mode: "primary", + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const names = (await load(tmp.path, (svc) => svc.list())).map((a) => a.name) + // Sorted: by order (alpha:1, beta:2), then alphabetical (gamma, zebra) + expect(names[0]).toBe("alpha") + expect(names[1]).toBe("beta") + expect(names[2]).toBe("gamma") + expect(names[3]).toBe("zebra") + }, + }) +}) + test("Agent.get returns undefined for non-existent agent", async () => { await using tmp = await tmpdir() await Instance.provide({