import { useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { RefreshCw, X, CodeXml } from 'lucide-react'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { cn } from '../../utils/cn'; import { request as invoke } from '../../utils/request'; import { showToast } from '../common/ToastContainer'; import { useProxyModels } from '../../hooks/useProxyModels'; import { SortableModelItem, type PreviewModelEntry } from './SortableModelItem'; interface OpenCodeSyncModalProps { proxyUrl: string; apiKey: string; getFormattedProxyUrl: (app: 'Claude' | 'Codex' | 'Gemini' | 'OpenCode' | 'Droid') => string; onClose: () => void; onSyncDone: () => void; } export function OpenCodeSyncModal({ proxyUrl, apiKey, onClose, onSyncDone }: OpenCodeSyncModalProps) { const { t } = useTranslation(); const { models: antigravityModels } = useProxyModels(); const [selectedModels, setSelectedModels] = useState>(new Set()); const [previewModels, setPreviewModels] = useState([]); const [syncing, setSyncing] = useState(false); const [configLoaded, setConfigLoaded] = useState(false); const [hasAuthPlugin, setHasAuthPlugin] = useState(false); const [customBaseUrl, setCustomBaseUrl] = useState(proxyUrl); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 3 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), ); const rebuildPreview = useCallback((selectedIds: Set) => { const selected = antigravityModels.filter(m => selectedIds.has(m.id)); const newEntries: PreviewModelEntry[] = selected.map((m, i) => ({ _uid: `new-${i}`, model: m.id, id: m.id, index: i, baseUrl: '', // OpenCode uses provider-level base URL apiKey: apiKey, displayName: m.name, noImageSupport: false, provider: m.id.includes('claude') ? 'anthropic' : 'google', isAg: true, })); setPreviewModels(newEntries); }, [antigravityModels, apiKey]); // 初始加载 opencode.json if (!configLoaded) { setConfigLoaded(true); invoke('get_opencode_config_content', { request: { fileName: 'opencode.json' } }) .then(content => { const parsed = JSON.parse(content); const existingModelIds = new Set(); // Priority 1: Read from antigravity-manager provider if (parsed.provider?.['antigravity-manager']?.models) { Object.keys(parsed.provider['antigravity-manager'].models).forEach(k => existingModelIds.add(k)); } // Fallback: legacy anthropic/google providers if (existingModelIds.size === 0) { if (parsed.provider?.anthropic?.models) { Object.keys(parsed.provider.anthropic.models).forEach(k => existingModelIds.add(k)); } if (parsed.provider?.google?.models) { Object.keys(parsed.provider.google.models).forEach(k => existingModelIds.add(k)); } } // Detect auth plugin conflict const plugins = parsed.plugin || []; const hasAuth = plugins.some((p: string) => p.includes('opencode-antigravity-auth')); setHasAuthPlugin(hasAuth); // Try to extract existing baseURL from antigravity-manager provider if (parsed.provider?.['antigravity-manager']?.options?.baseURL) { setCustomBaseUrl(parsed.provider['antigravity-manager'].options.baseURL); } setSelectedModels(existingModelIds); rebuildPreview(existingModelIds); }) .catch(() => rebuildPreview(new Set())); } const allSelected = antigravityModels.length > 0 && antigravityModels.every(m => selectedModels.has(m.id)); const toggleAll = () => { const next = allSelected ? new Set() : new Set(antigravityModels.map(m => m.id)); setSelectedModels(next); rebuildPreview(next); }; const toggleModel = (modelId: string) => { const next = new Set(selectedModels); if (next.has(modelId)) next.delete(modelId); else next.add(modelId); setSelectedModels(next); rebuildPreview(next); }; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIdx = previewModels.findIndex(m => m._uid === active.id); const newIdx = previewModels.findIndex(m => m._uid === over.id); if (oldIdx < 0 || newIdx < 0) return; setPreviewModels(arrayMove([...previewModels], oldIdx, newIdx).map((m, i) => ({ ...m, index: i, }))); }; const handleRemoveModel = (uid: string) => { const nextPreviews = previewModels.filter(m => m._uid !== uid); setPreviewModels(nextPreviews); const nextSelected = new Set(nextPreviews.map(p => p.model)); setSelectedModels(nextSelected); }; const executeOpenCodeSync = async () => { setSyncing(true); try { const models = previewModels.map(m => m.model); await invoke('execute_opencode_sync', { proxyUrl: customBaseUrl || proxyUrl, apiKey, syncAccounts: true, models }); showToast(t('proxy.opencode_sync.toast.sync_success', { defaultValue: 'OpenCode 同步成功' }), 'success'); onSyncDone(); onClose(); } catch (error: any) { showToast(error.toString(), 'error'); } finally { setSyncing(false); } }; const groups = [...new Set(antigravityModels.map(m => m.group))]; return (
{/* Header */}

{t('proxy.config.opencode_sync.modal_title', { defaultValue: '选择 OpenCode 模型' })}

~/.config/opencode/opencode.json

{/* Custom BaseURL Input */}
{t('proxy.config.opencode_sync.custom_base_url_desc', { defaultValue: 'For Docker Compose networking' })}
setCustomBaseUrl(e.target.value)} placeholder="e.g. http://antigravity-manager:8045/v1" className="w-full px-3 py-1.5 text-xs bg-white dark:bg-base-100 border border-gray-200 dark:border-base-300 rounded-lg focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all" /> {customBaseUrl !== proxyUrl && ( )}
{/* 模型选择区 */}
{t('proxy.config.opencode_sync.select_models', { defaultValue: '选择要同步的模型' })} {selectedModels.size}/{antigravityModels.length}
{groups.map(group => { const groupModels = antigravityModels.filter(m => m.group === group); return (
{group}
{groupModels.map(m => { const selected = selectedModels.has(m.id); return ( ); })}
); })}
{/* Auth Plugin Warning */} {hasAuthPlugin && (

{t('proxy.config.opencode_sync.auth_plugin_warning', { defaultValue: 'Sync chỉ tạo provider antigravity-manager và không ghi đè google provider/plugin.' })}

)} {/* Preview 主体区 */}
Sync Queue Preview {previewModels.length} models
m._uid)} strategy={verticalListSortingStrategy}>
{previewModels.map(entry => ( { }} onRemove={() => handleRemoveModel(entry._uid)} /> ))}
{/* Footer */}
); }