feat(electron): add Windows startup and system tray support (#2112)
Add Windows launch-at-login with background startup support and extend the native tray integration to Windows. Add a Windows-only setting to minimize or close the main window to the system tray, persist it through desktop settings, and expose it in Settings search and all locale dictionaries. Keep tray state synchronized with live sessions on both macOS and Windows, while preserving the existing macOS behavior.
This commit is contained in:
@@ -7,11 +7,13 @@ import {
|
||||
getDesktopLanAddress,
|
||||
getDesktopKeepAwake,
|
||||
getDesktopLaunchAtLogin,
|
||||
getDesktopMinimizeToTray,
|
||||
isDesktopLocalOriginActive,
|
||||
isDesktopShell,
|
||||
restartDesktopApp,
|
||||
setDesktopKeepAwake,
|
||||
setDesktopLaunchAtLogin,
|
||||
setDesktopMinimizeToTray,
|
||||
} from '@/lib/desktop';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
@@ -31,6 +33,9 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
const [launchAtLoginSupported, setLaunchAtLoginSupported] = React.useState(false);
|
||||
const [launchAtLoginEnabled, setLaunchAtLoginEnabled] = React.useState(false);
|
||||
const [isSavingLaunchAtLogin, setIsSavingLaunchAtLogin] = React.useState(false);
|
||||
const [minimizeToTraySupported, setMinimizeToTraySupported] = React.useState(false);
|
||||
const [minimizeToTrayEnabled, setMinimizeToTrayEnabled] = React.useState(false);
|
||||
const [isSavingMinimizeToTray, setIsSavingMinimizeToTray] = React.useState(false);
|
||||
const [keepAwakeSupported, setKeepAwakeSupported] = React.useState(false);
|
||||
const [keepAwakeEnabled, setKeepAwakeEnabled] = React.useState(false);
|
||||
const [isSavingKeepAwake, setIsSavingKeepAwake] = React.useState(false);
|
||||
@@ -112,6 +117,27 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
};
|
||||
}, [isLocalDesktop]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isLocalDesktop) {
|
||||
setMinimizeToTraySupported(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const status = await getDesktopMinimizeToTray();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setMinimizeToTraySupported(status?.supported === true);
|
||||
setMinimizeToTrayEnabled(status?.enabled === true);
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isLocalDesktop]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isLocalDesktop) {
|
||||
setKeepAwakeSupported(false);
|
||||
@@ -209,6 +235,33 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
}
|
||||
}, [isSavingLaunchAtLogin, launchAtLoginEnabled, launchAtLoginSupported, t]);
|
||||
|
||||
const handleMinimizeToTrayToggle = React.useCallback(async () => {
|
||||
if (!minimizeToTraySupported || isSavingMinimizeToTray) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextValue = !minimizeToTrayEnabled;
|
||||
setMinimizeToTrayEnabled(nextValue);
|
||||
setIsSavingMinimizeToTray(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const status = await setDesktopMinimizeToTray(nextValue);
|
||||
if (!status) {
|
||||
throw new Error(t('settings.openchamber.desktopNetwork.error.minimizeToTraySaveFailed'));
|
||||
}
|
||||
if (!status.supported) {
|
||||
throw new Error(t('settings.openchamber.desktopNetwork.error.minimizeToTrayUnsupported'));
|
||||
}
|
||||
setMinimizeToTrayEnabled(status.enabled);
|
||||
} catch (cause) {
|
||||
setMinimizeToTrayEnabled(!nextValue);
|
||||
setError(cause instanceof Error ? cause.message : t('settings.openchamber.desktopNetwork.error.minimizeToTraySaveFailed'));
|
||||
} finally {
|
||||
setIsSavingMinimizeToTray(false);
|
||||
}
|
||||
}, [isSavingMinimizeToTray, minimizeToTrayEnabled, minimizeToTraySupported, t]);
|
||||
|
||||
const handleKeepAwakeToggle = React.useCallback(async () => {
|
||||
if (!keepAwakeSupported || isSavingKeepAwake) {
|
||||
return;
|
||||
@@ -311,6 +364,35 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{minimizeToTraySupported ? (
|
||||
<div
|
||||
data-settings-item="sessions.desktop-minimize-to-tray"
|
||||
className="group flex cursor-pointer items-start gap-2 py-1.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleMinimizeToTrayToggle}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handleMinimizeToTrayToggle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={minimizeToTrayEnabled}
|
||||
onChange={handleMinimizeToTrayToggle}
|
||||
ariaLabel={t('settings.openchamber.desktopNetwork.field.minimizeToTrayAria')}
|
||||
disabled={isSavingMinimizeToTray}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="typography-ui-label text-foreground">{t('settings.openchamber.desktopNetwork.field.minimizeToTray')}</div>
|
||||
<div className="typography-micro text-muted-foreground/70">
|
||||
{t('settings.openchamber.desktopNetwork.field.minimizeToTrayDescription')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{keepAwakeSupported ? (
|
||||
<div
|
||||
data-settings-item="sessions.desktop-keep-awake"
|
||||
|
||||
@@ -334,6 +334,10 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
return isDesktopShell() && typeof window !== 'undefined'
|
||||
&& (window as unknown as { __OPENCHAMBER_PLATFORM__?: string }).__OPENCHAMBER_PLATFORM__ === 'darwin';
|
||||
}, []);
|
||||
const isWindows = React.useMemo(() => {
|
||||
return isDesktopShell() && typeof window !== 'undefined'
|
||||
&& (window as unknown as { __OPENCHAMBER_PLATFORM__?: string }).__OPENCHAMBER_PLATFORM__ === 'win32';
|
||||
}, []);
|
||||
|
||||
// keep platform check available for future window chrome tweaks
|
||||
|
||||
@@ -536,12 +540,12 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
const settingsSearchResults = React.useMemo(() => {
|
||||
return buildSettingsSearchResults({
|
||||
query: settingsSearchQuery,
|
||||
runtimeCtx: { ...runtimeCtx, isDesktopLocalOrigin, isMac },
|
||||
runtimeCtx: { ...runtimeCtx, isDesktopLocalOrigin, isMac, isWindows },
|
||||
visiblePageSlugs,
|
||||
t,
|
||||
getPageTitle,
|
||||
});
|
||||
}, [getPageTitle, isDesktopLocalOrigin, isMac, runtimeCtx, settingsSearchQuery, t, visiblePageSlugs]);
|
||||
}, [getPageTitle, isDesktopLocalOrigin, isMac, isWindows, runtimeCtx, settingsSearchQuery, t, visiblePageSlugs]);
|
||||
|
||||
const prepareSettingsSearchTarget = React.useCallback((result: SettingsSearchResult): string => {
|
||||
if (result.id.startsWith('agents.')) {
|
||||
|
||||
Reference in New Issue
Block a user