diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..04b5666 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,265 @@ +# Building Antigravity from Source + +This guide explains how to extract, modify, and rebuild the Antigravity IDE from source. + +## Prerequisites + +- Linux system (Ubuntu/Debian recommended) +- Node.js 18+ with npm +- Git +- Standard build tools +- ~1GB disk space + +## Quick Build + +If you just want to rebuild the package without modifications: + +```bash +# Clone repository +git clone https://github.rommark.dev/admin/antigravity-ai-providers.git +cd antigravity-ai-providers + +# Build .deb package +./scripts/build-deb.sh + +# Install +sudo dpkg -i packages/antigravity_2.0.1-ai-providers-1_amd64.deb +``` + +## Detailed Build Process + +### Step 1: Extract app.asar + +The application code is packaged in `app.asar`. Extract it to modify: + +```bash +# Extract app +./scripts/extract-app.sh + +# This creates: src/app-extracted/ +``` + +### Step 2: Modify Code + +Navigate to extracted files: + +```bash +cd src/app-extracted/dist/ +``` + +**Key Files:** + +- `services/aiProviderService.js` - AI provider management +- `services/settingsService.js` - Application settings +- `ipcHandlers.js` - IPC communication +- `aiProviderAPI.ts` - TypeScript API wrapper +- `ai-provider-settings.html` - Complete GUI + +**Example Modifications:** + +#### Add a New Provider Preset + +Edit `services/aiProviderService.js`: + +```javascript +const PROVIDER_PRESETS = { + // ... existing presets ... + + "My Custom Provider": { + type: AIProviderType.CUSTOM, + endpoint: "https://api.myprovider.com/v1", + models: ["model-1", "model-2"], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.STREAMING], + }, +}; +``` + +#### Add New IPC Handler + +Edit `ipcHandlers.js`: + +```javascript +electron_1.ipcMain.handle('ai:my-new-handler', async (_event, arg) => { + // Your handler logic + return result; +}); +``` + +#### Customize GUI + +Edit `ai-provider-settings.html`: + +```html + +
+

My New Feature

