fix: Replace broken web OAuth with manual token entry for Antigravity
Some checks failed
Release Binaries / release (push) Has been cancelled
Some checks failed
Release Binaries / release (push) Has been cancelled
- Removed Google OAuth popup flow (invalid_client error) - Added manual access token input option - Added instructions to use SDK mode with antigravity plugin (recommended) - Added copy button for CLI command
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Component, createSignal, onMount, For, Show } from 'solid-js'
|
||||
import { Rocket, CheckCircle, XCircle, Loader, Sparkles, LogIn, LogOut, Shield } from 'lucide-solid'
|
||||
import { Rocket, CheckCircle, XCircle, Loader, Sparkles, Key, LogOut, Shield, Terminal, Copy, ExternalLink } from 'lucide-solid'
|
||||
import { getUserScopedKey } from '../../lib/user-storage'
|
||||
|
||||
interface AntigravityModel {
|
||||
@@ -22,15 +22,16 @@ interface AntigravityToken {
|
||||
}
|
||||
|
||||
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)
|
||||
const [showTokenInput, setShowTokenInput] = createSignal(false)
|
||||
const [tokenInput, setTokenInput] = createSignal('')
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
|
||||
// Check stored token on mount
|
||||
onMount(async () => {
|
||||
@@ -100,104 +101,45 @@ const AntigravitySettings: Component = () => {
|
||||
}
|
||||
}
|
||||
|
||||
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.')
|
||||
const handleTokenSubmit = () => {
|
||||
const token = tokenInput().trim()
|
||||
if (!token) {
|
||||
setError('Please enter a valid token')
|
||||
return
|
||||
}
|
||||
|
||||
// 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,
|
||||
const tokenData: AntigravityToken = {
|
||||
access_token: token,
|
||||
expires_in: 3600, // 1 hour default
|
||||
created_at: Date.now()
|
||||
}
|
||||
|
||||
window.localStorage.setItem(
|
||||
getUserScopedKey(ANTIGRAVITY_TOKEN_KEY),
|
||||
JSON.stringify(token)
|
||||
JSON.stringify(tokenData)
|
||||
)
|
||||
|
||||
popup.close()
|
||||
clearInterval(checkClosed)
|
||||
setIsAuthenticating(false)
|
||||
setAuthStatus('authenticated')
|
||||
setShowTokenInput(false)
|
||||
setTokenInput('')
|
||||
setError(null)
|
||||
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)
|
||||
} catch (err) {
|
||||
setError('Failed to save token')
|
||||
}
|
||||
}
|
||||
|
||||
const signOut = () => {
|
||||
window.localStorage.removeItem(getUserScopedKey(ANTIGRAVITY_TOKEN_KEY))
|
||||
setAuthStatus('unauthenticated')
|
||||
setModels([])
|
||||
}
|
||||
|
||||
const copyCommand = async () => {
|
||||
const command = 'npx opencode-antigravity-auth login'
|
||||
await navigator.clipboard.writeText(command)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
@@ -223,7 +165,7 @@ const AntigravitySettings: Component = () => {
|
||||
</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>
|
||||
<p class="text-sm text-zinc-400">Premium models via Google authentication</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -254,66 +196,118 @@ const AntigravitySettings: Component = () => {
|
||||
<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>
|
||||
<h3 class="font-semibold text-purple-300 mb-1">Premium AI Models</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.
|
||||
and GPT-OSS 120B through Google's infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Authentication Section */}
|
||||
<div class="bg-zinc-900/50 border border-zinc-800 rounded-xl p-4">
|
||||
<div class="bg-zinc-900/50 border border-zinc-800 rounded-xl p-4 space-y-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>
|
||||
<h4 class="font-medium text-white">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'}
|
||||
? 'Token configured - you can use Antigravity models'
|
||||
: 'Configure authentication 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
|
||||
Configured
|
||||
</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
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={authStatus() === 'unauthenticated'}>
|
||||
{/* SDK Mode Instructions */}
|
||||
<div class="bg-blue-500/5 border border-blue-500/20 rounded-lg p-4 space-y-3">
|
||||
<div class="flex items-center gap-2 text-blue-400">
|
||||
<Terminal class="w-4 h-4" />
|
||||
<span class="font-medium text-sm">Recommended: Use OpenCode SDK Mode</span>
|
||||
</div>
|
||||
<p class="text-xs text-zinc-400">
|
||||
For the best experience, switch to OpenCode SDK mode in General settings.
|
||||
The Antigravity plugin handles authentication automatically via Google OAuth.
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="flex-1 px-3 py-2 bg-zinc-900 rounded text-xs font-mono text-zinc-300">
|
||||
npx opencode-antigravity-auth login
|
||||
</code>
|
||||
<button
|
||||
onClick={copyCommand}
|
||||
class="p-2 text-zinc-400 hover:text-white bg-zinc-800 hover:bg-zinc-700 rounded transition-colors"
|
||||
title="Copy command"
|
||||
>
|
||||
{copied() ? <CheckCircle class="w-4 h-4 text-emerald-400" /> : <Copy class="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Manual Token Entry */}
|
||||
<div class="border-t border-zinc-800 pt-4">
|
||||
<Show when={!showTokenInput()}>
|
||||
<button
|
||||
onClick={() => setShowTokenInput(true)}
|
||||
class="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors"
|
||||
>
|
||||
<Key class="w-4 h-4" />
|
||||
Or enter access token manually...
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
<Show when={showTokenInput()}>
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm font-medium text-zinc-300">
|
||||
Access Token
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="password"
|
||||
value={tokenInput()}
|
||||
onInput={(e) => setTokenInput(e.currentTarget.value)}
|
||||
placeholder="Paste your Google OAuth access token..."
|
||||
class="flex-1 px-3 py-2 bg-zinc-900 border border-zinc-700 rounded-lg text-sm text-white placeholder-zinc-500 focus:outline-none focus:border-purple-500"
|
||||
/>
|
||||
<button
|
||||
onClick={handleTokenSubmit}
|
||||
class="px-4 py-2 bg-purple-600 hover:bg-purple-500 text-white rounded-lg text-sm transition-colors"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setShowTokenInput(false); setTokenInput(''); }}
|
||||
class="px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded-lg text-sm transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-zinc-500">
|
||||
You can get an access token by running the Antigravity auth plugin in a terminal,
|
||||
or by extracting it from an authenticated Google Cloud session.
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
@@ -391,21 +385,9 @@ const AntigravitySettings: Component = () => {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!isLoading() && models().length === 0 && authStatus() === 'authenticated'}>
|
||||
<Show when={!isLoading() && models().length === 0}>
|
||||
<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>
|
||||
<p>Models will be available after configuration.</p>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -414,13 +396,26 @@ const AntigravitySettings: Component = () => {
|
||||
<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>• <strong>SDK Mode (Recommended):</strong> Switch to OpenCode binary mode and install the antigravity auth plugin</li>
|
||||
<li>• <strong>Native Mode:</strong> Enter your access token manually 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>
|
||||
|
||||
{/* Plugin Link */}
|
||||
<div class="flex justify-center">
|
||||
<a
|
||||
href="https://github.com/NoeFabris/opencode-antigravity-auth"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center gap-2 text-sm text-purple-400 hover:text-purple-300 transition-colors"
|
||||
>
|
||||
<ExternalLink class="w-4 h-4" />
|
||||
View Antigravity Auth Plugin on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user