import { Combobox } from "@kobalte/core/combobox" import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js" import { providers, fetchProviders } from "../stores/sessions" import { ChevronDown } from "lucide-solid" import type { Model } from "../types/session" import { getLogger } from "../lib/logger" import { getUserScopedKey } from "../lib/user-storage" const log = getLogger("session") const OPENCODE_ZEN_OFFLINE_STORAGE_KEY = "opencode-zen-offline-models" interface ModelSelectorProps { instanceId: string sessionId: string currentModel: { providerId: string; modelId: string } onModelChange: (model: { providerId: string; modelId: string }) => Promise } interface FlatModel extends Model { providerName: string key: string searchText: string } import { useQwenOAuth } from "../lib/integrations/qwen-oauth" export default function ModelSelector(props: ModelSelectorProps) { const instanceProviders = () => providers().get(props.instanceId) || [] const [isOpen, setIsOpen] = createSignal(false) const qwenAuth = useQwenOAuth() const [offlineModels, setOfflineModels] = createSignal>(new Set()) let triggerRef!: HTMLButtonElement let searchInputRef!: HTMLInputElement createEffect(() => { if (instanceProviders().length === 0) { fetchProviders(props.instanceId).catch((error) => log.error("Failed to fetch providers", error)) } }) const readOfflineModels = () => { if (typeof window === "undefined") return new Set() try { const raw = window.localStorage.getItem(getUserScopedKey(OPENCODE_ZEN_OFFLINE_STORAGE_KEY)) const parsed = raw ? JSON.parse(raw) : [] return new Set(Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : []) } catch { return new Set() } } const refreshOfflineModels = () => { setOfflineModels(readOfflineModels()) } onMount(() => { refreshOfflineModels() if (typeof window === "undefined") return const handleCustom = () => refreshOfflineModels() const handleStorage = (event: StorageEvent) => { if (event.key === getUserScopedKey(OPENCODE_ZEN_OFFLINE_STORAGE_KEY)) { refreshOfflineModels() } } window.addEventListener("opencode-zen-offline-models", handleCustom as EventListener) window.addEventListener("storage", handleStorage) onCleanup(() => { window.removeEventListener("opencode-zen-offline-models", handleCustom as EventListener) window.removeEventListener("storage", handleStorage) }) }) const isOfflineModel = (model: FlatModel) => model.providerId === "opencode-zen" && offlineModels().has(model.id) const allModels = createMemo(() => instanceProviders().flatMap((p) => p.models.map((m) => ({ ...m, providerName: p.name, key: `${m.providerId}/${m.id}`, searchText: `${m.name} ${p.name} ${m.providerId} ${m.id} ${m.providerId}/${m.id}`, })), ), ) const currentModelValue = createMemo(() => allModels().find((m) => m.providerId === props.currentModel.providerId && m.id === props.currentModel.modelId), ) const handleChange = async (value: FlatModel | null) => { if (!value) return // Auto-trigger Qwen OAuth if needed if (value.providerId === 'qwen-oauth' && !qwenAuth.isAuthenticated()) { const confirmed = window.confirm("Qwen Code requires authentication. Sign in now?") if (confirmed) { try { await qwenAuth.signIn() } catch (error) { log.error("Qwen authentication failed", error) // Continue to set model even if auth failed, to allow user to try again later // or user might have authenticatd in another tab } } } await props.onModelChange({ providerId: value.providerId, modelId: value.id }) } const customFilter = (option: FlatModel, inputValue: string) => { return option.searchText.toLowerCase().includes(inputValue.toLowerCase()) } createEffect(() => { if (isOpen()) { setTimeout(() => { searchInputRef?.focus() }, 100) } }) return ( ) }