Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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/<model>: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)
Expand Down
10 changes: 7 additions & 3 deletions bin/cli/commands/serve.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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`
);
}
}

Expand Down
6 changes: 4 additions & 2 deletions bin/cli/tray/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
40 changes: 22 additions & 18 deletions bin/cli/tray/traySystray.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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,
Expand Down
80 changes: 80 additions & 0 deletions tests/unit/tray-systray-loader-4605.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> }).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");
});
Loading