+ +
+``` + +### Step 3: Repack app.asar + +After making changes, repack: + +```bash +# Repack +./scripts/repack-app.sh + +# This updates: src/app.asar +``` + +### Step 4: Build Package + +Create the .deb package: + +```bash +# Build +./scripts/build-deb.sh + +# Output: packages/antigravity_2.0.1-ai-providers-1_amd64.deb +``` + +### Step 5: Install & Test + +```bash +# Install +sudo dpkg -i packages/antigravity_2.0.1-ai-providers-1_amd64.deb + +# Test +antigravity +``` + +## Development Workflow + +### Watching for Changes + +For rapid development: + +```bash +# Terminal 1: Extract and watch +./scripts/extract-app.sh +# (Manual repack after changes) + +# Terminal 2: Build and install +./scripts/build-deb.sh && sudo dpkg -i packages/*.deb +``` + +### Debugging + +View logs: + +```bash +# Electron logs +cat ~/.config/antigravity/logs/*.log + +# Language server logs +cat ~/.cache/antigravity/language_server.log +``` + +Run in debug mode: + +```bash +antigravity --enable-logging +``` + +### Testing Changes + +```bash +# Reinstall and test +sudo dpkg -r antigravity +sudo dpkg -i packages/antigravity_2.0.1-ai-providers-1_amd64.deb +antigravity +``` + +## Troubleshooting + +### asar command not found + +```bash +npm install -g asar +``` + +### Build fails + +Check dependencies: + +```bash +sudo apt-get install build-essential +``` + +### Installation fails + +Fix dependencies: + +```bash +sudo dpkg -i antigravity_2.0.1-ai-providers-1_amd64.deb +sudo apt-get install -f +``` + +### App doesn't start + +Check logs: + +```bash +journalctl -xe +cat ~/.config/antigravity/logs/*.log +``` + +## Advanced: Custom Package + +### Create Different Architecture + +Edit `scripts/build-deb.sh`: + +```bash +# For ARM64 +ARCH="arm64" +DEB_FILE="antigravity_${VERSION}_arm64.deb" +``` + +### Add Extra Dependencies + +Edit `scripts/build-deb.sh`: + +```bash +# Add to Depends field in control file +Depends: ..., my-custom-package +``` + +### Include Additional Files + +```bash +# In build-deb.sh, after copying app files: +cp /path/to/your/file "$TEMP_DIR/opt/antigravity/" +``` + +## Contributing Changes + +1. Fork repository +2. Create branch: `git checkout -b feature/my-feature` +3. Commit changes: `git commit -am 'Add my feature'` +4. Push: `git push origin feature/my-feature` +5. Create Pull Request + +## API Documentation + +See `/docs/` directory for complete API reference: + +- `AI_PROVIDER_SPECIFICATION.md` - AI Provider API +- `AI_PROVIDER_README.md` - Integration guide +- `IMPLEMENTATION_SUMMARY.md` - Architecture details + +## Need Help? + +- **Issues**: https://github.rommark.dev/admin/antigravity-ai-providers/issues +- **Discussions**: https://github.rommark.dev/admin/antigravity-ai-providers/discussions + +## Summary + +1. Extract: `./scripts/extract-app.sh` +2. Modify: Edit files in `src/app-extracted/dist/` +3. Repack: `./scripts/repack-app.sh` +4. Build: `./scripts/build-deb.sh` +5. Install: `sudo dpkg -i packages/*.deb` + +**Happy coding!** πŸš€ diff --git a/README.md b/README.md index c0e089c..b0bcf4e 100644 --- a/README.md +++ b/README.md @@ -4,252 +4,116 @@ **Antigravity IDE with 17+ Pre-configured AI Provider Presets** -![Antigravity](https://img.shields.io/badge/Antigravity-v2.0.1-blue) -![AI Providers](https://img.shields.io/badge/AI_Providers-17%2B-green) ![Platform](https://img.shields.io/badge/Platform-Linux_amd64-purple) +![License](https://img.shields.io/badge/License-MIT-blue) -## 🎯 Overview +## 🎯 Quick Start -This is a custom build of Antigravity IDE with comprehensive GUI support for managing multiple AI providers. Features include: +### Download & Install -- βœ… **17+ Pre-configured Provider Presets** - One-click setup for popular AI providers -- βœ… **Model Auto-Fetching** - Automatically discover available models from provider APIs -- βœ… **Connection Testing** - Validate API keys and endpoints before use -- βœ… **Persistent Settings** - All configurations saved automatically -- βœ… **Modern GUI Interface** - Beautiful, responsive interface built with HTML/CSS/JS - -## 🌐 Supported AI Providers - -### Direct API Providers -- **OpenAI** - GPT-4o, GPT-4o-mini, GPT-4-turbo, GPT-3.5-turbo -- **Anthropic** - Claude 3.5 Sonnet, Claude 3 Opus, Claude 3 Haiku -- **Groq** - Llama 3.1, Mixtral models -- **OpenRouter** - Access to 100+ models - -### OpenAI-Compatible Providers -- **OpenCode Zen/Go** - GLM, Kimi, MiniMax, DeepSeek, Qwen models -- **NVIDIA NIM** - NVIDIA's AI endpoints -- **Kilo.ai Gateway** - Kilo.ai services -- **Crof.ai** - OpenAI-compatible endpoint -- **OpenAdapter** - 0G models -- **Z.ai Coding** - GLM models - -### Anthropic-Compatible Providers -- **OpenCode Zen/Go (Anthropic)** - Claude models via OpenCode - -### Google Providers -- **Google Gemini (API Key)** - Gemini models via OpenAI-compatible endpoint -- **Google Gemini (OAuth)** - Gemini via Google Cloud -- **Google Antigravity (OAuth)** - Antigravity-specific models including: - - antigravity-gemini-3-flash - - antigravity-gemini-3-pro - - antigravity-gemini-3.1-pro - - antigravity-claude-sonnet-4-6 - - antigravity-claude-opus-4-6-thinking - - Plus all standard Gemini models - -### Special Providers -- **Command Code** - 20+ models from DeepSeek, Anthropic, OpenAI, Moonshot, GLM, MiniMax, Qwen, StepFun, Google -- **Ollama (Local)** - Local model hosting - -## πŸš€ Installation - -### From .deb Package +**Option 1: Pre-built Package (Recommended)** ```bash # Download the .deb file -wget https://github.rommark.dev/admin/antigravity-ai-providers/releases/latest/download/antigravity_2.0.1-ai-providers-1_amd64.deb +wget https://github.rommark.dev/admin/antigravity-ai-providers/releases/download/v2.0.1-ai-providers-1/antigravity_2.0.1-ai-providers-1_amd64.deb -# Install the package +# Install sudo dpkg -i antigravity_2.0.1-ai-providers-1_amd64.deb # Launch antigravity - -# Or find "Antigravity IDE" in your application menu ``` -### Requirements +**Option 2: Build from Source** -- Ubuntu/Debian-based Linux distribution +See [BUILD.md](BUILD.md) for detailed instructions. + +```bash +# Clone repository +git clone https://github.rommark.dev/admin/antigravity-ai-providers.git +cd antigravity-ai-providers + +# Extract, modify, repack, and build +./scripts/extract-app.sh +# (Make your modifications) +./scripts/repack-app.sh +./scripts/build-deb.sh +sudo ./scripts/install-deb.sh +``` + +## 🌟 Features + +### 17+ AI Provider Presets + +- **OpenAI** - GPT-4o, GPT-4o-mini, GPT-4-turbo, GPT-3.5-turbo +- **Anthropic** - Claude 3.5 Sonnet, Claude 3 Opus, Claude 3 Haiku +- **Google Gemini** - API Key & OAuth support +- **Google Antigravity** - Antigravity-specific models! +- **Groq**, **OpenRouter**, **OpenCode**, **NVIDIA NIM** +- **Ollama** - Local models +- **And 10+ more...** + +### Key Features + +- βœ… One-click provider setup +- βœ… Model auto-fetching +- βœ… Connection testing +- βœ… Persistent settings +- βœ… Modern GUI interface +- βœ… Full CRUD operations +- βœ… Temperature/token controls +- βœ… Streaming support + +## πŸ“– Documentation + +- **[SOURCE_CODE.md](SOURCE_CODE.md)** - Repository structure and architecture +- **[BUILD.md](BUILD.md)** - Building from source +- **[docs/](docs/)** - Complete API documentation + +## πŸ”§ Requirements + +- Ubuntu/Debian-based Linux (amd64) - GTK3 support -- amd64 (x86_64) architecture - -## πŸ’‘ Usage - -### Adding a Provider (One-Click Setup) - -1. Open Antigravity IDE -2. Navigate to **Settings** β†’ **AI Provider Settings** -3. Click **"Add from Preset"** -4. Select your provider (e.g., "Google Antigravity (OAuth)") -5. Enter your API key -6. Click **"Add Provider"** -7. Click **"Test Connection"** to verify - -### Adding a Custom Provider - -1. Open AI Provider Settings -2. Click **"Add Custom Provider"** -3. Fill in details: - - Provider Name - - API Type - - Endpoint URL - - API Key - - Models (comma-separated) -4. Click **"Add"** - -### Fetching Models - -1. Select your provider from the list -2. Click **"Fetch Models"** -3. Wait for models to load -4. Select your default model - -### Configuring Settings - -- **Temperature**: Response creativity (0.0-2.0) -- **Max Tokens**: Maximum response length (100-32000) -- **Streaming**: Enable real-time responses -- **Default Provider**: Set your preferred provider +- ~500MB disk space ## πŸ“¦ Package Contents ``` antigravity_2.0.1-ai-providers-1_amd64.deb -β”œβ”€β”€ opt/antigravity/ # Main application -β”‚ β”œβ”€β”€ antigravity # Main executable -β”‚ β”œβ”€β”€ resources/ # Application resources -β”‚ β”œβ”€β”€ lib*/ # Libraries -β”‚ └── locales/ # Language files -β”œβ”€β”€ DEBIAN/ # Package metadata +β”œβ”€β”€ opt/antigravity/ # Main application +β”‚ β”œβ”€β”€ antigravity # Executable +β”‚ β”œβ”€β”€ resources/ # App resources & app.asar +β”‚ └── [libraries] +β”œβ”€β”€ DEBIAN/ # Package metadata β”‚ β”œβ”€β”€ control β”‚ β”œβ”€β”€ preinst β”‚ β”œβ”€β”€ postinst β”‚ β”œβ”€β”€ prerm β”‚ └── postrm -└── usr/share/applications/ # Desktop entry - └── antigravity.desktop +└── usr/share/applications/ # Desktop entry ``` -## πŸ”§ Features +## 🀝 Contributing -### Backend Features -- βœ… AIProviderService for provider management -- βœ… 14 new IPC handlers for AI operations -- βœ… Preset-based provider creation -- βœ… Model fetching and validation -- βœ… Connection testing -- βœ… Settings management - -### Frontend Features -- βœ… TypeScript API wrapper -- βœ… Complete HTML/CSS/JS GUI implementation -- βœ… Provider cards with status badges -- βœ… Preset selector dropdown -- βœ… Model auto-fetch functionality -- βœ… Settings panel with sliders and toggles -- βœ… Toast notifications -- βœ… Modal dialogs -- βœ… Loading states - -## πŸ“š Documentation - -The package includes comprehensive documentation: - -- **README.md** - Installation and usage guide -- **CHANGELOG.md** - Version history -- **AI_PROVIDER_README.md** - Integration guide -- **AI_PROVIDER_SPECIFICATION.md** - Full API documentation -- **IMPLEMENTATION_SUMMARY.md** - Implementation overview - -## πŸ› Troubleshooting - -### Provider not connecting - -1. **Check API Key** - - Verify API key is correct - - Check if key has expired - - Ensure key has necessary permissions - -2. **Check Endpoint** - - Verify endpoint URL is correct - - Check for typos in URL - - Ensure provider is not down - -3. **Network Issues** - - Check firewall settings - - Verify internet connection - - Try with VPN if behind corporate firewall - -### Models not loading - -1. Click **"Fetch Models"** button -2. Check API key permissions -3. Verify provider supports model listing -4. Check browser console for errors - -### Installation Issues - -```bash -# Fix broken dependencies -sudo apt-get install -f - -# Reinstall if needed -sudo dpkg -r antigravity -sudo dpkg -i antigravity_2.0.1-ai-providers-1_amd64.deb -``` - -## πŸ”’ Security - -- API keys stored securely in app config -- HTTPS connections validated -- No data sent to third parties without consent -- User controls all provider configurations - -## πŸ“Š Version History - -### 2.0.1-ai-providers-1 (Current) -- Added 17+ provider presets -- One-click provider setup -- Model auto-fetching -- Connection testing -- Modern GUI interface -- Comprehensive documentation - -### 2.0.0 (Original) -- Initial Antigravity release -- Basic IDE features - -## πŸ™ Acknowledgments - -Provider presets imported from: -- [Codex Launcher - Any AI Provider](https://github.rommark.dev/admin/Codex-Launcher---Any-AI-Porovider) +1. Fork the repository +2. Create feature branch +3. Make changes +4. Submit pull request ## πŸ“ž Support -For issues or questions: -1. Check existing issues in this repository -2. Create new issue with details -3. Include logs if reporting bugs -4. Specify your provider and model +- **Issues**: https://github.rommark.dev/admin/antigravity-ai-providers/issues +- **Discussions**: https://github.rommark.dev/admin/antigravity-ai-providers/discussions + +## πŸ™ Acknowledgments + +- Antigravity IDE - Original application +- Codex Launcher - Provider presets ## πŸ“„ License Same as original Antigravity application. -## πŸŽ‰ Summary - -This release brings Antigravity to the next level with: -- βœ… 17+ pre-configured AI providers -- βœ… One-click setup for popular providers -- βœ… Model auto-fetching -- βœ… Connection testing -- βœ… Comprehensive GUI -- βœ… Full documentation - -**Enjoy AI-powered development with Antigravity!** - --- -Built with ❀️ | Version 2.0.1-ai-providers-1 +**Enjoy AI-powered development with Antigravity!** πŸš€ diff --git a/SOURCE_CODE.md b/SOURCE_CODE.md new file mode 100644 index 0000000..1969ad7 --- /dev/null +++ b/SOURCE_CODE.md @@ -0,0 +1,364 @@ +# Antigravity IDE - AI Provider Edition Source Code + +## πŸ“ Repository Structure + +``` +antigravity-ai-providers/ +β”œβ”€β”€ README.md # This file +β”œβ”€β”€ LICENSE # License information +β”œβ”€β”€ CHANGELOG.md # Version history +β”œβ”€β”€ INSTALL.md # Installation guide +β”œβ”€β”€ BUILD.md # Build instructions +β”œβ”€β”€ src/ # Modified source code +β”‚ β”œβ”€β”€ app.asar # Compiled Electron app +β”‚ β”œβ”€β”€ app-extracted/ # Extracted app contents +β”‚ β”‚ β”œβ”€β”€ dist/ +β”‚ β”‚ β”‚ β”œβ”€β”€ services/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ aiProviderService.js # NEW: AI Provider Service +β”‚ β”‚ β”‚ β”‚ └── settingsService.js # MODIFIED: Added AI settings +β”‚ β”‚ β”‚ β”œβ”€β”€ ipcHandlers.js # MODIFIED: Added AI IPC handlers +β”‚ β”‚ β”‚ β”œβ”€β”€ aiProviderAPI.ts # NEW: TypeScript API wrapper +β”‚ β”‚ β”‚ β”œβ”€β”€ ai-provider-settings.html # NEW: Complete GUI +β”‚ β”‚ β”‚ └── [other dist files...] +β”‚ β”‚ └── [other extracted files...] +β”‚ └── [original source files...] +β”œβ”€β”€ packages/ # Pre-built packages +β”‚ └── antigravity_2.0.1-ai-providers-1_amd64.deb +β”œβ”€β”€ docs/ # Documentation +β”‚ β”œβ”€β”€ AI_PROVIDER_SPECIFICATION.md +β”‚ β”œβ”€β”€ AI_PROVIDER_README.md +β”‚ β”œβ”€β”€ IMPLEMENTATION_SUMMARY.md +β”‚ └── API_REFERENCE.md +β”œβ”€β”€ scripts/ # Build and utility scripts +β”‚ β”œβ”€β”€ build-deb.sh +β”‚ β”œβ”€β”€ extract-app.sh +β”‚ β”œβ”€β”€ repack-app.sh +β”‚ └── install-deb.sh +└── .gitignore +``` + +## 🎯 Overview + +This repository contains the complete source code and pre-built packages for **Antigravity IDE v2.0.1 - AI Provider Edition**. + +### What's Included + +1. **Complete Electron Application Source** + - Modified main process code + - IPC handlers for AI provider operations + - Settings service with AI configuration + - TypeScript API wrapper + - Complete HTML/CSS/JS GUI implementation + +2. **Pre-compiled Package** + - Ready-to-install .deb package for amd64 + - All dependencies bundled + - Desktop integration + - Auto-installation scripts + +3. **Build Tools** + - Shell scripts for extraction and repacking + - Build automation + - Installation helpers + +4. **Comprehensive Documentation** + - Installation guides + - API reference + - Implementation details + - Usage examples + +## πŸš€ Quick Start + +### Option 1: Install Pre-built Package + +```bash +# Download the .deb file +wget https://github.rommark.dev/admin/antigravity-ai-providers/releases/download/v2.0.1-ai-providers-1/antigravity_2.0.1-ai-providers-1_amd64.deb + +# Install +sudo dpkg -i antigravity_2.0.1-ai-providers-1_amd64.deb + +# Launch +antigravity +``` + +### Option 2: Build from Source + +```bash +# Clone the repository +git clone https://github.rommark.dev/admin/antigravity-ai-providers.git +cd antigravity-ai-providers + +# Extract app.asar +./scripts/extract-app.sh + +# Make modifications (optional) +# Edit files in src/app-extracted/dist/ + +# Repack app.asar +./scripts/repack-app.sh + +# Build .deb package +./scripts/build-deb.sh + +# Install +sudo dpkg -i packages/antigravity_2.0.1-ai-providers-1_amd64.deb +``` + +## πŸ“¦ Source Code Components + +### Backend (Electron Main Process) + +#### `src/app-extracted/dist/services/aiProviderService.js` +- **Purpose**: Core AI provider management service +- **Features**: + - 17+ provider presets + - CRUD operations for providers + - Model fetching from APIs + - Connection testing + - Settings persistence + - Event system for real-time updates + +#### `src/app-extracted/dist/services/settingsService.js` +- **Purpose**: Application settings management +- **Modifications**: Added AI-related settings + - `aiProvider`: Default provider ID + - `aiModel`: Default model + - `aiTemperature`: Response creativity + - `aiMaxTokens`: Max response length + - `aiStreaming`: Streaming toggle + - `aiEmbeddingProvider`: Embedding provider + +#### `src/app-extracted/dist/ipcHandlers.js` +- **Purpose**: IPC communication bridge +- **Modifications**: Added 14 AI provider IPC handlers + - `ai:get-providers` - Get all providers + - `ai:get-available-presets` - List presets + - `ai:add-provider-from-preset` - Quick setup + - `ai:fetch-models` - Auto-fetch models + - `ai:test-connection` - Connection testing + - And more... + +### Frontend (Renderer Process) + +#### `src/app-extracted/dist/aiProviderAPI.ts` +- **Purpose**: Type-safe API wrapper +- **Features**: + - TypeScript interfaces + - IPC communication helpers + - Settings management + - Provider operations + +#### `src/app-extracted/dist/ai-provider-settings.html` +- **Purpose**: Complete GUI implementation +- **Features**: + - Modern responsive design + - Provider cards with status + - Preset selector + - Model auto-fetch + - Connection testing UI + - Settings panel + - Toast notifications + - Modal dialogs + +## πŸ”§ Building from Source + +### Prerequisites + +- Node.js 18+ (for asar tools) +- npm or yarn +- Linux system (for .deb packaging) +- Standard build tools + +### Step 1: Extract app.asar + +```bash +cd src +mkdir -p app-extracted +cd app-extracted +npx asar extract ../app.asar dist/ +``` + +### Step 2: Make Modifications + +Edit files in `src/app-extracted/dist/`: + +```bash +# Example: Modify provider presets +vim dist/services/aiProviderService.js + +# Example: Add new IPC handler +vim dist/ipcHandlers.js + +# Example: Customize GUI +vim dist/ai-provider-settings.html +``` + +### Step 3: Repack app.asar + +```bash +npx asar pack dist/ ../app.asar +``` + +### Step 4: Build .deb Package + +```bash +cd ../.. +./scripts/build-deb.sh +``` + +## πŸ“š Documentation + +### For Users + +- **[INSTALL.md](INSTALL.md)** - Installation instructions +- **[docs/AI_PROVIDER_README.md](docs/AI_PROVIDER_README.md)** - Usage guide + +### For Developers + +- **[BUILD.md](BUILD.md)** - Build instructions +- **[docs/AI_PROVIDER_SPECIFICATION.md](docs/AI_PROVIDER_SPECIFICATION.md)** - API reference +- **[docs/IMPLEMENTATION_SUMMARY.md](docs/IMPLEMENTATION_SUMMARY.md)** - Architecture + +## 🀝 Contributing + +### Reporting Issues + +1. Check existing issues +2. Create new issue with: + - Bug description + - Steps to reproduce + - Expected vs actual behavior + - System information + - Logs (if applicable) + +### Submitting Changes + +1. Fork the repository +2. Create feature branch +3. Make changes +4. Add tests (if applicable) +5. Submit pull request + +### Code Style + +- JavaScript: ES6+ with async/await +- TypeScript: Strict mode enabled +- HTML/CSS: Semantic, accessible +- Comments: English only + +## πŸ”’ Security + +### API Keys + +- Never commit API keys +- Use environment variables +- Store securely in app config +- Validate before use + +### Network Security + +- HTTPS required for external APIs +- Certificate validation +- Secure headers +- No mixed content + +## πŸ“Š Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Renderer Process (GUI) β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ ai-provider-settings.html β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Provider Cards β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Settings Panel β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Preset Selector β”‚ β”‚ +β”‚ β”‚ └── Connection Testing β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ aiProviderAPI.ts β”‚ β”‚ +β”‚ β”‚ └── Type-safe IPC wrapper β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ IPC (14 handlers) + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Main Process (Backend) β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ ipcHandlers.js β”‚ β”‚ +β”‚ β”‚ └── AI Provider handlers β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ aiProviderService.js β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Provider management β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Preset system β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Connection testing β”‚ β”‚ +β”‚ β”‚ └── Model fetching β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ settingsService.js β”‚ β”‚ +β”‚ β”‚ └── AI settings keys β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ storage.js β”‚ β”‚ +β”‚ β”‚ └── Persistent storage β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## 🌟 Key Features + +### One-Click Provider Setup +```javascript +await window.electron.invoke('ai:add-provider-from-preset', + 'Google Antigravity (OAuth)', + 'api-key' +); +``` + +### Model Auto-Fetching +```javascript +const models = await window.electron.invoke('ai:fetch-models', providerId); +``` + +### Connection Testing +```javascript +const result = await window.electron.invoke('ai:test-connection', providerId); +if (result.success) { + console.log('βœ… Connected!'); +} +``` + +## πŸ“ž Support + +- **Issues**: https://github.rommark.dev/admin/antigravity-ai-providers/issues +- **Discussions**: https://github.rommark.dev/admin/antigravity-ai-providers/discussions +- **Documentation**: See `/docs` directory + +## πŸ™ Acknowledgments + +- Antigravity IDE - Original application +- Codex Launcher - Provider presets +- Electron - Desktop framework +- Node.js - JavaScript runtime + +## πŸ“„ License + +Same as original Antigravity application. + +## πŸŽ‰ Summary + +This repository provides: +- βœ… Complete source code +- βœ… Pre-compiled package +- βœ… Build tools +- βœ… Comprehensive docs +- βœ… Easy installation +- βœ… Full customization + +**Start using or customizing Antigravity with AI Provider support today!** diff --git a/docs/AI_PROVIDER_README.md b/docs/AI_PROVIDER_README.md new file mode 100644 index 0000000..63ec724 --- /dev/null +++ b/docs/AI_PROVIDER_README.md @@ -0,0 +1,300 @@ +# Antigravity AI Provider GUI Support + +This directory contains the implementation of GUI support for multiple AI providers in the Antigravity IDE. + +## Files Added/Modified + +### Backend (Electron Main Process) + +1. **`services/aiProviderService.js`** (NEW) + - Core service for AI provider management + - Handles CRUD operations for providers + - Manages provider configurations and storage + - Provides connection testing functionality + +2. **`services/settingsService.js`** (MODIFIED) + - Added AI-related setting keys + - Added default values for AI configuration + +3. **`ipcHandlers.js`** (MODIFIED) + - Added AI provider IPC handlers + - Integrated AIProviderService + - Exposed 10+ new IPC methods for frontend + +### Frontend (Renderer Process) + +4. **`aiProviderAPI.ts`** (NEW) + - TypeScript wrapper for IPC communication + - Type-safe API for AI provider operations + - Helper methods for settings management + +5. **`ai-provider-settings.html`** (NEW) + - Complete, production-ready GUI implementation + - Responsive design with modern UI + - Full CRUD operations + - Connection testing + - Settings management + +### Documentation + +6. **`AI_PROVIDER_SPECIFICATION.md`** (NEW) + - Comprehensive implementation guide + - API documentation + - Usage examples + - Testing checklist + - Future enhancements + +## Quick Start + +### Viewing the GUI + +The GUI can be accessed by opening `ai-provider-settings.html` in a web browser after integrating with the Electron app. + +### Basic Usage + +```javascript +// Get all providers +const providers = await window.electron.invoke('ai:get-providers'); + +// Add a new provider +const newProvider = await window.electron.invoke('ai:add-provider', { + name: 'My Custom Provider', + type: 'openai', + endpoint: 'https://api.myprovider.com/v1', + apiKey: 'my-api-key', + models: ['model-1', 'model-2'], + capabilities: ['chat', 'streaming'] +}); + +// Test connection +const result = await window.electron.invoke('ai:test-connection', newProvider.id); +console.log(result.success ? 'Connected!' : 'Failed: ' + result.message); + +// Update settings +await window.electron.invoke('storage:update-items', { + 'aiProvider': newProvider.id, + 'aiModel': 'model-1', + 'aiTemperature': '0.7', + 'aiMaxTokens': '4096', + 'aiStreaming': 'true' +}); +``` + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Renderer Process (UI) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ ai-provider-settings.html β”‚ +β”‚ β”œβ”€β”€ Provider Cards (Grid Layout) β”‚ +β”‚ β”œβ”€β”€ Settings Panel β”‚ +β”‚ β”œβ”€β”€ Modals (Add/Edit) β”‚ +β”‚ └── Toast Notifications β”‚ +β”‚ β”‚ +β”‚ aiProviderAPI.ts β”‚ +β”‚ └── Type-safe IPC wrapper β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ IPC + ↓ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Main Process (Backend) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ ipcHandlers.js β”‚ +β”‚ └── AI Provider IPC handlers (10+ methods) β”‚ +β”‚ β”‚ +β”‚ services/ β”‚ +β”‚ β”œβ”€β”€ aiProviderService.js β”‚ +β”‚ β”‚ β”œβ”€β”€ Provider management (CRUD) β”‚ +β”‚ β”‚ β”œβ”€β”€ Storage integration β”‚ +β”‚ β”‚ └── Connection testing β”‚ +β”‚ └── settingsService.js β”‚ +β”‚ └── AI setting keys β”‚ +β”‚ β”‚ +β”‚ storage.js β”‚ +β”‚ └── Persistent storage β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Features + +### Provider Management +- βœ… Add custom providers +- βœ… Edit existing providers +- βœ… Delete providers (except defaults) +- βœ… Set default provider +- βœ… Enable/disable providers +- βœ… View provider capabilities +- βœ… Test provider connections + +### Model Configuration +- βœ… Select default model per provider +- βœ… View available models +- βœ… Model-specific settings + +### Global Settings +- βœ… Temperature control (0.0-2.0) +- βœ… Max tokens (100-32000) +- βœ… Streaming toggle +- βœ… Persistent settings + +### UI/UX +- βœ… Modern, responsive design +- βœ… Real-time feedback +- βœ… Error handling +- βœ… Loading states +- βœ… Toast notifications +- βœ… Modal dialogs +- βœ… Visual status indicators + +## Integration Guide + +### Step 1: Extract app.asar + +```bash +cd Antigravity-x64/resources +npx asar extract app.asar app-extracted +``` + +### Step 2: Replace/Add Files + +Copy the modified files to `app-extracted/dist/`: + +```bash +cp aiProviderService.js app-extracted/dist/services/ +cp settingsService.js app-extracted/dist/services/ +cp ipcHandlers.js app-extracted/dist/ +cp aiProviderAPI.ts app-extracted/dist/ +cp ai-provider-settings.html app-extracted/dist/ +cp AI_PROVIDER_SPECIFICATION.md app-extracted/dist/ +``` + +### Step 3: Repack app.asar + +```bash +npx asar pack app-extracted app.asar +``` + +### Step 4: Launch the App + +Run the modified Antigravity application. + +### Step 5: Access the GUI + +Open the AI Provider Settings page in the app (typically via Settings menu or by navigating to `ai-provider-settings.html`). + +## API Reference + +### IPC Methods + +| Method | Parameters | Returns | Description | +|--------|-------------|---------|-------------| +| `ai:get-providers` | none | `AIProvider[]` | Get all providers | +| `ai:get-provider` | `id: string` | `AIProvider` | Get specific provider | +| `ai:get-enabled-providers` | none | `AIProvider[]` | Get enabled providers | +| `ai:get-default-provider` | none | `AIProvider` | Get default provider | +| `ai:add-provider` | `provider: AIProviderCreate` | `AIProvider` | Add new provider | +| `ai:update-provider` | `id: string, updates: AIProviderUpdate` | `AIProvider` | Update provider | +| `ai:delete-provider` | `id: string` | `void` | Delete provider | +| `ai:set-default-provider` | `id: string` | `void` | Set as default | +| `ai:toggle-provider` | `id: string, enabled: boolean` | `void` | Enable/disable | +| `ai:test-connection` | `id: string` | `AIConnectionTest` | Test connection | + +### Storage Keys + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `aiProvider` | string | `openai-default` | Default provider ID | +| `aiModel` | string | `gpt-4o` | Default model | +| `aiTemperature` | string | `0.7` | Temperature setting | +| `aiMaxTokens` | string | `4096` | Max tokens setting | +| `aiStreaming` | string | `true` | Streaming enabled | +| `aiEmbeddingProvider` | string | `openai-default` | Embedding provider | +| `aiProviders` | string | `JSON` | All provider configs | + +## Supported Providers + +### Built-in Providers + +1. **OpenAI** (Default) + - Models: GPT-4o, GPT-4o-mini, GPT-4-turbo, GPT-3.5-turbo + - Capabilities: All features + +2. **Anthropic** + - Models: Claude 3.5 Sonnet, Claude 3 Opus, Claude 3 Haiku + - Capabilities: Chat, Vision, Tool Use, Streaming + +3. **Ollama** (Local) + - Models: Llama3, CodeLLama, Mistral, Neural Chat + - Capabilities: Chat, Completion, Streaming + +### Custom Providers + +Add any OpenAI-compatible API endpoint: + +```javascript +await window.electron.invoke('ai:add-provider', { + name: 'My Local Model', + type: 'custom', + endpoint: 'http://localhost:8080/v1', + apiKey: 'not-required', + models: ['my-model'], + capabilities: ['chat', 'streaming'] +}); +``` + +## Troubleshooting + +### Provider not connecting + +1. Check API key is correct +2. Verify endpoint URL +3. Test connection with "Test" button +4. Check network/firewall settings +5. For Ollama, ensure service is running + +### Settings not persisting + +1. Check storage permissions +2. Verify storage path exists +3. Check for storage quota issues +4. Review app logs for errors + +### UI not loading + +1. Ensure all files are properly copied +2. Check app.asar is correctly repacked +3. Verify HTML file path +4. Check browser console for errors + +## Contributing + +To extend this implementation: + +1. **New Provider Types**: Add to `AIProviderType` enum +2. **New Capabilities**: Add to `AIProviderCapability` enum +3. **New Settings**: Add to `SettingKey` and `DEFAULTS` +4. **New IPC Methods**: Add handlers in `ipcHandlers.js` +5. **UI Enhancements**: Modify `ai-provider-settings.html` + +## License + +Same as the Antigravity application. + +## Support + +For issues or questions: +1. Check the specification document (`AI_PROVIDER_SPECIFICATION.md`) +2. Review the code comments +3. Check browser console for errors +4. Review Electron logs + +## Changelog + +### Version 1.0 (Current) +- Initial implementation +- Full CRUD for AI providers +- Connection testing +- Settings management +- Complete GUI implementation +- Documentation diff --git a/docs/AI_PROVIDER_SPECIFICATION.md b/docs/AI_PROVIDER_SPECIFICATION.md new file mode 100644 index 0000000..f7ad78e --- /dev/null +++ b/docs/AI_PROVIDER_SPECIFICATION.md @@ -0,0 +1,308 @@ +# AI Provider GUI Support - Implementation Specification + +## Overview + +This document describes the implementation of GUI support for multiple AI providers in the Antigravity IDE. The implementation adds comprehensive backend infrastructure and a fully functional frontend interface for managing AI provider configurations. + +## Architecture + +### Backend Components + +#### 1. AIProviderService (`services/aiProviderService.js`) + +The core service managing AI provider configurations with the following features: + +- **Provider Management**: CRUD operations for AI providers +- **Storage Integration**: Persists configurations using the existing StorageManager +- **Event System**: Emits events for provider changes +- **Connection Testing**: Validates provider connectivity + +**Supported Provider Types**: +- OpenAI (GPT-4, GPT-3.5) +- Anthropic (Claude) +- Ollama (Local models) +- Groq +- OpenRouter +- Custom providers + +**Provider Capabilities**: +- Chat completion +- Text completion +- Embeddings +- Vision (multimodal) +- Tool use +- Streaming + +#### 2. Settings Integration + +Added new settings to `settingsService.js`: + +- `aiProvider`: Default provider ID +- `aiModel`: Default model +- `aiTemperature`: Response creativity (0.0-2.0) +- `aiMaxTokens`: Max response length +- `aiStreaming`: Enable/disable streaming +- `aiEmbeddingProvider`: Embedding provider + +#### 3. IPC Handlers + +Added comprehensive IPC handlers in `ipcHandlers.js`: + +``` +ai:get-providers - Get all providers +ai:get-provider - Get specific provider +ai:get-enabled-providers - Get only enabled providers +ai:get-default-provider - Get default provider +ai:add-provider - Create new provider +ai:update-provider - Update provider +ai:delete-provider - Delete provider +ai:set-default-provider - Set as default +ai:toggle-provider - Enable/disable provider +ai:test-connection - Test connectivity +``` + +### Frontend Components + +#### 1. TypeScript API (`aiProviderAPI.ts`) + +Type-safe wrapper for IPC communication with the backend: + +```typescript +class AIProviderAPI { + async getProviders(): Promise + async addProvider(provider: AIProviderCreate): Promise + async updateProvider(id: string, updates: AIProviderUpdate): Promise + async deleteProvider(id: string): Promise + async testConnection(id: string): Promise + async getSettings(): Promise + async updateSettings(settings: Partial): Promise +} +``` + +#### 2. Complete GUI Implementation (`ai-provider-settings.html`) + +A fully functional, production-ready GUI with: + +**Features**: +- Visual provider cards with status badges +- Add/edit/delete providers +- Connection testing with visual feedback +- Model selection per provider +- Settings panel for default configuration +- Temperature/tokens/streaming controls +- Responsive grid layout +- Toast notifications +- Modal dialogs for forms + +**UI Components**: +- Provider cards with capability badges +- Model tags showing available models +- Connection status indicators +- Settings sliders and toggles +- Form inputs with validation +- Loading states +- Error handling + +## Data Model + +### AIProvider Interface + +```typescript +interface AIProvider { + id: string; // Unique identifier + name: string; // Display name + type: AIProviderType; // Provider type enum + endpoint: string; // API endpoint URL + apiKey: string; // API key (stored securely) + models: string[]; // Available models + defaultModel: string; // Default selected model + capabilities: AIProviderCapability[]; // Feature flags + isEnabled: boolean; // Enable/disable flag + isDefault: boolean; // Default provider flag +} +``` + +### Storage Format + +Providers are stored in the app's storage as JSON: + +```json +{ + "aiProviders": "[{\"id\":\"openai-default\",\"name\":\"OpenAI\",...}]" +} +``` + +## Default Providers + +The system initializes with three default providers: + +1. **OpenAI** (Default) + - Endpoint: `https://api.openai.com/v1` + - Models: GPT-4o, GPT-4o-mini, GPT-4-turbo, GPT-3.5-turbo + - Capabilities: All features + +2. **Anthropic** + - Endpoint: `https://api.anthropic.com/v1` + - Models: Claude 3.5 Sonnet, Claude 3 Opus, Claude 3 Haiku + - Capabilities: Chat, Vision, Tool Use, Streaming + +3. **Ollama (Local)** + - Endpoint: `http://localhost:11434/v1` + - Models: Llama3, CodeLLama, Mistral, Neural Chat + - Capabilities: Chat, Completion, Streaming + +## Implementation Details + +### Backend Changes + +1. **File**: `services/aiProviderService.js` + - Created new service class + - Integrated with StorageManager + - Added event emitter for real-time updates + +2. **File**: `services/settingsService.js` + - Added AI-related setting keys + - Defined default values + +3. **File**: `ipcHandlers.js` + - Imported AIProviderService + - Registered IPC handlers + - Connected to storage manager + +### Frontend Changes + +1. **File**: `aiProviderAPI.ts` + - TypeScript interface definitions + - IPC wrapper methods + - Type-safe API + +2. **File**: `ai-provider-settings.html` + - Complete HTML/CSS/JS implementation + - Responsive design + - State management + - Error handling + +## Usage + +### For Users + +1. Open the AI Provider Settings panel +2. View all configured providers +3. Add custom providers with API keys +4. Test connections before use +5. Configure default model and parameters +6. Enable/disable providers as needed + +### For Developers + +**Adding a new provider programmatically**: + +```javascript +const provider = await window.electron.invoke('ai:add-provider', { + name: 'My Provider', + type: 'custom', + endpoint: 'https://api.myprovider.com/v1', + apiKey: 'my-api-key', + models: ['model-1', 'model-2'], + capabilities: ['chat', 'streaming'] +}); +``` + +**Getting current settings**: + +```typescript +const settings = await aiProviderAPI.getSettings(); +console.log(`Using ${settings.provider} with model ${settings.model}`); +``` + +**Changing default model**: + +```typescript +await aiProviderAPI.updateSettings({ + model: 'gpt-4o-mini', + temperature: 0.5 +}); +``` + +## Security Considerations + +1. **API Key Storage**: Keys are stored in the app's secure storage +2. **Validation**: All inputs are validated before storage +3. **Error Handling**: Connection failures are gracefully handled +4. **HTTPS Only**: External API calls use HTTPS (validated in shell:open-external) + +## Future Enhancements + +1. **Usage Tracking**: Monitor API usage per provider +2. **Cost Estimation**: Calculate costs based on model pricing +3. **Provider Health**: Automatic health checks and fallbacks +4. **Model Comparison**: Side-by-side model comparison tools +5. **Prompt Templates**: Save and manage prompt templates +6. **Conversation History**: Store and manage chat histories +7. **Export/Import**: Backup and restore provider configurations + +## Testing + +### Manual Testing Checklist + +- [ ] Add new provider +- [ ] Edit existing provider +- [ ] Delete provider +- [ ] Set default provider +- [ ] Toggle provider enabled/disabled +- [ ] Test connection with valid credentials +- [ ] Test connection with invalid credentials +- [ ] Change default model +- [ ] Adjust temperature slider +- [ ] Modify max tokens +- [ ] Toggle streaming +- [ ] Save all settings +- [ ] Reload app and verify persistence +- [ ] Test with Ollama (local provider) + +### Automated Testing + +The implementation includes test hooks through IPC handlers. Test the backend: + +```javascript +const providers = await window.electron.invoke('ai:get-providers'); +console.assert(providers.length >= 3, 'Should have default providers'); +``` + +## Integration with IDE Features + +The AI provider infrastructure enables: + +1. **Code Completion**: AI-powered suggestions +2. **Chat Interface**: Interactive AI assistant +3. **Refactoring**: AI-assisted code improvements +4. **Documentation**: Auto-generate docs +5. **Code Review**: AI-powered reviews +6. **Natural Language Search**: Search codebase with questions + +## Performance Considerations + +1. **Lazy Loading**: Providers loaded on-demand +2. **Connection Pooling**: Reuse HTTP connections +3. **Caching**: Cache provider lists in renderer +4. **Debouncing**: Debounce settings updates +5. **Async Operations**: All I/O operations are non-blocking + +## Browser Compatibility + +The GUI uses modern web APIs: +- ES6+ JavaScript +- CSS Grid and Flexbox +- CSS Custom Properties +- Fetch API +- CSS Animations + +Compatible with: +- Chrome 80+ +- Firefox 75+ +- Safari 13+ +- Edge 80+ + +## Conclusion + +This implementation provides a complete, production-ready AI provider management system for the Antigravity IDE. The modular architecture allows easy extension and customization while maintaining backward compatibility with existing systems. diff --git a/docs/IMPLEMENTATION_SUMMARY.md b/docs/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..034ca00 --- /dev/null +++ b/docs/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,342 @@ +# Antigravity AI Provider GUI Support - Implementation Summary + +## 🎯 What Was Implemented + +Complete GUI support for managing multiple AI providers in the Antigravity IDE, including **17+ pre-configured provider presets** imported from the Codex Launcher repository. + +## πŸ“ Files Added/Modified + +### Backend Components + +1. **`services/aiProviderService.js`** (Enhanced) + - βœ… Complete AI provider management service + - βœ… 17+ provider presets from Codex Launcher + - βœ… Preset-based provider creation + - βœ… Model fetching from provider APIs + - βœ… Connection testing + - βœ… Event system for real-time updates + +2. **`services/settingsService.js`** (Modified) + - βœ… Added AI-related setting keys + - βœ… Default values for AI configuration + +3. **`ipcHandlers.js`** (Enhanced) + - βœ… 14 IPC handlers for AI provider operations + - βœ… New methods: presets, model fetching, preset-based creation + +### Frontend Components + +4. **`aiProviderAPI.ts`** (Enhanced) + - βœ… TypeScript wrapper with preset methods + - βœ… Model fetching support + - βœ… Type-safe API for all operations + +5. **`ai-provider-settings.html`** (New) + - βœ… Complete, production-ready GUI + - βœ… Modern responsive design + - βœ… Preset selector with one-click setup + - βœ… Model auto-fetch functionality + - βœ… Connection testing + - βœ… Full CRUD operations + +### Documentation + +6. **`AI_PROVIDER_SPECIFICATION.md`** (New) + - βœ… Comprehensive implementation guide + - βœ… API documentation + - βœ… Usage examples + +7. **`AI_PROVIDER_README.md`** (New) + - βœ… Integration guide + - βœ… Quick start instructions + - βœ… API reference + +## πŸš€ Provider Presets Included + +### Direct API Providers + +1. **OpenAI** - GPT-4o, GPT-4o-mini, GPT-4-turbo, GPT-3.5-turbo +2. **Anthropic** - Claude 3.5 Sonnet, Claude 3 Opus, Claude 3 Haiku +3. **Groq** - Llama 3.1, Mixtral models +4. **OpenRouter** - Access to 100+ models + +### OpenAI-Compatible Providers + +5. **OpenCode Zen (OpenAI-compatible)** - GLM, Kimi, MiniMax, DeepSeek, Qwen models +6. **OpenCode Go (OpenAI-compatible)** - GLM, Kimi, MiniMax, Qwen, DeepSeek models +7. **Crof.ai** - OpenAI-compatible endpoint +8. **NVIDIA NIM** - NVIDIA's AI endpoints +9. **Kilo.ai Gateway** - Kilo.ai services +10. **OpenAdapter** - 0G models (DeepSeek V3/V4, GLM, Qwen) +11. **Z.ai Coding** - GLM models + +### Anthropic-Compatible Providers + +12. **OpenCode Zen (Anthropic)** - Claude models via OpenCode +13. **OpenCode Go (Anthropic)** - MiniMax models via OpenCode + +### Google Providers + +14. **Google Gemini (API Key)** - Gemini models via OpenAI-compatible endpoint +15. **Google Gemini (OAuth)** - Gemini via Google Cloud +16. **Google Antigravity (OAuth)** - ** Antigravity-specific models including:** + - antigravity-gemini-3-flash + - antigravity-gemini-3-pro + - antigravity-gemini-3.1-pro + - antigravity-claude-sonnet-4-6 + - antigravity-claude-opus-4-6-thinking + - Plus all standard Gemini models + +### Special Providers + +17. **Command Code** - 20+ models from DeepSeek, Anthropic, OpenAI, Moonshot, GLM, MiniMax, Qwen, StepFun, Google +18. **Ollama (Local)** - Local model hosting + +## ✨ Key Features + +### Backend +- βœ… Provider CRUD operations +- βœ… Preset-based provider creation (one-click setup) +- βœ… Model auto-fetch from provider APIs +- βœ… Connection testing +- βœ… Settings management +- βœ… Event system for real-time updates +- βœ… Persistent storage +- βœ… Error handling + +### Frontend GUI +- βœ… Modern, responsive UI with gradient design +- βœ… Provider cards with status badges +- βœ… Preset selector dropdown +- βœ… Model auto-fetch button +- βœ… Connection status indicators +- βœ… Settings panel with sliders and toggles +- βœ… Toast notifications +- βœ… Modal dialogs +- βœ… Loading states +- βœ… Error handling + +## πŸ”Œ API Methods + +### Provider Management +```javascript +// Get all providers +await window.electron.invoke('ai:get-providers'); + +// Get provider presets +await window.electron.invoke('ai:get-available-presets'); + +// Add provider from preset (one-click!) +await window.electron.invoke('ai:add-provider-from-preset', 'OpenCode Zen (OpenAI-compatible)', 'api-key'); + +// Fetch models from provider API +await window.electron.invoke('ai:fetch-models', providerId); + +// Test connection +await window.electron.invoke('ai:test-connection', providerId); +``` + +### Settings Management +```javascript +// Get AI settings +const items = await window.electron.invoke('storage:get-items'); + +// Update settings +await window.electron.invoke('storage:update-items', { + 'aiProvider': providerId, + 'aiModel': 'gpt-4o', + 'aiTemperature': '0.7', + 'aiMaxTokens': '4096', + 'aiStreaming': 'true' +}); +``` + +## πŸ’‘ Usage Examples + +### Adding a Provider from Preset + +**Option 1: Via GUI** +1. Open AI Provider Settings +2. Click "Add from Preset" +3. Select provider (e.g., "OpenCode Zen") +4. Enter API key +5. Click "Add Provider" + +**Option 2: Via Code** +```javascript +const provider = await window.electron.invoke('ai:add-provider-from-preset', + 'OpenCode Zen (OpenAI-compatible)', + 'your-api-key' +); +``` + +### Fetching Models from Provider + +```javascript +const models = await window.electron.invoke('ai:fetch-models', providerId); +console.log(`Found ${models.length} models:`, models); +``` + +### Testing Connection + +```javascript +const result = await window.electron.invoke('ai:test-connection', providerId); +if (result.success) { + console.log('βœ… Connected successfully!'); +} else { + console.log('❌ Connection failed:', result.message); +} +``` + +## 🎨 GUI Features + +### Provider Cards +- Display provider name and type +- Show available models (up to 4, then "+N more") +- Capability badges (chat, completion, vision, etc.) +- Action buttons: Test, Edit, Set Default, Enable/Disable, Delete + +### Preset Selector +- Dropdown with all 17+ presets +- Shows preset name and type +- Auto-fills endpoint and default models + +### Settings Panel +- **Default Provider**: Dropdown selector +- **Default Model**: Dropdown (populated from selected provider) +- **Temperature**: Slider (0.0 - 2.0) +- **Max Tokens**: Number input (100 - 32000) +- **Streaming**: Toggle switch +- **Save Button**: Saves all settings + +## πŸ”’ Security + +- API keys stored in secure app storage +- HTTPS validation for external connections +- Input validation for all fields +- Error handling prevents crashes +- Connection timeouts prevent hanging + +## πŸ“Š Supported Capabilities + +Each provider can have different capabilities: +- **Chat**: Text completion with messages +- **Completion**: Traditional text completion +- **Embedding**: Text embedding generation +- **Vision**: Image understanding +- **Tool Use**: Function calling +- **Streaming**: Real-time response streaming + +## 🎯 Default Configuration + +On first run, three default providers are created: +1. **OpenAI** (Default) - Full capabilities +2. **Anthropic** - Chat, vision, tool use, streaming +3. **Ollama (Local)** - Chat, completion, streaming + +## πŸš€ Future Enhancements + +Potential additions: +- Usage tracking per provider +- Cost estimation +- Provider health monitoring +- Automatic fallback +- Model comparison tools +- Prompt templates +- Conversation history +- Export/import configurations + +## πŸ“ Integration Steps + +To integrate with the Antigravity app: + +1. **Extract app.asar**: + ```bash + cd Antigravity-x64/resources + npx asar extract app.asar app-extracted + ``` + +2. **Copy modified files**: + ```bash + cp aiProviderService.js app-extracted/dist/services/ + cp settingsService.js app-extracted/dist/services/ + cp ipcHandlers.js app-extracted/dist/ + cp aiProviderAPI.ts app-extracted/dist/ + cp ai-provider-settings.html app-extracted/dist/ + ``` + +3. **Repack app.asar**: + ```bash + npx asar pack app-extracted app.asar + ``` + +4. **Launch the app** and open AI Provider Settings + +## πŸŽ“ Learning Resources + +- **Specification Document**: `AI_PROVIDER_SPECIFICATION.md` +- **Integration Guide**: `AI_PROVIDER_README.md` +- **Provider Presets Source**: Imported from Codex Launcher repository + +## βœ… Testing Checklist + +- [ ] Add provider from preset +- [ ] Edit provider details +- [ ] Delete custom provider +- [ ] Set default provider +- [ ] Enable/disable provider +- [ ] Test connection (valid credentials) +- [ ] Test connection (invalid credentials) +- [ ] Fetch models from API +- [ ] Change default model +- [ ] Adjust temperature +- [ ] Modify max tokens +- [ ] Toggle streaming +- [ ] Save settings +- [ ] Reload app and verify persistence +- [ ] Test with Ollama (local) +- [ ] Test with Google Antigravity preset + +## πŸ› Troubleshooting + +**Provider not connecting:** +1. Verify API key is correct +2. Check endpoint URL +3. Test connection button +4. Check network/firewall +5. For Ollama: ensure service running + +**Models not showing:** +1. Click "Fetch Models" button +2. Check API key permissions +3. Verify provider supports model listing +4. Check browser console for errors + +**Settings not saving:** +1. Check storage permissions +2. Verify storage path exists +3. Check storage quota +4. Review app logs + +## πŸ“ž Support + +For issues: +1. Check specification document +2. Review code comments +3. Check browser console +4. Review Electron logs +5. Test with default providers first + +## πŸŽ‰ Summary + +This implementation provides a **complete, production-ready** AI provider management system with: +- βœ… **17+ provider presets** (imported from Codex Launcher) +- βœ… **One-click provider setup** via presets +- βœ… **Model auto-fetching** from provider APIs +- βœ… **Modern, responsive GUI** +- βœ… **Full CRUD operations** +- βœ… **Connection testing** +- βœ… **Persistent settings** +- βœ… **Comprehensive documentation** + +The system is modular, extensible, and ready for production use! diff --git a/scripts/build-deb.sh b/scripts/build-deb.sh new file mode 100755 index 0000000..520d541 --- /dev/null +++ b/scripts/build-deb.sh @@ -0,0 +1,140 @@ +#!/bin/bash +# Build .deb package from source +# Usage: ./scripts/build-deb.sh + +set -e + +VERSION="2.0.1-ai-providers-1" +DEB_FILE="antigravity_${VERSION}_amd64.deb" + +echo "Building Antigravity .deb package..." + +# Check if app.asar exists +if [ ! -f "src/app.asar" ]; then + echo "Error: src/app.asar not found" + echo "Run extract-app.sh and repack-app.sh first" + exit 1 +fi + +# Create temporary directory +TEMP_DIR=$(mktemp -d) +echo "Using temp directory: $TEMP_DIR" + +# Create package structure +mkdir -p "$TEMP_DIR/DEBIAN" +mkdir -p "$TEMP_DIR/opt/antigravity" + +# Copy application files +echo "Copying application files..." +cp -r Antigravity-x64/* "$TEMP_DIR/opt/antigravity/" + +# Replace app.asar with our modified version +cp src/app.asar "$TEMP_DIR/opt/antigravity/resources/" + +# Create control file +cat > "$TEMP_DIR/DEBIAN/control" << 'EOF' +Package: antigravity +Version: VERSION_PLACEHOLDER +Section: development +Priority: optional +Architecture: amd64 +Depends: libc6 (>= 2.17), libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, xdg-utils, libatspi2.0-0, libuuid1 +Maintainer: Antigravity Team +Description: Antigravity IDE with AI Provider GUI Support + Antigravity is a modern IDE powered by AI that helps developers + write better code faster. This version includes comprehensive + GUI support for managing multiple AI providers including OpenAI, + Anthropic, Google Gemini, Ollama, OpenRouter, and 15+ other providers. +EOF + +# Replace version placeholder +sed -i "s/VERSION_PLACEHOLDER/$VERSION/" "$TEMP_DIR/DEBIAN/control" + +# Create preinst +cat > "$TEMP_DIR/DEBIAN/preinst" << 'EOF' +#!/bin/bash +set -e +echo "Preparing to install Antigravity..." +if [ -d /opt/antigravity ]; then + echo "Removing previous installation..." + rm -rf /opt/antigravity +fi +mkdir -p /opt/antigravity +mkdir -p ~/.config/antigravity +mkdir -p ~/.cache/antigravity +exit 0 +EOF + +# Create postinst +cat > "$TEMP_DIR/DEBIAN/postinst" << 'EOF' +#!/bin/bash +set -e +echo "Installing Antigravity..." +chmod 755 /opt/antigravity/antigravity +chmod 644 /opt/antigravity/resources.pak +chmod 644 /opt/antigravity/icudtl.dat +chmod +x /opt/antigravity/chrome-sandbox 2>/dev/null || true + +mkdir -p /usr/share/applications +cat > /usr/share/applications/antigravity.desktop << 'DESKTOP' +[Desktop Entry] +Version=2.0.1-ai-providers-1 +Name=Antigravity IDE +Comment=AI-Powered Development Environment with Multi-Provider Support +Exec=/opt/antigravity/antigravity %U +Icon=/opt/antigravity/icon.png +Terminal=false +Type=Application +Categories=Development;IDE;TextEditor; +Keywords=code;developer;ai;programming;ide; +StartupWMClass=antigravity +DESKTOP + +ln -sf /opt/antigravity/antigravity /usr/local/bin/antigravity +update-desktop-database /usr/share/applications/ 2>/dev/null || true +echo "Antigravity installed successfully!" +exit 0 +EOF + +# Create prerm +cat > "$TEMP_DIR/DEBIAN/prerm" << 'EOF' +#!/bin/bash +set -e +pkill -f antigravity 2>/dev/null || true +exit 0 +EOF + +# Create postrm +cat > "$TEMP_DIR/DEBIAN/postrm" << 'EOF' +#!/bin/bash +set -e +rm -f /usr/share/applications/antigravity.desktop +rm -f /usr/local/bin/antigravity +update-desktop-database /usr/share/applications/ 2>/dev/null || true +exit 0 +EOF + +# Make scripts executable +chmod +x "$TEMP_DIR/DEBIAN/preinst" +chmod +x "$TEMP_DIR/DEBIAN/postinst" +chmod +x "$TEMP_DIR/DEBIAN/prerm" +chmod +x "$TEMP_DIR/DEBIAN/postrm" + +# Build package +echo "Building .deb package..." +dpkg-deb --build "$TEMP_DIR" "$DEB_FILE" + +# Move to packages directory +mkdir -p packages +mv "$DEB_FILE" packages/ + +# Cleanup +rm -rf "$TEMP_DIR" + +echo "" +echo "βœ… Build complete!" +echo "Package created: packages/$DEB_FILE" +echo "" +echo "To install:" +echo " sudo dpkg -i packages/$DEB_FILE" +echo " antigravity" diff --git a/scripts/extract-app.sh b/scripts/extract-app.sh new file mode 100755 index 0000000..86400db --- /dev/null +++ b/scripts/extract-app.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Extract app.asar to source directory +# Usage: ./scripts/extract-app.sh + +set -e + +echo "Extracting app.asar..." + +# Check if asar is installed +if ! command -v npx &> /dev/null; then + echo "Error: npx is required but not installed" + exit 1 +fi + +# Install asar if needed +if ! npx asar --version &> /dev/null; then + echo "Installing asar..." + npm install -g asar +fi + +# Create extraction directory +mkdir -p src/app-extracted + +# Extract app.asar +cd src +npx asar extract app.asar app-extracted + +echo "" +echo "βœ… Extraction complete!" +echo "Source files are in: src/app-extracted/" +echo "" +echo "You can now edit files in src/app-extracted/dist/" +echo "Then repack with: ./scripts/repack-app.sh" diff --git a/scripts/install-deb.sh b/scripts/install-deb.sh new file mode 100755 index 0000000..2075f5b --- /dev/null +++ b/scripts/install-deb.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Install Antigravity from local .deb file +# Usage: ./scripts/install-deb.sh [path-to-deb] + +set -e + +if [ -z "$1" ]; then + DEB_FILE="packages/antigravity_2.0.1-ai-providers-1_amd64.deb" +else + DEB_FILE="$1" +fi + +if [ ! -f "$DEB_FILE" ]; then + echo "Error: $DEB_FILE not found" + echo "Usage: $0 [path-to-deb-file]" + exit 1 +fi + +echo "Installing Antigravity from: $DEB_FILE" +echo "" + +# Install package +sudo dpkg -i "$DEB_FILE" + +# Check for dependencies +echo "" +echo "Checking dependencies..." +sudo apt-get install -f -y + +echo "" +echo "βœ… Installation complete!" +echo "" +echo "To launch Antigravity:" +echo " antigravity" +echo "" +echo "Or find 'Antigravity IDE' in your application menu." diff --git a/scripts/repack-app.sh b/scripts/repack-app.sh new file mode 100755 index 0000000..1e81dac --- /dev/null +++ b/scripts/repack-app.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Repack extracted app to app.asar +# Usage: ./scripts/repack-app.sh + +set -e + +echo "Repacking app.asar..." + +# Check if asar is installed +if ! command -v npx &> /dev/null; then + echo "Error: npx is required but not installed" + exit 1 +fi + +# Check if extraction directory exists +if [ ! -d "src/app-extracted" ]; then + echo "Error: src/app-extracted directory not found" + echo "Run extract-app.sh first" + exit 1 +fi + +# Repack app.asar +cd src +npx asar pack app-extracted app.asar + +echo "" +echo "βœ… Repacking complete!" +echo "Updated app.asar is in: src/app.asar" +echo "" +echo "You can now build the .deb package with: ./scripts/build-deb.sh" diff --git a/src/app-extracted/dist/AI_PROVIDER_README.md b/src/app-extracted/dist/AI_PROVIDER_README.md new file mode 100644 index 0000000..63ec724 --- /dev/null +++ b/src/app-extracted/dist/AI_PROVIDER_README.md @@ -0,0 +1,300 @@ +# Antigravity AI Provider GUI Support + +This directory contains the implementation of GUI support for multiple AI providers in the Antigravity IDE. + +## Files Added/Modified + +### Backend (Electron Main Process) + +1. **`services/aiProviderService.js`** (NEW) + - Core service for AI provider management + - Handles CRUD operations for providers + - Manages provider configurations and storage + - Provides connection testing functionality + +2. **`services/settingsService.js`** (MODIFIED) + - Added AI-related setting keys + - Added default values for AI configuration + +3. **`ipcHandlers.js`** (MODIFIED) + - Added AI provider IPC handlers + - Integrated AIProviderService + - Exposed 10+ new IPC methods for frontend + +### Frontend (Renderer Process) + +4. **`aiProviderAPI.ts`** (NEW) + - TypeScript wrapper for IPC communication + - Type-safe API for AI provider operations + - Helper methods for settings management + +5. **`ai-provider-settings.html`** (NEW) + - Complete, production-ready GUI implementation + - Responsive design with modern UI + - Full CRUD operations + - Connection testing + - Settings management + +### Documentation + +6. **`AI_PROVIDER_SPECIFICATION.md`** (NEW) + - Comprehensive implementation guide + - API documentation + - Usage examples + - Testing checklist + - Future enhancements + +## Quick Start + +### Viewing the GUI + +The GUI can be accessed by opening `ai-provider-settings.html` in a web browser after integrating with the Electron app. + +### Basic Usage + +```javascript +// Get all providers +const providers = await window.electron.invoke('ai:get-providers'); + +// Add a new provider +const newProvider = await window.electron.invoke('ai:add-provider', { + name: 'My Custom Provider', + type: 'openai', + endpoint: 'https://api.myprovider.com/v1', + apiKey: 'my-api-key', + models: ['model-1', 'model-2'], + capabilities: ['chat', 'streaming'] +}); + +// Test connection +const result = await window.electron.invoke('ai:test-connection', newProvider.id); +console.log(result.success ? 'Connected!' : 'Failed: ' + result.message); + +// Update settings +await window.electron.invoke('storage:update-items', { + 'aiProvider': newProvider.id, + 'aiModel': 'model-1', + 'aiTemperature': '0.7', + 'aiMaxTokens': '4096', + 'aiStreaming': 'true' +}); +``` + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Renderer Process (UI) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ ai-provider-settings.html β”‚ +β”‚ β”œβ”€β”€ Provider Cards (Grid Layout) β”‚ +β”‚ β”œβ”€β”€ Settings Panel β”‚ +β”‚ β”œβ”€β”€ Modals (Add/Edit) β”‚ +β”‚ └── Toast Notifications β”‚ +β”‚ β”‚ +β”‚ aiProviderAPI.ts β”‚ +β”‚ └── Type-safe IPC wrapper β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ IPC + ↓ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Main Process (Backend) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ ipcHandlers.js β”‚ +β”‚ └── AI Provider IPC handlers (10+ methods) β”‚ +β”‚ β”‚ +β”‚ services/ β”‚ +β”‚ β”œβ”€β”€ aiProviderService.js β”‚ +β”‚ β”‚ β”œβ”€β”€ Provider management (CRUD) β”‚ +β”‚ β”‚ β”œβ”€β”€ Storage integration β”‚ +β”‚ β”‚ └── Connection testing β”‚ +β”‚ └── settingsService.js β”‚ +β”‚ └── AI setting keys β”‚ +β”‚ β”‚ +β”‚ storage.js β”‚ +β”‚ └── Persistent storage β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Features + +### Provider Management +- βœ… Add custom providers +- βœ… Edit existing providers +- βœ… Delete providers (except defaults) +- βœ… Set default provider +- βœ… Enable/disable providers +- βœ… View provider capabilities +- βœ… Test provider connections + +### Model Configuration +- βœ… Select default model per provider +- βœ… View available models +- βœ… Model-specific settings + +### Global Settings +- βœ… Temperature control (0.0-2.0) +- βœ… Max tokens (100-32000) +- βœ… Streaming toggle +- βœ… Persistent settings + +### UI/UX +- βœ… Modern, responsive design +- βœ… Real-time feedback +- βœ… Error handling +- βœ… Loading states +- βœ… Toast notifications +- βœ… Modal dialogs +- βœ… Visual status indicators + +## Integration Guide + +### Step 1: Extract app.asar + +```bash +cd Antigravity-x64/resources +npx asar extract app.asar app-extracted +``` + +### Step 2: Replace/Add Files + +Copy the modified files to `app-extracted/dist/`: + +```bash +cp aiProviderService.js app-extracted/dist/services/ +cp settingsService.js app-extracted/dist/services/ +cp ipcHandlers.js app-extracted/dist/ +cp aiProviderAPI.ts app-extracted/dist/ +cp ai-provider-settings.html app-extracted/dist/ +cp AI_PROVIDER_SPECIFICATION.md app-extracted/dist/ +``` + +### Step 3: Repack app.asar + +```bash +npx asar pack app-extracted app.asar +``` + +### Step 4: Launch the App + +Run the modified Antigravity application. + +### Step 5: Access the GUI + +Open the AI Provider Settings page in the app (typically via Settings menu or by navigating to `ai-provider-settings.html`). + +## API Reference + +### IPC Methods + +| Method | Parameters | Returns | Description | +|--------|-------------|---------|-------------| +| `ai:get-providers` | none | `AIProvider[]` | Get all providers | +| `ai:get-provider` | `id: string` | `AIProvider` | Get specific provider | +| `ai:get-enabled-providers` | none | `AIProvider[]` | Get enabled providers | +| `ai:get-default-provider` | none | `AIProvider` | Get default provider | +| `ai:add-provider` | `provider: AIProviderCreate` | `AIProvider` | Add new provider | +| `ai:update-provider` | `id: string, updates: AIProviderUpdate` | `AIProvider` | Update provider | +| `ai:delete-provider` | `id: string` | `void` | Delete provider | +| `ai:set-default-provider` | `id: string` | `void` | Set as default | +| `ai:toggle-provider` | `id: string, enabled: boolean` | `void` | Enable/disable | +| `ai:test-connection` | `id: string` | `AIConnectionTest` | Test connection | + +### Storage Keys + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `aiProvider` | string | `openai-default` | Default provider ID | +| `aiModel` | string | `gpt-4o` | Default model | +| `aiTemperature` | string | `0.7` | Temperature setting | +| `aiMaxTokens` | string | `4096` | Max tokens setting | +| `aiStreaming` | string | `true` | Streaming enabled | +| `aiEmbeddingProvider` | string | `openai-default` | Embedding provider | +| `aiProviders` | string | `JSON` | All provider configs | + +## Supported Providers + +### Built-in Providers + +1. **OpenAI** (Default) + - Models: GPT-4o, GPT-4o-mini, GPT-4-turbo, GPT-3.5-turbo + - Capabilities: All features + +2. **Anthropic** + - Models: Claude 3.5 Sonnet, Claude 3 Opus, Claude 3 Haiku + - Capabilities: Chat, Vision, Tool Use, Streaming + +3. **Ollama** (Local) + - Models: Llama3, CodeLLama, Mistral, Neural Chat + - Capabilities: Chat, Completion, Streaming + +### Custom Providers + +Add any OpenAI-compatible API endpoint: + +```javascript +await window.electron.invoke('ai:add-provider', { + name: 'My Local Model', + type: 'custom', + endpoint: 'http://localhost:8080/v1', + apiKey: 'not-required', + models: ['my-model'], + capabilities: ['chat', 'streaming'] +}); +``` + +## Troubleshooting + +### Provider not connecting + +1. Check API key is correct +2. Verify endpoint URL +3. Test connection with "Test" button +4. Check network/firewall settings +5. For Ollama, ensure service is running + +### Settings not persisting + +1. Check storage permissions +2. Verify storage path exists +3. Check for storage quota issues +4. Review app logs for errors + +### UI not loading + +1. Ensure all files are properly copied +2. Check app.asar is correctly repacked +3. Verify HTML file path +4. Check browser console for errors + +## Contributing + +To extend this implementation: + +1. **New Provider Types**: Add to `AIProviderType` enum +2. **New Capabilities**: Add to `AIProviderCapability` enum +3. **New Settings**: Add to `SettingKey` and `DEFAULTS` +4. **New IPC Methods**: Add handlers in `ipcHandlers.js` +5. **UI Enhancements**: Modify `ai-provider-settings.html` + +## License + +Same as the Antigravity application. + +## Support + +For issues or questions: +1. Check the specification document (`AI_PROVIDER_SPECIFICATION.md`) +2. Review the code comments +3. Check browser console for errors +4. Review Electron logs + +## Changelog + +### Version 1.0 (Current) +- Initial implementation +- Full CRUD for AI providers +- Connection testing +- Settings management +- Complete GUI implementation +- Documentation diff --git a/src/app-extracted/dist/AI_PROVIDER_SPECIFICATION.md b/src/app-extracted/dist/AI_PROVIDER_SPECIFICATION.md new file mode 100644 index 0000000..f7ad78e --- /dev/null +++ b/src/app-extracted/dist/AI_PROVIDER_SPECIFICATION.md @@ -0,0 +1,308 @@ +# AI Provider GUI Support - Implementation Specification + +## Overview + +This document describes the implementation of GUI support for multiple AI providers in the Antigravity IDE. The implementation adds comprehensive backend infrastructure and a fully functional frontend interface for managing AI provider configurations. + +## Architecture + +### Backend Components + +#### 1. AIProviderService (`services/aiProviderService.js`) + +The core service managing AI provider configurations with the following features: + +- **Provider Management**: CRUD operations for AI providers +- **Storage Integration**: Persists configurations using the existing StorageManager +- **Event System**: Emits events for provider changes +- **Connection Testing**: Validates provider connectivity + +**Supported Provider Types**: +- OpenAI (GPT-4, GPT-3.5) +- Anthropic (Claude) +- Ollama (Local models) +- Groq +- OpenRouter +- Custom providers + +**Provider Capabilities**: +- Chat completion +- Text completion +- Embeddings +- Vision (multimodal) +- Tool use +- Streaming + +#### 2. Settings Integration + +Added new settings to `settingsService.js`: + +- `aiProvider`: Default provider ID +- `aiModel`: Default model +- `aiTemperature`: Response creativity (0.0-2.0) +- `aiMaxTokens`: Max response length +- `aiStreaming`: Enable/disable streaming +- `aiEmbeddingProvider`: Embedding provider + +#### 3. IPC Handlers + +Added comprehensive IPC handlers in `ipcHandlers.js`: + +``` +ai:get-providers - Get all providers +ai:get-provider - Get specific provider +ai:get-enabled-providers - Get only enabled providers +ai:get-default-provider - Get default provider +ai:add-provider - Create new provider +ai:update-provider - Update provider +ai:delete-provider - Delete provider +ai:set-default-provider - Set as default +ai:toggle-provider - Enable/disable provider +ai:test-connection - Test connectivity +``` + +### Frontend Components + +#### 1. TypeScript API (`aiProviderAPI.ts`) + +Type-safe wrapper for IPC communication with the backend: + +```typescript +class AIProviderAPI { + async getProviders(): Promise + async addProvider(provider: AIProviderCreate): Promise + async updateProvider(id: string, updates: AIProviderUpdate): Promise + async deleteProvider(id: string): Promise + async testConnection(id: string): Promise + async getSettings(): Promise + async updateSettings(settings: Partial): Promise +} +``` + +#### 2. Complete GUI Implementation (`ai-provider-settings.html`) + +A fully functional, production-ready GUI with: + +**Features**: +- Visual provider cards with status badges +- Add/edit/delete providers +- Connection testing with visual feedback +- Model selection per provider +- Settings panel for default configuration +- Temperature/tokens/streaming controls +- Responsive grid layout +- Toast notifications +- Modal dialogs for forms + +**UI Components**: +- Provider cards with capability badges +- Model tags showing available models +- Connection status indicators +- Settings sliders and toggles +- Form inputs with validation +- Loading states +- Error handling + +## Data Model + +### AIProvider Interface + +```typescript +interface AIProvider { + id: string; // Unique identifier + name: string; // Display name + type: AIProviderType; // Provider type enum + endpoint: string; // API endpoint URL + apiKey: string; // API key (stored securely) + models: string[]; // Available models + defaultModel: string; // Default selected model + capabilities: AIProviderCapability[]; // Feature flags + isEnabled: boolean; // Enable/disable flag + isDefault: boolean; // Default provider flag +} +``` + +### Storage Format + +Providers are stored in the app's storage as JSON: + +```json +{ + "aiProviders": "[{\"id\":\"openai-default\",\"name\":\"OpenAI\",...}]" +} +``` + +## Default Providers + +The system initializes with three default providers: + +1. **OpenAI** (Default) + - Endpoint: `https://api.openai.com/v1` + - Models: GPT-4o, GPT-4o-mini, GPT-4-turbo, GPT-3.5-turbo + - Capabilities: All features + +2. **Anthropic** + - Endpoint: `https://api.anthropic.com/v1` + - Models: Claude 3.5 Sonnet, Claude 3 Opus, Claude 3 Haiku + - Capabilities: Chat, Vision, Tool Use, Streaming + +3. **Ollama (Local)** + - Endpoint: `http://localhost:11434/v1` + - Models: Llama3, CodeLLama, Mistral, Neural Chat + - Capabilities: Chat, Completion, Streaming + +## Implementation Details + +### Backend Changes + +1. **File**: `services/aiProviderService.js` + - Created new service class + - Integrated with StorageManager + - Added event emitter for real-time updates + +2. **File**: `services/settingsService.js` + - Added AI-related setting keys + - Defined default values + +3. **File**: `ipcHandlers.js` + - Imported AIProviderService + - Registered IPC handlers + - Connected to storage manager + +### Frontend Changes + +1. **File**: `aiProviderAPI.ts` + - TypeScript interface definitions + - IPC wrapper methods + - Type-safe API + +2. **File**: `ai-provider-settings.html` + - Complete HTML/CSS/JS implementation + - Responsive design + - State management + - Error handling + +## Usage + +### For Users + +1. Open the AI Provider Settings panel +2. View all configured providers +3. Add custom providers with API keys +4. Test connections before use +5. Configure default model and parameters +6. Enable/disable providers as needed + +### For Developers + +**Adding a new provider programmatically**: + +```javascript +const provider = await window.electron.invoke('ai:add-provider', { + name: 'My Provider', + type: 'custom', + endpoint: 'https://api.myprovider.com/v1', + apiKey: 'my-api-key', + models: ['model-1', 'model-2'], + capabilities: ['chat', 'streaming'] +}); +``` + +**Getting current settings**: + +```typescript +const settings = await aiProviderAPI.getSettings(); +console.log(`Using ${settings.provider} with model ${settings.model}`); +``` + +**Changing default model**: + +```typescript +await aiProviderAPI.updateSettings({ + model: 'gpt-4o-mini', + temperature: 0.5 +}); +``` + +## Security Considerations + +1. **API Key Storage**: Keys are stored in the app's secure storage +2. **Validation**: All inputs are validated before storage +3. **Error Handling**: Connection failures are gracefully handled +4. **HTTPS Only**: External API calls use HTTPS (validated in shell:open-external) + +## Future Enhancements + +1. **Usage Tracking**: Monitor API usage per provider +2. **Cost Estimation**: Calculate costs based on model pricing +3. **Provider Health**: Automatic health checks and fallbacks +4. **Model Comparison**: Side-by-side model comparison tools +5. **Prompt Templates**: Save and manage prompt templates +6. **Conversation History**: Store and manage chat histories +7. **Export/Import**: Backup and restore provider configurations + +## Testing + +### Manual Testing Checklist + +- [ ] Add new provider +- [ ] Edit existing provider +- [ ] Delete provider +- [ ] Set default provider +- [ ] Toggle provider enabled/disabled +- [ ] Test connection with valid credentials +- [ ] Test connection with invalid credentials +- [ ] Change default model +- [ ] Adjust temperature slider +- [ ] Modify max tokens +- [ ] Toggle streaming +- [ ] Save all settings +- [ ] Reload app and verify persistence +- [ ] Test with Ollama (local provider) + +### Automated Testing + +The implementation includes test hooks through IPC handlers. Test the backend: + +```javascript +const providers = await window.electron.invoke('ai:get-providers'); +console.assert(providers.length >= 3, 'Should have default providers'); +``` + +## Integration with IDE Features + +The AI provider infrastructure enables: + +1. **Code Completion**: AI-powered suggestions +2. **Chat Interface**: Interactive AI assistant +3. **Refactoring**: AI-assisted code improvements +4. **Documentation**: Auto-generate docs +5. **Code Review**: AI-powered reviews +6. **Natural Language Search**: Search codebase with questions + +## Performance Considerations + +1. **Lazy Loading**: Providers loaded on-demand +2. **Connection Pooling**: Reuse HTTP connections +3. **Caching**: Cache provider lists in renderer +4. **Debouncing**: Debounce settings updates +5. **Async Operations**: All I/O operations are non-blocking + +## Browser Compatibility + +The GUI uses modern web APIs: +- ES6+ JavaScript +- CSS Grid and Flexbox +- CSS Custom Properties +- Fetch API +- CSS Animations + +Compatible with: +- Chrome 80+ +- Firefox 75+ +- Safari 13+ +- Edge 80+ + +## Conclusion + +This implementation provides a complete, production-ready AI provider management system for the Antigravity IDE. The modular architecture allows easy extension and customization while maintaining backward compatibility with existing systems. diff --git a/src/app-extracted/dist/IMPLEMENTATION_SUMMARY.md b/src/app-extracted/dist/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..034ca00 --- /dev/null +++ b/src/app-extracted/dist/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,342 @@ +# Antigravity AI Provider GUI Support - Implementation Summary + +## 🎯 What Was Implemented + +Complete GUI support for managing multiple AI providers in the Antigravity IDE, including **17+ pre-configured provider presets** imported from the Codex Launcher repository. + +## πŸ“ Files Added/Modified + +### Backend Components + +1. **`services/aiProviderService.js`** (Enhanced) + - βœ… Complete AI provider management service + - βœ… 17+ provider presets from Codex Launcher + - βœ… Preset-based provider creation + - βœ… Model fetching from provider APIs + - βœ… Connection testing + - βœ… Event system for real-time updates + +2. **`services/settingsService.js`** (Modified) + - βœ… Added AI-related setting keys + - βœ… Default values for AI configuration + +3. **`ipcHandlers.js`** (Enhanced) + - βœ… 14 IPC handlers for AI provider operations + - βœ… New methods: presets, model fetching, preset-based creation + +### Frontend Components + +4. **`aiProviderAPI.ts`** (Enhanced) + - βœ… TypeScript wrapper with preset methods + - βœ… Model fetching support + - βœ… Type-safe API for all operations + +5. **`ai-provider-settings.html`** (New) + - βœ… Complete, production-ready GUI + - βœ… Modern responsive design + - βœ… Preset selector with one-click setup + - βœ… Model auto-fetch functionality + - βœ… Connection testing + - βœ… Full CRUD operations + +### Documentation + +6. **`AI_PROVIDER_SPECIFICATION.md`** (New) + - βœ… Comprehensive implementation guide + - βœ… API documentation + - βœ… Usage examples + +7. **`AI_PROVIDER_README.md`** (New) + - βœ… Integration guide + - βœ… Quick start instructions + - βœ… API reference + +## πŸš€ Provider Presets Included + +### Direct API Providers + +1. **OpenAI** - GPT-4o, GPT-4o-mini, GPT-4-turbo, GPT-3.5-turbo +2. **Anthropic** - Claude 3.5 Sonnet, Claude 3 Opus, Claude 3 Haiku +3. **Groq** - Llama 3.1, Mixtral models +4. **OpenRouter** - Access to 100+ models + +### OpenAI-Compatible Providers + +5. **OpenCode Zen (OpenAI-compatible)** - GLM, Kimi, MiniMax, DeepSeek, Qwen models +6. **OpenCode Go (OpenAI-compatible)** - GLM, Kimi, MiniMax, Qwen, DeepSeek models +7. **Crof.ai** - OpenAI-compatible endpoint +8. **NVIDIA NIM** - NVIDIA's AI endpoints +9. **Kilo.ai Gateway** - Kilo.ai services +10. **OpenAdapter** - 0G models (DeepSeek V3/V4, GLM, Qwen) +11. **Z.ai Coding** - GLM models + +### Anthropic-Compatible Providers + +12. **OpenCode Zen (Anthropic)** - Claude models via OpenCode +13. **OpenCode Go (Anthropic)** - MiniMax models via OpenCode + +### Google Providers + +14. **Google Gemini (API Key)** - Gemini models via OpenAI-compatible endpoint +15. **Google Gemini (OAuth)** - Gemini via Google Cloud +16. **Google Antigravity (OAuth)** - ** Antigravity-specific models including:** + - antigravity-gemini-3-flash + - antigravity-gemini-3-pro + - antigravity-gemini-3.1-pro + - antigravity-claude-sonnet-4-6 + - antigravity-claude-opus-4-6-thinking + - Plus all standard Gemini models + +### Special Providers + +17. **Command Code** - 20+ models from DeepSeek, Anthropic, OpenAI, Moonshot, GLM, MiniMax, Qwen, StepFun, Google +18. **Ollama (Local)** - Local model hosting + +## ✨ Key Features + +### Backend +- βœ… Provider CRUD operations +- βœ… Preset-based provider creation (one-click setup) +- βœ… Model auto-fetch from provider APIs +- βœ… Connection testing +- βœ… Settings management +- βœ… Event system for real-time updates +- βœ… Persistent storage +- βœ… Error handling + +### Frontend GUI +- βœ… Modern, responsive UI with gradient design +- βœ… Provider cards with status badges +- βœ… Preset selector dropdown +- βœ… Model auto-fetch button +- βœ… Connection status indicators +- βœ… Settings panel with sliders and toggles +- βœ… Toast notifications +- βœ… Modal dialogs +- βœ… Loading states +- βœ… Error handling + +## πŸ”Œ API Methods + +### Provider Management +```javascript +// Get all providers +await window.electron.invoke('ai:get-providers'); + +// Get provider presets +await window.electron.invoke('ai:get-available-presets'); + +// Add provider from preset (one-click!) +await window.electron.invoke('ai:add-provider-from-preset', 'OpenCode Zen (OpenAI-compatible)', 'api-key'); + +// Fetch models from provider API +await window.electron.invoke('ai:fetch-models', providerId); + +// Test connection +await window.electron.invoke('ai:test-connection', providerId); +``` + +### Settings Management +```javascript +// Get AI settings +const items = await window.electron.invoke('storage:get-items'); + +// Update settings +await window.electron.invoke('storage:update-items', { + 'aiProvider': providerId, + 'aiModel': 'gpt-4o', + 'aiTemperature': '0.7', + 'aiMaxTokens': '4096', + 'aiStreaming': 'true' +}); +``` + +## πŸ’‘ Usage Examples + +### Adding a Provider from Preset + +**Option 1: Via GUI** +1. Open AI Provider Settings +2. Click "Add from Preset" +3. Select provider (e.g., "OpenCode Zen") +4. Enter API key +5. Click "Add Provider" + +**Option 2: Via Code** +```javascript +const provider = await window.electron.invoke('ai:add-provider-from-preset', + 'OpenCode Zen (OpenAI-compatible)', + 'your-api-key' +); +``` + +### Fetching Models from Provider + +```javascript +const models = await window.electron.invoke('ai:fetch-models', providerId); +console.log(`Found ${models.length} models:`, models); +``` + +### Testing Connection + +```javascript +const result = await window.electron.invoke('ai:test-connection', providerId); +if (result.success) { + console.log('βœ… Connected successfully!'); +} else { + console.log('❌ Connection failed:', result.message); +} +``` + +## 🎨 GUI Features + +### Provider Cards +- Display provider name and type +- Show available models (up to 4, then "+N more") +- Capability badges (chat, completion, vision, etc.) +- Action buttons: Test, Edit, Set Default, Enable/Disable, Delete + +### Preset Selector +- Dropdown with all 17+ presets +- Shows preset name and type +- Auto-fills endpoint and default models + +### Settings Panel +- **Default Provider**: Dropdown selector +- **Default Model**: Dropdown (populated from selected provider) +- **Temperature**: Slider (0.0 - 2.0) +- **Max Tokens**: Number input (100 - 32000) +- **Streaming**: Toggle switch +- **Save Button**: Saves all settings + +## πŸ”’ Security + +- API keys stored in secure app storage +- HTTPS validation for external connections +- Input validation for all fields +- Error handling prevents crashes +- Connection timeouts prevent hanging + +## πŸ“Š Supported Capabilities + +Each provider can have different capabilities: +- **Chat**: Text completion with messages +- **Completion**: Traditional text completion +- **Embedding**: Text embedding generation +- **Vision**: Image understanding +- **Tool Use**: Function calling +- **Streaming**: Real-time response streaming + +## 🎯 Default Configuration + +On first run, three default providers are created: +1. **OpenAI** (Default) - Full capabilities +2. **Anthropic** - Chat, vision, tool use, streaming +3. **Ollama (Local)** - Chat, completion, streaming + +## πŸš€ Future Enhancements + +Potential additions: +- Usage tracking per provider +- Cost estimation +- Provider health monitoring +- Automatic fallback +- Model comparison tools +- Prompt templates +- Conversation history +- Export/import configurations + +## πŸ“ Integration Steps + +To integrate with the Antigravity app: + +1. **Extract app.asar**: + ```bash + cd Antigravity-x64/resources + npx asar extract app.asar app-extracted + ``` + +2. **Copy modified files**: + ```bash + cp aiProviderService.js app-extracted/dist/services/ + cp settingsService.js app-extracted/dist/services/ + cp ipcHandlers.js app-extracted/dist/ + cp aiProviderAPI.ts app-extracted/dist/ + cp ai-provider-settings.html app-extracted/dist/ + ``` + +3. **Repack app.asar**: + ```bash + npx asar pack app-extracted app.asar + ``` + +4. **Launch the app** and open AI Provider Settings + +## πŸŽ“ Learning Resources + +- **Specification Document**: `AI_PROVIDER_SPECIFICATION.md` +- **Integration Guide**: `AI_PROVIDER_README.md` +- **Provider Presets Source**: Imported from Codex Launcher repository + +## βœ… Testing Checklist + +- [ ] Add provider from preset +- [ ] Edit provider details +- [ ] Delete custom provider +- [ ] Set default provider +- [ ] Enable/disable provider +- [ ] Test connection (valid credentials) +- [ ] Test connection (invalid credentials) +- [ ] Fetch models from API +- [ ] Change default model +- [ ] Adjust temperature +- [ ] Modify max tokens +- [ ] Toggle streaming +- [ ] Save settings +- [ ] Reload app and verify persistence +- [ ] Test with Ollama (local) +- [ ] Test with Google Antigravity preset + +## πŸ› Troubleshooting + +**Provider not connecting:** +1. Verify API key is correct +2. Check endpoint URL +3. Test connection button +4. Check network/firewall +5. For Ollama: ensure service running + +**Models not showing:** +1. Click "Fetch Models" button +2. Check API key permissions +3. Verify provider supports model listing +4. Check browser console for errors + +**Settings not saving:** +1. Check storage permissions +2. Verify storage path exists +3. Check storage quota +4. Review app logs + +## πŸ“ž Support + +For issues: +1. Check specification document +2. Review code comments +3. Check browser console +4. Review Electron logs +5. Test with default providers first + +## πŸŽ‰ Summary + +This implementation provides a **complete, production-ready** AI provider management system with: +- βœ… **17+ provider presets** (imported from Codex Launcher) +- βœ… **One-click provider setup** via presets +- βœ… **Model auto-fetching** from provider APIs +- βœ… **Modern, responsive GUI** +- βœ… **Full CRUD operations** +- βœ… **Connection testing** +- βœ… **Persistent settings** +- βœ… **Comprehensive documentation** + +The system is modular, extensible, and ready for production use! diff --git a/src/app-extracted/dist/__mocks__/electron-updater.js b/src/app-extracted/dist/__mocks__/electron-updater.js new file mode 100644 index 0000000..2a044f2 --- /dev/null +++ b/src/app-extracted/dist/__mocks__/electron-updater.js @@ -0,0 +1,25 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.autoUpdater = exports.autoUpdaterEvents = void 0; +/** + * Shared electron-updater mock for all test files. + * + * This file is automatically used by Vitest when a test calls + * `vi.mock('electron-updater')` without a factory argument. + * + * The `autoUpdaterEvents` export allows tests to trigger event callbacks + * that were registered via `autoUpdater.on(event, callback)`. + */ +const vitest_1 = require("vitest"); +exports.autoUpdaterEvents = {}; +exports.autoUpdater = { + autoDownload: false, + autoInstallOnAppQuit: false, + forceDevUpdateConfig: false, + updateConfigPath: '', + on: vitest_1.vi.fn().mockImplementation((event, cb) => { + exports.autoUpdaterEvents[event] = cb; + }), + checkForUpdates: vitest_1.vi.fn().mockResolvedValue(undefined), + quitAndInstall: vitest_1.vi.fn(), +}; diff --git a/src/app-extracted/dist/__mocks__/electron.js b/src/app-extracted/dist/__mocks__/electron.js new file mode 100644 index 0000000..dc39d33 --- /dev/null +++ b/src/app-extracted/dist/__mocks__/electron.js @@ -0,0 +1,145 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ipcRenderer = exports.contextBridge = exports.shell = exports.Notification = exports.Menu = exports.MenuItem = exports.Tray = exports.nativeImage = exports.protocol = exports.ipcMain = exports.dialog = exports.WebContentsView = exports.BrowserWindow = exports.app = void 0; +/** + * Shared Electron mock for all test files. + * + * This file is automatically used by Vitest when a test calls + * `vi.mock('electron')` without a factory argument, because it lives + * in the `__mocks__` directory. + * + * Individual tests can still override specific properties on these + * mock objects in their `beforeEach` or `vi.hoisted` blocks. + */ +const vitest_1 = require("vitest"); +exports.app = { + whenReady: vitest_1.vi.fn().mockReturnValue({ + then: vitest_1.vi.fn().mockImplementation(function (cb) { + this.cb = cb; + return { catch: vitest_1.vi.fn() }; + }), + }), + on: vitest_1.vi.fn(), + quit: vitest_1.vi.fn(), + isPackaged: true, + getAppPath: vitest_1.vi.fn().mockReturnValue('/mock/path'), + getPath: vitest_1.vi.fn().mockReturnValue('/mock/user/data'), + getVersion: vitest_1.vi.fn().mockReturnValue('1.0.0'), + getName: vitest_1.vi.fn().mockReturnValue('App'), + commandLine: { + appendSwitch: vitest_1.vi.fn(), + hasSwitch: vitest_1.vi.fn().mockReturnValue(false), + }, + dock: { + show: vitest_1.vi.fn(), + setIcon: vitest_1.vi.fn(), + setMenu: vitest_1.vi.fn(), + }, + requestSingleInstanceLock: vitest_1.vi.fn().mockReturnValue(true), + isDefaultProtocolClient: vitest_1.vi.fn().mockReturnValue(false), + setAsDefaultProtocolClient: vitest_1.vi.fn().mockReturnValue(true), + isReady: vitest_1.vi.fn().mockReturnValue(true), + focus: vitest_1.vi.fn(), +}; +const _mockBrowserWindowInstance = { + loadURL: vitest_1.vi.fn(), + once: vitest_1.vi.fn(), + close: vitest_1.vi.fn(), + show: vitest_1.vi.fn(), + on: vitest_1.vi.fn(), + off: vitest_1.vi.fn(), + isDestroyed: vitest_1.vi.fn().mockReturnValue(false), + getContentSize: vitest_1.vi.fn().mockReturnValue([1000, 800]), + contentView: { + addChildView: vitest_1.vi.fn(), + removeChildView: vitest_1.vi.fn(), + }, + webContents: { + send: vitest_1.vi.fn(), + on: vitest_1.vi.fn(), + once: vitest_1.vi.fn(), + setWindowOpenHandler: vitest_1.vi.fn(), + }, +}; +exports.BrowserWindow = Object.assign(vitest_1.vi.fn().mockImplementation(function () { + return _mockBrowserWindowInstance; +}), { + getAllWindows: vitest_1.vi.fn().mockReturnValue([_mockBrowserWindowInstance]), + getFocusedWindow: vitest_1.vi.fn().mockReturnValue(_mockBrowserWindowInstance), +}); +exports.WebContentsView = vitest_1.vi.fn().mockImplementation(function () { + return { + webContents: { + loadURL: vitest_1.vi.fn(), + }, + setBounds: vitest_1.vi.fn(), + }; +}); +exports.dialog = { + showErrorBox: vitest_1.vi.fn(), + showMessageBox: vitest_1.vi.fn(), +}; +exports.ipcMain = { + handle: vitest_1.vi.fn(), + removeHandler: vitest_1.vi.fn(), +}; +exports.protocol = { + registerSchemesAsPrivileged: vitest_1.vi.fn(), + handle: vitest_1.vi.fn(), +}; +const _mockNativeImage = { + setTemplateImage: vitest_1.vi.fn(), +}; +exports.nativeImage = { + createFromPath: vitest_1.vi.fn().mockReturnValue(_mockNativeImage), +}; +exports.Tray = vitest_1.vi.fn().mockImplementation(function () { + return { + setToolTip: vitest_1.vi.fn(), + setContextMenu: vitest_1.vi.fn(), + on: vitest_1.vi.fn(), + destroy: vitest_1.vi.fn(), + }; +}); +exports.MenuItem = vitest_1.vi.fn().mockImplementation(function (options) { + Object.assign(this, options); +}); +const _mockMenuInstance = { + items: [ + { + label: 'File', + submenu: { + insert: vitest_1.vi.fn(), + }, + }, + ], + getMenuItemById: vitest_1.vi.fn().mockReturnValue({ label: '' }), +}; +exports.Menu = { + buildFromTemplate: vitest_1.vi.fn().mockReturnValue(_mockMenuInstance), + getApplicationMenu: vitest_1.vi.fn().mockReturnValue(_mockMenuInstance), + setApplicationMenu: vitest_1.vi.fn(), +}; +const _mockNotificationInstance = { + show: vitest_1.vi.fn(), + on: vitest_1.vi.fn(), +}; +exports.Notification = Object.assign(vitest_1.vi.fn().mockImplementation(function () { + return _mockNotificationInstance; +}), { + // Static method + isSupported: vitest_1.vi.fn().mockReturnValue(true), + // Expose the mock instance for assertions + _mockInstance: _mockNotificationInstance, +}); +exports.shell = { + openExternal: vitest_1.vi.fn().mockResolvedValue(undefined), +}; +exports.contextBridge = { + exposeInMainWorld: vitest_1.vi.fn(), +}; +exports.ipcRenderer = { + invoke: vitest_1.vi.fn(), + on: vitest_1.vi.fn(), + removeListener: vitest_1.vi.fn(), +}; diff --git a/src/app-extracted/dist/ai-provider-settings.html b/src/app-extracted/dist/ai-provider-settings.html new file mode 100644 index 0000000..3e704bd --- /dev/null +++ b/src/app-extracted/dist/ai-provider-settings.html @@ -0,0 +1,1070 @@ + + + + + + AI Provider Settings + + + +
+
+

