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();