NatUI
Internals

Wire protocol

Specify NatUI protocol version 1 messages, operations, events, selection, and sequence acknowledgements.

The Node renderer and native host communicate with one UTF-8 JSON object per line. The process transport uses the host's standard input and output. Diagnostics go to standard error, and a host must not write non-protocol text to standard output.

Receivers ignore unknown message types for forward compatibility.

Handshake

  1. The host sends {"t":"ready","platform":"macos","protocol":1,"hostApi":2}.
  2. JavaScript checks that the protocol is 1, the host API is at least 2, and the platform is known. Node mode also checks that the platform matches the current operating system.
  3. JavaScript sends a window message, then begins committing the React tree.

A mismatch is a startup failure. JavaScript terminates the incompatible or half-started host. Embedded startup also removes its host-to-JavaScript receive hook.

The protocol version describes wire-format compatibility. The host API is a monotonic capability level for additive components and native behavior that keep the same wire format. A renderer accepts a host API equal to or newer than its required level.

Packaged applications add a bundle manifest check before this handshake. The native loader validates bundle schema, exact protocol, minimum host API, platform, architecture, and the SHA-256 digest of the JavaScript entry before evaluating it.

JavaScript to host messages

MessageShapeMeaning
window{"t":"window","props":{...}}Configure and show the main window
commit{"t":"commit","ops":[...]}Apply one atomic React commit
dump{"t":"dump","rid":number}Request the native node tree
screenshot{"t":"screenshot","path":string,"rid":number}Render the window to a PNG
emit{"t":"emit","id":number,"name":string,"payload"?:object}Synthesize an event
edit{"t":"edit","id":number,"value":any}Perform an optimistic controlled edit
requestClose{"t":"requestClose"}Verification only: run the native window-close path
quit{"t":"quit"}Exit cleanly

Debug messages can be removed from a production host. requestClose asks the host to take the same close path as the window close button, so packaging verification can exercise shutdown without a user gesture.

Commit operations

Node ids are positive integers. Id 0 is the root container.

OperationMeaning
createCreate a detached typed node with serialized props
createTextCreate a detached raw text node
appendAppend or move a child to the end of a parent
insertInsert or move a child before a sibling
removeDestroy a child and its subtree
updateReplace all props, optionally carrying ack
textReplace raw text content
clearDestroy every root child

The host applies every operation in a commit before updating visible UI.

Host to JavaScript messages

MessageMeaning
readyReport platform, protocol version, and host API level
eventReport an interaction on a node
windowReport native window close
treeReply to dump, echoing its rid
shotReply to screenshot, echoing its rid, with optional error
quitAckDebug acknowledgement that the host received quit

The host always replies to screenshot. On failure it includes error and does not produce a usable file.

Requests carry a rid (request id) that the host copies onto its reply. Replies pair with requests by that id, never by arrival order. A host that answers out of order, or answers late after the requester gave up, therefore can never resolve the wrong caller. Echoing rid is required from host API 2.

Events

ComponentEventPayload value
Button, Linkpressnone
TextField, SearchFieldchange, submitstring
Togglechangeboolean
Slider, Stepperchangenumber
Picker, TabViewchangestring
DatePickerchangelocal ISO string
TextEditorchangestring
Sheet, Alert, Popoverchangeboolean
Alertselectbutton id
SplitViewchangeall or detailOnly
DisclosureGroupchangeboolean
List, Tablechangestring, string array, or null
TablesortChange{ key, order }
MenuBar, Menu, ContextMenuselectitem id
Toolbaractionitem id
Toolbarsearchtext

Only change events carry seq. Press, submit, select, action, search, and sort requests are fire-and-forget.

Sequence acknowledgements

On every optimistic controlled edit:

  1. The host increments a per-node counter and sends it as seq.
  2. JavaScript records the highest sequence processed for that node.
  3. An update generated after that event carries the sequence as ack.
  4. If the host has a newer sequence, it keeps its local value while applying other props.
  5. When ack equals the local counter, the full update is authoritative.

An update without ack is fully authoritative.

After the synchronous handler flush, JavaScript compares the committed value with the event value. If they differ, it synthesizes a corrective update with the event acknowledgement. This covers handlers that reject or clamp input and controls without handlers.

An asynchronously adopted value is corrected first, then applied by the later React commit.

Controlled kinds

Each controlled kind has exactly one controlled prop named value.

KindValue
TextField, SearchField, TextEditorstring
Toggleboolean
Slider, Steppernumber
Picker, TabViewstring
DatePickerlocal ISO string
Sheet, Alert, Popoverboolean
SplitViewall or detailOnly; native visibility changes are optimistic
List, Tablestring, string array, or null
DisclosureGroupboolean

Presentation hosts can optimistically set only false, representing native dismissal. Keeping value true prevents dismissal through the same corrective mechanism.

Presentation ordering

Sheet and popover content materializes eagerly whether or not it is presented.

The host ignores alert children because alert content is data-driven. A button press emits select first and dismissal change(false) second. The ordering lets the select handler close state without re-presenting the alert between events.

A popover's ordinary children form its anchor. Its first PopoverContent child forms the presented body; the host ignores extra content slots and reports a diagnostic.

Menu items are recursive JSON data:

MenuItemSpec =
  { id, label, systemImage?, role?, shortcut?, disabled?, checked?, children? }
  | { divider: true }

Selection fires only for enabled leaves. checked is prop-driven. Command roles do not emit selection, except destructive, which is a style and still emits. Command roles invoke platform actions where supported; the Windows about role is currently inert.

Shortcuts use tokens such as cmd, shift, alt, and ctrl plus a key.

Toolbar items can be spacers, flexible spaces, buttons, toggles, menus, or search fields. Buttons and toggles emit action. Toolbar menu leaves also emit action. Search text emits search and is not echoed into the item spec.

Selection and sorting

A List becomes selectable when its props include value, including null. Rows use the common tag prop as identity.

Single selection is a string or null. Multiple selection is a sorted string array, and an empty selection is [], not null. Give every row in a selectable list a tag.

Table selection uses row ids and the same value shapes.

Sorting is request-based. The host emits sortChange without a sequence number and never reorders rows. The application sorts its data and returns both reordered rows and the new descriptor.

Root-attached chrome

MenuBar and Toolbar remain protocol nodes and appear in tree dumps, but they do not render in normal content. For portable behavior, provide one direct root child of each kind. macOS hoists the first direct root child. Windows creates hosted chrome for every such node when it is created, even if it is nested, so duplicate or nested chrome is intentionally unsupported application structure.

Structurally equal props do not rebuild chrome, which prevents unrelated React commits from tearing down an open menu.

Node props

All dimensions are logical points. Colors are #RRGGBB or #RRGGBBAA.

Common props cover padding, background, corner radius, frame, opacity, disabled and hidden state, foreground color, tooltips, selection tags, badges, and accessibility fields.

Component-specific props and platform mappings are documented in the component catalog. Every wire prop must be JSON-compatible. Event handlers remain in JavaScript and never cross the wire.

On this page