mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 12:22:14 +03:00
The wired tray path (serve.mjs -> tray/index.mjs -> traySystray.mjs) loaded
systray2 via an inline loader calling require("module") inside an ESM .mjs file
(package type:module), which throws ReferenceError: require is not defined and
was silently swallowed -> no tray, no diagnostic (regressed in v3.8.34). Even
when fixed, systray2 is not in node_modules; it is lazily installed into
~/.omniroute/runtime by trayRuntime.ts.
initSystrayUnix now delegates loading to trayRuntime.ts::loadSystray (async),
fixes the icon path (icon.png, not icons/icon.png), sets isTemplateIcon:false
(full-color icon rendered as a white square under macOS template mode), and
serve.mjs surfaces tray-start failures to stderr instead of swallowing them.
TDD: tray-systray-loader-4605.test.ts drives the loader seam via an injected
ctor (native binary can't run in unit tests) and asserts the menu/icon/template
invariants; fail-before confirmed by restoring the broken require() loader.
This commit is contained in:
committed by
GitHub
parent
99b17f13b6
commit
ba38e31ff4
@@ -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/<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)
|
||||
|
||||
@@ -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`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
80
tests/unit/tray-systray-loader-4605.test.ts
Normal file
80
tests/unit/tray-systray-loader-4605.test.ts
Normal file
@@ -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");
|
||||
});
|
||||
Reference in New Issue
Block a user