Add model offline badge and streaming safeguards

This commit is contained in:
Gemini AI
2025-12-24 13:52:33 +04:00
Unverified
parent 68b6c39934
commit d153892bdf
4 changed files with 1081 additions and 23 deletions

View File

@@ -94,7 +94,7 @@ interface MessagePartProps {
return ( return (
<Switch> <Switch>
<Match when={partType() === "text"}> <Match when={partType() === "text"}>
<Show when={!(props.part.type === "text" && props.part.synthetic) && partHasRenderableText(props.part)}> <Show when={!(props.part.type === "text" && props.part.synthetic && isAssistantMessage()) && partHasRenderableText(props.part)}>
<div class={textContainerClass()}> <div class={textContainerClass()}>
<Show <Show
when={isAssistantMessage()} when={isAssistantMessage()}

View File

@@ -1,11 +1,12 @@
import { Combobox } from "@kobalte/core/combobox" import { Combobox } from "@kobalte/core/combobox"
import { createEffect, createMemo, createSignal } from "solid-js" import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
import { providers, fetchProviders } from "../stores/sessions" import { providers, fetchProviders } from "../stores/sessions"
import { ChevronDown } from "lucide-solid" import { ChevronDown } from "lucide-solid"
import type { Model } from "../types/session" import type { Model } from "../types/session"
import { getLogger } from "../lib/logger" import { getLogger } from "../lib/logger"
const log = getLogger("session") const log = getLogger("session")
const OPENCODE_ZEN_OFFLINE_STORAGE_KEY = "opencode-zen-offline-models"
interface ModelSelectorProps { interface ModelSelectorProps {
instanceId: string instanceId: string
@@ -20,9 +21,13 @@ interface FlatModel extends Model {
searchText: string searchText: string
} }
import { useQwenOAuth } from "../lib/integrations/qwen-oauth"
export default function ModelSelector(props: ModelSelectorProps) { export default function ModelSelector(props: ModelSelectorProps) {
const instanceProviders = () => providers().get(props.instanceId) || [] const instanceProviders = () => providers().get(props.instanceId) || []
const [isOpen, setIsOpen] = createSignal(false) const [isOpen, setIsOpen] = createSignal(false)
const qwenAuth = useQwenOAuth()
const [offlineModels, setOfflineModels] = createSignal<Set<string>>(new Set())
let triggerRef!: HTMLButtonElement let triggerRef!: HTMLButtonElement
let searchInputRef!: HTMLInputElement let searchInputRef!: HTMLInputElement
@@ -32,6 +37,41 @@ export default function ModelSelector(props: ModelSelectorProps) {
} }
}) })
const readOfflineModels = () => {
if (typeof window === "undefined") return new Set<string>()
try {
const raw = window.localStorage.getItem(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<string>()
}
}
const refreshOfflineModels = () => {
setOfflineModels(readOfflineModels())
}
onMount(() => {
refreshOfflineModels()
if (typeof window === "undefined") return
const handleCustom = () => refreshOfflineModels()
const handleStorage = (event: StorageEvent) => {
if (event.key === 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<FlatModel[]>(() => const allModels = createMemo<FlatModel[]>(() =>
instanceProviders().flatMap((p) => instanceProviders().flatMap((p) =>
p.models.map((m) => ({ p.models.map((m) => ({
@@ -49,6 +89,21 @@ export default function ModelSelector(props: ModelSelectorProps) {
const handleChange = async (value: FlatModel | null) => { const handleChange = async (value: FlatModel | null) => {
if (!value) return 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 }) await props.onModelChange({ providerId: value.providerId, modelId: value.id })
} }
@@ -83,8 +138,11 @@ export default function ModelSelector(props: ModelSelectorProps) {
class="selector-option" class="selector-option"
> >
<div class="selector-option-content"> <div class="selector-option-content">
<Combobox.ItemLabel class="selector-option-label"> <Combobox.ItemLabel class="selector-option-label flex items-center gap-2">
{itemProps.item.rawValue.name} <span class="truncate">{itemProps.item.rawValue.name}</span>
{isOfflineModel(itemProps.item.rawValue) && (
<span class="selector-badge selector-badge-warning">Offline</span>
)}
</Combobox.ItemLabel> </Combobox.ItemLabel>
<Combobox.ItemDescription class="selector-option-description"> <Combobox.ItemDescription class="selector-option-description">
{itemProps.item.rawValue.providerName} {itemProps.item.rawValue.providerId}/ {itemProps.item.rawValue.providerName} {itemProps.item.rawValue.providerId}/
@@ -106,8 +164,11 @@ export default function ModelSelector(props: ModelSelectorProps) {
class="selector-trigger" class="selector-trigger"
> >
<div class="selector-trigger-label selector-trigger-label--stacked"> <div class="selector-trigger-label selector-trigger-label--stacked">
<span class="selector-trigger-primary selector-trigger-primary--align-left"> <span class="selector-trigger-primary selector-trigger-primary--align-left flex items-center gap-2">
Model: {currentModelValue()?.name ?? "None"} <span class="truncate">Model: {currentModelValue()?.name ?? "None"}</span>
{currentModelValue() && isOfflineModel(currentModelValue() as FlatModel) && (
<span class="selector-badge selector-badge-warning">Offline</span>
)}
</span> </span>
{currentModelValue() && ( {currentModelValue() && (
<span class="selector-trigger-secondary"> <span class="selector-trigger-secondary">

File diff suppressed because it is too large Load Diff

View File

@@ -152,6 +152,11 @@
color: var(--text-inverted); color: var(--text-inverted);
} }
.selector-badge-warning {
background-color: var(--status-warning);
color: var(--text-inverted);
}
.selector-badge-version { .selector-badge-version {
@apply text-xs; @apply text-xs;
color: var(--text-muted); color: var(--text-muted);