πŸ€– AI Provider Settings

+

Configure and manage your AI provider connections

+
+ +
+
+

Active Providers

+
+
+
πŸ”„
+
Loading providers...
+
+
+ +
+ +
+

Model Settings

+
+
+ + +
+ +
+ + +
+ +
+ +
Controls randomness. Lower = more focused, Higher = more creative
+
+ + 0.7 +
+
+ +
+ +
Maximum number of tokens in the response
+ +
+ +
+ +
Stream responses as they're generated (faster perceived response)
+ +
+ +
+ +
+
+
+
+
+ + + + + +
+ + + + diff --git a/src/app-extracted/dist/aiProviderAPI.ts b/src/app-extracted/dist/aiProviderAPI.ts new file mode 100644 index 0000000..ee22e93 --- /dev/null +++ b/src/app-extracted/dist/aiProviderAPI.ts @@ -0,0 +1,163 @@ +export enum AIProviderCapability { + CHAT = 'chat', + COMPLETION = 'completion', + EMBEDDING = 'embedding', + VISION = 'vision', + TOOL_USE = 'tool_use', + STREAMING = 'streaming', +} + +export enum AIProviderType { + OPENAI = 'openai', + ANTHROPIC = 'anthropic', + OLLAMA = 'ollama', + GROQ = 'groq', + OPENROUTER = 'openrouter', + CUSTOM = 'custom', +} + +export interface AIProvider { + id: string; + name: string; + type: AIProviderType; + endpoint: string; + apiKey: string; + models: string[]; + defaultModel: string; + capabilities: AIProviderCapability[]; + isEnabled: boolean; + isDefault: boolean; +} + +export interface AIProviderCreate { + name: string; + type: AIProviderType; + endpoint: string; + apiKey: string; + models: string[]; + defaultModel?: string; + capabilities?: AIProviderCapability[]; +} + +export interface AIProviderUpdate { + name?: string; + endpoint?: string; + apiKey?: string; + models?: string[]; + defaultModel?: string; + capabilities?: AIProviderCapability[]; + isEnabled?: boolean; + isDefault?: boolean; +} + +export interface AIConnectionTest { + success: boolean; + status: number; + message: string; +} + +export interface AISettings { + provider: string; + model: string; + temperature: number; + maxTokens: number; + streaming: boolean; + embeddingProvider: string; +} + +export class AIProviderAPI { + async getProviders(): Promise { + return await (window as any).electron.invoke('ai:get-providers'); + } + + async getProvider(id: string): Promise { + return await (window as any).electron.invoke('ai:get-provider', id); + } + + async getEnabledProviders(): Promise { + return await (window as any).electron.invoke('ai:get-enabled-providers'); + } + + async getDefaultProvider(): Promise { + return await (window as any).electron.invoke('ai:get-default-provider'); + } + + async getAvailablePresets(): Promise { + return await (window as any).electron.invoke('ai:get-available-presets'); + } + + async getPreset(presetName: string): Promise { + return await (window as any).electron.invoke('ai:get-preset', presetName); + } + + async addProvider(provider: AIProviderCreate): Promise { + return await (window as any).electron.invoke('ai:add-provider', provider); + } + + async addProviderFromPreset(presetName: string, apiKey: string = ''): Promise { + return await (window as any).electron.invoke('ai:add-provider-from-preset', presetName, apiKey); + } + + async updateProvider(id: string, updates: AIProviderUpdate): Promise { + return await (window as any).electron.invoke('ai:update-provider', id, updates); + } + + async deleteProvider(id: string): Promise { + return await (window as any).electron.invoke('ai:delete-provider', id); + } + + async setDefaultProvider(id: string): Promise { + return await (window as any).electron.invoke('ai:set-default-provider', id); + } + + async toggleProvider(id: string, enabled: boolean): Promise { + return await (window as any).electron.invoke('ai:toggle-provider', id, enabled); + } + + async testConnection(id: string): Promise { + return await (window as any).electron.invoke('ai:test-connection', id); + } + + async fetchModels(id: string): Promise { + return await (window as any).electron.invoke('ai:fetch-models', id); + } + + async getSettings(): Promise { + const items = await (window as any).electron.invoke('storage:get-items'); + return { + provider: items['aiProvider'] || 'openai-default', + model: items['aiModel'] || 'gpt-4o', + temperature: parseFloat(items['aiTemperature'] || '0.7'), + maxTokens: parseInt(items['aiMaxTokens'] || '4096', 10), + streaming: items['aiStreaming'] === 'true', + embeddingProvider: items['aiEmbeddingProvider'] || 'openai-default', + }; + } + + async updateSettings(settings: Partial): Promise { + const changes: Record = {}; + + if (settings.provider !== undefined) { + changes['aiProvider'] = settings.provider; + } + if (settings.model !== undefined) { + changes['aiModel'] = settings.model; + } + if (settings.temperature !== undefined) { + changes['aiTemperature'] = String(settings.temperature); + } + if (settings.maxTokens !== undefined) { + changes['aiMaxTokens'] = String(settings.maxTokens); + } + if (settings.streaming !== undefined) { + changes['aiStreaming'] = String(settings.streaming); + } + if (settings.embeddingProvider !== undefined) { + changes['aiEmbeddingProvider'] = settings.embeddingProvider; + } + + await (window as any).electron.invoke('storage:update-items', changes); + } +} + +export const aiProviderAPI = new AIProviderAPI(); diff --git a/src/app-extracted/dist/constants.js b/src/app-extracted/dist/constants.js new file mode 100644 index 0000000..2d7e3bb --- /dev/null +++ b/src/app-extracted/dist/constants.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LS_CERT_FINGERPRINT = exports.WINDOW_ORIGIN = exports.LS_LOG_FILE_NAME = exports.DYNAMIC_PORT = void 0; +/** Pass 0 to the LS so the OS assigns an available port automatically. */ +exports.DYNAMIC_PORT = 0; +exports.LS_LOG_FILE_NAME = 'language_server.log'; +exports.WINDOW_ORIGIN = 'https://127.0.0.1'; +exports.LS_CERT_FINGERPRINT = 'sha256/sTZpQemOWEytaZqa7P/y/dNXbHMdOAzMvzHEhUwHZXw='; diff --git a/src/app-extracted/dist/customScheme.js b/src/app-extracted/dist/customScheme.js new file mode 100644 index 0000000..f460dc5 --- /dev/null +++ b/src/app-extracted/dist/customScheme.js @@ -0,0 +1,55 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.extensionAuthorities = void 0; +exports.registerCustomSchemes = registerCustomSchemes; +exports.registerCustomSchemeHandlers = registerCustomSchemeHandlers; +const electron_1 = require("electron"); +// A map of extension authority -> original URL (http://localhost:) +// The authority is usually a hash of unique extension identifiers +// like extension ID + port + project ID. An extension running on localhost: +// is then exposed on plugin://. +exports.extensionAuthorities = new Map(); +function registerCustomSchemes() { + electron_1.protocol.registerSchemesAsPrivileged([ + { + scheme: 'plugin', + privileges: { + standard: true, + secure: true, + supportFetchAPI: true, + corsEnabled: true, + allowServiceWorkers: true, + codeCache: true, + }, + }, + ]); +} +function registerCustomSchemeHandlers() { + // Handle custom scheme for UI extensions + electron_1.protocol.handle('plugin', async (request) => { + const url = new URL(request.url); + const authority = url.hostname; + const originalHost = exports.extensionAuthorities.get(authority); + if (!originalHost) { + return new Response(null, { status: 404 }); + } + const targetUrl = new URL(url.pathname + url.search, originalHost); + try { + const fetchOptions = { + method: request.method, + headers: request.headers, + body: request.body, + }; + if (request.body) { + // Required by Electron's net.fetch when the body is a stream + fetchOptions.duplex = 'half'; + } + const response = await electron_1.net.fetch(targetUrl.toString(), fetchOptions); + return response; + } + catch (err) { + console.error(`Failed to proxy request to ${targetUrl}:`, err); + return new Response(null, { status: 500 }); + } + }); +} diff --git a/src/app-extracted/dist/ideInstall/constants.js b/src/app-extracted/dist/ideInstall/constants.js new file mode 100644 index 0000000..d9ecb29 --- /dev/null +++ b/src/app-extracted/dist/ideInstall/constants.js @@ -0,0 +1,131 @@ +"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 }); +exports.WIZARD_SHOWN_KEY = void 0; +exports.fetchIdeDownloadUrl = fetchIdeDownloadUrl; +exports.getPlatformKey = getPlatformKey; +exports.getIdeInstallPath = getIdeInstallPath; +exports.shouldShowIdeInstallWizard = shouldShowIdeInstallWizard; +/** + * IDE Install β€” Constants, platform helpers, and condition checks. + */ +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); +const os = __importStar(require("os")); +const main_1 = __importDefault(require("electron-log/main")); +const paths_1 = require("../paths"); +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- +exports.WIZARD_SHOWN_KEY = 'ide-install-wizard-shown'; +/** + * Fetches the latest stable IDE download URL for a given platform. + */ +async function fetchIdeDownloadUrl(platformKey) { + const url = `https://antigravity-ide-auto-updater-974169037036.us-central1.run.app/api/update/${platformKey}/stable/latest`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch IDE download URL: ${response.status} ${response.statusText}`); + } + const data = (await response.json()); + if (!data.url) { + throw new Error(`No download URL found in the auto-updater response for platform: ${platformKey}`); + } + return data.url; +} +// --------------------------------------------------------------------------- +// Platform Helpers +// --------------------------------------------------------------------------- +function getPlatformKey() { + if (process.platform === 'darwin' && process.arch === 'x64') { + return 'darwin'; + } + let suffix = ''; + if (process.platform === 'win32') { + suffix = '-user'; + } + return `${process.platform}-${process.arch}${suffix}`; +} +/** + * Returns the expected installation path for the IDE. + */ +function getIdeInstallPath() { + switch (process.platform) { + case 'darwin': + return '/Applications/Antigravity IDE.app'; + case 'win32': + return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'Programs', 'Antigravity IDE'); + case 'linux': + return path.join(os.homedir(), '.local', 'share', 'antigravity-ide'); + default: + return path.join(os.homedir(), 'antigravity-ide'); + } +} +// --------------------------------------------------------------------------- +// Condition Checks +// --------------------------------------------------------------------------- +/** + * Determines whether the IDE install wizard should be shown. + * + * Conditions (all must be true): + * 1. Wizard has not been shown before (checked via storage) + * 2. `~/.gemini/antigravity-ide` does NOT exist + * 3. `~/.gemini/antigravity` DOES exist + */ +async function shouldShowIdeInstallWizard(storageManager) { + // 1. Already shown? + const items = await storageManager.getItems(); + if (items[exports.WIZARD_SHOWN_KEY] === 'true') { + main_1.default.info('[IDE Wizard] Already shown, skipping.'); + return false; + } + // 1a. If not shown before, then now mark it as shown. + await storageManager.updateItems({ [exports.WIZARD_SHOWN_KEY]: 'true' }); + // 2. IDE already installed separately? + if (fs.existsSync(paths_1.IDE_NEW_DATA_DIR)) { + main_1.default.info(`[IDE Wizard] ${paths_1.IDE_NEW_DATA_DIR} exists β€” IDE already installed, skipping.`); + return false; + } + // 3. Old IDE data present (user was migrated)? + if (!fs.existsSync(paths_1.IDE_OLD_DATA_DIR)) { + main_1.default.info(`[IDE Wizard] ${paths_1.IDE_OLD_DATA_DIR} not found β€” user was not migrated, skipping.`); + return false; + } + main_1.default.info('[IDE Wizard] All conditions met β€” will show wizard.'); + return true; +} diff --git a/src/app-extracted/dist/ideInstall/index.js b/src/app-extracted/dist/ideInstall/index.js new file mode 100644 index 0000000..89d2b43 --- /dev/null +++ b/src/app-extracted/dist/ideInstall/index.js @@ -0,0 +1,29 @@ +"use strict"; +/** + * IDE Install β€” Public API. + * + * Re-exports the public surface from the sub-modules so consumers + * can simply `import { … } from './ideInstall'`. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.showIdeInstallWizard = exports.maybeShowIdeInstallWizard = exports.downloadAndInstallIde = exports.copyUserData = exports.extractIde = exports.downloadFile = exports.shouldShowIdeInstallWizard = exports.getIdeInstallPath = exports.getPlatformKey = exports.fetchIdeDownloadUrl = exports.WIZARD_SHOWN_KEY = exports.IDE_BACKUP_DATA_DIR = exports.IDE_NEW_DATA_DIR = exports.IDE_OLD_DATA_DIR = void 0; +// Constants, platform helpers, condition checks +var paths_1 = require("../paths"); +Object.defineProperty(exports, "IDE_OLD_DATA_DIR", { enumerable: true, get: function () { return paths_1.IDE_OLD_DATA_DIR; } }); +Object.defineProperty(exports, "IDE_NEW_DATA_DIR", { enumerable: true, get: function () { return paths_1.IDE_NEW_DATA_DIR; } }); +Object.defineProperty(exports, "IDE_BACKUP_DATA_DIR", { enumerable: true, get: function () { return paths_1.IDE_BACKUP_DATA_DIR; } }); +var constants_1 = require("./constants"); +Object.defineProperty(exports, "WIZARD_SHOWN_KEY", { enumerable: true, get: function () { return constants_1.WIZARD_SHOWN_KEY; } }); +Object.defineProperty(exports, "fetchIdeDownloadUrl", { enumerable: true, get: function () { return constants_1.fetchIdeDownloadUrl; } }); +Object.defineProperty(exports, "getPlatformKey", { enumerable: true, get: function () { return constants_1.getPlatformKey; } }); +Object.defineProperty(exports, "getIdeInstallPath", { enumerable: true, get: function () { return constants_1.getIdeInstallPath; } }); +Object.defineProperty(exports, "shouldShowIdeInstallWizard", { enumerable: true, get: function () { return constants_1.shouldShowIdeInstallWizard; } }); +var service_1 = require("./service"); +Object.defineProperty(exports, "downloadFile", { enumerable: true, get: function () { return service_1.downloadFile; } }); +Object.defineProperty(exports, "extractIde", { enumerable: true, get: function () { return service_1.extractIde; } }); +Object.defineProperty(exports, "copyUserData", { enumerable: true, get: function () { return service_1.copyUserData; } }); +Object.defineProperty(exports, "downloadAndInstallIde", { enumerable: true, get: function () { return service_1.downloadAndInstallIde; } }); +// Wizard window +var wizard_1 = require("./wizard"); +Object.defineProperty(exports, "maybeShowIdeInstallWizard", { enumerable: true, get: function () { return wizard_1.maybeShowIdeInstallWizard; } }); +Object.defineProperty(exports, "showIdeInstallWizard", { enumerable: true, get: function () { return wizard_1.showIdeInstallWizard; } }); diff --git a/src/app-extracted/dist/ideInstall/service.js b/src/app-extracted/dist/ideInstall/service.js new file mode 100644 index 0000000..0ef51bb --- /dev/null +++ b/src/app-extracted/dist/ideInstall/service.js @@ -0,0 +1,197 @@ +"use strict"; +/** + * IDE Install Service β€” Download, extract, copy, and launch logic. + */ +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 }); +exports.downloadFile = downloadFile; +exports.extractIde = extractIde; +exports.copyUserData = copyUserData; +exports.downloadAndInstallIde = downloadAndInstallIde; +const fs = __importStar(require("fs")); +const fsPromises = __importStar(require("fs/promises")); +const path = __importStar(require("path")); +const os = __importStar(require("os")); +const https = __importStar(require("https")); +const http = __importStar(require("http")); +const main_1 = __importDefault(require("electron-log/main")); +const constants_1 = require("./constants"); +const paths_1 = require("../paths"); +// --------------------------------------------------------------------------- +// Download +// --------------------------------------------------------------------------- +function downloadFile(url, destPath, onProgress, maxRedirects = 5) { + return new Promise((resolve, reject) => { + if (maxRedirects <= 0) { + reject(new Error('Too many redirects')); + return; + } + const proto = url.startsWith('https') ? https : http; + const req = proto.get(url, (res) => { + if (res.statusCode && + res.statusCode >= 300 && + res.statusCode < 400 && + res.headers.location) { + const redirectUrl = res.headers.location.startsWith('http') + ? res.headers.location + : new URL(res.headers.location, url).toString(); + downloadFile(redirectUrl, destPath, onProgress, maxRedirects - 1) + .then(resolve) + .catch(reject); + return; + } + if (res.statusCode && res.statusCode >= 400) { + reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`)); + return; + } + const totalBytes = parseInt(res.headers['content-length'] || '0', 10); + let downloadedBytes = 0; + const dir = path.dirname(destPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + const fileStream = fs.createWriteStream(destPath); + res.on('data', (chunk) => { + downloadedBytes += chunk.length; + if (totalBytes > 0 && onProgress) { + onProgress(Math.round((downloadedBytes / totalBytes) * 100)); + } + }); + res.pipe(fileStream); + fileStream.on('finish', () => { + fileStream.close(); + resolve(); + }); + fileStream.on('error', (err) => { + fs.unlinkSync(destPath); + reject(err); + }); + }); + req.on('error', reject); + }); +} +// --------------------------------------------------------------------------- +// Extract +// --------------------------------------------------------------------------- +async function extractIde(archivePath, installPath) { + const { execFile } = await Promise.resolve().then(() => __importStar(require('child_process'))); + const { promisify } = await Promise.resolve().then(() => __importStar(require('util'))); + const execFileAsync = promisify(execFile); + if (!fs.existsSync(path.dirname(installPath))) { + await fsPromises.mkdir(path.dirname(installPath), { recursive: true }); + } + switch (process.platform) { + case 'darwin': { + const tempDir = path.join(os.tmpdir(), 'antigravity-ide-extract'); + if (fs.existsSync(tempDir)) { + await execFileAsync('rm', ['-rf', tempDir]); + } + await fsPromises.mkdir(tempDir, { recursive: true }); + await execFileAsync('unzip', ['-o', '-q', archivePath, '-d', tempDir]); + const entries = await fsPromises.readdir(tempDir); + const appBundle = entries.find((e) => e.endsWith('.app')); + if (!appBundle) { + throw new Error('No .app bundle found in the downloaded archive'); + } + if (fs.existsSync(installPath)) { + await execFileAsync('rm', ['-rf', installPath]); + } + await execFileAsync('mv', [path.join(tempDir, appBundle), installPath]); + if (fs.existsSync(tempDir)) { + await execFileAsync('rm', ['-rf', tempDir]); + } + break; + } + case 'linux': { + if (!fs.existsSync(installPath)) { + await fsPromises.mkdir(installPath, { recursive: true }); + } + await execFileAsync('tar', [ + '-xzf', + archivePath, + '-C', + installPath, + '--strip-components=1', + ]); + break; + } + case 'win32': { + await execFileAsync(archivePath, ['/VERYSILENT', '/MERGETASKS=!runcode']); + break; + } + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } +} +// --------------------------------------------------------------------------- +// Copy User Data +// --------------------------------------------------------------------------- +async function copyUserData(sourcePath, destPath) { + if (!fs.existsSync(sourcePath)) { + main_1.default.warn(`[IDE Wizard] Source path does not exist: ${sourcePath}`); + return; + } + await fsPromises.cp(sourcePath, destPath, { recursive: true, force: true }); + main_1.default.info(`[IDE Wizard] Copied user data: ${sourcePath} β†’ ${destPath}`); +} +// --------------------------------------------------------------------------- +// Download & Install (orchestrator) +// --------------------------------------------------------------------------- +async function downloadAndInstallIde() { + const platformKey = (0, constants_1.getPlatformKey)(); + const downloadUrl = await (0, constants_1.fetchIdeDownloadUrl)(platformKey); + const ext = process.platform === 'win32' + ? '.exe' + : process.platform === 'linux' + ? '.tar.gz' + : '.zip'; + const tempFile = path.join(os.tmpdir(), `antigravity-ide-download${ext}`); + main_1.default.info(`[IDE Wizard] Downloading IDE from ${downloadUrl}…`); + await downloadFile(downloadUrl, tempFile); + const installPath = (0, constants_1.getIdeInstallPath)(); + main_1.default.info(`[IDE Wizard] Installing IDE to ${installPath}…`); + await extractIde(tempFile, installPath); + main_1.default.info(`[IDE Wizard] Copying user data…`); + await copyUserData(paths_1.IDE_OLD_DATA_DIR, paths_1.IDE_NEW_DATA_DIR); + try { + await fsPromises.unlink(tempFile); + } + catch { + /* ignore */ + } +} diff --git a/src/app-extracted/dist/ideInstall/wizard.js b/src/app-extracted/dist/ideInstall/wizard.js new file mode 100644 index 0000000..00cb2b0 --- /dev/null +++ b/src/app-extracted/dist/ideInstall/wizard.js @@ -0,0 +1,155 @@ +"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 }); +exports.showIdeInstallWizard = showIdeInstallWizard; +exports.maybeShowIdeInstallWizard = maybeShowIdeInstallWizard; +/** + * IDE Install Wizard β€” Window orchestration and IPC handlers. + */ +const electron_1 = require("electron"); +const path = __importStar(require("path")); +const fs = __importStar(require("fs")); +const main_1 = __importDefault(require("electron-log/main")); +const constants_1 = require("./constants"); +const paths_1 = require("../paths"); +const service_1 = require("./service"); +const wizardHtml_1 = require("./wizardHtml"); +/** + * Shows the IDE install wizard as a modal window. + * Returns a promise that resolves when the wizard is dismissed. + */ +function showIdeInstallWizard() { + return new Promise((resolve) => { + const wizardWindow = new electron_1.BrowserWindow({ + width: 720, + height: 580, + resizable: false, + minimizable: false, + maximizable: false, + fullscreenable: false, + titleBarStyle: 'hidden', + trafficLightPosition: { x: 12, y: 12 }, + backgroundColor: '#0D0D0D', + show: false, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, 'wizardPreload.js'), + }, + }); + const iconPath = path.join(__dirname, '..', '..', 'icon.png'); + let iconBase64 = ''; + try { + if (fs.existsSync(iconPath)) { + iconBase64 = fs.readFileSync(iconPath).toString('base64'); + } + else { + main_1.default.warn(`[IDE Wizard] Icon not found at ${iconPath}`); + } + } + catch (e) { + main_1.default.error(`[IDE Wizard] Failed to read icon: ${e}`); + } + const html = (0, wizardHtml_1.getWizardHtml)(iconBase64); + let isResolved = false; + const cleanup = () => { + if (isResolved) { + return; + } + isResolved = true; + electron_1.ipcMain.removeHandler('wizard:complete'); + resolve(); + }; + electron_1.ipcMain.handle('wizard:complete', async (_event, shouldDownload) => { + cleanup(); + wizardWindow.close(); + if (shouldDownload) { + main_1.default.info('[IDE Wizard] Background download requested. Starting installation in background...'); + void (0, service_1.downloadAndInstallIde)().catch((err) => { + main_1.default.error(`[IDE Wizard] Background download/install failed: ${err}`); + }); + } + }); + wizardWindow.on('closed', () => { + cleanup(); + }); + const doSetup = async () => { + // If the old Antigravity user data directory exists, copy it to the new IDE + // data dir and to a backup directory. + if (fs.existsSync(paths_1.IDE_OLD_DATA_DIR)) { + if (!fs.existsSync(paths_1.IDE_NEW_DATA_DIR)) { + try { + await (0, service_1.copyUserData)(paths_1.IDE_OLD_DATA_DIR, paths_1.IDE_NEW_DATA_DIR); + } + catch (err) { + main_1.default.error(`[IDE Wizard] Failed to copy to new IDE data dir: ${err}`); + } + } + if (!fs.existsSync(paths_1.IDE_BACKUP_DATA_DIR)) { + try { + await (0, service_1.copyUserData)(paths_1.IDE_OLD_DATA_DIR, paths_1.IDE_BACKUP_DATA_DIR); + } + catch (err) { + main_1.default.error(`[IDE Wizard] Failed to copy to backup IDE data dir: ${err}`); + } + } + } + if (!wizardWindow.isDestroyed()) { + wizardWindow.webContents.send('wizard:setup-complete'); + } + }; + wizardWindow.once('ready-to-show', () => { + wizardWindow.show(); + void doSetup(); + }); + void wizardWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`); + }); +} +/** + * Checks conditions and shows the IDE install wizard if appropriate. + * This should be called early in the app lifecycle, before the LS starts. + * Returns true if the wizard was shown, false otherwise. + */ +async function maybeShowIdeInstallWizard(storageManager) { + const shouldShow = await (0, constants_1.shouldShowIdeInstallWizard)(storageManager); + if (!shouldShow) { + return false; + } + await showIdeInstallWizard(); + return true; +} diff --git a/src/app-extracted/dist/ideInstall/wizardHtml.js b/src/app-extracted/dist/ideInstall/wizardHtml.js new file mode 100644 index 0000000..9a3ee89 --- /dev/null +++ b/src/app-extracted/dist/ideInstall/wizardHtml.js @@ -0,0 +1,286 @@ +"use strict"; +/** + * IDE Install Wizard β€” HTML template for the wizard UI. + * + * This is a self-contained page with all CSS/JS embedded, rendered inline + * in a standalone BrowserWindow. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getWizardHtml = getWizardHtml; +/** + * Returns the inline HTML for the IDE install wizard. + * This is a self-contained page with all CSS/JS embedded. + */ +function getWizardHtml(iconBase64) { + return ` + + + + +Welcome to Antigravity + + + +
+
+ + +
+
+
+
+
Setting up…
+
+ + +
+
+ Antigravity Icon +
+

