From 6355ca1ab8f2c9a95c9d67997bbcb87b8e736e7d Mon Sep 17 00:00:00 2001
From: CasualDeveloper <10153929+CasualDeveloper@users.noreply.github.com>
Date: Fri, 13 Feb 2026 23:16:41 +0800
Subject: [PATCH] feat: add agent default variant handling in TUI and desktop
- Resolve variants via selected/configured/current helper flow in both clients
- Allow agent-configured variants across models when the active model supports that variant key
- Update TUI hydration to avoid promoting configured defaults into explicit overrides
- Add and adjust variant tests for app, TUI, and agent config
- Add a triage agent variant example
Closes #7138
---
.opencode/agent/triage.md | 1 +
packages/app/src/context/local.tsx | 3 ++
.../app/src/context/model-variant.test.ts | 29 +++++++++----
packages/app/src/context/model-variant.ts | 8 ----
.../cli/cmd/tui/component/prompt/index.tsx | 19 +++++++--
.../src/cli/cmd/tui/context/local.tsx | 42 +++++++++++++------
.../src/cli/cmd/tui/context/model-variant.ts | 36 ++++++++++++++++
packages/opencode/test/agent/agent.test.ts | 28 +++++++++++++
.../test/cli/tui/model-variant.test.ts | 40 ++++++++++++++++++
9 files changed, 174 insertions(+), 32 deletions(-)
create mode 100644 packages/opencode/src/cli/cmd/tui/context/model-variant.ts
create mode 100644 packages/opencode/test/cli/tui/model-variant.test.ts
diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md
index a77b92737bc9..b2f1bdffa56f 100644
--- a/.opencode/agent/triage.md
+++ b/.opencode/agent/triage.md
@@ -3,6 +3,7 @@ mode: primary
hidden: true
model: opencode/minimax-m2.5
color: "#44BA81"
+variant: "high"
tools:
"*": false
"github-triage": true
diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx
index ac5da60e8629..474aad4f39ad 100644
--- a/packages/app/src/context/local.tsx
+++ b/packages/app/src/context/local.tsx
@@ -206,6 +206,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
configured: this.configured(),
})
},
+ effective() {
+ return this.current()
+ },
list() {
const m = current()
if (!m) return []
diff --git a/packages/app/src/context/model-variant.test.ts b/packages/app/src/context/model-variant.test.ts
index 01b149fd2676..6d5e4dd2fbdb 100644
--- a/packages/app/src/context/model-variant.test.ts
+++ b/packages/app/src/context/model-variant.test.ts
@@ -2,15 +2,15 @@ import { describe, expect, test } from "bun:test"
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
describe("model variant", () => {
- test("resolves configured agent variant when model matches", () => {
+ test("resolves configured agent variant when model supports variant", () => {
const value = getConfiguredAgentVariant({
agent: {
model: { providerID: "openai", modelID: "gpt-5.2" },
variant: "xhigh",
},
model: {
- providerID: "openai",
- modelID: "gpt-5.2",
+ providerID: "anthropic",
+ modelID: "claude-sonnet-4",
variants: { low: {}, high: {}, xhigh: {} },
},
})
@@ -18,7 +18,7 @@ describe("model variant", () => {
expect(value).toBe("xhigh")
})
- test("ignores configured variant when model does not match", () => {
+ test("ignores configured variant when model does not support it", () => {
const value = getConfiguredAgentVariant({
agent: {
model: { providerID: "openai", modelID: "gpt-5.2" },
@@ -27,7 +27,7 @@ describe("model variant", () => {
model: {
providerID: "anthropic",
modelID: "claude-sonnet-4",
- variants: { low: {}, high: {}, xhigh: {} },
+ variants: { low: {}, high: {} },
},
})
@@ -44,17 +44,17 @@ describe("model variant", () => {
expect(value).toBe("high")
})
- test("cycles from configured variant to next", () => {
+ test("starts cycling from first variant when no explicit selection", () => {
const value = cycleModelVariant({
variants: ["low", "high", "xhigh"],
selected: undefined,
configured: "high",
})
- expect(value).toBe("xhigh")
+ expect(value).toBe("low")
})
- test("wraps from configured last variant to first", () => {
+ test("starts from first even when configured is last", () => {
const value = cycleModelVariant({
variants: ["low", "high", "xhigh"],
selected: undefined,
@@ -63,4 +63,17 @@ describe("model variant", () => {
expect(value).toBe("low")
})
+
+ test("cycles through all variants from explicit selection", () => {
+ const variants = ["low", "high", "xhigh"]
+ const first = cycleModelVariant({ variants, selected: undefined, configured: "high" })
+ const second = cycleModelVariant({ variants, selected: first, configured: "high" })
+ const third = cycleModelVariant({ variants, selected: second, configured: "high" })
+ const fourth = cycleModelVariant({ variants, selected: third, configured: "high" })
+
+ expect(first).toBe("low")
+ expect(second).toBe("high")
+ expect(third).toBe("xhigh")
+ expect(fourth).toBeUndefined()
+ })
})
diff --git a/packages/app/src/context/model-variant.ts b/packages/app/src/context/model-variant.ts
index 6b7ae7256409..10c75b59b1e0 100644
--- a/packages/app/src/context/model-variant.ts
+++ b/packages/app/src/context/model-variant.ts
@@ -20,10 +20,7 @@ type VariantInput = {
export function getConfiguredAgentVariant(input: { agent: Agent | undefined; model: Model | undefined }) {
if (!input.agent?.variant) return undefined
- if (!input.agent.model) return undefined
if (!input.model?.variants) return undefined
- if (input.agent.model.providerID !== input.model.providerID) return undefined
- if (input.agent.model.modelID !== input.model.modelID) return undefined
if (!(input.agent.variant in input.model.variants)) return undefined
return input.agent.variant
}
@@ -41,10 +38,5 @@ export function cycleModelVariant(input: VariantInput) {
if (index === input.variants.length - 1) return undefined
return input.variants[index + 1]
}
- if (input.configured && input.variants.includes(input.configured)) {
- const index = input.variants.indexOf(input.configured)
- if (index === input.variants.length - 1) return input.variants[0]
- return input.variants[index + 1]
- }
return input.variants[0]
}
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index cefef208de4a..f99618d4f8fe 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -32,6 +32,7 @@ import { useToast } from "../../ui/toast"
import { useKV } from "../../context/kv"
import { useTextareaKeybindings } from "../textarea-keybindings"
import { DialogSkill } from "../dialog-skill"
+import { getConfiguredAgentVariant } from "@tui/context/model-variant"
export type PromptProps = {
sessionID?: string
@@ -161,7 +162,17 @@ export function Prompt(props: PromptProps) {
if (msg.agent && isPrimaryAgent) {
local.agent.set(msg.agent)
if (msg.model) local.model.set(msg.model)
- if (msg.variant) local.model.variant.set(msg.variant)
+ if (msg.variant) {
+ const info = local.agent.list().find((x) => x.name === msg.agent)
+ const provider = msg.model ? sync.data.provider.find((x) => x.id === msg.model.providerID) : undefined
+ const model = msg.model ? provider?.models[msg.model.modelID] : undefined
+ const configured = getConfiguredAgentVariant({
+ agent: { variant: info?.variant },
+ model: { variants: model?.variants },
+ })
+ if (msg.variant === configured) local.model.variant.set(undefined)
+ if (msg.variant !== configured) local.model.variant.set(msg.variant)
+ }
}
}
})
@@ -743,8 +754,8 @@ export function Prompt(props: PromptProps) {
const showVariant = createMemo(() => {
const variants = local.model.variant.list()
if (variants.length === 0) return false
- const current = local.model.variant.current()
- return !!current
+ const effective = local.model.variant.effective()
+ return !!effective
})
const placeholderText = createMemo(() => {
@@ -1007,7 +1018,7 @@ export function Prompt(props: PromptProps) {
ยท
- {local.model.variant.current()}
+ {local.model.variant.effective()}
diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx
index 72c72dc5bb3c..4e2ce09f12f5 100644
--- a/packages/opencode/src/cli/cmd/tui/context/local.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx
@@ -12,6 +12,7 @@ import { Provider } from "@/provider/provider"
import { useArgs } from "./args"
import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core"
+import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
@@ -324,12 +325,33 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
},
variant: {
- current() {
+ configured() {
+ const a = agent.current()
+ const m = currentModel()
+ if (!m) return undefined
+ const provider = sync.data.provider.find((x) => x.id === m.providerID)
+ const info = provider?.models[m.modelID]
+ return getConfiguredAgentVariant({
+ agent: { variant: a.variant },
+ model: { variants: info?.variants },
+ })
+ },
+ selected() {
const m = currentModel()
if (!m) return undefined
const key = `${m.providerID}/${m.modelID}`
return modelStore.variant[key]
},
+ current() {
+ return resolveModelVariant({
+ variants: this.list(),
+ selected: this.selected(),
+ configured: this.configured(),
+ })
+ },
+ effective() {
+ return this.current()
+ },
list() {
const m = currentModel()
if (!m) return []
@@ -348,17 +370,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
cycle() {
const variants = this.list()
if (variants.length === 0) return
- const current = this.current()
- if (!current) {
- this.set(variants[0])
- return
- }
- const index = variants.indexOf(current)
- if (index === -1 || index === variants.length - 1) {
- this.set(undefined)
- return
- }
- this.set(variants[index + 1])
+ this.set(
+ cycleModelVariant({
+ variants,
+ selected: this.selected(),
+ configured: this.configured(),
+ }),
+ )
},
},
}
diff --git a/packages/opencode/src/cli/cmd/tui/context/model-variant.ts b/packages/opencode/src/cli/cmd/tui/context/model-variant.ts
new file mode 100644
index 000000000000..f6c430ea4ae4
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/context/model-variant.ts
@@ -0,0 +1,36 @@
+type Agent = {
+ variant?: string
+}
+
+type Model = {
+ variants?: Record
+}
+
+type VariantInput = {
+ variants: string[]
+ selected: string | undefined
+ configured: string | undefined
+}
+
+export function getConfiguredAgentVariant(input: { agent: Agent | undefined; model: Model | undefined }) {
+ if (!input.agent?.variant) return undefined
+ if (!input.model?.variants) return undefined
+ if (!(input.agent.variant in input.model.variants)) return undefined
+ return input.agent.variant
+}
+
+export function resolveModelVariant(input: VariantInput) {
+ if (input.selected && input.variants.includes(input.selected)) return input.selected
+ if (input.configured && input.variants.includes(input.configured)) return input.configured
+ return undefined
+}
+
+export function cycleModelVariant(input: VariantInput) {
+ if (input.variants.length === 0) return undefined
+ if (input.selected && input.variants.includes(input.selected)) {
+ const index = input.variants.indexOf(input.selected)
+ if (index === input.variants.length - 1) return undefined
+ return input.variants[index + 1]
+ }
+ return input.variants[0]
+}
diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts
index 9f8de04f80c4..c7c6834caad9 100644
--- a/packages/opencode/test/agent/agent.test.ts
+++ b/packages/opencode/test/agent/agent.test.ts
@@ -687,3 +687,31 @@ test("defaultAgent throws when all primary agents are disabled", async () => {
},
})
})
+
+test("agent variant can be set from config", async () => {
+ await using tmp = await tmpdir({
+ config: {
+ agent: {
+ build: { variant: "high" },
+ },
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const build = await Agent.get("build")
+ expect(build?.variant).toBe("high")
+ },
+ })
+})
+
+test("agent variant defaults to undefined when not set", async () => {
+ await using tmp = await tmpdir()
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const build = await Agent.get("build")
+ expect(build?.variant).toBeUndefined()
+ },
+ })
+})
diff --git a/packages/opencode/test/cli/tui/model-variant.test.ts b/packages/opencode/test/cli/tui/model-variant.test.ts
new file mode 100644
index 000000000000..b64847373677
--- /dev/null
+++ b/packages/opencode/test/cli/tui/model-variant.test.ts
@@ -0,0 +1,40 @@
+import { describe, expect, test } from "bun:test"
+import {
+ cycleModelVariant,
+ getConfiguredAgentVariant,
+ resolveModelVariant,
+} from "../../../src/cli/cmd/tui/context/model-variant"
+
+describe("tui model variant", () => {
+ test("resolves configured variant when active model supports it", () => {
+ const value = getConfiguredAgentVariant({
+ agent: { variant: "high" },
+ model: { variants: { low: {}, high: {} } },
+ })
+
+ expect(value).toBe("high")
+ })
+
+ test("prefers selected variant over configured variant", () => {
+ const value = resolveModelVariant({
+ variants: ["low", "high", "xhigh"],
+ selected: "xhigh",
+ configured: "high",
+ })
+
+ expect(value).toBe("xhigh")
+ })
+
+ test("cycles through all variants from explicit selection", () => {
+ const variants = ["low", "high", "xhigh"]
+ const first = cycleModelVariant({ variants, selected: undefined, configured: "high" })
+ const second = cycleModelVariant({ variants, selected: first, configured: "high" })
+ const third = cycleModelVariant({ variants, selected: second, configured: "high" })
+ const fourth = cycleModelVariant({ variants, selected: third, configured: "high" })
+
+ expect(first).toBe("low")
+ expect(second).toBe("high")
+ expect(third).toBe("xhigh")
+ expect(fourth).toBeUndefined()
+ })
+})