feat: Add Antigravity provider integration + fix native mode startup
Some checks failed
Release Binaries / release (push) Has been cancelled

- Added Antigravity AI provider with Google OAuth authentication
- New integration client (antigravity.ts) with automatic endpoint fallback
- API routes for /api/antigravity/* (models, auth-status, test, chat)
- AntigravitySettings.tsx for Advanced Settings panel
- Updated session-api.ts and session-actions.ts for provider routing
- Updated opencode.jsonc with Antigravity plugin and 11 models:
  - Gemini 3 Pro Low/High, Gemini 3 Flash
  - Claude Sonnet 4.5 (+ thinking variants)
  - Claude Opus 4.5 (+ thinking variants)
  - GPT-OSS 120B Medium

- Fixed native mode startup error (was trying to launch __nomadarch_native__ as binary)
- Native mode workspaces now skip binary launch and are immediately ready
This commit is contained in:
Gemini AI
2025-12-27 04:01:38 +04:00
Unverified
parent 4aa4795d4b
commit bb1c0d81f2
10 changed files with 1747 additions and 9 deletions

View File

@@ -0,0 +1,428 @@
import { Component, createSignal, onMount, For, Show } from 'solid-js'
import { Rocket, CheckCircle, XCircle, Loader, Sparkles, LogIn, LogOut, Shield } from 'lucide-solid'
import { getUserScopedKey } from '../../lib/user-storage'
interface AntigravityModel {
id: string
name: string
family?: string
reasoning?: boolean
tool_call?: boolean
limit?: {
context: number
output: number
}
}
interface AntigravityToken {
access_token: string
refresh_token?: string
expires_in: number
created_at: number
}
const ANTIGRAVITY_TOKEN_KEY = "antigravity_oauth_token"
const GOOGLE_OAUTH_CLIENT_ID = "782068742485-pf45b4gldtk7q847g3v5ercqfl31nkud.apps.googleusercontent.com" // Antigravity/Gemini CLI client
const AntigravitySettings: Component = () => {
const [models, setModels] = createSignal<AntigravityModel[]>([])
const [isLoading, setIsLoading] = createSignal(true)
const [connectionStatus, setConnectionStatus] = createSignal<'idle' | 'testing' | 'connected' | 'failed'>('idle')
const [authStatus, setAuthStatus] = createSignal<'unknown' | 'authenticated' | 'unauthenticated'>('unknown')
const [isAuthenticating, setIsAuthenticating] = createSignal(false)
const [error, setError] = createSignal<string | null>(null)
// Check stored token on mount
onMount(async () => {
checkAuthStatus()
await loadModels()
await testConnection()
})
const getStoredToken = (): AntigravityToken | null => {
if (typeof window === "undefined") return null
try {
const raw = window.localStorage.getItem(getUserScopedKey(ANTIGRAVITY_TOKEN_KEY))
if (!raw) return null
return JSON.parse(raw)
} catch {
return null
}
}
const isTokenValid = (token: AntigravityToken | null): boolean => {
if (!token) return false
const createdAt = token.created_at > 1e12 ? Math.floor(token.created_at / 1000) : token.created_at
const expiresAt = (createdAt + token.expires_in) * 1000 - 300000 // 5 min buffer
return Date.now() < expiresAt
}
const checkAuthStatus = () => {
const token = getStoredToken()
if (isTokenValid(token)) {
setAuthStatus('authenticated')
} else {
setAuthStatus('unauthenticated')
}
}
const loadModels = async () => {
setIsLoading(true)
try {
const response = await fetch('/api/antigravity/models')
if (response.ok) {
const data = await response.json()
setModels(data.models || [])
setError(null)
} else {
throw new Error('Failed to load models')
}
} catch (err) {
console.error('Failed to load Antigravity models:', err)
setError('Failed to load models')
} finally {
setIsLoading(false)
}
}
const testConnection = async () => {
setConnectionStatus('testing')
try {
const response = await fetch('/api/antigravity/test')
if (response.ok) {
const data = await response.json()
setConnectionStatus(data.connected ? 'connected' : 'failed')
} else {
setConnectionStatus('failed')
}
} catch (err) {
setConnectionStatus('failed')
}
}
const startGoogleOAuth = async () => {
setIsAuthenticating(true)
setError(null)
try {
// Open Google OAuth in a new window
const redirectUri = `${window.location.origin}/auth/antigravity/callback`
const scope = encodeURIComponent("openid email profile https://www.googleapis.com/auth/cloud-platform")
const state = crypto.randomUUID()
// Store state for verification
window.localStorage.setItem('antigravity_oauth_state', state)
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
`client_id=${GOOGLE_OAUTH_CLIENT_ID}&` +
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
`response_type=token&` +
`scope=${scope}&` +
`state=${state}&` +
`prompt=consent`
// Open popup
const width = 500
const height = 600
const left = (window.screen.width - width) / 2
const top = (window.screen.height - height) / 2
const popup = window.open(
authUrl,
'antigravity-oauth',
`width=${width},height=${height},left=${left},top=${top}`
)
if (!popup) {
throw new Error('Failed to open authentication window. Please allow popups.')
}
// Poll for token in URL hash
const checkClosed = setInterval(() => {
try {
if (popup.closed) {
clearInterval(checkClosed)
setIsAuthenticating(false)
checkAuthStatus()
loadModels()
} else {
// Check if we can access the popup location (same origin after redirect)
const hash = popup.location.hash
if (hash && hash.includes('access_token')) {
const params = new URLSearchParams(hash.substring(1))
const accessToken = params.get('access_token')
const expiresIn = parseInt(params.get('expires_in') || '3600', 10)
if (accessToken) {
const token: AntigravityToken = {
access_token: accessToken,
expires_in: expiresIn,
created_at: Date.now()
}
window.localStorage.setItem(
getUserScopedKey(ANTIGRAVITY_TOKEN_KEY),
JSON.stringify(token)
)
popup.close()
clearInterval(checkClosed)
setIsAuthenticating(false)
setAuthStatus('authenticated')
loadModels()
}
}
}
} catch (e) {
// Cross-origin error - popup is on Google's domain, still waiting
}
}, 500)
// Cleanup after 5 minutes
setTimeout(() => {
clearInterval(checkClosed)
if (!popup.closed) {
popup.close()
}
setIsAuthenticating(false)
}, 300000)
} catch (err: any) {
console.error('OAuth error:', err)
setError(err.message || 'Authentication failed')
setIsAuthenticating(false)
}
}
const signOut = () => {
window.localStorage.removeItem(getUserScopedKey(ANTIGRAVITY_TOKEN_KEY))
setAuthStatus('unauthenticated')
setModels([])
}
const formatNumber = (num: number): string => {
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`
return num.toString()
}
const getModelFamily = (model: AntigravityModel): { label: string; color: string } => {
if (model.id.startsWith('gemini')) return { label: 'Gemini', color: 'bg-blue-500/20 text-blue-400' }
if (model.id.startsWith('claude')) return { label: 'Claude', color: 'bg-orange-500/20 text-orange-400' }
if (model.id.startsWith('gpt')) return { label: 'GPT', color: 'bg-green-500/20 text-green-400' }
return { label: model.family || 'Other', color: 'bg-zinc-700 text-zinc-400' }
}
return (
<div class="space-y-6 p-6">
{/* Header */}
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 bg-gradient-to-br from-purple-500/20 to-blue-500/20 rounded-lg">
<Rocket class="w-6 h-6 text-purple-400" />
</div>
<div>
<h2 class="text-xl font-semibold text-white">Antigravity</h2>
<p class="text-sm text-zinc-400">Premium models via Google OAuth</p>
</div>
</div>
<div class="flex items-center gap-2">
{connectionStatus() === 'testing' && (
<span class="flex items-center gap-2 text-sm text-zinc-400">
<Loader class="w-4 h-4 animate-spin" />
Testing...
</span>
)}
{connectionStatus() === 'connected' && (
<span class="flex items-center gap-2 text-sm text-emerald-400">
<CheckCircle class="w-4 h-4" />
Connected
</span>
)}
{connectionStatus() === 'failed' && (
<span class="flex items-center gap-2 text-sm text-red-400">
<XCircle class="w-4 h-4" />
Offline
</span>
)}
</div>
</div>
{/* Info Banner */}
<div class="bg-gradient-to-r from-purple-500/10 via-blue-500/10 to-purple-500/10 border border-purple-500/20 rounded-xl p-4">
<div class="flex items-start gap-3">
<Sparkles class="w-5 h-5 text-purple-400 mt-0.5" />
<div>
<h3 class="font-semibold text-purple-300 mb-1">Premium AI Models via Google</h3>
<p class="text-sm text-zinc-300">
Antigravity provides access to Gemini 3 Pro/Flash, Claude Sonnet 4.5, Claude Opus 4.5,
and GPT-OSS 120B through Google's rate limits. Sign in with your Google account to get started.
</p>
</div>
</div>
</div>
{/* Authentication Section */}
<div class="bg-zinc-900/50 border border-zinc-800 rounded-xl p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Shield class="w-5 h-5 text-zinc-400" />
<div>
<h4 class="font-medium text-white">Google OAuth Authentication</h4>
<p class="text-xs text-zinc-500">
{authStatus() === 'authenticated'
? 'You are signed in and can use Antigravity models'
: 'Sign in with Google to access premium models'}
</p>
</div>
</div>
<Show when={authStatus() === 'unauthenticated'}>
<button
onClick={startGoogleOAuth}
disabled={isAuthenticating()}
class="flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-500 disabled:bg-purple-600/50 text-white rounded-lg transition-colors"
>
{isAuthenticating() ? (
<>
<Loader class="w-4 h-4 animate-spin" />
Signing in...
</>
) : (
<>
<LogIn class="w-4 h-4" />
Sign in with Google
</>
)}
</button>
</Show>
<Show when={authStatus() === 'authenticated'}>
<div class="flex items-center gap-3">
<span class="flex items-center gap-2 px-3 py-1.5 bg-emerald-500/20 text-emerald-400 rounded-lg text-sm">
<CheckCircle class="w-4 h-4" />
Authenticated
</span>
<button
onClick={signOut}
class="flex items-center gap-2 px-3 py-1.5 text-sm text-zinc-400 hover:text-white bg-zinc-800 hover:bg-zinc-700 rounded-lg transition-colors"
>
<LogOut class="w-4 h-4" />
Sign out
</button>
</div>
</Show>
</div>
</div>
{/* Error Display */}
<Show when={error()}>
<div class="p-4 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
{error()}
</div>
</Show>
{/* Models Grid */}
<div class="space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-white">Available Models</h3>
<button
onClick={loadModels}
disabled={isLoading()}
class="flex items-center gap-2 px-3 py-1.5 text-sm bg-zinc-800 hover:bg-zinc-700 rounded-lg transition-colors"
>
{isLoading() ? <Loader class="w-4 h-4 animate-spin" /> : null}
Refresh
</button>
</div>
<Show when={isLoading()}>
<div class="flex items-center justify-center py-12">
<div class="flex items-center gap-3 text-zinc-400">
<Loader class="w-6 h-6 animate-spin" />
<span>Loading models...</span>
</div>
</div>
</Show>
<Show when={!isLoading() && models().length > 0}>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<For each={models()}>
{(model) => {
const family = getModelFamily(model)
return (
<div class="group bg-zinc-900/50 border border-zinc-800 hover:border-purple-500/50 rounded-xl p-4 transition-all">
<div class="flex items-start justify-between mb-3">
<div>
<h4 class="font-semibold text-white group-hover:text-purple-300 transition-colors">
{model.name}
</h4>
<p class="text-xs text-zinc-500 font-mono">{model.id}</p>
</div>
<span class={`px-2 py-0.5 text-[10px] font-bold uppercase rounded ${family.color}`}>
{family.label}
</span>
</div>
<div class="flex flex-wrap gap-2 mb-3">
{model.reasoning && (
<span class="px-2 py-0.5 text-[10px] bg-purple-500/20 text-purple-400 rounded">
Thinking
</span>
)}
{model.tool_call && (
<span class="px-2 py-0.5 text-[10px] bg-blue-500/20 text-blue-400 rounded">
Tool Use
</span>
)}
</div>
{model.limit && (
<div class="flex items-center gap-4 text-xs text-zinc-500">
<span>Context: {formatNumber(model.limit.context)}</span>
<span>Output: {formatNumber(model.limit.output)}</span>
</div>
)}
</div>
)
}}
</For>
</div>
</Show>
<Show when={!isLoading() && models().length === 0 && authStatus() === 'authenticated'}>
<div class="text-center py-12 text-zinc-500">
<p>No models available at this time.</p>
<button
onClick={loadModels}
class="mt-4 px-4 py-2 text-sm bg-purple-500/20 text-purple-400 hover:bg-purple-500/30 rounded-lg transition-colors"
>
Try Again
</button>
</div>
</Show>
<Show when={!isLoading() && models().length === 0 && authStatus() === 'unauthenticated'}>
<div class="text-center py-12 text-zinc-500">
<p>Sign in with Google to see available models.</p>
</div>
</Show>
</div>
{/* Usage Info */}
<div class="bg-zinc-900/50 border border-zinc-800 rounded-xl p-4">
<h4 class="font-medium text-white mb-2">How to Use</h4>
<ul class="text-sm text-zinc-400 space-y-1">
<li>• Sign in with your Google account above</li>
<li>• Select any Antigravity model from the model picker in chat</li>
<li>• Models include Gemini 3, Claude Sonnet/Opus 4.5, and GPT-OSS</li>
<li>• Thinking-enabled models show step-by-step reasoning</li>
<li>• Uses Google's rate limits for maximum throughput</li>
</ul>
</div>
</div>
)
}
export default AntigravitySettings