Electron Package Smoke — a packaging defect that had been hidden behind another
packaging defect for nine days. Once the loginHeaderCapture fix let the main process
start, the server underneath died on 'Cannot find module next': resources/app/server.js
shipped without resources/app/node_modules.
electron-builder discards the ROOT node_modules in code, not by configuration —
app-builder-lib/out/util/filter.js:42 has a hard-coded `if (relative === "node_modules")
return false` that runs before any filter pattern. The second extraResources entry
pointing INTO ../.build/electron-standalone/node_modules is what sidesteps it, because
those relative paths are never equal to "node_modules". #10325 removed that entry as an
apparent duplicate and flipped the test to assert "exactly once", freezing the
regression as if it were the contract. Restored, and the unit guard now pins both
entries — proven by mutation: reverting package.json to the post-#10325 shape fails the
guard 3/4, restoring it passes 4/4.
group-b-quota-plans-config — the assertion was impossible to satisfy on ANY route, and
the page was never broken. layout.tsx hands the whole message catalogue to
NextIntlClientProvider, React serialises that prop into the RSC payload, and en.json
carries "Internal Server Error" twice, so page.content() always contains it: probing
/dashboard, /dashboard/costs, /dashboard/settings and /login showed the string present
with every page rendering fine, and a pageerror probe on the failing run captured zero
client exceptions. This is the same trap that killed the sibling not.toContain("500")
in fc77100c3f ("raw HTML is unreliable") — that one was removed, this one was kept.
Now asserts on rendered text, which still catches a real error boundary. The pageerror
capture stays: the CI failure carried no stack trace, which is why it was misread twice.
Integration — 10 of the 14 shard-2 reds, all sibling-test gaps behind security fixes:
monitoring health now takes a Request and requires management auth (GHSA-mvf8-qc78-5mxm);
the OAuth import routes moved to requireManagementAuth (GHSA-mg76) — the test accepts
both guard shapes and gained a stronger anchor that every exported handler awaits a
guard on its own request, mutation-verified; skill tool names are derived from
encodeSkillToolName() and the fake upstream now returns the encoded name so
decodeSkillToolName() is exercised too; previous_response_id now fails closed (#10262);
proxy_logs persist as an async batch (#11182) so the test flushes first;
providerQuotaOverrides joined GET /api/resilience (#9871); the reasoning fixture used a
model that stopped being thinking-incompatible, replaced and pinned with a premise
assert so it cannot rot silently again.
A vacuous assert.ok(true, "all 10 streams completed without hanging") was replaced with
real anchors — content must arrive on every stream and the active Timeout count must not
grow.
Four are deliberately left red rather than aligned, each now tracked: #11551 (the
/v1/models after() wiring is dead — the route passes a third argument to a two-parameter
function and catalogCache never imports after, so the #8728 contract is unimplemented),
#11552 (~27% of requests emit an extra discarded upstream call; the delivered
distribution is exactly 0.70, so weighted routing is correct and the waste is the real
finding), the fixed-account combo pin (aligning it would destroy the per-step attribution
the test exists for), and the web_search fallback already tracked as #11524.
Package Artifact — the provenance stamp I added last round used git rev-parse HEAD, which
under pull_request is the ephemeral merge commit and therefore never an ancestor of the
release branch. Now takes the PR head sha.
Refs #10692
OmniRoute Electron Desktop App
This directory contains the Electron desktop application wrapper for OmniRoute.
Architecture (v1.6.4)
electron/
├── main.js # Main process — window, tray, server lifecycle, CSP, IPC
├── preload.js # Preload script — secure IPC bridge with disposer pattern
├── package.json # Electron-specific dependencies & electron-builder config
├── types.d.ts # TypeScript definitions (AppInfo, ServerStatus, ElectronAPI)
└── assets/ # Application icons and resources
src/shared/hooks/
└── useElectron.ts # React hooks — useSyncExternalStore, zero re-renders
Key Design Decisions
| Decision | Rationale |
|---|---|
waitForServer() polling |
Prevents blank screen on cold start — polls http://localhost:PORT before loading |
stdio: 'pipe' |
Captures server stdout/stderr for logging + readiness detection (not inherit) |
| Disposer pattern | onServerStatus() returns () => void for precise listener cleanup (no removeAllListeners) |
useSyncExternalStore |
Zero re-renders for useIsElectron() — no useState + useEffect cycle |
| CSP via session headers | Content-Security-Policy restricts script-src, connect-src etc. per Electron best practices |
| Platform-conditional titlebar | titleBarStyle: 'hiddenInset' only on macOS; default on Windows/Linux |
Development
Prerequisites
- Build the Next.js app first:
npm run build
- Install Electron dependencies:
cd electron
npm install
Running in Development
- Start the Next.js development server:
npm run dev
- In another terminal, start Electron:
cd electron
npm run dev
Running in Production Mode
- Build Next.js in standalone mode:
npm run build
- Start Electron:
cd electron
npm start
Building
Build for Current Platform
cd electron
npm run build
Build for Specific Platforms
# Windows
npm run build:win
# macOS (x64 + arm64)
npm run build:mac
# Linux
npm run build:linux
Output
Built applications are placed in dist-electron/:
- Windows:
.exeinstaller (NSIS) + portable.exe - macOS:
.dmginstaller (Intel + Apple Silicon) - Linux:
.AppImage
Installation
macOS
- Download the latest
.dmgfrom the Releases page. - Open the
.dmgfile. - Drag
OmniRoute.appto the Applications folder. - Launch from Applications.
⚠️ Note: The app is not signed with an Apple Developer certificate yet. If macOS blocks the app, run:
xattr -cr /Applications/OmniRoute.appOr right-click the app → Open → Open (to bypass Gatekeeper on first launch).
Windows
Installer (Recommended):
- Download
OmniRoute.Setup.*.exefrom Releases. - Run the installer.
- Launch from Start Menu or Desktop shortcut.
Portable (No Installation):
- Download
OmniRoute.exefrom Releases. - Run directly from any folder.
Linux
- Download the
.AppImagefrom Releases. - Make it executable:
chmod +x OmniRoute-*.AppImage - Run:
./OmniRoute-*.AppImage
Features
- Server Readiness — Waits for health check before showing window
- System Tray — Minimize to tray with quick actions (open, port change, quit)
- Port Management — Change port from tray menu (server restarts automatically)
- Remote Server Mode — Point the shell at an already-running OmniRoute server (e.g. a Docker/OrbStack container, or another machine) instead of spawning a local one — see below
- Window Controls — Custom minimize, maximize, close via IPC
- Content Security Policy — Restrictive CSP via session headers
- Offline Support — Bundled Next.js standalone server
- Single Instance — Only one app instance can run at a time
Remote Server Mode
By default the desktop shell spawns and manages its own bundled Next.js server. If you already run OmniRoute elsewhere — most commonly in a Docker/OrbStack container, so provider credentials and env-var handling stay isolated from the host — you can point the shell at that instance instead, so it's purely a native window + tray onto a server you already run.
Via the tray menu: Remote Server → Connect to Remote Server…, enter the server's
URL (e.g. http://localhost:20128), and save. Leave the field blank and save to
disconnect and go back to the local embedded server. The preference persists across
restarts in <data dir>/electron-preferences.json (see DATA_DIR above for where that
lives on your platform).
Via environment variable: set OMNIROUTE_REMOTE_URL before launching the app (e.g.
OMNIROUTE_REMOTE_URL=http://localhost:20128 npm run dev, or export it in the
environment that launches the packaged app). The env var always wins over the persisted
preference and is session-scoped — it doesn't get written to the prefs file.
Only http:// and https:// URLs are accepted; anything else is rejected before the
window loads.
Configuration
Environment Variables
| Variable | Default | Description |
|---|---|---|
OMNIROUTE_PORT |
20128 |
Server port |
OMNIROUTE_MEMORY_MB |
512 |
Node.js heap limit (64–16384 MB) |
OMNIROUTE_REMOTE_URL |
(unset) | Attach to this server instead of spawning a local one — see Remote Server Mode |
NODE_ENV |
production |
Set to development for dev mode |
Custom Icon
Place your icons in assets/:
icon.ico— Windows icon (256×256)icon.icns— macOS icon bundleicon.png— Linux/general use (512×512)tray-icon.png— System tray icon (16×16 or 32×32)
IPC Channels
Invoke (Renderer → Main, async)
| Channel | Returns | Description |
|---|---|---|
get-app-info |
AppInfo |
App name, version, platform, isDev, port, remoteServerUrl |
open-external |
void |
Open URL in default browser (http/https only) |
get-data-dir |
string |
Get userData directory path |
restart-server |
{ success } |
Stop + restart server (5s timeout + SIGKILL) |
Send (Renderer → Main, fire-and-forget)
| Channel | Description |
|---|---|
window-minimize |
Minimize window |
window-maximize |
Toggle maximize/restore |
window-close |
Close window (minimize to tray) |
Receive (Main → Renderer, events)
| Channel | Payload | Emitted When |
|---|---|---|
server-status |
ServerStatus |
Server starts, stops, errors, or restarts |
port-changed |
number |
Port change via tray menu |
Note
: Listeners return disposer functions for precise cleanup. See
useServerStatusandusePortChangedhooks.
Security
| Feature | Implementation |
|---|---|
| Context Isolation | contextIsolation: true — renderer cannot access Node.js |
| Node Integration | nodeIntegration: false — no require() in renderer |
| IPC Whitelist | Channel names validated in preload via safeInvoke/safeSend/safeOn |
| URL Validation | shell.openExternal() only allows http: / https: protocols |
| CSP | Content-Security-Policy header set via session.webRequest.onHeadersReceived |
| Web Security | webSecurity: true — same-origin policy enforced |
React Hooks
| Hook | Returns | Description |
|---|---|---|
useIsElectron() |
boolean |
Zero-render detection via useSyncExternalStore |
useElectronAppInfo() |
{ appInfo, loading, error } |
App info from main process |
useDataDir() |
{ dataDir, loading, error } |
User data directory |
useWindowControls() |
{ minimize, maximize, close } |
Window control actions |
useOpenExternal() |
{ openExternal } |
Open URLs in browser |
useServerControls() |
{ restart, restarting } |
Server restart control |
useServerStatus(cb) |
Disposer | Listen for server status events |
usePortChanged(cb) |
Disposer | Listen for port change events |
Troubleshooting
App Won't Start
- Check if port 20128 is available:
lsof -i :20128 - Check console logs for
[Electron]prefix - Verify the build output exists in
.build/next/standalone
White Screen
- Verify Next.js build exists — server readiness waits 30s max
- Check
[Server]and[Server:err]log output - Look for CSP violations in developer console
Build Fails
Ensure you have build tools installed:
- Windows: Visual Studio Build Tools
- macOS: Xcode Command Line Tools
- Linux:
build-essential,libsecret-1-dev
License
MIT