AG X v2.0.3 - Antigravity fork with multi-provider support

Features:
- Welcome screen on first run (provider choice before LS starts)
- 15+ AI providers (Google Gemini, OpenAI, Anthropic, DeepSeek, Ollama, etc.)
- Provider config syncs to endpoints.json for translation proxy
- Built-in Node.js translation proxy for non-native backends
- Auto-update support, tray integration, URI scheme handler
This commit is contained in:
admin
2026-05-22 23:20:10 +04:00
Unverified
commit 43e2a2f78f
46 changed files with 7719 additions and 0 deletions

696
dist/provider/settings.html vendored Normal file
View File

@@ -0,0 +1,696 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Provider Settings</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e; color: #e0e0e0;
min-height: 100vh; padding: 0;
}
.header {
background: linear-gradient(135deg, #16213e 0%, #0f3460 100%);
padding: 20px 32px; border-bottom: 1px solid #2a2a4a;
display: flex; align-items: center; justify-content: space-between;
-webkit-app-region: drag;
}
.header h1 { font-size: 20px; font-weight: 600; color: #fff; }
.header .subtitle { font-size: 12px; color: #8892b0; margin-top: 2px; }
.header-badge {
background: #e94560; color: #fff; font-size: 10px;
padding: 2px 8px; border-radius: 10px; font-weight: 600;
}
.container { padding: 24px 32px; max-width: 900px; margin: 0 auto; }
/* Category headings */
.category-title {
font-size: 11px; font-weight: 700; color: #8892b0;
text-transform: uppercase; letter-spacing: 1px;
margin: 20px 0 10px 0; padding-bottom: 4px;
border-bottom: 1px solid #2a2a4a;
}
.provider-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px; margin-bottom: 20px;
}
.provider-card {
background: #16213e; border: 2px solid #2a2a4a; border-radius: 12px;
padding: 14px; cursor: pointer; transition: all 0.2s;
position: relative;
}
.provider-card:hover { border-color: #0f3460; transform: translateY(-1px); }
.provider-card.active { border-color: #e94560; background: #1a1a3e; }
.provider-card.active::after {
content: '✓'; position: absolute; top: 10px; right: 12px;
color: #e94560; font-size: 18px; font-weight: bold;
}
.provider-icon { font-size: 26px; margin-bottom: 6px; }
.provider-name { font-size: 13px; font-weight: 600; color: #fff; }
.provider-desc { font-size: 10px; color: #8892b0; margin-top: 3px; line-height: 1.3; }
.section {
background: #16213e; border: 1px solid #2a2a4a; border-radius: 12px;
padding: 24px; margin-bottom: 20px;
}
.section-title {
font-size: 15px; font-weight: 600; color: #fff;
margin-bottom: 16px; display: flex; align-items: center; gap: 8px;
}
.section-title .icon { font-size: 18px; }
.form-group { margin-bottom: 16px; }
.form-group label {
display: block; font-size: 12px; font-weight: 500;
color: #8892b0; margin-bottom: 6px; text-transform: uppercase;
letter-spacing: 0.5px;
}
.form-group input, .form-group select {
width: 100%; padding: 10px 14px; background: #0f1a2e;
border: 1px solid #2a2a4a; border-radius: 8px; color: #e0e0e0;
font-size: 14px; outline: none; transition: border-color 0.2s;
}
.form-group input:focus, .form-group select:focus {
border-color: #e94560;
}
.form-group input::placeholder { color: #555; }
.form-group select option { background: #16213e; color: #e0e0e0; }
.api-key-wrapper { position: relative; }
.api-key-wrapper input { padding-right: 40px; }
.toggle-visibility {
position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
background: none; border: none; color: #8892b0; cursor: pointer;
font-size: 14px;
}
.btn-row { display: flex; gap: 12px; margin-top: 24px; }
.btn {
padding: 10px 24px; border-radius: 8px; font-size: 14px;
font-weight: 600; cursor: pointer; border: none; transition: all 0.2s;
}
.btn-primary { background: #e94560; color: #fff; }
.btn-primary:hover { background: #c73650; }
.btn-secondary { background: #2a2a4a; color: #e0e0e0; }
.btn-secondary:hover { background: #3a3a5a; }
.btn-danger { background: #4a1a1a; color: #e94560; }
.btn-danger:hover { background: #5a2020; }
.status-bar {
background: #0f1a2e; border-top: 1px solid #2a2a4a;
padding: 10px 32px; display: flex; align-items: center;
justify-content: space-between; font-size: 12px; color: #8892b0;
}
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
.status-dot.connected { background: #4caf50; }
.status-dot.disconnected { background: #e94560; }
.status-dot.unknown { background: #ffa726; }
.toast {
position: fixed; bottom: 60px; left: 50%; transform: translateX(-50%);
background: #4caf50; color: #fff; padding: 10px 24px; border-radius: 8px;
font-size: 14px; font-weight: 500; opacity: 0; transition: opacity 0.3s;
pointer-events: none; z-index: 100;
}
.toast.show { opacity: 1; }
.toast.error { background: #e94560; }
.help-text { font-size: 11px; color: #666; margin-top: 4px; }
.models-container {
display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px;
}
.model-chip {
padding: 4px 12px; background: #0f1a2e; border: 1px solid #2a2a4a;
border-radius: 16px; font-size: 12px; color: #8892b0; cursor: pointer;
transition: all 0.2s;
}
.model-chip:hover { border-color: #e94560; color: #e0e0e0; }
.model-chip.selected { background: #e94560; color: #fff; border-color: #e94560; }
.test-result {
margin-top: 12px; padding: 12px; border-radius: 8px;
font-size: 13px; font-family: monospace; display: none;
}
.test-result.success { display: block; background: #1a2e1a; border: 1px solid #2e4a2e; color: #4caf50; }
.test-result.error { display: block; background: #2e1a1a; border: 1px solid #4a2e2e; color: #e94560; }
.tab-bar { display: flex; gap: 0; margin-bottom: 20px; }
.tab {
padding: 8px 20px; background: transparent; border: none;
border-bottom: 2px solid transparent; color: #8892b0;
cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.tab:hover { color: #e0e0e0; }
.tab.active { color: #e94560; border-bottom-color: #e94560; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.badge {
display: inline-block; padding: 2px 8px; border-radius: 4px;
font-size: 10px; font-weight: 600; margin-left: 6px;
}
.badge-native { background: #1a3a1a; color: #4caf50; }
.badge-proxy { background: #3a3a1a; color: #ffa726; }
.badge-cc { background: #3a1a3a; color: #ce93d8; }
</style>
</head>
<body>
<div class="header">
<div>
<h1>AI Provider Settings</h1>
<div class="subtitle">Choose and configure your AI provider — AG X supports 20+ providers</div>
</div>
<span class="header-badge">v3.7</span>
</div>
<div class="container">
<div class="tab-bar">
<button class="tab active" data-tab="providers">Provider</button>
<button class="tab" data-tab="advanced">Advanced</button>
<button class="tab" data-tab="about">About</button>
</div>
<!-- Providers Tab -->
<div class="tab-content active" id="tab-providers">
<div class="section">
<div class="section-title"><span class="icon"></span> Select AI Provider</div>
<div id="providerGridContainer"></div>
</div>
<div class="section" id="providerConfig">
<div class="section-title"><span class="icon">🔧</span> <span id="configTitle">Provider Configuration</span></div>
<div class="form-group">
<label>API Base URL</label>
<input type="text" id="apiUrl" placeholder="https://api.example.com">
<div class="help-text" id="apiUrlHelp"></div>
</div>
<div class="form-group" id="apiKeyGroup">
<label>API Key</label>
<div class="api-key-wrapper">
<input type="password" id="apiKey" placeholder="Enter your API key">
<button class="toggle-visibility" onclick="toggleKeyVisibility()">👁</button>
</div>
<div class="help-text" id="apiKeyHelp"></div>
</div>
<div class="form-group">
<label>Model</label>
<input type="text" id="modelInput" placeholder="Enter model name or select below">
<div class="models-container" id="modelChips"></div>
</div>
<div class="form-group">
<button class="btn btn-secondary" onclick="testConnection()">Test Connection</button>
<div class="test-result" id="testResult"></div>
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" onclick="saveSettings()">Save & Apply</button>
<button class="btn btn-secondary" onclick="resetToDefaults()">Reset Defaults</button>
</div>
</div>
<!-- Advanced Tab -->
<div class="tab-content" id="tab-advanced">
<div class="section">
<div class="section-title"><span class="icon">🛠️</span> Advanced Settings</div>
<div class="form-group">
<label>Proxy Port</label>
<input type="number" id="proxyPort" value="9876" min="1024" max="65535">
<div class="help-text">Port for the local API proxy (default: 9876). Restart required.</div>
</div>
<div class="form-group">
<label>Translate Proxy Path</label>
<div style="padding:10px;background:#0f1a2e;border-radius:8px;font-size:12px;color:#4caf50;">✅ Built-in Node.js translation proxy — no external tools needed</div>
<div class="help-text">Translation proxy is built into AG X for all provider types.</div>
</div>
<div class="form-group">
<label>Command Code Version</label>
<input type="text" id="ccVersion" value="0.26.8">
<div class="help-text">x-command-code-version header value (for Command Code provider only).</div>
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" onclick="saveAdvanced()">Save Advanced</button>
</div>
</div>
<!-- About Tab -->
<div class="tab-content" id="tab-about">
<div class="section">
<div class="section-title"><span class="icon"></span> About Provider System</div>
<p style="color: #8892b0; line-height: 1.6; font-size: 13px;">
AG X supports multiple AI providers through a modular provider system.<br><br>
<strong style="color:#e0e0e0;">Provider Types:</strong><br>
<strong>Native</strong> — Direct connection to the provider API (Google Gemini, OpenAI)<br>
<strong>OpenAI-Compatible</strong> — Any provider with a Chat Completions endpoint<br>
<strong>Anthropic</strong> — Claude models via the Messages API<br>
<strong>Command Code</strong> — 20+ models via Command Code's /alpha/generate API<br><br>
<strong style="color:#e0e0e0;">Translation:</strong><br>
Simple providers use a built-in lightweight proxy. Complex providers (Anthropic, Command Code)
uses the <strong>built-in Node.js proxy</strong>
for full Responses API ↔ provider API translation.<br><br>
<strong style="color:#e0e0e0;">Supported Providers:</strong><br>
Google Gemini, OpenAI, Anthropic, Z.AI, OpenCode (Zen/Go), Crof.ai, NVIDIA NIM,
Kilo.ai, Command Code, OpenRouter, OpenAdapter, DeepSeek, Ollama, Together AI, Groq, and Custom.
</p>
</div>
</div>
</div>
<div class="status-bar">
<div>
<span class="status-dot unknown" id="statusDot"></span>
<span id="statusText">Loading provider configuration…</span>
</div>
<div id="backendBadge" style="font-size:11px; color:#555;"></div>
</div>
<div class="toast" id="toast"></div>
<script>
const { ipcRenderer } = require('electron');
// Provider definitions — mirrors providerService.js
const PROVIDERS = {
google_gemini: {
name: 'Google Gemini (OAuth)', icon: '🔮',
desc: 'Built-in Gemini — zero config',
backendType: 'gemini-native', requiresApiKey: false,
apiUrl: 'https://daily-cloudcode-pa.sandbox.googleapis.com',
models: ['gemini-2.5-pro','gemini-2.5-flash','gemini-2.0-flash','gemini-3-flash-preview','gemini-3-pro-preview'],
defaultModel: 'gemini-2.5-pro',
category: 'Google',
},
openai: {
name: 'OpenAI', icon: '🟢',
desc: 'GPT models via Responses API',
backendType: 'native', requiresApiKey: true, apiKeyHint: 'sk-...',
apiUrl: 'https://api.openai.com/v1',
models: ['gpt-4o','gpt-4o-mini','o1','o1-mini','o3','o3-mini'],
defaultModel: 'gpt-4o',
category: 'Direct',
},
anthropic: {
name: 'Anthropic', icon: '🟣',
desc: 'Claude models via Messages API',
backendType: 'anthropic', requiresApiKey: true, apiKeyHint: 'sk-ant-...',
apiUrl: 'https://api.anthropic.com',
models: ['claude-sonnet-4-20250514','claude-3-5-sonnet-20241022','claude-3-5-haiku-20241022','claude-3-opus-20240229'],
defaultModel: 'claude-sonnet-4-20250514',
category: 'Direct',
},
z_ai: {
name: 'Z.AI Coding', icon: '🅩',
desc: 'GLM & Z models via Z.AI',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'Z.AI Key',
apiUrl: 'https://api.z.ai/api/coding/paas/v4',
models: ['glm-5.1','glm-4.7','GLM-4-Plus','GLM-4-Long','GLM-4-Flash','GLM-4-FlashX','GLM-Z1-Flash'],
defaultModel: 'glm-5.1',
category: 'OpenAI-Compatible',
},
opencode_zen: {
name: 'OpenCode Zen', icon: '🧘',
desc: 'Multi-model via OpenCode Zen',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'OpenCode Key',
apiUrl: 'https://opencode.ai/zen/v1',
models: ['glm-5.1','glm-5','kimi-k2.5','kimi-k2.6','minimax-m2.7','minimax-m2.5','deepseek-v4-flash-free','qwen3.6-plus','big-pickle'],
defaultModel: 'glm-5.1',
category: 'OpenAI-Compatible',
},
opencode_go: {
name: 'OpenCode Go', icon: '🚀',
desc: 'Multi-model via OpenCode Go',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'OpenCode Key',
apiUrl: 'https://opencode.ai/zen/go/v1',
models: ['glm-5.1','glm-5','kimi-k2.5','kimi-k2.6','mimo-v2.5','minimax-m2.7','qwen3.6-plus','deepseek-v4-pro','deepseek-v4-flash'],
defaultModel: 'glm-5.1',
category: 'OpenAI-Compatible',
},
opencode_zen_anthropic: {
name: 'OpenCode Zen (Anthropic)', icon: '🧘',
desc: 'Claude models via OpenCode Zen',
backendType: 'anthropic', requiresApiKey: true, apiKeyHint: 'OpenCode Key',
apiUrl: 'https://opencode.ai/zen/v1',
models: ['claude-opus-4-7','claude-opus-4-6','claude-sonnet-4-6','claude-sonnet-4-5','claude-sonnet-4','claude-haiku-4-5'],
defaultModel: 'claude-sonnet-4-6',
category: 'OpenAI-Compatible',
},
opencode_go_anthropic: {
name: 'OpenCode Go (Anthropic)', icon: '🚀',
desc: 'Claude models via OpenCode Go',
backendType: 'anthropic', requiresApiKey: true, apiKeyHint: 'OpenCode Key',
apiUrl: 'https://opencode.ai/zen/go/v1',
models: ['minimax-m2.7','minimax-m2.5'],
defaultModel: 'minimax-m2.7',
category: 'OpenAI-Compatible',
},
crof_ai: {
name: 'Crof.ai', icon: '🌐',
desc: 'Models via Crof.ai',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'Crof.ai Key',
apiUrl: 'https://crof.ai/v1',
models: [],
defaultModel: '',
category: 'OpenAI-Compatible',
},
nvidia_nim: {
name: 'NVIDIA NIM', icon: '💚',
desc: 'NVIDIA accelerated inference',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'nvapi-...',
apiUrl: 'https://integrate.api.nvidia.com/v1',
models: [],
defaultModel: '',
category: 'OpenAI-Compatible',
},
kilo_ai: {
name: 'Kilo.ai', icon: '⚖️',
desc: 'Multi-provider gateway',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'Kilo.ai Key',
apiUrl: 'https://api.kilo.ai/api/gateway',
models: [],
defaultModel: '',
category: 'OpenAI-Compatible',
},
command_code: {
name: 'Command Code', icon: '⌘',
desc: '20+ models via CC API',
backendType: 'command-code', requiresApiKey: true, apiKeyHint: 'CC API Key',
apiUrl: 'https://api.commandcode.ai',
models: [
'deepseek/deepseek-v4-flash','deepseek/deepseek-v4-pro',
'anthropic:claude-sonnet-4-6','anthropic:claude-haiku-4-5-20251001',
'anthropic:claude-opus-4-7','anthropic:claude-opus-4-6',
'openai:gpt-5.5','openai:gpt-5.4','openai:gpt-5.4-mini','openai:gpt-5.3-codex',
'moonshotai/Kimi-K2.6','moonshotai/Kimi-K2.5',
'zai-org/GLM-5.1','zai-org/GLM-5',
'MiniMaxAI/MiniMax-M2.7','MiniMaxAI/MiniMax-M2.5',
'Qwen/Qwen3.6-Max-Preview','Qwen/Qwen3.6-Plus',
'stepfun/Step-3.5-Flash','google/gemini-3.1-flash-lite',
],
defaultModel: 'deepseek/deepseek-v4-flash',
category: 'Command Code',
},
openrouter: {
name: 'OpenRouter', icon: '🔀',
desc: 'Hundreds of models',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'sk-or-...',
apiUrl: 'https://openrouter.ai/api/v1',
models: [],
defaultModel: '',
category: 'OpenAI-Compatible',
},
openadapter: {
name: 'OpenAdapter', icon: '🔌',
desc: 'Free/proxy models',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'OA Key',
apiUrl: 'https://api.openadapter.in/v1',
models: ['0G-DeepSeek-V3','0G-DeepSeek-v4-Pro','0G-GLM-5','0G-GLM-5.1','0G-Qwen3.6','0G-Qwen-VL'],
defaultModel: '0G-DeepSeek-v4-Pro',
category: 'OpenAI-Compatible',
},
deepseek: {
name: 'DeepSeek', icon: '🔍',
desc: 'DeepSeek models directly',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'DS Key',
apiUrl: 'https://api.deepseek.com/v1',
models: ['deepseek-chat','deepseek-reasoner'],
defaultModel: 'deepseek-chat',
category: 'OpenAI-Compatible',
},
ollama: {
name: 'Ollama (Local)', icon: '🦙',
desc: 'Run models locally',
backendType: 'openai-compat', requiresApiKey: false,
apiUrl: 'http://127.0.0.1:11434',
models: ['llama3.1','llama3','codellama','mistral','mixtral','deepseek-coder','qwen2.5-coder'],
defaultModel: 'llama3.1',
category: 'Local',
},
together: {
name: 'Together AI', icon: '🤝',
desc: 'Open-source models',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'Together Key',
apiUrl: 'https://api.together.xyz/v1',
models: [],
defaultModel: '',
category: 'OpenAI-Compatible',
},
groq: {
name: 'Groq', icon: '⚡',
desc: 'Ultra-fast inference',
backendType: 'openai-compat', requiresApiKey: true, apiKeyHint: 'gsk_...',
apiUrl: 'https://api.groq.com/openai/v1',
models: [],
defaultModel: '',
category: 'OpenAI-Compatible',
},
custom: {
name: 'Custom Provider', icon: '⚙️',
desc: 'Any OpenAI-compat endpoint',
backendType: 'openai-compat', requiresApiKey: false,
apiUrl: '',
models: [],
defaultModel: '',
category: 'Custom',
},
};
// Category order
const CATEGORIES = ['Google', 'Direct', 'OpenAI-Compatible', 'Command Code', 'Local', 'Custom'];
let currentConfig = null;
let selectedProvider = null;
async function init() {
try {
currentConfig = await ipcRenderer.invoke('provider:get-config');
} catch (e) {
console.error('Failed to load config:', e);
currentConfig = { activeProvider: 'google_gemini', providers: {} };
}
selectedProvider = currentConfig.activeProvider;
renderProviderGrid();
selectProvider(selectedProvider);
updateStatus();
}
function renderProviderGrid() {
const container = document.getElementById('providerGridContainer');
container.innerHTML = '';
// Group providers by category
const grouped = {};
for (const [key, prov] of Object.entries(PROVIDERS)) {
const cat = prov.category || 'Other';
if (!grouped[cat]) grouped[cat] = [];
grouped[cat].push({ key, ...prov });
}
for (const cat of CATEGORIES) {
if (!grouped[cat]) continue;
const title = document.createElement('div');
title.className = 'category-title';
title.textContent = cat;
container.appendChild(title);
const grid = document.createElement('div');
grid.className = 'provider-grid';
for (const prov of grouped[cat]) {
const card = document.createElement('div');
card.className = 'provider-card' + (prov.key === selectedProvider ? ' active' : '');
card.dataset.provider = prov.key;
card.innerHTML = `
<div class="provider-icon">${prov.icon}</div>
<div class="provider-name">${prov.name}</div>
<div class="provider-desc">${prov.desc}</div>
`;
card.addEventListener('click', () => selectProvider(prov.key));
grid.appendChild(card);
}
container.appendChild(grid);
}
}
function selectProvider(key) {
selectedProvider = key;
// Update active card
document.querySelectorAll('.provider-card').forEach(c => {
c.classList.toggle('active', c.dataset.provider === key);
});
const prov = PROVIDERS[key];
const config = currentConfig?.providers?.[key] || {};
document.getElementById('configTitle').textContent = prov.name + ' Configuration';
document.getElementById('apiUrl').value = config.apiUrl || prov.apiUrl || '';
document.getElementById('apiKey').value = config.apiKey || '';
document.getElementById('modelInput').value = config.model || config.defaultModel || prov.defaultModel || '';
document.getElementById('apiKeyHelp').textContent = prov.apiKeyHint || '';
// Show/hide API key field
const apiKeyGroup = document.getElementById('apiKeyGroup');
apiKeyGroup.style.display = prov.requiresApiKey ? 'block' : 'none';
// Render model chips
const chipsContainer = document.getElementById('modelChips');
const models = prov.models || [];
if (models.length === 0) {
chipsContainer.innerHTML = '<div class="help-text">Type a model name above or use "Fetch Models" after saving</div>';
} else {
const currentModel = document.getElementById('modelInput').value;
chipsContainer.innerHTML = models.map(m =>
`<div class="model-chip${m === currentModel ? ' selected' : ''}" onclick="selectModel('${m}')">${m}</div>`
).join('');
}
// Backend badge
const badge = document.getElementById('backendBadge');
const bt = prov.backendType;
const badges = {
'gemini-native': '<span class="badge badge-native">NATIVE</span>',
'native': '<span class="badge badge-native">NATIVE</span>',
'openai-compat': '<span class="badge badge-proxy">OPENAI-COMPAT</span>',
'anthropic': '<span class="badge badge-proxy">ANTHROPIC</span>',
'command-code': '<span class="badge badge-cc">COMMAND CODE</span>',
};
badge.innerHTML = (badges[bt] || bt) + ' ' + prov.name;
updateStatus();
}
function selectModel(model) {
document.getElementById('modelInput').value = model;
document.querySelectorAll('.model-chip').forEach(c => {
c.classList.toggle('selected', c.textContent === model);
});
}
function toggleKeyVisibility() {
const input = document.getElementById('apiKey');
input.type = input.type === 'password' ? 'text' : 'password';
}
async function saveSettings() {
const key = selectedProvider;
const prov = PROVIDERS[key];
const settings = {
activeProvider: key,
providerConfig: {
apiUrl: document.getElementById('apiUrl').value,
apiKey: document.getElementById('apiKey').value,
model: document.getElementById('modelInput').value,
backendType: prov.backendType,
requiresApiKey: prov.requiresApiKey,
},
};
try {
const result = await ipcRenderer.invoke('provider:save', settings);
currentConfig = await ipcRenderer.invoke('provider:get-config');
if (result && result.needsRestart) {
showToast('✓ Provider configured! Starting AG X...');
updateStatus();
// Auto-close settings window after a brief delay
setTimeout(() => {
const currentWindow = require('electron').remote?.getCurrentWindow();
if (currentWindow) currentWindow.close();
// Fallback: close via ipcRenderer
require('electron').ipcRenderer.send('provider:close-settings');
}, 1500);
} else {
showToast('Settings saved! Provider: ' + prov.name);
updateStatus();
}
} catch (e) {
showToast('Error saving: ' + e.message, true);
}
}
async function resetToDefaults() {
try {
await ipcRenderer.invoke('provider:reset');
currentConfig = await ipcRenderer.invoke('provider:get-config');
selectedProvider = currentConfig.activeProvider;
renderProviderGrid();
selectProvider(selectedProvider);
showToast('Reset to defaults');
updateStatus();
} catch (e) {
showToast('Error resetting: ' + e.message, true);
}
}
async function testConnection() {
const resultEl = document.getElementById('testResult');
resultEl.className = 'test-result';
resultEl.style.display = 'block';
resultEl.textContent = 'Testing connection…';
const prov = PROVIDERS[selectedProvider];
try {
const result = await ipcRenderer.invoke('provider:test-connection', {
provider: selectedProvider,
apiUrl: document.getElementById('apiUrl').value,
apiKey: document.getElementById('apiKey').value,
model: document.getElementById('modelInput').value,
backendType: prov.backendType,
});
if (result.success) {
resultEl.className = 'test-result success';
resultEl.textContent = '✓ ' + result.message;
} else {
resultEl.className = 'test-result error';
resultEl.textContent = '✗ ' + result.error;
}
} catch (e) {
resultEl.className = 'test-result error';
resultEl.textContent = '✗ ' + e.message;
}
}
async function saveAdvanced() {
try {
await ipcRenderer.invoke('provider:save-advanced', {
proxyPort: parseInt(document.getElementById('proxyPort').value) || 9876,
translateProxyPath: document.getElementById('translateProxyPath').value,
ccVersion: document.getElementById('ccVersion').value,
});
showToast('Advanced settings saved');
} catch (e) {
showToast('Error: ' + e.message, true);
}
}
function showToast(message, isError = false) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = 'toast show' + (isError ? ' error' : '');
setTimeout(() => { toast.className = 'toast'; }, 3000);
}
function updateStatus() {
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
const prov = PROVIDERS[selectedProvider];
if (prov) {
const config = currentConfig?.providers?.[selectedProvider];
if (prov.backendType === 'gemini-native' || prov.backendType === 'native') {
dot.className = 'status-dot connected';
text.textContent = prov.name + ' — Direct connection (no proxy needed)';
} else if (config?.apiKey) {
dot.className = 'status-dot unknown';
text.textContent = prov.name + ' — API key configured, proxy will start on launch';
} else {
dot.className = 'status-dot disconnected';
text.textContent = prov.name + ' — API key required';
}
}
}
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active'));
tab.classList.add('active');
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
});
});
init();
</script>
</body>
</html>

176
dist/provider/welcome.html vendored Normal file
View File

@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to AG X</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #0a0a1a 0%, #1a1a3e 50%, #0a0a1a 100%);
color: #e0e0e0; min-height: 100vh;
display: flex; align-items: center; justify-content: center;
overflow: hidden;
}
.welcome-container {
text-align: center; max-width: 620px; width: 100%; padding: 40px;
}
.logo { font-size: 64px; margin-bottom: 8px; }
.app-name {
font-size: 42px; font-weight: 800;
background: linear-gradient(135deg, #e94560, #ff6b8a, #e94560);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
margin-bottom: 8px;
}
.app-version {
font-size: 13px; color: #555; margin-bottom: 32px;
letter-spacing: 2px; text-transform: uppercase;
}
.tagline {
font-size: 16px; color: #8892b0; margin-bottom: 40px;
line-height: 1.6;
}
.choice-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 20px;
margin-bottom: 32px;
}
.choice-card {
background: rgba(22, 33, 62, 0.8);
border: 2px solid #2a2a4a; border-radius: 16px;
padding: 28px 20px; cursor: pointer; transition: all 0.3s;
text-align: center; position: relative; overflow: hidden;
}
.choice-card::before {
content: ''; position: absolute; top: 0; left: 0; right: 0;
height: 3px; background: transparent; transition: background 0.3s;
}
.choice-card:hover {
border-color: #e94560; transform: translateY(-3px);
box-shadow: 0 8px 30px rgba(233, 69, 96, 0.15);
}
.choice-card:hover::before { background: linear-gradient(90deg, #e94560, #ff6b8a); }
.choice-card .icon { font-size: 40px; margin-bottom: 12px; }
.choice-card h3 { font-size: 16px; color: #fff; margin-bottom: 8px; }
.choice-card p { font-size: 12px; color: #8892b0; line-height: 1.5; }
.choice-card .badge {
display: inline-block; margin-top: 10px; padding: 3px 10px;
border-radius: 10px; font-size: 10px; font-weight: 600;
}
.badge-easy { background: #1a3a1a; color: #4caf50; }
.badge-power { background: #3a2a1a; color: #ffa726; }
.divider {
display: flex; align-items: center; margin: 24px 0;
color: #444; font-size: 12px; text-transform: uppercase;
letter-spacing: 1px;
}
.divider::before, .divider::after {
content: ''; flex: 1; height: 1px; background: #2a2a4a;
}
.divider span { padding: 0 16px; }
.footer {
font-size: 11px; color: #444; margin-top: 20px;
}
.footer a { color: #e94560; text-decoration: none; }
.footer a:hover { text-decoration: underline; }
/* Animation */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.welcome-container > * {
animation: fadeIn 0.5s ease forwards;
opacity: 0;
}
.welcome-container > *:nth-child(1) { animation-delay: 0.1s; }
.welcome-container > *:nth-child(2) { animation-delay: 0.2s; }
.welcome-container > *:nth-child(3) { animation-delay: 0.25s; }
.welcome-container > *:nth-child(4) { animation-delay: 0.3s; }
.welcome-container > *:nth-child(5) { animation-delay: 0.4s; }
.welcome-container > *:nth-child(6) { animation-delay: 0.5s; }
/* Particles */
.particles {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 0;
}
.particle {
position: absolute; width: 2px; height: 2px;
background: rgba(233, 69, 96, 0.3); border-radius: 50%;
animation: float linear infinite;
}
@keyframes float {
0% { transform: translateY(100vh) scale(0); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(-10vh) scale(1); opacity: 0; }
}
</style>
</head>
<body>
<div class="particles" id="particles"></div>
<div class="welcome-container">
<div class="logo"></div>
<div class="app-name">AG X</div>
<div class="app-version">AI-Powered Code Intelligence</div>
<div class="tagline">
Choose how you'd like to connect to AI.<br>
You can always change this later from the menu.
</div>
<div class="choice-grid">
<div class="choice-card" id="choiceGoogle" onclick="chooseGoogle()">
<div class="icon">🔮</div>
<h3>Google Gemini</h3>
<p>Sign in with your Google account for instant access to Gemini models. No API key needed.</p>
<span class="badge badge-easy">Easiest — zero config</span>
</div>
<div class="choice-card" id="choiceProvider" onclick="chooseProvider()">
<div class="icon">🔌</div>
<h3>Another AI Provider</h3>
<p>Use OpenAI, Anthropic, DeepSeek, Ollama, or 15+ other providers with your own API key.</p>
<span class="badge badge-power">Power users</span>
</div>
</div>
<div class="footer">
Provider settings are always available via <strong>Menu → AI Provider Settings</strong> or the <strong>tray icon</strong>.
</div>
</div>
<script>
const { ipcRenderer } = require('electron');
// Create particles
(function() {
const container = document.getElementById('particles');
for (let i = 0; i < 30; i++) {
const p = document.createElement('div');
p.className = 'particle';
p.style.left = Math.random() * 100 + '%';
p.style.animationDuration = (8 + Math.random() * 12) + 's';
p.style.animationDelay = Math.random() * 10 + 's';
p.style.width = p.style.height = (1 + Math.random() * 3) + 'px';
container.appendChild(p);
}
})();
function chooseGoogle() {
document.getElementById('choiceGoogle').style.borderColor = '#e94560';
document.getElementById('choiceGoogle').style.background = 'rgba(233,69,96,0.1)';
ipcRenderer.send('welcome:choice', { provider: 'google_gemini' });
}
function chooseProvider() {
document.getElementById('choiceProvider').style.borderColor = '#ffa726';
document.getElementById('choiceProvider').style.background = 'rgba(255,167,38,0.1)';
ipcRenderer.send('welcome:choice', { provider: 'custom' });
}
</script>
</body>
</html>