Compare commits
2 Commits
v2.0.1-ai-
...
v2.0.1-ai-
265
BUILD.md
Normal file
265
BUILD.md
Normal file
@@ -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
|
||||
<!-- Add your UI changes -->
|
||||
<div id="my-new-section">
|
||||
<h2>My New Feature</h2>
|
||||
<!-- ... -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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!** 🚀
|
||||
278
README.md
278
README.md
@@ -4,124 +4,86 @@
|
||||
|
||||
**Antigravity IDE with 17+ Pre-configured AI Provider Presets**
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 🎯 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
|
||||
│ ├── antigravity # Executable
|
||||
│ ├── resources/ # App resources & app.asar
|
||||
│ └── [libraries]
|
||||
├── DEBIAN/ # Package metadata
|
||||
│ ├── control
|
||||
│ ├── preinst
|
||||
@@ -129,127 +91,29 @@ antigravity_2.0.1-ai-providers-1_amd64.deb
|
||||
│ ├── prerm
|
||||
│ └── postrm
|
||||
└── usr/share/applications/ # Desktop entry
|
||||
└── antigravity.desktop
|
||||
```
|
||||
|
||||
## 🔧 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!** 🚀
|
||||
|
||||
364
SOURCE_CODE.md
Normal file
364
SOURCE_CODE.md
Normal file
@@ -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!**
|
||||
300
docs/AI_PROVIDER_README.md
Normal file
300
docs/AI_PROVIDER_README.md
Normal file
@@ -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
|
||||
308
docs/AI_PROVIDER_SPECIFICATION.md
Normal file
308
docs/AI_PROVIDER_SPECIFICATION.md
Normal file
@@ -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<AIProvider[]>
|
||||
async addProvider(provider: AIProviderCreate): Promise<AIProvider>
|
||||
async updateProvider(id: string, updates: AIProviderUpdate): Promise<AIProvider>
|
||||
async deleteProvider(id: string): Promise<void>
|
||||
async testConnection(id: string): Promise<AIConnectionTest>
|
||||
async getSettings(): Promise<AISettings>
|
||||
async updateSettings(settings: Partial<AISettings>): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
#### 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.
|
||||
342
docs/IMPLEMENTATION_SUMMARY.md
Normal file
342
docs/IMPLEMENTATION_SUMMARY.md
Normal file
@@ -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!
|
||||
140
scripts/build-deb.sh
Executable file
140
scripts/build-deb.sh
Executable file
@@ -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 <support@antigravity.dev>
|
||||
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"
|
||||
33
scripts/extract-app.sh
Executable file
33
scripts/extract-app.sh
Executable file
@@ -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"
|
||||
36
scripts/install-deb.sh
Executable file
36
scripts/install-deb.sh
Executable file
@@ -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."
|
||||
30
scripts/repack-app.sh
Executable file
30
scripts/repack-app.sh
Executable file
@@ -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"
|
||||
300
src/app-extracted/dist/AI_PROVIDER_README.md
vendored
Normal file
300
src/app-extracted/dist/AI_PROVIDER_README.md
vendored
Normal file
@@ -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
|
||||
308
src/app-extracted/dist/AI_PROVIDER_SPECIFICATION.md
vendored
Normal file
308
src/app-extracted/dist/AI_PROVIDER_SPECIFICATION.md
vendored
Normal file
@@ -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<AIProvider[]>
|
||||
async addProvider(provider: AIProviderCreate): Promise<AIProvider>
|
||||
async updateProvider(id: string, updates: AIProviderUpdate): Promise<AIProvider>
|
||||
async deleteProvider(id: string): Promise<void>
|
||||
async testConnection(id: string): Promise<AIConnectionTest>
|
||||
async getSettings(): Promise<AISettings>
|
||||
async updateSettings(settings: Partial<AISettings>): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
#### 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.
|
||||
342
src/app-extracted/dist/IMPLEMENTATION_SUMMARY.md
vendored
Normal file
342
src/app-extracted/dist/IMPLEMENTATION_SUMMARY.md
vendored
Normal file
@@ -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!
|
||||
25
src/app-extracted/dist/__mocks__/electron-updater.js
vendored
Normal file
25
src/app-extracted/dist/__mocks__/electron-updater.js
vendored
Normal file
@@ -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(),
|
||||
};
|
||||
145
src/app-extracted/dist/__mocks__/electron.js
vendored
Normal file
145
src/app-extracted/dist/__mocks__/electron.js
vendored
Normal file
@@ -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(),
|
||||
};
|
||||
1070
src/app-extracted/dist/ai-provider-settings.html
vendored
Normal file
1070
src/app-extracted/dist/ai-provider-settings.html
vendored
Normal file
File diff suppressed because it is too large
Load Diff
163
src/app-extracted/dist/aiProviderAPI.ts
vendored
Normal file
163
src/app-extracted/dist/aiProviderAPI.ts
vendored
Normal file
@@ -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<AIProvider[]> {
|
||||
return await (window as any).electron.invoke('ai:get-providers');
|
||||
}
|
||||
|
||||
async getProvider(id: string): Promise<AIProvider | null> {
|
||||
return await (window as any).electron.invoke('ai:get-provider', id);
|
||||
}
|
||||
|
||||
async getEnabledProviders(): Promise<AIProvider[]> {
|
||||
return await (window as any).electron.invoke('ai:get-enabled-providers');
|
||||
}
|
||||
|
||||
async getDefaultProvider(): Promise<AIProvider> {
|
||||
return await (window as any).electron.invoke('ai:get-default-provider');
|
||||
}
|
||||
|
||||
async getAvailablePresets(): Promise<string[]> {
|
||||
return await (window as any).electron.invoke('ai:get-available-presets');
|
||||
}
|
||||
|
||||
async getPreset(presetName: string): Promise<any> {
|
||||
return await (window as any).electron.invoke('ai:get-preset', presetName);
|
||||
}
|
||||
|
||||
async addProvider(provider: AIProviderCreate): Promise<AIProvider> {
|
||||
return await (window as any).electron.invoke('ai:add-provider', provider);
|
||||
}
|
||||
|
||||
async addProviderFromPreset(presetName: string, apiKey: string = ''): Promise<AIProvider> {
|
||||
return await (window as any).electron.invoke('ai:add-provider-from-preset', presetName, apiKey);
|
||||
}
|
||||
|
||||
async updateProvider(id: string, updates: AIProviderUpdate): Promise<AIProvider> {
|
||||
return await (window as any).electron.invoke('ai:update-provider', id, updates);
|
||||
}
|
||||
|
||||
async deleteProvider(id: string): Promise<void> {
|
||||
return await (window as any).electron.invoke('ai:delete-provider', id);
|
||||
}
|
||||
|
||||
async setDefaultProvider(id: string): Promise<void> {
|
||||
return await (window as any).electron.invoke('ai:set-default-provider', id);
|
||||
}
|
||||
|
||||
async toggleProvider(id: string, enabled: boolean): Promise<void> {
|
||||
return await (window as any).electron.invoke('ai:toggle-provider', id, enabled);
|
||||
}
|
||||
|
||||
async testConnection(id: string): Promise<AIConnectionTest> {
|
||||
return await (window as any).electron.invoke('ai:test-connection', id);
|
||||
}
|
||||
|
||||
async fetchModels(id: string): Promise<string[]> {
|
||||
return await (window as any).electron.invoke('ai:fetch-models', id);
|
||||
}
|
||||
|
||||
async getSettings(): Promise<AISettings> {
|
||||
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<AISettings>): Promise<void> {
|
||||
const changes: Record<string, string | null> = {};
|
||||
|
||||
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();
|
||||
8
src/app-extracted/dist/constants.js
vendored
Normal file
8
src/app-extracted/dist/constants.js
vendored
Normal file
@@ -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=';
|
||||
55
src/app-extracted/dist/customScheme.js
vendored
Normal file
55
src/app-extracted/dist/customScheme.js
vendored
Normal file
@@ -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:<port>)
|
||||
// The authority is usually a hash of unique extension identifiers
|
||||
// like extension ID + port + project ID. An extension running on localhost:<port>
|
||||
// is then exposed on plugin://<authority>.
|
||||
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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
131
src/app-extracted/dist/ideInstall/constants.js
vendored
Normal file
131
src/app-extracted/dist/ideInstall/constants.js
vendored
Normal file
@@ -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;
|
||||
}
|
||||
29
src/app-extracted/dist/ideInstall/index.js
vendored
Normal file
29
src/app-extracted/dist/ideInstall/index.js
vendored
Normal file
@@ -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; } });
|
||||
197
src/app-extracted/dist/ideInstall/service.js
vendored
Normal file
197
src/app-extracted/dist/ideInstall/service.js
vendored
Normal file
@@ -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 */
|
||||
}
|
||||
}
|
||||
155
src/app-extracted/dist/ideInstall/wizard.js
vendored
Normal file
155
src/app-extracted/dist/ideInstall/wizard.js
vendored
Normal file
@@ -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;
|
||||
}
|
||||
286
src/app-extracted/dist/ideInstall/wizardHtml.js
vendored
Normal file
286
src/app-extracted/dist/ideInstall/wizardHtml.js
vendored
Normal file
@@ -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 `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Welcome to Antigravity</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #000000;
|
||||
--bg-secondary: #1A1A1A;
|
||||
--bg-tertiary: #242424;
|
||||
--bg-hover: #2A2A2A;
|
||||
--text-primary: #F5F5F5;
|
||||
--text-secondary: #A0A0A0;
|
||||
--text-muted: #666;
|
||||
--accent: #2F80ED;
|
||||
--accent-hover: #2D74D7;
|
||||
--border: #2A2A2A;
|
||||
--radius: 12px;
|
||||
--radius-sm: 8px;
|
||||
--transition: 200ms ease;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
-webkit-app-region: drag;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Traffic-light spacer for macOS */
|
||||
.titlebar-spacer {
|
||||
height: 38px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 68px 68px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* --- Step screens --- */
|
||||
.step {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
animation: fadeIn 0.4s ease;
|
||||
}
|
||||
.step.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Icon */
|
||||
.icon-wrapper {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.icon-wrapper img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
/* Loader styling */
|
||||
.loader {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.loader div {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--accent);
|
||||
opacity: 0.3;
|
||||
animation: dot-pulse 1.5s infinite ease-in-out;
|
||||
}
|
||||
.loader div:nth-child(1) { animation-delay: 0s; }
|
||||
.loader div:nth-child(2) { animation-delay: 0.3s; }
|
||||
.loader div:nth-child(3) { animation-delay: 0.6s; }
|
||||
|
||||
@keyframes dot-pulse {
|
||||
0%, 100% { opacity: 0.2; transform: scale(0.9); }
|
||||
50% { opacity: 0.7; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* Checkbox styling */
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
transition: color var(--transition);
|
||||
margin-bottom: 18px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.checkbox-label:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.checkbox-label input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.custom-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #333;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: var(--transition);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.checkbox-label:hover .custom-checkbox {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.checkbox-label input:checked + .custom-checkbox {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.custom-checkbox::after {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 8px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg) translate(-1px, -1px);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkbox-label input:checked + .custom-checkbox::after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 13px 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="titlebar-spacer"></div>
|
||||
<div class="container">
|
||||
|
||||
<!-- Step 0: Setting up -->
|
||||
<div id="step-setup" class="step active">
|
||||
<div class="loader">
|
||||
<div></div><div></div><div></div>
|
||||
</div>
|
||||
<div class="text" style="font-size: 13px; opacity: 0.6; letter-spacing: 0.03em;">Setting up…</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Welcome -->
|
||||
<div id="step-ask" class="step">
|
||||
<div class="icon-wrapper">
|
||||
<img src="data:image/png;base64,${iconBase64}" alt="Antigravity Icon">
|
||||
</div>
|
||||
<h1>Welcome to the new Antigravity!</h1>
|
||||
<p>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 <b>Antigravity IDE</b>.</p>
|
||||
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="chk-download" checked>
|
||||
<span class="custom-checkbox"></span>
|
||||
<span>Download the Antigravity IDE</span>
|
||||
</label>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn-primary" id="btn-skip">Explore the new Antigravity</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showStep(stepId) {
|
||||
document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
|
||||
document.getElementById(stepId).classList.add('active');
|
||||
}
|
||||
|
||||
document.getElementById('btn-skip').addEventListener('click', async () => {
|
||||
const chk = document.getElementById('chk-download');
|
||||
const shouldDownload = chk ? chk.checked : false;
|
||||
await window.wizardAPI.completeWizard(shouldDownload);
|
||||
});
|
||||
|
||||
window.wizardAPI.onSetupComplete(() => {
|
||||
showStep('step-ask');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
23
src/app-extracted/dist/ideInstall/wizardPreload.js
vendored
Normal file
23
src/app-extracted/dist/ideInstall/wizardPreload.js
vendored
Normal file
@@ -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);
|
||||
350
src/app-extracted/dist/ideInstallService.test.js
vendored
Normal file
350
src/app-extracted/dist/ideInstallService.test.js
vendored
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
259
src/app-extracted/dist/ipcHandlers.js
vendored
Normal file
259
src/app-extracted/dist/ipcHandlers.js
vendored
Normal file
@@ -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);
|
||||
});
|
||||
}
|
||||
88
src/app-extracted/dist/ipcHandlers.test.js
vendored
Normal file
88
src/app-extracted/dist/ipcHandlers.test.js
vendored
Normal file
@@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
19
src/app-extracted/dist/keybindings.js
vendored
Normal file
19
src/app-extracted/dist/keybindings.js
vendored
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
428
src/app-extracted/dist/languageServer.js
vendored
Normal file
428
src/app-extracted/dist/languageServer.js
vendored
Normal file
@@ -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 <proto> port at <N> 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
|
||||
}
|
||||
});
|
||||
}
|
||||
81
src/app-extracted/dist/languageServer.test.js
vendored
Normal file
81
src/app-extracted/dist/languageServer.test.js
vendored
Normal file
@@ -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('');
|
||||
});
|
||||
});
|
||||
100
src/app-extracted/dist/loadingOverlay.js
vendored
Normal file
100
src/app-extracted/dist/loadingOverlay.js
vendored
Normal file
@@ -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 `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: ${backgroundColor};
|
||||
color: ${foregroundColor};
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
-webkit-app-region: drag;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.loader {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.loader div {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: ${foregroundColor};
|
||||
opacity: 0.3;
|
||||
animation: dot-pulse 1.5s infinite ease-in-out;
|
||||
}
|
||||
.loader div:nth-child(1) { animation-delay: 0s; }
|
||||
.loader div:nth-child(2) { animation-delay: 0.3s; }
|
||||
.loader div:nth-child(3) { animation-delay: 0.6s; }
|
||||
.text {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.03em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
@keyframes dot-pulse {
|
||||
0%, 100% { opacity: 0.2; transform: scale(0.9); }
|
||||
50% { opacity: 0.7; transform: scale(1.1); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="loader">
|
||||
<div></div><div></div><div></div>
|
||||
</div>
|
||||
<div class="text">Loading Antigravity</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
455
src/app-extracted/dist/main.js
vendored
Normal file
455
src/app-extracted/dist/main.js
vendored
Normal file
@@ -0,0 +1,455 @@
|
||||
"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');
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// AI Provider Setup Functions
|
||||
// ---------------------------------------------------------------------------
|
||||
let providerSetupWindow = null;
|
||||
|
||||
async function checkAIProviderSetup() {
|
||||
try {
|
||||
// Check if any provider is configured
|
||||
const items = await storageManager.getItems();
|
||||
const hasProvider = items['aiProvider'] && items['aiModel'];
|
||||
const hasProvidersConfig = items['aiProviders'];
|
||||
|
||||
// Return true if setup is complete
|
||||
return hasProvider || hasProvidersConfig;
|
||||
} catch (error) {
|
||||
console.error('Error checking AI provider setup:', error);
|
||||
return true; // Skip wizard if error
|
||||
}
|
||||
}
|
||||
|
||||
async function showProviderSetupWizard() {
|
||||
return new Promise((resolve) => {
|
||||
const wizardUrl = `file://${__dirname}/provider-setup-wizard.html`;
|
||||
|
||||
providerSetupWindow = new electron_1.BrowserWindow({
|
||||
width: 1000,
|
||||
height: 800,
|
||||
title: 'Antigravity - AI Provider Setup',
|
||||
icon: `${__dirname}/icon.png`,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: `${__dirname}/preload.js`,
|
||||
},
|
||||
});
|
||||
|
||||
providerSetupWindow.loadFile(`${__dirname}/provider-setup-wizard.html`);
|
||||
|
||||
// Handle setup completion
|
||||
providerSetupWindow.webContents.on('did-finish-load', () => {
|
||||
console.log('Provider setup wizard loaded');
|
||||
});
|
||||
|
||||
// Listen for setup completion message
|
||||
electron_1.ipcMain.once('provider-setup:complete', (event, data) => {
|
||||
console.log('Provider setup completed:', data);
|
||||
if (providerSetupWindow) {
|
||||
providerSetupWindow.close();
|
||||
providerSetupWindow = null;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Listen for setup cancelled
|
||||
electron_1.ipcMain.once('provider-setup:cancelled', () => {
|
||||
console.log('Provider setup cancelled');
|
||||
if (providerSetupWindow) {
|
||||
providerSetupWindow.close();
|
||||
providerSetupWindow = null;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
providerSetupWindow.on('closed', () => {
|
||||
providerSetupWindow = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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;
|
||||
});
|
||||
|
||||
// Check if AI providers are configured - show wizard if not
|
||||
const aiProviderSetupComplete = await checkAIProviderSetup();
|
||||
if (!aiProviderSetupComplete && !HEADLESS) {
|
||||
await showProviderSetupWizard();
|
||||
return; // Wait for wizard to complete
|
||||
}
|
||||
// Handle requests coming from custom schemes
|
||||
(0, customScheme_1.registerCustomSchemeHandlers)();
|
||||
|
||||
// Handler for launching main app after provider setup
|
||||
electron_1.ipcMain.handle('app:launch-main', async () => {
|
||||
// Resume the app startup
|
||||
hasStartedMainApplication = false;
|
||||
// Restart the initialization process
|
||||
await startMainApplication();
|
||||
});
|
||||
/**
|
||||
* Main application startup function
|
||||
*/
|
||||
async function startMainApplication() {
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
200
src/app-extracted/dist/main.test.js
vendored
Normal file
200
src/app-extracted/dist/main.test.js
vendored
Normal file
@@ -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}/`);
|
||||
});
|
||||
});
|
||||
59
src/app-extracted/dist/menu.js
vendored
Normal file
59
src/app-extracted/dist/menu.js
vendored
Normal file
@@ -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);
|
||||
}
|
||||
49
src/app-extracted/dist/paths.js
vendored
Normal file
49
src/app-extracted/dist/paths.js
vendored
Normal file
@@ -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');
|
||||
109
src/app-extracted/dist/preload.js
vendored
Normal file
109
src/app-extracted/dist/preload.js
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
"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),
|
||||
};
|
||||
|
||||
const appAPI = {
|
||||
launchMain: () => electron_1.ipcRenderer.invoke('app:launch-main'),
|
||||
};
|
||||
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);
|
||||
electron_1.contextBridge.exposeInMainWorld('app', appAPI);
|
||||
812
src/app-extracted/dist/provider-setup-wizard.html
vendored
Normal file
812
src/app-extracted/dist/provider-setup-wizard.html
vendored
Normal file
@@ -0,0 +1,812 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Antigravity - AI Provider Setup</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 32px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.step.active {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.step.active .step-number {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step.completed .step-number {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.options-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.option-card {
|
||||
border: 3px solid #e0e0e0;
|
||||
border-radius: 16px;
|
||||
padding: 28px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.2);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.option-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.option-description {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.option-features {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.feature-item::before {
|
||||
content: "✓";
|
||||
color: #4caf50;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 14px 32px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.action-buttons .btn-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.provider-form {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
padding: 28px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.preset-section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.preset-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.preset-card {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.preset-card:hover {
|
||||
border-color: #667eea;
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
}
|
||||
|
||||
.preset-card.selected {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||||
}
|
||||
|
||||
.preset-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.preset-models {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.success-screen {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.success-description {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-content p {
|
||||
margin-top: 16px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading-overlay" class="loading-overlay hidden">
|
||||
<div class="loading-content">
|
||||
<div class="spinner"></div>
|
||||
<p id="loading-message">Setting up...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🤖 Welcome to Antigravity</h1>
|
||||
<p>Choose how you want to connect to AI providers</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Step 1: Choose Setup Method -->
|
||||
<div id="step1" class="setup-step">
|
||||
<div class="step-indicator">
|
||||
<div class="step active">
|
||||
<div class="step-number">1</div>
|
||||
<span>Choose Setup</span>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">2</div>
|
||||
<span>Configure</span>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<span>Complete</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-grid">
|
||||
<!-- Google OAuth -->
|
||||
<div class="option-card" onclick="selectOption('google-oauth')" data-option="google-oauth">
|
||||
<div class="option-icon">🔐</div>
|
||||
<div class="option-title">Google OAuth</div>
|
||||
<div class="option-description">
|
||||
Sign in with your Google account to access Google AI services including Gemini and Antigravity models.
|
||||
</div>
|
||||
<div class="option-features">
|
||||
<div class="feature-item">Easy one-click setup</div>
|
||||
<div class="feature-item">Access to Gemini models</div>
|
||||
<div class="feature-item">Antigravity-specific models</div>
|
||||
<div class="feature-item">No API key needed</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Provider -->
|
||||
<div class="option-card" onclick="selectOption('custom-provider')" data-option="custom-provider">
|
||||
<div class="option-icon">⚙️</div>
|
||||
<div class="option-title">Custom Provider</div>
|
||||
<div class="option-description">
|
||||
Use your own API key to connect to any AI provider. Choose from 17+ pre-configured providers.
|
||||
</div>
|
||||
<div class="option-features">
|
||||
<div class="feature-item">17+ provider presets</div>
|
||||
<div class="feature-item">Bring your own API key</div>
|
||||
<div class="feature-item">Full control over settings</div>
|
||||
<div class="feature-item">OpenAI, Anthropic, and more</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<div></div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" id="next-btn" disabled onclick="goToStep2()">
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Configure Provider -->
|
||||
<div id="step2" class="setup-step hidden">
|
||||
<div class="step-indicator">
|
||||
<div class="step completed">
|
||||
<div class="step-number">✓</div>
|
||||
<span>Choose Setup</span>
|
||||
</div>
|
||||
<div class="step active">
|
||||
<div class="step-number">2</div>
|
||||
<span>Configure</span>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<span>Complete</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Google OAuth Configuration -->
|
||||
<div id="google-config" class="hidden">
|
||||
<h2 style="font-size: 24px; color: #333; margin-bottom: 20px;">🔐 Google Account Setup</h2>
|
||||
<p style="font-size: 16px; color: #666; margin-bottom: 30px;">
|
||||
Click the button below to sign in with your Google account. You'll be redirected to Google's OAuth page.
|
||||
</p>
|
||||
|
||||
<div style="text-align: center; margin: 40px 0;">
|
||||
<button class="btn btn-primary" style="font-size: 18px; padding: 16px 40px;" onclick="startGoogleOAuth()">
|
||||
🔐 Sign in with Google
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="background: #f0f7ff; border-left: 4px solid #667eea; padding: 16px; border-radius: 4px; margin: 20px 0;">
|
||||
<strong>What you'll get:</strong>
|
||||
<ul style="margin: 12px 0 0 20px; line-height: 1.8;">
|
||||
<li>Access to Google Gemini models</li>
|
||||
<li>Antigravity-specific models (antigravity-gemini-3-flash, etc.)</li>
|
||||
<li>Claude Sonnet via Antigravity</li>
|
||||
<li>And more...</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Provider Configuration -->
|
||||
<div id="custom-config" class="hidden">
|
||||
<h2 style="font-size: 24px; color: #333; margin-bottom: 20px;">⚙️ Custom Provider Setup</h2>
|
||||
|
||||
<div class="preset-section">
|
||||
<label class="form-label">Choose a Provider Preset</label>
|
||||
<div class="preset-grid" id="preset-grid">
|
||||
<!-- Presets will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="provider-form" id="provider-form" style="margin-top: 24px;">
|
||||
<div class="form-title" id="form-title">Configure Provider</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Provider Name</label>
|
||||
<input type="text" id="provider-name" class="form-input" placeholder="My Custom Provider">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">API Type</label>
|
||||
<select id="provider-type" class="form-select">
|
||||
<option value="openai">OpenAI (GPT-4, GPT-3.5)</option>
|
||||
<option value="anthropic">Anthropic (Claude)</option>
|
||||
<option value="google_gemini">Google Gemini</option>
|
||||
<option value="google_antigravity">Google Antigravity</option>
|
||||
<option value="ollama">Ollama (Local)</option>
|
||||
<option value="groq">Groq</option>
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">API Endpoint</label>
|
||||
<input type="text" id="provider-endpoint" class="form-input" placeholder="https://api.openai.com/v1">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">API Key</label>
|
||||
<input type="password" id="provider-apikey" class="form-input" placeholder="sk-...">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Default Model</label>
|
||||
<input type="text" id="provider-model" class="form-input" placeholder="gpt-4o">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-secondary" onclick="goToStep1()">
|
||||
← Back
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" id="setup-btn" onclick="completeSetup()">
|
||||
Complete Setup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Success -->
|
||||
<div id="step3" class="setup-step hidden">
|
||||
<div class="success-screen">
|
||||
<div class="success-icon">🎉</div>
|
||||
<div class="success-title">Setup Complete!</div>
|
||||
<div class="success-description">
|
||||
Antigravity is ready to use with your AI provider.
|
||||
</div>
|
||||
<button class="btn btn-primary" style="font-size: 18px; padding: 16px 40px;" onclick="launchAntigravity()">
|
||||
🚀 Launch Antigravity
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let selectedOption = null;
|
||||
let selectedPreset = null;
|
||||
let providers = [];
|
||||
|
||||
// Initialize
|
||||
async function init() {
|
||||
try {
|
||||
// Load available presets
|
||||
const presets = await window.electron.invoke('ai:get-available-presets');
|
||||
renderPresets(presets);
|
||||
|
||||
// Load existing providers
|
||||
providers = await window.electron.invoke('ai:get-providers');
|
||||
} catch (error) {
|
||||
console.error('Error initializing:', error);
|
||||
showError('Failed to initialize. Please restart the application.');
|
||||
}
|
||||
}
|
||||
|
||||
function selectOption(option) {
|
||||
selectedOption = option;
|
||||
|
||||
// Update UI
|
||||
document.querySelectorAll('.option-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
document.querySelector(`[data-option="${option}"]`).classList.add('selected');
|
||||
|
||||
// Enable next button
|
||||
document.getElementById('next-btn').disabled = false;
|
||||
}
|
||||
|
||||
function goToStep1() {
|
||||
document.querySelectorAll('.setup-step').forEach(step => step.classList.add('hidden'));
|
||||
document.getElementById('step1').classList.remove('hidden');
|
||||
updateStepIndicators(1);
|
||||
}
|
||||
|
||||
function goToStep2() {
|
||||
if (!selectedOption) return;
|
||||
|
||||
document.querySelectorAll('.setup-step').forEach(step => step.classList.add('hidden'));
|
||||
document.getElementById('step2').classList.remove('hidden');
|
||||
updateStepIndicators(2);
|
||||
|
||||
// Show appropriate config
|
||||
document.getElementById('google-config').classList.toggle('hidden', selectedOption !== 'google-oauth');
|
||||
document.getElementById('custom-config').classList.toggle('hidden', selectedOption !== 'custom-provider');
|
||||
}
|
||||
|
||||
function goToStep3() {
|
||||
document.querySelectorAll('.setup-step').forEach(step => step.classList.add('hidden'));
|
||||
document.getElementById('step3').classList.remove('hidden');
|
||||
updateStepIndicators(3);
|
||||
}
|
||||
|
||||
function updateStepIndicators(currentStep) {
|
||||
document.querySelectorAll('.step').forEach((step, index) => {
|
||||
step.classList.remove('active', 'completed');
|
||||
if (index + 1 < currentStep) {
|
||||
step.classList.add('completed');
|
||||
step.querySelector('.step-number').textContent = '✓';
|
||||
} else if (index + 1 === currentStep) {
|
||||
step.classList.add('active');
|
||||
step.querySelector('.step-number').textContent = index + 1;
|
||||
} else {
|
||||
step.querySelector('.step-number').textContent = index + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderPresets(presets) {
|
||||
const grid = document.getElementById('preset-grid');
|
||||
|
||||
// Group presets by category
|
||||
const categories = {
|
||||
'Google': presets.filter(p => p.toLowerCase().includes('google') || p.toLowerCase().includes('gemini') || p.toLowerCase().includes('antigravity')),
|
||||
'OpenAI': presets.filter(p => p.toLowerCase().includes('openai')),
|
||||
'Anthropic': presets.filter(p => p.toLowerCase().includes('anthropic')),
|
||||
'OpenRouter': presets.filter(p => p.toLowerCase().includes('router')),
|
||||
'Other': presets.filter(p =>
|
||||
!p.toLowerCase().includes('google') &&
|
||||
!p.toLowerCase().includes('gemini') &&
|
||||
!p.toLowerCase().includes('antigravity') &&
|
||||
!p.toLowerCase().includes('openai') &&
|
||||
!p.toLowerCase().includes('anthropic') &&
|
||||
!p.toLowerCase().includes('router')
|
||||
)
|
||||
};
|
||||
|
||||
grid.innerHTML = Object.entries(categories)
|
||||
.filter(([name, items]) => items.length > 0)
|
||||
.map(([category, items]) => `
|
||||
${items.map(preset => `
|
||||
<div class="preset-card" onclick="selectPreset('${preset}')" data-preset="${preset}">
|
||||
<div class="preset-name">${preset}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function selectPreset(presetName) {
|
||||
selectedPreset = presetName;
|
||||
|
||||
// Update UI
|
||||
document.querySelectorAll('.preset-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
document.querySelector(`[data-preset="${presetName}"]`).classList.add('selected');
|
||||
|
||||
// Get preset details
|
||||
window.electron.invoke('ai:get-preset', presetName).then(preset => {
|
||||
if (preset) {
|
||||
document.getElementById('form-title').textContent = `Configure: ${presetName}`;
|
||||
document.getElementById('provider-name').value = presetName;
|
||||
document.getElementById('provider-type').value = preset.type || 'custom';
|
||||
document.getElementById('provider-endpoint').value = preset.endpoint || '';
|
||||
|
||||
if (preset.models && preset.models.length > 0) {
|
||||
document.getElementById('provider-model').value = preset.models[0];
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('Error loading preset:', err);
|
||||
});
|
||||
}
|
||||
|
||||
function startGoogleOAuth() {
|
||||
showLoading('Starting Google OAuth flow...');
|
||||
|
||||
// Add Google Antigravity OAuth provider
|
||||
window.electron.invoke('ai:add-provider', {
|
||||
name: 'Google Antigravity (OAuth)',
|
||||
type: 'google_antigravity',
|
||||
endpoint: 'https://daily-cloudcode-pa.sandbox.googleapis.com',
|
||||
apiKey: 'oauth',
|
||||
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'
|
||||
],
|
||||
defaultModel: 'antigravity-gemini-3-flash',
|
||||
capabilities: ['chat', 'vision', 'tool_use', 'streaming']
|
||||
}).then(() => {
|
||||
hideLoading();
|
||||
showSuccess('Google Antigravity OAuth configured successfully!');
|
||||
setTimeout(() => goToStep3(), 1500);
|
||||
}).catch(err => {
|
||||
hideLoading();
|
||||
showError('Failed to configure Google OAuth: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
async function completeSetup() {
|
||||
if (selectedOption === 'google-oauth') {
|
||||
startGoogleOAuth();
|
||||
return;
|
||||
}
|
||||
|
||||
// Custom provider setup
|
||||
const name = document.getElementById('provider-name').value.trim();
|
||||
const endpoint = document.getElementById('provider-endpoint').value.trim();
|
||||
const apiKey = document.getElementById('provider-apikey').value.trim();
|
||||
const model = document.getElementById('provider-model').value.trim();
|
||||
const type = document.getElementById('provider-type').value;
|
||||
|
||||
if (!name || !endpoint) {
|
||||
showError('Please fill in all required fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading('Setting up your provider...');
|
||||
|
||||
try {
|
||||
await window.electron.invoke('ai:add-provider', {
|
||||
name: name,
|
||||
type: type,
|
||||
endpoint: endpoint,
|
||||
apiKey: apiKey,
|
||||
models: model ? [model] : [],
|
||||
defaultModel: model,
|
||||
capabilities: ['chat', 'streaming']
|
||||
});
|
||||
|
||||
// Set as default provider
|
||||
const providers = await window.electron.invoke('ai:get-providers');
|
||||
const newProvider = providers[providers.length - 1];
|
||||
|
||||
if (newProvider) {
|
||||
await window.electron.invoke('ai:set-default-provider', newProvider.id);
|
||||
|
||||
// Update settings
|
||||
await window.electron.invoke('storage:update-items', {
|
||||
'aiProvider': newProvider.id,
|
||||
'aiModel': model || 'gpt-4o',
|
||||
'aiTemperature': '0.7',
|
||||
'aiMaxTokens': '4096',
|
||||
'aiStreaming': 'true'
|
||||
});
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
showSuccess('Provider configured successfully!');
|
||||
setTimeout(() => goToStep3(), 1500);
|
||||
} catch (error) {
|
||||
hideLoading();
|
||||
showError('Failed to setup provider: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function launchAntigravity() {
|
||||
showLoading('Launching Antigravity...');
|
||||
|
||||
// Close this window and launch main app
|
||||
window.electron.invoke('app:launch-main').then(() => {
|
||||
window.close();
|
||||
}).catch(err => {
|
||||
hideLoading();
|
||||
showError('Failed to launch Antigravity: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
function showLoading(message) {
|
||||
document.getElementById('loading-message').textContent = message;
|
||||
document.getElementById('loading-overlay').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loading-overlay').classList.add('hidden');
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
alert('Error: ' + message);
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
alert('Success: ' + message);
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
519
src/app-extracted/dist/services/aiProviderService.js
vendored
Normal file
519
src/app-extracted/dist/services/aiProviderService.js
vendored
Normal file
@@ -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;
|
||||
62
src/app-extracted/dist/services/settingsService.js
vendored
Normal file
62
src/app-extracted/dist/services/settingsService.js
vendored
Normal file
@@ -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;
|
||||
65
src/app-extracted/dist/services/settingsService.test.js
vendored
Normal file
65
src/app-extracted/dist/services/settingsService.test.js
vendored
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
128
src/app-extracted/dist/storage.js
vendored
Normal file
128
src/app-extracted/dist/storage.js
vendored
Normal file
@@ -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;
|
||||
142
src/app-extracted/dist/storage.test.js
vendored
Normal file
142
src/app-extracted/dist/storage.test.js
vendored
Normal file
@@ -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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
23
src/app-extracted/dist/test/helpers.js
vendored
Normal file
23
src/app-extracted/dist/test/helpers.js
vendored
Normal file
@@ -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(() => { });
|
||||
}
|
||||
79
src/app-extracted/dist/tray.js
vendored
Normal file
79
src/app-extracted/dist/tray.js
vendored
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/app-extracted/dist/tray.test.js
vendored
Normal file
87
src/app-extracted/dist/tray.test.js
vendored
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
2
src/app-extracted/dist/types.js
vendored
Normal file
2
src/app-extracted/dist/types.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
241
src/app-extracted/dist/updater.js
vendored
Normal file
241
src/app-extracted/dist/updater.js
vendored
Normal file
@@ -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();
|
||||
}
|
||||
91
src/app-extracted/dist/updater.test.js
vendored
Normal file
91
src/app-extracted/dist/updater.test.js
vendored
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
269
src/app-extracted/dist/utils.js
vendored
Normal file
269
src/app-extracted/dist/utils.js
vendored
Normal file
@@ -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}`);
|
||||
}
|
||||
}
|
||||
73
src/app-extracted/dist/utils.test.js
vendored
Normal file
73
src/app-extracted/dist/utils.test.js
vendored
Normal file
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
BIN
src/app-extracted/icon.png
Normal file
BIN
src/app-extracted/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
18
src/app-extracted/package.json
Normal file
18
src/app-extracted/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
BIN
src/app-extracted/trayTemplate.png
Normal file
BIN
src/app-extracted/trayTemplate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 355 B |
BIN
src/app-extracted/trayTemplate@2x.png
Normal file
BIN
src/app-extracted/trayTemplate@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 651 B |
BIN
src/app.asar
Normal file
BIN
src/app.asar
Normal file
Binary file not shown.
Reference in New Issue
Block a user