Compare commits

...
Author SHA1 Message Date
Kit Langton 0c0552b23c fix(tui): show the default model for sessions stored without one
Sessions created through the raw API with no model (agent: null, model: null)
run fine on the server's default model, but the prompt status line rendered
only the stored session model and claimed 'No provider selected - Connect a
provider' even with providers connected. The session selection now falls back
to GET /api/model/default, gated on the model still being available, so 'No
provider selected' appears only when there genuinely is no usable provider.
2026-08-23 20:26:45 -04:00
3 changed files with 88 additions and 3 deletions
+40 -2
View File
@@ -1,7 +1,7 @@
import { createStore } from "solid-js/store"
import { dedupeWith } from "effect/Array"
import { createSimpleContext } from "./helper"
import { batch, createMemo, onCleanup } from "solid-js"
import { batch, createMemo, createResource, onCleanup } from "solid-js"
import { useEvent } from "./event"
import path from "path"
import { useTuiPaths } from "./runtime"
@@ -32,6 +32,22 @@ export function parseModel(model: string) {
}
}
/**
* A session stored without a model runs on the server's default model, so the
* status line shows that effective model instead of claiming no provider is
* selected. "No provider selected" remains only when no usable default exists.
*/
export function withDefaultModelFallback(options: {
selection: (ModelPreferenceModel & { variant?: string }) | undefined
defaultModel: ModelPreferenceModel | undefined
isValid: (model: ModelPreferenceModel) => boolean
variantPreference: (model: ModelPreferenceModel) => string | undefined
}) {
if (options.selection) return options.selection
if (!options.defaultModel || !options.isValid(options.defaultModel)) return undefined
return { ...options.defaultModel, variant: normalizeModelVariant(options.variantPreference(options.defaultModel)) }
}
export function recentModels(model: ModelPreferenceModel, recent: ModelPreferenceModel[]) {
const seen = new Set<string>()
return [model, ...recent]
@@ -217,8 +233,30 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
)
})
const [serverDefaultModel] = createResource(
() => {
const ref = location.ref ?? data.location.default()
// Refetch when the catalog changes, such as a provider connecting.
return JSON.stringify([ref.directory, ref.workspaceID, models()?.length ?? -1])
},
async () => {
const ref = location.ref ?? data.location.default()
const response = await client.api.model
.default({ location: { directory: ref.directory, workspace: ref.workspaceID } })
.catch(() => undefined)
if (!response?.data) return undefined
return { providerID: response.data.providerID, modelID: response.data.id }
},
)
const currentSelection = createMemo<ModelSelection | undefined>(() => {
if (route.data.type === "session") return sessionSelection(route.data.sessionID)
if (route.data.type === "session")
return withDefaultModelFallback({
selection: sessionSelection(route.data.sessionID),
defaultModel: serverDefaultModel(),
isValid: isModelValid,
variantPreference: (model) => preferences.variant[modelPreferenceKey(model)],
})
const model = newSessionModel()
if (!model) return
return { ...model, variant: normalizeModelVariant(preferences.variant[modelPreferenceKey(model)]) }
+43 -1
View File
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test"
import { parseModel, recentModels } from "../../src/context/local"
import { parseModel, recentModels, withDefaultModelFallback } from "../../src/context/local"
test("parses model IDs containing slashes", () => {
expect(parseModel("provider/family/model")).toEqual({
@@ -20,3 +20,45 @@ test("moves a model to the front, deduplicates, and limits recents", () => {
...recent.slice(6, 10),
])
})
test("session selection wins over the default model", () => {
const selection = { providerID: "openai", modelID: "gpt", variant: "high" }
expect(
withDefaultModelFallback({
selection,
defaultModel: { providerID: "opencode", modelID: "fable" },
isValid: () => true,
variantPreference: () => undefined,
}),
).toBe(selection)
})
test("sessions without a stored model fall back to the server default", () => {
expect(
withDefaultModelFallback({
selection: undefined,
defaultModel: { providerID: "opencode", modelID: "fable" },
isValid: () => true,
variantPreference: (model) => (model.modelID === "fable" ? "max" : undefined),
}),
).toEqual({ providerID: "opencode", modelID: "fable", variant: "max" })
})
test("no provider is reported only without a usable default", () => {
expect(
withDefaultModelFallback({
selection: undefined,
defaultModel: undefined,
isValid: () => true,
variantPreference: () => undefined,
}),
).toBeUndefined()
expect(
withDefaultModelFallback({
selection: undefined,
defaultModel: { providerID: "gone", modelID: "model" },
isValid: () => false,
variantPreference: () => undefined,
}),
).toBeUndefined()
})
+5
View File
@@ -152,6 +152,11 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } },
data: [],
})
if (url.pathname === "/api/model/default")
return json({
location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } },
data: null,
})
if (url.pathname === "/api/reference")
return json({ location: { directory, project: { id: "proj_test", directory, canonical: directory } }, data: [] })
if (url.pathname === "/api/websearch/provider") {