Skip to content

OpenTray v0.21.1: turning one command into a tray app

jixoai v0.21.1 opentray

The OpenTray repo's first GitHub Release. It gives lightweight programs such as CLI tools, AI skills, and local daemons a resident tray entry without pulling in Electron or a full desktop application framework. One `npx create-opentray` line wraps an existing command into a tray app, and one `createTray()` call is enough on the SDK side.

이 글은 영어로 작성되었습니다. 중국어 버전 읽기 →

$ npm view create-opentray version
0.21.1

OpenTray v0.21.1 is out (2026-09-06, the repo's first GitHub Release). It gives lightweight programs such as CLI tools, AI skills, and local daemons a resident tray entry: an icon and a menu that receive click events, without pulling in Electron or a full desktop application framework for one small icon.

One command turns an existing command into a tray app

If you already have a command that serves HTTP locally, npx create-opentray wraps it in an OpenTray app. All three entry paths are in the create-app guide.

npx create-opentray web      # browser wizard, interactive
npx create-opentray create --app-id com.example.build \
  --app-name "Build" --exec "npm run dev"   # fully non-interactive
npx create-opentray skill    # the built-in AI skill, for agents to read

Command input in the wizard is now a single-row input group (command families, organized by runner family): a family-selector prefix plus the command body. The families cover npm (npx/pnpx/bunx/yarn dlx/nubx/deno run/vpx), go run, cargo install, uvx/pipx run, and dnx. Each family renders as a read-only command surface that opens a structured form for the runner, the package identifier (name / module / crate / tool id), the version, and the arguments. Confirming writes the command string back; cancelling discards the whole input.

The appId is derived from the family, so the same package keeps a stable identity when you switch runners:

npx @deepseek-ai/dsh@latest web   → web.dsh.npmjs
go run rsc.io/fortune@latest      → fortune.golang
cargo install ripgrep (rg)        → rg.rust

Application code calls one createTray()

Writing a tray app needs no bundler and loads no Node addon. Application code calls createTray() and owns its own foreground and background lifetime:

import { createTray, type TrayIcon } from "opentray";

let tray;
tray = await createTray(
  {
    id: "com.example.first-app",
    icon: { "text-only": "OT" },
    menu: {
      items: [
        { title: "Quit", primaryEvent: true, onMenuClick: () => void tray.destroy() },
      ],
    },
  },
  { appId: "com.example.first-app", appName: "First App" },
);

Application code talks to the runtime over the public tray/session protocol. The runtime side has four entities.

  • App: the runtime identity the caller holds, and the isolation boundary

  • Tray: a single tray icon and its state under one app

  • Session: the live source of authority for tray events and mutations

  • Extension: a native capability package scoped to this app and its trays

The Space and Surface entities of the previous public API are retired.

Every package in the workspace publishes on one fixed version: opentray (SDK + CLI), @opentray/spec (the protocol contract), @opentray/packaging with four bundler adapters (vite/tsdown/esbuild/webpack writing the same manifest shape), @opentray/ext-webview with platform WebView dynamic libraries, and @opentray/ext-badge. The Lynx extension is maintained separately at jixoai/opentray-ext-lynx.

Other changes

  • Fixed the fresh-install 404: the private workspace packages @create-opentray/core and @create-opentray/cli shipped in runtime dependencies (0b0fe63). That rule was located by a real install-failure report on 2026-08-20, and this is the first version where command families installs.

  • Release automation: every published version now cuts a GitHub Release of its own (tag = the package version, notes = the changesets-generated changelog section), and this site's version pill showed a real number for the first time instead of "v—". This one was cut through the bootstrap path; from the next version on, release notes are fully automated.

  • @opentray/spec, @opentray/packaging, and @opentray/vite-plugin publish at 0.21.1 alongside it.

Upgrade

npm i create-opentray@0.21.1   # 0.21.0 never installed, so upgrading is mandatory
npm i opentray@0.21.1          # the SDK, same line

When using official extensions, lock the same protocol line: pnpm add opentray@stable-A-B @opentray/ext-webview@stable-A-B (protocol-line tags are published by @opentray/spec; do not mix with latest).

Acknowledgements

Thanks to the install-failure report from 2026-08-20. The devDependencies rule was tightened because of it.

Links