Skip to content

Commit 2198880

Browse files
feat: add agent default variant handling in TUI and desktop
- Resolve configured variants through shared helpers used by app and TUI - Respect agent-configured variants unless the user selects an override or Default - Restore and hydrate TUI variant state without stale overrides or legacy default sentinels - Add variant tests for shared helpers and agent config Closes #22065
1 parent 4781564 commit 2198880

8 files changed

Lines changed: 95 additions & 24 deletions

File tree

packages/app/src/context/local.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { createStore } from "solid-js/store"
66
import { useModels } from "@/context/models"
77
import { useProviders } from "@/hooks/use-providers"
88
import { Persist, persisted } from "@/utils/persist"
9-
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
9+
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "@opencode-ai/core/util/model-variant"
1010
import { useSDK } from "./sdk"
1111
import { useSync } from "./sync"
1212

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ type Model = AgentModel & {
1212
variants?: Record<string, unknown>
1313
}
1414

15+
// selected: string = user-chosen variant name
16+
// selected: null = user explicitly chose "default" (clears any agent-configured variant)
17+
// selected: undefined = no user choice yet (fall back to agent-configured variant)
1518
type VariantInput = {
1619
variants: string[]
1720
selected: string | null | undefined
@@ -43,6 +46,7 @@ export function cycleModelVariant(input: VariantInput) {
4346
if (index === input.variants.length - 1) return undefined
4447
return input.variants[index + 1]
4548
}
49+
// No explicit selection: start cycling from the agent-configured variant.
4650
if (input.configured && input.variants.includes(input.configured)) {
4751
const index = input.variants.indexOf(input.configured)
4852
if (index === input.variants.length - 1) return input.variants[0]

packages/app/src/context/model-variant.test.ts renamed to packages/core/test/util/model-variant.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from "bun:test"
2-
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
2+
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "../../src/util/model-variant"
33

44
describe("model variant", () => {
55
test("resolves configured agent variant when model matches", () => {
@@ -83,4 +83,17 @@ describe("model variant", () => {
8383

8484
expect(value).toBe("low")
8585
})
86+
87+
test("cycles through all variants from explicit selection", () => {
88+
const variants = ["low", "high", "xhigh"]
89+
const first = cycleModelVariant({ variants, selected: undefined, configured: undefined })
90+
const second = cycleModelVariant({ variants, selected: first, configured: undefined })
91+
const third = cycleModelVariant({ variants, selected: second, configured: undefined })
92+
const fourth = cycleModelVariant({ variants, selected: third, configured: undefined })
93+
94+
expect(first).toBe("low")
95+
expect(second).toBe("high")
96+
expect(third).toBe("xhigh")
97+
expect(fourth).toBeUndefined()
98+
})
8699
})

packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,13 @@ export function DialogModel(props: { providerID?: string }) {
135135
function onSelect(providerID: string, modelID: string) {
136136
local.model.set({ providerID, modelID }, { recent: true })
137137
const list = local.model.variant.list()
138-
const cur = local.model.variant.selected()
139-
if (cur === "default" || (cur && list.includes(cur))) {
138+
const selected = local.model.variant.selected()
139+
const current = local.model.variant.current()
140+
if (
141+
selected === null ||
142+
(selected && list.includes(selected)) ||
143+
(!selected && current && list.includes(current))
144+
) {
140145
dialog.clear()
141146
return
142147
}

packages/opencode/src/cli/cmd/tui/component/dialog-variant.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function DialogVariant() {
3232
<DialogSelect<string>
3333
options={options()}
3434
title={"Select variant"}
35-
current={local.model.variant.selected()}
35+
current={local.model.variant.current() ?? "default"}
3636
flat={true}
3737
/>
3838
)

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,13 @@ export function Prompt(props: PromptProps) {
287287
if (!args.agent) local.agent.set(msg.agent)
288288
if (msg.model) {
289289
local.model.set(msg.model)
290-
local.model.variant.set(msg.model.variant)
290+
const configured = local.model.variant.configured()
291+
if (!msg.model.variant || msg.model.variant === configured) {
292+
local.model.variant.clear()
293+
}
294+
if (msg.model.variant && msg.model.variant !== configured) {
295+
local.model.variant.set(msg.model.variant)
296+
}
291297
}
292298
}
293299
}

packages/opencode/src/cli/cmd/tui/context/local.tsx

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useArgs } from "./args"
1212
import { useSDK } from "./sdk"
1313
import { RGBA } from "@opentui/core"
1414
import { Filesystem } from "@/util/filesystem"
15+
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "@opencode-ai/core/util/model-variant"
1516

1617
export function parseModel(model: string) {
1718
const [providerID, ...rest] = model.split("/")
@@ -118,7 +119,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
118119
providerID: string
119120
modelID: string
120121
}[]
121-
variant: Record<string, string | undefined>
122+
variant: Record<string, string | null | undefined>
122123
}>({
123124
ready: false,
124125
model: {},
@@ -149,7 +150,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
149150
.then((x: any) => {
150151
if (Array.isArray(x.recent)) setModelStore("recent", x.recent)
151152
if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
152-
if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant)
153+
if (typeof x.variant === "object" && x.variant !== null) {
154+
const variant = Object.fromEntries(
155+
Object.entries(x.variant).map(([key, value]) => [
156+
key,
157+
value === "default" ? null : typeof value === "string" || value === null ? value : undefined,
158+
]),
159+
) as Record<string, string | null | undefined>
160+
setModelStore("variant", variant)
161+
if (Object.values(x.variant).includes("default")) state.pending = true
162+
}
153163
})
154164
.catch(() => {})
155165
.finally(() => {
@@ -334,17 +344,29 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
334344
})
335345
},
336346
variant: {
347+
configured() {
348+
const a = agent.current()
349+
const m = currentModel()
350+
if (!m || !a) return undefined
351+
const provider = sync.data.provider.find((x) => x.id === m.providerID)
352+
const info = provider?.models[m.modelID]
353+
return getConfiguredAgentVariant({
354+
agent: { model: a.model, variant: a.variant },
355+
model: { providerID: m.providerID, modelID: m.modelID, variants: info?.variants },
356+
})
357+
},
337358
selected() {
338359
const m = currentModel()
339360
if (!m) return undefined
340361
const key = `${m.providerID}/${m.modelID}`
341362
return modelStore.variant[key]
342363
},
343364
current() {
344-
const v = this.selected()
345-
if (!v) return undefined
346-
if (!this.list().includes(v)) return undefined
347-
return v
365+
return resolveModelVariant({
366+
variants: this.list(),
367+
selected: this.selected(),
368+
configured: this.configured(),
369+
})
348370
},
349371
list() {
350372
const m = currentModel()
@@ -358,23 +380,26 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
358380
const m = currentModel()
359381
if (!m) return
360382
const key = `${m.providerID}/${m.modelID}`
361-
setModelStore("variant", key, value ?? "default")
383+
setModelStore("variant", key, value ?? null)
384+
save()
385+
},
386+
clear() {
387+
const m = currentModel()
388+
if (!m) return
389+
const key = `${m.providerID}/${m.modelID}`
390+
setModelStore("variant", key, undefined)
362391
save()
363392
},
364393
cycle() {
365394
const variants = this.list()
366395
if (variants.length === 0) return
367-
const current = this.current()
368-
if (!current) {
369-
this.set(variants[0])
370-
return
371-
}
372-
const index = variants.indexOf(current)
373-
if (index === -1 || index === variants.length - 1) {
374-
this.set(undefined)
375-
return
376-
}
377-
this.set(variants[index + 1])
396+
this.set(
397+
cycleModelVariant({
398+
variants,
399+
selected: this.selected(),
400+
configured: this.configured(),
401+
}),
402+
)
378403
},
379404
},
380405
}

packages/opencode/test/agent/agent.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,3 +740,21 @@ test("defaultAgent throws when all primary agents are disabled", async () => {
740740
},
741741
})
742742
})
743+
744+
test("agent variant can be set from config", async () => {
745+
await using tmp = await tmpdir({
746+
config: {
747+
agent: {
748+
build: { variant: "high" },
749+
},
750+
},
751+
})
752+
const build = await load(tmp.path, (svc) => svc.get("build"))
753+
expect(build?.variant).toBe("high")
754+
})
755+
756+
test("agent variant defaults to undefined when not set", async () => {
757+
await using tmp = await tmpdir()
758+
const build = await load(tmp.path, (svc) => svc.get("build"))
759+
expect(build?.variant).toBeUndefined()
760+
})

0 commit comments

Comments
 (0)