Compare commits

...
Author SHA1 Message Date
rekram1-node 104b483bb9 fix(tui): default to configured agent variants 2026-09-03 14:47:55 +00:00
2 changed files with 192 additions and 16 deletions
+36 -16
View File
@@ -67,7 +67,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return !!models()?.some((item) => item.providerID === model.providerID && item.id === model.modelID)
}
function getFirstValidModel(...modelFns: (() => ModelPreferenceModel | undefined)[]) {
function getFirstValidModel<T extends ModelPreferenceModel>(...modelFns: (() => T | undefined)[]) {
for (const modelFn of modelFns) {
const model = modelFn()
if (model && isModelValid(model)) return model
@@ -140,7 +140,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
variant: {},
})
const [selectionState, setSelectionState] = createStore<{
newSessionModelByLocationAgent: Record<string, ModelPreferenceModel | undefined>
newSessionModelByLocationAgent: Record<string, ModelSelection | undefined>
draftBySession: Record<string, ModelSelection | undefined>
}>({
newSessionModelByLocationAgent: {},
@@ -210,9 +210,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const newSessionModel = createMemo(() => {
const a = agent.current()
return getFirstValidModel(
return getFirstValidModel<ModelSelection>(
() => a && selectionState.newSessionModelByLocationAgent[locationAgentKey(a.id)],
() => a?.model && { providerID: a.model.providerID, modelID: a.model.id },
() =>
a?.model && {
providerID: a.model.providerID,
modelID: a.model.id,
variant: a.model.variant,
},
fallbackModel,
)
})
@@ -221,7 +226,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (route.data.type === "session") return sessionSelection(route.data.sessionID)
const model = newSessionModel()
if (!model) return
return { ...model, variant: normalizeModelVariant(preferences.variant[modelPreferenceKey(model)]) }
return {
...model,
variant: normalizeModelVariant(model.variant ?? preferences.variant[modelPreferenceKey(model)]),
}
})
const currentModel = createMemo(() => {
@@ -259,22 +267,25 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
function selectModel(model: ModelPreferenceModel) {
const current = currentSelection()
const preferred = normalizeModelVariant(
current?.providerID === model.providerID && current.modelID === model.modelID
? current.variant
: preferences.variant[modelPreferenceKey(model)],
)
const info = models()?.find((item) => item.providerID === model.providerID && item.id === model.modelID)
const variant = preferred && info?.variants?.some((item) => item.id === preferred) ? preferred : undefined
if (route.data.type === "session") {
const sessionID = route.data.sessionID
const current = sessionSelection(sessionID)
const preferred = normalizeModelVariant(
current?.providerID === model.providerID && current.modelID === model.modelID
? current.variant
: preferences.variant[modelPreferenceKey(model)],
)
const info = models()?.find((item) => item.providerID === model.providerID && item.id === model.modelID)
const variant = preferred && info?.variants?.some((item) => item.id === preferred) ? preferred : undefined
setSessionDraft(sessionID, { ...model, variant })
return true
}
const current = agent.current()
if (!current) return false
setSelectionState("newSessionModelByLocationAgent", locationAgentKey(current.id), model)
const a = agent.current()
if (!a) return false
setSelectionState("newSessionModelByLocationAgent", locationAgentKey(a.id), {
...model,
variant: variant ?? "default",
})
return true
}
@@ -438,6 +449,15 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (route.data.type === "session") {
setSessionDraft(route.data.sessionID, { ...m, variant: normalizeModelVariant(value) })
}
if (route.data.type !== "session") {
const current = agent.current()
if (current)
setSelectionState("newSessionModelByLocationAgent", locationAgentKey(current.id), {
...m,
// Keep an explicit default distinct from an absent preference.
variant: value ?? "default",
})
}
setPreferences("variant", modelPreferenceKey(m), normalizeModelVariant(value))
savePreferences()
},
@@ -0,0 +1,156 @@
/** @jsxImportSource @opentui/solid */
import { expect, test } from "bun:test"
import { testRender } from "@opentui/solid"
import path from "node:path"
import { ConfigProvider } from "../../src/config"
import { ArgsProvider } from "../../src/context/args"
import { ClientProvider } from "../../src/context/client"
import { DataProvider, useData } from "../../src/context/data"
import { Keymap } from "../../src/context/keymap"
import { LocalProvider, useLocal } from "../../src/context/local"
import { LocationProvider } from "../../src/context/location"
import { PermissionProvider } from "../../src/context/permission"
import { RouteProvider, useRoute } from "../../src/context/route"
import { TuiAppProvider } from "../../src/context/runtime"
import { StorageProvider, useStorage } from "../../src/context/storage"
import { ThemeProvider } from "../../src/context/theme"
import { ToastProvider } from "../../src/ui/toast"
import { createApi, createEventStream, createFetch, directory, json } from "../fixture/tui-client"
import { emptyThemeSource, tmpdir } from "../fixture/fixture"
import { TestTuiContexts } from "../fixture/tui-environment"
import { createTuiResolvedConfig } from "../fixture/tui-runtime"
import { catalogModel, catalogProvider } from "../mini/fixture/catalog"
test.each([
{ name: "agent low over saved medium", configured: "low", saved: "medium", expected: "low" },
{ name: "agent low without saved preference", configured: "low", saved: undefined, expected: "low" },
{ name: "saved medium without agent variant", configured: undefined, saved: "medium", expected: "medium" },
{ name: "agent default over saved medium", configured: "default", saved: "medium", expected: undefined },
{
name: "existing session retains explicit high",
configured: "low",
saved: "medium",
expected: "high",
session: true,
},
])("variant selection: $name", async ({ configured, saved, expected, session }) => {
const temporary = await tmpdir()
await Bun.write(
path.join(temporary.path, "model.json"),
JSON.stringify({
recent: [],
favorite: [],
variant: { "xai/grok-4.6": saved, "xai/other": "high" },
}),
)
const events = createEventStream()
const agent = { id: "build", mode: "primary", model: { providerID: "xai", id: "grok-4.6", variant: configured } }
const plan = { ...agent, id: "plan", model: { ...agent.model, variant: "high" } }
const model = catalogModel({ id: "grok-4.6", providerID: "xai", variants: ["low", "medium", "high", "xhigh"] })
const location = { directory, project: { id: "proj_test", directory, canonical: directory } }
const calls = createFetch((url) => {
if (url.pathname === "/api/agent") return json({ location, data: [agent, plan] })
if (url.pathname === "/api/model")
return json({ location, data: [model, { ...model, id: "other" }, { ...model, id: "plain" }] })
if (url.pathname === "/api/provider") return json({ location, data: [catalogProvider("xai", "xAI")] })
}, events)
let local!: ReturnType<typeof useLocal>
let storage!: ReturnType<typeof useStorage>
let data!: ReturnType<typeof useData>
let route!: ReturnType<typeof useRoute>
function Probe() {
local = useLocal()
storage = useStorage()
data = useData()
route = useRoute()
return (
<text>
{local.model.ready && local.model.catalogReady && local.agent.current()
? `ready ${JSON.stringify(local.model.selection())}`
: "loading"}
</text>
)
}
const app = await testRender(
() => (
<TestTuiContexts paths={{ state: temporary.path }}>
<TuiAppProvider value={{ name: "test", version: "test", channel: "test" }}>
<StorageProvider>
<ArgsProvider>
<ConfigProvider config={createTuiResolvedConfig()}>
<Keymap.Provider>
<ToastProvider>
<RouteProvider initialRoute={{ type: "home" }}>
<ClientProvider api={createApi(calls.fetch)}>
<PermissionProvider>
<DataProvider directory={directory}>
<LocationProvider>
<ThemeProvider mode="dark" source={emptyThemeSource}>
<LocalProvider>
<Probe />
</LocalProvider>
</ThemeProvider>
</LocationProvider>
</DataProvider>
</PermissionProvider>
</ClientProvider>
</RouteProvider>
</ToastProvider>
</Keymap.Provider>
</ConfigProvider>
</ArgsProvider>
</StorageProvider>
</TuiAppProvider>
</TestTuiContexts>
),
{ width: 120, height: 10 },
)
app.renderer.start()
try {
await app.waitForFrame((frame) => frame.includes("loading") || frame.includes("ready"))
await data.location.sync()
await app.waitForFrame((frame) => frame.includes("ready"))
if (session) {
data.session.remember({
id: "ses_variant",
projectID: "proj_test",
location: { directory },
agent: "build",
model: { providerID: "xai", id: "grok-4.6", variant: "high" },
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 1, updated: 1 },
})
route.navigate({ type: "session", sessionID: "ses_variant" })
}
expect(local.agent.current()?.model?.variant).toBe(configured)
expect(local.model.selection()).toEqual({ providerID: "xai", modelID: "grok-4.6", variant: expected })
local.model.set({ providerID: "xai", modelID: "grok-4.6" })
expect(local.model.selection()?.variant).toBe(expected)
local.model.variant.set("high")
expect(local.model.variant.current()).toBe("high")
local.model.variant.set(undefined)
expect(local.model.selection()?.variant).toBeUndefined()
local.model.variant.cycle()
expect(local.model.variant.current()).toBe("low")
if (!session) {
local.model.variant.set(undefined)
local.agent.set("plan")
expect(local.model.variant.current()).toBe("high")
local.model.variant.set("xhigh")
local.agent.set("build")
expect(local.model.selection()?.variant).toBeUndefined()
}
local.model.set({ providerID: "xai", modelID: "other" })
expect(local.model.variant.current()).toBe("high")
local.model.variant.set(undefined)
local.model.set({ providerID: "xai", modelID: "plain" })
expect(local.model.selection()?.variant).toBeUndefined()
local.model.set({ providerID: "xai", modelID: "other" })
expect(local.model.selection()?.variant).toBeUndefined()
} finally {
app.renderer.destroy()
await storage?.flush()
await temporary[Symbol.asyncDispose]()
}
})