feat(setttings): support auto launch config (#415)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
@@ -122,6 +122,9 @@ ClawX はドキュメント処理スキル(`pdf`、`xlsx`、`docx`、`pptx`)
|
|||||||
### 🌙 アダプティブテーマ
|
### 🌙 アダプティブテーマ
|
||||||
ライトモード、ダークモード、またはシステム同期テーマ。ClawXはあなたの好みに自動的に適応します。
|
ライトモード、ダークモード、またはシステム同期テーマ。ClawXはあなたの好みに自動的に適応します。
|
||||||
|
|
||||||
|
### 🚀 自動起動設定
|
||||||
|
**設定 → 通用** から **システム起動時に自動起動** を有効化すると、ログイン後に ClawX が自動的に起動します。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## はじめに
|
## はじめに
|
||||||
|
|||||||
@@ -123,6 +123,9 @@ Connect to multiple AI providers (OpenAI, Anthropic, and more) with credentials
|
|||||||
### 🌙 Adaptive Theming
|
### 🌙 Adaptive Theming
|
||||||
Light mode, dark mode, or system-synchronized themes. ClawX adapts to your preferences automatically.
|
Light mode, dark mode, or system-synchronized themes. ClawX adapts to your preferences automatically.
|
||||||
|
|
||||||
|
### 🚀 Startup Launch Control
|
||||||
|
In **Settings → General**, you can enable **Launch at system startup** so ClawX starts automatically after login.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|||||||
@@ -123,6 +123,9 @@ ClawX 还会内置预装完整的文档处理技能(`pdf`、`xlsx`、`docx`、
|
|||||||
### 🌙 自适应主题
|
### 🌙 自适应主题
|
||||||
支持浅色模式、深色模式或跟随系统主题。ClawX 自动适应你的偏好设置。
|
支持浅色模式、深色模式或跟随系统主题。ClawX 自动适应你的偏好设置。
|
||||||
|
|
||||||
|
### 🚀 开机启动控制
|
||||||
|
在 **设置 → 通用** 中,你可以开启 **开机自动启动**,让 ClawX 在系统登录后自动启动。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { IncomingMessage, ServerResponse } from 'http';
|
import type { IncomingMessage, ServerResponse } from 'http';
|
||||||
import { applyProxySettings } from '../../main/proxy';
|
import { applyProxySettings } from '../../main/proxy';
|
||||||
|
import { syncLaunchAtStartupSettingFromStore } from '../../main/launch-at-startup';
|
||||||
import { getAllSettings, getSetting, resetSettings, setSetting, type AppSettings } from '../../utils/store';
|
import { getAllSettings, getSetting, resetSettings, setSetting, type AppSettings } from '../../utils/store';
|
||||||
import type { HostApiContext } from '../context';
|
import type { HostApiContext } from '../context';
|
||||||
import { parseJsonBody, sendJson } from '../route-utils';
|
import { parseJsonBody, sendJson } from '../route-utils';
|
||||||
@@ -23,6 +24,10 @@ function patchTouchesProxy(patch: Partial<AppSettings>): boolean {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function patchTouchesLaunchAtStartup(patch: Partial<AppSettings>): boolean {
|
||||||
|
return Object.prototype.hasOwnProperty.call(patch, 'launchAtStartup');
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleSettingsRoutes(
|
export async function handleSettingsRoutes(
|
||||||
req: IncomingMessage,
|
req: IncomingMessage,
|
||||||
res: ServerResponse,
|
res: ServerResponse,
|
||||||
@@ -44,6 +49,9 @@ export async function handleSettingsRoutes(
|
|||||||
if (patchTouchesProxy(patch)) {
|
if (patchTouchesProxy(patch)) {
|
||||||
await handleProxySettingsChange(ctx);
|
await handleProxySettingsChange(ctx);
|
||||||
}
|
}
|
||||||
|
if (patchTouchesLaunchAtStartup(patch)) {
|
||||||
|
await syncLaunchAtStartupSettingFromStore();
|
||||||
|
}
|
||||||
sendJson(res, 200, { success: true });
|
sendJson(res, 200, { success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendJson(res, 500, { success: false, error: String(error) });
|
sendJson(res, 500, { success: false, error: String(error) });
|
||||||
@@ -76,6 +84,9 @@ export async function handleSettingsRoutes(
|
|||||||
) {
|
) {
|
||||||
await handleProxySettingsChange(ctx);
|
await handleProxySettingsChange(ctx);
|
||||||
}
|
}
|
||||||
|
if (key === 'launchAtStartup') {
|
||||||
|
await syncLaunchAtStartupSettingFromStore();
|
||||||
|
}
|
||||||
sendJson(res, 200, { success: true });
|
sendJson(res, 200, { success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendJson(res, 500, { success: false, error: String(error) });
|
sendJson(res, 500, { success: false, error: String(error) });
|
||||||
@@ -87,6 +98,7 @@ export async function handleSettingsRoutes(
|
|||||||
try {
|
try {
|
||||||
await resetSettings();
|
await resetSettings();
|
||||||
await handleProxySettingsChange(ctx);
|
await handleProxySettingsChange(ctx);
|
||||||
|
await syncLaunchAtStartupSettingFromStore();
|
||||||
sendJson(res, 200, { success: true, settings: await getAllSettings() });
|
sendJson(res, 200, { success: true, settings: await getAllSettings() });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendJson(res, 500, { success: false, error: String(error) });
|
sendJson(res, 500, { success: false, error: String(error) });
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { ensureClawXContext, repairClawXOnlyBootstrapFiles } from '../utils/open
|
|||||||
import { autoInstallCliIfNeeded, generateCompletionCache, installCompletionToProfile } from '../utils/openclaw-cli';
|
import { autoInstallCliIfNeeded, generateCompletionCache, installCompletionToProfile } from '../utils/openclaw-cli';
|
||||||
import { isQuitting, setQuitting } from './app-state';
|
import { isQuitting, setQuitting } from './app-state';
|
||||||
import { applyProxySettings } from './proxy';
|
import { applyProxySettings } from './proxy';
|
||||||
|
import { syncLaunchAtStartupSettingFromStore } from './launch-at-startup';
|
||||||
import { getSetting } from '../utils/store';
|
import { getSetting } from '../utils/store';
|
||||||
import { ensureBuiltinSkillsInstalled, ensurePreinstalledSkillsInstalled } from '../utils/skill-config';
|
import { ensureBuiltinSkillsInstalled, ensurePreinstalledSkillsInstalled } from '../utils/skill-config';
|
||||||
import { startHostApiServer } from '../api/server';
|
import { startHostApiServer } from '../api/server';
|
||||||
@@ -162,6 +163,7 @@ async function initialize(): Promise<void> {
|
|||||||
|
|
||||||
// Apply persisted proxy settings before creating windows or network requests.
|
// Apply persisted proxy settings before creating windows or network requests.
|
||||||
await applyProxySettings();
|
await applyProxySettings();
|
||||||
|
await syncLaunchAtStartupSettingFromStore();
|
||||||
|
|
||||||
// Set application menu
|
// Set application menu
|
||||||
createMenu();
|
createMenu();
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { getProviderConfig } from '../utils/provider-registry';
|
|||||||
import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth';
|
import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth';
|
||||||
import { browserOAuthManager, type BrowserOAuthProviderType } from '../utils/browser-oauth';
|
import { browserOAuthManager, type BrowserOAuthProviderType } from '../utils/browser-oauth';
|
||||||
import { applyProxySettings } from './proxy';
|
import { applyProxySettings } from './proxy';
|
||||||
|
import { syncLaunchAtStartupSettingFromStore } from './launch-at-startup';
|
||||||
import { proxyAwareFetch } from '../utils/proxy-fetch';
|
import { proxyAwareFetch } from '../utils/proxy-fetch';
|
||||||
import { getRecentTokenUsageHistory } from '../utils/token-usage';
|
import { getRecentTokenUsageHistory } from '../utils/token-usage';
|
||||||
import { getProviderService } from '../services/providers/provider-service';
|
import { getProviderService } from '../services/providers/provider-service';
|
||||||
@@ -224,6 +225,10 @@ function isProxyKey(key: keyof AppSettings): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLaunchAtStartupKey(key: keyof AppSettings): boolean {
|
||||||
|
return key === 'launchAtStartup';
|
||||||
|
}
|
||||||
|
|
||||||
function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
|
function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
|
||||||
const providerService = getProviderService();
|
const providerService = getProviderService();
|
||||||
const handleProxySettingsChange = async () => {
|
const handleProxySettingsChange = async () => {
|
||||||
@@ -694,6 +699,9 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
|
|||||||
if (isProxyKey(key)) {
|
if (isProxyKey(key)) {
|
||||||
await handleProxySettingsChange();
|
await handleProxySettingsChange();
|
||||||
}
|
}
|
||||||
|
if (isLaunchAtStartupKey(key)) {
|
||||||
|
await syncLaunchAtStartupSettingFromStore();
|
||||||
|
}
|
||||||
data = { success: true };
|
data = { success: true };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -706,6 +714,9 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
|
|||||||
if (entries.some(([key]) => isProxyKey(key))) {
|
if (entries.some(([key]) => isProxyKey(key))) {
|
||||||
await handleProxySettingsChange();
|
await handleProxySettingsChange();
|
||||||
}
|
}
|
||||||
|
if (entries.some(([key]) => isLaunchAtStartupKey(key))) {
|
||||||
|
await syncLaunchAtStartupSettingFromStore();
|
||||||
|
}
|
||||||
data = { success: true };
|
data = { success: true };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -713,6 +724,7 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
|
|||||||
await resetSettings();
|
await resetSettings();
|
||||||
const settings = await getAllSettings();
|
const settings = await getAllSettings();
|
||||||
await handleProxySettingsChange();
|
await handleProxySettingsChange();
|
||||||
|
await syncLaunchAtStartupSettingFromStore();
|
||||||
data = { success: true, settings };
|
data = { success: true, settings };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -2239,6 +2251,9 @@ function registerSettingsHandlers(gatewayManager: GatewayManager): void {
|
|||||||
) {
|
) {
|
||||||
await handleProxySettingsChange();
|
await handleProxySettingsChange();
|
||||||
}
|
}
|
||||||
|
if (key === 'launchAtStartup') {
|
||||||
|
await syncLaunchAtStartupSettingFromStore();
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
@@ -2259,6 +2274,9 @@ function registerSettingsHandlers(gatewayManager: GatewayManager): void {
|
|||||||
)) {
|
)) {
|
||||||
await handleProxySettingsChange();
|
await handleProxySettingsChange();
|
||||||
}
|
}
|
||||||
|
if (entries.some(([key]) => key === 'launchAtStartup')) {
|
||||||
|
await syncLaunchAtStartupSettingFromStore();
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
@@ -2267,6 +2285,7 @@ function registerSettingsHandlers(gatewayManager: GatewayManager): void {
|
|||||||
await resetSettings();
|
await resetSettings();
|
||||||
const settings = await getAllSettings();
|
const settings = await getAllSettings();
|
||||||
await handleProxySettingsChange();
|
await handleProxySettingsChange();
|
||||||
|
await syncLaunchAtStartupSettingFromStore();
|
||||||
return { success: true, settings };
|
return { success: true, settings };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
85
electron/main/launch-at-startup.ts
Normal file
85
electron/main/launch-at-startup.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { app } from 'electron';
|
||||||
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
import { getSetting } from '../utils/store';
|
||||||
|
|
||||||
|
const LINUX_AUTOSTART_FILE = join('.config', 'autostart', 'clawx.desktop');
|
||||||
|
|
||||||
|
function quoteDesktopArg(value: string): string {
|
||||||
|
if (!value) return '""';
|
||||||
|
const escaped = value.replace(/(["\\`$])/g, '\\$1');
|
||||||
|
if (/[\s"'\\`$]/.test(value)) {
|
||||||
|
return `"${escaped}"`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLinuxExecCommand(): string {
|
||||||
|
if (app.isPackaged) {
|
||||||
|
return quoteDesktopArg(process.execPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const launchArgs = process.argv.slice(1).filter(Boolean);
|
||||||
|
const cmdParts = [process.execPath, ...launchArgs].map(quoteDesktopArg);
|
||||||
|
return cmdParts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLinuxDesktopEntry(): string {
|
||||||
|
return [
|
||||||
|
'[Desktop Entry]',
|
||||||
|
'Type=Application',
|
||||||
|
'Version=1.0',
|
||||||
|
'Name=ClawX',
|
||||||
|
'Comment=ClawX - AI Assistant',
|
||||||
|
`Exec=${getLinuxExecCommand()}`,
|
||||||
|
'Terminal=false',
|
||||||
|
'Categories=Utility;',
|
||||||
|
'X-GNOME-Autostart-enabled=true',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyLinuxLaunchAtStartup(enabled: boolean): Promise<void> {
|
||||||
|
const targetPath = join(app.getPath('home'), LINUX_AUTOSTART_FILE);
|
||||||
|
if (enabled) {
|
||||||
|
await mkdir(dirname(targetPath), { recursive: true });
|
||||||
|
await writeFile(targetPath, getLinuxDesktopEntry(), 'utf8');
|
||||||
|
logger.info(`Launch-at-startup enabled via desktop entry: ${targetPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await rm(targetPath, { force: true });
|
||||||
|
logger.info(`Launch-at-startup disabled and desktop entry removed: ${targetPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyWindowsOrMacLaunchAtStartup(enabled: boolean): void {
|
||||||
|
app.setLoginItemSettings({
|
||||||
|
openAtLogin: enabled,
|
||||||
|
openAsHidden: false,
|
||||||
|
});
|
||||||
|
logger.info(`Launch-at-startup ${enabled ? 'enabled' : 'disabled'} via login items`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyLaunchAtStartupSetting(enabled: boolean): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
await applyLinuxLaunchAtStartup(enabled);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32' || process.platform === 'darwin') {
|
||||||
|
applyWindowsOrMacLaunchAtStartup(enabled);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(`Launch-at-startup unsupported on platform: ${process.platform}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to apply launch-at-startup=${enabled}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncLaunchAtStartupSettingFromStore(): Promise<void> {
|
||||||
|
const launchAtStartup = await getSetting('launchAtStartup');
|
||||||
|
await applyLaunchAtStartupSetting(Boolean(launchAtStartup));
|
||||||
|
}
|
||||||
@@ -2,13 +2,15 @@
|
|||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"subtitle": "Configure your ClawX experience",
|
"subtitle": "Configure your ClawX experience",
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "General",
|
||||||
"description": "Customize the look and feel",
|
"description": "Customize the look and feel",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"language": "Language"
|
"language": "Language",
|
||||||
|
"launchAtStartup": "Launch at system startup",
|
||||||
|
"launchAtStartupDesc": "Automatically launch ClawX when you log in"
|
||||||
},
|
},
|
||||||
"aiProviders": {
|
"aiProviders": {
|
||||||
"title": "AI Providers",
|
"title": "AI Providers",
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
"title": "設定",
|
"title": "設定",
|
||||||
"subtitle": "ClawX の体験をカスタマイズ",
|
"subtitle": "ClawX の体験をカスタマイズ",
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "外観",
|
"title": "通用",
|
||||||
"description": "外観とスタイルをカスタマイズ",
|
"description": "外観とスタイルをカスタマイズ",
|
||||||
"theme": "テーマ",
|
"theme": "テーマ",
|
||||||
"light": "ライト",
|
"light": "ライト",
|
||||||
"dark": "ダーク",
|
"dark": "ダーク",
|
||||||
"system": "システム",
|
"system": "システム",
|
||||||
"language": "言語"
|
"language": "言語",
|
||||||
|
"launchAtStartup": "システム起動時に自動起動",
|
||||||
|
"launchAtStartupDesc": "ログイン時に ClawX を自動的に起動します"
|
||||||
},
|
},
|
||||||
"aiProviders": {
|
"aiProviders": {
|
||||||
"title": "AI プロバイダー",
|
"title": "AI プロバイダー",
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
"title": "设置",
|
"title": "设置",
|
||||||
"subtitle": "配置您的 ClawX 体验",
|
"subtitle": "配置您的 ClawX 体验",
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "外观",
|
"title": "通用",
|
||||||
"description": "自定义外观和风格",
|
"description": "自定义外观和风格",
|
||||||
"theme": "主题",
|
"theme": "主题",
|
||||||
"light": "浅色",
|
"light": "浅色",
|
||||||
"dark": "深色",
|
"dark": "深色",
|
||||||
"system": "跟随系统",
|
"system": "跟随系统",
|
||||||
"language": "语言"
|
"language": "语言",
|
||||||
|
"launchAtStartup": "开机自动启动",
|
||||||
|
"launchAtStartupDesc": "登录系统后自动启动 ClawX"
|
||||||
},
|
},
|
||||||
"aiProviders": {
|
"aiProviders": {
|
||||||
"title": "AI 模型提供商",
|
"title": "AI 模型提供商",
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ export function Settings() {
|
|||||||
setTheme,
|
setTheme,
|
||||||
language,
|
language,
|
||||||
setLanguage,
|
setLanguage,
|
||||||
|
launchAtStartup,
|
||||||
|
setLaunchAtStartup,
|
||||||
gatewayAutoStart,
|
gatewayAutoStart,
|
||||||
setGatewayAutoStart,
|
setGatewayAutoStart,
|
||||||
proxyEnabled,
|
proxyEnabled,
|
||||||
@@ -435,6 +437,18 @@ export function Settings() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-[15px] font-medium text-foreground/80">{t('appearance.launchAtStartup')}</Label>
|
||||||
|
<p className="text-[13px] text-muted-foreground mt-1">
|
||||||
|
{t('appearance.launchAtStartupDesc')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={launchAtStartup}
|
||||||
|
onCheckedChange={setLaunchAtStartup}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,13 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
}).catch(() => { });
|
}).catch(() => { });
|
||||||
},
|
},
|
||||||
setStartMinimized: (startMinimized) => set({ startMinimized }),
|
setStartMinimized: (startMinimized) => set({ startMinimized }),
|
||||||
setLaunchAtStartup: (launchAtStartup) => set({ launchAtStartup }),
|
setLaunchAtStartup: (launchAtStartup) => {
|
||||||
|
set({ launchAtStartup });
|
||||||
|
void hostApiFetch('/api/settings/launchAtStartup', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ value: launchAtStartup }),
|
||||||
|
}).catch(() => { });
|
||||||
|
},
|
||||||
setTelemetryEnabled: (telemetryEnabled) => {
|
setTelemetryEnabled: (telemetryEnabled) => {
|
||||||
set({ telemetryEnabled });
|
set({ telemetryEnabled });
|
||||||
void hostApiFetch('/api/settings/telemetryEnabled', {
|
void hostApiFetch('/api/settings/telemetryEnabled', {
|
||||||
|
|||||||
91
tests/unit/launch-at-startup.test.ts
Normal file
91
tests/unit/launch-at-startup.test.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { access, readFile, rm } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
|
||||||
|
const {
|
||||||
|
testHome,
|
||||||
|
electronAppMock,
|
||||||
|
setLoginItemSettingsMock,
|
||||||
|
} = vi.hoisted(() => {
|
||||||
|
const suffix = Math.random().toString(36).slice(2);
|
||||||
|
const setLoginItemSettingsMock = vi.fn();
|
||||||
|
const electronAppMock = {
|
||||||
|
isPackaged: true,
|
||||||
|
getPath: (name: string) => (name === 'home' ? `/tmp/clawx-launch-startup-${suffix}` : '/tmp'),
|
||||||
|
setLoginItemSettings: setLoginItemSettingsMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
testHome: `/tmp/clawx-launch-startup-${suffix}`,
|
||||||
|
electronAppMock,
|
||||||
|
setLoginItemSettingsMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
app: electronAppMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function setPlatform(platform: string): void {
|
||||||
|
Object.defineProperty(process, 'platform', { value: platform, writable: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('launch-at-startup integration', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.resetModules();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
electronAppMock.isPackaged = true;
|
||||||
|
await rm(testHome, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
Object.defineProperty(process, 'platform', { value: originalPlatform, writable: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses login item settings on Windows', async () => {
|
||||||
|
setPlatform('win32');
|
||||||
|
const { applyLaunchAtStartupSetting } = await import('@electron/main/launch-at-startup');
|
||||||
|
|
||||||
|
await applyLaunchAtStartupSetting(true);
|
||||||
|
expect(setLoginItemSettingsMock).toHaveBeenCalledWith({
|
||||||
|
openAtLogin: true,
|
||||||
|
openAsHidden: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses login item settings on macOS', async () => {
|
||||||
|
setPlatform('darwin');
|
||||||
|
const { applyLaunchAtStartupSetting } = await import('@electron/main/launch-at-startup');
|
||||||
|
|
||||||
|
await applyLaunchAtStartupSetting(false);
|
||||||
|
expect(setLoginItemSettingsMock).toHaveBeenCalledWith({
|
||||||
|
openAtLogin: false,
|
||||||
|
openAsHidden: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates and removes Linux autostart desktop entry', async () => {
|
||||||
|
setPlatform('linux');
|
||||||
|
const { applyLaunchAtStartupSetting } = await import('@electron/main/launch-at-startup');
|
||||||
|
|
||||||
|
const autostartPath = join(testHome, '.config', 'autostart', 'clawx.desktop');
|
||||||
|
await applyLaunchAtStartupSetting(true);
|
||||||
|
|
||||||
|
const content = await readFile(autostartPath, 'utf8');
|
||||||
|
expect(content).toContain('[Desktop Entry]');
|
||||||
|
expect(content).toContain('Name=ClawX');
|
||||||
|
expect(content).toContain('Exec=');
|
||||||
|
|
||||||
|
await applyLaunchAtStartupSetting(false);
|
||||||
|
await expect(access(autostartPath)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw on unsupported platforms', async () => {
|
||||||
|
setPlatform('freebsd');
|
||||||
|
const { applyLaunchAtStartupSetting } = await import('@electron/main/launch-at-startup');
|
||||||
|
|
||||||
|
await expect(applyLaunchAtStartupSetting(true)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -47,6 +47,30 @@ describe('Settings Store', () => {
|
|||||||
setDevModeUnlocked(true);
|
setDevModeUnlocked(true);
|
||||||
expect(useSettingsStore.getState().devModeUnlocked).toBe(true);
|
expect(useSettingsStore.getState().devModeUnlocked).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should persist launch-at-startup setting through host api', () => {
|
||||||
|
const invoke = vi.mocked(window.electron.ipcRenderer.invoke);
|
||||||
|
invoke.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
status: 200,
|
||||||
|
ok: true,
|
||||||
|
json: { success: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { setLaunchAtStartup } = useSettingsStore.getState();
|
||||||
|
setLaunchAtStartup(true);
|
||||||
|
|
||||||
|
expect(useSettingsStore.getState().launchAtStartup).toBe(true);
|
||||||
|
expect(invoke).toHaveBeenCalledWith(
|
||||||
|
'hostapi:fetch',
|
||||||
|
expect.objectContaining({
|
||||||
|
path: '/api/settings/launchAtStartup',
|
||||||
|
method: 'PUT',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Gateway Store', () => {
|
describe('Gateway Store', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user