fix: prevent duplicate AI models in selector and fix TypeScript errors
Some checks failed
Release Binaries / release (push) Has been cancelled
Some checks failed
Release Binaries / release (push) Has been cancelled
Changes: 1. Enhanced removeDuplicateProviders() to filter out duplicate providers from SDK when the same provider exists in extras (qwen-oauth, zai, ollama-cloud, antigravity) 2. Added logic to remove any Qwen-related SDK providers when qwen-oauth is authenticated 3. Fixed missing setActiveParentSession import in instance-shell2.tsx These changes ensure: - No duplicate models appear in the model selector - Qwen OAuth models don't duplicate with any SDK Qwen providers - TypeScript compilation passes successfully
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
||||
getSessionInfo,
|
||||
sessions,
|
||||
setActiveSession,
|
||||
setActiveParentSession,
|
||||
executeCustomCommand,
|
||||
sendMessage,
|
||||
runShellCommand,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component, createSignal, onMount, onCleanup, For, Show } from 'solid-js'
|
||||
import { Rocket, CheckCircle, XCircle, Loader, Sparkles, LogIn, LogOut, Shield, ExternalLink, Copy } from 'lucide-solid'
|
||||
import { getUserScopedKey } from '../../lib/user-storage'
|
||||
import { instances } from '../../stores/instances'
|
||||
import { fetchProviders } from '../../stores/session-api'
|
||||
|
||||
interface AntigravityModel {
|
||||
id: string
|
||||
@@ -22,19 +24,22 @@ interface AntigravityToken {
|
||||
}
|
||||
|
||||
const ANTIGRAVITY_TOKEN_KEY = "antigravity_oauth_token"
|
||||
const ANTIGRAVITY_PROJECT_KEY = "antigravity_project_id"
|
||||
|
||||
const AntigravitySettings: Component = () => {
|
||||
const [models, setModels] = createSignal<AntigravityModel[]>([])
|
||||
const [isLoading, setIsLoading] = createSignal(true)
|
||||
const [connectionStatus, setConnectionStatus] = createSignal<'idle' | 'testing' | 'connected' | 'failed'>('idle')
|
||||
const [connectionIssue, setConnectionIssue] = createSignal<{ title: string; message: string; link?: string } | null>(null)
|
||||
const [authStatus, setAuthStatus] = createSignal<'unknown' | 'authenticated' | 'unauthenticated'>('unknown')
|
||||
const [error, setError] = createSignal<string | null>(null)
|
||||
const [projectId, setProjectId] = createSignal("")
|
||||
|
||||
// Device auth state
|
||||
const [isAuthenticating, setIsAuthenticating] = createSignal(false)
|
||||
const [deviceAuthSession, setDeviceAuthSession] = createSignal<{
|
||||
sessionId: string
|
||||
userCode: string
|
||||
userCode?: string
|
||||
verificationUrl: string
|
||||
} | null>(null)
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
@@ -43,6 +48,10 @@ const AntigravitySettings: Component = () => {
|
||||
|
||||
// Check stored token on mount
|
||||
onMount(async () => {
|
||||
const storedProjectId = window.localStorage.getItem(getUserScopedKey(ANTIGRAVITY_PROJECT_KEY))
|
||||
if (storedProjectId) {
|
||||
setProjectId(storedProjectId)
|
||||
}
|
||||
checkAuthStatus()
|
||||
await loadModels()
|
||||
await testConnection()
|
||||
@@ -72,6 +81,48 @@ const AntigravitySettings: Component = () => {
|
||||
return Date.now() < expiresAt
|
||||
}
|
||||
|
||||
const parseSubscriptionIssue = (raw: string | null | undefined) => {
|
||||
if (!raw) return null
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
const errorPayload = parsed?.error
|
||||
const message = typeof errorPayload?.message === "string" ? errorPayload.message : raw
|
||||
const details = Array.isArray(errorPayload?.details) ? errorPayload.details : []
|
||||
const reason = details.find((entry: any) => entry?.reason)?.reason
|
||||
const helpLink = details
|
||||
.flatMap((entry: any) => Array.isArray(entry?.links) ? entry.links : [])
|
||||
.find((link: any) => typeof link?.url === "string")?.url
|
||||
|
||||
if (reason === "SUBSCRIPTION_REQUIRED" || /Gemini Code Assist license/i.test(message)) {
|
||||
return {
|
||||
title: "Subscription required",
|
||||
message,
|
||||
link: helpLink
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (/SUBSCRIPTION_REQUIRED/i.test(raw) || /Gemini Code Assist license/i.test(raw)) {
|
||||
return {
|
||||
title: "Subscription required",
|
||||
message: raw
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const token = getStoredToken()
|
||||
const headers: Record<string, string> = {}
|
||||
if (token?.access_token && isTokenValid(token)) {
|
||||
headers.Authorization = `Bearer ${token.access_token}`
|
||||
}
|
||||
if (projectId()) {
|
||||
headers["X-Antigravity-Project"] = projectId()
|
||||
}
|
||||
return Object.keys(headers).length > 0 ? headers : undefined
|
||||
}
|
||||
|
||||
const checkAuthStatus = () => {
|
||||
const token = getStoredToken()
|
||||
if (isTokenValid(token)) {
|
||||
@@ -84,7 +135,9 @@ const AntigravitySettings: Component = () => {
|
||||
const loadModels = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/antigravity/models')
|
||||
const response = await fetch('/api/antigravity/models', {
|
||||
headers: getAuthHeaders()
|
||||
})
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setModels(data.models || [])
|
||||
@@ -102,12 +155,24 @@ const AntigravitySettings: Component = () => {
|
||||
|
||||
const testConnection = async () => {
|
||||
setConnectionStatus('testing')
|
||||
setConnectionIssue(null)
|
||||
try {
|
||||
const response = await fetch('/api/antigravity/test')
|
||||
const response = await fetch('/api/antigravity/test', {
|
||||
headers: getAuthHeaders()
|
||||
})
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setConnectionStatus(data.connected ? 'connected' : 'failed')
|
||||
const issue = parseSubscriptionIssue(data.error)
|
||||
if (issue) {
|
||||
setConnectionIssue(issue)
|
||||
}
|
||||
} else {
|
||||
const errorText = await response.text().catch(() => "")
|
||||
const issue = parseSubscriptionIssue(errorText)
|
||||
if (issue) {
|
||||
setConnectionIssue(issue)
|
||||
}
|
||||
setConnectionStatus('failed')
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -115,6 +180,8 @@ const AntigravitySettings: Component = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const offlineLabel = () => connectionIssue()?.title ?? "Offline"
|
||||
|
||||
// Start device authorization flow
|
||||
const startDeviceAuth = async () => {
|
||||
setIsAuthenticating(true)
|
||||
@@ -127,12 +194,14 @@ const AntigravitySettings: Component = () => {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(errorData.error || errorData.details || 'Failed to start authentication')
|
||||
const base = errorData.error || 'Failed to start authentication'
|
||||
const details = errorData.details ? ` - ${errorData.details}` : ''
|
||||
throw new Error(`${base}${details}`)
|
||||
}
|
||||
|
||||
const data = await response.json() as {
|
||||
sessionId: string
|
||||
userCode: string
|
||||
userCode?: string
|
||||
verificationUrl: string
|
||||
expiresIn: number
|
||||
interval: number
|
||||
@@ -140,7 +209,7 @@ const AntigravitySettings: Component = () => {
|
||||
|
||||
setDeviceAuthSession({
|
||||
sessionId: data.sessionId,
|
||||
userCode: data.userCode,
|
||||
userCode: data.userCode || "",
|
||||
verificationUrl: data.verificationUrl
|
||||
})
|
||||
|
||||
@@ -210,6 +279,14 @@ const AntigravitySettings: Component = () => {
|
||||
setAuthStatus('authenticated')
|
||||
setError(null)
|
||||
loadModels()
|
||||
await testConnection()
|
||||
for (const instance of instances().values()) {
|
||||
try {
|
||||
await fetchProviders(instance.id)
|
||||
} catch (refreshError) {
|
||||
console.error(`Failed to refresh providers for instance ${instance.id}:`, refreshError)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -254,11 +331,18 @@ const AntigravitySettings: Component = () => {
|
||||
const signOut = () => {
|
||||
window.localStorage.removeItem(getUserScopedKey(ANTIGRAVITY_TOKEN_KEY))
|
||||
setAuthStatus('unauthenticated')
|
||||
setConnectionIssue(null)
|
||||
setConnectionStatus('idle')
|
||||
for (const instance of instances().values()) {
|
||||
fetchProviders(instance.id).catch((refreshError) => {
|
||||
console.error(`Failed to refresh providers for instance ${instance.id}:`, refreshError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const copyCode = async () => {
|
||||
const session = deviceAuthSession()
|
||||
if (session) {
|
||||
if (session?.userCode) {
|
||||
await navigator.clipboard.writeText(session.userCode)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
@@ -308,7 +392,7 @@ const AntigravitySettings: Component = () => {
|
||||
{connectionStatus() === 'failed' && (
|
||||
<span class="flex items-center gap-2 text-sm text-red-400">
|
||||
<XCircle class="w-4 h-4" />
|
||||
Offline
|
||||
{offlineLabel()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -386,21 +470,30 @@ const AntigravitySettings: Component = () => {
|
||||
<Show when={deviceAuthSession()}>
|
||||
<div class="bg-purple-500/10 border border-purple-500/30 rounded-lg p-4 space-y-4">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-zinc-300 mb-3">
|
||||
Enter this code on the Google sign-in page:
|
||||
</p>
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<code class="px-6 py-3 bg-zinc-900 rounded-lg text-2xl font-mono font-bold text-white tracking-widest">
|
||||
{deviceAuthSession()?.userCode}
|
||||
</code>
|
||||
<button
|
||||
onClick={copyCode}
|
||||
class="p-2 text-zinc-400 hover:text-white bg-zinc-800 hover:bg-zinc-700 rounded-lg transition-colors"
|
||||
title="Copy code"
|
||||
>
|
||||
{copied() ? <CheckCircle class="w-5 h-5 text-emerald-400" /> : <Copy class="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
<Show
|
||||
when={Boolean(deviceAuthSession()?.userCode)}
|
||||
fallback={
|
||||
<p class="text-sm text-zinc-300">
|
||||
Complete the sign-in in the browser window.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<p class="text-sm text-zinc-300 mb-3">
|
||||
Enter this code on the Google sign-in page:
|
||||
</p>
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<code class="px-6 py-3 bg-zinc-900 rounded-lg text-2xl font-mono font-bold text-white tracking-widest">
|
||||
{deviceAuthSession()?.userCode}
|
||||
</code>
|
||||
<button
|
||||
onClick={copyCode}
|
||||
class="p-2 text-zinc-400 hover:text-white bg-zinc-800 hover:bg-zinc-700 rounded-lg transition-colors"
|
||||
title="Copy code"
|
||||
>
|
||||
{copied() ? <CheckCircle class="w-5 h-5 text-emerald-400" /> : <Copy class="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center gap-2 text-sm text-purple-300">
|
||||
@@ -428,6 +521,38 @@ const AntigravitySettings: Component = () => {
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2 text-sm text-zinc-400">
|
||||
<label class="text-xs uppercase tracking-wide text-zinc-500">Project ID (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={projectId()}
|
||||
onInput={(event) => {
|
||||
const value = event.currentTarget.value.trim()
|
||||
setProjectId(value)
|
||||
if (typeof window !== "undefined") {
|
||||
const key = getUserScopedKey(ANTIGRAVITY_PROJECT_KEY)
|
||||
if (value) {
|
||||
window.localStorage.setItem(key, value)
|
||||
} else {
|
||||
window.localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
}}
|
||||
class="w-full bg-zinc-900/70 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:ring-2 focus:ring-purple-500/50"
|
||||
placeholder="e.g. my-gcp-project-id"
|
||||
/>
|
||||
<p class="text-xs text-zinc-500">
|
||||
Set this only if your account is tied to a specific Code Assist project.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => testConnection()}
|
||||
class="w-fit px-3 py-1.5 text-xs bg-zinc-800 hover:bg-zinc-700 rounded-lg text-zinc-200"
|
||||
>
|
||||
Re-check connection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
@@ -437,6 +562,23 @@ const AntigravitySettings: Component = () => {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={connectionIssue()}>
|
||||
<div class="p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg text-amber-200 text-sm space-y-2">
|
||||
<div class="font-semibold">{connectionIssue()?.title}</div>
|
||||
<div>{connectionIssue()?.message}</div>
|
||||
<Show when={connectionIssue()?.link}>
|
||||
<a
|
||||
href={connectionIssue()?.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-2 text-amber-300 hover:text-amber-200 underline"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Models Grid */}
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
@@ -331,20 +331,32 @@ async function fetchExtraProviders(): Promise<Provider[]> {
|
||||
}
|
||||
|
||||
function removeDuplicateProviders(base: Provider[], extras: Provider[]): Provider[] {
|
||||
// Collect all extra provider IDs and model IDs to prevent duplicates
|
||||
const extraProviderIds = new Set(extras.map((provider) => provider.id))
|
||||
const extraModelIds = new Set(extras.flatMap((provider) => provider.models.map((model) => model.id)))
|
||||
if (!extras.some((provider) => provider.id === "opencode-zen")) {
|
||||
return base
|
||||
}
|
||||
|
||||
return base.filter((provider) => {
|
||||
if (provider.id === "opencode-zen") return false
|
||||
if (provider.id === "opencode" && provider.models.every((model) => extraModelIds.has(model.id))) {
|
||||
// Remove base providers that have the same ID as an extra provider
|
||||
// This prevents qwen-oauth, zai, ollama-cloud, antigravity duplicates
|
||||
if (extraProviderIds.has(provider.id)) {
|
||||
return false
|
||||
}
|
||||
// Special case: remove opencode if opencode-zen is present and covers all models
|
||||
if (provider.id === "opencode" && extraProviderIds.has("opencode-zen") &&
|
||||
provider.models.every((model) => extraModelIds.has(model.id))) {
|
||||
return false
|
||||
}
|
||||
// Remove any qwen-related SDK providers when qwen-oauth is present
|
||||
if (extraProviderIds.has("qwen-oauth") &&
|
||||
(provider.id.toLowerCase().includes("qwen") ||
|
||||
provider.models.some((m) => m.id.toLowerCase().includes("qwen")))) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
interface SessionForkResponse {
|
||||
id: string
|
||||
title?: string
|
||||
|
||||
Reference in New Issue
Block a user