AG X v2.0.3 - Antigravity fork with multi-provider support
Features: - Welcome screen on first run (provider choice before LS starts) - 15+ AI providers (Google Gemini, OpenAI, Anthropic, DeepSeek, Ollama, etc.) - Provider config syncs to endpoints.json for translation proxy - Built-in Node.js translation proxy for non-native backends - Auto-update support, tray integration, URI scheme handler
This commit is contained in:
116
dist/services/apiProxy.js
vendored
Normal file
116
dist/services/apiProxy.js
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ApiProxy = void 0;
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const child_process = require("child_process");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const fs = require("fs");
|
||||
const translationProxy_1 = require("./translationProxy");
|
||||
|
||||
/**
|
||||
* API Proxy for AG X.
|
||||
*
|
||||
* All backends now use the built-in Node.js translation proxy.
|
||||
* No external Python or tools required.
|
||||
*/
|
||||
class ApiProxy {
|
||||
constructor(providerService) {
|
||||
this.providerService = providerService;
|
||||
this.server = null;
|
||||
this.proxyProcess = null;
|
||||
this.port = providerService.getProxyPort();
|
||||
}
|
||||
start() {
|
||||
if (this.server || this.proxyProcess) {
|
||||
return;
|
||||
}
|
||||
const backendType = this.providerService.getActiveBackendType();
|
||||
if (backendType === 'gemini-native' || backendType === 'native') {
|
||||
console.log('[ApiProxy] Native backend, no proxy needed');
|
||||
return;
|
||||
}
|
||||
this.startInternalProxy();
|
||||
}
|
||||
/**
|
||||
* Start the built-in Node.js translation proxy as a child process
|
||||
*/
|
||||
startInternalProxy() {
|
||||
const activeProvider = this.providerService.getActiveProvider();
|
||||
const config = this.providerService.getProviderConfig(activeProvider);
|
||||
if (!config) {
|
||||
console.error('[ApiProxy] No provider config found');
|
||||
return;
|
||||
}
|
||||
// Prepare the proxy config
|
||||
const configDir = path.join(os.homedir(), '.cache', 'ag-x-proxy');
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
const models = (config.models || []).map((m) => {
|
||||
if (typeof m === 'string') {
|
||||
return { id: m, object: "model", created: 1700000000, owned_by: activeProvider };
|
||||
}
|
||||
return m;
|
||||
});
|
||||
const proxyConfig = {
|
||||
port: this.port,
|
||||
backend_type: config.backendType || 'openai-compat',
|
||||
target_url: config.apiUrl || '',
|
||||
api_key: config.apiKey || '',
|
||||
cc_version: config.ccVersion || '0.26.8',
|
||||
oauth_provider: config.oauthProvider || '',
|
||||
reasoning_enabled: config.reasoningEnabled !== undefined ? config.reasoningEnabled : true,
|
||||
reasoning_effort: config.reasoningEffort || 'medium',
|
||||
models: models,
|
||||
};
|
||||
const configPath = path.join(configDir, `ag-x-proxy-${this.port}.json`);
|
||||
fs.writeFileSync(configPath, JSON.stringify(proxyConfig, null, 2), 'utf-8');
|
||||
console.log(`[ApiProxy] Starting built-in Node.js translation proxy on port ${this.port}`);
|
||||
// Use Electron's Node.js binary to run our translation proxy
|
||||
const proxyScript = path.join(__dirname, 'translationProxy.js');
|
||||
const nodeBin = process.execPath;
|
||||
this.proxyProcess = child_process.spawn(nodeBin, [proxyScript], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
detached: false,
|
||||
env: { ...process.env },
|
||||
});
|
||||
this.proxyProcess.stdout?.on('data', (data) => {
|
||||
const lines = data.toString().trim().split('\n');
|
||||
for (const line of lines) {
|
||||
console.log(`[Proxy] ${line}`);
|
||||
}
|
||||
});
|
||||
this.proxyProcess.stderr?.on('data', (data) => {
|
||||
const lines = data.toString().trim().split('\n');
|
||||
for (const line of lines) {
|
||||
console.log(`[Proxy] ${line}`);
|
||||
}
|
||||
});
|
||||
this.proxyProcess.on('error', (err) => {
|
||||
console.error('[ApiProxy] Proxy error:', err);
|
||||
});
|
||||
this.proxyProcess.on('exit', (code, signal) => {
|
||||
console.log(`[ApiProxy] Proxy exited with code ${code}, signal ${signal}`);
|
||||
this.proxyProcess = null;
|
||||
});
|
||||
this.proxyProcess.unref();
|
||||
console.log(`[ApiProxy] Built-in Node.js proxy spawned for ${config.backendType} on port ${this.port}`);
|
||||
}
|
||||
stop() {
|
||||
if (this.proxyProcess) {
|
||||
try {
|
||||
this.proxyProcess.kill();
|
||||
}
|
||||
catch (e) { /* ignore */ }
|
||||
this.proxyProcess = null;
|
||||
}
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
this.server = null;
|
||||
console.log('[ApiProxy] Stopped');
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.ApiProxy = ApiProxy;
|
||||
358
dist/services/providerService.js
vendored
Normal file
358
dist/services/providerService.js
vendored
Normal file
@@ -0,0 +1,358 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ProviderService = exports.ProviderType = void 0;
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
var ProviderType;
|
||||
(function (ProviderType) {
|
||||
ProviderType["GOOGLE_GEMINI"] = "google_gemini";
|
||||
ProviderType["OPENAI"] = "openai";
|
||||
ProviderType["ANTHROPIC"] = "anthropic";
|
||||
ProviderType["Z_AI"] = "z_ai";
|
||||
ProviderType["OPENCODE_ZEN"] = "opencode_zen";
|
||||
ProviderType["OPENCODE_GO"] = "opencode_go";
|
||||
ProviderType["OPENCODE_ZEN_ANTHROPIC"] = "opencode_zen_anthropic";
|
||||
ProviderType["OPENCODE_GO_ANTHROPIC"] = "opencode_go_anthropic";
|
||||
ProviderType["CROF_AI"] = "crof_ai";
|
||||
ProviderType["NVIDIA_NIM"] = "nvidia_nim";
|
||||
ProviderType["KILO_AI"] = "kilo_ai";
|
||||
ProviderType["COMMAND_CODE"] = "command_code";
|
||||
ProviderType["OPENROUTER"] = "openrouter";
|
||||
ProviderType["OPENADAPTER"] = "openadapter";
|
||||
ProviderType["DEEPSEEK"] = "deepseek";
|
||||
ProviderType["OLLAMA"] = "ollama";
|
||||
ProviderType["TOGETHER"] = "together";
|
||||
ProviderType["GROQ"] = "groq";
|
||||
ProviderType["CUSTOM"] = "custom";
|
||||
})(ProviderType = exports.ProviderType || (exports.ProviderType = {}));
|
||||
|
||||
const PREDEFINED_PROVIDERS = {
|
||||
[ProviderType.GOOGLE_GEMINI]: {
|
||||
name: 'Google Gemini (OAuth)',
|
||||
icon: '🔮',
|
||||
description: 'Built-in Gemini via Google OAuth — zero config',
|
||||
apiUrl: 'https://daily-cloudcode-pa.sandbox.googleapis.com',
|
||||
backendType: 'gemini-native',
|
||||
requiresApiKey: false,
|
||||
apiKeyHint: '',
|
||||
models: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-3-flash-preview', 'gemini-3-pro-preview'],
|
||||
defaultModel: 'gemini-2.5-pro',
|
||||
},
|
||||
[ProviderType.OPENAI]: {
|
||||
name: 'OpenAI',
|
||||
icon: '🟢',
|
||||
description: 'GPT models via Responses API',
|
||||
apiUrl: 'https://api.openai.com/v1',
|
||||
backendType: 'native',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'OpenAI API Key (sk-...)',
|
||||
models: ['gpt-4o', 'gpt-4o-mini', 'o1', 'o1-mini', 'o3', 'o3-mini'],
|
||||
defaultModel: 'gpt-4o',
|
||||
},
|
||||
[ProviderType.ANTHROPIC]: {
|
||||
name: 'Anthropic',
|
||||
icon: '🟣',
|
||||
description: 'Claude models via Messages API',
|
||||
apiUrl: 'https://api.anthropic.com',
|
||||
backendType: 'anthropic',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'Anthropic API Key (sk-ant-...)',
|
||||
models: ['claude-sonnet-4-20250514', 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'],
|
||||
defaultModel: 'claude-sonnet-4-20250514',
|
||||
},
|
||||
[ProviderType.Z_AI]: {
|
||||
name: 'Z.AI Coding',
|
||||
icon: '🅩',
|
||||
description: 'GLM & Z models via Z.AI',
|
||||
apiUrl: 'https://api.z.ai/api/coding/paas/v4',
|
||||
backendType: 'openai-compat',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'Z.AI API Key',
|
||||
models: ['glm-5.1', 'glm-4.7', 'GLM-4-Plus', 'GLM-4-Long', 'GLM-4-Flash', 'GLM-4-FlashX', 'GLM-Z1-Flash'],
|
||||
defaultModel: 'glm-5.1',
|
||||
},
|
||||
[ProviderType.OPENCODE_ZEN]: {
|
||||
name: 'OpenCode Zen',
|
||||
icon: '🧘',
|
||||
description: 'Multi-model via OpenCode Zen (OpenAI-compat)',
|
||||
apiUrl: 'https://opencode.ai/zen/v1',
|
||||
backendType: 'openai-compat',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'OpenCode API Key',
|
||||
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'],
|
||||
defaultModel: 'glm-5.1',
|
||||
},
|
||||
[ProviderType.OPENCODE_GO]: {
|
||||
name: 'OpenCode Go',
|
||||
icon: '🚀',
|
||||
description: 'Multi-model via OpenCode Go (OpenAI-compat)',
|
||||
apiUrl: 'https://opencode.ai/zen/go/v1',
|
||||
backendType: 'openai-compat',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'OpenCode API Key',
|
||||
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'],
|
||||
defaultModel: 'glm-5.1',
|
||||
},
|
||||
[ProviderType.OPENCODE_ZEN_ANTHROPIC]: {
|
||||
name: 'OpenCode Zen (Anthropic)',
|
||||
icon: '🧘',
|
||||
description: 'Claude models via OpenCode Zen',
|
||||
apiUrl: 'https://opencode.ai/zen/v1',
|
||||
backendType: 'anthropic',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'OpenCode API Key',
|
||||
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'],
|
||||
defaultModel: 'claude-sonnet-4-6',
|
||||
},
|
||||
[ProviderType.OPENCODE_GO_ANTHROPIC]: {
|
||||
name: 'OpenCode Go (Anthropic)',
|
||||
icon: '🚀',
|
||||
description: 'Claude models via OpenCode Go',
|
||||
apiUrl: 'https://opencode.ai/zen/go/v1',
|
||||
backendType: 'anthropic',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'OpenCode API Key',
|
||||
models: ['minimax-m2.7', 'minimax-m2.5'],
|
||||
defaultModel: 'minimax-m2.7',
|
||||
},
|
||||
[ProviderType.CROF_AI]: {
|
||||
name: 'Crof.ai',
|
||||
icon: '🌐',
|
||||
description: 'OpenAI-compatible models via Crof.ai',
|
||||
apiUrl: 'https://crof.ai/v1',
|
||||
backendType: 'openai-compat',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'Crof.ai API Key',
|
||||
models: [],
|
||||
defaultModel: '',
|
||||
},
|
||||
[ProviderType.NVIDIA_NIM]: {
|
||||
name: 'NVIDIA NIM',
|
||||
icon: '💚',
|
||||
description: 'NVIDIA accelerated inference models',
|
||||
apiUrl: 'https://integrate.api.nvidia.com/v1',
|
||||
backendType: 'openai-compat',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'NVIDIA API Key (nvapi-...)',
|
||||
models: [],
|
||||
defaultModel: '',
|
||||
},
|
||||
[ProviderType.KILO_AI]: {
|
||||
name: 'Kilo.ai Gateway',
|
||||
icon: '⚖️',
|
||||
description: 'Multi-provider via Kilo.ai',
|
||||
apiUrl: 'https://api.kilo.ai/api/gateway',
|
||||
backendType: 'openai-compat',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'Kilo.ai API Key',
|
||||
models: [],
|
||||
defaultModel: '',
|
||||
},
|
||||
[ProviderType.COMMAND_CODE]: {
|
||||
name: 'Command Code',
|
||||
icon: '⌘',
|
||||
description: '20+ models via Command Code API',
|
||||
apiUrl: 'https://api.commandcode.ai',
|
||||
backendType: 'command-code',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'Command Code API Key',
|
||||
ccVersion: '0.26.8',
|
||||
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',
|
||||
],
|
||||
defaultModel: 'deepseek/deepseek-v4-flash',
|
||||
},
|
||||
[ProviderType.OPENROUTER]: {
|
||||
name: 'OpenRouter',
|
||||
icon: '🔀',
|
||||
description: 'Route to hundreds of models via OpenRouter',
|
||||
apiUrl: 'https://openrouter.ai/api/v1',
|
||||
backendType: 'openai-compat',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'OpenRouter API Key (sk-or-...)',
|
||||
models: [],
|
||||
defaultModel: '',
|
||||
},
|
||||
[ProviderType.OPENADAPTER]: {
|
||||
name: 'OpenAdapter',
|
||||
icon: '🔌',
|
||||
description: 'Free/proxy models via OpenAdapter',
|
||||
apiUrl: 'https://api.openadapter.in/v1',
|
||||
backendType: 'openai-compat',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'OpenAdapter API Key',
|
||||
models: ['0G-DeepSeek-V3', '0G-DeepSeek-v4-Pro', '0G-GLM-5', '0G-GLM-5.1', '0G-Qwen3.6', '0G-Qwen-VL'],
|
||||
defaultModel: '0G-DeepSeek-v4-Pro',
|
||||
},
|
||||
[ProviderType.DEEPSEEK]: {
|
||||
name: 'DeepSeek',
|
||||
icon: '🔍',
|
||||
description: 'DeepSeek models directly',
|
||||
apiUrl: 'https://api.deepseek.com/v1',
|
||||
backendType: 'openai-compat',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'DeepSeek API Key',
|
||||
models: ['deepseek-chat', 'deepseek-reasoner'],
|
||||
defaultModel: 'deepseek-chat',
|
||||
},
|
||||
[ProviderType.OLLAMA]: {
|
||||
name: 'Ollama (Local)',
|
||||
icon: '🦙',
|
||||
description: 'Run models locally with Ollama',
|
||||
apiUrl: 'http://127.0.0.1:11434',
|
||||
backendType: 'openai-compat',
|
||||
requiresApiKey: false,
|
||||
apiKeyHint: '',
|
||||
models: ['llama3.1', 'llama3', 'codellama', 'mistral', 'mixtral', 'deepseek-coder', 'qwen2.5-coder'],
|
||||
defaultModel: 'llama3.1',
|
||||
},
|
||||
[ProviderType.TOGETHER]: {
|
||||
name: 'Together AI',
|
||||
icon: '🤝',
|
||||
description: 'Open-source models via Together',
|
||||
apiUrl: 'https://api.together.xyz/v1',
|
||||
backendType: 'openai-compat',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'Together API Key',
|
||||
models: [],
|
||||
defaultModel: '',
|
||||
},
|
||||
[ProviderType.GROQ]: {
|
||||
name: 'Groq',
|
||||
icon: '⚡',
|
||||
description: 'Ultra-fast inference via Groq',
|
||||
apiUrl: 'https://api.groq.com/openai/v1',
|
||||
backendType: 'openai-compat',
|
||||
requiresApiKey: true,
|
||||
apiKeyHint: 'Groq API Key (gsk_...)',
|
||||
models: [],
|
||||
defaultModel: '',
|
||||
},
|
||||
[ProviderType.CUSTOM]: {
|
||||
name: 'Custom Provider',
|
||||
icon: '⚙️',
|
||||
description: 'Any OpenAI-compatible endpoint',
|
||||
apiUrl: '',
|
||||
backendType: 'openai-compat',
|
||||
requiresApiKey: false,
|
||||
apiKeyHint: 'API Key (if required)',
|
||||
models: [],
|
||||
defaultModel: '',
|
||||
},
|
||||
};
|
||||
|
||||
class ProviderService {
|
||||
constructor(storageManager) {
|
||||
this.storageManager = storageManager;
|
||||
this.configPath = path.join(os.homedir(), '.ag-x', 'ag-x', 'provider_config.json');
|
||||
this.config = this.loadConfig();
|
||||
}
|
||||
loadConfig() {
|
||||
try {
|
||||
if (fs.existsSync(this.configPath)) {
|
||||
const content = fs.readFileSync(this.configPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Error loading provider config:', e);
|
||||
}
|
||||
// Build defaults from all predefined providers
|
||||
const providers = {};
|
||||
for (const [key, preset] of Object.entries(PREDEFINED_PROVIDERS)) {
|
||||
providers[key] = {
|
||||
...preset,
|
||||
apiKey: '',
|
||||
model: preset.defaultModel,
|
||||
enabled: key === ProviderType.GOOGLE_GEMINI,
|
||||
};
|
||||
}
|
||||
return {
|
||||
activeProvider: ProviderType.GOOGLE_GEMINI,
|
||||
providers,
|
||||
};
|
||||
}
|
||||
saveConfig() {
|
||||
try {
|
||||
const dir = path.dirname(this.configPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8');
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Error saving provider config:', e);
|
||||
}
|
||||
}
|
||||
getActiveProvider() {
|
||||
return this.config.activeProvider;
|
||||
}
|
||||
setActiveProvider(providerType) {
|
||||
this.config.activeProvider = providerType;
|
||||
this.saveConfig();
|
||||
}
|
||||
getProviderConfig(providerType) {
|
||||
return this.config.providers[providerType] || null;
|
||||
}
|
||||
updateProviderConfig(providerType, updates) {
|
||||
if (this.config.providers[providerType]) {
|
||||
Object.assign(this.config.providers[providerType], updates);
|
||||
this.saveConfig();
|
||||
}
|
||||
}
|
||||
getAllProviders() {
|
||||
return this.config.providers;
|
||||
}
|
||||
getPredefinedProviders() {
|
||||
return PREDEFINED_PROVIDERS;
|
||||
}
|
||||
getActiveProviderApiUrl() {
|
||||
const provider = this.config.providers[this.config.activeProvider];
|
||||
return provider?.apiUrl || 'https://daily-cloudcode-pa.sandbox.googleapis.com';
|
||||
}
|
||||
getActiveProviderApiKey() {
|
||||
const provider = this.config.providers[this.config.activeProvider];
|
||||
return provider?.apiKey || '';
|
||||
}
|
||||
getActiveProviderModel() {
|
||||
const provider = this.config.providers[this.config.activeProvider];
|
||||
return provider?.model || '';
|
||||
}
|
||||
getActiveBackendType() {
|
||||
const provider = this.config.providers[this.config.activeProvider];
|
||||
return provider?.backendType || 'gemini-native';
|
||||
}
|
||||
getActiveCcVersion() {
|
||||
const provider = this.config.providers[this.config.activeProvider];
|
||||
return provider?.ccVersion || '0.26.8';
|
||||
}
|
||||
/**
|
||||
* Does this provider need the built-in translation proxy?
|
||||
* gemini-native and native (OpenAI) go direct; everything else needs proxy.
|
||||
*/
|
||||
needsProxy() {
|
||||
const bt = this.getActiveBackendType();
|
||||
return bt !== 'gemini-native' && bt !== 'native';
|
||||
}
|
||||
/**
|
||||
* Does this provider need the full translation proxy (anthropic, command-code)?
|
||||
* command-code and anthropic need the full Python proxy for full compatibility.
|
||||
*/
|
||||
needsTranslateProxy() {
|
||||
const bt = this.getActiveBackendType();
|
||||
return bt === 'command-code' || bt === 'anthropic';
|
||||
}
|
||||
getProxyPort() {
|
||||
return 9876;
|
||||
}
|
||||
}
|
||||
exports.ProviderService = ProviderService;
|
||||
49
dist/services/settingsService.js
vendored
Normal file
49
dist/services/settingsService.js
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
"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 || (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],
|
||||
]);
|
||||
/**
|
||||
* A thin wrapper around StorageManager to listen for changes
|
||||
* in settings and apply their side effects.
|
||||
*/
|
||||
class SettingsService {
|
||||
constructor(storageManager) {
|
||||
this.storageManager = storageManager;
|
||||
this.storageManager.onDidChange((changes) => {
|
||||
this.applySideEffects(changes);
|
||||
});
|
||||
void this.initialize();
|
||||
}
|
||||
async initialize() {
|
||||
const items = await this.storageManager.getItems();
|
||||
this.applySideEffects(items);
|
||||
}
|
||||
applySideEffects(settings) {
|
||||
const val = settings[SettingKey.KEEP_COMPUTER_AWAKE];
|
||||
if (val !== undefined) {
|
||||
const preventSleep = val === null
|
||||
? exports.DEFAULTS.get(SettingKey.KEEP_COMPUTER_AWAKE)
|
||||
: val === 'true';
|
||||
utils_1.SleepBlocker.getInstance().shouldKeepComputerAwake(preventSleep);
|
||||
}
|
||||
}
|
||||
async getSetting(key) {
|
||||
const items = await this.storageManager.getItems();
|
||||
return items[key] === 'true';
|
||||
}
|
||||
}
|
||||
exports.SettingsService = SettingsService;
|
||||
65
dist/services/settingsService.test.js
vendored
Normal file
65
dist/services/settingsService.test.js
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const vitest_1 = require("vitest");
|
||||
const settingsService_1 = require("./settingsService");
|
||||
const utils_1 = require("../utils");
|
||||
vitest_1.vi.mock('../storage');
|
||||
vitest_1.vi.mock('../utils');
|
||||
(0, vitest_1.describe)('SettingsService', () => {
|
||||
let settingsService;
|
||||
let mockStorageManager;
|
||||
let mockSleepBlocker;
|
||||
(0, vitest_1.beforeEach)(() => {
|
||||
vitest_1.vi.clearAllMocks();
|
||||
mockStorageManager = {
|
||||
getItems: vitest_1.vi.fn().mockResolvedValue({
|
||||
runInBackground: String(process.platform !== 'win32'),
|
||||
keepComputerAwake: 'false',
|
||||
}),
|
||||
onDidChange: vitest_1.vi.fn().mockReturnValue({ dispose: vitest_1.vi.fn() }),
|
||||
};
|
||||
mockSleepBlocker = {
|
||||
shouldKeepComputerAwake: vitest_1.vi.fn(),
|
||||
};
|
||||
vitest_1.vi.mocked(utils_1.SleepBlocker.getInstance).mockReturnValue(mockSleepBlocker);
|
||||
settingsService = new settingsService_1.SettingsService(mockStorageManager);
|
||||
});
|
||||
(0, vitest_1.it)('should return defaults when storage is empty', async () => {
|
||||
(0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.RUN_IN_BACKGROUND)).toBe(true);
|
||||
(0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.KEEP_COMPUTER_AWAKE)).toBe(false);
|
||||
});
|
||||
(0, vitest_1.it)('should return values from storage', async () => {
|
||||
mockStorageManager.getItems.mockResolvedValue({
|
||||
runInBackground: 'false',
|
||||
keepComputerAwake: 'true',
|
||||
});
|
||||
(0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.RUN_IN_BACKGROUND)).toBe(false);
|
||||
(0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.KEEP_COMPUTER_AWAKE)).toBe(true);
|
||||
});
|
||||
(0, vitest_1.it)('should return updated value after storage change', async () => {
|
||||
mockStorageManager.getItems.mockResolvedValue({
|
||||
[settingsService_1.SettingKey.RUN_IN_BACKGROUND]: 'false',
|
||||
});
|
||||
(0, vitest_1.expect)(await settingsService.getSetting(settingsService_1.SettingKey.RUN_IN_BACKGROUND)).toBe(false);
|
||||
});
|
||||
(0, vitest_1.it)('should trigger SleepBlocker on keepComputerAwake change', async () => {
|
||||
let changeListener;
|
||||
mockStorageManager.onDidChange.mockImplementation((listener) => {
|
||||
changeListener = listener;
|
||||
return { dispose: vitest_1.vi.fn() };
|
||||
});
|
||||
// Instantiate again to trigger constructor with the new mock
|
||||
settingsService = new settingsService_1.SettingsService(mockStorageManager);
|
||||
// Simulate change
|
||||
changeListener({ keepComputerAwake: 'true' });
|
||||
(0, vitest_1.expect)(mockSleepBlocker.shouldKeepComputerAwake).toHaveBeenCalledWith(true);
|
||||
});
|
||||
(0, vitest_1.it)('should trigger initial SleepBlocker state', async () => {
|
||||
mockStorageManager.getItems.mockResolvedValue({
|
||||
keepComputerAwake: 'true',
|
||||
});
|
||||
settingsService = new settingsService_1.SettingsService(mockStorageManager);
|
||||
await new Promise(process.nextTick);
|
||||
(0, vitest_1.expect)(mockSleepBlocker.shouldKeepComputerAwake).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
1191
dist/services/translationProxy.js
vendored
Normal file
1191
dist/services/translationProxy.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user