OpenTray · desktop status runtime
The tray-first desktop status runtime for Node, Deno, and Bun.
A desktop status runtime for Node/Deno/Bun CLI and AI-skill ecosystems. The platform model is tray-first: one Tray atom per status, one live Session as the authority, optional native Extensions — and createTray() starts the local broker automatically.
$node first-app.mjs
tray: com.example.first-app · icon "OT"
menu: Quit (primaryEvent)
broker: started automatically
session: live — events streaming
Quick start
Call createTray() for the first app
The default runtime starts the local broker automatically. Runtime identity (appId/appName) is separate from tray projection; primaryEvent is a role on a normal menu item and 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
Platform model
Tray-first by design
App is the caller-owned runtime identity and isolation boundary. Tray is one desktop status atom owned by that app/runtime. Session is the live source of authority for tray events and mutations. Extension is an optional native capability atom scoped to app and tray.
App · Tray · Session · Extension
SDK
createTray() is the whole entry
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
The default createTray() transport targets the local runtime host and starts it on first use when needed. Ordinary app code talks to the packaged opentray executable through the public tray/session protocol — no Node addon, no worker split for tray work.
tray/session protocol · auto-started host
Scaffold
Wrap a command into an app
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 … --exec …), or through the built-in AI skill (npx create-opentray skill).
npx create-opentray · web · create · skill
Extension family
Native capabilities as atoms
@opentray/ext-webview is the rich popup facade — its windows are tray-owned utilities by default, and style.appMode 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 staging
@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 — all four write the same manifest shape.
opentray-app-manifest.json · 4 adapters
Platform runtimes
Per-platform binaries
Platform runtime packages such as @opentray/darwin-arm64 carry bin/opentray or bin/opentray.exe. 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
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.