Welcome to the new Antigravity!

+

Antigravity has been redesigned to put agents first with new capabilities. If you'd still like a code editor, you can download it as a separate app named Antigravity IDE.

+ + + +
+ +
+
+ +
+ + + +`; +} diff --git a/src/app-extracted/dist/ideInstall/wizardPreload.js b/src/app-extracted/dist/ideInstall/wizardPreload.js new file mode 100644 index 0000000..a4484c1 --- /dev/null +++ b/src/app-extracted/dist/ideInstall/wizardPreload.js @@ -0,0 +1,23 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * Preload script for the IDE Install Wizard window. + * + * This is a minimal, self-contained preload that exposes only the APIs + * needed by the wizard's inline HTML UI. It runs in its own + * BrowserWindow, separate from the main app window and its preload. + */ +const electron_1 = require("electron"); +const wizardAPI = { + completeWizard: (shouldDownload) => electron_1.ipcRenderer.invoke('wizard:complete', shouldDownload), + onSetupComplete: (callback) => { + const handler = () => { + callback(); + }; + electron_1.ipcRenderer.on('wizard:setup-complete', handler); + return () => { + electron_1.ipcRenderer.removeListener('wizard:setup-complete', handler); + }; + }, +}; +electron_1.contextBridge.exposeInMainWorld('wizardAPI', wizardAPI); diff --git a/src/app-extracted/dist/ideInstallService.test.js b/src/app-extracted/dist/ideInstallService.test.js new file mode 100644 index 0000000..3f871bb --- /dev/null +++ b/src/app-extracted/dist/ideInstallService.test.js @@ -0,0 +1,350 @@ +"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; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const vitest_1 = require("vitest"); +const helpers_1 = require("./test/helpers"); +// Use the shared auto-mocks from __mocks__/ +vitest_1.vi.mock('electron'); +// Mock electron-log +vitest_1.vi.mock('electron-log/main', () => ({ + default: { + info: vitest_1.vi.fn(), + warn: vitest_1.vi.fn(), + error: vitest_1.vi.fn(), + }, +})); +// Mock fs +vitest_1.vi.mock('fs', () => ({ + existsSync: vitest_1.vi.fn(), + mkdirSync: vitest_1.vi.fn(), + createWriteStream: vitest_1.vi.fn(), + unlinkSync: vitest_1.vi.fn(), +})); +// Mock fs/promises +vitest_1.vi.mock('fs/promises', () => ({ + cp: vitest_1.vi.fn().mockResolvedValue(undefined), + mkdir: vitest_1.vi.fn().mockResolvedValue(undefined), + rm: vitest_1.vi.fn().mockResolvedValue(undefined), + rename: vitest_1.vi.fn().mockResolvedValue(undefined), + readdir: vitest_1.vi.fn().mockResolvedValue(['Antigravity.app']), + unlink: vitest_1.vi.fn().mockResolvedValue(undefined), +})); +// Mock child_process (used by extractIde) +vitest_1.vi.mock('child_process', () => ({ + execFile: vitest_1.vi.fn(), +})); +// Mock storage +const mockStorageManager = { + getItems: vitest_1.vi.fn(), + updateItems: vitest_1.vi.fn(), + onDidChange: vitest_1.vi.fn().mockReturnValue({ dispose: vitest_1.vi.fn() }), +}; +(0, vitest_1.describe)('ideInstallService', () => { + (0, vitest_1.beforeEach)(() => { + vitest_1.vi.clearAllMocks(); + vitest_1.vi.resetModules(); + (0, helpers_1.silenceConsole)(); + }); + (0, vitest_1.afterEach)(() => { + vitest_1.vi.restoreAllMocks(); + }); + (0, vitest_1.describe)('shouldShowIdeInstallWizard', () => { + (0, vitest_1.it)('should return false if wizard was already shown', async () => { + mockStorageManager.getItems.mockResolvedValue({ + 'ide-install-wizard-shown': 'true', + }); + const { shouldShowIdeInstallWizard } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + const result = await shouldShowIdeInstallWizard(mockStorageManager); + (0, vitest_1.expect)(result).toBe(false); + }); + (0, vitest_1.it)('should return false if ~/.gemini/antigravity-ide already exists', async () => { + const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs'))); + mockStorageManager.getItems.mockResolvedValue({}); + // First call: IDE_NEW_DATA_DIR exists + vitest_1.vi.mocked(existsSync).mockReturnValue(true); + const { shouldShowIdeInstallWizard } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + const result = await shouldShowIdeInstallWizard(mockStorageManager); + (0, vitest_1.expect)(result).toBe(false); + }); + (0, vitest_1.it)('should return false if ~/.gemini/antigravity does not exist', async () => { + const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs'))); + mockStorageManager.getItems.mockResolvedValue({}); + // Both dirs don't exist + vitest_1.vi.mocked(existsSync).mockReturnValue(false); + const { shouldShowIdeInstallWizard } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + const result = await shouldShowIdeInstallWizard(mockStorageManager); + (0, vitest_1.expect)(result).toBe(false); + }); + (0, vitest_1.it)('should return true when all conditions are met', async () => { + const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs'))); + mockStorageManager.getItems.mockResolvedValue({}); + // IDE_NEW_DATA_DIR does NOT exist (first call), IDE_OLD_DATA_DIR DOES exist (second call) + vitest_1.vi.mocked(existsSync) + .mockReturnValueOnce(false) // IDE_NEW_DATA_DIR + .mockReturnValueOnce(true); // IDE_OLD_DATA_DIR + const { shouldShowIdeInstallWizard } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + const result = await shouldShowIdeInstallWizard(mockStorageManager); + (0, vitest_1.expect)(result).toBe(true); + }); + }); + (0, vitest_1.describe)('getPlatformKey', () => { + let originalPlatform; + let originalArch; + (0, vitest_1.beforeEach)(() => { + originalPlatform = process.platform; + originalArch = process.arch; + }); + (0, vitest_1.afterEach)(() => { + Object.defineProperty(process, 'platform', { + value: originalPlatform, + configurable: true, + }); + Object.defineProperty(process, 'arch', { + value: originalArch, + configurable: true, + }); + }); + (0, vitest_1.it)('should return just "darwin" on macOS x64', async () => { + Object.defineProperty(process, 'platform', { + value: 'darwin', + configurable: true, + }); + Object.defineProperty(process, 'arch', { + value: 'x64', + configurable: true, + }); + const { getPlatformKey } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + (0, vitest_1.expect)(getPlatformKey()).toBe('darwin'); + }); + (0, vitest_1.it)('should return "darwin-arm64" on macOS arm64', async () => { + Object.defineProperty(process, 'platform', { + value: 'darwin', + configurable: true, + }); + Object.defineProperty(process, 'arch', { + value: 'arm64', + configurable: true, + }); + const { getPlatformKey } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + (0, vitest_1.expect)(getPlatformKey()).toBe('darwin-arm64'); + }); + (0, vitest_1.it)('should return "win32-x64-user" on Windows x64', async () => { + Object.defineProperty(process, 'platform', { + value: 'win32', + configurable: true, + }); + Object.defineProperty(process, 'arch', { + value: 'x64', + configurable: true, + }); + const { getPlatformKey } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + (0, vitest_1.expect)(getPlatformKey()).toBe('win32-x64-user'); + }); + (0, vitest_1.it)('should return "linux-x64" on Linux x64', async () => { + Object.defineProperty(process, 'platform', { + value: 'linux', + configurable: true, + }); + Object.defineProperty(process, 'arch', { + value: 'x64', + configurable: true, + }); + const { getPlatformKey } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + (0, vitest_1.expect)(getPlatformKey()).toBe('linux-x64'); + }); + }); + (0, vitest_1.describe)('getIdeInstallPath', () => { + (0, vitest_1.it)('should return a non-empty install path', async () => { + const { getIdeInstallPath } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + const installPath = getIdeInstallPath(); + (0, vitest_1.expect)(installPath).toBeTruthy(); + (0, vitest_1.expect)(typeof installPath).toBe('string'); + }); + }); + (0, vitest_1.describe)('copyUserData', () => { + (0, vitest_1.it)('should recursively copy source to destination', async () => { + const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs'))); + const fsPromises = await Promise.resolve().then(() => __importStar(require('fs/promises'))); + vitest_1.vi.mocked(existsSync).mockReturnValue(true); + const { copyUserData } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + await copyUserData('/source', '/dest'); + (0, vitest_1.expect)(fsPromises.cp).toHaveBeenCalledWith('/source', '/dest', { + recursive: true, + force: true, + }); + }); + (0, vitest_1.it)('should skip copy if source does not exist', async () => { + const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs'))); + const fsPromises = await Promise.resolve().then(() => __importStar(require('fs/promises'))); + vitest_1.vi.mocked(existsSync).mockReturnValue(false); + const { copyUserData } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + await copyUserData('/nonexistent', '/dest'); + (0, vitest_1.expect)(fsPromises.cp).not.toHaveBeenCalled(); + }); + }); + (0, vitest_1.describe)('maybeShowIdeInstallWizard', () => { + (0, vitest_1.it)('should return false when conditions are not met', async () => { + mockStorageManager.getItems.mockResolvedValue({ + 'ide-install-wizard-shown': 'true', + }); + const { maybeShowIdeInstallWizard } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + const result = await maybeShowIdeInstallWizard(mockStorageManager); + (0, vitest_1.expect)(result).toBe(false); + }); + (0, vitest_1.it)('should copy to NEW and BACKUP dirs if conditions are met and source exists', async () => { + const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs'))); + const fsPromises = await Promise.resolve().then(() => __importStar(require('fs/promises'))); + mockStorageManager.getItems.mockResolvedValue({}); + vitest_1.vi.mocked(existsSync).mockImplementation((p) => { + const pathStr = String(p); + if (pathStr.endsWith('antigravity-ide')) { + return false; + } + if (pathStr.endsWith('antigravity-backup')) { + return false; + } + if (pathStr.endsWith('antigravity')) { + return true; + } + if (pathStr.endsWith('icon.png')) { + return true; + } + return false; + }); + const { maybeShowIdeInstallWizard } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + const wizardPromise = maybeShowIdeInstallWizard(mockStorageManager); + // Trigger ready-to-show to run the setup copying logic + const { BrowserWindow } = await Promise.resolve().then(() => __importStar(require('electron'))); + const mockWindowInstance = vitest_1.vi.mocked(BrowserWindow).mock.results[0].value; + const readyToShowHandler = vitest_1.vi.mocked(mockWindowInstance.once).mock.calls.find((call) => call[0] === 'ready-to-show')?.[1]; + if (readyToShowHandler) { + await readyToShowHandler(); + } + // Simulate complete to resolve the promise + const { ipcMain } = await Promise.resolve().then(() => __importStar(require('electron'))); + const completeHandler = vitest_1.vi.mocked(ipcMain.handle).mock.calls.find((call) => call[0] === 'wizard:complete'); + if (completeHandler) { + await completeHandler[1]({}, false); + } + const result = await wizardPromise; + (0, vitest_1.expect)(result).toBe(true); + (0, vitest_1.expect)(fsPromises.cp).toHaveBeenCalledTimes(2); + }); + }); + (0, vitest_1.describe)('fetchIdeDownloadUrl', () => { + (0, vitest_1.it)('should fetch the correct URL from the API', async () => { + // Mock fetch + const mockFetch = vitest_1.vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + url: 'https://edgedl.me.gvt1.com/edgedl/release2/antigravity/darwin-arm/Antigravity IDE.zip', + }), + }); + global.fetch = mockFetch; + const { fetchIdeDownloadUrl } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + const url = await fetchIdeDownloadUrl('darwin-arm64'); + (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://antigravity-ide-auto-updater-974169037036.us-central1.run.app/api/update/darwin-arm64/stable/latest'); + (0, vitest_1.expect)(url).toBe('https://edgedl.me.gvt1.com/edgedl/release2/antigravity/darwin-arm/Antigravity IDE.zip'); + }); + (0, vitest_1.it)('should throw an error if the API request fails', async () => { + const mockFetch = vitest_1.vi.fn().mockResolvedValue({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + }); + global.fetch = mockFetch; + const { fetchIdeDownloadUrl } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + await (0, vitest_1.expect)(fetchIdeDownloadUrl('darwin-arm64')).rejects.toThrow('Failed to fetch IDE download URL: 500 Internal Server Error'); + }); + (0, vitest_1.it)('should throw an error if the API response has no URL', async () => { + const mockFetch = vitest_1.vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({}), + }); + global.fetch = mockFetch; + const { fetchIdeDownloadUrl } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + await (0, vitest_1.expect)(fetchIdeDownloadUrl('darwin-arm64')).rejects.toThrow('No download URL found in the auto-updater response for platform: darwin-arm64'); + }); + }); + (0, vitest_1.describe)('showIdeInstallWizard', () => { + (0, vitest_1.it)('should create a BrowserWindow with correct options', async () => { + const { BrowserWindow } = await Promise.resolve().then(() => __importStar(require('electron'))); + mockStorageManager.getItems.mockResolvedValue({}); + mockStorageManager.updateItems.mockResolvedValue(undefined); + const { showIdeInstallWizard } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + // Start the wizard but don't await β€” we'll resolve it via the mock + const wizardPromise = showIdeInstallWizard(); + // Verify BrowserWindow was created with expected options + (0, vitest_1.expect)(BrowserWindow).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ + width: 720, + height: 580, + resizable: false, + titleBarStyle: 'hidden', + backgroundColor: '#0D0D0D', + webPreferences: vitest_1.expect.objectContaining({ + nodeIntegration: false, + contextIsolation: true, + }), + })); + // Simulate complete to resolve the promise + const { ipcMain } = await Promise.resolve().then(() => __importStar(require('electron'))); + const completeHandler = vitest_1.vi.mocked(ipcMain.handle).mock.calls.find((call) => call[0] === 'wizard:complete'); + if (completeHandler) { + await completeHandler[1]({}, undefined); + } + const result = await wizardPromise; + (0, vitest_1.expect)(result).toBeUndefined(); + }); + (0, vitest_1.it)('should start background download if shouldDownload is true when skipping', async () => { + mockStorageManager.getItems.mockResolvedValue({}); + mockStorageManager.updateItems.mockResolvedValue(undefined); + const serviceModule = await Promise.resolve().then(() => __importStar(require('./ideInstall/service'))); + const downloadSpy = vitest_1.vi + .spyOn(serviceModule, 'downloadAndInstallIde') + .mockResolvedValue(undefined); + const { showIdeInstallWizard } = await Promise.resolve().then(() => __importStar(require('./ideInstall'))); + const wizardPromise = showIdeInstallWizard(); + // Simulate complete with shouldDownload = true + const { ipcMain } = await Promise.resolve().then(() => __importStar(require('electron'))); + const completeHandler = vitest_1.vi.mocked(ipcMain.handle).mock.calls.find((call) => call[0] === 'wizard:complete'); + if (completeHandler) { + await completeHandler[1]({}, true); + } + const result = await wizardPromise; + (0, vitest_1.expect)(result).toBeUndefined(); + (0, vitest_1.expect)(downloadSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app-extracted/dist/ipcHandlers.js b/src/app-extracted/dist/ipcHandlers.js new file mode 100644 index 0000000..d54543b --- /dev/null +++ b/src/app-extracted/dist/ipcHandlers.js @@ -0,0 +1,259 @@ +"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; + }; + 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 }); +exports.registerIpcHandlers = registerIpcHandlers; +const electron_1 = require("electron"); +const electron_updater_1 = require("electron-updater"); +const updater_1 = require("./updater"); +const main_1 = __importDefault(require("electron-log/main")); +const fs = __importStar(require("fs/promises")); +const customScheme_1 = require("./customScheme"); +const tray_1 = require("./tray"); +const aiProviderService_1 = require("./services/aiProviderService"); + +let aiProviderService; +/** + * Registers all IPC handlers for the main process. + */ +function registerIpcHandlers(storageManager) { + // Initialize AI Provider Service + aiProviderService = new aiProviderService_1.AIProviderService(storageManager); + aiProviderService.initialize().catch(err => { + console.error('Failed to initialize AI Provider Service:', err); + }); + // Dialog + electron_1.ipcMain.handle('dialog:open-workspace', async () => { + const result = await electron_1.dialog.showOpenDialog({ + properties: ['openDirectory', 'createDirectory'], + title: 'Open workspace', + }); + if (result.canceled || result.filePaths.length === 0) { + return undefined; + } + return result.filePaths[0]; + }); + // Auto-updater + electron_1.ipcMain.handle('updater:apply', async () => { + (0, updater_1.broadcastState)({ type: 'ready' }); + }); + electron_1.ipcMain.handle('updater:quit-and-install', () => { + if (!electron_1.app.isPackaged) { + console.log('[AutoUpdater] Skipping quitAndInstall (requires a packaged app).'); + return; + } + electron_updater_1.autoUpdater.quitAndInstall(); + }); + // Notifications + electron_1.ipcMain.handle('notification:send', (_, options) => { + const notification = new electron_1.Notification({ + title: options.title, + body: options.body, + silent: options.silent ?? false, + }); + notification.on('click', () => { + const win = electron_1.BrowserWindow.getAllWindows()[0]; + if (win) { + if (win.isMinimized()) { + win.restore(); + } + win.show(); + win.focus(); + if (options.payload) { + win.webContents.send('notification:clicked', options.payload); + } + } + }); + notification.show(); + }); + // Note: copied from our desktop AGY implementation: + // vs/platform/nativeNotification/electron-main/electronNotificationService.ts + electron_1.ipcMain.handle('notification:open-system-preferences', async () => { + if (process.platform === 'darwin') { + void electron_1.shell.openExternal('x-apple.systempreferences:com.apple.preference.notifications'); + } + else if (process.platform === 'win32') { + void electron_1.shell.openExternal('ms-settings:notifications'); + } + else if (process.platform === 'linux') { + const { exec } = await Promise.resolve().then(() => __importStar(require('child_process'))); + const commands = [ + 'gnome-control-center notifications', + 'systemsettings kcm_notifications', + 'xfce4-notifyd-config', + 'gnome-control-center', + 'systemsettings', + ]; + for (const command of commands) { + try { + exec(command); + return; // If one command executes without immediate error, assume success for now + } + catch { + // Try next + } + } + } + }); + // Storage + electron_1.ipcMain.handle('storage:get-items', async () => { + return storageManager.getItems(); + }); + electron_1.ipcMain.handle('storage:update-items', async (_event, changes) => { + await storageManager.updateItems(changes); + }); + // Logs + electron_1.ipcMain.handle('logs:electron', async () => { + try { + const logPath = main_1.default.transports.file.getFile().path; + const contents = await fs.readFile(logPath, 'utf-8'); + return contents; + } + catch (err) { + return `Failed to read logs: ${String(err)}`; + } + }); + // Sidecar extension custom scheme + electron_1.ipcMain.handle('extensions:send-authorities', async (_event, authorities) => { + customScheme_1.extensionAuthorities.clear(); + for (const [key, value] of Object.entries(authorities)) { + customScheme_1.extensionAuthorities.set(key, value); + } + }); + // Agent + electron_1.ipcMain.handle('agent:update-active-count', async (_event, count) => { + (0, tray_1.updateTrayAgentCount)(count); + }); + // Window + electron_1.ipcMain.handle('window:set-title-bar-overlay', async (_event, options) => { + const win = electron_1.BrowserWindow.getFocusedWindow() || electron_1.BrowserWindow.getAllWindows()[0]; + if (win && process.platform === 'win32') { + win.setTitleBarOverlay({ + color: options.color, + symbolColor: options.symbolColor, + height: 30, + }); + } + }); + electron_1.ipcMain.handle('window:minimize', async () => { + const win = electron_1.BrowserWindow.getFocusedWindow() || electron_1.BrowserWindow.getAllWindows()[0]; + if (win) { + win.minimize(); + } + }); + electron_1.ipcMain.handle('window:maximize', async () => { + const win = electron_1.BrowserWindow.getFocusedWindow() || electron_1.BrowserWindow.getAllWindows()[0]; + if (win) { + win.maximize(); + } + }); + electron_1.ipcMain.handle('window:unmaximize', async () => { + const win = electron_1.BrowserWindow.getFocusedWindow() || electron_1.BrowserWindow.getAllWindows()[0]; + if (win) { + win.unmaximize(); + } + }); + electron_1.ipcMain.handle('window:is-maximized', async () => { + const win = electron_1.BrowserWindow.getFocusedWindow() || electron_1.BrowserWindow.getAllWindows()[0]; + return win ? win.isMaximized() : false; + }); + electron_1.ipcMain.handle('window:close', async () => { + const win = electron_1.BrowserWindow.getFocusedWindow() || electron_1.BrowserWindow.getAllWindows()[0]; + if (win) { + win.close(); + } + }); + electron_1.ipcMain.handle('window:toggle-devtools', async () => { + const win = electron_1.BrowserWindow.getFocusedWindow() || electron_1.BrowserWindow.getAllWindows()[0]; + if (win) { + win.webContents.toggleDevTools(); + } + }); + // Auto-updater manual check + electron_1.ipcMain.handle('updater:check-for-updates', () => { + (0, updater_1.checkForUpdates)(true); + }); + // Safe external shell launch + electron_1.ipcMain.handle('shell:open-external', async (_event, url) => { + if (url.startsWith('https://') || url.startsWith('http://')) { + await electron_1.shell.openExternal(url); + } + }); + // AI Provider handlers + electron_1.ipcMain.handle('ai:get-providers', async () => { + return aiProviderService.getAllProviders(); + }); + electron_1.ipcMain.handle('ai:get-provider', async (_event, id) => { + return aiProviderService.getProvider(id); + }); + electron_1.ipcMain.handle('ai:get-enabled-providers', async () => { + return aiProviderService.getEnabledProviders(); + }); + electron_1.ipcMain.handle('ai:get-default-provider', async () => { + return aiProviderService.getDefaultProvider(); + }); + electron_1.ipcMain.handle('ai:get-available-presets', async () => { + return aiProviderService.getAvailablePresets(); + }); + electron_1.ipcMain.handle('ai:get-preset', async (_event, presetName) => { + return aiProviderService.getPreset(presetName); + }); + electron_1.ipcMain.handle('ai:add-provider', async (_event, provider) => { + return aiProviderService.addProvider(provider); + }); + electron_1.ipcMain.handle('ai:add-provider-from-preset', async (_event, presetName, apiKey) => { + return aiProviderService.addProviderFromPreset(presetName, apiKey); + }); + electron_1.ipcMain.handle('ai:update-provider', async (_event, id, updates) => { + return aiProviderService.updateProvider(id, updates); + }); + electron_1.ipcMain.handle('ai:delete-provider', async (_event, id) => { + await aiProviderService.deleteProvider(id); + }); + electron_1.ipcMain.handle('ai:set-default-provider', async (_event, id) => { + await aiProviderService.setDefaultProvider(id); + }); + electron_1.ipcMain.handle('ai:toggle-provider', async (_event, id, enabled) => { + await aiProviderService.toggleProvider(id, enabled); + }); + electron_1.ipcMain.handle('ai:test-connection', async (_event, id) => { + return aiProviderService.testConnection(id); + }); + electron_1.ipcMain.handle('ai:fetch-models', async (_event, id) => { + return aiProviderService.fetchModels(id); + }); +} diff --git a/src/app-extracted/dist/ipcHandlers.test.js b/src/app-extracted/dist/ipcHandlers.test.js new file mode 100644 index 0000000..79d722c --- /dev/null +++ b/src/app-extracted/dist/ipcHandlers.test.js @@ -0,0 +1,88 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const vitest_1 = require("vitest"); +const electron_1 = require("./__mocks__/electron"); +vitest_1.vi.mock('electron'); +vitest_1.vi.mock('electron-updater'); +// Capture registered handlers so we can invoke them in tests +const handlers = new Map(); +vitest_1.vi.mocked(electron_1.ipcMain.handle).mockImplementation((channel, handler) => { + handlers.set(channel, handler); +}); +// Import after mocks are in place +const ipcHandlers_1 = require("./ipcHandlers"); +(0, vitest_1.describe)('ipcHandlers β€” notifications', () => { + (0, vitest_1.beforeEach)(() => { + handlers.clear(); + vitest_1.vi.clearAllMocks(); + (0, ipcHandlers_1.registerIpcHandlers)({}); + }); + (0, vitest_1.describe)('notification:send', () => { + (0, vitest_1.it)('should create and show a Notification with the given options', () => { + const handler = handlers.get('notification:send'); + const options = { + id: 'test-1', + title: 'Test Title', + body: 'Test Body', + silent: true, + }; + handler({ sender: { send: vitest_1.vi.fn() } }, options); + (0, vitest_1.expect)(electron_1.Notification).toHaveBeenCalledWith({ + title: 'Test Title', + body: 'Test Body', + silent: true, + }); + (0, vitest_1.expect)(electron_1.Notification._mockInstance.show).toHaveBeenCalled(); + }); + (0, vitest_1.it)('should default silent to false when not specified', () => { + const handler = handlers.get('notification:send'); + handler({ sender: { send: vitest_1.vi.fn() } }, { id: 'test-2', title: 'T', body: 'B' }); + (0, vitest_1.expect)(electron_1.Notification).toHaveBeenCalledWith({ + title: 'T', + body: 'B', + silent: false, + }); + }); + (0, vitest_1.it)('should register a click handler that focuses the window', () => { + const handler = handlers.get('notification:send'); + handler({ sender: { send: vitest_1.vi.fn() } }, { id: 'test-3', title: 'T', body: 'B' }); + // Verify 'click' listener was registered + (0, vitest_1.expect)(electron_1.Notification._mockInstance.on).toHaveBeenCalledWith('click', vitest_1.expect.any(Function)); + // Simulate click + const clickHandler = vitest_1.vi.mocked(electron_1.Notification._mockInstance.on).mock + .calls[0][1]; + const mockWin = electron_1.BrowserWindow.getAllWindows()[0]; + Object.assign(mockWin, { + isMinimized: vitest_1.vi.fn().mockReturnValue(true), + restore: vitest_1.vi.fn(), + show: vitest_1.vi.fn(), + focus: vitest_1.vi.fn(), + }); + clickHandler(); + (0, vitest_1.expect)(mockWin.isMinimized).toHaveBeenCalled(); + (0, vitest_1.expect)(mockWin.restore).toHaveBeenCalled(); + (0, vitest_1.expect)(mockWin.show).toHaveBeenCalled(); + (0, vitest_1.expect)(mockWin.focus).toHaveBeenCalled(); + }); + }); + (0, vitest_1.describe)('notification:open-system-preferences', () => { + (0, vitest_1.it)('should call shell.openExternal on macOS', () => { + // Override platform for this test + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'darwin' }); + const handler = handlers.get('notification:open-system-preferences'); + handler({}); + (0, vitest_1.expect)(electron_1.shell.openExternal).toHaveBeenCalledWith('x-apple.systempreferences:com.apple.preference.notifications'); + // Restore + Object.defineProperty(process, 'platform', { value: originalPlatform }); + }); + (0, vitest_1.it)('should not call shell.openExternal on non-macOS', () => { + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'linux' }); + const handler = handlers.get('notification:open-system-preferences'); + handler({}); + (0, vitest_1.expect)(electron_1.shell.openExternal).not.toHaveBeenCalled(); + Object.defineProperty(process, 'platform', { value: originalPlatform }); + }); + }); +}); diff --git a/src/app-extracted/dist/keybindings.js b/src/app-extracted/dist/keybindings.js new file mode 100644 index 0000000..1b3a0f2 --- /dev/null +++ b/src/app-extracted/dist/keybindings.js @@ -0,0 +1,19 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.registerKeybindings = registerKeybindings; +const utils_1 = require("./utils"); +function registerKeybindings(win, actions) { + win.webContents.on('before-input-event', (event, input) => { + if (input.type === 'keyDown') { + const isCmdOrCtrl = (0, utils_1.isMacOS)() ? input.meta : input.control; + if (isCmdOrCtrl && input.shift && input.key.toLowerCase() === 'n') { + actions.createNewWindow(); + event.preventDefault(); + } + if (isCmdOrCtrl && input.key.toLowerCase() === 'q') { + actions.onQuitRequested(); + event.preventDefault(); + } + } + }); +} diff --git a/src/app-extracted/dist/languageServer.js b/src/app-extracted/dist/languageServer.js new file mode 100644 index 0000000..7a740f0 --- /dev/null +++ b/src/app-extracted/dist/languageServer.js @@ -0,0 +1,428 @@ +"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 }); +exports.LS_BINARY = void 0; +exports.getLsCL = getLsCL; +exports.getLsProcess = getLsProcess; +exports.getLsPort = getLsPort; +exports.clearLsProcess = clearLsProcess; +exports.extractCrashStackTrace = extractCrashStackTrace; +exports.startLanguageServer = startLanguageServer; +exports.setIntentionalTermination = setIntentionalTermination; +exports.startAndMonitorLanguageServer = startAndMonitorLanguageServer; +exports.killLanguageServer = killLanguageServer; +exports.setupLocalCertTrust = setupLocalCertTrust; +const child_process_1 = require("child_process"); +const electron_1 = require("electron"); +const shell_env_1 = require("shell-env"); +const fs = __importStar(require("fs")); +const path_1 = __importDefault(require("path")); +const readline = __importStar(require("readline")); +const stream_1 = require("stream"); +const paths_1 = require("./paths"); +const constants_1 = require("./constants"); +const utils_1 = require("./utils"); +// --------------------------------------------------------------------------- +// Config +// --------------------------------------------------------------------------- +const LS_STARTUP_TIMEOUT_MS = 60000; +// --------------------------------------------------------------------------- +// Crash Monitoring Constants +// --------------------------------------------------------------------------- +const RESTART_WINDOW_MS = 60000; +const MAX_RESTARTS = 3; +const RESTART_COOLDOWN_MS = 2000; +const MAX_STDERR_BUFFER = 100000; +const CRASH_TRIGGER_PHRASES = [ + 'panic:', + 'fatal error:', + 'unexpected fault address', + 'runtime:', + 'running GoogleExitFunction', + 'panic serving', +]; +const isWindows = process.platform === 'win32'; +const binName = isWindows ? 'language_server.exe' : 'language_server'; +exports.LS_BINARY = electron_1.app.isPackaged + ? path_1.default.join(process.resourcesPath, 'bin', binName) + : process.env.CODEIUM_LANGUAGE_SERVER_BIN || + path_1.default.join(__dirname, '..', 'bin', binName); +/** + * Gets the build CL of the language server by running it with --stamp. + */ +function getLsCL() { + return new Promise((resolve) => { + (0, child_process_1.execFile)(exports.LS_BINARY, ['--stamp'], (error, stdout, _stderr) => { + if (error) { + console.error('Failed to get LS stamp:', error); + resolve(''); + return; + } + const match = /Built at CL: (\d+)/.exec(stdout); + if (match) { + resolve(match[1]); + } + else { + resolve(''); + } + }); + }); +} +// Pattern: "listening on port at for HTTP or HTTPS" +const PORT_PATTERN = /listening on \w+ port at (\d+) for HTTP(S)?\b/i; +// Pattern: OAuth authorization URL +const AUTH_URL_PATTERN = /https:\/\/accounts\.google\.com\/o\/oauth2\/auth\S+/; +// --------------------------------------------------------------------------- +// State +// --------------------------------------------------------------------------- +let _lsProcess = null; +let _lsPort = 0; +let _intentionalTermination = false; +let _restartCount = 0; +let _lastRestartTime = 0; +/** Returns the active language server process, or null if not running. */ +function getLsProcess() { + return _lsProcess; +} +/** Returns the active language server port, or 0 if not running. */ +function getLsPort() { + return _lsPort; +} +/** Clears the language server process reference (call after killing it). */ +function clearLsProcess() { + _lsProcess = null; +} +// --------------------------------------------------------------------------- +// Crash log extraction +// --------------------------------------------------------------------------- +/** + * Extract lines after a crash trigger phrase from a list of stderr lines. + * Returns all lines from the first trigger phrase onwards. + */ +function getLinesAfterCrash(lines) { + const crashLines = []; + let foundTrigger = false; + for (const line of lines) { + if (CRASH_TRIGGER_PHRASES.some((phrase) => line.includes(phrase))) { + foundTrigger = true; + } + if (foundTrigger) { + crashLines.push(line); + } + } + return crashLines; +} +/** + * Best-effort extraction of the crash stack trace from buffered stderr. + * Returns the stack trace string, or undefined if no trigger phrase was found. + */ +function extractCrashStackTrace(stderr) { + const lines = stderr.split('\n'); + const crashLines = getLinesAfterCrash(lines); + return crashLines.length > 0 ? crashLines.join('\n') : undefined; +} +/** + * Sets environment variables for bundled node modules so the language + * server can find them. + * + * NOTE: If you add a new module that needs to be executed this way: + * 1. Add it to `asarUnpack` in `package.json` so it is available on the filesystem. + * 2. Add it to `modules` in the callsite of setupNodeModules. + */ +function setupNodeModules(env, modules) { + for (const mod of modules) { + let entryPoint = ''; + if (!electron_1.app.isPackaged) { + entryPoint = path_1.default.join(__dirname, '..', 'node_modules', mod.name, ...mod.relativePath); + } + else { + entryPoint = path_1.default.join(process.resourcesPath, 'app.asar.unpacked', 'node_modules', mod.name, ...mod.relativePath); + } + env[mod.envVar] = entryPoint; + } +} +/** + * Spawn the language server and resolve with a LanguageServerHandle once + * the LS reports its HTTP port. Rejects on timeout or unexpected exit + * during startup. + * + * After resolving, callers should monitor `handle.exitPromise` to detect + * crashes that occur after startup. + */ +function startLanguageServer(port, csrf, headless) { + return new Promise((resolve, reject) => { + const logStream = fs.createWriteStream((0, paths_1.getLsLogPath)(), { flags: 'w' }); + // We need to pass the override flags because the LS is running in standalone mode + const args = [ + '--standalone', + '--override_ide_name', + 'antigravity', + '--subclient_type', + 'hub', + '--override_ide_version', + electron_1.app.getVersion(), + '--override_user_agent_name', + 'antigravity', + '--https_server_port', + String(port), + '--csrf_token', + csrf, + '--app_data_dir', + (0, paths_1.getAppDataDirName)(), + '--api_server_url', + 'https://generativelanguage.googleapis.com', + '--cloud_code_endpoint', + 'https://daily-cloudcode-pa.googleapis.com', + '--enable_sidecars', + ]; + if (headless) { + args.push('--headless'); + } + console.log(`\nSpawning: ${exports.LS_BINARY} ${args.join(' ')}\n`); + // Electron apps don't inherit shell environment variables when they are not launched through the terminal. + // We need to load the shell env explicitly so the language server can discover tools in the user's environment. + const env = { ...process.env, ...(0, shell_env_1.shellEnvSync)() }; + // We don't read the file to avoid adding start up latency. + // LS will read when browser recording encoder is invoked. + env['AGY_BROWSER_ACTIVE_PORT_FILE'] = (0, paths_1.getActivePortFilePath)(); + (0, utils_1.setupNodeWrapper)(env); + setupNodeModules(env, [ + { + name: 'chrome-devtools-mcp', + envVar: 'CHROME_DEVTOOLS_MCP_JS', + relativePath: ['build', 'src', 'bin', 'chrome-devtools-mcp.js'], + }, + ]); + _lsProcess = (0, child_process_1.spawn)(exports.LS_BINARY, args, { + stdio: ['pipe', 'pipe', 'pipe'], + env, + }); + if (!headless) { + // Close stdin immediately β€” the LS may block waiting for metadata on stdin. + _lsProcess.stdin.end(); + } + const combined = new stream_1.PassThrough(); + _lsProcess.stdout.pipe(combined, { end: false }); + _lsProcess.stderr.pipe(combined, { end: false }); + // Buffer stderr for crash log extraction (ring buffer) + const stderrChunks = []; + let stderrLength = 0; + _lsProcess.stderr.on('data', (data) => { + const str = data.toString(); + stderrChunks.push(str); + stderrLength += str.length; + while (stderrChunks.length > 0 && stderrLength > MAX_STDERR_BUFFER) { + stderrLength -= stderrChunks.shift().length; + } + }); + let resolved = false; + let logStreamEnded = false; + const timer = setTimeout(() => { + if (!resolved) { + resolved = true; + reject(new Error(`Timeout: language server did not report its port within ${LS_STARTUP_TIMEOUT_MS / 1000}s`)); + } + }, LS_STARTUP_TIMEOUT_MS); + const rl = readline.createInterface({ input: combined, crlfDelay: Infinity }); + rl.on('close', () => { + if (!logStreamEnded) { + logStreamEnded = true; + logStream.end(); + } + }); + rl.on('line', (line) => { + if (!logStreamEnded) { + logStream.write(line + '\n'); + } + if (!resolved) { + const m = PORT_PATTERN.exec(line); + if (m) { + resolved = true; + clearTimeout(timer); + const actualPort = parseInt(m[1], 10); + _lsPort = actualPort; + resolve({ + port: actualPort, + process: _lsProcess, + exitPromise, + }); + } + } + const authMatch = AUTH_URL_PATTERN.exec(line); + if (authMatch) { + console.log('\n' + '='.repeat(60)); + console.log(' Please visit the following URL to authorize.'); + console.log(' After authorizing, paste the authorization code below.'); + console.log(` ${authMatch[0]}`); + console.log('='.repeat(60) + '\n'); + } + }); + // Exit promise β€” resolves whenever the process exits (whether during + // startup or after). Includes crash stack trace extraction. + const exitPromise = new Promise((exitResolve) => { + _lsProcess.on('exit', (code, signal) => { + if (!logStreamEnded) { + logStreamEnded = true; + logStream.end(); + } + const fullStderr = stderrChunks.join(''); + const crashStackTrace = extractCrashStackTrace(fullStderr); + // If we haven't resolved the startup promise yet, reject it. + if (!resolved) { + resolved = true; + clearTimeout(timer); + reject(new Error(`Language server exited unexpectedly (code=${code}, signal=${signal})`)); + } + exitResolve({ code, signal, crashStackTrace }); + }); + }); + }); +} +/** Sets whether the termination was intentional (suppresses crash reports). */ +function setIntentionalTermination(value) { + _intentionalTermination = value; +} +/** + * Start the language server AND set up the restart monitoring loop. + * Resolves with the handle on first successful startup. + */ +async function startAndMonitorLanguageServer(port, csrf, options = {}) { + setIntentionalTermination(false); // Reset + const handle = await startLanguageServer(port, csrf, options.headless); + _lsPort = handle.port; + if (options.onPortChanged) { + options.onPortChanged(_lsPort); + } + monitorLsCrashInternal(handle, port, csrf, options); + return handle; +} +function monitorLsCrashInternal(handle, port, csrf, options) { + void handle.exitPromise.then(async (exitInfo) => { + clearLsProcess(); + if (_intentionalTermination) { + return; + } + const { code, signal, crashStackTrace } = exitInfo; + const summary = signal + ? `killed by signal ${signal}` + : `exited with code ${code}`; + console.error(`\nLanguage server crashed: ${summary}`); + if (crashStackTrace) { + console.error('--- Crash Stack Trace ---'); + console.error(crashStackTrace); + console.error('--- End Crash Stack trace ---'); + } + const now = Date.now(); + if (now - _lastRestartTime > RESTART_WINDOW_MS) { + _restartCount = 0; + } + _lastRestartTime = now; + if (_restartCount >= MAX_RESTARTS) { + const msg = `Language server crashed ${MAX_RESTARTS} times in a row. Giving up.`; + console.error(msg); + return; + } + _restartCount++; + console.log(`Attempting restart ${_restartCount}/${MAX_RESTARTS} in ${RESTART_COOLDOWN_MS / 1000}s...`); + await sleep(RESTART_COOLDOWN_MS); + if (_intentionalTermination) { + return; + } + try { + const newHandle = await startLanguageServer(port, csrf); + _lsPort = newHandle.port; + if (options.onPortChanged) { + options.onPortChanged(_lsPort); + } + // Recurse + monitorLsCrashInternal(newHandle, port, csrf, options); + } + catch (err) { + console.error(`Failed to restart language server: ${err.message}`); + } + }); +} +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} +async function killLanguageServer() { + setIntentionalTermination(true); + const proc = getLsProcess(); + if (proc) { + const pid = proc.pid; + console.log('Shutting down language server…'); + const exitPromise = new Promise((resolve) => { + proc.once('exit', () => { + resolve(); + }); + }); + proc.kill('SIGTERM'); + const result = await Promise.race([ + exitPromise.then(() => 'exited'), + new Promise((resolve) => setTimeout(() => resolve('timeout'), 5000)), + ]); + if (result === 'timeout' && pid !== undefined) { + console.warn(`Language server (PID ${pid}) did not exit gracefully within 5s. Sending SIGKILL.`); + try { + process.kill(pid, 'SIGKILL'); + } + catch { + // Process already dead or exited + } + } + clearLsProcess(); + } +} +/** + * Sets up certificate verification in Electron to trust the local self-signed cert + * used by the language server. It verifies that the certificate fingerprint matches + * the hardcoded `LS_CERT_FINGERPRINT`. + * + * TODO: Generate the cert.pem file dynamically + */ +function setupLocalCertTrust() { + electron_1.session.defaultSession.setCertificateVerifyProc((request, callback) => { + if ((request.hostname === '127.0.0.1' || request.hostname === 'localhost') && + request.certificate.fingerprint === constants_1.LS_CERT_FINGERPRINT) { + callback(0); // Accept + } + else { + callback(-3); // Default validation + } + }); +} diff --git a/src/app-extracted/dist/languageServer.test.js b/src/app-extracted/dist/languageServer.test.js new file mode 100644 index 0000000..62d2112 --- /dev/null +++ b/src/app-extracted/dist/languageServer.test.js @@ -0,0 +1,81 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const vitest_1 = require("vitest"); +const child_process_1 = require("child_process"); +vitest_1.vi.mock('electron', () => ({ + app: { + isPackaged: false, + }, +})); +vitest_1.vi.mock('child_process', async () => { + return { + execFile: vitest_1.vi.fn(), + spawn: vitest_1.vi.fn(), + }; +}); +const languageServer_1 = require("./languageServer"); +(0, vitest_1.describe)('extractCrashStackTrace', () => { + (0, vitest_1.it)('should extract lines after "running GoogleExitFunction"', () => { + const stderr = [ + 'INFO: server starting', + 'INFO: listening on port 5387', + 'running GoogleExitFunction', + 'goroutine 1 [running]:', + 'main.main()', + ].join('\n'); + const result = (0, languageServer_1.extractCrashStackTrace)(stderr); + (0, vitest_1.expect)(result).toContain('running GoogleExitFunction'); + (0, vitest_1.expect)(result).toContain('goroutine 1 [running]:'); + (0, vitest_1.expect)(result).toContain('main.main()'); + (0, vitest_1.expect)(result).not.toContain('server starting'); + }); + (0, vitest_1.it)('should extract lines after "http2: panic serving"', () => { + const stderr = [ + 'INFO: normal log', + 'http2: panic serving 127.0.0.1:443', + 'runtime error: invalid memory address', + ].join('\n'); + const result = (0, languageServer_1.extractCrashStackTrace)(stderr); + (0, vitest_1.expect)(result).toContain('http2: panic serving'); + (0, vitest_1.expect)(result).toContain('runtime error'); + (0, vitest_1.expect)(result).not.toContain('normal log'); + }); + (0, vitest_1.it)('should return undefined when no crash trigger is found', () => { + const stderr = [ + 'INFO: server starting', + 'INFO: listening on port 5387', + 'INFO: server shutting down', + ].join('\n'); + const result = (0, languageServer_1.extractCrashStackTrace)(stderr); + (0, vitest_1.expect)(result).toBeUndefined(); + }); + (0, vitest_1.it)('should return undefined for empty stderr', () => { + (0, vitest_1.expect)((0, languageServer_1.extractCrashStackTrace)('')).toBeUndefined(); + }); +}); +(0, vitest_1.describe)('getLsCL', () => { + (0, vitest_1.it)('should return CL number when stamp output contains it', async () => { + const mockExecFile = child_process_1.execFile; + mockExecFile.mockImplementation((file, args, callback) => { + callback(null, 'Built at CL: 12345\n', ''); + }); + const cl = await (0, languageServer_1.getLsCL)(); + (0, vitest_1.expect)(cl).toBe('12345'); + }); + (0, vitest_1.it)('should return empty string when stamp output does not contain CL', async () => { + const mockExecFile = child_process_1.execFile; + mockExecFile.mockImplementation((file, args, callback) => { + callback(null, 'Built on: today\n', ''); + }); + const cl = await (0, languageServer_1.getLsCL)(); + (0, vitest_1.expect)(cl).toBe(''); + }); + (0, vitest_1.it)('should return empty string when execFile fails', async () => { + const mockExecFile = child_process_1.execFile; + mockExecFile.mockImplementation((file, args, callback) => { + callback(new Error('fail'), '', ''); + }); + const cl = await (0, languageServer_1.getLsCL)(); + (0, vitest_1.expect)(cl).toBe(''); + }); +}); diff --git a/src/app-extracted/dist/loadingOverlay.js b/src/app-extracted/dist/loadingOverlay.js new file mode 100644 index 0000000..7bd869e --- /dev/null +++ b/src/app-extracted/dist/loadingOverlay.js @@ -0,0 +1,100 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.attachLoadingOverlay = attachLoadingOverlay; +const electron_1 = require("electron"); +/** + * Generates the HTML content for the initial loading screen overlay. + * This is injected into a WebContentsView and shown to the user before + * the main application bundle finishes loading. + * + * @param foregroundColor - The text and loader animation color (hex or CSS color string). + * @param backgroundColor - The background color of the loading view. + */ +function getLoadingHtml(foregroundColor, backgroundColor) { + return ` + + + + + + +
+
+
+
Loading Antigravity
+ + + `; +} +/** + * Attaches a temporary WebContentsView overlay that shows a loading animation. + * It is automatically removed when the window's main content finishes loading. + */ +function attachLoadingOverlay(win, foregroundColor, backgroundColor) { + const view = new electron_1.WebContentsView({ + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + }, + }); + const html = getLoadingHtml(foregroundColor, backgroundColor); + void view.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`); + win.contentView.addChildView(view); + const updateBounds = () => { + const [width, height] = win.getContentSize(); + view.setBounds({ x: 0, y: 0, width, height }); + }; + updateBounds(); + win.on('resize', updateBounds); + win.webContents.once('did-finish-load', () => { + try { + win.contentView.removeChildView(view); + } + catch (_) { + // In case window was closed quickly + } + win.off('resize', updateBounds); + }); +} diff --git a/src/app-extracted/dist/main.js b/src/app-extracted/dist/main.js new file mode 100644 index 0000000..a209636 --- /dev/null +++ b/src/app-extracted/dist/main.js @@ -0,0 +1,365 @@ +"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 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; +// --------------------------------------------------------------------------- +// 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('antigravity://')); + if (url) { + handleDeepLink(url); + } +}); +(0, customScheme_1.registerCustomSchemes)(); +// Register as default protocol client for deep linking +const PROTOCOL = 'antigravity'; +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); + // Handle deep link URL from command line arguments (All platforms) + const deepLinkFromArg = process.argv.find((arg) => arg.startsWith('antigravity://')); + if (deepLinkFromArg) { + console.log('Launched with deep link:', deepLinkFromArg); + pendingDeepLink = deepLinkFromArg; + } + // Register IPC handlers + (0, ipcHandlers_1.registerIpcHandlers)(storageManager); + 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: 'Antigravity', + 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…`); + 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}`); + // Apply cert trust + (0, languageServer_1.setupLocalCertTrust)(); + if (!HEADLESS) { + const windows = electron_1.BrowserWindow.getAllWindows(); + for (const win of windows) { + 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('='.repeat(60) + '\n'); + if (HEADLESS) { + // In headless mode, forward stdin to the Language Server to allow interaction via terminal. + 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); + (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: '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; +}); +/** + * 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; + } + 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(); + } + // 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); + } +}); diff --git a/src/app-extracted/dist/main.test.js b/src/app-extracted/dist/main.test.js new file mode 100644 index 0000000..a484fdf --- /dev/null +++ b/src/app-extracted/dist/main.test.js @@ -0,0 +1,200 @@ +"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; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const vitest_1 = require("vitest"); +const helpers_1 = require("./test/helpers"); +const constants_1 = require("./constants"); +// Use the shared auto-mocks from __mocks__/ +vitest_1.vi.mock('electron'); +vitest_1.vi.mock('fs', () => ({ + existsSync: vitest_1.vi.fn(), + readFileSync: vitest_1.vi.fn(), +})); +vitest_1.vi.mock('crypto', () => ({ + randomUUID: vitest_1.vi.fn().mockReturnValue('mock-uuid'), +})); +vitest_1.vi.mock('./utils', () => ({ + sleep: vitest_1.vi.fn().mockResolvedValue(undefined), + createWindow: vitest_1.vi.fn(), + showOrCreateWindow: vitest_1.vi.fn(), + ensureAppIsInDock: vitest_1.vi.fn(), + isMacOS: vitest_1.vi.fn().mockReturnValue(true), + showQuitConfirmation: false, + setShowQuitConfirmation: vitest_1.vi.fn(), + SleepBlocker: { + getInstance: vitest_1.vi.fn().mockReturnValue({ + shouldKeepComputerAwake: vitest_1.vi.fn(), + }), + }, +})); +vitest_1.vi.mock('./languageServer', () => ({ + LS_BINARY: '/mock/ls', + getLsProcess: vitest_1.vi.fn(), + clearLsProcess: vitest_1.vi.fn(), + startLanguageServer: vitest_1.vi.fn(), + killLanguageServer: vitest_1.vi.fn(), + startAndMonitorLanguageServer: vitest_1.vi.fn(), + setupLocalCertTrust: vitest_1.vi.fn(), + getLsCL: vitest_1.vi.fn().mockResolvedValue('12345'), +})); +vitest_1.vi.mock('./updater', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + initAutoUpdater: vitest_1.vi.fn(), + }; +}); +vitest_1.vi.mock('./ipcHandlers', () => ({ + registerIpcHandlers: vitest_1.vi.fn(), +})); +vitest_1.vi.mock('./tray', () => ({ + createTray: vitest_1.vi.fn(), +})); +vitest_1.vi.mock('./ideInstall', () => ({ + maybeShowIdeInstallWizard: vitest_1.vi.fn().mockResolvedValue('skipped'), +})); +(0, vitest_1.describe)('main', () => { + (0, vitest_1.beforeEach)(async () => { + vitest_1.vi.clearAllMocks(); + vitest_1.vi.resetModules(); + (0, helpers_1.silenceConsole)(); + const { app } = await Promise.resolve().then(() => __importStar(require('electron'))); + app.setAboutPanelOptions = vitest_1.vi.fn(); + }); + (0, vitest_1.it)('should initialize the app correctly on successful startup', async () => { + const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs'))); + const { createWindow } = await Promise.resolve().then(() => __importStar(require('./utils'))); + const { startAndMonitorLanguageServer } = await Promise.resolve().then(() => __importStar(require('./languageServer'))); + const { initAutoUpdater } = await Promise.resolve().then(() => __importStar(require('./updater'))); + const { createTray } = await Promise.resolve().then(() => __importStar(require('./tray'))); + const { app } = await Promise.resolve().then(() => __importStar(require('electron'))); + const { registerIpcHandlers } = await Promise.resolve().then(() => __importStar(require('./ipcHandlers'))); + const ACTUAL_PORT = 49152; + vitest_1.vi.mocked(existsSync).mockReturnValue(true); + vitest_1.vi.mocked(startAndMonitorLanguageServer).mockResolvedValue({ + port: ACTUAL_PORT, + process: { pid: 1234 }, + exitPromise: new Promise(() => { }), + }); + // Import main to trigger top-level registration + await Promise.resolve().then(() => __importStar(require('./main'))); + // Trigger the whenReady callback + const whenReadyCall = vitest_1.vi.mocked(app.whenReady).mock.results[0].value; + await whenReadyCall.cb(); + (0, vitest_1.expect)(startAndMonitorLanguageServer).toHaveBeenCalledWith(constants_1.DYNAMIC_PORT, 'mock-uuid', vitest_1.expect.objectContaining({ headless: false })); + (0, vitest_1.expect)(registerIpcHandlers).toHaveBeenCalled(); + (0, vitest_1.expect)(createWindow).toHaveBeenCalled(); + (0, vitest_1.expect)(createTray).toHaveBeenCalled(); + (0, vitest_1.expect)(initAutoUpdater).toHaveBeenCalled(); + (0, vitest_1.expect)(app.setAboutPanelOptions).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ + applicationVersion: '1.0.0', + version: '12345', + })); + }); + (0, vitest_1.it)('should quit if language server binary is missing', async () => { + const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs'))); + const { app, dialog } = await Promise.resolve().then(() => __importStar(require('electron'))); + vitest_1.vi.mocked(existsSync).mockReturnValue(false); + await Promise.resolve().then(() => __importStar(require('./main'))); + const whenReadyCall = vitest_1.vi.mocked(app.whenReady).mock.results[0].value; + await whenReadyCall.cb(); + (0, vitest_1.expect)(dialog.showErrorBox).toHaveBeenCalledWith('Binary not found', vitest_1.expect.stringContaining('language_server binary not found')); + (0, vitest_1.expect)(app.quit).toHaveBeenCalled(); + }); + (0, vitest_1.it)('should use dynamic port assignment (port 0) without port-conflict checks', async () => { + const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs'))); + const { createWindow } = await Promise.resolve().then(() => __importStar(require('./utils'))); + const { startAndMonitorLanguageServer } = await Promise.resolve().then(() => __importStar(require('./languageServer'))); + const { app } = await Promise.resolve().then(() => __importStar(require('electron'))); + // Simulate the OS assigning a random high port + const OS_ASSIGNED_PORT = 63421; + vitest_1.vi.mocked(existsSync).mockReturnValue(true); + vitest_1.vi.mocked(startAndMonitorLanguageServer).mockResolvedValue({ + port: OS_ASSIGNED_PORT, + process: { pid: 1234 }, + exitPromise: new Promise(() => { }), + }); + await Promise.resolve().then(() => __importStar(require('./main'))); + const whenReadyCall = vitest_1.vi.mocked(app.whenReady).mock.results[0].value; + await whenReadyCall.cb(); + // Verify port 0 (DYNAMIC_PORT) is passed β€” the OS picks the real port + (0, vitest_1.expect)(startAndMonitorLanguageServer).toHaveBeenCalledWith(constants_1.DYNAMIC_PORT, 'mock-uuid', vitest_1.expect.objectContaining({ headless: false })); + // Window should load the OS-assigned port, not a hardcoded one + (0, vitest_1.expect)(createWindow).toHaveBeenCalledWith(`https://127.0.0.1:${OS_ASSIGNED_PORT}/`); + }); + (0, vitest_1.it)('should quit on language server startup failure', async () => { + const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs'))); + const { startAndMonitorLanguageServer } = await Promise.resolve().then(() => __importStar(require('./languageServer'))); + const { app, dialog } = await Promise.resolve().then(() => __importStar(require('electron'))); + vitest_1.vi.mocked(existsSync).mockReturnValue(true); + vitest_1.vi.mocked(startAndMonitorLanguageServer).mockRejectedValue(new Error('Timeout: language server did not report its port within 60s')); + await Promise.resolve().then(() => __importStar(require('./main'))); + const whenReadyCall = vitest_1.vi.mocked(app.whenReady).mock.results[0].value; + await whenReadyCall.cb(); + (0, vitest_1.expect)(dialog.showErrorBox).toHaveBeenCalledWith('Startup failed', vitest_1.expect.stringContaining('Timeout')); + (0, vitest_1.expect)(app.quit).toHaveBeenCalled(); + }); + (0, vitest_1.it)('should reload windows when onPortChanged is called', async () => { + const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs'))); + const { startAndMonitorLanguageServer } = await Promise.resolve().then(() => __importStar(require('./languageServer'))); + const { app, BrowserWindow } = await Promise.resolve().then(() => __importStar(require('electron'))); + vitest_1.vi.mocked(existsSync).mockReturnValue(true); + let onPortChangedCallback; + vitest_1.vi.mocked(startAndMonitorLanguageServer).mockImplementation(async (port, csrf, options) => { + onPortChangedCallback = options?.onPortChanged; + return { + port: 49152, + process: { + pid: 1234, + }, + exitPromise: new Promise(() => { }), + }; + }); + await Promise.resolve().then(() => __importStar(require('./main'))); + const whenReadyCall = vitest_1.vi.mocked(app.whenReady).mock.results[0].value; + await whenReadyCall.cb(); + (0, vitest_1.expect)(onPortChangedCallback).toBeDefined(); + // Re-trigger port changed (simulating auto-restart event) + const NEW_PORT = 50000; + if (onPortChangedCallback) { + onPortChangedCallback(NEW_PORT); + } + // Verify BrowserWindow instances are reloaded with new URL + (0, vitest_1.expect)(BrowserWindow.getAllWindows).toHaveBeenCalled(); + const windows = vitest_1.vi.mocked(BrowserWindow.getAllWindows).mock.results[0] + .value; + (0, vitest_1.expect)(windows[0].loadURL).toHaveBeenCalledWith(`https://127.0.0.1:${NEW_PORT}/`); + }); +}); diff --git a/src/app-extracted/dist/menu.js b/src/app-extracted/dist/menu.js new file mode 100644 index 0000000..4ae073c --- /dev/null +++ b/src/app-extracted/dist/menu.js @@ -0,0 +1,59 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.setupApplicationMenu = setupApplicationMenu; +const electron_1 = require("electron"); +const utils_1 = require("./utils"); +const updater_1 = require("./updater"); +/** + * Applies modifications to the default application menu. + */ +function setupApplicationMenu(url) { + const menu = electron_1.Menu.getApplicationMenu(); + if (!menu) { + return; + } + // Adds a "New Window" item to the top of the existing File menu. + addItemToSubmenu(menu, 'File', 0, new electron_1.MenuItem({ + label: 'New Window', + accelerator: 'CmdOrCtrl+Shift+N', + click: () => { + (0, utils_1.createWindow)(url); + }, + })); + // Add "Check for Updates" to the application menu on macOS. + if ((0, utils_1.isMacOS)()) { + const appSubmenu = menu.items[0]?.submenu; + if (appSubmenu) { + appSubmenu.insert(1, new electron_1.MenuItem({ + id: 'check-for-updates', + label: updater_1.MenuUpdateStep.CheckForUpdates, + click: (menuItem) => { + const action = updater_1.updateActions[menuItem.label]; + action?.(); + }, + })); + } + } + // Adds Docs and Toggle Developer Tools to the Help menu + addItemToSubmenu(menu, 'Help', 0, new electron_1.MenuItem({ + label: 'Docs', + click: async () => { + await electron_1.shell.openExternal('https://antigravity.google/docs'); + }, + })); + addItemToSubmenu(menu, 'Help', 1, new electron_1.MenuItem({ + role: 'toggleDevTools', + })); + // Re-apply the menu so the change takes effect. + electron_1.Menu.setApplicationMenu(menu); +} +/** + * Adds a menu item to a submenu of the main application menu. + */ +function addItemToSubmenu(appMenu, submenuLabel, position, item) { + const submenuItem = appMenu.items.find((item) => item.label === submenuLabel); + if (!submenuItem?.submenu) { + return; + } + submenuItem.submenu.insert(position, item); +} diff --git a/src/app-extracted/dist/paths.js b/src/app-extracted/dist/paths.js new file mode 100644 index 0000000..7e6464a --- /dev/null +++ b/src/app-extracted/dist/paths.js @@ -0,0 +1,49 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.IDE_BACKUP_DATA_DIR = exports.IDE_NEW_DATA_DIR = exports.IDE_OLD_DATA_DIR = void 0; +exports.getAppDataDirName = getAppDataDirName; +exports.getAppDataDir = getAppDataDir; +exports.getSettingsPbPath = getSettingsPbPath; +exports.getAppStoragePath = getAppStoragePath; +exports.getActivePortFilePath = getActivePortFilePath; +exports.getLsLogPath = getLsLogPath; +const electron_1 = require("electron"); +const path_1 = __importDefault(require("path")); +const os_1 = __importDefault(require("os")); +const constants_1 = require("./constants"); +function getAppDataDirName() { + return `antigravity${electron_1.app.isPackaged ? '' : '-dev'}`; +} +function getAppDataDir() { + return path_1.default.join(os_1.default.homedir(), '.gemini', getAppDataDirName()); +} +function getSettingsPbPath() { + return path_1.default.join(os_1.default.homedir(), '.gemini', 'config', 'config.json'); +} +/** + * Returns the path to the persistent app storage file. + * This is used to back a lightweight key-value store for UI state, + * and is not used for e.g. settings or other "core" app state. + */ +function getAppStoragePath() { + return path_1.default.join(electron_1.app.getPath('userData'), 'app_storage.json'); +} +/** + * Returns the path to the file used to communicate AGY Hub's remote debugging port. + * Used by recording encoder. + */ +function getActivePortFilePath() { + return path_1.default.join(electron_1.app.getPath('userData'), 'DevToolsActivePort'); +} +function getLsLogPath() { + return path_1.default.join(electron_1.app.getPath('logs'), constants_1.LS_LOG_FILE_NAME); +} +/** User data dir for the old IDE (source for copy). */ +exports.IDE_OLD_DATA_DIR = path_1.default.join(os_1.default.homedir(), '.gemini', 'antigravity'); +/** User data dir for the separately installed IDE (destination for copy). */ +exports.IDE_NEW_DATA_DIR = path_1.default.join(os_1.default.homedir(), '.gemini', 'antigravity-ide'); +/** User data dir for backup (destination for backup copy). */ +exports.IDE_BACKUP_DATA_DIR = path_1.default.join(os_1.default.homedir(), '.gemini', 'antigravity-backup'); diff --git a/src/app-extracted/dist/preload.js b/src/app-extracted/dist/preload.js new file mode 100644 index 0000000..50f57a6 --- /dev/null +++ b/src/app-extracted/dist/preload.js @@ -0,0 +1,104 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * Preload script β€” runs in every BrowserWindow before the page loads. + * Exposes a minimal, secure API via contextBridge so the renderer can + * communicate with the main-process auto-updater without nodeIntegration. + */ +const electron_1 = require("electron"); +const updaterAPI = { + onStateChanged: (callback) => { + const handler = (_event, state) => { + callback(state); + }; + electron_1.ipcRenderer.on('updater:state-changed', handler); + // Return unsubscribe function + return () => { + electron_1.ipcRenderer.removeListener('updater:state-changed', handler); + }; + }, + applyUpdate: () => electron_1.ipcRenderer.invoke('updater:apply'), + quitAndInstall: () => electron_1.ipcRenderer.invoke('updater:quit-and-install'), + checkForUpdates: () => electron_1.ipcRenderer.invoke('updater:check-for-updates'), +}; +const dialogAPI = { + showOpenDialog: () => electron_1.ipcRenderer.invoke('dialog:open-workspace'), +}; +const notificationAPI = { + send: (options) => electron_1.ipcRenderer.invoke('notification:send', options), + openSystemPreferences: () => electron_1.ipcRenderer.invoke('notification:open-system-preferences'), + onClicked: (callback) => { + const handler = (_event, payload) => { + callback(payload); + }; + electron_1.ipcRenderer.on('notification:clicked', handler); + return () => { + electron_1.ipcRenderer.removeListener('notification:clicked', handler); + }; + }, +}; +const storageAPI = { + getItems: () => electron_1.ipcRenderer.invoke('storage:get-items'), + updateItems: (changes) => electron_1.ipcRenderer.invoke('storage:update-items', changes), + onChanged: (callback) => { + const handler = (_event, changes) => { + callback(changes); + }; + electron_1.ipcRenderer.on('storage:changed', handler); + return () => { + electron_1.ipcRenderer.removeListener('storage:changed', handler); + }; + }, +}; +const logsAPI = { + getElectronLogs: () => electron_1.ipcRenderer.invoke('logs:electron'), +}; +const extensionsAPI = { + sendAuthorities: (authoritiesMap) => electron_1.ipcRenderer.invoke('extensions:send-authorities', authoritiesMap), +}; +const deepLinkAPI = { + onDeepLink: (callback) => { + const handler = (_event, url) => { + callback(url); + }; + electron_1.ipcRenderer.on('deep-link', handler); + return () => { + electron_1.ipcRenderer.removeListener('deep-link', handler); + }; + }, + getStoredDeepLink: () => electron_1.ipcRenderer.invoke('deep-link:get-stored'), +}; +const agentAPI = { + updateActiveAgentCount: (count) => electron_1.ipcRenderer.invoke('agent:update-active-count', count), +}; +const electronNativeAPI = { + getZoomLevel: () => electron_1.webFrame.getZoomFactor(), + setTitleBarOverlay: (options) => electron_1.ipcRenderer.invoke('window:set-title-bar-overlay', options), + minimize: () => electron_1.ipcRenderer.invoke('window:minimize'), + maximize: () => electron_1.ipcRenderer.invoke('window:maximize'), + unmaximize: () => electron_1.ipcRenderer.invoke('window:unmaximize'), + isMaximized: () => electron_1.ipcRenderer.invoke('window:is-maximized'), + close: () => electron_1.ipcRenderer.invoke('window:close'), + toggleDevTools: () => electron_1.ipcRenderer.invoke('window:toggle-devtools'), + zoomIn: () => { + const current = electron_1.webFrame.getZoomLevel(); + electron_1.webFrame.setZoomLevel(current + 0.5); + }, + zoomOut: () => { + const current = electron_1.webFrame.getZoomLevel(); + electron_1.webFrame.setZoomLevel(current - 0.5); + }, + resetZoom: () => { + electron_1.webFrame.setZoomLevel(0); + }, + openExternal: (url) => electron_1.ipcRenderer.invoke('shell:open-external', url), +}; +electron_1.contextBridge.exposeInMainWorld('electronUpdater', updaterAPI); +electron_1.contextBridge.exposeInMainWorld('dialog', dialogAPI); +electron_1.contextBridge.exposeInMainWorld('nativeNotifications', notificationAPI); +electron_1.contextBridge.exposeInMainWorld('nativeStorage', storageAPI); +electron_1.contextBridge.exposeInMainWorld('logs', logsAPI); +electron_1.contextBridge.exposeInMainWorld('extensions', extensionsAPI); +electron_1.contextBridge.exposeInMainWorld('deepLink', deepLinkAPI); +electron_1.contextBridge.exposeInMainWorld('agent', agentAPI); +electron_1.contextBridge.exposeInMainWorld('electronNative', electronNativeAPI); diff --git a/src/app-extracted/dist/services/aiProviderService.js b/src/app-extracted/dist/services/aiProviderService.js new file mode 100644 index 0000000..240f05f --- /dev/null +++ b/src/app-extracted/dist/services/aiProviderService.js @@ -0,0 +1,519 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AIProviderService = exports.AIProviderType = exports.AIProviderCapability = void 0; +var AIProviderCapability; +(function (AIProviderCapability) { + AIProviderCapability["CHAT"] = "chat"; + AIProviderCapability["COMPLETION"] = "completion"; + AIProviderCapability["EMBEDDING"] = "embedding"; + AIProviderCapability["VISION"] = "vision"; + AIProviderCapability["TOOL_USE"] = "tool_use"; + AIProviderCapability["STREAMING"] = "streaming"; +})(AIProviderCapability = exports.AIProviderCapability || (exports.AIProviderCapability = {})); +var AIProviderType; +(function (AIProviderType) { + AIProviderType["OPENAI"] = "openai"; + AIProviderType["ANTHROPIC"] = "anthropic"; + AIProviderType["OLLAMA"] = "ollama"; + AIProviderType["GROQ"] = "groq"; + AIProviderType["OPENROUTER"] = "openrouter"; + AIProviderType["CUSTOM"] = "custom"; + AIProviderType["GOOGLE_GEMINI"] = "google_gemini"; + AIProviderType["COMMAND_CODE"] = "command_code"; + AIProviderType["NVIDIA_NIM"] = "nvidia_nim"; + AIProviderType["CROF_AI"] = "crof_ai"; + AIProviderType["KILO_AI"] = "kilo_ai"; + AIProviderType["OPEN_ADAPTER"] = "open_adapter"; + AIProviderType["Z_AI"] = "z_ai"; + AIProviderType["GOOGLE_ANTIGRAVITY"] = "google_antigravity"; +})(AIProviderType = exports.AIProviderType || (exports.AIProviderType = {})); + +const PROVIDER_PRESETS = { + "Custom": { + type: AIProviderType.CUSTOM, + endpoint: "", + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.STREAMING], + }, + "OpenAI": { + type: AIProviderType.OPENAI, + endpoint: "https://api.openai.com/v1", + models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-3.5-turbo"], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.EMBEDDING, AIProviderCapability.VISION, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING], + }, + "Anthropic": { + type: AIProviderType.ANTHROPIC, + endpoint: "https://api.anthropic.com/v1", + models: ["claude-sonnet-4-20250514", "claude-3-5-sonnet-latest", "claude-3-opus-latest", "claude-3-haiku-latest"], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.VISION, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING], + }, + "OpenCode Zen (OpenAI-compatible)": { + type: AIProviderType.CUSTOM, + endpoint: "https://opencode.ai/zen/v1", + models: [ + "glm-5.1", "glm-5", "kimi-k2.5", "kimi-k2.6", + "minimax-m2.7", "minimax-m2.5", "minimax-m2.5-free", + "deepseek-v4-flash-free", "nemotron-3-super-free", + "qwen3.6-plus", "qwen3.5-plus", "big-pickle", + ], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING], + }, + "OpenCode Zen (Anthropic)": { + type: AIProviderType.ANTHROPIC, + endpoint: "https://opencode.ai/zen/v1", + models: [ + "claude-opus-4-7", "claude-opus-4-6", "claude-opus-4-5", + "claude-opus-4-1", "claude-sonnet-4-6", "claude-sonnet-4-5", + "claude-sonnet-4", "claude-haiku-4-5", "claude-3-5-haiku", + ], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.VISION, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING], + }, + "OpenCode Go (OpenAI-compatible)": { + type: AIProviderType.CUSTOM, + endpoint: "https://opencode.ai/zen/go/v1", + models: [ + "glm-5.1", "glm-5", "kimi-k2.5", "kimi-k2.6", + "mimo-v2.5", "mimo-v2.5-pro", "minimax-m2.7", "minimax-m2.5", + "qwen3.6-plus", "qwen3.5-plus", "deepseek-v4-pro", "deepseek-v4-flash", + ], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING], + }, + "OpenCode Go (Anthropic)": { + type: AIProviderType.ANTHROPIC, + endpoint: "https://opencode.ai/zen/go/v1", + models: ["minimax-m2.7", "minimax-m2.5"], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.VISION, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING], + }, + "Crof.ai": { + type: AIProviderType.CUSTOM, + endpoint: "https://crof.ai/v1", + models: [], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING], + }, + "NVIDIA NIM": { + type: AIProviderType.NVIDIA_NIM, + endpoint: "https://integrate.api.nvidia.com/v1", + models: [], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING], + }, + "Kilo.ai Gateway": { + type: AIProviderType.KILO_AI, + endpoint: "https://api.kilo.ai/api/gateway", + models: [], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING], + }, + "Command Code": { + type: AIProviderType.COMMAND_CODE, + endpoint: "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", + ], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING], + }, + "OpenRouter": { + type: AIProviderType.OPENROUTER, + endpoint: "https://openrouter.ai/api/v1", + models: [], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING], + }, + "Google Gemini (API Key)": { + type: AIProviderType.GOOGLE_GEMINI, + endpoint: "https://generativelanguage.googleapis.com/v1beta/openai", + models: [ + "gemini-2.5-flash", "gemini-2.5-pro", + "gemini-2.0-flash", "gemini-2.0-flash-lite", + "gemini-2.5-flash-preview-native-audio-dialog", + ], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.VISION, AIProviderCapability.STREAMING], + }, + "Google Gemini (OAuth)": { + type: AIProviderType.GOOGLE_GEMINI, + endpoint: "https://cloudcode-pa.googleapis.com", + models: ["gemini-2.5-flash", "gemini-2.5-pro"], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.VISION, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING], + }, + "Google Antigravity (OAuth)": { + type: AIProviderType.GOOGLE_ANTIGRAVITY, + endpoint: "https://daily-cloudcode-pa.sandbox.googleapis.com", + models: [ + "antigravity-gemini-3-flash", + "antigravity-gemini-3-pro", + "antigravity-gemini-3.1-pro", + "antigravity-claude-sonnet-4-6", + "antigravity-claude-opus-4-6-thinking", + "gemini-2.5-flash", "gemini-2.5-pro", + "gemini-3-flash-preview", "gemini-3-pro-preview", "gemini-3.1-pro-preview", + ], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.VISION, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING], + }, + "OpenAdapter": { + type: AIProviderType.OPEN_ADAPTER, + endpoint: "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", + ], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING], + }, + "Z.ai Coding": { + type: AIProviderType.Z_AI, + endpoint: "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", + ], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING], + }, + "Ollama (Local)": { + type: AIProviderType.OLLAMA, + endpoint: "http://localhost:11434/v1", + apiKey: "ollama", + models: ["llama3", "codellama", "mistral", "neural-chat"], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING], + }, + "Groq": { + type: AIProviderType.GROQ, + endpoint: "https://api.groq.com/openai/v1", + models: ["llama-3.1-8b-instant", "llama-3.1-70b-versatile", "mixtral-8x7b-32768"], + capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING], + }, +}; + +const DEFAULT_PROVIDERS = [ + { + id: 'openai-default', + name: 'OpenAI', + type: AIProviderType.OPENAI, + endpoint: 'https://api.openai.com/v1', + apiKey: '', + models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'], + defaultModel: 'gpt-4o', + capabilities: [ + AIProviderCapability.CHAT, + AIProviderCapability.COMPLETION, + AIProviderCapability.EMBEDDING, + AIProviderCapability.VISION, + AIProviderCapability.TOOL_USE, + AIProviderCapability.STREAMING, + ], + isEnabled: true, + isDefault: true, + }, + { + id: 'anthropic-default', + name: 'Anthropic', + type: AIProviderType.ANTHROPIC, + endpoint: 'https://api.anthropic.com/v1', + apiKey: '', + models: ['claude-sonnet-4-20250514', 'claude-3-5-sonnet-latest', 'claude-3-opus-latest', 'claude-3-haiku-latest'], + defaultModel: 'claude-sonnet-4-20250514', + capabilities: [ + AIProviderCapability.CHAT, + AIProviderCapability.VISION, + AIProviderCapability.TOOL_USE, + AIProviderCapability.STREAMING, + ], + isEnabled: true, + isDefault: false, + }, + { + id: 'ollama-default', + name: 'Ollama (Local)', + type: AIProviderType.OLLAMA, + endpoint: 'http://localhost:11434/v1', + apiKey: 'ollama', + models: ['llama3', 'codellama', 'mistral', 'neural-chat'], + defaultModel: 'llama3', + capabilities: [ + AIProviderCapability.CHAT, + AIProviderCapability.COMPLETION, + AIProviderCapability.STREAMING, + ], + isEnabled: true, + isDefault: false, + }, +]; + +class AIProviderService { + constructor(storageManager) { + this.storageManager = storageManager; + this.providers = new Map(); + this.eventEmitter = new (require('events').EventEmitter)(); + } + + async initialize() { + const items = await this.storageManager.getItems(); + const storedProviders = items['aiProviders']; + + if (storedProviders) { + try { + const providersArray = JSON.parse(storedProviders); + providersArray.forEach((provider) => { + this.providers.set(provider.id, provider); + }); + } catch (e) { + console.error('Error loading AI providers:', e); + this.loadDefaultProviders(); + } + } else { + this.loadDefaultProviders(); + } + } + + loadDefaultProviders() { + DEFAULT_PROVIDERS.forEach((provider) => { + this.providers.set(provider.id, { ...provider }); + }); + this.persistProviders(); + } + + async persistProviders() { + const providersArray = Array.from(this.providers.values()); + await this.storageManager.updateItems({ + 'aiProviders': JSON.stringify(providersArray), + }); + } + + getAllProviders() { + return Array.from(this.providers.values()); + } + + getProvider(id) { + return this.providers.get(id); + } + + getEnabledProviders() { + return this.getAllProviders().filter(p => p.isEnabled); + } + + getDefaultProvider() { + return this.getAllProviders().find(p => p.isDefault) || this.getAllProviders()[0]; + } + + getAvailablePresets() { + return Object.keys(PROVIDER_PRESETS); + } + + getPreset(presetName) { + return PROVIDER_PRESETS[presetName]; + } + + async addProvider(provider) { + const newProvider = { + id: `custom-${Date.now()}`, + name: provider.name || 'Custom Provider', + type: provider.type || AIProviderType.CUSTOM, + endpoint: provider.endpoint || '', + apiKey: provider.apiKey || '', + models: provider.models || [], + defaultModel: provider.defaultModel || (provider.models && provider.models[0]) || '', + capabilities: provider.capabilities || [AIProviderCapability.CHAT], + isEnabled: true, + isDefault: false, + ...provider, + }; + + this.providers.set(newProvider.id, newProvider); + await this.persistProviders(); + this.emit('provider-added', newProvider); + return newProvider; + } + + async addProviderFromPreset(presetName, apiKey = '') { + const preset = PROVIDER_PRESETS[presetName]; + if (!preset) { + throw new Error(`Preset "${presetName}" not found`); + } + + const normalizedName = presetName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); + const newProvider = { + id: `${normalizedName}-${Date.now()}`, + name: presetName, + type: preset.type || AIProviderType.CUSTOM, + endpoint: preset.endpoint || '', + apiKey: apiKey || preset.apiKey || '', + models: preset.models || [], + defaultModel: preset.models && preset.models[0] || '', + capabilities: preset.capabilities || [AIProviderCapability.CHAT, AIProviderCapability.STREAMING], + isEnabled: true, + isDefault: false, + presetName: presetName, + }; + + this.providers.set(newProvider.id, newProvider); + await this.persistProviders(); + this.emit('provider-added', newProvider); + return newProvider; + } + + async updateProvider(id, updates) { + const provider = this.providers.get(id); + if (!provider) { + throw new Error(`Provider with id ${id} not found`); + } + + const updatedProvider = { + ...provider, + ...updates, + id: provider.id, + }; + + this.providers.set(id, updatedProvider); + await this.persistProviders(); + this.emit('provider-updated', updatedProvider); + return updatedProvider; + } + + async deleteProvider(id) { + const provider = this.providers.get(id); + if (!provider) { + throw new Error(`Provider with id ${id} not found`); + } + + this.providers.delete(id); + await this.persistProviders(); + this.emit('provider-deleted', id); + } + + async setDefaultProvider(id) { + const provider = this.providers.get(id); + if (!provider) { + throw new Error(`Provider with id ${id} not found`); + } + + this.getAllProviders().forEach((p) => { + p.isDefault = p.id === id; + }); + + await this.persistProviders(); + this.emit('default-provider-changed', id); + } + + async toggleProvider(id, enabled) { + const provider = this.providers.get(id); + if (!provider) { + throw new Error(`Provider with id ${id} not found`); + } + + provider.isEnabled = enabled; + await this.persistProviders(); + this.emit('provider-toggled', { id, enabled }); + } + + async testConnection(id) { + const provider = this.providers.get(id); + if (!provider) { + throw new Error(`Provider with id ${id} not found`); + } + + if (!provider.endpoint) { + return { + success: false, + status: -1, + message: 'No endpoint configured', + }; + } + + try { + const headers = { + 'Content-Type': 'application/json', + }; + + if (provider.apiKey && provider.apiKey !== 'ollama') { + headers['Authorization'] = `Bearer ${provider.apiKey}`; + } + + const response = await fetch(`${provider.endpoint}/models`, { + method: 'GET', + headers: headers, + }); + + return { + success: response.ok, + status: response.status, + message: response.ok ? 'Connection successful' : `Error: ${response.statusText}`, + }; + } catch (error) { + return { + success: false, + status: -1, + message: `Connection failed: ${error.message}`, + }; + } + } + + async fetchModels(id) { + const provider = this.providers.get(id); + if (!provider) { + throw new Error(`Provider with id ${id} not found`); + } + + if (!provider.endpoint) { + throw new Error('No endpoint configured'); + } + + try { + const headers = { + 'Content-Type': 'application/json', + }; + + if (provider.apiKey && provider.apiKey !== 'ollama') { + headers['Authorization'] = `Bearer ${provider.apiKey}`; + } + + const response = await fetch(`${provider.endpoint}/models`, { + method: 'GET', + headers: headers, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.statusText}`); + } + + const data = await response.json(); + + let models = []; + if (data.data && Array.isArray(data.data)) { + models = data.data.map(model => model.id || model.model || model.name).filter(Boolean); + } else if (data.models && Array.isArray(data.models)) { + models = data.models.map(model => model.id || model.model || model.name).filter(Boolean); + } + + if (models.length > 0) { + provider.models = models; + provider.defaultModel = models[0]; + await this.persistProviders(); + this.emit('provider-updated', provider); + } + + return models; + } catch (error) { + throw new Error(`Failed to fetch models: ${error.message}`); + } + } + + on(event, listener) { + this.eventEmitter.on(event, listener); + } + + off(event, listener) { + this.eventEmitter.off(event, listener); + } + + emit(event, ...args) { + this.eventEmitter.emit(event, ...args); + } +} + +exports.AIProviderService = AIProviderService; diff --git a/src/app-extracted/dist/services/settingsService.js b/src/app-extracted/dist/services/settingsService.js new file mode 100644 index 0000000..923db9b --- /dev/null +++ b/src/app-extracted/dist/services/settingsService.js @@ -0,0 +1,62 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SettingsService = exports.DEFAULTS = exports.SettingKey = void 0; +const utils_1 = require("../utils"); +// Setting keys +var SettingKey; +(function (SettingKey) { + SettingKey["RUN_IN_BACKGROUND"] = "runInBackground"; + SettingKey["KEEP_COMPUTER_AWAKE"] = "keepComputerAwake"; + SettingKey["AI_PROVIDER"] = "aiProvider"; + SettingKey["AI_MODEL"] = "aiModel"; + SettingKey["AI_PROVIDERS_CONFIG"] = "aiProviders"; + SettingKey["AI_TEMPERATURE"] = "aiTemperature"; + SettingKey["AI_MAX_TOKENS"] = "aiMaxTokens"; + SettingKey["AI_STREAMING"] = "aiStreaming"; + SettingKey["AI_EMBEDDING_PROVIDER"] = "aiEmbeddingProvider"; +})(SettingKey || (exports.SettingKey = SettingKey = {})); +// Default values +exports.DEFAULTS = new Map([ + // The following setting is off by default for windows because the app + // icon is not as discoverable in the bottom right corner menu bar as + // it is on macOS and linux. + [SettingKey.RUN_IN_BACKGROUND, process.platform !== 'win32'], + [SettingKey.KEEP_COMPUTER_AWAKE, false], + [SettingKey.AI_PROVIDER, 'openai-default'], + [SettingKey.AI_MODEL, 'gpt-4o'], + [SettingKey.AI_TEMPERATURE, '0.7'], + [SettingKey.AI_MAX_TOKENS, '4096'], + [SettingKey.AI_STREAMING, 'true'], + [SettingKey.AI_EMBEDDING_PROVIDER, 'openai-default'], +]); +/** + * A thin wrapper around StorageManager to listen for changes + * in settings and apply their side effects. + */ +class SettingsService { + constructor(storageManager) { + this.storageManager = storageManager; + this.storageManager.onDidChange((changes) => { + this.applySideEffects(changes); + }); + void this.initialize(); + } + async initialize() { + const items = await this.storageManager.getItems(); + this.applySideEffects(items); + } + applySideEffects(settings) { + const val = settings[SettingKey.KEEP_COMPUTER_AWAKE]; + if (val !== undefined) { + const preventSleep = val === null + ? exports.DEFAULTS.get(SettingKey.KEEP_COMPUTER_AWAKE) + : val === 'true'; + utils_1.SleepBlocker.getInstance().shouldKeepComputerAwake(preventSleep); + } + } + async getSetting(key) { + const items = await this.storageManager.getItems(); + return items[key] === 'true'; + } +} +exports.SettingsService = SettingsService; diff --git a/src/app-extracted/dist/services/settingsService.test.js b/src/app-extracted/dist/services/settingsService.test.js new file mode 100644 index 0000000..79939f2 --- /dev/null +++ b/src/app-extracted/dist/services/settingsService.test.js @@ -0,0 +1,65 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const vitest_1 = require("vitest"); +const settingsService_1 = require("./settingsService"); +const utils_1 = require("../utils"); +vitest_1.vi.mock('../storage'); +vitest_1.vi.mock('../utils'); +(0, vitest_1.describe)('SettingsService', () => { + let settingsService; + let mockStorageManager; + let mockSleepBlocker; + (0, vitest_1.beforeEach)(() => { + vitest_1.vi.clearAllMocks(); + mockStorageManager = { + getItems: vitest_1.vi.fn().mockResolvedValue({ + runInBackground: String(process.platform !== 'win32'), + keepComputerAwake: 'false', + }), + onDidChange: vitest_1.vi.fn().mockReturnValue({ dispose: vitest_1.vi.fn() }), + }; + mockSleepBlocker = { + shouldKeepComputerAwake: vitest_1.vi.fn(), + }; + vitest_1.vi.mocked(utils_1.SleepBlocker.getInstance).mockReturnValue(mockSleepBlocker); + settingsService = new settingsService_1.SettingsService(mockStorageManager); + }); + (0, vitest_1.it)('should return defaults when storage is empty', async () => { + (0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.RUN_IN_BACKGROUND)).toBe(true); + (0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.KEEP_COMPUTER_AWAKE)).toBe(false); + }); + (0, vitest_1.it)('should return values from storage', async () => { + mockStorageManager.getItems.mockResolvedValue({ + runInBackground: 'false', + keepComputerAwake: 'true', + }); + (0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.RUN_IN_BACKGROUND)).toBe(false); + (0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.KEEP_COMPUTER_AWAKE)).toBe(true); + }); + (0, vitest_1.it)('should return updated value after storage change', async () => { + mockStorageManager.getItems.mockResolvedValue({ + [settingsService_1.SettingKey.RUN_IN_BACKGROUND]: 'false', + }); + (0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.RUN_IN_BACKGROUND)).toBe(false); + }); + (0, vitest_1.it)('should trigger SleepBlocker on keepComputerAwake change', async () => { + let changeListener; + mockStorageManager.onDidChange.mockImplementation((listener) => { + changeListener = listener; + return { dispose: vitest_1.vi.fn() }; + }); + // Instantiate again to trigger constructor with the new mock + settingsService = new settingsService_1.SettingsService(mockStorageManager); + // Simulate change + changeListener({ keepComputerAwake: 'true' }); + (0, vitest_1.expect)(mockSleepBlocker.shouldKeepComputerAwake).toHaveBeenCalledWith(true); + }); + (0, vitest_1.it)('should trigger initial SleepBlocker state', async () => { + mockStorageManager.getItems.mockResolvedValue({ + keepComputerAwake: 'true', + }); + settingsService = new settingsService_1.SettingsService(mockStorageManager); + await new Promise(process.nextTick); + (0, vitest_1.expect)(mockSleepBlocker.shouldKeepComputerAwake).toHaveBeenCalledWith(true); + }); +}); diff --git a/src/app-extracted/dist/storage.js b/src/app-extracted/dist/storage.js new file mode 100644 index 0000000..fc72672 --- /dev/null +++ b/src/app-extracted/dist/storage.js @@ -0,0 +1,128 @@ +"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; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.StorageManager = void 0; +const fs = __importStar(require("fs/promises")); +const fs_1 = require("fs"); +const path = __importStar(require("path")); +const electron_1 = require("electron"); +const events_1 = require("events"); +/** + * Manages persistent storage for the application. + * Stores key-value pairs. + */ +class StorageManager { + constructor(storagePath, defaults) { + this.storagePath = storagePath; + this.defaults = defaults; + this.emitter = new events_1.EventEmitter(); + this.onDidChange = (listener) => { + this.emitter.on('changed', listener); + return { + dispose: () => this.emitter.off('changed', listener), + }; + }; + } + /** + * Gets raw items from the storage file. + */ + async getRawItems() { + try { + if (!(0, fs_1.existsSync)(this.storagePath)) { + return {}; + } + const content = await fs.readFile(this.storagePath, 'utf-8'); + if (!content || content.trim() === '') { + return {}; + } + return JSON.parse(content); + } + catch (e) { + console.error('Error reading storage items:', e); + return {}; + } + } + /** + * Gets all items from the storage, with defaults applied. + * + * @returns A record of key-value pairs. + */ + async getItems() { + const items = await this.getRawItems(); + const merged = { ...items }; + if (this.defaults) { + for (const [key, value] of this.defaults.entries()) { + if (merged[key] === undefined) { + merged[key] = String(value); + } + } + } + return merged; + } + /** + * Updates items in the storage. + * + * @param changes A record of key-value pairs to update. If a value is null, the key will be deleted. + */ + async updateItems(changes) { + try { + const currentItems = await this.getRawItems(); + for (const [key, value] of Object.entries(changes)) { + if (value === null) { + delete currentItems[key]; + } + else { + currentItems[key] = value; + } + } + // Ensure directory exists + const dir = path.dirname(this.storagePath); + if (!(0, fs_1.existsSync)(dir)) { + await fs.mkdir(dir, { recursive: true }); + } + await fs.writeFile(this.storagePath, JSON.stringify(currentItems, null, 2), 'utf-8'); + const windows = electron_1.BrowserWindow.getAllWindows(); + for (const win of windows) { + win.webContents.send('storage:changed', changes); + } + this.emitter.emit('changed', changes); + } + catch (e) { + console.error('Error updating storage items:', e); + throw e; + } + } +} +exports.StorageManager = StorageManager; diff --git a/src/app-extracted/dist/storage.test.js b/src/app-extracted/dist/storage.test.js new file mode 100644 index 0000000..5a46185 --- /dev/null +++ b/src/app-extracted/dist/storage.test.js @@ -0,0 +1,142 @@ +"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; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const vitest_1 = require("vitest"); +const storage_1 = require("./storage"); +const fs = __importStar(require("fs/promises")); +const fs_1 = require("fs"); +const settingsService_1 = require("./services/settingsService"); +vitest_1.vi.mock('fs/promises'); +vitest_1.vi.mock('fs'); +vitest_1.vi.mock('electron'); +(0, vitest_1.describe)('StorageManager', () => { + const mockPath = '/fake/path/storage.json'; + let storageManager; + (0, vitest_1.beforeEach)(() => { + vitest_1.vi.clearAllMocks(); + storageManager = new storage_1.StorageManager(mockPath, settingsService_1.DEFAULTS); + }); + (0, vitest_1.describe)('getItems', () => { + (0, vitest_1.it)('should return defaults if file does not exist', async () => { + vitest_1.vi.mocked(fs_1.existsSync).mockReturnValue(false); + const items = await storageManager.getItems(); + (0, vitest_1.expect)(items).toEqual({ + runInBackground: String(process.platform !== 'win32'), + keepComputerAwake: 'false', + }); + (0, vitest_1.expect)(fs_1.existsSync).toHaveBeenCalledWith(mockPath); + }); + (0, vitest_1.it)('should return defaults if file is empty', async () => { + vitest_1.vi.mocked(fs_1.existsSync).mockReturnValue(true); + vitest_1.vi.mocked(fs.readFile).mockResolvedValue(''); + const items = await storageManager.getItems(); + (0, vitest_1.expect)(items).toEqual({ + runInBackground: String(process.platform !== 'win32'), + keepComputerAwake: 'false', + }); + }); + (0, vitest_1.it)('should return parsed JSON object merged with defaults if file contains valid JSON', async () => { + vitest_1.vi.mocked(fs_1.existsSync).mockReturnValue(true); + vitest_1.vi.mocked(fs.readFile).mockResolvedValue('{"key1": "value1"}'); + const items = await storageManager.getItems(); + (0, vitest_1.expect)(items).toEqual({ + key1: 'value1', + runInBackground: String(process.platform !== 'win32'), + keepComputerAwake: 'false', + }); + }); + (0, vitest_1.it)('should handle JSON parse error and return defaults', async () => { + vitest_1.vi.mocked(fs_1.existsSync).mockReturnValue(true); + vitest_1.vi.mocked(fs.readFile).mockResolvedValue('invalid-json'); + const items = await storageManager.getItems(); + (0, vitest_1.expect)(items).toEqual({ + runInBackground: String(process.platform !== 'win32'), + keepComputerAwake: 'false', + }); + }); + }); + (0, vitest_1.describe)('updateItems', () => { + (0, vitest_1.it)('should save updates merge with existing items', async () => { + // Setup: file exists and has some data + vitest_1.vi.mocked(fs_1.existsSync).mockReturnValue(true); + vitest_1.vi.mocked(fs.readFile).mockResolvedValue('{"key1": "value1"}'); + vitest_1.vi.mocked(fs.writeFile).mockResolvedValue(undefined); + await storageManager.updateItems({ key2: 'value2', key1: 'newValue1' }); + // Should write merged data + (0, vitest_1.expect)(fs.writeFile).toHaveBeenCalledWith(mockPath, vitest_1.expect.stringContaining('"key1": "newValue1"'), 'utf-8'); + (0, vitest_1.expect)(fs.writeFile).toHaveBeenCalledWith(mockPath, vitest_1.expect.stringContaining('"key2": "value2"'), 'utf-8'); + }); + (0, vitest_1.it)('should create directory if it does not exist before writing', async () => { + // Mock existsSync(file) as true for reading, but mock existsSync(dir) as false for mkdir + vitest_1.vi.mocked(fs_1.existsSync).mockImplementation((path) => { + if (path === mockPath) { + return true; // file exists for read + } + if (path === '/fake/path') { + return false; // dir doesn't exist + } + return false; + }); + vitest_1.vi.mocked(fs.readFile).mockResolvedValue('{}'); + vitest_1.vi.mocked(fs.mkdir).mockResolvedValue(undefined); + vitest_1.vi.mocked(fs.writeFile).mockResolvedValue(undefined); + await storageManager.updateItems({ key: 'value' }); + (0, vitest_1.expect)(fs.mkdir).toHaveBeenCalledWith('/fake/path', { recursive: true }); + (0, vitest_1.expect)(fs.writeFile).toHaveBeenCalled(); + }); + (0, vitest_1.it)('should broadcast storage:changed to all windows', async () => { + vitest_1.vi.mocked(fs_1.existsSync).mockReturnValue(true); + vitest_1.vi.mocked(fs.readFile).mockResolvedValue('{"key1": "value1"}'); + vitest_1.vi.mocked(fs.writeFile).mockResolvedValue(undefined); + const { BrowserWindow } = await Promise.resolve().then(() => __importStar(require('electron'))); + const mockWindows = BrowserWindow.getAllWindows(); + const mockWebContents = mockWindows[0].webContents; + await storageManager.updateItems({ key2: 'value2' }); + (0, vitest_1.expect)(BrowserWindow.getAllWindows).toHaveBeenCalled(); + (0, vitest_1.expect)(mockWebContents.send).toHaveBeenCalledWith('storage:changed', { + key2: 'value2', + }); + }); + (0, vitest_1.it)('should emit changed event when items are updated', async () => { + vitest_1.vi.mocked(fs_1.existsSync).mockReturnValue(true); + vitest_1.vi.mocked(fs.readFile).mockResolvedValue('{}'); + vitest_1.vi.mocked(fs.writeFile).mockResolvedValue(undefined); + const listener = vitest_1.vi.fn(); + storageManager.onDidChange(listener); + await storageManager.updateItems({ key: 'value' }); + (0, vitest_1.expect)(listener).toHaveBeenCalledWith({ key: 'value' }); + }); + }); +}); diff --git a/src/app-extracted/dist/test/helpers.js b/src/app-extracted/dist/test/helpers.js new file mode 100644 index 0000000..6ae57b1 --- /dev/null +++ b/src/app-extracted/dist/test/helpers.js @@ -0,0 +1,23 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DEFAULT_WINDOW_URL = void 0; +exports.silenceConsole = silenceConsole; +/** + * Shared test helpers and utilities. + * + * For module mocks (electron, electron-updater), use the auto-mock files + * in `src/__mocks__/` instead. This file is for runtime helpers that + * are called in beforeEach/afterEach blocks. + */ +const vitest_1 = require("vitest"); +const constants_1 = require("../constants"); +exports.DEFAULT_WINDOW_URL = `${constants_1.WINDOW_ORIGIN}:${constants_1.DYNAMIC_PORT}/`; +/** + * Silence console output during tests. Call in `beforeEach`. + * Restoring is handled by `vi.restoreAllMocks()` in `afterEach`. + */ +function silenceConsole() { + vitest_1.vi.spyOn(console, 'log').mockImplementation(() => { }); + vitest_1.vi.spyOn(console, 'warn').mockImplementation(() => { }); + vitest_1.vi.spyOn(console, 'error').mockImplementation(() => { }); +} diff --git a/src/app-extracted/dist/tray.js b/src/app-extracted/dist/tray.js new file mode 100644 index 0000000..429e20a --- /dev/null +++ b/src/app-extracted/dist/tray.js @@ -0,0 +1,79 @@ +"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; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createTray = createTray; +exports.updateTrayAgentCount = updateTrayAgentCount; +const electron_1 = require("electron"); +const path = __importStar(require("path")); +const utils_1 = require("./utils"); +// Keep tray as a global variable to prevent it from being garbage collected. +let tray = null; +let contextMenu = null; +/** + * Creates a system tray icon with a context menu to focus a window or quit the app. + * + * For macOS it uses a template image to automatically handle light/dark mode. + * Other platforms use the normal app icon. + */ +function createTray(actions) { + // On macOS use a template image (auto-inverts for dark/light menu bar). + // Otherwise use a full-color icon since template images are unsupported + // and a solid-black glyph can be invisible on dark panels. + const iconFile = (0, utils_1.isMacOS)() ? 'trayTemplate.png' : 'icon.png'; + const icon = electron_1.nativeImage.createFromPath(path.join(__dirname, '..', iconFile)); + if ((0, utils_1.isMacOS)()) { + icon.setTemplateImage(true); + } + tray = new electron_1.Tray(icon); + tray.setToolTip(electron_1.app.getName()); + contextMenu = electron_1.Menu.buildFromTemplate(actions); + tray.setContextMenu(contextMenu); +} +/** + * Updates the active agents count in the tray menu. + */ +function updateTrayAgentCount(count) { + if (tray && contextMenu) { + const countItem = contextMenu.items.find((item) => item.id === 'running-agents'); + if (countItem) { + countItem.label = + (count > 0 ? `${count}` : 'No') + + ' agent' + + (count === 1 ? '' : 's') + + ' running'; + tray.setContextMenu(contextMenu); + } + } +} diff --git a/src/app-extracted/dist/tray.test.js b/src/app-extracted/dist/tray.test.js new file mode 100644 index 0000000..6c702d7 --- /dev/null +++ b/src/app-extracted/dist/tray.test.js @@ -0,0 +1,87 @@ +"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; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const vitest_1 = require("vitest"); +vitest_1.vi.mock('electron'); +(0, vitest_1.describe)('tray', () => { + (0, vitest_1.beforeEach)(() => { + vitest_1.vi.clearAllMocks(); + vitest_1.vi.resetModules(); + }); + (0, vitest_1.it)('should create a tray with a context menu', async () => { + const { Tray, Menu, nativeImage } = await Promise.resolve().then(() => __importStar(require('electron'))); + const { createTray } = await Promise.resolve().then(() => __importStar(require('./tray'))); + createTray([ + { label: 'Open App', click: vitest_1.vi.fn() }, + { type: 'separator' }, + { label: 'Quit', click: vitest_1.vi.fn() }, + ]); + const trayInstance = vitest_1.vi.mocked(Tray).mock.results[0].value; + (0, vitest_1.expect)(nativeImage.createFromPath).toHaveBeenCalled(); + (0, vitest_1.expect)(Tray).toHaveBeenCalled(); + (0, vitest_1.expect)(trayInstance.setToolTip).toHaveBeenCalled(); + (0, vitest_1.expect)(Menu.buildFromTemplate).toHaveBeenCalled(); + (0, vitest_1.expect)(trayInstance.setContextMenu).toHaveBeenCalled(); + }); + (0, vitest_1.it)('should call openApp when Open is clicked', async () => { + const { Menu } = await Promise.resolve().then(() => __importStar(require('electron'))); + const { createTray } = await Promise.resolve().then(() => __importStar(require('./tray'))); + const openApp = vitest_1.vi.fn(); + createTray([ + { label: 'Open App', click: openApp }, + { type: 'separator' }, + { label: 'Quit', click: vitest_1.vi.fn() }, + ]); + const menuTemplate = vitest_1.vi.mocked(Menu.buildFromTemplate).mock.calls[0][0]; + const openItem = menuTemplate[0]; + openItem.click(); + (0, vitest_1.expect)(openApp).toHaveBeenCalled(); + }); + (0, vitest_1.it)('should call quitApp when Quit is clicked', async () => { + const { Menu } = await Promise.resolve().then(() => __importStar(require('electron'))); + const { createTray } = await Promise.resolve().then(() => __importStar(require('./tray'))); + const quitApp = vitest_1.vi.fn(); + createTray([ + { label: 'Open App', click: vitest_1.vi.fn() }, + { type: 'separator' }, + { label: 'Quit', click: quitApp }, + ]); + const menuTemplate = vitest_1.vi.mocked(Menu.buildFromTemplate).mock.calls[0][0]; + // Quit is the third item (after separator) + const quitItem = menuTemplate[2]; + quitItem.click(); + (0, vitest_1.expect)(quitApp).toHaveBeenCalled(); + }); +}); diff --git a/src/app-extracted/dist/types.js b/src/app-extracted/dist/types.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/src/app-extracted/dist/types.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/app-extracted/dist/updater.js b/src/app-extracted/dist/updater.js new file mode 100644 index 0000000..5ba8445 --- /dev/null +++ b/src/app-extracted/dist/updater.js @@ -0,0 +1,241 @@ +"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; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.updateActions = exports.MenuUpdateStep = void 0; +exports.broadcastState = broadcastState; +exports.initAutoUpdater = initAutoUpdater; +exports.checkForUpdates = checkForUpdates; +exports.quitAndInstall = quitAndInstall; +const electron_updater_1 = require("electron-updater"); +const electron_1 = require("electron"); +const path = __importStar(require("path")); +const child_process_1 = require("child_process"); +var MenuUpdateStep; +(function (MenuUpdateStep) { + MenuUpdateStep["CheckForUpdates"] = "Check for Updates"; + MenuUpdateStep["CheckingForUpdates"] = "Checking for Updates..."; + MenuUpdateStep["DownloadingUpdate"] = "Downloading Update..."; + MenuUpdateStep["RestartToUpdate"] = "Restart to Update"; +})(MenuUpdateStep || (exports.MenuUpdateStep = MenuUpdateStep = {})); +exports.updateActions = { + [MenuUpdateStep.CheckForUpdates]: () => checkForUpdates(true), + [MenuUpdateStep.CheckingForUpdates]: undefined, + [MenuUpdateStep.DownloadingUpdate]: undefined, + [MenuUpdateStep.RestartToUpdate]: () => quitAndInstall(), +}; +// True if the last call to check for updates was from a user click in the menu. +let isManualCheck = false; +// How long to wait after app start before first update check (ms) +const INITIAL_CHECK_DELAY_MS = 10000; // 10 seconds +// How often to re-check for updates after the initial check (ms) +const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour +/** Broadcast a state change to every open BrowserWindow. */ +function broadcastState(state) { + for (const win of electron_1.BrowserWindow.getAllWindows()) { + win.webContents.send('updater:state-changed', state); + } +} +/** + * Updates the state of the menu item based on the current step of the updater. + */ +function updateMenuState(step) { + const menu = electron_1.Menu.getApplicationMenu(); + if (menu) { + const item = menu.getMenuItemById('check-for-updates'); + if (item) { + item.label = step; + item.enabled = exports.updateActions[step] !== undefined; + } + } +} +/** + * Initializes the auto-updater and registers IPC handlers. + * Call once after the first window is created. + * + * The updater will: + * 1. Wait INITIAL_CHECK_DELAY_MS ms, then check for updates. + * 2. Re-check every CHECK_INTERVAL_MS ms. + * 3. Download updates automatically in the background. + * 4. Broadcast state to the renderer so AppUpdateButton can display progress. + */ +function initAutoUpdater(isHeadless) { + // In dev mode (npm start), electron-updater skips checks because the app + // isn't packaged. Force it to use the dev config file instead. + if (!electron_1.app.isPackaged) { + electron_updater_1.autoUpdater.forceDevUpdateConfig = true; + electron_updater_1.autoUpdater.updateConfigPath = path.join(electron_1.app.getAppPath(), 'dev-app-update.yml'); + } + // Set the channel based on architecture and OS. + // On Windows, we need to explicitly append '-win' to match the artifact name. + // On macOS and linux, Electron automatically appends the OS to the channel name. + if (process.platform === 'win32') { + electron_updater_1.autoUpdater.channel = `latest-${process.arch}-win`; + } + else { + electron_updater_1.autoUpdater.channel = `latest-${process.arch}`; + } + electron_updater_1.autoUpdater.autoDownload = true; + electron_updater_1.autoUpdater.autoInstallOnAppQuit = electron_1.app.isPackaged; + // Auto-updater event handlers β†’ broadcast to renderer + electron_updater_1.autoUpdater.on('checking-for-update', () => { + console.log('[AutoUpdater] Checking for update…'); + broadcastState({ type: 'checking for updates' }); + updateMenuState(MenuUpdateStep.CheckingForUpdates); + }); + electron_updater_1.autoUpdater.on('update-available', (info) => { + console.log(`[AutoUpdater] Update available: ${info.version}`); + broadcastState({ + type: 'available for download', + update: { version: info.version }, + }); + updateMenuState(MenuUpdateStep.DownloadingUpdate); + isManualCheck = false; + }); + electron_updater_1.autoUpdater.on('update-not-available', (info) => { + console.log(`[AutoUpdater] Up to date (${info.version})`); + broadcastState({ type: 'idle' }); + updateMenuState(MenuUpdateStep.CheckForUpdates); + if (isManualCheck && !isHeadless) { + const win = electron_1.BrowserWindow.getFocusedWindow(); + const options = { + type: 'info', + title: 'Check for Updates', + message: 'No updates available', + buttons: ['OK'], + }; + if (win) { + electron_1.dialog.showMessageBox(win, options); + } + else { + electron_1.dialog.showMessageBox(options); + } + } + isManualCheck = false; + }); + electron_updater_1.autoUpdater.on('download-progress', () => { + broadcastState({ type: 'downloading' }); + updateMenuState(MenuUpdateStep.DownloadingUpdate); + }); + electron_updater_1.autoUpdater.on('update-downloaded', (info) => { + console.log(`[AutoUpdater] Update downloaded: ${info.version}`); + if (isHeadless) { + // Proceed to auto install in headless mode + if (electron_1.app.isPackaged) { + if (process.platform === 'linux') { + const downloadedFilePath = info.downloadedFile; + headlessQuitAndInstall(downloadedFilePath); + } + else { + electron_updater_1.autoUpdater.quitAndInstall(); + } + } + else { + console.log('[AutoUpdater] Headless mode: Skipping quitAndInstall (not packaged).'); + } + return; + } + broadcastState({ + type: 'ready', + update: { version: info.version }, + }); + updateMenuState(MenuUpdateStep.RestartToUpdate); + }); + electron_updater_1.autoUpdater.on('error', (err) => { + console.error('[AutoUpdater] Error:', err.message); + broadcastState({ type: 'idle' }); + updateMenuState(MenuUpdateStep.CheckForUpdates); + isManualCheck = false; + }); + // Schedule periodic checks + setTimeout(() => { + checkForUpdates(); + setInterval(checkForUpdates, CHECK_INTERVAL_MS); + }, INITIAL_CHECK_DELAY_MS); +} +function checkForUpdates(isManual = false) { + isManualCheck = isManual; + electron_updater_1.autoUpdater.checkForUpdates().catch((err) => { + console.error('[AutoUpdater] Failed to check for updates:', err.message); + }); +} +function quitAndInstall() { + electron_updater_1.autoUpdater.quitAndInstall(); +} +/** + * Electron native quitAndInstall doesn't relaunch the app with command line arguments. + * This function waits for the app process to quit, manually replaces the executable with + * the downloaded update, and then relaunches it with the right headless flags. + */ +function headlessQuitAndInstall(downloadedFilePath) { + console.log('[AutoUpdater] Headless mode: Scheduling post-quit restart.'); + try { + const currentPid = process.pid; + const appPath = process.env.APPIMAGE || process.execPath; + const args = [ + '--ozone-platform=headless', + '--headless', + '--disable-gpu', + '--no-sandbox', + ]; + let script = ''; + if (downloadedFilePath) { + console.log(`[AutoUpdater] Will manually replace ${appPath} with ${downloadedFilePath}`); + script = ` + while kill -0 ${currentPid} 2>/dev/null; do sleep 0.5; done + cp -f "${downloadedFilePath}" "${appPath}" + chmod +x "${appPath}" + "${appPath}" ${args.join(' ')} + `; + } + else { + console.warn('[AutoUpdater] No downloaded file path found, relaunching without update.'); + script = ` + while kill -0 ${currentPid} 2>/dev/null; do sleep 0.5; done + sleep 3 + "${appPath}" ${args.join(' ')} + `; + } + const child = (0, child_process_1.spawn)('sh', ['-c', script], { + detached: true, + stdio: 'ignore', + env: { ...process.env, ELECTRON_OZONE_PLATFORM_HINT: 'headless' }, + }); + child.unref(); + } + catch (e) { + console.error('[AutoUpdater] Failed to schedule restart:', e); + } + electron_1.app.quit(); +} diff --git a/src/app-extracted/dist/updater.test.js b/src/app-extracted/dist/updater.test.js new file mode 100644 index 0000000..450712e --- /dev/null +++ b/src/app-extracted/dist/updater.test.js @@ -0,0 +1,91 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const vitest_1 = require("vitest"); +const updater_1 = require("./updater"); +const electron_updater_1 = require("electron-updater"); +const electron_1 = require("electron"); +const child_process_1 = require("child_process"); +const electron_updater_2 = require("./__mocks__/electron-updater"); +// Use shared auto-mocks from __mocks__/ +vitest_1.vi.mock('electron'); +vitest_1.vi.mock('electron-updater'); +vitest_1.vi.mock('child_process', () => ({ + spawn: vitest_1.vi.fn(() => ({ + unref: vitest_1.vi.fn(), + })), +})); +(0, vitest_1.describe)('updater', () => { + (0, vitest_1.beforeEach)(() => { + vitest_1.vi.clearAllMocks(); // Clear mock history before each test + vitest_1.vi.useFakeTimers(); + }); + (0, vitest_1.it)('should register IPC handlers and updater events on init', () => { + (0, updater_1.initAutoUpdater)(false); + // Verify autoUpdater events were registered + (0, vitest_1.expect)(electron_updater_1.autoUpdater.on).toHaveBeenCalledWith('checking-for-update', vitest_1.expect.any(Function)); + (0, vitest_1.expect)(electron_updater_1.autoUpdater.on).toHaveBeenCalledWith('update-available', vitest_1.expect.any(Function)); + (0, vitest_1.expect)(electron_updater_1.autoUpdater.on).toHaveBeenCalledWith('update-not-available', vitest_1.expect.any(Function)); + (0, vitest_1.expect)(electron_updater_1.autoUpdater.on).toHaveBeenCalledWith('download-progress', vitest_1.expect.any(Function)); + (0, vitest_1.expect)(electron_updater_1.autoUpdater.on).toHaveBeenCalledWith('update-downloaded', vitest_1.expect.any(Function)); + (0, vitest_1.expect)(electron_updater_1.autoUpdater.on).toHaveBeenCalledWith('error', vitest_1.expect.any(Function)); + }); + (0, vitest_1.it)('should schedule an update check', () => { + (0, updater_1.initAutoUpdater)(false); + // Fast-forward the 10-second delay + vitest_1.vi.advanceTimersByTime(10000); + (0, vitest_1.expect)(electron_updater_1.autoUpdater.checkForUpdates).toHaveBeenCalled(); + }); + (0, vitest_1.it)('should auto-install on update-downloaded in headless mode (packaged)', () => { + (0, updater_1.initAutoUpdater)(true); + const callback = electron_updater_2.autoUpdaterEvents['update-downloaded']; + (0, vitest_1.expect)(callback).toBeDefined(); + callback({ version: '1.2.3' }); + if (process.platform === 'linux') { + (0, vitest_1.expect)(child_process_1.spawn).toHaveBeenCalled(); + (0, vitest_1.expect)(electron_1.app.quit).toHaveBeenCalled(); + (0, vitest_1.expect)(electron_updater_1.autoUpdater.quitAndInstall).not.toHaveBeenCalled(); + } + else { + (0, vitest_1.expect)(electron_updater_1.autoUpdater.quitAndInstall).toHaveBeenCalled(); + } + (0, vitest_1.expect)(electron_1.BrowserWindow.getAllWindows).not.toHaveBeenCalled(); + }); + (0, vitest_1.it)('should prompt the user on update-downloaded in normal mode (packaged)', () => { + (0, updater_1.initAutoUpdater)(false); + const callback = electron_updater_2.autoUpdaterEvents['update-downloaded']; + (0, vitest_1.expect)(callback).toBeDefined(); + callback({ version: '1.2.3' }); + (0, vitest_1.expect)(electron_updater_1.autoUpdater.quitAndInstall).not.toHaveBeenCalled(); + const win = vitest_1.vi.mocked(electron_1.BrowserWindow.getAllWindows).mock.results[0].value[0]; + (0, vitest_1.expect)(win.webContents.send).toHaveBeenCalledWith('updater:state-changed', { + type: 'ready', + update: { version: '1.2.3' }, + }); + }); + (0, vitest_1.it)('should show modal on update-not-available if manual check', () => { + (0, updater_1.initAutoUpdater)(false); + (0, updater_1.checkForUpdates)(true); + const callback = electron_updater_2.autoUpdaterEvents['update-not-available']; + (0, vitest_1.expect)(callback).toBeDefined(); + callback({ version: '1.0.0' }); + (0, vitest_1.expect)(electron_1.dialog.showMessageBox).toHaveBeenCalledWith(vitest_1.expect.anything(), vitest_1.expect.objectContaining({ + message: 'No updates available', + })); + }); + (0, vitest_1.it)('should NOT show modal on update-not-available if periodic check', () => { + (0, updater_1.initAutoUpdater)(false); + (0, updater_1.checkForUpdates)(false); + const callback = electron_updater_2.autoUpdaterEvents['update-not-available']; + (0, vitest_1.expect)(callback).toBeDefined(); + callback({ version: '1.0.0' }); + (0, vitest_1.expect)(electron_1.dialog.showMessageBox).not.toHaveBeenCalled(); + }); + (0, vitest_1.it)('should NOT show modal on update-not-available if manual check in headless mode', () => { + (0, updater_1.initAutoUpdater)(true); + (0, updater_1.checkForUpdates)(true); + const callback = electron_updater_2.autoUpdaterEvents['update-not-available']; + (0, vitest_1.expect)(callback).toBeDefined(); + callback({ version: '1.0.0' }); + (0, vitest_1.expect)(electron_1.dialog.showMessageBox).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app-extracted/dist/utils.js b/src/app-extracted/dist/utils.js new file mode 100644 index 0000000..6f84592 --- /dev/null +++ b/src/app-extracted/dist/utils.js @@ -0,0 +1,269 @@ +"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 }); +exports.SleepBlocker = exports.showOrCreateWindow = exports.showQuitConfirmation = void 0; +exports.setShowQuitConfirmation = setShowQuitConfirmation; +exports.isMacOS = isMacOS; +exports.createWindow = createWindow; +exports.getNodeWrapperPaths = getNodeWrapperPaths; +exports.setupNodeWrapper = setupNodeWrapper; +const electron_1 = require("electron"); +const constants_1 = require("./constants"); +const keybindings_1 = require("./keybindings"); +const path_1 = __importDefault(require("path")); +const fs = __importStar(require("fs")); +const paths_1 = require("./paths"); +const loadingOverlay_1 = require("./loadingOverlay"); +exports.showQuitConfirmation = false; +function setShowQuitConfirmation(value) { + exports.showQuitConfirmation = value; +} +function isMacOS() { + return process.platform === 'darwin'; +} +/** + * Reads the user's theme preference from the settings file. + */ +function getThemeMode() { + try { + const filePath = (0, paths_1.getSettingsPbPath)(); + if (!fs.existsSync(filePath)) { + return 'DARK'; + } + const content = fs.readFileSync(filePath, 'utf-8'); + const config = JSON.parse(content); + const themeMode = config?.userSettings?.themeMode; + if (themeMode && themeMode.includes('INHERIT')) { + return electron_1.nativeTheme.shouldUseDarkColors ? 'DARK' : 'LIGHT'; + } + if (themeMode && themeMode.includes('LIGHT')) { + return 'LIGHT'; + } + return 'DARK'; + } + catch (e) { + console.error('Error reading theme mode:', e); + return 'DARK'; + } +} +/** + * Ensures the app is visible in the dock for MacOS with the icon set. + * When refocusing the app after being hidden in the dock, the icon is sometimes lost. + * This ensures the icon is always visible. + */ +function ensureAppIsInDock() { + void electron_1.app.dock?.show(); + if (isMacOS() && electron_1.app.dock) { + const iconPath = path_1.default.join(__dirname, '..', 'icon.png'); + electron_1.app.dock.setIcon(electron_1.nativeImage.createFromPath(iconPath)); + } +} +// --------------------------------------------------------------------------- +// Window Management +// --------------------------------------------------------------------------- +/** + * Creates and returns a new BrowserWindow pointed at `url`. + * Uses a hidden title bar with native traffic lights on macOS. + * Node integration is disabled and context isolation is enabled for security. + */ +function createWindow(url) { + ensureAppIsInDock(); + const theme = getThemeMode().toUpperCase(); + const isLight = theme.includes('LIGHT'); + const backgroundColor = isLight ? '#FAFAFA' : '#131313'; + const foregroundColor = isLight ? '#383A42' : '#FAFAFA'; + const win = new electron_1.BrowserWindow({ + width: 1400, + height: 900, + title: electron_1.app.getName(), + icon: path_1.default.join(__dirname, '..', 'icon.png'), + titleBarStyle: 'hidden', + titleBarOverlay: isMacOS() + ? false + : { + color: backgroundColor, + symbolColor: foregroundColor, + height: 30, + }, + backgroundColor, + trafficLightPosition: { x: 12, y: 12 }, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path_1.default.join(__dirname, 'preload.js'), + }, + }); + win.webContents.setWindowOpenHandler((details) => { + void electron_1.shell.openExternal(details.url); + return { action: 'deny' }; + }); + (0, loadingOverlay_1.attachLoadingOverlay)(win, foregroundColor, backgroundColor); + (0, keybindings_1.registerKeybindings)(win, { + createNewWindow: () => { + void createWindow(url); + }, + onQuitRequested: () => { + exports.showQuitConfirmation = true; + electron_1.app.quit(); + }, + }); + void win.loadURL(url); + return win; +} +/** + * Focuses a window if it exists, or creates a new one. + */ +const showOrCreateWindow = (port) => { + const wins = electron_1.BrowserWindow.getAllWindows(); + if (wins.length > 0) { + wins[0].show(); + wins[0].focus(); + } + else { + createWindow(`${constants_1.WINDOW_ORIGIN}:${port}/`); + } +}; +exports.showOrCreateWindow = showOrCreateWindow; +/** + * Manages the power save blocker to keep the computer awake. + */ +class SleepBlocker { + constructor() { + this.currentBlockerId = null; + } + static getInstance() { + if (!SleepBlocker.instance) { + SleepBlocker.instance = new SleepBlocker(); + } + return SleepBlocker.instance; + } + shouldKeepComputerAwake(keep) { + if (keep) { + if (this.currentBlockerId === null) { + this.currentBlockerId = electron_1.powerSaveBlocker.start('prevent-display-sleep'); + console.log('Power save blocker started:', this.currentBlockerId); + } + } + else { + if (this.currentBlockerId !== null) { + electron_1.powerSaveBlocker.stop(this.currentBlockerId); + console.log('Power save blocker stopped:', this.currentBlockerId); + this.currentBlockerId = null; + } + } + } +} +exports.SleepBlocker = SleepBlocker; +function getNodeWrapperPaths(envPath, os, isPackaged, userDataPath, baseDir) { + const delimiter = os === 'win32' ? ';' : ':'; + if (!isPackaged) { + const devBinPath = path_1.default.join(baseDir, '..', 'node_modules', '.bin'); + return { + newEnvPath: `${devBinPath}${delimiter}${envPath || ''}`, + nodeWrapperPath: undefined, + binPath: undefined, + }; + } + const binPath = path_1.default.join(userDataPath, 'bin'); + const nodeWrapperPath = path_1.default.join(binPath, os === 'win32' ? 'agy-node.cmd' : 'agy-node'); + return { + newEnvPath: `${binPath}${delimiter}${envPath || ''}`, + nodeWrapperPath, + binPath, + }; +} +/** + * Sets up a wrapper script for Node.js that runs Electron as Node. + * This allows running standard Node scripts using the Electron binary. + */ +function setupNodeWrapper(env) { + const userDataPath = electron_1.app.isPackaged ? electron_1.app.getPath('userData') : ''; + // Windows environment variables are case-insensitive, but when copying process.env + // into a plain object, we might get 'Path' instead of 'PATH'. We need to find + // the actual key used to avoid creating case-duplicate keys (e.g. 'Path' and 'PATH') + // which can confuse child_process.spawn on Windows. + const isWindows = process.platform === 'win32'; + const pathKey = isWindows + ? Object.keys(env).find((k) => k.toUpperCase() === 'PATH') || 'PATH' + : 'PATH'; + const { newEnvPath, nodeWrapperPath, binPath } = getNodeWrapperPaths(env[pathKey], process.platform, electron_1.app.isPackaged, userDataPath, __dirname); + env[pathKey] = newEnvPath; + // In non-packaged dev mode, we don't create a wrapper and it'll just use machine node + if (!nodeWrapperPath || !binPath) { + return; + } + if (!fs.existsSync(binPath)) { + fs.mkdirSync(binPath, { recursive: true }); + } + let nodeWrapperContent = ''; + switch (process.platform) { + case 'win32': + nodeWrapperContent = `@echo off\nset ELECTRON_RUN_AS_NODE=1\n"${process.execPath}" %*\n`; + break; + case 'darwin': { + // Use the Helper app instead of the main executable to prevent macOS + // from bouncing a new Dock icon when this script is executed. The Helper + // has LSUIElement=true in its Info.plist, running it invisibly. + const appName = path_1.default.basename(process.execPath); + let electronBinary = process.execPath; + const helperPath = path_1.default.join(path_1.default.dirname(process.execPath), '..', 'Frameworks', `${appName} Helper.app`, 'Contents', 'MacOS', `${appName} Helper`); + if (fs.existsSync(helperPath)) { + electronBinary = helperPath; + } + nodeWrapperContent = `#!/bin/sh\nELECTRON_RUN_AS_NODE=1 exec "${electronBinary}" "$@"\n`; + break; + } + default: // linux, etc. + nodeWrapperContent = `#!/bin/sh\nELECTRON_RUN_AS_NODE=1 exec "${process.execPath}" "$@"\n`; + break; + } + try { + const existingContent = fs.existsSync(nodeWrapperPath) + ? fs.readFileSync(nodeWrapperPath, 'utf-8') + : ''; + if (existingContent !== nodeWrapperContent) { + fs.writeFileSync(nodeWrapperPath, nodeWrapperContent); + if (process.platform !== 'win32') { + fs.chmodSync(nodeWrapperPath, 0o755); + } + } + } + catch (err) { + console.error(`Failed to create node wrapper: ${err}`); + } +} diff --git a/src/app-extracted/dist/utils.test.js b/src/app-extracted/dist/utils.test.js new file mode 100644 index 0000000..826f095 --- /dev/null +++ b/src/app-extracted/dist/utils.test.js @@ -0,0 +1,73 @@ +"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; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const vitest_1 = require("vitest"); +const helpers_1 = require("./test/helpers"); +vitest_1.vi.mock('electron'); +vitest_1.vi.mock('path', () => { + const join = vitest_1.vi.fn((...args) => args.join('/')); + return { + join, + default: { + join, + }, + }; +}); +(0, vitest_1.describe)('utils', () => { + (0, vitest_1.beforeEach)(() => { + vitest_1.vi.clearAllMocks(); + vitest_1.vi.useFakeTimers(); + (0, helpers_1.silenceConsole)(); + }); + (0, vitest_1.afterEach)(() => { + vitest_1.vi.restoreAllMocks(); + }); + (0, vitest_1.describe)('createWindow', () => { + (0, vitest_1.it)('should ensure dock is initialized when a window is created', async () => { + vitest_1.vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin'); + const { app } = await Promise.resolve().then(() => __importStar(require('electron'))); + const { createWindow } = await Promise.resolve().then(() => __importStar(require('./utils'))); + const dockShowSpy = vitest_1.vi.spyOn(vitest_1.vi.mocked(app).dock, 'show'); + createWindow('http://localhost:3000/'); + (0, vitest_1.expect)(dockShowSpy).toHaveBeenCalled(); + (0, vitest_1.expect)(vitest_1.vi.mocked(app).dock?.setIcon).toHaveBeenCalled(); + }); + (0, vitest_1.it)('should register before-input-event listener for keybindings', async () => { + const { createWindow } = await Promise.resolve().then(() => __importStar(require('./utils'))); + const win = createWindow('http://localhost:3000/'); + (0, vitest_1.expect)(win.webContents.on).toHaveBeenCalledWith('before-input-event', vitest_1.expect.any(Function)); + }); + }); +}); diff --git a/src/app-extracted/icon.png b/src/app-extracted/icon.png new file mode 100644 index 0000000..e02be84 Binary files /dev/null and b/src/app-extracted/icon.png differ diff --git a/src/app-extracted/package.json b/src/app-extracted/package.json new file mode 100644 index 0000000..d85fece --- /dev/null +++ b/src/app-extracted/package.json @@ -0,0 +1,18 @@ +{ + "name": "antigravity", + "productName": "Antigravity", + "version": "2.0.1", + "description": "Antigravity - Agentic Desktop Application", + "homepage": "https://antigravity.google", + "author": { + "name": "Google", + "email": "antigravity-support@google.com" + }, + "main": "dist/main.js", + "dependencies": { + "chrome-devtools-mcp": "^0.23.0", + "electron-log": "^5.4.3", + "electron-updater": "^6.8.3", + "shell-env": "^4.0.3" + } +} \ No newline at end of file diff --git a/src/app-extracted/trayTemplate.png b/src/app-extracted/trayTemplate.png new file mode 100644 index 0000000..41f0a1d Binary files /dev/null and b/src/app-extracted/trayTemplate.png differ diff --git a/src/app-extracted/trayTemplate@2x.png b/src/app-extracted/trayTemplate@2x.png new file mode 100644 index 0000000..7bad380 Binary files /dev/null and b/src/app-extracted/trayTemplate@2x.png differ diff --git a/src/app.asar b/src/app.asar new file mode 100644 index 0000000..362fde2 Binary files /dev/null and b/src/app.asar differ