Skip to content

OpenTray v0.33~0.34: better runtime reliability, declarative window control

jixoai v0.34.1 opentray

0.33.0 through 0.34.1. Every SDK call is deadline-bounded and an uninvited broker death recovers in-process with zero consumer code. Window dragging and resizing bind to page elements through bindWindowRegion(), and macOS gains a native resize session. Fixes for invisible win32 notifications, dialogs that failed or crashed, text tray titles, a layout re-entrancy abort, and overlay runtime loading.

Esta publicación está escrita en inglés. Léela en chino →

$ npm view opentray version
0.34.1

OpenTray 0.33.0 through 0.34.1 improves runtime reliability and widens window control. Every SDK call is now deadline-bounded, and an uninvited broker death recovers in-process with zero consumer code. Window dragging and resizing bind to page elements through a declarative API, and macOS gains a native resize session. The range also carries seven real-machine Windows fixes, one darwin connect fix, and one release realignment. The previous post covered up to 0.32.0; this one covers every change in the range grouped by theme, with a permanent release link for each version at the end.

A dead broker recovers without your code

From 0.33.0, a tray tool ships with zero recovery code of its own. After the broker process is killed with kill -9, the SDK reconnects and rebuilds state inside the same process, and the consumer only hears about it through a state event.

Hung calls are gone as well. Every SDK call carries a deadline (interactive 5 s, teardown 2 s, bootstrap 10 s, overridable per call) and settles with a typed TransportTimeoutError. Deferred operations the user is holding, such as a dialog waiting for a click, are exempt; they settle only through their terminal frame.

Automatic recovery triggers on socket close or on a half-open connection the heartbeat detects. The budget defaults to 3 attempts per 10 minutes; once exhausted, calls fail fast with TransportAbandonedError, the runtime enters the terminal abandoned state, and the app keeps running headless. The current state is projected through tray.onTransportStateChange as healthy, recovering, or abandoned.

const tray = await createTray(
  { id: "com.example.tool", tooltip: { title: "tool" } },
  { appId: "com.example.tool", appName: "Tool" }
);

tray.onTransportStateChange?.((state) => {
  // "healthy" | "recovering" | "abandoned"
  setUiStatus(state);
});

The boundaries are part of the contract. WebView pages reload from their URL after a recovery, the same as a user refresh, so page state must persist itself. The lost-events window between death and recovery is bounded: those events are gone, and the rebuilt runtime re-emits a full state snapshot for the queryable families. Declarative state is replayed; imperative one-shot commands are not. Apps that prefer a process-level hand-over pass runtimeOptions.recovery.restartApp (it replaces in-process recovery), and the supervisor performs a bounded teardown, invokes the callback exactly once, and goes terminal.

Patches closed recovery gaps that real machines exposed. 0.33.1 fixed shutdown racing recovery, routed the WebView rebuild callbacks so zero-config recovery completes, and bounded each reconnect attempt. From 0.33.2, recovery preserves each window's settled visibility, so a hidden retained window no longer pops back open. From 0.33.3, the supervisor holds the host event loop across the death-to-reconnect window, so a bare process with no other loop holder no longer exits mid-recovery (real-machine evidence 2026-09-22).

Mechanism: the two layers behind the deadlines and recovery

The first layer is the call deadline: interactive, teardown, and bootstrap budgets are overridable per call, and deferred operations follow a two-phase rule that transport deadlines never kill. The second layer is process recovery: after cooldown backoff the broker is respawned through the same identity-gated connect path, artifact identity checks and lock reclaim are reapplied, tray and WebView state replay declaratively, and a full state snapshot is re-emitted last. The heartbeat is single-flight per generation (0.33.1 on), each reconnect is bounded by recovery.connectTimeoutMs (default 10 s), and late-resolving connections are closed on arrival. The test suite carries a permanent kill -9 drill gate and a bare-child-process leg, and the drill's broker resolution covers the Windows opentray.exe cargo output.

Window drag and resize without hand-written events

From 0.34.0, pages drive native window behavior through a declarative entry point. navigator.opentrayWindow.bindWindowRegion(target, behavior) accepts an element, an element array, a selector, or { root, selector } and binds the match to native window behaviors, so pointerdown handlers no longer need hand-writing.

