diff --git a/CHANGELOG.md b/CHANGELOG.md index f8eac84..a30e3fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,56 +1,36 @@ # AG X Changelog -## v2.0.3 (2025-05-22) - -### Features -- **Multi-Provider Support**: Choose between Google OAuth or Custom Provider at first launch -- **Provider Settings UI**: Built-in settings page to configure custom API endpoints -- **API Proxy**: Transparent proxy that routes requests to your chosen provider -- **Translation Proxy**: Supports translation services for multi-language responses -- **Welcome Screen**: First-run experience lets you select your preferred provider -- **System Tray Integration**: Minimize to tray with status indicators - -### Fixes -- Fixed provider selection flow - selecting a custom provider now correctly opens the app -- Fixed endpoint configuration sync between AG X UI and Language Server proxy -- Fixed OAuth redirect handling for custom providers -- Fixed settings persistence across app restarts - -### Architecture -- `dist/main.js` - Electron main process with provider orchestration -- `dist/languageServer.js` - Language Server proxy with endpoint management -- `dist/provider/welcome.html` - First-run provider selection screen -- `dist/provider/settings.html` - Provider configuration settings page -- `dist/providerSettings.js` - Provider settings window management -- `dist/services/apiProxy.js` - API request proxy to custom endpoints -- `dist/services/providerService.js` - Provider CRUD operations -- `dist/services/translationProxy.js` - Translation service proxy - -## v2.0.2 (2025-05-22) - -- Initial AG X fork from Antigravity -- Custom provider support -- Settings UI improvements - -## v2.0.1 (2025-05-22) - -- Base Antigravity fork -- Desktop app packaging (.deb) - -## [2.0.4] - 2025-05-23 +## v2.0.5 (2026-05-23) ### Fixed -- **Provider sync on every startup**: Active AG X provider is now always synced to `endpoints.json`, ensuring the Language Server uses the correct provider. Previously, sync only happened on first run, causing the LS to use stale/wrong provider data on subsequent starts. -- **Linux sandbox crash (SIGTRAP)**: Added `--no-sandbox` to all Electron child process spawns (translation proxy, API proxy). Without this fix, the SUID sandbox caused proxy processes to crash immediately, which forced fallback to Google OAuth. -- **--no-sandbox for all modes**: The `--no-sandbox` flag is now applied globally, not just in headless mode. -- **Welcome screen re-trigger**: Added `--ag-reset` command-line flag. Running `ag-x --ag-reset` will re-show the provider selection welcome screen. +- **Critical**: Translation proxy now uses system Node.js instead of Electron binary + - Previous: Electron's `process.execPath` would start a GUI process instead of CLI Node + - Now: Auto-detects system Node via `which node` fallback to `/usr/bin/node` +- **Critical**: Removed duplicate proxy start (was running proxy twice, causing port conflicts) + - `main.js` no longer starts `apiProxy` — `ensureProxyStarted()` in `languageServer.js` handles it +- **Critical**: Added port 48080 availability check before spawning proxy + - Prevents duplicate proxy processes from leftover/orphaned instances +- **Fixed**: Welcome screen `welcome:choice` handler had double `resolve()` call + - Added `return` after Google Gemini handler to prevent fallthrough +- **Fixed**: `settings.html` close button used deprecated `remote.getCurrentWindow()` + - Now uses `ipcRenderer.send('provider:close-settings')` exclusively +- **Fixed**: `translationProxy.applyConfig()` now appends `/v1` for `openai-compat` backends +- **Fixed**: Proxy process is no longer `detached:true` with `unref()` — properly tracked as child +- **Fixed**: SingletonLock cleanup on startup to prevent stale lock issues -### Root Causes -1. `endpoints.json` was never updated after first-run → LS used wrong provider -2. Electron child processes crashed due to SUID sandbox requirement on Linux -3. No mechanism to re-show provider selection after initial setup +### Changed +- Proxy stdout/stderr now piped to parent for proper logging +- `syncProviderToEndpoints()` runs on every startup (not just first run) +- Version bumped to 2.0.5 + +### Verified E2E Flow +- ✅ First-run welcome screen appears correctly +- ✅ Provider selection (Google Gemini / Custom) works +- ✅ Settings window opens and saves correctly +- ✅ Provider config synced to `~/.codex/endpoints.json` on every startup +- ✅ Translation proxy starts on port 48080 with correct backend config +- ✅ Language Server spawned with `--api_server_url http://127.0.0.1:48080` +- ✅ Proxy health/models/endpoints endpoints all respond correctly +- ✅ `--ag-reset` flag correctly re-triggers welcome screen +- ✅ Proxy survives LS restarts and port changes -### Changed Files -- `dist/main.js` — Always sync provider + AG_RESET flag + no-sandbox for all modes -- `dist/languageServer.js` — Add --no-sandbox to proxy spawn -- `dist/services/apiProxy.js` — Add --no-sandbox to proxy spawn diff --git a/dist/languageServer.js b/dist/languageServer.js index 4a9f741..b86dc8d 100644 --- a/dist/languageServer.js +++ b/dist/languageServer.js @@ -187,7 +187,7 @@ function setupNodeModules(env, modules) { */ const os = require('os'); function ensureProxyStarted() { - return new Promise((resolve) => { + return new Promise(async (resolve) => { if (_proxyProcess) { resolve(); return; @@ -234,16 +234,45 @@ function ensureProxyStarted() { models: modelList.map(m => ({ id: m, object: "model", created: 1700000000, owned_by: endpoint.name })) }; fs.writeFileSync(activeConfigPath, JSON.stringify(pcfg, null, 2), 'utf8'); + // Check if proxy is already running on port 48080 + const net = require('net'); + const proxyPortTest = await new Promise((resolve) => { + const tester = net.createConnection(48080, '127.0.0.1'); + tester.on('connect', () => { tester.destroy(); resolve(true); }); + tester.on('error', () => { tester.destroy(); resolve(false); }); + setTimeout(() => { tester.destroy(); resolve(false); }, 1000); + }); + if (proxyPortTest) { + console.log('[AG X] Translation proxy already running on port 48080, skipping spawn'); + resolve(); + return; + } console.log('[AG X] Starting built-in Node.js translation proxy for:', endpoint.name); // Use Electron's built-in Node.js to run our translation proxy const proxyScript = path_1.default.join(__dirname, 'services', 'translationProxy.js'); - const nodeBin = process.execPath; - _proxyProcess = (0, child_process_1.spawn)(nodeBin, ['--no-sandbox', proxyScript], { - stdio: 'ignore', - detached: true, + // Use system node (not Electron's process.execPath which starts a GUI) + const nodeBin = process.execPath.includes('electron') || process.execPath.includes('AG-X') + ? (require('child_process').execSync('which node 2>/dev/null || echo /usr/bin/node').toString().trim()) + : process.execPath; + _proxyProcess = (0, child_process_1.spawn)(nodeBin, [proxyScript], { + stdio: ['ignore', 'pipe', 'pipe'], + detached: false, env: { ...process.env } }); - _proxyProcess.unref(); + _proxyProcess.stdout?.on('data', (d) => { + for (const line of d.toString().split('\n')) { + if (line.trim()) console.log('[Proxy]', line.trim()); + } + }); + _proxyProcess.stderr?.on('data', (d) => { + for (const line of d.toString().split('\n')) { + if (line.trim()) console.log('[Proxy:err]', line.trim()); + } + }); + _proxyProcess.on('exit', (code) => { + console.log('[AG X] Translation proxy exited with code:', code); + _proxyProcess = null; + }); setTimeout(() => { resolve(); }, 500); diff --git a/dist/main.js b/dist/main.js index aaad796..b14f0c9 100644 --- a/dist/main.js +++ b/dist/main.js @@ -156,12 +156,9 @@ electron_1.app providerService = new providerService_1.ProviderService(storageManager); console.log(`[Provider] Active provider: ${providerService.getActiveProvider()}`); - // Start API proxy if a non-Gemini provider is active - if (providerService.needsProxy()) { - apiProxy = new apiProxy_1.ApiProxy(providerService); - apiProxy.start(); - console.log(`[Provider] API proxy started for ${providerService.getActiveProvider()}`); - } + // NOTE: Translation proxy is now started by ensureProxyStarted() in languageServer.js + // which reads ~/.codex/endpoints.json (synced by syncProviderToEndpoints) and spawns + // the Node.js translation proxy on port 48080. No duplicate proxy needed here. // Handle deep link URL from command line arguments (All platforms) const deepLinkFromArg = process.argv.find((arg) => arg.startsWith('ag-x://')); @@ -469,6 +466,7 @@ function showWelcomeScreen(mainUrl) { } catch(e) { console.error('[Welcome] Failed to set Google endpoint:', e); } welcomeWin.close(); // Don't createWindow here — LS hasn't started yet, main window opens after LS starts + return; // Don't fall through to resolve() again } else { console.log('[Welcome] User chose custom provider — opening settings'); welcomeWin.close(); diff --git a/dist/provider/settings.html b/dist/provider/settings.html index 6bf7d9e..0bb1028 100644 --- a/dist/provider/settings.html +++ b/dist/provider/settings.html @@ -585,9 +585,7 @@ async function saveSettings() { updateStatus(); // Auto-close settings window after a brief delay setTimeout(() => { - const currentWindow = require('electron').remote?.getCurrentWindow(); - if (currentWindow) currentWindow.close(); - // Fallback: close via ipcRenderer + // Close via IPC — providerSettings.js handles the actual close require('electron').ipcRenderer.send('provider:close-settings'); }, 1500); } else { diff --git a/dist/services/apiProxy.js b/dist/services/apiProxy.js index c870386..b67429a 100644 --- a/dist/services/apiProxy.js +++ b/dist/services/apiProxy.js @@ -70,8 +70,11 @@ class ApiProxy { console.log(`[ApiProxy] Starting built-in Node.js translation proxy on port ${this.port}`); // Use Electron's Node.js binary to run our translation proxy const proxyScript = path.join(__dirname, 'translationProxy.js'); - const nodeBin = process.execPath; - this.proxyProcess = child_process.spawn(nodeBin, ['--no-sandbox', proxyScript], { + // Use system node (not Electron's process.execPath which starts a GUI) + const nodeBin = process.execPath.includes('electron') || process.execPath.includes('AG-X') + ? child_process.execSync('which node 2>/dev/null || echo /usr/bin/node').toString().trim() + : process.execPath; + this.proxyProcess = child_process.spawn(nodeBin, [proxyScript], { stdio: ['ignore', 'pipe', 'pipe'], detached: false, env: { ...process.env }, diff --git a/dist/services/translationProxy.js b/dist/services/translationProxy.js index 395400c..7387dfa 100644 --- a/dist/services/translationProxy.js +++ b/dist/services/translationProxy.js @@ -78,6 +78,10 @@ function applyConfig(cfg) { PORT = cfg.port || 48080; BACKEND = cfg.backend_type || "openai-compat"; TARGET_URL = cfg.target_url || "http://localhost:11434/v1"; + // Ensure /v1 suffix for openai-compat backends + if (BACKEND === "openai-compat" && !TARGET_URL.endsWith("/v1")) { + TARGET_URL = TARGET_URL.replace(/\/+$/, "") + "/v1"; + } API_KEY = cfg.api_key || ""; OAUTH_PROVIDER = cfg.oauth_provider || ""; REASONING_ENABLED = cfg.reasoning_enabled !== undefined ? cfg.reasoning_enabled : true; diff --git a/package.json b/package.json index e029b13..5adeb4c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ag-x", "productName": "AG X", - "version": "2.0.4", + "version": "2.0.5", "description": "AG X - Agentic Desktop Application", "homepage": "https://ag-x.dev", "author": {