diff --git a/CHANGELOG.md b/CHANGELOG.md index 5840b66b74..4c990e2a1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ _In development — bullets added per PR; finalized at release._ ### 🔧 Bug Fixes +- **cli (tray):** fix `omniroute server --tray` showing no tray on macOS/Linux with no error printed. The wired Unix tray path loaded `systray2` through an inline loader that called `require("module")` inside an ESM `.mjs` file (`"type":"module"`) → `ReferenceError: require is not defined`, silently swallowed (regressed in v3.8.34); even if it had loaded, `systray2` isn't in `node_modules` (it's lazily installed into `~/.omniroute/runtime`). The loader now delegates to the runtime loader, the icon path (`icon.png`) is corrected, `isTemplateIcon` is `false` (the full-color icon rendered as a white square under macOS template mode), and tray start failures are surfaced to stderr instead of being swallowed (#4605 — thanks @ProgMEM-CC) - **agent-bridge (antigravity):** unwrap the cloudcode-pa `.request` envelope when converting Antigravity IDE requests. The real IDE sends `cloudcode-pa.googleapis.com/v1internal:generateContent` with the Gemini request nested under `.request` (`{ project, model, request: { contents, systemInstruction, generationConfig } }`), but the bridge read those fields at the top level — yielding an empty conversation, so prompts hung mid-execution. The legacy `/v1beta/models/:generateContent` top-level shape still works (#4294 — thanks @shabeer) - **dashboard:** add a GitHub releases fallback to the "Update Available" lookup. After the v3.8.28 fix added an npm-registry HTTP fallback, the banner could still stay hidden on networks that reach GitHub (where the news feed already loads) but not `registry.npmjs.org`. `resolveLatestVersion()` now tries npm CLI → npm registry → GitHub releases (`/repos/diegosouzapw/OmniRoute/releases/latest`) before giving up, and logs a warning only when all three fail (#4100) - **command-code:** omit `max_tokens` when the client omits it so the upstream applies the model's native default, fixing `400 "expected <=200000"` on `/alpha/generate` for high-cap models; an explicit oversized client value is clamped to the 200k endpoint ceiling (#5221 — thanks @adivekar-utexas) diff --git a/bin/cli/commands/serve.mjs b/bin/cli/commands/serve.mjs index 45c6a06a30..eed761c693 100644 --- a/bin/cli/commands/serve.mjs +++ b/bin/cli/commands/serve.mjs @@ -313,7 +313,7 @@ async function maybeStartTray(port, apiPort, supervisor) { if (!isTraySupported()) return; const { default: open } = await import("open").catch(() => ({ default: null })); const dashboardUrl = `http://localhost:${port}`; - const tray = initTray({ + const tray = await initTray({ port, onQuit: () => { killTrayIfActive(); @@ -329,8 +329,12 @@ async function maybeStartTray(port, apiPort, supervisor) { const { killTray } = await import("../tray/index.mjs"); _killTray = killTray; } - } catch { - // tray is optional — do not fail the server + } catch (err) { + // tray is optional — do not fail the server, but surface why it failed so + // "--tray shows nothing" is diagnosable instead of silent (#4605). + process.stderr.write( + `[omniroute][tray] failed to start: ${err?.message ?? String(err)}\n` + ); } } diff --git a/bin/cli/tray/index.mjs b/bin/cli/tray/index.mjs index ab68812591..5745062e66 100644 --- a/bin/cli/tray/index.mjs +++ b/bin/cli/tray/index.mjs @@ -5,10 +5,12 @@ let active = null; export { isTraySupported }; -export function initTray({ port, onQuit, onOpenDashboard, onShowLogs }) { +export async function initTray({ port, onQuit, onOpenDashboard, onShowLogs }) { if (!isTraySupported()) return null; const ctx = { port, onQuit, onOpenDashboard, onShowLogs }; - active = process.platform === "win32" ? initWinTray(ctx) : initSystrayUnix(ctx); + // initSystrayUnix is async: it lazily installs/loads systray2 from the runtime + // dir (trayRuntime.ts) rather than from node_modules. (#4605) + active = process.platform === "win32" ? initWinTray(ctx) : await initSystrayUnix(ctx); return active; } diff --git a/bin/cli/tray/traySystray.mjs b/bin/cli/tray/traySystray.mjs index 4c5bcb3c5e..c7916a4108 100644 --- a/bin/cli/tray/traySystray.mjs +++ b/bin/cli/tray/traySystray.mjs @@ -14,30 +14,31 @@ export function isTraySupported() { return true; } -function loadSystray2() { - const candidates = [ - () => { - const { createRequire } = require("module"); - const req = createRequire(import.meta.url); - return req("systray2").default; - }, - ]; - for (const attempt of candidates) { - try { - return attempt(); - } catch {} - } - return null; +// systray2 is NOT a static dependency — it is lazily installed into +// ~/.omniroute/runtime by trayRuntime.ts (loadSystray). The previous inline +// loader called `require("module")`, which throws `ReferenceError: require is +// not defined` in this ESM file (package "type":"module"); the throw was +// silently swallowed, so the tray never appeared on macOS/Linux with no error +// printed (#4605, regressed in v3.8.34). Delegate to the runtime loader, which +// resolves systray2 from the runtime dir and surfaces install/import failures. +async function loadSystray2() { + const { loadSystray } = await import("../runtime/trayRuntime.ts"); + return loadSystray(); } function getIconBase64() { - const iconPath = join(__dirname, "icons", "icon.png"); + // Icon ships at bin/cli/tray/icon.png — the previous "icons/icon.png" path + // never existed, so the tray was created with an empty icon (#4605). + const iconPath = join(__dirname, "icon.png"); if (existsSync(iconPath)) return readFileSync(iconPath).toString("base64"); return ""; } -export function initSystrayUnix({ port, onQuit, onOpenDashboard, onShowLogs }) { - const SysTray = loadSystray2(); +export async function initSystrayUnix( + { port, onQuit, onOpenDashboard, onShowLogs }, + loadCtor = loadSystray2 +) { + const SysTray = await loadCtor(); if (!SysTray) return null; const autostartEnabled = isAutostartEnabled(); @@ -57,7 +58,10 @@ export function initSystrayUnix({ port, onQuit, onOpenDashboard, onShowLogs }) { tray = new SysTray({ menu: { icon: getIconBase64(), - isTemplateIcon: process.platform === "darwin", + // isTemplateIcon must be false: icon.png is a full-color RGBA logo, and + // macOS template mode uses only the alpha channel → a solid white square + // (the icon looked "missing" even when the tray loaded). (PR #1080) + isTemplateIcon: false, title: "", tooltip: `OmniRoute — port ${port}`, items, diff --git a/tests/unit/tray-systray-loader-4605.test.ts b/tests/unit/tray-systray-loader-4605.test.ts new file mode 100644 index 0000000000..adbf760308 --- /dev/null +++ b/tests/unit/tray-systray-loader-4605.test.ts @@ -0,0 +1,80 @@ +// Regression coverage for #4605 — `omniroute server --tray` showed no tray on +// macOS/Linux with no error printed. +// +// Root cause (regressed in v3.8.34): the wired Unix tray path +// (serve.mjs → tray/index.mjs → traySystray.mjs) loaded systray2 via an inline +// loader that called `require("module")` inside an ESM `.mjs` file (package +// "type":"module"). `require` is undefined in ESM, so it threw +// `ReferenceError: require is not defined`, which a bare `catch {}` swallowed → +// `loadSystray2()` returned null → no tray, no diagnostic. Even had it loaded, +// systray2 is not in node_modules (it is lazily installed into +// ~/.omniroute/runtime by trayRuntime.ts), the icon was read from a +// non-existent "icons/icon.png" path, and `isTemplateIcon` was true on darwin +// (full-color icon → white square). +// +// The fix makes `initSystrayUnix` async and delegates loading to the runtime +// loader. We can't spawn the real native Go binary in a unit test, so — like +// cli-tray-systray2.test.ts — we drive the seam with an injected ctor loader +// and assert the menu/icon/template invariants. + +import test from "node:test"; +import assert from "node:assert/strict"; +import { initSystrayUnix } from "../../bin/cli/tray/traySystray.mjs"; + +class FakeSysTray { + static lastOpts: unknown = null; + onClickFn: unknown = null; + constructor(opts: unknown) { + FakeSysTray.lastOpts = opts; + } + onClick(fn: unknown) { + this.onClickFn = fn; + } + ready() { + return Promise.resolve(); + } + sendAction() {} + kill() {} +} + +const opts = { + port: 20128, + onQuit: () => {}, + onOpenDashboard: () => {}, + onShowLogs: () => {}, +}; + +test("initSystrayUnix loads the injected SysTray ctor and builds the menu (#4605)", async () => { + FakeSysTray.lastOpts = null; + // Async loader seam — proves initSystrayUnix awaits the runtime loader instead + // of the old broken inline `require("module")` path. + const tray = await initSystrayUnix(opts, async () => FakeSysTray as unknown as new () => unknown); + + assert.ok(tray, "a tray instance must be created when a SysTray ctor is available"); + + const menu = (FakeSysTray.lastOpts as { menu: Record }).menu; + // White-square fix: must not be a macOS template icon. + assert.equal(menu.isTemplateIcon, false, "isTemplateIcon must be false (full-color icon)"); + // Icon-path fix: the bundled icon.png must resolve to a non-empty base64 blob. + assert.ok( + typeof menu.icon === "string" && (menu.icon as string).length > 0, + "menu.icon must be a non-empty base64 string (icon.png path fix)" + ); + + const items = menu.items as Array<{ title: string }>; + const titles = items.map((i) => i.title); + assert.equal(items.length, 5, "menu should keep its 5 items"); + assert.ok( + titles.includes("Show Logs"), + `the "Show Logs" item must be preserved, got: ${titles.join(", ")}` + ); + assert.ok( + titles.includes("Quit OmniRoute"), + `the Quit item must be present, got: ${titles.join(", ")}` + ); +}); + +test("initSystrayUnix returns null without throwing when the loader yields null (#4605)", async () => { + const tray = await initSystrayUnix(opts, async () => null); + assert.equal(tray, null, "must degrade to null when systray2 cannot be loaded"); +});