From 1c7cdf68e3c88ae4ad6e058191e955cc42864fbb Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 22 Jul 2026 10:08:58 +0300 Subject: [PATCH] feat: add a macOS menu bar toggle for desktop Adds a General settings switch to show or hide the macOS menu bar item. Persists the new desktopMacMenuBarEnabled setting and passes tray state into Electron. Disables tray sync and native tray setup when the menu bar is turned off. --- packages/electron/README.md | 2 ++ packages/electron/main.mjs | 27 ++++++++++------ packages/electron/preload.mjs | 2 ++ .../openchamber/DesktopNetworkSettings.tsx | 31 +++++++++++++++++-- packages/ui/src/hooks/useTraySync.ts | 7 +++-- packages/ui/src/lib/desktop.ts | 2 ++ .../ui/src/lib/i18n/messages/en.settings.ts | 3 ++ .../ui/src/lib/i18n/messages/es.settings.ts | 3 ++ .../ui/src/lib/i18n/messages/fr.settings.ts | 3 ++ .../ui/src/lib/i18n/messages/ja.settings.ts | 3 ++ .../ui/src/lib/i18n/messages/ko.settings.ts | 3 ++ .../ui/src/lib/i18n/messages/pl.settings.ts | 3 ++ .../src/lib/i18n/messages/pt-BR.settings.ts | 3 ++ .../ui/src/lib/i18n/messages/uk.settings.ts | 3 ++ .../src/lib/i18n/messages/zh-CN.settings.ts | 3 ++ .../src/lib/i18n/messages/zh-TW.settings.ts | 3 ++ packages/ui/src/lib/persistence.ts | 3 ++ packages/ui/src/lib/settings/search.ts | 8 +++++ packages/ui/src/types/desktop.d.ts | 2 +- .../server/lib/opencode/settings-helpers.js | 3 ++ .../lib/opencode/settings-helpers.test.js | 14 +++++++++ 21 files changed, 116 insertions(+), 15 deletions(-) diff --git a/packages/electron/README.md b/packages/electron/README.md index 7dfa7efa..d783fc0b 100644 --- a/packages/electron/README.md +++ b/packages/electron/README.md @@ -90,6 +90,8 @@ A loopback-only updater fixture is available for contributor QA of N-to-N+1 AppI The package supports macOS, Windows, and Linux desktop features. Linux AppImage builds include in-app window controls and auto-update; system tray and launch-at-login remain macOS/Windows only. Some native discovery helpers are platform-specific. For example, app icon fetching and app filtering currently only work on macOS, while opening files in installed apps and installed-app discovery work on macOS and Windows (Linux returns an empty list without errors). +The macOS menu bar item is enabled by default and can be disabled in General settings. The setting applies after restart; while disabled, Desktop does not create the native tray controller or start the renderer subscriptions, polling, quota refresh, or IPC updates that feed it. + ## Bundled OpenCode CLI Packaged Desktop builds include the official OpenCode CLI that matches the pinned `@opencode-ai/sdk` version in the root `package.json`. `prepare:opencode-cli` downloads the platform-specific release artifact, caches it under `packages/electron/.cache/opencode-cli`, stages `opencode` or `opencode.exe` into `resources/opencode-cli`, and verifies `opencode --version` before packaging. Re-running the step is fast when the staged binary already matches the pinned version. diff --git a/packages/electron/main.mjs b/packages/electron/main.mjs index c0720727..fcddf8d2 100644 --- a/packages/electron/main.mjs +++ b/packages/electron/main.mjs @@ -215,6 +215,7 @@ const state = { sshStatuses: new Map(), sshLogs: new Map(), trayController: null, + trayFocusListener: null, lastFocusedWindowId: null, keepAwakeBlockerId: null, }; @@ -329,6 +330,10 @@ const prepareForQuit = ({ installingUpdate = false } = {}) => { } state.trayController = null; } + if (state.trayFocusListener) { + app.removeListener('browser-window-focus', state.trayFocusListener); + state.trayFocusListener = null; + } if (state.mainWindow && !state.mainWindow.isDestroyed()) { try { @@ -2221,6 +2226,7 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } const usesCustomTitleBar = process.platform === 'darwin' || usesFramelessChrome; // macOS vibrancy, on by default; users can disable it (Appearance settings). const useVibrancy = process.platform === 'darwin' && readSettingsRoot().desktopVibrancy !== false; + const trayEnabled = process.platform !== 'darwin' || readSettingsRoot().desktopMacMenuBarEnabled !== false; const titleBarOverlayEnabled = false; const autoHidesNativeMenuBar = process.platform !== 'darwin'; const windowIconPath = getWindowIconPath(); @@ -2256,6 +2262,7 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } `--openchamber-home=${desktopHome}`, `--openchamber-macos-major=${desktopMacosMajor}`, `--openchamber-mac-vibrancy=${useVibrancy ? '1' : '0'}`, + `--openchamber-tray-enabled=${trayEnabled ? '1' : '0'}`, `--openchamber-boot-outcome=${JSON.stringify(state.bootOutcome || null)}`, `--openchamber-relay-host-id=${rendererRuntimeConfig.relayHostId || ''}`, ], @@ -2619,6 +2626,7 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj const usesFramelessChrome = process.platform === 'win32' || process.platform === 'linux'; // macOS vibrancy, on by default; users can disable it (Appearance settings). const useVibrancy = process.platform === 'darwin' && readSettingsRoot().desktopVibrancy !== false; + const trayEnabled = process.platform !== 'darwin' || readSettingsRoot().desktopMacMenuBarEnabled !== false; const browserWindow = new BrowserWindow({ title: 'OpenChamber Mini Chat', width: MINI_CHAT_WINDOW_WIDTH, @@ -2644,6 +2652,7 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj `--openchamber-runtime-headers=${JSON.stringify(desktopRequestHeaders)}`, `--openchamber-home=${desktopHome}`, `--openchamber-macos-major=${desktopMacosMajor}`, + `--openchamber-tray-enabled=${trayEnabled ? '1' : '0'}`, ], preload: isDev ? path.join(__dirname, 'preload.mjs') : path.join(app.getAppPath(), 'preload.mjs'), backgroundThrottling: false, @@ -4634,15 +4643,6 @@ ipcMain.handle('openchamber:file:grant-existing', async (event, filePath) => { // Icon assets: a calm outline (idle), a statically filled cube (a finished // session left unread), and an eased sequence the busy state breathes through. const TRAY_BREATH_FRAME_COUNT = 16; -// Track the most recently focused window (main or mini-chat) so tray actions -// can target the surface the user was last using, even when the tray menu is -// open and nothing is focused right now. -app.on('browser-window-focus', (_event, browserWindow) => { - if (browserWindow && !browserWindow.isDestroyed()) { - state.lastFocusedWindowId = browserWindow.id; - } -}); - // The window the user is "on" for tray routing: the focused one, else the last // focused that is still alive. const resolveTraySurface = () => { @@ -4692,6 +4692,7 @@ const trayIconAssets = () => { const setupTray = () => { if (!['darwin', 'win32'].includes(process.platform) || state.trayController) return; + if (process.platform === 'darwin' && readSettingsRoot().desktopMacMenuBarEnabled === false) return; const assets = trayIconAssets(); if (!fs.existsSync(assets.idleIconPath)) { log.warn('[electron] tray icon missing, skipping tray setup', { iconPath: assets.idleIconPath }); @@ -4705,6 +4706,14 @@ const setupTray = () => { // Seed an empty snapshot so the icon appears immediately; the renderer // pushes the real state once the sync stores are mounted. state.trayController.update({ sessions: [], approvals: [] }); + if (!state.trayFocusListener) { + state.trayFocusListener = (_event, browserWindow) => { + if (browserWindow && !browserWindow.isDestroyed()) { + state.lastFocusedWindowId = browserWindow.id; + } + }; + app.on('browser-window-focus', state.trayFocusListener); + } } catch (error) { log.warn('[electron] failed to set up tray', error); state.trayController = null; diff --git a/packages/electron/preload.mjs b/packages/electron/preload.mjs index cf35004b..74ac1a95 100644 --- a/packages/electron/preload.mjs +++ b/packages/electron/preload.mjs @@ -22,6 +22,7 @@ const macVibrancySupported = process.platform === 'darwin'; // Effective state for this window (main process resolves the saved preference // and passes it in). Defaults on when supported unless explicitly '0'. const hasMacVibrancy = macVibrancySupported && readArgValue('--openchamber-mac-vibrancy') !== '0'; +const trayEnabled = process.platform !== 'darwin' || readArgValue('--openchamber-tray-enabled') !== '0'; // Preload re-executes on every cross-origin navigation (we run with // sandbox:false, per-document). Two separate concerns to balance: @@ -97,6 +98,7 @@ contextBridge.exposeInMainWorld('__OPENCHAMBER_ELECTRON__', { runtime: 'electron', macVibrancy: hasMacVibrancy, macVibrancySupported, + trayEnabled, }); contextBridge.exposeInMainWorld('__OPENCHAMBER_PLATFORM__', process.platform); diff --git a/packages/ui/src/components/sections/openchamber/DesktopNetworkSettings.tsx b/packages/ui/src/components/sections/openchamber/DesktopNetworkSettings.tsx index a7b6b4e9..e537c445 100644 --- a/packages/ui/src/components/sections/openchamber/DesktopNetworkSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/DesktopNetworkSettings.tsx @@ -42,6 +42,9 @@ export const DesktopNetworkSettings: React.FC = () => { const { t } = useI18n(); const tUnsafe = React.useCallback((key: string) => t(key as Parameters[0]), [t]); const isLocalDesktop = isDesktopShell() && isDesktopLocalOriginActive(); + const isMacDesktop = isLocalDesktop + && typeof window !== 'undefined' + && window.__OPENCHAMBER_PLATFORM__ === 'darwin'; const showWindowControlsPosition = usesFramelessElectronChrome(); const desktopWindowControlsPosition = useUIStore((state) => state.desktopWindowControlsPosition); const setDesktopWindowControlsPosition = useUIStore((state) => state.setDesktopWindowControlsPosition); @@ -60,6 +63,8 @@ export const DesktopNetworkSettings: React.FC = () => { const [minimizeToTraySupported, setMinimizeToTraySupported] = React.useState(false); const [minimizeToTrayEnabled, setMinimizeToTrayEnabled] = React.useState(false); const [isSavingMinimizeToTray, setIsSavingMinimizeToTray] = React.useState(false); + const [savedMacMenuBarEnabled, setSavedMacMenuBarEnabled] = React.useState(true); + const [draftMacMenuBarEnabled, setDraftMacMenuBarEnabled] = React.useState(true); const [keepAwakeSupported, setKeepAwakeSupported] = React.useState(false); const [keepAwakeEnabled, setKeepAwakeEnabled] = React.useState(false); const [isSavingKeepAwake, setIsSavingKeepAwake] = React.useState(false); @@ -88,6 +93,7 @@ export const DesktopNetworkSettings: React.FC = () => { desktopUiPassword?: unknown; desktopLanAccessActive?: unknown; desktopLanAccessBlockedReason?: unknown; + desktopMacMenuBarEnabled?: unknown; }; if (cancelled) { return; @@ -103,6 +109,9 @@ export const DesktopNetworkSettings: React.FC = () => { setLanAccessBlockedReason( typeof data?.desktopLanAccessBlockedReason === 'string' ? data.desktopLanAccessBlockedReason : null ); + const macMenuBarEnabled = data?.desktopMacMenuBarEnabled !== false; + setSavedMacMenuBarEnabled(macMenuBarEnabled); + setDraftMacMenuBarEnabled(macMenuBarEnabled); setError(null); } catch (cause) { if (!cancelled) { @@ -203,7 +212,9 @@ export const DesktopNetworkSettings: React.FC = () => { }; }, [draftValue, isLocalDesktop]); - const isDirty = draftValue !== savedValue || draftPassword !== savedPassword; + const isDirty = draftValue !== savedValue + || draftPassword !== savedPassword + || draftMacMenuBarEnabled !== savedMacMenuBarEnabled; const currentPort = React.useMemo(() => { if (typeof window === 'undefined') { return null; @@ -329,6 +340,7 @@ export const DesktopNetworkSettings: React.FC = () => { body: JSON.stringify({ desktopLanAccessEnabled: draftValue, desktopUiPassword: draftPassword, + desktopMacMenuBarEnabled: draftMacMenuBarEnabled, }), }); @@ -338,6 +350,7 @@ export const DesktopNetworkSettings: React.FC = () => { setSavedValue(draftValue); setSavedPassword(draftPassword); + setSavedMacMenuBarEnabled(draftMacMenuBarEnabled); const restarted = await restartDesktopApp(); if (!restarted) { @@ -347,7 +360,7 @@ export const DesktopNetworkSettings: React.FC = () => { setError(cause instanceof Error ? cause.message : t('settings.openchamber.desktopNetwork.error.saveFailed')); setIsSaving(false); } - }, [draftPassword, draftValue, isDirty, t]); + }, [draftMacMenuBarEnabled, draftPassword, draftValue, isDirty, t]); if (!isLocalDesktop && !showWindowControlsPosition) { return null; @@ -379,7 +392,7 @@ export const DesktopNetworkSettings: React.FC = () => { {isLocalDesktop ? (
- {(launchAtLoginSupported || minimizeToTraySupported || keepAwakeSupported) ? ( + {(launchAtLoginSupported || isMacDesktop || minimizeToTraySupported || keepAwakeSupported) ? (
{launchAtLoginSupported ? ( { /> ) : null} + {isMacDesktop ? ( + + ) : null} + {minimizeToTraySupported ? ( { return platform === 'darwin' || platform === 'win32'; }; +const isTrayEnabled = (): boolean => + typeof window !== 'undefined' && window.__OPENCHAMBER_ELECTRON__?.trayEnabled !== false; + const permissionLabel = (request: PermissionRequest): string => { const head = typeof request.permission === 'string' ? request.permission : 'Permission'; const pattern = Array.isArray(request.patterns) ? request.patterns.find((p) => typeof p === 'string' && p.trim()) : ''; @@ -418,7 +421,7 @@ const buildSnapshot = (instanceName: string): TraySnapshot => { export const useTraySync = (): void => { React.useEffect(() => { - if (!isTrayPlatform() || !canUseElectronDesktopIPC()) return; + if (!isTrayPlatform() || !isTrayEnabled() || !canUseElectronDesktopIPC()) return; let disposed = false; let lastSerialized = ''; @@ -578,7 +581,7 @@ export const useTraySync = (): void => { }, []); React.useEffect(() => { - if (!isTrayPlatform() || typeof window === 'undefined') return; + if (!isTrayPlatform() || !isTrayEnabled() || typeof window === 'undefined') return; const bridge = (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__; const listen = bridge?.listen; if (typeof listen !== 'function') return; diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts index 372325ee..2379463c 100644 --- a/packages/ui/src/lib/desktop.ts +++ b/packages/ui/src/lib/desktop.ts @@ -58,6 +58,7 @@ export type DesktopSettings = { desktopLanAccessEnabled?: boolean; desktopKeepAwakeEnabled?: boolean; desktopMinimizeToTrayEnabled?: boolean; + desktopMacMenuBarEnabled?: boolean; desktopUiPassword?: string; projects?: ProjectEntry[]; activeProjectId?: string; @@ -224,6 +225,7 @@ type ElectronRuntimeGlobal = { runtime?: string; macVibrancy?: boolean; macVibrancySupported?: boolean; + trayEnabled?: boolean; }; const getElectronRuntime = (): ElectronRuntimeGlobal | null => { diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index d6868da7..6ce2f82e 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -949,6 +949,9 @@ export const settingsDict = { 'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'Start OpenChamber at login', 'settings.openchamber.desktopNetwork.field.launchAtLogin': 'Start OpenChamber when you log in', 'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': 'Starts the app in the background without opening a window. Use the desktop status icon to open it.', + 'settings.openchamber.desktopNetwork.field.macMenuBarAria': 'Show OpenChamber in the macOS menu bar', + 'settings.openchamber.desktopNetwork.field.macMenuBar': 'Show OpenChamber in the menu bar', + 'settings.openchamber.desktopNetwork.field.macMenuBarDescription': 'Requires an app restart. When off, OpenChamber does not create the menu bar item or run its session, approval, and usage updates.', 'settings.openchamber.desktopNetwork.field.minimizeToTrayAria': 'Minimize and close OpenChamber to the system tray', 'settings.openchamber.desktopNetwork.field.minimizeToTray': 'Minimize and close to the system tray', 'settings.openchamber.desktopNetwork.field.minimizeToTrayDescription': 'Keeps OpenChamber running in the Windows system tray when the main window is minimized or closed.', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index 288591d1..89baaf06 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -916,6 +916,9 @@ export const settingsDict = { "settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Iniciar OpenChamber al iniciar sesión", "settings.openchamber.desktopNetwork.field.launchAtLogin": "Iniciar OpenChamber al iniciar sesión", "settings.openchamber.desktopNetwork.field.launchAtLoginDescription": "Inicia la app en segundo plano sin abrir una ventana. Usa el icono de estado del escritorio para abrirla.", + "settings.openchamber.desktopNetwork.field.macMenuBarAria": "Mostrar OpenChamber en la barra de menús de macOS", + "settings.openchamber.desktopNetwork.field.macMenuBar": "Mostrar OpenChamber en la barra de menús", + "settings.openchamber.desktopNetwork.field.macMenuBarDescription": "Requiere reiniciar la aplicación. Al desactivarlo, OpenChamber no crea el elemento de la barra de menús ni ejecuta sus actualizaciones de sesiones, aprobaciones y uso.", "settings.openchamber.desktopNetwork.field.minimizeToTrayAria": "Minimizar y cerrar OpenChamber a la bandeja del sistema", "settings.openchamber.desktopNetwork.field.minimizeToTray": "Minimizar y cerrar a la bandeja del sistema", "settings.openchamber.desktopNetwork.field.minimizeToTrayDescription": "Mantiene OpenChamber en ejecución en la bandeja del sistema de Windows cuando la ventana principal se minimiza o se cierra.", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index 740b2773..a6aa451b 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -837,6 +837,9 @@ export const settingsDict = { 'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'Démarrez OpenChamber lors de la connexion', 'settings.openchamber.desktopNetwork.field.launchAtLogin': 'Démarrez OpenChamber lorsque vous vous connectez', 'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': 'Démarre l\'application en arrière-plan sans ouvrir de fenêtre. Utilisez l\'icône d\'état du bureau pour l\'ouvrir.', + 'settings.openchamber.desktopNetwork.field.macMenuBarAria': 'Afficher OpenChamber dans la barre des menus macOS', + 'settings.openchamber.desktopNetwork.field.macMenuBar': 'Afficher OpenChamber dans la barre des menus', + 'settings.openchamber.desktopNetwork.field.macMenuBarDescription': 'Nécessite un redémarrage de l\'application. Lorsque cette option est désactivée, OpenChamber ne crée pas l\'élément de barre des menus et n\'exécute pas ses mises à jour de sessions, d\'approbations et d\'utilisation.', 'settings.openchamber.desktopNetwork.field.minimizeToTrayAria': 'Réduire et fermer OpenChamber dans la zone de notification', 'settings.openchamber.desktopNetwork.field.minimizeToTray': 'Réduire et fermer dans la zone de notification', 'settings.openchamber.desktopNetwork.field.minimizeToTrayDescription': 'Garde OpenChamber actif dans la zone de notification Windows lorsque la fenêtre principale est réduite ou fermée.', diff --git a/packages/ui/src/lib/i18n/messages/ja.settings.ts b/packages/ui/src/lib/i18n/messages/ja.settings.ts index 1d7c1d27..478d5fdb 100644 --- a/packages/ui/src/lib/i18n/messages/ja.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja.settings.ts @@ -949,6 +949,9 @@ export const settingsDict = { 'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'ログイン時に OpenChamber を起動', 'settings.openchamber.desktopNetwork.field.launchAtLogin': 'ログイン時に OpenChamber を起動', 'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': 'ウィンドウを開かずにバックグラウンドでアプリを起動します。デスクトップのステータスアイコンから開けます。', + 'settings.openchamber.desktopNetwork.field.macMenuBarAria': 'macOS のメニューバーに OpenChamber を表示', + 'settings.openchamber.desktopNetwork.field.macMenuBar': 'メニューバーに OpenChamber を表示', + 'settings.openchamber.desktopNetwork.field.macMenuBarDescription': 'アプリの再起動が必要です。オフにすると、OpenChamber はメニューバー項目を作成せず、セッション、承認、使用量の更新も実行しません。', 'settings.openchamber.desktopNetwork.field.minimizeToTrayAria': 'OpenChamber をシステムトレイへ最小化または閉じる', 'settings.openchamber.desktopNetwork.field.minimizeToTray': 'システムトレイへ最小化または閉じる', 'settings.openchamber.desktopNetwork.field.minimizeToTrayDescription': 'メインウィンドウを最小化または閉じたときも、OpenChamber を Windows のシステムトレイで実行し続けます。', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index 28813f8b..5066f388 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -916,6 +916,9 @@ export const settingsDict = { 'settings.openchamber.desktopNetwork.field.launchAtLoginAria': '로그인 시 OpenChamber 시작', 'settings.openchamber.desktopNetwork.field.launchAtLogin': '로그인 시 OpenChamber 시작', 'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': '창을 열지 않고 백그라운드에서 앱을 시작합니다. 데스크톱 상태 아이콘으로 열 수 있습니다.', + 'settings.openchamber.desktopNetwork.field.macMenuBarAria': 'macOS 메뉴 막대에 OpenChamber 표시', + 'settings.openchamber.desktopNetwork.field.macMenuBar': '메뉴 막대에 OpenChamber 표시', + 'settings.openchamber.desktopNetwork.field.macMenuBarDescription': '앱을 다시 시작해야 합니다. 끄면 OpenChamber가 메뉴 막대 항목을 만들지 않으며 세션, 승인 및 사용량 업데이트도 실행하지 않습니다.', 'settings.openchamber.desktopNetwork.field.minimizeToTrayAria': 'OpenChamber를 시스템 트레이로 최소화하거나 닫기', 'settings.openchamber.desktopNetwork.field.minimizeToTray': '시스템 트레이로 최소화하고 닫기', 'settings.openchamber.desktopNetwork.field.minimizeToTrayDescription': '메인 창을 최소화하거나 닫아도 OpenChamber가 Windows 시스템 트레이에서 계속 실행됩니다.', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index 00324bf4..1699f680 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -756,6 +756,9 @@ export const settingsDict = { 'settings.openchamber.desktopNetwork.field.launchAtLogin': 'Uruchamiaj OpenChamber przy logowaniu', 'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'Uruchamiaj OpenChamber przy logowaniu', 'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': 'Uruchamia aplikację w tle bez otwierania okna. Otworzysz ją z ikony stanu na pulpicie.', + 'settings.openchamber.desktopNetwork.field.macMenuBarAria': 'Pokazuj OpenChamber na pasku menu macOS', + 'settings.openchamber.desktopNetwork.field.macMenuBar': 'Pokazuj OpenChamber na pasku menu', + 'settings.openchamber.desktopNetwork.field.macMenuBarDescription': 'Wymaga ponownego uruchomienia aplikacji. Po wyłączeniu OpenChamber nie tworzy elementu paska menu ani nie uruchamia aktualizacji sesji, zatwierdzeń i użycia.', 'settings.openchamber.desktopNetwork.field.minimizeToTrayAria': 'Minimalizuj i zamykaj OpenChamber do zasobnika systemowego', 'settings.openchamber.desktopNetwork.field.minimizeToTray': 'Minimalizuj i zamykaj do zasobnika systemowego', 'settings.openchamber.desktopNetwork.field.minimizeToTrayDescription': 'Utrzymuje OpenChamber w zasobniku systemowym Windows, gdy główne okno jest zminimalizowane lub zamknięte.', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index f8421526..3f4b08be 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -916,6 +916,9 @@ export const settingsDict = { "settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Iniciar o OpenChamber ao fazer login", "settings.openchamber.desktopNetwork.field.launchAtLogin": "Iniciar o OpenChamber ao fazer login", "settings.openchamber.desktopNetwork.field.launchAtLoginDescription": "Inicia o app em segundo plano sem abrir uma janela. Use o ícone de status da área de trabalho para abrir.", + "settings.openchamber.desktopNetwork.field.macMenuBarAria": "Mostrar o OpenChamber na barra de menus do macOS", + "settings.openchamber.desktopNetwork.field.macMenuBar": "Mostrar o OpenChamber na barra de menus", + "settings.openchamber.desktopNetwork.field.macMenuBarDescription": "Exige reiniciar o aplicativo. Quando desativado, o OpenChamber não cria o item da barra de menus nem executa as atualizações de sessões, aprovações e uso.", "settings.openchamber.desktopNetwork.field.minimizeToTrayAria": "Minimizar e fechar o OpenChamber na bandeja do sistema", "settings.openchamber.desktopNetwork.field.minimizeToTray": "Minimizar e fechar na bandeja do sistema", "settings.openchamber.desktopNetwork.field.minimizeToTrayDescription": "Mantém o OpenChamber em execução na bandeja do sistema do Windows quando a janela principal é minimizada ou fechada.", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 30c6e680..c590e73a 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -916,6 +916,9 @@ export const settingsDict = { "settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Запускати OpenChamber під час входу в систему", "settings.openchamber.desktopNetwork.field.launchAtLogin": "Запускати OpenChamber під час входу в систему", "settings.openchamber.desktopNetwork.field.launchAtLoginDescription": "Запускає застосунок у фоні без відкриття вікна. Відкрийте його через піктограму стану на робочому столі.", + "settings.openchamber.desktopNetwork.field.macMenuBarAria": "Показувати OpenChamber у смузі меню macOS", + "settings.openchamber.desktopNetwork.field.macMenuBar": "Показувати OpenChamber у смузі меню", + "settings.openchamber.desktopNetwork.field.macMenuBarDescription": "Потребує перезапуску застосунку. Якщо вимкнено, OpenChamber не створює елемент смуги меню та не запускає пов’язані оновлення сесій, запитів на підтвердження й використання.", "settings.openchamber.desktopNetwork.field.minimizeToTrayAria": "Згортати й закривати OpenChamber у системний трей", "settings.openchamber.desktopNetwork.field.minimizeToTray": "Згортати й закривати в системний трей", "settings.openchamber.desktopNetwork.field.minimizeToTrayDescription": "Залишає OpenChamber запущеним у системному треї Windows, коли головне вікно згорнуто або закрито.", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index 871f9f66..0373577b 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -916,6 +916,9 @@ export const settingsDict = { 'settings.openchamber.desktopNetwork.field.launchAtLoginAria': '登录时启动 OpenChamber', 'settings.openchamber.desktopNetwork.field.launchAtLogin': '登录时启动 OpenChamber', 'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': '在后台启动应用且不打开窗口。可通过桌面状态图标打开。', + 'settings.openchamber.desktopNetwork.field.macMenuBarAria': '在 macOS 菜单栏中显示 OpenChamber', + 'settings.openchamber.desktopNetwork.field.macMenuBar': '在菜单栏中显示 OpenChamber', + 'settings.openchamber.desktopNetwork.field.macMenuBarDescription': '需要重启应用。关闭后,OpenChamber 不会创建菜单栏项目,也不会运行相关的会话、审批和用量更新。', 'settings.openchamber.desktopNetwork.field.minimizeToTrayAria': '将 OpenChamber 最小化和关闭到系统托盘', 'settings.openchamber.desktopNetwork.field.minimizeToTray': '最小化和关闭到系统托盘', 'settings.openchamber.desktopNetwork.field.minimizeToTrayDescription': '主窗口最小化或关闭时,让 OpenChamber 继续在 Windows 系统托盘中运行。', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index 86fef3e6..15d61334 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -1928,6 +1928,9 @@ 'settings.openchamber.desktopNetwork.field.launchAtLogin': '登入時自動啟動 OpenChamber', 'settings.openchamber.desktopNetwork.field.launchAtLoginAria': '登入時自動啟動 OpenChamber', 'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': '在背景啟動應用程式,不開啟視窗。可透過桌面狀態圖示開啟。', + 'settings.openchamber.desktopNetwork.field.macMenuBarAria': '在 macOS 選單列中顯示 OpenChamber', + 'settings.openchamber.desktopNetwork.field.macMenuBar': '在選單列中顯示 OpenChamber', + 'settings.openchamber.desktopNetwork.field.macMenuBarDescription': '需要重新啟動應用程式。關閉後,OpenChamber 不會建立選單列項目,也不會執行相關的工作階段、核准與用量更新。', 'settings.openchamber.desktopNetwork.field.minimizeToTrayAria': '將 OpenChamber 最小化和關閉到系統匣', 'settings.openchamber.desktopNetwork.field.minimizeToTray': '最小化和關閉到系統匣', 'settings.openchamber.desktopNetwork.field.minimizeToTrayDescription': '主視窗最小化或關閉時,讓 OpenChamber 繼續在 Windows 系統匣中執行。', diff --git a/packages/ui/src/lib/persistence.ts b/packages/ui/src/lib/persistence.ts index 38744b7c..621d584a 100644 --- a/packages/ui/src/lib/persistence.ts +++ b/packages/ui/src/lib/persistence.ts @@ -993,6 +993,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => { if (typeof candidate.desktopMinimizeToTrayEnabled === 'boolean') { result.desktopMinimizeToTrayEnabled = candidate.desktopMinimizeToTrayEnabled; } + if (typeof candidate.desktopMacMenuBarEnabled === 'boolean') { + result.desktopMacMenuBarEnabled = candidate.desktopMacMenuBarEnabled; + } const projects = sanitizeProjects(candidate.projects); if (projects) { diff --git a/packages/ui/src/lib/settings/search.ts b/packages/ui/src/lib/settings/search.ts index bea64f51..bc8ddb31 100644 --- a/packages/ui/src/lib/settings/search.ts +++ b/packages/ui/src/lib/settings/search.ts @@ -396,6 +396,14 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [ keywords: ['desktop', 'window', 'controls', 'minimize', 'maximize', 'close', 'titlebar', 'linux', 'windows'], isAvailable: (ctx) => ctx.isDesktop && (ctx.isWindows || !ctx.isMac), }, + { + id: 'sessions.desktop-mac-menu-bar', + page: 'general', + titleKey: 'settings.openchamber.desktopNetwork.field.macMenuBar', + descriptionKey: 'settings.openchamber.desktopNetwork.field.macMenuBarDescription', + keywords: ['desktop', 'menu bar', 'tray', 'status item', 'macos', 'background'], + isAvailable: (ctx) => ctx.isDesktopLocalOrigin && ctx.isMac, + }, { id: 'sessions.desktop-minimize-to-tray', page: 'general', diff --git a/packages/ui/src/types/desktop.d.ts b/packages/ui/src/types/desktop.d.ts index 2718e17d..069dd056 100644 --- a/packages/ui/src/types/desktop.d.ts +++ b/packages/ui/src/types/desktop.d.ts @@ -5,7 +5,7 @@ declare global { __OPENCHAMBER_HOME__?: string; __OPENCHAMBER_MACOS_MAJOR__?: number; __OPENCHAMBER_LOCAL_ORIGIN__?: string; - __OPENCHAMBER_ELECTRON__?: { runtime?: string; macVibrancy?: boolean; macVibrancySupported?: boolean }; + __OPENCHAMBER_ELECTRON__?: { runtime?: string; macVibrancy?: boolean; macVibrancySupported?: boolean; trayEnabled?: boolean }; __OPENCHAMBER_PLATFORM__?: string; __OPENCHAMBER_DESKTOP_BOOT_OUTCOME__?: DesktopBootOutcome; } diff --git a/packages/web/server/lib/opencode/settings-helpers.js b/packages/web/server/lib/opencode/settings-helpers.js index b8e21eee..2964e56a 100644 --- a/packages/web/server/lib/opencode/settings-helpers.js +++ b/packages/web/server/lib/opencode/settings-helpers.js @@ -185,6 +185,9 @@ export const createSettingsHelpers = (dependencies) => { if (typeof candidate.desktopMinimizeToTrayEnabled === 'boolean') { result.desktopMinimizeToTrayEnabled = candidate.desktopMinimizeToTrayEnabled; } + if (typeof candidate.desktopMacMenuBarEnabled === 'boolean') { + result.desktopMacMenuBarEnabled = candidate.desktopMacMenuBarEnabled; + } if (typeof candidate.desktopWindowControlsPosition === 'string') { const mode = candidate.desktopWindowControlsPosition.trim(); if (mode === 'auto' || mode === 'left' || mode === 'right') { diff --git a/packages/web/server/lib/opencode/settings-helpers.test.js b/packages/web/server/lib/opencode/settings-helpers.test.js index b6aff598..3336c902 100644 --- a/packages/web/server/lib/opencode/settings-helpers.test.js +++ b/packages/web/server/lib/opencode/settings-helpers.test.js @@ -124,6 +124,20 @@ describe('settings helpers', () => { }); }); + it('accepts desktopMacMenuBarEnabled as a persisted shared setting', () => { + const helpers = createTestHelpers(); + + expect(helpers.sanitizeSettingsUpdate({ desktopMacMenuBarEnabled: true })).toEqual({ + desktopMacMenuBarEnabled: true, + }); + expect(helpers.sanitizeSettingsUpdate({ desktopMacMenuBarEnabled: false })).toEqual({ + desktopMacMenuBarEnabled: false, + }); + expect(helpers.formatSettingsResponse({ desktopMacMenuBarEnabled: false })).toMatchObject({ + desktopMacMenuBarEnabled: false, + }); + }); + it('sanitizes the persisted permission auto-accept policy', () => { const helpers = createTestHelpers();