From 8b0f4ba3bc5fc146bad4730b1ccdcd5be55f192d Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 8 Sep 2026 03:14:34 +0200 Subject: feat(client): create the system tray at launch, not on first minimise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ensureTray() was reachable only from the window:minimize-to-tray handler, so the indicator did not exist until you had already hidden the window into it. That is backwards on both desktops — most of what a tray is for is finding an application that is not in front of you — and on Windows it read as the app having no tray presence at all. Created during app.whenReady(), after registerBridge() and before createWindow(). The order matters: buildTrayMenu reads the nodeService that registerBridge assigns, so the other way round puts the Start/Stop entry on the menu one five-second poll late. The menu's labels were the one thing that came *from* the minimise call, since the main process has no i18n. A new tray:labels IPC (platform.setTrayLabels) carries them instead, sent from the renderer's boot once initLocale() has a catalogue; a language change reloads the page, so the same call covers it. The window between launch and that first message shows TRAY_FALLBACK, in English. §5.10's two platform gates become one — trayOS() in main.js, which every tray path calls. test_desktop_shell.py's existing test is rewritten against it and two are added: the launch ordering, and that no tray path tests process.platform inline instead of calling the gate. Windows still files a new tray icon under hidden icons until the person drags it onto the taskbar. No API promotes it; documented in WINDOWS-PORT.md §5.11 rather than worked around. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01V8EDjk6pkYZrCbo63m2x87 --- packages/meshbay-client/src/main.js | 30 +++++++- packages/meshbay-client/src/preload.js | 5 ++ packages/meshbay-hub/src/meshbay_hub/static/app.js | 21 ++++-- .../meshbay-hub/src/meshbay_hub/static/platform.js | 16 ++++- packages/meshbay-hub/tests/test_desktop_shell.py | 80 +++++++++++++++++----- 5 files changed, 126 insertions(+), 26 deletions(-) (limited to 'packages') diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index 27c81b8..0a5723b 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -330,6 +330,12 @@ let mainWindow = null; // ── System tray ────────────────────────────────────────────────────────────── // Linux and Windows. // +// Created at launch, not on the first "minimise to tray". An indicator that +// only appears once you have already hidden the window is one you cannot use +// to find the application, which is most of what a tray is for -- and on +// Windows it made the app look like it had no tray presence at all until you +// went looking for one. +// // The context menu is not optional. Under libappindicator -- which is how GNOME // shows a tray at all, via the AppIndicator extension -- `tray.on('click')` // never fires: the indicator only opens its menu. A tray whose only affordance @@ -342,6 +348,11 @@ let mainWindow = null; let tray = null; let trayLabels = null; let trayTimer = null; + +// The same test preload.js publishes as `capabilities.tray`. macOS is excluded +// deliberately: it has a menu bar rather than a tray, and the window controls +// there already do what hiding to an indicator does elsewhere. +const trayOS = () => process.platform === 'linux' || process.platform === 'win32'; let nodeService = null; // assigned by registerBridge() const TRAY_FALLBACK = { @@ -649,13 +660,24 @@ function registerBridge() { // make "minimise to tray" mean "exit". A hidden window keeps the session, the // transfers and the node connection exactly as they were. ipcMain.handle('window:minimize-to-tray', (_e, labels) => { - const trayOS = process.platform === 'linux' || process.platform === 'win32'; - if (!trayOS || !mainWindow) return false; + if (!trayOS() || !mainWindow) return false; ensureTray(labels); mainWindow.hide(); return true; }); + // The renderer sends these once it has a locale, and again after a language + // change (which reloads the page, so the same call covers both). Until then + // the tray created at launch shows TRAY_FALLBACK, in English, for the + // fraction of a second the catalogue takes to arrive -- the alternative, + // waiting for the renderer before creating it at all, is the behaviour this + // replaces. + ipcMain.handle('tray:labels', (_e, labels) => { + if (!trayOS()) return false; + ensureTray(labels); + return true; + }); + ipcMain.handle('device:ensure', () => ensureDeviceKey()); ipcMain.handle('device:public', () => { const key = deviceKey(); @@ -1660,7 +1682,11 @@ if (!app.requestSingleInstanceLock()) { app.whenReady().then(() => { registerUiProtocol(); + // Before ensureTray: buildTrayMenu reads `nodeService`, which registerBridge + // assigns, so creating the tray after it means the Start/Stop entry is on + // the very first menu rather than appearing one poll later. registerBridge(); + if (trayOS()) ensureTray(); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js index b390bfb..13d4f37 100644 --- a/packages/meshbay-client/src/preload.js +++ b/packages/meshbay-client/src/preload.js @@ -50,6 +50,11 @@ contextBridge.exposeInMainWorld('meshbay', { // Labels are passed in because the main process has no i18n; see main.js. minimizeToTray: (labels) => ipcRenderer.invoke('window:minimize-to-tray', labels), + // The tray now exists from launch, so its menu needs translating before + // anyone minimises into it. Sent once the interface has its catalogue, and + // again after a language change. + setTrayLabels: (labels) => ipcRenderer.invoke('tray:labels', labels), + // Ask the main process to call the hub. The renderer has an `app://` origin, // which CORS refuses and which is not a credential anyway. fetch: (url, init) => ipcRenderer.invoke('hub:fetch', url, init), diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 9f925c3..c2858f4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -245,9 +245,7 @@ function Nav({ user, theme, onThemeChange, onLogout, onMenuToggle, unreadCount,