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

587
dist/main.js vendored Normal file
View File

@@ -0,0 +1,587 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const electron_1 = require("electron");
const main_1 = __importDefault(require("electron-log/main"));
const ipcHandlers_1 = require("./ipcHandlers");
const fs = __importStar(require("fs"));
const crypto = __importStar(require("crypto"));
const readline = __importStar(require("readline"));
const utils_1 = require("./utils");
const languageServer_1 = require("./languageServer");
const updater_1 = require("./updater");
const constants_1 = require("./constants");
const tray_1 = require("./tray");
const storage_1 = require("./storage");
const paths_1 = require("./paths");
const menu_1 = require("./menu");
const customScheme_1 = require("./customScheme");
const settingsService_1 = require("./services/settingsService");
const ideInstall_1 = require("./ideInstall");
const providerService_1 = require("./services/providerService");
const apiProxy_1 = require("./services/apiProxy");
const providerSettings_1 = require("./providerSettings");
const path_2 = require("path");
const fs_1 = require("fs");
const gotTheLock = electron_1.app.requestSingleInstanceLock();
if (!gotTheLock) {
electron_1.app.quit();
process.exit(0);
}
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
let storageManager;
let settingsService;
let hasStartedMainApplication = false;
let isQuitting = false;
let providerService;
let apiProxy;
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
// Driven by ELECTRON_OZONE_PLATFORM_HINT=headless env var.
// This single env var both prevents GTK from crashing (Electron 33+)
// and tells our code to skip createWindow().
const HEADLESS = process.env.ELECTRON_OZONE_PLATFORM_HINT === 'headless';
// When set, skip LS startup and load this URL directly (for dev iteration).
const DEV_URL = process.env.DEV_URL;
if (HEADLESS) {
electron_1.app.commandLine.appendSwitch('ozone-platform', 'headless');
electron_1.app.commandLine.appendSwitch('headless');
electron_1.app.commandLine.appendSwitch('disable-gpu');
electron_1.app.commandLine.appendSwitch('no-sandbox');
}
if (!electron_1.app.commandLine.hasSwitch('remote-debugging-port')) {
electron_1.app.commandLine.appendSwitch('remote-debugging-port', '0');
}
// ---------------------------------------------------------------------------
// Application Lifecycle
// ---------------------------------------------------------------------------
let pendingDeepLink = null;
function handleDeepLink(url) {
const wins = electron_1.BrowserWindow.getAllWindows();
// This block handles deep links when windows are already open.
if (wins.length > 0) {
if (wins[0].isMinimized()) {
wins[0].restore();
}
wins[0].show();
wins[0].focus();
electron_1.app.focus({ steal: true });
wins[0].webContents.send('deep-link', url);
}
else {
pendingDeepLink = url;
}
}
electron_1.app.on('second-instance', (event, commandLine) => {
const wins = electron_1.BrowserWindow.getAllWindows();
if (wins.length > 0) {
if (wins[0].isMinimized()) {
wins[0].restore();
}
wins[0].show();
wins[0].focus();
electron_1.app.focus({ steal: true });
}
const url = commandLine.find((arg) => arg.startsWith('ag-x://'));
if (url) {
handleDeepLink(url);
}
});
(0, customScheme_1.registerCustomSchemes)();
// Register as default protocol client for deep linking
const PROTOCOL = 'ag-x';
if (!electron_1.app.isDefaultProtocolClient(PROTOCOL)) {
electron_1.app.setAsDefaultProtocolClient(PROTOCOL);
}
electron_1.app.on('open-url', (event, url) => {
event.preventDefault();
handleDeepLink(url);
});
/**
* App entry point. Runs once Electron has finished initializing.
* Validates the LS binary, frees the port if needed, spawns the LS,
* and opens the initial browser window.
*/
electron_1.app
.whenReady()
.then(async () => {
// Initialize electron-log and override console
main_1.default.initialize();
Object.assign(console, main_1.default.functions);
const storagePath = (0, paths_1.getAppStoragePath)();
storageManager = new storage_1.StorageManager(storagePath, settingsService_1.DEFAULTS);
settingsService = new settingsService_1.SettingsService(storageManager);
// Initialize AI Provider Service
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()}`);
}
// Handle deep link URL from command line arguments (All platforms)
const deepLinkFromArg = process.argv.find((arg) => arg.startsWith('ag-x://'));
if (deepLinkFromArg) {
console.log('Launched with deep link:', deepLinkFromArg);
pendingDeepLink = deepLinkFromArg;
}
// Register IPC handlers
(0, ipcHandlers_1.registerIpcHandlers)(storageManager);
// Register provider IPC handlers
electron_1.ipcMain.handle('provider:get-active', async () => {
return providerService.getActiveProvider();
});
electron_1.ipcMain.handle('provider:get-all', async () => {
return providerService.getAllProviders();
});
electron_1.ipcMain.handle('provider:open-settings', async () => {
(0, providerSettings_1.openProviderSettings)(providerService);
});
electron_1.ipcMain.handle('deep-link:get-stored', () => {
const link = pendingDeepLink;
pendingDeepLink = null; // Clear after read
return link;
});
// Handle requests coming from custom schemes
(0, customScheme_1.registerCustomSchemeHandlers)();
// Set About panel options with LS CL
const cl = await (0, languageServer_1.getLsCL)();
electron_1.app.setAboutPanelOptions({
applicationName: 'AG X',
applicationVersion: electron_1.app.getVersion(),
version: cl || undefined,
});
// Pre-onboarding: check if we should offer to re-install the IDE.
// This runs before the LS starts so we can show a standalone wizard.
if (!HEADLESS) {
await (0, ideInstall_1.maybeShowIdeInstallWizard)(storageManager);
}
if (DEV_URL) {
console.log('Starting in dev mode with URL:', DEV_URL);
(0, utils_1.createWindow)(DEV_URL);
hasStartedMainApplication = true;
return;
}
if (!fs.existsSync(languageServer_1.LS_BINARY)) {
const msg = `language_server binary not found at:\n${languageServer_1.LS_BINARY}\n\nPlease build set a valid location.`;
if (HEADLESS) {
console.error('ERROR:', msg);
}
else {
await electron_1.dialog.showErrorBox('Binary not found', msg);
}
electron_1.app.quit();
return;
}
const csrf = crypto.randomUUID();
console.log(`Starting app (v${electron_1.app.getVersion()}) with dynamic port…`);
// Check if first run BEFORE starting LS so we can configure provider first
const isFirstRun = !fs_1.existsSync(providerService.configPath);
if (isFirstRun && !HEADLESS) {
console.log('[Welcome] First run detected — showing provider choice screen');
await showWelcomeScreen('about:blank');
console.log('[Welcome] User selected provider:', providerService.getActiveProvider());
// Sync to endpoints.json (for Google, the handler already set the endpoint;
// for custom, the settings save handler synced. This is a safety sync.)
if (providerService.getActiveProvider() !== 'google_gemini') {
syncProviderToEndpoints(providerService);
}
}
let handle;
const targetPort = Number(process.env.JETSKI_LS_PORT) || constants_1.DYNAMIC_PORT;
try {
handle = await (0, languageServer_1.startAndMonitorLanguageServer)(targetPort, csrf, {
headless: HEADLESS,
onPortChanged: (newPort) => {
const newUrl = `${constants_1.WINDOW_ORIGIN}:${newPort}/`;
console.log(`[Auto-Restart] Port changed! Reloading all windows with URL: ${newUrl}`);
(0, languageServer_1.setupLocalCertTrust)();
if (!HEADLESS) {
const windows = electron_1.BrowserWindow.getAllWindows();
for (const win of windows) {
if (win.getTitle() === 'Welcome to AG X' || win.getTitle() === 'AI Provider Settings') continue;
void win.loadURL(newUrl);
}
}
},
});
}
catch (err) {
const msg = err.message;
if (HEADLESS) {
console.error('Startup failed:', msg);
}
else {
await electron_1.dialog.showErrorBox('Startup failed', msg);
}
electron_1.app.quit();
return;
}
const url = `${constants_1.WINDOW_ORIGIN}:${handle.port}/`;
console.log('\n' + '='.repeat(60));
console.log(` Local: ${url}`);
console.log(` LS Logs: ${(0, paths_1.getLsLogPath)()}`);
console.log(` Electron Logs: ${main_1.default.transports.file.getFile().path}`);
console.log(` Provider: ${providerService.getActiveProvider()}`);
console.log('='.repeat(60) + '\n');
if (HEADLESS) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.on('line', (line) => {
const lsProc = (0, languageServer_1.getLsProcess)();
if (lsProc && lsProc.stdin) {
lsProc.stdin.write(line + '\n');
console.log('-> Forwarded input to Language Server.');
}
else {
console.log('Language Server process is not running.');
}
});
}
// Initial window — opened once after the LS has successfully started.
if (!HEADLESS) {
(0, menu_1.setupApplicationMenu)(url);
// Create the main window (welcome screen already completed above for first run)
(0, utils_1.createWindow)(url);
if (electron_1.app.dock) {
const dockMenu = electron_1.Menu.buildFromTemplate([
{
label: 'New Window',
click: () => (0, utils_1.createWindow)(url),
},
]);
electron_1.app.dock.setMenu(dockMenu);
}
(0, tray_1.createTray)([
{
id: 'running-agents',
label: 'No agents running',
enabled: false,
},
{ type: 'separator' },
{
label: `Open ${electron_1.app.getName()}`,
click: () => (0, utils_1.showOrCreateWindow)((0, languageServer_1.getLsPort)()),
},
{
label: 'AI Provider Settings',
click: () => (0, providerSettings_1.openProviderSettings)(providerService),
},
{ type: 'separator' },
{
label: 'Quit',
click: () => {
// Triggers 'before-quit' to run graceful cleanup without confirmation.
electron_1.app.quit();
},
},
]);
}
// Start checking for app updates.
(0, updater_1.initAutoUpdater)(HEADLESS);
hasStartedMainApplication = true;
})
.catch(() => {
hasStartedMainApplication = true;
});
/**
* Shows the first-run welcome screen for provider selection.
* Returns a Promise that resolves when the user has made their choice.
*/
// === AG X: Sync provider config to ~/.codex/endpoints.json ===
function syncProviderToEndpoints(providerService) {
try {
const os = require('os');
const fs = require('fs');
const pathMod = require('path');
const home = os.homedir();
const endpointsPath = pathMod.join(home, '.codex', 'endpoints.json');
const activePath = pathMod.join(home, '.codex', '.active-endpoint.json');
const activeProvider = providerService.getActiveProvider();
const providerConfig = providerService.getProviderConfig(activeProvider);
if (!providerConfig) {
console.log('[Sync] No provider config to sync');
return;
}
let endpointsData = { default: '', endpoints: [] };
if (fs.existsSync(endpointsPath)) {
try {
endpointsData = JSON.parse(fs.readFileSync(endpointsPath, 'utf8'));
} catch (e) {}
}
const endpointName = 'AG X: ' + (providerConfig.name || activeProvider);
const existingIdx = endpointsData.endpoints.findIndex(e => e.name === endpointName);
const endpoint = {
name: endpointName,
backend_type: providerConfig.backendType || 'openai-compat',
base_url: providerConfig.apiUrl || '',
api_key: providerConfig.apiKey || '',
default_model: providerConfig.model || providerConfig.defaultModel || '',
models: providerConfig.models || [],
provider_preset: 'AG X',
};
if (existingIdx >= 0) {
endpointsData.endpoints[existingIdx] = endpoint;
} else {
endpointsData.endpoints.push(endpoint);
}
endpointsData.default = endpointName;
fs.writeFileSync(endpointsPath, JSON.stringify(endpointsData, null, 2), 'utf-8');
fs.writeFileSync(activePath, JSON.stringify({ active: endpointName }, null, 2), 'utf-8');
console.log('[Sync] Provider synced to endpoints.json:', endpointName);
} catch (err) {
console.error('[Sync] Failed to sync provider to endpoints:', err);
}
}
// === End sync function ===
function showWelcomeScreen(mainUrl) {
return new Promise((resolve) => {
const welcomeWin = new electron_1.BrowserWindow({
width: 720,
height: 560,
title: 'Welcome to AG X',
resizable: false,
minimizable: false,
maximizable: false,
fullscreenable: false,
backgroundColor: '#0a0a1a',
show: true,
center: true,
frame: false,
titleBarStyle: 'hidden',
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: false,
},
});
welcomeWin.loadFile(path_2.join(__dirname, 'provider', 'welcome.html')).catch((err) => {
console.error('[Welcome] Failed to load welcome.html:', err);
});
welcomeWin.webContents.on('did-finish-load', () => {
console.log('[Welcome] Welcome screen loaded successfully');
});
welcomeWin.webContents.on('did-fail-load', (_event, code, desc) => {
console.error('[Welcome] Failed to load:', code, desc);
});
console.log('[Welcome] Window created, title:', welcomeWin.getTitle());
welcomeWin.on('closed', () => {
// If closed without choosing, default to Google Gemini
if (!resolved) {
resolved = true;
providerService.setActiveProvider('google_gemini');
providerService.saveConfig();
resolve();
}
});
let resolved = false;
electron_1.ipcMain.once('welcome:choice', (_event, data) => {
if (resolved) return;
resolved = true;
if (data.provider === 'google_gemini') {
console.log('[Welcome] User chose Google Gemini (OAuth)');
providerService.setActiveProvider('google_gemini');
providerService.saveConfig();
// Set Google as active endpoint in ~/.codex/
try {
const os = require('os');
const fs = require('fs');
const pathMod = require('path');
const activePath = pathMod.join(os.homedir(), '.codex', '.active-endpoint.json');
const endpointsPath = pathMod.join(os.homedir(), '.codex', 'endpoints.json');
let epData = { default: '', endpoints: [] };
if (fs.existsSync(endpointsPath)) {
try { epData = JSON.parse(fs.readFileSync(endpointsPath, 'utf8')); } catch(e) {}
}
let googleEp = epData.endpoints.find(e => e.name === 'Google Gemini');
if (!googleEp) {
googleEp = {
name: 'Google Gemini',
backend_type: 'gemini-native',
base_url: 'https://daily-cloudcode-pa.sandbox.googleapis.com',
api_key: '',
default_model: 'gemini-2.5-pro',
models: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash'],
provider_preset: 'AG X',
};
epData.endpoints.push(googleEp);
}
epData.default = 'Google Gemini';
fs.writeFileSync(endpointsPath, JSON.stringify(epData, null, 2), 'utf-8');
fs.writeFileSync(activePath, JSON.stringify({ active: 'Google Gemini' }, null, 2), 'utf-8');
console.log('[Welcome] Google Gemini endpoint set as active');
} 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
} else {
console.log('[Welcome] User chose custom provider — opening settings');
welcomeWin.close();
// Open provider settings and wait for user to save
(0, providerSettings_1.openProviderSettings)(providerService);
// Listen for the settings save to complete before continuing
const settingsSaveHandler = async () => {
console.log('[Welcome] Provider settings saved, syncing and continuing startup');
syncProviderToEndpoints(providerService);
electron_1.ipcMain.removeListener('provider:settings-saved', settingsSaveHandler);
resolve();
};
electron_1.ipcMain.on('provider:settings-saved', settingsSaveHandler);
return; // Don't resolve yet - wait for settings save
}
resolve();
});
});
}
/**
* Fired when all windows have been closed.
* On macOS the app (and LS) stay alive so the user can re-open via the tray.
* On all other platforms, shut down the LS and quit.
*/
electron_1.app.on('window-all-closed', async () => {
if (isQuitting) {
return;
}
if (!hasStartedMainApplication) {
return;
}
// Show provider settings on first launch if no API key configured
const activeProviderType = providerService.getActiveProvider();
const activeProviderConfig = providerService.getProviderConfig(activeProviderType);
if (activeProviderConfig && activeProviderConfig.requiresApiKey && !activeProviderConfig.apiKey) {
console.log('[Provider] No API key configured for active provider, showing settings...');
// Delay slightly so the main window renders first
setTimeout(() => {
(0, providerSettings_1.openProviderSettings)(providerService);
}, 2000);
}
const runInBackground = await settingsService.getSetting(settingsService_1.SettingKey.RUN_IN_BACKGROUND);
if (!runInBackground) {
// Triggers 'before-quit' to run graceful cleanup without confirmation.
electron_1.app.quit();
}
else {
electron_1.app.dock?.hide();
}
});
/**
* Fired just before the app quits (e.g. Cmd+Q on macOS, or after
* window-all-closed on non-macOS). Ensures the LS is terminated even if
* window-all-closed didn't handle it (e.g. on macOS quit via menu).
*/
electron_1.app.on('before-quit', async (event) => {
if (isQuitting) {
return;
}
if (!utils_1.showQuitConfirmation) {
event.preventDefault();
isQuitting = true;
// Destroy all windows to terminate renderers and release keep-alive sockets
const windows = electron_1.BrowserWindow.getAllWindows();
for (const win of windows) {
win.destroy();
}
// Stop API proxy
if (apiProxy) {
apiProxy.stop();
}
// Close all active connections and kill the language server in parallel
await Promise.all([
electron_1.session.defaultSession.closeAllConnections().catch((err) => {
console.error('Failed to close session connections:', err);
}),
(0, languageServer_1.killLanguageServer)(),
]);
electron_1.app.quit();
return;
}
// Show a confirmation dialog before quitting
event.preventDefault();
const win = electron_1.BrowserWindow.getFocusedWindow() || electron_1.BrowserWindow.getAllWindows()[0];
const options = {
type: 'question',
buttons: ['Cancel', 'Quit'],
defaultId: 1,
cancelId: 0,
title: 'Confirm Quit',
message: 'Are you sure you want to quit?',
detail: 'There may be agents or background tasks running.',
};
(0, utils_1.setShowQuitConfirmation)(false);
if (win) {
void electron_1.dialog.showMessageBox(win, options).then((result) => {
if (result.response === 1) {
// Quit - this will retrigger 'before-quit'
electron_1.app.quit();
}
});
}
});
/**
* Fired when the app is re-activated (e.g. clicking the dock icon on macOS).
* Re-opens a window if none are currently open.
*/
electron_1.app.on('activate', () => {
// On Mac, re-open a window when the user clicks the dock
// icon and no windows are open.
if (!HEADLESS && electron_1.BrowserWindow.getAllWindows().length === 0) {
const url = DEV_URL ?? `${constants_1.WINDOW_ORIGIN}:${(0, languageServer_1.getLsPort)()}/`;
(0, utils_1.createWindow)(url);
}
});