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.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user