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:
2026-05-22 10:34:10 +00:00
Unverified
parent e6b6405912
commit 0265d58123
55 changed files with 9030 additions and 210 deletions

View 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

View 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.

View 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!

View 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(),
};

View 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(),
};

File diff suppressed because it is too large Load Diff

163
src/app-extracted/dist/aiProviderAPI.ts vendored Normal file
View 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
View 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
View 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 });
}
});
}

View 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;
}

View 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; } });

View 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 */
}
}

View 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;
}

View 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>`;
}

View 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);

View 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
View 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);
});
}

View 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
View 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
View 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
}
});
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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);

View 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;

View 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;

View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

241
src/app-extracted/dist/updater.js vendored Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

BIN
src/app.asar Normal file

Binary file not shown.