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:
achcyano
2026-07-10 12:40:48 +03:00
committed by GitHub
parent 51e6ae7e3f
commit 4296efb64d
20 changed files with 325 additions and 39 deletions
@@ -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.')) {