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() + }) +})