Skip to content

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.

Tray-first Node · Deno · Bun Rust core MIT
GitHub ↗
opentray — first-app

$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.

first-app.mjs
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.

Directorynpm packagePurpose
packages/cliopentrayDeveloper-facing tray-first SDK and CLI package.
packages/spec@opentray/specTypeScript protocol and shared contract package.
packages/packaging@opentray/packagingBundler-neutral runtime artifact staging contract.
packages/vite-plugin@opentray/vite-pluginFirst Vite adapter over the packaging contract.
packages/ext-webview@opentray/ext-webviewRich popup extension facade.
packages/ext-webview-*@opentray/ext-webview-*Platform WebView dynamic library packages.
packages/ext-badge@opentray/ext-badgePlatform badge/progress/overlay API extension.
packages/ext-island@opentray/ext-islandRoadmap 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.