Complete source code - AI Provider Edition v2.0.1
Added complete source code and pre-compiled application: Source Code: - app.asar (compiled Electron app) - app-extracted/ (all extracted source files) - dist/services/aiProviderService.js - dist/services/settingsService.js - dist/ipcHandlers.js - dist/aiProviderAPI.ts - dist/ai-provider-settings.html - And all other application files Build Tools: - scripts/extract-app.sh - scripts/repack-app.sh - scripts/build-deb.sh - scripts/install-deb.sh Documentation: - SOURCE_CODE.md (repository structure) - BUILD.md (build instructions) - README.md (overview) - docs/ (complete API docs) - AI_PROVIDER_SPECIFICATION.md - AI_PROVIDER_README.md - IMPLEMENTATION_SUMMARY.md Features included: - 17+ AI provider presets - One-click provider setup - Model auto-fetching - Connection testing - Modern GUI interface - Complete IPC handlers (14 new) - TypeScript API wrapper - Persistent settings Ready to build and customize!
This commit is contained in:
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!** 🚀
|
||||||
284
README.md
284
README.md
@@ -4,252 +4,116 @@
|
|||||||
|
|
||||||
**Antigravity IDE with 17+ Pre-configured AI Provider Presets**
|
**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
|
**Option 1: Pre-built Package (Recommended)**
|
||||||
- ✅ **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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Download the .deb file
|
# 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
|
sudo dpkg -i antigravity_2.0.1-ai-providers-1_amd64.deb
|
||||||
|
|
||||||
# Launch
|
# Launch
|
||||||
antigravity
|
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
|
- GTK3 support
|
||||||
- amd64 (x86_64) architecture
|
- ~500MB disk space
|
||||||
|
|
||||||
## 💡 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
|
|
||||||
|
|
||||||
## 📦 Package Contents
|
## 📦 Package Contents
|
||||||
|
|
||||||
```
|
```
|
||||||
antigravity_2.0.1-ai-providers-1_amd64.deb
|
antigravity_2.0.1-ai-providers-1_amd64.deb
|
||||||
├── opt/antigravity/ # Main application
|
├── opt/antigravity/ # Main application
|
||||||
│ ├── antigravity # Main executable
|
│ ├── antigravity # Executable
|
||||||
│ ├── resources/ # Application resources
|
│ ├── resources/ # App resources & app.asar
|
||||||
│ ├── lib*/ # Libraries
|
│ └── [libraries]
|
||||||
│ └── locales/ # Language files
|
├── DEBIAN/ # Package metadata
|
||||||
├── DEBIAN/ # Package metadata
|
|
||||||
│ ├── control
|
│ ├── control
|
||||||
│ ├── preinst
|
│ ├── preinst
|
||||||
│ ├── postinst
|
│ ├── postinst
|
||||||
│ ├── prerm
|
│ ├── prerm
|
||||||
│ └── postrm
|
│ └── postrm
|
||||||
└── usr/share/applications/ # Desktop entry
|
└── usr/share/applications/ # Desktop entry
|
||||||
└── antigravity.desktop
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Features
|
## 🤝 Contributing
|
||||||
|
|
||||||
### Backend Features
|
1. Fork the repository
|
||||||
- ✅ AIProviderService for provider management
|
2. Create feature branch
|
||||||
- ✅ 14 new IPC handlers for AI operations
|
3. Make changes
|
||||||
- ✅ Preset-based provider creation
|
4. Submit pull request
|
||||||
- ✅ 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)
|
|
||||||
|
|
||||||
## 📞 Support
|
## 📞 Support
|
||||||
|
|
||||||
For issues or questions:
|
- **Issues**: https://github.rommark.dev/admin/antigravity-ai-providers/issues
|
||||||
1. Check existing issues in this repository
|
- **Discussions**: https://github.rommark.dev/admin/antigravity-ai-providers/discussions
|
||||||
2. Create new issue with details
|
|
||||||
3. Include logs if reporting bugs
|
## 🙏 Acknowledgments
|
||||||
4. Specify your provider and model
|
|
||||||
|
- Antigravity IDE - Original application
|
||||||
|
- Codex Launcher - Provider presets
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
Same as original Antigravity application.
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
365
src/app-extracted/dist/main.js
vendored
Normal file
365
src/app-extracted/dist/main.js
vendored
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || (function () {
|
||||||
|
var ownKeys = function(o) {
|
||||||
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||||
|
var ar = [];
|
||||||
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
return ownKeys(o);
|
||||||
|
};
|
||||||
|
return function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const electron_1 = require("electron");
|
||||||
|
const main_1 = __importDefault(require("electron-log/main"));
|
||||||
|
const ipcHandlers_1 = require("./ipcHandlers");
|
||||||
|
const fs = __importStar(require("fs"));
|
||||||
|
const crypto = __importStar(require("crypto"));
|
||||||
|
const readline = __importStar(require("readline"));
|
||||||
|
const utils_1 = require("./utils");
|
||||||
|
const languageServer_1 = require("./languageServer");
|
||||||
|
const updater_1 = require("./updater");
|
||||||
|
const constants_1 = require("./constants");
|
||||||
|
const tray_1 = require("./tray");
|
||||||
|
const storage_1 = require("./storage");
|
||||||
|
const paths_1 = require("./paths");
|
||||||
|
const menu_1 = require("./menu");
|
||||||
|
const customScheme_1 = require("./customScheme");
|
||||||
|
const settingsService_1 = require("./services/settingsService");
|
||||||
|
const ideInstall_1 = require("./ideInstall");
|
||||||
|
const gotTheLock = electron_1.app.requestSingleInstanceLock();
|
||||||
|
if (!gotTheLock) {
|
||||||
|
electron_1.app.quit();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// State
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
let storageManager;
|
||||||
|
let settingsService;
|
||||||
|
let hasStartedMainApplication = false;
|
||||||
|
let isQuitting = false;
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Config
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Driven by ELECTRON_OZONE_PLATFORM_HINT=headless env var.
|
||||||
|
// This single env var both prevents GTK from crashing (Electron 33+)
|
||||||
|
// and tells our code to skip createWindow().
|
||||||
|
const HEADLESS = process.env.ELECTRON_OZONE_PLATFORM_HINT === 'headless';
|
||||||
|
// When set, skip LS startup and load this URL directly (for dev iteration).
|
||||||
|
const DEV_URL = process.env.DEV_URL;
|
||||||
|
if (HEADLESS) {
|
||||||
|
electron_1.app.commandLine.appendSwitch('ozone-platform', 'headless');
|
||||||
|
electron_1.app.commandLine.appendSwitch('headless');
|
||||||
|
electron_1.app.commandLine.appendSwitch('disable-gpu');
|
||||||
|
electron_1.app.commandLine.appendSwitch('no-sandbox');
|
||||||
|
}
|
||||||
|
if (!electron_1.app.commandLine.hasSwitch('remote-debugging-port')) {
|
||||||
|
electron_1.app.commandLine.appendSwitch('remote-debugging-port', '0');
|
||||||
|
}
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Application Lifecycle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
let pendingDeepLink = null;
|
||||||
|
function handleDeepLink(url) {
|
||||||
|
const wins = electron_1.BrowserWindow.getAllWindows();
|
||||||
|
// This block handles deep links when windows are already open.
|
||||||
|
if (wins.length > 0) {
|
||||||
|
if (wins[0].isMinimized()) {
|
||||||
|
wins[0].restore();
|
||||||
|
}
|
||||||
|
wins[0].show();
|
||||||
|
wins[0].focus();
|
||||||
|
electron_1.app.focus({ steal: true });
|
||||||
|
wins[0].webContents.send('deep-link', url);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pendingDeepLink = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
electron_1.app.on('second-instance', (event, commandLine) => {
|
||||||
|
const wins = electron_1.BrowserWindow.getAllWindows();
|
||||||
|
if (wins.length > 0) {
|
||||||
|
if (wins[0].isMinimized()) {
|
||||||
|
wins[0].restore();
|
||||||
|
}
|
||||||
|
wins[0].show();
|
||||||
|
wins[0].focus();
|
||||||
|
electron_1.app.focus({ steal: true });
|
||||||
|
}
|
||||||
|
const url = commandLine.find((arg) => arg.startsWith('antigravity://'));
|
||||||
|
if (url) {
|
||||||
|
handleDeepLink(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(0, customScheme_1.registerCustomSchemes)();
|
||||||
|
// Register as default protocol client for deep linking
|
||||||
|
const PROTOCOL = 'antigravity';
|
||||||
|
if (!electron_1.app.isDefaultProtocolClient(PROTOCOL)) {
|
||||||
|
electron_1.app.setAsDefaultProtocolClient(PROTOCOL);
|
||||||
|
}
|
||||||
|
electron_1.app.on('open-url', (event, url) => {
|
||||||
|
event.preventDefault();
|
||||||
|
handleDeepLink(url);
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* App entry point. Runs once Electron has finished initializing.
|
||||||
|
* Validates the LS binary, frees the port if needed, spawns the LS,
|
||||||
|
* and opens the initial browser window.
|
||||||
|
*/
|
||||||
|
electron_1.app
|
||||||
|
.whenReady()
|
||||||
|
.then(async () => {
|
||||||
|
// Initialize electron-log and override console
|
||||||
|
main_1.default.initialize();
|
||||||
|
Object.assign(console, main_1.default.functions);
|
||||||
|
const storagePath = (0, paths_1.getAppStoragePath)();
|
||||||
|
storageManager = new storage_1.StorageManager(storagePath, settingsService_1.DEFAULTS);
|
||||||
|
settingsService = new settingsService_1.SettingsService(storageManager);
|
||||||
|
// Handle deep link URL from command line arguments (All platforms)
|
||||||
|
const deepLinkFromArg = process.argv.find((arg) => arg.startsWith('antigravity://'));
|
||||||
|
if (deepLinkFromArg) {
|
||||||
|
console.log('Launched with deep link:', deepLinkFromArg);
|
||||||
|
pendingDeepLink = deepLinkFromArg;
|
||||||
|
}
|
||||||
|
// Register IPC handlers
|
||||||
|
(0, ipcHandlers_1.registerIpcHandlers)(storageManager);
|
||||||
|
electron_1.ipcMain.handle('deep-link:get-stored', () => {
|
||||||
|
const link = pendingDeepLink;
|
||||||
|
pendingDeepLink = null; // Clear after read
|
||||||
|
return link;
|
||||||
|
});
|
||||||
|
// Handle requests coming from custom schemes
|
||||||
|
(0, customScheme_1.registerCustomSchemeHandlers)();
|
||||||
|
// Set About panel options with LS CL
|
||||||
|
const cl = await (0, languageServer_1.getLsCL)();
|
||||||
|
electron_1.app.setAboutPanelOptions({
|
||||||
|
applicationName: 'Antigravity',
|
||||||
|
applicationVersion: electron_1.app.getVersion(),
|
||||||
|
version: cl || undefined,
|
||||||
|
});
|
||||||
|
// Pre-onboarding: check if we should offer to re-install the IDE.
|
||||||
|
// This runs before the LS starts so we can show a standalone wizard.
|
||||||
|
if (!HEADLESS) {
|
||||||
|
await (0, ideInstall_1.maybeShowIdeInstallWizard)(storageManager);
|
||||||
|
}
|
||||||
|
if (DEV_URL) {
|
||||||
|
console.log('Starting in dev mode with URL:', DEV_URL);
|
||||||
|
(0, utils_1.createWindow)(DEV_URL);
|
||||||
|
hasStartedMainApplication = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(languageServer_1.LS_BINARY)) {
|
||||||
|
const msg = `language_server binary not found at:\n${languageServer_1.LS_BINARY}\n\nPlease build set a valid location.`;
|
||||||
|
if (HEADLESS) {
|
||||||
|
console.error('ERROR:', msg);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await electron_1.dialog.showErrorBox('Binary not found', msg);
|
||||||
|
}
|
||||||
|
electron_1.app.quit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const csrf = crypto.randomUUID();
|
||||||
|
console.log(`Starting app (v${electron_1.app.getVersion()}) with dynamic port…`);
|
||||||
|
let handle;
|
||||||
|
const targetPort = Number(process.env.JETSKI_LS_PORT) || constants_1.DYNAMIC_PORT;
|
||||||
|
try {
|
||||||
|
handle = await (0, languageServer_1.startAndMonitorLanguageServer)(targetPort, csrf, {
|
||||||
|
headless: HEADLESS,
|
||||||
|
onPortChanged: (newPort) => {
|
||||||
|
const newUrl = `${constants_1.WINDOW_ORIGIN}:${newPort}/`;
|
||||||
|
console.log(`[Auto-Restart] Port changed! Reloading all windows with URL: ${newUrl}`);
|
||||||
|
// Apply cert trust
|
||||||
|
(0, languageServer_1.setupLocalCertTrust)();
|
||||||
|
if (!HEADLESS) {
|
||||||
|
const windows = electron_1.BrowserWindow.getAllWindows();
|
||||||
|
for (const win of windows) {
|
||||||
|
void win.loadURL(newUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
const msg = err.message;
|
||||||
|
if (HEADLESS) {
|
||||||
|
console.error('Startup failed:', msg);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await electron_1.dialog.showErrorBox('Startup failed', msg);
|
||||||
|
}
|
||||||
|
electron_1.app.quit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `${constants_1.WINDOW_ORIGIN}:${handle.port}/`;
|
||||||
|
console.log('\n' + '='.repeat(60));
|
||||||
|
console.log(` Local: ${url}`);
|
||||||
|
console.log(` LS Logs: ${(0, paths_1.getLsLogPath)()}`);
|
||||||
|
console.log(` Electron Logs: ${main_1.default.transports.file.getFile().path}`);
|
||||||
|
console.log('='.repeat(60) + '\n');
|
||||||
|
if (HEADLESS) {
|
||||||
|
// In headless mode, forward stdin to the Language Server to allow interaction via terminal.
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
rl.on('line', (line) => {
|
||||||
|
const lsProc = (0, languageServer_1.getLsProcess)();
|
||||||
|
if (lsProc && lsProc.stdin) {
|
||||||
|
lsProc.stdin.write(line + '\n');
|
||||||
|
console.log('-> Forwarded input to Language Server.');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('Language Server process is not running.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Initial window — opened once after the LS has successfully started.
|
||||||
|
if (!HEADLESS) {
|
||||||
|
(0, menu_1.setupApplicationMenu)(url);
|
||||||
|
(0, utils_1.createWindow)(url);
|
||||||
|
if (electron_1.app.dock) {
|
||||||
|
const dockMenu = electron_1.Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: 'New Window',
|
||||||
|
click: () => (0, utils_1.createWindow)(url),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
electron_1.app.dock.setMenu(dockMenu);
|
||||||
|
}
|
||||||
|
(0, tray_1.createTray)([
|
||||||
|
{
|
||||||
|
id: 'running-agents',
|
||||||
|
label: 'No agents running',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: `Open ${electron_1.app.getName()}`,
|
||||||
|
click: () => (0, utils_1.showOrCreateWindow)((0, languageServer_1.getLsPort)()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Quit',
|
||||||
|
click: () => {
|
||||||
|
// Triggers 'before-quit' to run graceful cleanup without confirmation.
|
||||||
|
electron_1.app.quit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// Start checking for app updates.
|
||||||
|
(0, updater_1.initAutoUpdater)(HEADLESS);
|
||||||
|
hasStartedMainApplication = true;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
hasStartedMainApplication = true;
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Fired when all windows have been closed.
|
||||||
|
* On macOS the app (and LS) stay alive so the user can re-open via the tray.
|
||||||
|
* On all other platforms, shut down the LS and quit.
|
||||||
|
*/
|
||||||
|
electron_1.app.on('window-all-closed', async () => {
|
||||||
|
if (isQuitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasStartedMainApplication) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const runInBackground = await settingsService.getSetting(settingsService_1.SettingKey.RUN_IN_BACKGROUND);
|
||||||
|
if (!runInBackground) {
|
||||||
|
// Triggers 'before-quit' to run graceful cleanup without confirmation.
|
||||||
|
electron_1.app.quit();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
electron_1.app.dock?.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Fired just before the app quits (e.g. Cmd+Q on macOS, or after
|
||||||
|
* window-all-closed on non-macOS). Ensures the LS is terminated even if
|
||||||
|
* window-all-closed didn't handle it (e.g. on macOS quit via menu).
|
||||||
|
*/
|
||||||
|
electron_1.app.on('before-quit', async (event) => {
|
||||||
|
if (isQuitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!utils_1.showQuitConfirmation) {
|
||||||
|
event.preventDefault();
|
||||||
|
isQuitting = true;
|
||||||
|
// Destroy all windows to terminate renderers and release keep-alive sockets
|
||||||
|
const windows = electron_1.BrowserWindow.getAllWindows();
|
||||||
|
for (const win of windows) {
|
||||||
|
win.destroy();
|
||||||
|
}
|
||||||
|
// Close all active connections and kill the language server in parallel
|
||||||
|
await Promise.all([
|
||||||
|
electron_1.session.defaultSession.closeAllConnections().catch((err) => {
|
||||||
|
console.error('Failed to close session connections:', err);
|
||||||
|
}),
|
||||||
|
(0, languageServer_1.killLanguageServer)(),
|
||||||
|
]);
|
||||||
|
electron_1.app.quit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Show a confirmation dialog before quitting
|
||||||
|
event.preventDefault();
|
||||||
|
const win = electron_1.BrowserWindow.getFocusedWindow() || electron_1.BrowserWindow.getAllWindows()[0];
|
||||||
|
const options = {
|
||||||
|
type: 'question',
|
||||||
|
buttons: ['Cancel', 'Quit'],
|
||||||
|
defaultId: 1,
|
||||||
|
cancelId: 0,
|
||||||
|
title: 'Confirm Quit',
|
||||||
|
message: 'Are you sure you want to quit?',
|
||||||
|
detail: 'There may be agents or background tasks running.',
|
||||||
|
};
|
||||||
|
(0, utils_1.setShowQuitConfirmation)(false);
|
||||||
|
if (win) {
|
||||||
|
void electron_1.dialog.showMessageBox(win, options).then((result) => {
|
||||||
|
if (result.response === 1) {
|
||||||
|
// Quit - this will retrigger 'before-quit'
|
||||||
|
electron_1.app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Fired when the app is re-activated (e.g. clicking the dock icon on macOS).
|
||||||
|
* Re-opens a window if none are currently open.
|
||||||
|
*/
|
||||||
|
electron_1.app.on('activate', () => {
|
||||||
|
// On Mac, re-open a window when the user clicks the dock
|
||||||
|
// icon and no windows are open.
|
||||||
|
if (!HEADLESS && electron_1.BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
const url = DEV_URL ?? `${constants_1.WINDOW_ORIGIN}:${(0, languageServer_1.getLsPort)()}/`;
|
||||||
|
(0, utils_1.createWindow)(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
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');
|
||||||
104
src/app-extracted/dist/preload.js
vendored
Normal file
104
src/app-extracted/dist/preload.js
vendored
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
/**
|
||||||
|
* Preload script — runs in every BrowserWindow before the page loads.
|
||||||
|
* Exposes a minimal, secure API via contextBridge so the renderer can
|
||||||
|
* communicate with the main-process auto-updater without nodeIntegration.
|
||||||
|
*/
|
||||||
|
const electron_1 = require("electron");
|
||||||
|
const updaterAPI = {
|
||||||
|
onStateChanged: (callback) => {
|
||||||
|
const handler = (_event, state) => {
|
||||||
|
callback(state);
|
||||||
|
};
|
||||||
|
electron_1.ipcRenderer.on('updater:state-changed', handler);
|
||||||
|
// Return unsubscribe function
|
||||||
|
return () => {
|
||||||
|
electron_1.ipcRenderer.removeListener('updater:state-changed', handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
applyUpdate: () => electron_1.ipcRenderer.invoke('updater:apply'),
|
||||||
|
quitAndInstall: () => electron_1.ipcRenderer.invoke('updater:quit-and-install'),
|
||||||
|
checkForUpdates: () => electron_1.ipcRenderer.invoke('updater:check-for-updates'),
|
||||||
|
};
|
||||||
|
const dialogAPI = {
|
||||||
|
showOpenDialog: () => electron_1.ipcRenderer.invoke('dialog:open-workspace'),
|
||||||
|
};
|
||||||
|
const notificationAPI = {
|
||||||
|
send: (options) => electron_1.ipcRenderer.invoke('notification:send', options),
|
||||||
|
openSystemPreferences: () => electron_1.ipcRenderer.invoke('notification:open-system-preferences'),
|
||||||
|
onClicked: (callback) => {
|
||||||
|
const handler = (_event, payload) => {
|
||||||
|
callback(payload);
|
||||||
|
};
|
||||||
|
electron_1.ipcRenderer.on('notification:clicked', handler);
|
||||||
|
return () => {
|
||||||
|
electron_1.ipcRenderer.removeListener('notification:clicked', handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const storageAPI = {
|
||||||
|
getItems: () => electron_1.ipcRenderer.invoke('storage:get-items'),
|
||||||
|
updateItems: (changes) => electron_1.ipcRenderer.invoke('storage:update-items', changes),
|
||||||
|
onChanged: (callback) => {
|
||||||
|
const handler = (_event, changes) => {
|
||||||
|
callback(changes);
|
||||||
|
};
|
||||||
|
electron_1.ipcRenderer.on('storage:changed', handler);
|
||||||
|
return () => {
|
||||||
|
electron_1.ipcRenderer.removeListener('storage:changed', handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const logsAPI = {
|
||||||
|
getElectronLogs: () => electron_1.ipcRenderer.invoke('logs:electron'),
|
||||||
|
};
|
||||||
|
const extensionsAPI = {
|
||||||
|
sendAuthorities: (authoritiesMap) => electron_1.ipcRenderer.invoke('extensions:send-authorities', authoritiesMap),
|
||||||
|
};
|
||||||
|
const deepLinkAPI = {
|
||||||
|
onDeepLink: (callback) => {
|
||||||
|
const handler = (_event, url) => {
|
||||||
|
callback(url);
|
||||||
|
};
|
||||||
|
electron_1.ipcRenderer.on('deep-link', handler);
|
||||||
|
return () => {
|
||||||
|
electron_1.ipcRenderer.removeListener('deep-link', handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getStoredDeepLink: () => electron_1.ipcRenderer.invoke('deep-link:get-stored'),
|
||||||
|
};
|
||||||
|
const agentAPI = {
|
||||||
|
updateActiveAgentCount: (count) => electron_1.ipcRenderer.invoke('agent:update-active-count', count),
|
||||||
|
};
|
||||||
|
const electronNativeAPI = {
|
||||||
|
getZoomLevel: () => electron_1.webFrame.getZoomFactor(),
|
||||||
|
setTitleBarOverlay: (options) => electron_1.ipcRenderer.invoke('window:set-title-bar-overlay', options),
|
||||||
|
minimize: () => electron_1.ipcRenderer.invoke('window:minimize'),
|
||||||
|
maximize: () => electron_1.ipcRenderer.invoke('window:maximize'),
|
||||||
|
unmaximize: () => electron_1.ipcRenderer.invoke('window:unmaximize'),
|
||||||
|
isMaximized: () => electron_1.ipcRenderer.invoke('window:is-maximized'),
|
||||||
|
close: () => electron_1.ipcRenderer.invoke('window:close'),
|
||||||
|
toggleDevTools: () => electron_1.ipcRenderer.invoke('window:toggle-devtools'),
|
||||||
|
zoomIn: () => {
|
||||||
|
const current = electron_1.webFrame.getZoomLevel();
|
||||||
|
electron_1.webFrame.setZoomLevel(current + 0.5);
|
||||||
|
},
|
||||||
|
zoomOut: () => {
|
||||||
|
const current = electron_1.webFrame.getZoomLevel();
|
||||||
|
electron_1.webFrame.setZoomLevel(current - 0.5);
|
||||||
|
},
|
||||||
|
resetZoom: () => {
|
||||||
|
electron_1.webFrame.setZoomLevel(0);
|
||||||
|
},
|
||||||
|
openExternal: (url) => electron_1.ipcRenderer.invoke('shell:open-external', url),
|
||||||
|
};
|
||||||
|
electron_1.contextBridge.exposeInMainWorld('electronUpdater', updaterAPI);
|
||||||
|
electron_1.contextBridge.exposeInMainWorld('dialog', dialogAPI);
|
||||||
|
electron_1.contextBridge.exposeInMainWorld('nativeNotifications', notificationAPI);
|
||||||
|
electron_1.contextBridge.exposeInMainWorld('nativeStorage', storageAPI);
|
||||||
|
electron_1.contextBridge.exposeInMainWorld('logs', logsAPI);
|
||||||
|
electron_1.contextBridge.exposeInMainWorld('extensions', extensionsAPI);
|
||||||
|
electron_1.contextBridge.exposeInMainWorld('deepLink', deepLinkAPI);
|
||||||
|
electron_1.contextBridge.exposeInMainWorld('agent', agentAPI);
|
||||||
|
electron_1.contextBridge.exposeInMainWorld('electronNative', electronNativeAPI);
|
||||||
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