OpenTray · desktop status runtime
A desktop home for your CLI and AI skills — without the weight of an app framework.
CLI tools and AI skills do long, quiet work — builds, agents, watchers — and finish in a terminal nobody keeps open. OpenTray is a desktop status runtime for Node, Deno, and Bun: one createTray() call starts the local broker automatically and puts a resident status atom in the tray — Rust core, public tray/session protocol, no Node addon, no app framework.
$node first-app.mjs
broker: started automatically — Rust core
tray: com.example.first-app · icon "OT" resident
menu: Quit (primaryEvent)
session: live — events streaming
Quick start
One install, one createTray()
The packaged runtime starts the local broker on first use — visible tray text is icon projection, runtime identity (appId/appName) is separate, and primaryEvent is a role on a normal menu item that emits the usual menuClick.
import {
createTray,
type CreateTrayHandle,
type CreateTrayOptions,
type TrayIcon,
} from "opentray";
const icon: TrayIcon = { "text-only": "OT" };
let tray: CreateTrayHandle;
const options: CreateTrayOptions = {
id: "com.example.first-app",
icon,
menu: {
items: [
{
title: "Quit",
primaryEvent: true,
onMenuClick: () => void tray.destroy(),
},
],
},
};
tray = await createTray(options, {
appId: "com.example.first-app",
appName: "First App",
});Already have a command that serves HTTP locally? npx create-opentray wraps it into an OpenTray-hosted app.
What’s inside
Why it exists
Not a tray icon — an entry point
The goal was never "show a tray icon": lightweight tools deserve a system-level entry point without being forced into Electron or a full desktop app framework. The model stays four atoms: App is the caller-owned runtime identity and isolation boundary, Tray is one desktop status atom, Session is the live source of authority for tray events and mutations, Extension is the optional native capability atom.
App · Tray · Session · Extension
SDK
One call owns its lifetime
You should not bootstrap an app shell to show status. Application code calls createTray() directly and owns foreground/background lifetime itself. Top-level createTray(...) and its returned setMenu(...) accept app-facing menu shorthand; lower-level createClient(...) stays protocol-only for tools that need exact wire shapes.
opentray · menu shorthand · typed handles
Rust core
A broker, not an addon
Why trust a tray that arrives over npm? The native side is a Rust core delivered as a packaged executable: the default createTray() transport targets the local runtime host and starts it on first use when needed, and ordinary app code talks to it through the public tray/session protocol. No Node addon, no worker split for tray work.
tray/session protocol · auto-started Rust host
Onboarding
Bring your own command
You should not rewrite a working tool to give it a tray. Already have a command that serves HTTP locally? npx create-opentray wraps it into an OpenTray-hosted app — interactively through the browser wizard (create-opentray web), fully non-interactively (create-opentray create --app-id … --app-name … --exec …), or by handing the built-in AI skill to your agent (npx create-opentray skill).
npx create-opentray · web · create · skill
Extension family
Native capability, opt-in atoms
When a status atom needs more than a menu, extensions add native capability without changing the model: @opentray/ext-webview is the rich popup facade — its windows are tray-owned utilities by default, and style.appMode: true makes one behave as an ordinary desktop window (taskbar/Alt-Tab, Dock/Command-Tab). @opentray/ext-badge adds badge/progress/overlay APIs; @opentray/ext-island is the roadmap dynamic island extension.
ext-webview · ext-badge · ext-island
Packaging layer
Bundler-neutral, four adapters
Shipping should not depend on your bundler. @opentray/packaging stages runtime executables, native sidecars, and companion assets into app-id-derived output paths and writes an opentray-app-manifest.json manifest; adapters ship for Vite, tsdown, esbuild, and webpack, and all four write the same manifest shape.
opentray-app-manifest.json · 4 adapters
Platform runtimes
Six platforms, plain binaries
The runtime arrives the boring way: platform packages such as @opentray/darwin-arm64 carry bin/opentray or bin/opentray.exe for three OSes × two architectures (darwin, linux, windows × arm64, x64). Packaging stays a build-layer concern — it stages artifacts and emits manifest truth; tray lifecycle, session authority, and extension dispatch stay with the runtime.
@opentray/<os>-<arch> · bin/opentray
Releases
Protocol-line dist-tags
Mixed package sets should not drift apart. When an app uses official extensions, lock the same OpenTray protocol-line tag across the package set: pnpm add opentray@stable-A-B @opentray/ext-webview@stable-A-B. alpha-A-B carries alpha packages on the same protocol line; the tags are published by @opentray/spec.
stable-A-B · alpha-A-B
Workspace
One repository, one package family
The npm surface mirrors the repository layout. The Lynx extension is maintained in the independent jixoai/opentray-ext-lynx repository; OpenTray core does not build, stage, or publish Lynx artifacts.
| Directory | npm package | Purpose |
|---|---|---|
| packages/cli | opentray | Developer-facing tray-first SDK and CLI package. |
| packages/spec | @opentray/spec | TypeScript protocol and shared contract package. |
| packages/packaging | @opentray/packaging | Bundler-neutral runtime artifact staging contract. |
| packages/vite-plugin | @opentray/vite-plugin | First Vite adapter over the packaging contract. |
| packages/ext-webview | @opentray/ext-webview | Rich popup extension facade. |
| packages/ext-webview-* | @opentray/ext-webview-* | Platform WebView dynamic library packages. |
| packages/ext-badge | @opentray/ext-badge | Platform badge/progress/overlay API extension. |
| packages/ext-island | @opentray/ext-island | Roadmap dynamic island / live activity extension. |
| packages/<os>-<arch> | @opentray/<os>-<arch> | Platform runtime artifact packages. |
Ecosystem
Where things live
Source, packages, and the public guides — the same links the README carries.
- GitHub · jixoai/opentray ↗ Source, issues, and release history.
- npm · opentray ↗ The developer-facing tray-first SDK and CLI package.
- create-app guide ↗ Wrapping an HTTP-serving command with npx create-opentray.
- app-mode decision guide ↗ Normal apps, tray utilities, and mixed-window products on @opentray/ext-webview.
- opentray-ext-lynx ↗ The Lynx extension, maintained in its own repository.
