import { createSimpleContext } from "@opencode-ai/ui/context" import { base64Encode } from "@opencode-ai/core/util/encode" import { useParams } from "@solidjs/router" import { batch, createEffect, createMemo } from "solid-js" import { createStore } from "solid-js/store" import { useModels } from "@/context/models" import { useProviders } from "@/hooks/use-providers" import { Persist, persisted } from "@/utils/persist" import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant" import { useSDK } from "./sdk" import { useSync } from "./sync" export type ModelKey = { providerID: string; modelID: string; variant?: string } type State = { agent?: string model?: ModelKey variant?: string | null } type Saved = { session: Record } const WORKSPACE_KEY = "__workspace__" const handoff = new Map() const handoffKey = (dir: string, id: string) => `${dir}\n${id}` const migrate = (value: unknown) => { if (!value || typeof value !== "object") return { session: {} } const item = value as { session?: Record pick?: Record } if (item.session && typeof item.session === "object") return { session: item.session } if (!item.pick || typeof item.pick !== "object") return { session: {} } return { session: Object.fromEntries(Object.entries(item.pick).filter(([key]) => key !== WORKSPACE_KEY)), } } const clone = (value: State | undefined) => { if (!value) return undefined return { ...value, model: value.model ? { ...value.model } : undefined, } satisfies State } export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ name: "Local", init: () => { const params = useParams() const sdk = useSDK() const sync = useSync() const providers = useProviders() const models = useModels() const id = createMemo(() => params.id || undefined) const list = createMemo(() => sync.data.agent.filter((item) => item.mode !== "subagent" && !item.hidden)) const connected = createMemo(() => new Set(providers.connected().map((item) => item.id))) const [saved, setSaved] = persisted( { ...Persist.workspace(sdk.directory, "model-selection", ["model-selection.v1"]), migrate, }, createStore({ session: {}, }), ) const [store, setStore] = createStore<{ current?: string draft?: State last?: { type: "agent" | "model" | "variant" agent?: string model?: ModelKey | null variant?: string | null } }>({ current: list()[0]?.name, draft: undefined, last: undefined, }) const validModel = (model: ModelKey) => { const provider = providers.all().find((item) => item.id === model.providerID) return !!provider?.models[model.modelID] && connected().has(model.providerID) } const firstModel = (...items: Array<() => ModelKey | undefined>) => { for (const item of items) { const model = item() if (!model) continue if (validModel(model)) return model } } const pickAgent = (name: string | undefined) => { const items = list() if (items.length === 0) return undefined return items.find((item) => item.name === name) ?? items[0] } createEffect(() => { const items = list() if (items.length === 0) { if (store.current !== undefined) setStore("current", undefined) return } if (items.some((item) => item.name === store.current)) return setStore("current", items[0]?.name) }) const scope = createMemo(() => { const session = id() if (!session) return store.draft return saved.session[session] ?? handoff.get(handoffKey(sdk.directory, session)) }) createEffect(() => { const session = id() if (!session) return const key = handoffKey(sdk.directory, session) const next = handoff.get(key) if (!next) return if (saved.session[session] !== undefined) { handoff.delete(key) return } setSaved("session", session, clone(next)) handoff.delete(key) }) const configuredModel = () => { if (!sync.data.config.model) return const [providerID, modelID] = sync.data.config.model.split("/") const model = { providerID, modelID } if (validModel(model)) return model } const recentModel = () => { for (const item of models.recent.list()) { if (validModel(item)) return item } } const defaultModel = () => { const defaults = providers.default() for (const provider of providers.connected()) { const configured = defaults[provider.id] if (configured) { const model = { providerID: provider.id, modelID: configured } if (validModel(model)) return model } const first = Object.values(provider.models)[0] if (!first) continue const model = { providerID: provider.id, modelID: first.id } if (validModel(model)) return model } } const fallback = createMemo(() => configuredModel() ?? recentModel() ?? defaultModel()) const agent = { list, current() { return pickAgent(scope()?.agent ?? store.current) }, set(name: string | undefined) { const item = pickAgent(name) if (!item) { setStore("current", undefined) return } batch(() => { setStore("current", item.name) setStore("last", { type: "agent", agent: item.name, model: item.model, variant: item.variant ?? null, }) const prev = scope() const next = { agent: item.name, model: item.model ?? prev?.model, variant: item.variant ?? prev?.variant, } satisfies State const session = id() if (session) { setSaved("session", session, next) return } setStore("draft", next) }) }, move(direction: 1 | -1) { const items = list() if (items.length === 0) { setStore("current", undefined) return } let next = items.findIndex((item) => item.name === agent.current()?.name) + direction if (next < 0) next = items.length - 1 if (next >= items.length) next = 0 const item = items[next] if (!item) return agent.set(item.name) }, } const current = () => { const item = firstModel( () => scope()?.model, () => agent.current()?.model, fallback, ) if (!item) return undefined return models.find(item) } const configured = () => { const item = agent.current() const model = current() if (!item || !model) return undefined return getConfiguredAgentVariant({ agent: { model: item.model, variant: item.variant }, model: { providerID: model.provider.id, modelID: model.id, variants: model.variants }, }) } const selected = () => scope()?.variant const snapshot = () => { const model = current() return { agent: agent.current()?.name, model: model ? { providerID: model.provider.id, modelID: model.id } : undefined, variant: selected(), } satisfies State } const write = (next: Partial) => { const state = { ...(scope() ?? { agent: agent.current()?.name }), ...next, } satisfies State const session = id() if (session) { setSaved("session", session, state) return } setStore("draft", state) } const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean)) const model = { ready: models.ready, current, recent, list: models.list, cycle(direction: 1 | -1) { const items = recent() const item = current() if (!item) return const index = items.findIndex((entry) => entry?.provider.id === item.provider.id && entry?.id === item.id) if (index === -1) return let next = index + direction if (next < 0) next = items.length - 1 if (next >= items.length) next = 0 const entry = items[next] if (!entry) return model.set({ providerID: entry.provider.id, modelID: entry.id }) }, set(item: ModelKey | undefined, options?: { recent?: boolean }) { batch(() => { setStore("last", { type: "model", agent: agent.current()?.name, model: item ?? null, variant: selected(), }) write({ model: item }) if (!item) return models.setVisibility(item, true) if (!options?.recent) return models.recent.push(item) }) }, visible(item: ModelKey) { return models.visible(item) }, setVisibility(item: ModelKey, visible: boolean) { models.setVisibility(item, visible) }, variant: { configured, selected, current() { return resolveModelVariant({ variants: this.list(), selected: this.selected(), configured: this.configured(), }) }, list() { const item = current() if (!item?.variants) return [] return Object.keys(item.variants) }, set(value: string | undefined) { batch(() => { const model = current() setStore("last", { type: "variant", agent: agent.current()?.name, model: model ? { providerID: model.provider.id, modelID: model.id } : null, variant: value ?? null, }) write({ variant: value ?? null }) }) }, cycle() { const items = this.list() if (items.length === 0) return this.set( cycleModelVariant({ variants: items, selected: this.selected(), configured: this.configured(), }), ) }, }, } const result = { slug: createMemo(() => base64Encode(sdk.directory)), model, agent, session: { reset() { setStore("draft", undefined) }, promote(dir: string, session: string) { const next = clone(snapshot()) if (!next) return if (dir === sdk.directory) { setSaved("session", session, next) setStore("draft", undefined) return } handoff.set(handoffKey(dir, session), next) setStore("draft", undefined) }, restore(msg: { sessionID: string; agent: string; model: ModelKey }) { const session = id() if (!session) return if (msg.sessionID !== session) return if (saved.session[session] !== undefined) return if (handoff.has(handoffKey(sdk.directory, session))) return setSaved("session", session, { agent: msg.agent, model: msg.model, variant: msg.model?.variant ?? null, }) }, }, } return result }, })