tui: hydrate model after catalog loads

Session model was applied once on prompt mount, before the
location-scoped catalog finished loading, so startup could pick a
fallback or miss durable model changes. Hydrate reactively once the
active location's models are valid.
This commit is contained in:
Simon Klee 2026-07-27 15:29:45 +02:00
commit 29494bbf71
No known key found for this signature in database
GPG key ID: B91696044D47BEA3
6 changed files with 149 additions and 32 deletions

View file

@ -7,16 +7,20 @@ import { DialogVariant } from "./dialog-variant"
import * as fuzzysort from "fuzzysort"
import { useConnected } from "./use-connected"
import { useData } from "../context/data"
import { useLocation } from "../context/location"
export function DialogModel(props: { providerID?: string }) {
const local = useLocal()
const data = useData()
const location = useLocation()
const dialog = useDialog()
const [query, setQuery] = createSignal("")
const connected = useConnected()
const providers = createMemo(() => new Map((data.location.provider.list() ?? []).map((item) => [item.id, item])))
const models = createMemo(() => data.location.model.list() ?? [])
const providers = createMemo(
() => new Map((data.location.provider.list(location.ref) ?? []).map((item) => [item.id, item])),
)
const models = createMemo(() => data.location.model.list(location.ref) ?? [])
const showExtra = createMemo(() => connected() && !props.providerID)

View file

@ -360,7 +360,8 @@ export function Prompt(props: PromptProps) {
),
)
// Initialize agent/model/variant from the durable V2 Session state.
// Initialize the agent from the durable V2 Session state. Model hydration is
// reactive in LocalProvider so it can wait for the location catalog.
let syncedSessionID: string | undefined
createEffect(() => {
const sessionID = props.sessionID
@ -369,10 +370,6 @@ export function Prompt(props: PromptProps) {
if (!session) return
const agent = session.agent && local.agent.list().find((agent) => agent.id === session.agent)
if (agent && !args.agent) local.agent.set(agent.id)
if (session.model) {
local.model.set({ providerID: session.model.providerID, modelID: session.model.id })
local.model.variant.set(session.model.variant)
}
syncedSessionID = sessionID
})

View file

@ -1,8 +1,10 @@
import { createMemo } from "solid-js"
import { useData } from "../context/data"
import { useLocation } from "../context/location"
import { hasConnectedProvider } from "../util/connected-provider"
export function useConnected() {
const data = useData()
return createMemo(() => hasConnectedProvider(data.location.integration.list() ?? []))
const location = useLocation()
return createMemo(() => hasConnectedProvider(data.location.integration.list(location.ref) ?? []))
}

View file

@ -22,6 +22,7 @@ import { useToast } from "../ui/toast"
import { useRoute } from "./route"
import { useData } from "./data"
import { usePermission } from "./permission"
import { useLocation } from "./location"
export function parseModel(model: string) {
const [providerID, ...rest] = model.split("/")
@ -56,11 +57,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const args = useArgs()
const event = useEvent()
const permission = usePermission()
const location = useLocation()
const models = () => data.location.model.list(location.ref)
function isModelValid(model: ModelPreferenceModel) {
return !!data.location.model
.list()
?.some((item) => item.providerID === model.providerID && item.id === model.modelID)
return !!models()?.some((item) => item.providerID === model.providerID && item.id === model.modelID)
}
function getFirstValidModel(...modelFns: (() => ModelPreferenceModel | undefined)[]) {
@ -73,9 +75,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
function createAgent() {
const agents = createMemo(() =>
(data.location.agent.list() ?? []).filter((agent) => agent.mode !== "subagent" && !agent.hidden),
(data.location.agent.list(location.ref) ?? []).filter((agent) => agent.mode !== "subagent" && !agent.hidden),
)
const visibleAgents = createMemo(() =>
(data.location.agent.list(location.ref) ?? []).filter((agent) => !agent.hidden),
)
const visibleAgents = createMemo(() => (data.location.agent.list() ?? []).filter((agent) => !agent.hidden))
const [agentStore, setAgentStore] = createStore({
current: undefined as string | undefined,
})
@ -190,7 +194,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
}
const model = data.location.model.list()?.[0]
const model = models()?.[0]
if (!model) return undefined
return {
providerID: model.providerID,
@ -209,6 +213,33 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
)
})
let syncedSessionModel: string | undefined
createEffect(() => {
if (route.data.type !== "session") {
syncedSessionModel = undefined
return
}
const session = data.session.get(route.data.sessionID)
const selected = session?.model
const a = agent.current()
if (!selected || !a) return
if (args.model) {
const requested = parseModel(args.model)
if (isModelValid(requested)) return
}
const model = { providerID: selected.providerID, modelID: selected.id }
if (!isModelValid(model)) return
const fingerprint = [session.id, a.id, selected.providerID, selected.id, selected.variant ?? "default"].join(
":",
)
if (fingerprint === syncedSessionModel) return
syncedSessionModel = fingerprint
batch(() => {
setModelStore("model", a.id, model)
setModelStore("variant", modelPreferenceKey(model), normalizeModelVariant(selected.variant))
})
})
return {
current: currentModel,
get ready() {
@ -229,10 +260,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
reasoning: false,
}
}
const provider = data.location.provider.list()?.find((item) => item.id === value.providerID)
const info = data.location.model
.list()
?.find((item) => item.providerID === value.providerID && item.id === value.modelID)
const provider = data.location.provider.list(location.ref)?.find((item) => item.id === value.providerID)
const info = models()?.find((item) => item.providerID === value.providerID && item.id === value.modelID)
return {
provider: provider?.name ?? value.providerID,
model: info?.name ?? value.modelID,
@ -340,9 +369,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
list() {
const m = currentModel()
if (!m) return []
const info = data.location.model
.list()
?.find((item) => item.providerID === m.providerID && item.id === m.modelID)
const info = models()?.find((item) => item.providerID === m.providerID && item.id === m.modelID)
return info?.variants?.map((variant) => variant.id) ?? []
},
set(value: string | undefined) {

View file

@ -4,6 +4,7 @@ import { useClient } from "./client"
import { useData } from "./data"
const context = createContext<{
readonly ref: LocationRef | undefined
readonly current: LocationGetOutput | undefined
set: (location?: LocationRef) => void
}>()
@ -34,6 +35,9 @@ export function LocationProvider(props: ParentProps) {
return (
<context.Provider
value={{
get ref() {
return ref()
},
get current() {
return current()
},