const bridge = navigator.opentrayWindow;

bridge.bindWindowRegion(titlebar, "auto"); // move + double-click maximize/restore
bridge.bindWindowRegion(dragStrip, "move");
const grip = bridge.bindWindowRegion(gripEl, "resize-bottom-right");
grip.setBehavior("none"); // pause without unbinding

The default behavior 'auto' follows platform caption semantics, move on press plus double-click zoom. The remaining values are 'none', 'move', 'zoom', and the eight-direction resize-<edge> (resize-top through resize-bottom-right). Resize behaviors require a frameless window, and binding one on a platform without support throws a TypeError. macOS backs the resize handles with a native soft-resize session, reusing the same internal command Windows already serves.

Matching is strict. Only presses whose target is the bound element itself trigger the behavior, and descendants keep their normal interactivity. Selector matches snapshot at bind time, and rebinding an element replaces its previous binding. The imperative startAppRegionDrag() remains the primitive underneath, and existing callers are unaffected.

Details: the soft-resize session and the probe tests

The macOS soft-resize session drives setFrame through a local event monitor for the whole drag, sharing the internal startSoftResize command with Windows; zoom pairing is recorded per handle. Nine bootstrap probe tests pin behavior normalization, target matching, and the posting and validation contracts.

Other changes

  • Fixed: win32 notifications were completely invisible on current Windows 11 (the staged binary predated the notification bridge, NIIF_NONE is silently dropped by the OS, and balloons stop presenting after the tray flyout is opened while the API keeps returning TRUE; real-machine evidence 2026-09-23). Delivery moved to WinRT toasts under a per-app AUMID with a long-lived worker thread (ce1bf51).

  • Fixed: win32 message dialogs failed with 0x80070057, and file dialogs crashed on real desktops. A v6 TaskDialog is rejected before creating anything until the process initializes the common controls once (real-machine evidence 2026-09-22), and calling Show through an IModalWindow pointer crashed 15/15 (comdlg32+0x777c8, real-machine evidence 2026-09-23). TaskDialog presentation now initializes the common controls per process, and Show is dispatched through the IFileDialog pointer (c025c8cf).

  • Fixed: win32 text-only tray titles rendered as transparent slots; they are now rasterized into RGBA icons through GDI and follow the taskbar theme (c806ed2).

  • Fixed: win32 layout-transaction re-entrancy aborted the broker with 0xC0000409 (real-machine evidence 2026-09-24); every borrow inside relayout is now a try_borrow behind a per-window re-entrancy ledger (9dc1213).

  • Fixed: input in the win32 overlay titlebar band is back with the page. ExtendsContentIntoTitlebar claimed the whole band as a native drag region, so interactive controls a page placed in its custom titlebar were unclickable; the apply path now installs a degenerate drag rectangle, and dragging and double-click zoom return to the page contract (e9823dc).

  • Fixed: the win32 titlebar overlay failed with os error 126 on machines whose runtimes are only OS-provisioned; the fallback adds the runtime package to the process graph through the OS-public Package Dependency APIs (real-machine evidence 2026-09-28, 2a08658).

  • Fixed: win32 pixel-sourced tray icons now register at the tray's physical slot size, and isTemplate theme tinting works on win32 (e1ff250).

  • Fixed: darwin bare-script consumers (entry script with no package.json ancestor) crashed at connect; package identity resolution now falls through to the ambient/cwd fallbacks (b90bef4, 0.34.1).

  • Release: the 0.33.4 workflow skipped @opentray/icon; 0.33.5 re-versioned the fixed group to republish and realign (3ddd839).

  • Docs: the consumer skill references cover the toast channel, recovery budgets, and isTemplate platform guidance; examples gained notify-repro, dialog-selftest, webview-recovery, and a window-region smoke.

No breaking changes in this range. 54 commits after 0.32.0: 5 feat, 14 fix, 16 docs, 4 test, 15 chore, counted by conventional-commit prefix.

Upgrade

npm i opentray@0.34.1

Links