// @native-sdk/core — the SDK module an app core imports. The subset program
// maps the "@native-sdk/core" specifier onto this file, so stock tsc types
// it; the transpiler lowers references to it onto the rt kernel and never
// emits this module's own code.
//
// `Cmd` is the typed-effects surface (spec section 2): `update` (and, since
// v2, `initialModel`) may return `[model, cmd]`, where the cmd is INERT DATA
// describing effects for the runtime to perform after the returned model
// commits. Commands are built only from these factories, and only in that
// return path (NS1017) — they never live in the Model, in a Msg, in a local,
// or in a helper.
//
// The v5 command set:
//
// Cmd.none no effects (what a bare `return model` means)
// Cmd.persist() ask the host to persist the committed model
// Cmd.now("tick") request a timestamp; the runtime dispatches
// the named Msg arm with the time (ms) as its
// single number payload field
// Cmd.host(name, ...args) a host command by name with scalar args —
// the host interprets the name
// Cmd.host(name, payload) the same, carrying one bytes payload — a
// Uint8Array, or a flat record of number /
// boolean / Uint8Array fields that lowers to
// bytes (hostRecordBytes below)
// Cmd.request(name, payload, a routed host command: the host performs it
// { key?, ok, err }) and dispatches the `ok` Msg arm with the
// result bytes, or the `err` arm with the error
// bytes — each arm carries exactly one
// Uint8Array payload field, checked by tsc. The
// optional `key` names the in-flight effect:
// re-issuing a live key replaces it, and
// Cmd.cancel(key) drops it.
// Cmd.cancel(key) drop the in-flight keyed effect — request,
// readFile/writeFile/readFileStream/fetch/
// clipboardRead, or
// delay — SILENTLY (no terminal arm dispatch).
// Aimed at a live spawn, streaming fetch, or
// streaming service or write-file sink it
// stays LOUD: the err arm runs with
// "cancelled" — ending a stream is observable
// Cmd.batch([a, b, ...]) several commands from one dispatch
//
// The named engine ops (each maps onto the host's effect engine directly;
// routing follows the request rules — string-literal arm names, tsc-checked
// arm shapes):
//
// Cmd.readFile(path, { key?, ok, err })
// whole-file read; ok arm carries the bytes
// (one Uint8Array field), err arm the reason
// bytes ("not_found", "io_failed", "truncated",
// "rejected")
// Cmd.writeFile(path, bytes, { key?, ok, err })
// whole-file write (parents created, replaced
// whole); ok arm carries NOTHING (an arm with
// no payload fields), err arm the reason bytes
// Cmd.appendFile / statFile / deleteFile
// bounded append, metadata probe, and deletion
// Cmd.readFileStream 256-KiB chunks, then done(total) or err
// Cmd.writeFileStream / writeFileChunk / writeFileClose
// atomic streamed sink; chunks are acknowledged
// in order and close installs the destination
// Cmd.fetch({ url, method?, headers?, body?, timeoutMs? }, { key?, ok, err })
// buffered HTTP(S) exchange; ok arm carries a
// two-field record — one number field (the real
// HTTP status, non-2xx included) and one
// Uint8Array field (the whole body) — err arm
// the reason bytes ("connect_failed",
// "tls_failed", "protocol_failed", "timed_out",
// "rejected", "truncated")
// Cmd.fetch({ ..., maxLineBytes? }, { key?, line, ok, err })
// streaming HTTP(S) exchange; each complete
// response line dispatches the one-bytes-field
// `line` arm, then exactly one terminal follows:
// `ok` with the HTTP status as its one number
// field, or `err` with the transport reason
// (or "truncated" if any line was cut/dropped)
// Cmd.clipboardWrite(bytes) fire-and-forget clipboard write
// Cmd.clipboardRead({ key?, ok, err })
// clipboard read; ok arm carries the text bytes,
// err arm the reason bytes ("failed",
// "rejected")
// Cmd.showNotification({ id?, title, subtitle?, body?, actionLabel?, actionCommand? })
// fire-and-forget desktop notification; the OS
// remains authoritative over final delivery
// Cmd.openExternalUrl(url) open an allowed HTTP(S) URL in the system
// browser; denied/invalid URLs fail closed
// Cmd.revealPath(path) reveal a path in the system file manager;
// invalid/unavailable requests fail closed
// Cmd.credentials.set(key, secret, route)
// Cmd.credentials.get(key, route)
// Cmd.credentials.delete(key, route)
// routed access to Keychain / Secret Service /
// Credential Manager
// Cmd.formatLocalTime(ms, style, route)
// format an epoch timestamp through the host's
// current locale and time zone
// Cmd.delay(key, ms, "fired") a keyed ONE-SHOT timer: dispatches the named
// arm once after `ms`, with the fire time (ms)
// as its single number payload; re-issuing a
// live key re-arms from now (debounce), and
// Cmd.cancel(key) drops it silently
//
// The streaming ops (one issue, MANY result Msgs across dispatches, a keyed
// lifecycle the app drives):
//
// Cmd.spawn(argv, { key?, stdin?, line?, exit, err })
// Cmd.spawn(argv, { key?, stdin?, collect: true, exit, err })
// run a subprocess. Line mode streams each
// stdout line to the `line` arm (one Uint8Array
// field; omit `line` to drop lines); collect
// mode buffers whole stdout instead. Exactly one
// terminal follows: a clean exit dispatches
// `exit` — line mode: one number field (the exit
// code); collect mode: a two-field record, one
// number (the code) and one Uint8Array (the
// collected stdout), matched by type — and every
// other end dispatches `err` with the reason
// bytes ("signaled", "cancelled", "rejected",
// "spawn_failed", "truncated"). Cmd.cancel(key)
// ends the child mid-stream (err arm
// "cancelled" — never silent).
// Cmd.audioPlay(key, { path?, url?, cachePath?, expectedBytes? }, { event })
// open (or replace — one player is the whole
// surface) the audio event stream: every
// playback event dispatches the `event` arm (the
// six-field record below) until Cmd.audioStop
// closes the stream.
// Cmd.audioPause(key) / audioResume(key) / audioStop(key)
// Cmd.audioSeek(key, ms) / Cmd.audioSetVolume(key, volume)
// drive the open stream in place — fire-and-
// forget control verbs whose consequences arrive
// on the event stream; aimed at a key with no
// open stream they no-op.
// Cmd.videoLoad(key, { surface, path?, url?, autoplay?, loop?, muted? }, { event })
// open (or replace — one player is the whole
// surface) the video event stream feeding the
// media-surface `surface` names (the same
// model-owned id the markup binds): every
// playback event dispatches the `event` arm (the
// seven-field record below) until Cmd.videoStop
// closes the stream. Pixels never ride the
// events — decoded frames flow platform-side
// into the bound surface.
// Cmd.videoPlay(key) / videoPause(key) / videoStop(key)
// Cmd.videoSeek(key, ms) / Cmd.videoSetVolume(key, volume)
// Cmd.videoSetMuted(key, muted) / Cmd.videoSetLoop(key, loop)
// drive the open playback in place — fire-and-
// forget control verbs whose consequences arrive
// on the event stream; aimed at a key with no
// open stream they no-op.
// Cmd.imageLoad(id, { path?, url?, cachePath?, expectedBytes? }, { event })
// load an image at runtime by its model-owned
// numeric ImageId (the id markup binds:
// , avatar likewise): the
// host resolves the source cascade (local path
// first, then a verified cache entry, then the
// network), decodes through the platform codec,
// registers the pixels under the id, and
// dispatches exactly ONE `event` arm (the
// five-field record below, the requested id
// echoed) — state "loaded" with the decoded
// width/height, or one failure class. One load
// per id at a time: a duplicate live id
// dispatches state "rejected".
// Cmd.imageCancel(id) end the in-flight load under the id, if any
// — loud, the spawn discipline: the load's
// event arm delivers state "cancelled" and the
// id frees for a fresh load once it lands. An
// id with no live load no-ops.
// Cmd.imageUnregister(id) free the registry slot under the id: the
// pixels are released, views referencing the
// id draw their fallback, and the slot is
// open for another load. Synchronous registry
// surgery like registration itself — not an
// effect, no Msg follows; an id with no
// registration no-ops. A load IN FLIGHT under
// the id is untouched: its terminal still
// registers — cancel the load first to keep
// the slot free.
// Cmd.channelOpen(key, { event })
// open an EXTERNAL-SOURCE CHANNEL under the
// app's numeric key: the host stages a
// long-lived, thread-safe posting seam its
// NATIVE side feeds — embedders and
// platform-services extensions post bytes
// from their own threads (sockets, watchers,
// workers), and each accepted post arrives as
// one "data" event through the `event` arm
// (the five-field record below) with the
// honest back-pressure counters aboard. The
// TS tier opens, closes, and receives;
// POSTING is not a TS verb — transpiled cores
// are single-threaded by design, so the
// posting handle lives on the native side
// (`Effects.channelHandle(key)`). One channel
// per key at a time: a duplicate live key
// dispatches state "rejected". No timer
// polling anywhere: the source wakes the
// loop itself.
// Cmd.channelClose(key) close the open channel under the key, if
// any: staged posts flush, exactly one
// "closed" event (final drop totals)
// dispatches the event arm, and the key
// frees. A key with no open channel no-ops.
// Cmd.ptySpawn(argv, { key?, cols?, rows?, term?, event })
// open a PSEUDO-TERMINAL SESSION — a spawn
// with a different transport: run argv on a
// fresh pty whose initial grid is cols x rows
// (80x24 by default) and whose TERM is `term`
// (omitted = the engine's default). Every
// session event dispatches the `event` arm
// (the six-field record below): "output"
// events carry coalesced batches of child
// output across dispatches — feed them to the
// terminal emulator — until the exactly-one
// "exit" terminal (a refused spawn is one
// "exit" with reason "rejected"; a transport
// that could not start, one with
// "spawn_failed"). One session per key at a
// time, never replaced implicitly — kill it
// first, the spawn discipline.
// Cmd.ptyWrite(key, bytes) write bytes toward the session's child —
// keystrokes and pastes, fire-and-forget: a
// key with no open session no-ops, and
// refused payloads count into the exit's
// droppedWrites, never silence.
// Cmd.ptyResize(key, cols, rows)
// push a new grid to the session so the child
// receives SIGWINCH — fire-and-forget like
// ptyWrite; a key with no open session
// no-ops.
// Cmd.ptyKill(key) terminate the session's child — LOUD, the
// spawn cancel discipline: the session's one
// "exit" terminal arrives through its own
// event arm with reason "cancelled" and the
// key frees once it lands. A key with no open
// session no-ops. Sessions are their own
// family's to end: the string-keyed
// Cmd.cancel never touches them, the way
// Cmd.audioStop is audio's close.
//
// The window verbs (fire-and-forget, no result Msg — the window's own
// frame event carries the state):
//
// Cmd.showWindow(label) un-hide + activate the window with the
// declared label — the counterpart to a
// `close_policy = "hide"` hide and the tray
// "Open" consequence; also restores a
// minimized window. An unknown label is a
// no-op.
// Cmd.hideWindow(label) order a live window out while retaining
// its views; showWindow is the inverse.
// Cmd.setDockPresence(visible) show or remove the app from the macOS
// Dock/app switcher.
// Cmd.quitApp() graceful terminate, the tray "Quit"
// consequence: the host quits through the
// SAME shutdown path a last-window close
// takes, so the stop hook runs exactly once
// and a recording session seals its journal.
//
// The keyed-effect discipline is ONE rule: a keyed effect REPLACES its live
// predecessor (the superseded effect's result is dropped — no message), and
// Cmd.cancel drops it silently. That holds for request, readFile, writeFile,
// readFileStream, buffered fetch, clipboardRead, and delay alike. A reissued
// readFileStream key retires the old read silently and starts the replacement.
// Live spawn, streaming-fetch, streaming-service, and write-file SINK keys are
// the exceptions: a duplicate REJECTS the new stream/sink so two producers are
// never spliced. Cancelling a sink is loud (`err: cancelled`) because a
// half-written export is observable; cancelling a read stream is silent.
//
// `Sub` is the recurring-effects surface: an app may export
// `subscriptions(model): Sub` returning declarative descriptors the
// host reconciles after every commit (Sub.timer fires the named arm with the
// current time on each interval). Like Cmd, Sub values are inert data, legal
// only in that function's return path (NS1025). Sub and the streaming Cmds
// are different animals on purpose: a Sub is DECLARED from the model (the
// host starts/stops it by reconciliation; the app never opens one), while
// spawn/audioPlay/videoLoad streams are Cmd-INITIATED — imperative opens
// with a keyed lifecycle the app drives and cancels.
//
// The factories return plain frozen-shape objects so the same core runs
// under node: a dev harness can interpret the `op` tags directly.
// The byte-text method surface (s.toUpperCase(), s.split(sep), ... on
// Uint8Array): the transpiler adds the ambient file to every core's program
// itself; this reference carries the same surface into EDITORS of apps that
// import "@native-sdk/core", so tsc-in-the-editor and `native check` agree.
///
/// The ASCII text intrinsic: turn a string literal or template whose code
/// units are all 0x00..0x7f into bytes. The transpiler recognizes calls BY
/// IDENTITY (this import, renames honored) and folds them at compile time —
/// a literal argument becomes rodata, a template becomes frame-arena bytes
/// via bufPrint — so no string ever exists at native runtime. Under node
/// this body runs as-is, byte for byte the same result. Use `utf8Bytes` for
/// user-visible text that may contain Unicode. Arguments must be literals
/// or templates; dynamic text lives in the model as Uint8Array from the
/// start.
export function asciiBytes(s: string): Uint8Array {
for (let i = 0; i < s.length; i++) {
if (s.charCodeAt(i) > 0x7f) {
throw new RangeError("asciiBytes accepts ASCII only; use utf8Bytes for Unicode text");
}
}
const out = new Uint8Array(s.length);
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
return out;
}
/// The UTF-8 text intrinsic. Like `asciiBytes`, literals become rodata and
/// templates become frame-arena bytes in a native core; under node this
/// implementation performs the same encoding directly. Invalid lone UTF-16
/// surrogates encode as U+FFFD, matching the platform TextEncoder contract.
export function utf8Bytes(s: string): Uint8Array {
let byteLength = 0;
for (let i = 0; i < s.length; i++) {
const code = s.charCodeAt(i);
if (code <= 0x7f) {
byteLength += 1;
} else if (code <= 0x7ff) {
byteLength += 2;
} else if (code >= 0xd800 && code <= 0xdbff && i + 1 < s.length) {
const next = s.charCodeAt(i + 1);
if (next >= 0xdc00 && next <= 0xdfff) {
byteLength += 4;
i += 1;
} else {
byteLength += 3;
}
} else {
byteLength += 3;
}
}
const out = new Uint8Array(byteLength);
let at = 0;
for (let i = 0; i < s.length; i++) {
let code = s.charCodeAt(i);
if (code >= 0xd800 && code <= 0xdbff && i + 1 < s.length) {
const next = s.charCodeAt(i + 1);
if (next >= 0xdc00 && next <= 0xdfff) {
code = 0x10000 + ((code - 0xd800) << 10) + (next - 0xdc00);
i += 1;
} else {
code = 0xfffd;
}
} else if (code >= 0xd800 && code <= 0xdfff) {
code = 0xfffd;
}
if (code <= 0x7f) {
out[at] = code;
at += 1;
} else if (code <= 0x7ff) {
out[at] = 0xc0 | (code >> 6);
out[at + 1] = 0x80 | (code & 0x3f);
at += 2;
} else if (code <= 0xffff) {
out[at] = 0xe0 | (code >> 12);
out[at + 1] = 0x80 | ((code >> 6) & 0x3f);
out[at + 2] = 0x80 | (code & 0x3f);
at += 3;
} else {
out[at] = 0xf0 | (code >> 18);
out[at + 1] = 0x80 | ((code >> 12) & 0x3f);
out[at + 2] = 0x80 | ((code >> 6) & 0x3f);
out[at + 3] = 0x80 | (code & 0x3f);
at += 4;
}
}
return out;
}
/// Every app Msg is a discriminated union on a string `kind` tag.
export type Msgish = { readonly kind: string };
/** Cooperative cancellation capability supplied by generated service hosts. */
export interface ServiceCancellation {
/** True after Cmd.cancel or the operation deadline requests cancellation. */
readonly cancelled: () => boolean;
/** Throw a boundary-tagged cancellation error when cancellation was requested. */
readonly throwIfCancelled: () => void;
}
/// The Msg arms `Cmd.now` may target: arms whose payload is exactly one
/// number-typed field (the runtime dispatches the arm with the timestamp in
/// that field). Anything else is unrepresentable — the runtime has only a
/// number to give back.
export type TimestampKind = M extends Msgish
? {
[K in Exclude]-?: M[K] extends number
? [Exclude] extends [K]
? M["kind"]
: never
: never;
}[Exclude]
: never;
/// The Msg arms a routed host result may target: arms whose payload is
/// exactly one Uint8Array-typed field (the runtime dispatches the arm with
/// the result/error bytes in that field).
export type BytesKind = M extends Msgish
? {
[K in Exclude]-?: M[K] extends Uint8Array
? [Exclude] extends [K]
? M["kind"]
: never
: never;
}[Exclude]
: never;
/// The Msg arms a generated typed service client may target: exactly one
/// payload field whose type is the operation's declared result shape.
export type ServiceKind = M extends Msgish
? {
[K in Exclude]-?: M[K] extends P
? P extends M[K]
? [Exclude] extends [K]
? M["kind"]
: never
: never
: never;
}[Exclude]
: never;
/// Routing for a generated typed service call. Success carries the
/// operation's declared record/value shape; failures remain UTF-8 bytes so
/// transport, timeout, cancellation, and service `{ kind, message }` errors
/// share one stable error channel.
export interface ServiceRoute {
readonly key?: string;
readonly ok: ServiceKind;
readonly err: BytesKind;
}
/// A streaming service opens an external-source channel before issuing the
/// request. Interim chunks arrive as canonical bytes on `event`; `ok` is the
/// one typed terminal and `err` is the loud cancellation/timeout/error arm.
export interface ServiceStreamRoute extends ServiceRoute {
readonly channelKey: number;
readonly event: ChannelEventKind;
}
/// The Msg arms a payload-less routed result may target: arms with no
/// payload fields at all (`Cmd.writeFile`'s ok route — a successful write
/// has nothing to report beyond success).
export type EmptyKind = M extends Msgish
? [Exclude] extends [never]
? M["kind"]
: never
: never;
/// The Msg arms a buffered fetch response may target: arms whose payload is
/// exactly two fields — one number (the HTTP status) and one Uint8Array (the
/// body). The runtime matches the fields by TYPE, so their names are yours.
export type FetchedKind = M extends Msgish
? {
[K in Exclude]-?: M[K] extends Uint8Array
? Exclude extends infer O
? O extends keyof M
? M[O] extends number
? [Exclude] extends [never]
? M["kind"]
: never
: never
: never
: never
: never;
}[Exclude]
: never;
// ------------------------------------------------- wiring channel shapes
// The generated wiring's opt-in host-event channels: export the channel
// and it is wired (`commandMsg(name: string): Msg | null` is the same
// family — menus, shortcuts, chrome tabs — and predates these). Event
// RECORD shapes (`frameMsg`'s FrameEvent, `keyMsg`'s KeyEvent,
// `dropMsg`'s FileDropEvent, the appearanceMsg/chromeMsg arm payloads) are DECLARED IN YOUR CORE and
// matched by field name, the TextInputEvent rule — they must emit as your
// module's own records, so an SDK interface cannot stand in for them.
/// One `envMsgs` entry: `export const envMsgs = [{ env: "NAME", msg:
/// "" }] as const` — each named environment variable present at launch
/// dispatches its value through the arm (exactly one `Uint8Array` field) as
/// an ordinary journaled Msg right after the boot command. The core itself
/// never reads the environment (NS1005); replay carries the recorded values.
export interface EnvMsg {
readonly env: string;
readonly msg: BytesKind;
}
/// The audio event states, mirroring the engine's event vocabulary: `loaded`
/// acknowledges a successful load with the player's duration estimate;
/// `position` ticks at the platform's honest cadence (~500ms) while playing;
/// `completed` fires exactly once at the natural end; `failed` reports a
/// load/decode/device failure; `rejected` a command the effects layer refused
/// (an empty or over-long source); `spectrum` carries a band-magnitude
/// analysis frame from hosts that analyze their playback.
export type AudioState = "loaded" | "position" | "completed" | "failed" | "rejected" | "spectrum";
/// The payload shape of an audio event arm — six fields, matched by NAME (the
/// one SDK-fixed record shape, so the host can build it from your union).
/// `state` must be a named string-literal-union alias carrying exactly the
/// six AudioState members (any declaration order — the host matches members
/// by name). `positionMs`/`durationMs` are milliseconds; `playing` is the
/// player's transport state; `buffering` is true while a streamed source is
/// stalled waiting for network bytes; `bands` is the 32 spectrum band
/// magnitudes (0..255 each, all zeros outside "spectrum" events).
export type AudioEventArm = {
readonly state: AudioState;
readonly positionMs: number;
readonly durationMs: number;
readonly playing: boolean;
readonly buffering: boolean;
readonly bands: Uint8Array;
};
/// The Msg arms an audio event stream may target: arms whose payload is
/// exactly the six AudioEventArm fields. The `state` check runs BOTH
/// directions: the `&` constraint holds the arm's states to AudioState,
/// and the tuple-wrapped reverse check holds AudioState to the arm's
/// states — a narrower union would silently drop event states the host
/// emits, so it is refused here, not discovered at runtime.
export type AudioEventKind = M extends Msgish
? [Exclude] extends [keyof AudioEventArm]
? [keyof AudioEventArm] extends [Exclude]
? M extends Msgish & AudioEventArm
? [AudioState] extends [M["state"]]
? M["kind"]
: never
: never
: never
: never
: never;
/// The video event states, the audio vocabulary without spectrum: `loaded`
/// acknowledges a successful load with the player's duration estimate and
/// the stream's decoded pixel dimensions; `position` ticks at the
/// platform's honest cadence (~500ms) while playing; `completed` fires
/// exactly once when a NON-LOOPING playback reaches its natural end (a
/// looping playback wraps and never completes); `failed` reports a
/// load/decode/device failure; `rejected` a command the effects layer
/// refused (an empty or over-long source, a non-http(s) url, an invalid
/// surface id).
export type VideoState = "loaded" | "position" | "completed" | "failed" | "rejected";
/// The payload shape of a video event arm — seven fields, matched by NAME
/// (the AudioEventArm convention). `state` must be a named
/// string-literal-union alias carrying exactly the five VideoState members
/// (any declaration order — the host matches members by name).
/// `positionMs`/`durationMs` are milliseconds; `playing` is the player's
/// transport state; `buffering` is true while a streamed source is stalled
/// waiting for network bytes; `width`/`height` are the stream's decoded
/// pixel dimensions (delivered on "loaded", 0 elsewhere).
export type VideoEventArm = {
readonly state: VideoState;
readonly positionMs: number;
readonly durationMs: number;
readonly playing: boolean;
readonly buffering: boolean;
readonly width: number;
readonly height: number;
};
/// The Msg arms a video event stream may target: arms whose payload is
/// exactly the seven VideoEventArm fields. The `state` check runs BOTH
/// directions (the AudioEventKind convention): the `&` constraint holds
/// the arm's states to VideoState, and the tuple-wrapped reverse check
/// holds VideoState to the arm's states — a narrower union would
/// silently drop event states the host emits, so it is refused here,
/// not discovered at runtime.
export type VideoEventKind = M extends Msgish
? [Exclude] extends [keyof VideoEventArm]
? [keyof VideoEventArm] extends [Exclude]
? M extends Msgish & VideoEventArm
? [VideoState] extends [M["state"]]
? M["kind"]
: never
: never
: never
: never
: never;
/// The image load result states, mirroring the engine's outcome vocabulary:
/// "loaded" means the pixels are registered under the requested id (width and
/// height carry what the codec decoded); every other state is the failure
/// class — "rejected" (a refused command: invalid id, no source, a duplicate
/// live id), "not_found" (missing local file, no url), "io_failed" (a local
/// read failure), "connect_failed"/"tls_failed"/"protocol_failed"/"timed_out"
/// (the fetch taxonomy), "http_status" (a non-2xx answer; `status` carries
/// it), "cancelled", "too_large" (source or decoded pixels over budget),
/// "unsupported" (no platform codec), "decode_failed", "registry_full", and
/// "alloc_failed" (the host refused the memory the registration needed —
/// resource exhaustion, not corrupt bytes: the same source may load once
/// memory frees).
export type ImageState =
| "loaded"
| "rejected"
| "not_found"
| "io_failed"
| "connect_failed"
| "tls_failed"
| "protocol_failed"
| "timed_out"
| "http_status"
| "cancelled"
| "too_large"
| "unsupported"
| "decode_failed"
| "registry_full"
| "alloc_failed";
/// The payload shape of an image result arm — five fields, matched by NAME
/// (the AudioEventArm convention). `id` is the requested ImageId echoed
/// verbatim, so two concurrent loads sharing one event arm stay
/// distinguishable in `update` (an id the wire cannot carry exactly — 0,
/// negatives, fractions, 2^53 and past — echoes 0, the no-image sentinel:
/// there is no honest integer to echo); `state` must be a named
/// string-literal-union alias carrying exactly the fifteen ImageState
/// members (any declaration order — the host matches members by name);
/// `width`/`height` are the decoded pixel dimensions ("loaded" only, 0
/// otherwise); `status` is the HTTP status for url loads that performed an
/// exchange, 0 when none occurred (local paths, cache hits) — 0 is signal,
/// not a missing value: a cache hit is a real "loaded" with no exchange
/// behind it, so apps can distinguish a network load from a cached one.
export type ImageEventArm = {
readonly id: number;
readonly state: ImageState;
readonly width: number;
readonly height: number;
readonly status: number;
};
/// The Msg arms an image load result may target: arms whose payload is
/// exactly the five ImageEventArm fields. The `state` check runs BOTH
/// directions (the AudioEventKind convention): the `&` constraint holds
/// the arm's states to ImageState, and the tuple-wrapped reverse check
/// holds ImageState to the arm's states — a narrower union would
/// silently drop result states the host emits, so it is refused here,
/// not discovered at runtime.
export type ImageEventKind = M extends Msgish
? [Exclude] extends [keyof ImageEventArm]
? [keyof ImageEventArm] extends [Exclude]
? M extends Msgish & ImageEventArm
? [ImageState] extends [M["state"]]
? M["kind"]
: never
: never
: never
: never
: never;
/// The external-source channel event states, mirroring the engine's
/// vocabulary: "data" is one delivered post from the native side's
/// thread-safe handle; "closed" is the exactly-one terminal
/// `Cmd.channelClose` produces (final drop totals aboard); "rejected" is
/// the exactly-one terminal of a refused open (duplicate live key, a
/// full channel table, an occupied engine key).
export type ChannelState = "data" | "closed" | "rejected";
/// The payload shape of a channel event arm — five fields, matched by
/// NAME (the AudioEventArm convention). `key` is the channel key echoed
/// verbatim, so concurrent channels sharing one arm stay
/// distinguishable (a key the wire cannot carry exactly echoes 0);
/// `state` must be a named string-literal-union alias carrying exactly
/// the three ChannelState members (any declaration order — the host
/// matches members by name); `bytes` is the post's payload ("data"
/// events only, empty otherwise); `droppedPending`/`droppedTotal` are
/// the back-pressure counters — posts the native side's handle refused
/// since the previous delivered event, and over the channel's whole
/// life. Never silent drops: the counters ride every event.
export type ChannelEventArm = {
readonly key: number;
readonly state: ChannelState;
readonly bytes: Uint8Array;
readonly droppedPending: number;
readonly droppedTotal: number;
};
/// The Msg arms a channel event stream may target: arms whose payload is
/// exactly the five ChannelEventArm fields. The `state` check runs BOTH
/// directions (the AudioEventKind convention): the `&` constraint holds
/// the arm's states to ChannelState, and the tuple-wrapped reverse check
/// holds ChannelState to the arm's states — a narrower union would
/// silently drop event states the host emits, so it is refused here,
/// not discovered at runtime.
export type ChannelEventKind = M extends Msgish
? [Exclude] extends [keyof ChannelEventArm]
? [keyof ChannelEventArm] extends [Exclude]
? M extends Msgish & ChannelEventArm
? [ChannelState] extends [M["state"]]
? M["kind"]
: never
: never
: never
: never
: never;
/// `Cmd.channelOpen` routing: every channel event dispatches the `event`
/// arm (the five-field ChannelEventArm record, matched by field name).
export interface ChannelRoute {
readonly event: ChannelEventKind;
}
/// The native capture source. "microphone" captures the selected/default
/// input device; "system" captures the process-independent desktop output
/// mix (the OS may present a screen/audio consent prompt).
export type AudioCaptureSource = "microphone" | "system";
/// Capture stream states. A successful stream begins with "started", emits
/// zero or more "data" chunks, and ends with "stopped" after
/// `Cmd.audioCaptureStop`. "failed" reports a source/permission failure;
/// stop that stream to retire it. "rejected" means the runtime refused the
/// start before a native source was opened.
export type AudioCaptureState = "started" | "data" | "failed" | "stopped" | "rejected";
export type AudioCaptureSampleRate = 16000 | 24000 | 48000;
export type AudioCaptureChannels = 1 | 2;
/// Requested canonical output format. Native inputs are converted to
/// interleaved signed 16-bit little-endian PCM before delivery.
export interface AudioCaptureSpec {
readonly source: AudioCaptureSource;
readonly sampleRate?: AudioCaptureSampleRate;
readonly channels?: AudioCaptureChannels;
}
/// The payload shape of an audio-capture event arm, matched by field name.
/// `pcm` is populated only for "data" and is valid for this dispatch; copy
/// bytes retained by the model. Chunks are at most 20ms. Drop counters make
/// bounded-queue back-pressure observable rather than silent.
export type AudioCaptureEventArm = {
readonly key: number;
readonly state: AudioCaptureState;
readonly source: AudioCaptureSource;
readonly sampleRate: number;
readonly channels: number;
readonly timestampMs: number;
readonly frames: number;
readonly pcm: Uint8Array;
readonly droppedPending: number;
readonly droppedTotal: number;
};
export type AudioCaptureEventKind = M extends Msgish
? [Exclude] extends [keyof AudioCaptureEventArm]
? [keyof AudioCaptureEventArm] extends [Exclude]
? M extends Msgish & AudioCaptureEventArm
? [AudioCaptureState] extends [M["state"]]
? [AudioCaptureSource] extends [M["source"]]
? M["kind"]
: never
: never
: never
: never
: never
: never;
export interface AudioCaptureRoute {
readonly event: AudioCaptureEventKind;
}
/// The pty session event states, mirroring the engine's vocabulary:
/// "output" is one coalesced batch of child output; "exit" is the
/// exactly-one terminal every session produces — a clean end, a signal,
/// a `Cmd.ptyKill`, a refused spawn, or a transport that could not
/// start (the `reason` field tells which).
export type PtyState = "output" | "exit";
/// How a pty session ended, the spawn exit vocabulary: "exited" with
/// the child's code, "signaled" with the signal, "cancelled" after
/// `Cmd.ptyKill`, "rejected" for a spawn refused before a child existed
/// (duplicate live key, full table, bad grid), "spawn_failed" when the
/// pty or exec could not start.
export type PtyExitReason = "exited" | "signaled" | "cancelled" | "rejected" | "spawn_failed";
/// The payload shape of a pty event arm — seven fields, matched by NAME
/// (the AudioEventArm convention). `key` is the app's own session key
/// as byte text (the `Cmd.ptySpawn` `key`, or empty bytes when the spawn
/// named none) — two sessions routing one event arm are told apart by
/// this field. `state`
/// must be a named string-literal-union alias carrying exactly the two
/// PtyState members and `reason` one carrying exactly the five
/// PtyExitReason members (any declaration order — the host matches
/// members by name); `bytes` is the coalesced output batch ("output"
/// events only, empty otherwise); `code` is the child's exit code on an
/// "exited" end, -1 otherwise; `signal` is the fatal signal after a
/// "signaled" end, else 0; `droppedWrites` counts `Cmd.ptyWrite`
/// payloads refused over the session's life — zero means every write
/// reached the child, never a silent drop.
export type PtyEventArm = {
readonly key: Uint8Array;
readonly state: PtyState;
readonly bytes: Uint8Array;
readonly code: number;
readonly reason: PtyExitReason;
readonly signal: number;
readonly droppedWrites: number;
};
/// The Msg arms a pty session may target: arms whose payload is exactly
/// the seven PtyEventArm fields. The `state` and `reason` checks run BOTH
/// directions (the AudioEventKind convention): the `&` constraint holds
/// the arm's unions to PtyState/PtyExitReason, and the tuple-wrapped
/// reverse checks hold them to the arm's — a narrower union would
/// silently drop events the host emits, so it is refused here, not
/// discovered at runtime.
export type PtyEventKind = M extends Msgish
? [Exclude] extends [keyof PtyEventArm]
? [keyof PtyEventArm] extends [Exclude]
? M extends Msgish & PtyEventArm
? [PtyState] extends [M["state"]]
? [PtyExitReason] extends [M["reason"]]
? M["kind"]
: never
: never
: never
: never
: never
: never;
/// `Cmd.ptySpawn` routing: every session event dispatches the `event`
/// arm (the seven-field PtyEventArm record, matched by field name).
/// `cols`/`rows` are the initial grid the child observes (80x24 when
/// omitted); `term` is the TERM the child starts with (omitted = the
/// engine's default). The optional `key` names the session for
/// ptyWrite/ptyResize/ptyKill.
export interface PtyRoute {
readonly key?: string;
readonly cols?: number;
readonly rows?: number;
readonly term?: string;
readonly event: PtyEventKind;
}
/// One field of a host record payload; see hostRecordBytes for the encoding.
export type HostScalar = number | boolean | Uint8Array;
/// A structured host payload: a flat record of scalar/bytes fields, lowered
/// to one bytes payload at build time (natively) and by hostRecordBytes
/// (under node) — the same bytes either way.
export type HostRecord = { readonly [field: string]: HostScalar };
/// How a `Cmd.request` result comes back: the host dispatches the `ok` arm
/// with the result bytes on success, or the `err` arm with the error bytes.
/// Arm names are string literals — the routing is data, never a callback.
/// `key` (optional) names the in-flight effect for replace/cancel semantics.
export interface RequestRoute {
readonly key?: string;
readonly ok: BytesKind;
readonly err: BytesKind;
}
/// `Cmd.writeFile` routing: the ok arm carries no payload (success has
/// nothing to report); the err arm carries the reason bytes.
export interface WriteRoute {
readonly key?: string;
readonly ok: EmptyKind;
readonly err: BytesKind;
}
/// Streaming read routing: zero or more 256-KiB `chunk` messages, then one
/// `done` carrying the total byte count, or one `err` carrying a closed file
/// outcome.
export interface FileReadStreamRoute {
readonly key?: string;
readonly chunk: BytesKind;
readonly done: TimestampKind;
readonly err: BytesKind;
}
export interface FileStatArm {
readonly exists: boolean;
readonly size: number;
readonly mtimeMs: number;
}
export type FileStatKind = M extends Msgish
? [Exclude] extends [keyof FileStatArm]
? [keyof FileStatArm] extends [Exclude]
? M extends Msgish & FileStatArm ? M["kind"] : never
: never
: never
: never;
export interface FileStatRoute {
readonly key?: string;
readonly ok: FileStatKind;
readonly err: BytesKind;
}
/// Pagination controls for `Cmd.store.scan`. `limit` defaults to 100 and is
/// bounded at 256. `after` is the opaque key cursor returned by the previous
/// page; omit it for the first page.
export interface StoreScanOptions {
readonly limit?: number;
readonly after?: string | Uint8Array;
}
/// Dynamic UTF-8 SQLite TEXT. App-core text is byte-honest, so generated
/// TEXT parameters wrap model bytes with this marker; literal strings remain
/// accepted by the raw escape hatch.
export interface DbText {
readonly __dbText: true;
readonly bytes: ReadonlyArray;
}
export function dbText(bytes: Uint8Array): DbText {
const out: number[] = [];
for (let i = 0; i < bytes.length; i++) out.push(bytes[i]!);
return { __dbText: true, bytes: out };
}
/// Runtime SQL parameter values. Numbers preserve integer storage when they
/// are finite integral values; booleans bind as SQLite INTEGER 0/1.
export type DbValue = null | number | string | Uint8Array | boolean | DbText;
export type DbStatement = readonly [sql: string, params: ReadonlyArray];
/// A query dispatches every encoded row page through `page`, then exactly one
/// payload-less `done`. Any closed DbOutcome instead dispatches `err` as its
/// UTF-8 name (`constraint`, `busy`, `io_failed`, `corrupt`, `misuse`,
/// `rejected`, or `cancelled`).
export interface DbRowsRoute {
readonly key?: string;
readonly page: BytesKind;
readonly done: EmptyKind;
readonly err: BytesKind;
}
/// A generated declared-query route. The row parameter is intentionally
/// phantom on the wire: generated `decodePage` turns each bounded page
/// into this exact row shape without reflection.
export interface TypedRowsRoute extends DbRowsRoute {
readonly __row?: Row;
}
/// One generated, already-validated write statement. Only generated query
/// constructors create the brand; `Cmd.qTx` therefore cannot receive a
/// raw SQL string by accident.
export interface TypedDbStatement {
readonly sql: string;
readonly params: ReadonlyArray;
readonly __typedDbStatement: true;
}
// @native-sqlite-generated-types
/// `Cmd.fetch` routing: the ok arm carries `{ status, body }` (one number
/// field, one bytes field — matched by type); the err arm the reason bytes.
export interface FetchRoute {
readonly key?: string;
readonly ok: FetchedKind;
readonly err: BytesKind;
}
/// Streaming `Cmd.fetch` routing: each complete response line dispatches the
/// `line` arm with its bytes; the one successful terminal dispatches `ok` with
/// the HTTP status. A cut/dropped line dispatches `err` with `"truncated"` at
/// the terminal; transport failure and cancellation dispatch `err` with their
/// machine-readable reason bytes.
export interface FetchStreamRoute {
readonly key?: string;
readonly line: BytesKind;
readonly ok: TimestampKind;
readonly err: BytesKind;
}
/// `Cmd.spawn` routing, line mode: each stdout line dispatches the optional
/// `line` arm (one bytes field; omitted = lines dropped), a clean exit the
/// `exit` arm (one number field — the exit code), every other end the `err`
/// arm with the reason bytes.
export interface SpawnRoute {
readonly key?: string;
readonly stdin?: Uint8Array;
readonly line?: BytesKind;
readonly exit: TimestampKind;
readonly err: BytesKind;
}
/// `Cmd.spawn` routing, collect mode: whole stdout buffers until the exit,
/// which dispatches the `exit` arm as a two-field record — one number field
/// (the exit code) and one bytes field (the collected stdout), matched by
/// type so the names are yours. No line arm: there is no line framing.
export interface SpawnCollectRoute {
readonly key?: string;
readonly stdin?: Uint8Array;
readonly collect: true;
readonly exit: FetchedKind;
readonly err: BytesKind;
}
/// A `Cmd.audioPlay` source: the resolution cascade of the engine underneath.
/// The local `path` is tried first; a missing file falls through to `url`
/// (streamed progressively, cached at `cachePath` when given and verified
/// against `expectedBytes` — 0/omitted means unknown size, existence alone
/// qualifies a cache entry). At least one of path/url must be present.
export interface AudioSource {
readonly path?: Uint8Array;
readonly url?: Uint8Array;
readonly cachePath?: Uint8Array;
readonly expectedBytes?: number;
}
/// `Cmd.audioPlay` routing: every playback event dispatches the `event` arm
/// (the six-field AudioEventArm record, matched by field name).
export interface AudioRoute {
readonly event: AudioEventKind;
}
/// A `Cmd.videoLoad` source. `surface` is the model-owned media-surface id
/// the markup binds — the texture channel the decoded frames feed. The
/// local `path` is tried first; a missing file falls through to `url`
/// (streamed progressively, playable before the download finishes). At
/// least one of path/url must be present. `autoplay` (default true) starts
/// playback as soon as the load lands — false loads paused at position
/// zero, the poster-frame shape; `loop` wraps from the natural end back to
/// zero (a looping playback never delivers "completed"); `muted` starts
/// the audio track muted, independent of the remembered volume.
export interface VideoSource {
readonly surface: number;
readonly path?: Uint8Array;
readonly url?: Uint8Array;
readonly autoplay?: boolean;
readonly loop?: boolean;
readonly muted?: boolean;
}
/// `Cmd.videoLoad` routing: every playback event dispatches the `event`
/// arm (the seven-field VideoEventArm record, matched by field name).
export interface VideoRoute {
readonly event: VideoEventKind;
}
/// A `Cmd.imageLoad` source: the audio cascade's shape exactly. The local
/// `path` is tried first; a missing file falls through to `url` (fetched
/// whole, installed at `cachePath` when given and verified against
/// `expectedBytes` — 0/omitted means unknown size, existence alone qualifies
/// a cache entry; omit `cachePath` and the host derives the conventional
/// content-addressed path when a caches directory is configured). At least
/// one of path/url must be present.
export interface ImageSource {
readonly path?: Uint8Array;
readonly url?: Uint8Array;
readonly cachePath?: Uint8Array;
readonly expectedBytes?: number;
}
/// `Cmd.imageLoad` routing: the ONE terminal result dispatches the `event`
/// arm (the five-field ImageEventArm record, matched by field name).
export interface ImageRoute {
readonly event: ImageEventKind;
}
/// The closed HTTP verb set of `Cmd.fetch` (wire value = declaration order).
export type FetchMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD";
/// A `Cmd.fetch` request. `url` is bytes (asciiBytes for literals); headers
/// are a flat record whose NAMES are compile-time text and whose VALUES are
/// string literals or runtime bytes (`Uint8Array` — how a launch-supplied
/// credential rides an `Authorization` header); `timeoutMs` omitted means
/// the host engine's default.
export interface FetchSpec {
readonly url: Uint8Array;
readonly method?: FetchMethod;
readonly headers?: { readonly [name: string]: string | Uint8Array };
readonly body?: Uint8Array;
readonly timeoutMs?: number;
}
/// A line-streamed fetch request. `maxLineBytes` overrides the engine's 4 KiB
/// per-line default for SSE/NDJSON protocols whose individual records are
/// larger; it is bounded by the engine's 256 KiB per-line ceiling. If a line
/// is cut or dropped, the stream still ends through `err: "truncated"` rather
/// than reporting a successful terminal.
export interface FetchStreamSpec extends FetchSpec {
readonly maxLineBytes?: number;
}
/// A desktop notification request. Text is bytes so every field may come
/// directly from model data; subtitle and body default to empty. Delivery is
/// fire-and-forget because the OS may suppress an accepted request through
/// Focus / Do Not Disturb or the user's notification settings.
export interface NotificationSpec {
readonly id?: Uint8Array;
readonly title: Uint8Array;
readonly subtitle?: Uint8Array;
readonly body?: Uint8Array;
readonly actionLabel?: Uint8Array;
readonly actionCommand?: Uint8Array;
}
/// The three bounded host-local timestamp presentations. Unlike the markup
/// `date`/`time`/`datetime` functions (fixed UTC), this is intentionally an
/// effect because the user's locale and time zone are ambient host state.
export type LocalTimeStyle = "date" | "time" | "datetime";
/// An inert command value, parameterized by the app's Msg union so the
/// factories can validate message targets. Opaque to app code: build with
/// the `Cmd.*` factories, return from `update`/`initialModel`, never inspect
/// or store.
export type Cmd =
| { readonly op: "none" }
| { readonly op: "persist" }
| { readonly op: "now"; readonly msgKind: string }
| { readonly op: "host"; readonly name: string; readonly args: readonly number[] }
| { readonly op: "host_bytes"; readonly name: string; readonly payload: Uint8Array }
| {
readonly op: "request";
readonly name: string;
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly typedService: boolean;
readonly payload: Uint8Array;
}
| {
readonly op: "service_stream_request";
readonly name: string;
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly typedService: true;
readonly channelKey: number;
readonly eventKind: string;
readonly maxPending: number;
readonly payload: Uint8Array;
}
| { readonly op: "cancel"; readonly key: string }
| {
readonly op: "read_file";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly path: Uint8Array;
}
| {
readonly op: "write_file";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly path: Uint8Array;
readonly bytes: Uint8Array;
}
| {
readonly op: "append_file";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly path: Uint8Array;
readonly bytes: Uint8Array;
}
| {
readonly op: "stat_file";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly path: Uint8Array;
}
| {
readonly op: "delete_file";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly path: Uint8Array;
}
| {
readonly op: "read_file_stream";
readonly key: string;
readonly chunkKind: string;
readonly doneKind: string;
readonly errKind: string;
readonly path: Uint8Array;
}
| {
readonly op: "write_file_stream";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly path: Uint8Array;
}
| {
readonly op: "write_file_chunk";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly bytes: Uint8Array;
}
| {
readonly op: "write_file_close";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
}
| {
readonly op: "store_set";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly storeKey: string;
readonly bytes: Uint8Array;
}
| {
readonly op: "store_get" | "store_delete";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly storeKey: string;
}
| {
readonly op: "store_scan";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly prefix: string;
readonly limit: number;
readonly after: string | Uint8Array;
}
| {
readonly op: "store_set_many";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly entries: ReadonlyArray;
}
| {
readonly op: "db_query";
readonly key: string;
readonly pageKind: string;
readonly doneKind: string;
readonly errKind: string;
readonly sql: string;
readonly params: ReadonlyArray;
}
| {
readonly op: "db_exec";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly statements: ReadonlyArray;
}
| {
readonly op: "fetch";
readonly key: string;
readonly okKind: string;
readonly errKind: string;
readonly method: FetchMethod;
readonly timeoutMs: number;
readonly url: Uint8Array;
/// Header pairs, already in TS-field-name (code-unit) sort order.
/// A string value is compile-time text; a Uint8Array value is
/// runtime bytes (both encode as the record's length-prefixed
/// value field).
readonly headers: readonly { readonly name: string; readonly value: string | Uint8Array }[];
readonly body: Uint8Array;
}
| {
readonly op: "fetch_stream";
readonly key: string;
readonly lineKind: string;
readonly okKind: string;
readonly errKind: string;
readonly method: FetchMethod;
readonly timeoutMs: number;
readonly maxLineBytes: number;
readonly url: Uint8Array;
readonly headers: readonly { readonly name: string; readonly value: string | Uint8Array }[];
readonly body: Uint8Array;
}
| { readonly op: "clip_write"; readonly bytes: Uint8Array }
| { readonly op: "clip_read"; readonly key: string; readonly okKind: string; readonly errKind: string }
| { readonly op: "show_notification"; readonly id: Uint8Array; readonly title: Uint8Array; readonly subtitle: Uint8Array; readonly body: Uint8Array; readonly actionLabel: Uint8Array; readonly actionCommand: Uint8Array }
| { readonly op: "delay"; readonly key: string; readonly afterMs: number; readonly msgKind: string }
| {
readonly op: "spawn";
readonly key: string;
/// "" = no line routing (collect mode, or a line spawn that only
/// cares about the exit).
readonly lineKind: string;
readonly exitKind: string;
readonly errKind: string;
readonly collect: boolean;
readonly argv: readonly Uint8Array[];
readonly stdin: Uint8Array;
}
| {
readonly op: "audio_play";
readonly key: string;
readonly eventKind: string;
readonly path: Uint8Array;
readonly url: Uint8Array;
readonly cachePath: Uint8Array;
readonly expectedBytes: number;
}
| {
readonly op: "audio_ctl";
readonly key: string;
readonly verb: "pause" | "resume" | "stop" | "seek" | "volume";
/// Seek position (ms) / volume (0..1); 0 for the value-less verbs.
readonly value: number;
}
| {
readonly op: "video_load";
readonly key: string;
readonly eventKind: string;
readonly surface: number;
readonly path: Uint8Array;
readonly url: Uint8Array;
readonly autoplay: boolean;
readonly loop: boolean;
readonly muted: boolean;
}
| {
readonly op: "video_ctl";
readonly key: string;
readonly verb: "play" | "pause" | "stop" | "seek" | "volume" | "muted" | "loop";
/// Seek position (ms) / volume (0..1) / the muted-loop switch
/// (0 = off, 1 = on); 0 for the value-less verbs.
readonly value: number;
}
| { readonly op: "window_show"; readonly label: string }
| { readonly op: "window_hide"; readonly label: string }
| { readonly op: "dock_presence"; readonly visible: boolean }
| { readonly op: "quit_app" }
| {
readonly op: "image_load";
readonly id: number;
readonly eventKind: string;
readonly path: Uint8Array;
readonly url: Uint8Array;
readonly cachePath: Uint8Array;
readonly expectedBytes: number;
}
| { readonly op: "image_cancel"; readonly id: number }
| { readonly op: "image_unregister"; readonly id: number }
| { readonly op: "channel_open"; readonly key: number; readonly eventKind: string; readonly maxPending: number }
| { readonly op: "channel_close"; readonly key: number }
| {
readonly op: "audio_capture_start";
readonly key: number;
readonly source: AudioCaptureSource;
readonly sampleRate: number;
readonly channels: number;
readonly eventKind: string;
}
| { readonly op: "audio_capture_stop"; readonly key: number }
| {
readonly op: "pty_spawn";
readonly key: string;
readonly eventKind: string;
readonly cols: number;
readonly rows: number;
/// "" = the engine's default TERM (the wire never bakes it in).
readonly term: string;
readonly argv: readonly Uint8Array[];
}
| { readonly op: "pty_write"; readonly key: string; readonly bytes: Uint8Array }
| { readonly op: "pty_resize"; readonly key: string; readonly cols: number; readonly rows: number }
| { readonly op: "pty_kill"; readonly key: string }
| { readonly op: "batch"; readonly cmds: readonly Cmd[] };
/// The wire encoding of a host record payload, byte-identical to what the
/// transpiler derives from the record's TS shape at build time: fields
/// sorted by name (code-unit order), concatenated with no field headers —
/// number -> f64 little-endian (8 bytes), boolean -> one 0/1 byte,
/// Uint8Array -> u32 little-endian length + bytes.
export function hostRecordBytes(payload: HostRecord): Uint8Array {
const names = Object.keys(payload).sort();
let len = 0;
for (const n of names) {
const v = payload[n];
if (typeof v === "number") len += 8;
else if (typeof v === "boolean") len += 1;
else len += 4 + v.length;
}
const out = new Uint8Array(len);
const dv = new DataView(out.buffer);
let off = 0;
for (const n of names) {
const v = payload[n];
if (typeof v === "number") {
dv.setFloat64(off, v, true);
off += 8;
} else if (typeof v === "boolean") {
out[off] = v ? 1 : 0;
off += 1;
} else {
dv.setUint32(off, v.length, true);
off += 4;
out.set(v, off);
off += v.length;
}
}
return out;
}
function serviceU32(value: number): Uint8Array {
const out = new Uint8Array(4);
out[0] = value & 255;
out[1] = (value >>> 8) & 255;
out[2] = (value >>> 16) & 255;
out[3] = (value >>> 24) & 255;
return out;
}
/// Canonical service-value codec primitives. Generated clients compose these
/// in declaration order; the service host and Zig result dispatcher consume
/// the same format as shim_rt (LE scalars, length-prefixed bytes/slices,
/// declaration-order enum/union tags).
export function serviceConcat(parts: readonly Uint8Array[]): Uint8Array {
let length = 0;
for (const part of parts) length += part.length;
const out = new Uint8Array(length);
let at = 0;
for (const part of parts) {
out.set(part, at);
at += part.length;
}
return out;
}
export function serviceBoolBytes(value: boolean): Uint8Array {
return new Uint8Array([value ? 1 : 0]);
}
export function serviceF64Bytes(value: number): Uint8Array {
const out = new Uint8Array(8);
const view = new DataView(out.buffer);
view.setFloat64(0, Number.isNaN(value) ? Number.NaN : value, true);
return out;
}
export function serviceI64Bytes(value: number): Uint8Array {
const out = new Uint8Array(8);
const view = new DataView(out.buffer);
const base = 4294967296;
let low = value % base;
if (low < 0) low += base;
const high = Math.floor(value / base);
view.setUint32(0, low, true);
view.setInt32(4, high, true);
return out;
}
export function serviceBytes(value: Uint8Array): Uint8Array {
return serviceConcat([serviceU32(value.length), value]);
}
export function serviceEnumBytes(index: number): Uint8Array {
return serviceU32(index);
}
export function serviceUnionBytes(index: number): Uint8Array {
return new Uint8Array([index]);
}
export function serviceOptionalBytes(value: Uint8Array | null): Uint8Array {
return value === null ? new Uint8Array([0]) : serviceConcat([new Uint8Array([1]), value]);
}
export function serviceSliceBytes(values: readonly Uint8Array[]): Uint8Array {
return serviceConcat([serviceU32(values.length), ...values]);
}
function lowerHostPayload(payload: Uint8Array | HostRecord): Uint8Array {
return payload instanceof Uint8Array ? payload : hostRecordBytes(payload);
}
/// A host command by name; the host decides what the name means. The name is
/// a string literal. Args are scalar numbers, or exactly one bytes payload
/// (a Uint8Array, or a flat record that lowers to bytes).
function hostCmd(name: string, payload: Uint8Array | HostRecord): Cmd;
function hostCmd(name: string, ...args: readonly number[]): Cmd;
function hostCmd(name: string, ...rest: readonly (number | Uint8Array | HostRecord)[]): Cmd {
const first = rest[0];
if (rest.length === 1 && typeof first === "object" && first !== null) {
return { op: "host_bytes", name, payload: lowerHostPayload(first) };
}
return { op: "host", name, args: rest as readonly number[] };
}
/// HTTP(S) as effect data. The two-field `{ ok, err }` route buffers the whole
/// response. Adding `line` selects line streaming: each complete line arrives
/// through that one-bytes-field arm, then `ok` receives the terminal HTTP
/// status as its one number payload. A non-2xx status is still `ok` because the
/// server delivered a response; cut/dropped stream lines, transport failures,
/// and cancellation use `err` (`"truncated"` for stream data loss).
function fetchCmd(spec: FetchSpec, route: FetchRoute): Cmd;
function fetchCmd(spec: FetchStreamSpec, route: FetchStreamRoute): Cmd;
function fetchCmd(
spec: FetchStreamSpec,
route: FetchRoute | FetchStreamRoute,
): Cmd {
const names = Object.keys(spec.headers ?? {}).sort();
const headers = names.map((n) => ({ name: n, value: spec.headers![n] }));
if ("line" in route) {
return {
op: "fetch_stream",
key: route.key ?? "",
lineKind: route.line,
okKind: route.ok,
errKind: route.err,
method: spec.method ?? "GET",
timeoutMs: spec.timeoutMs ?? 0,
maxLineBytes: spec.maxLineBytes ?? 0,
url: spec.url,
headers,
body: spec.body ?? new Uint8Array(0),
};
}
return {
op: "fetch",
key: route.key ?? "",
okKind: route.ok,
errKind: route.err,
method: spec.method ?? "GET",
timeoutMs: spec.timeoutMs ?? 0,
url: spec.url,
headers,
body: spec.body ?? new Uint8Array(0),
};
}
export const Cmd = {
/// No effects. `return model` is sugar for `return [model, Cmd.none]`.
none: { op: "none" } as Cmd,
/// Snapshot the just-committed Model through the engine-owned store.
/// Requires app.zon's `persist` capability and restore-route config;
/// the host owns coalescing, atomic placement, and journal/replay.
persist(): Cmd {
return { op: "persist" };
},
/// Request the current time. The runtime dispatches the named Msg arm with
/// the timestamp (milliseconds, a plain number) as its single payload field.
now(msgKind: TimestampKind): Cmd {
return { op: "now", msgKind };
},
host: hostCmd,
/// A routed host command: the host performs `name` with the payload and
/// dispatches exactly one result Msg back — the `ok` arm with the result
/// bytes, or the `err` arm with the error bytes. Both arms must carry
/// exactly one Uint8Array payload field (tsc checks that). An optional
/// `key` names the in-flight effect: re-issuing a live key replaces it,
/// and Cmd.cancel(key) drops it.
request(
name: string,
payload: Uint8Array | HostRecord,
route: RequestRoute,
): Cmd {
return {
op: "request",
name,
key: route.key ?? "",
okKind: route.ok,
errKind: route.err,
typedService: false,
payload: lowerHostPayload(payload),
};
},
/// Generated typed service clients use this constructor after encoding the
/// request shape. It lowers to the ordinary request wire record with the
/// typed-result bit set so the host decodes the success payload.
serviceRequest(
name: string,
payload: Uint8Array,
route: ServiceRoute,
): Cmd {
return {
op: "request",
name,
key: route.key ?? "",
okKind: route.ok,
errKind: route.err,
typedService: true,
payload,
};
},
serviceStreamRequest(
name: string,
channelKey: number,
payload: Uint8Array,
route: ServiceStreamRoute,
maxPending: number,
): Cmd {
return {
op: "service_stream_request",
name,
key: route.key ?? "",
okKind: route.ok,
errKind: route.err,
typedService: true,
channelKey,
eventKind: route.event,
maxPending,
payload: serviceConcat([serviceF64Bytes(channelKey), payload]),
};
},
/// Drop the in-flight keyed effect — request, named engine op, or delay —
/// with this key, if any, SILENTLY (neither routing arm is dispatched for
/// it). Live spawn, streaming-fetch, streaming-service, and write-file-sink operations are the exceptions:
/// cancel ends the stream and its err arm runs with "cancelled".
cancel(key: string): Cmd {
return { op: "cancel", key };
},
/// Read a whole file. Exactly one terminal Msg: the `ok` arm with the
/// content bytes (one Uint8Array field), or the `err` arm with the reason
/// bytes ("not_found", "io_failed", "truncated", "rejected").
readFile(path: Uint8Array, route: RequestRoute): Cmd {
return { op: "read_file", key: route.key ?? "", okKind: route.ok, errKind: route.err, path };
},
/// Write a whole file (parent directories created, an existing file
/// replaced whole). Exactly one terminal Msg: the `ok` arm — which carries
/// no payload — or the `err` arm with the reason bytes.
writeFile(path: Uint8Array, bytes: Uint8Array, route: WriteRoute): Cmd {
return { op: "write_file", key: route.key ?? "", okKind: route.ok, errKind: route.err, path, bytes };
},
appendFile(path: Uint8Array, bytes: Uint8Array, route: WriteRoute): Cmd {
return { op: "append_file", key: route.key ?? "", okKind: route.ok, errKind: route.err, path, bytes };
},
statFile(path: Uint8Array, route: FileStatRoute): Cmd {
return { op: "stat_file", key: route.key ?? "", okKind: route.ok, errKind: route.err, path };
},
/// Delete one file. The ok arm carries no payload. A final symlink is
/// unlinked without deleting its target. A missing file routes the err arm
/// with "not_found"; directories and other OS refusals route "io_failed".
deleteFile(path: Uint8Array, route: WriteRoute): Cmd {
return { op: "delete_file", key: route.key ?? "", okKind: route.ok, errKind: route.err, path };
},
readFileStream(path: Uint8Array, route: FileReadStreamRoute): Cmd {
return { op: "read_file_stream", key: route.key ?? "", chunkKind: route.chunk, doneKind: route.done, errKind: route.err, path };
},
writeFileStream(key: string, path: Uint8Array, route: WriteRoute): Cmd {
return { op: "write_file_stream", key, okKind: route.ok, errKind: route.err, path };
},
writeFileChunk(key: string, bytes: Uint8Array, route: WriteRoute): Cmd {
return { op: "write_file_chunk", key, okKind: route.ok, errKind: route.err, bytes };
},
writeFileClose(key: string, route: WriteRoute): Cmd {
return { op: "write_file_close", key, okKind: route.ok, errKind: route.err };
},
/// Capability-gated, engine-owned per-record storage. Keys are UTF-8 text
/// up to 512 bytes; values are bytes up to 1 MiB. Results remain effects:
/// they arrive through the supplied Msg routes after update commits.
store: {
set(storeKey: string, bytes: Uint8Array, route: WriteRoute): Cmd {
return { op: "store_set", key: route.key ?? "", okKind: route.ok, errKind: route.err, storeKey, bytes };
},
/// The ok payload is `[1][value...]` for a hit or `[0]` for a miss, so
/// an empty stored value remains distinguishable from absence.
get(storeKey: string, route: RequestRoute): Cmd {
return { op: "store_get", key: route.key ?? "", okKind: route.ok, errKind: route.err, storeKey };
},
/// Deleting an absent key succeeds.
delete(storeKey: string, route: WriteRoute): Cmd {
return { op: "store_delete", key: route.key ?? "", okKind: route.ok, errKind: route.err, storeKey };
},
/// The ok payload is a length-prefixed page of `(key,value)` pairs and
/// a next-key cursor. Pages end at record boundaries; data is never cut.
scan(prefix: string, options: StoreScanOptions, route: RequestRoute): Cmd {
return {
op: "store_scan",
key: route.key ?? "",
okKind: route.ok,
errKind: route.err,
prefix,
limit: options.limit ?? 0,
after: options.after ?? "",
};
},
/// Atomically upsert all entries (at most 64 entries / 8 MiB encoded).
setMany(entries: ReadonlyArray, route: WriteRoute): Cmd {
return { op: "store_set_many", key: route.key ?? "", okKind: route.ok, errKind: route.err, entries };
},
},
/// App-scoped OS credential storage. The app manifest id supplies the
/// service namespace, so authored code chooses only a UTF-8 key. Declare
/// both the `credentials` capability and permission in app.zon.
credentials: {
set(credentialKey: string, secret: Uint8Array, route: WriteRoute): Cmd {
return {
op: "request",
name: "core.credentials.set",
key: route.key ?? "",
okKind: route.ok,
errKind: route.err,
typedService: false,
payload: hostRecordBytes({ key: utf8Bytes(credentialKey), secret }),
};
},
/// The ok arm carries secret bytes; a miss routes err with `miss`.
get(credentialKey: string, route: RequestRoute): Cmd {
return Cmd.request("core.credentials.get", { key: utf8Bytes(credentialKey) }, route);
},
/// Deleting an absent key succeeds.
delete(credentialKey: string, route: WriteRoute): Cmd {
return {
op: "request",
name: "core.credentials.delete",
key: route.key ?? "",
okKind: route.ok,
errKind: route.err,
typedService: false,
payload: hostRecordBytes({ key: utf8Bytes(credentialKey) }),
};
},
},
/// Capability-gated relational SQLite. Reads remain effects: pages and the
/// terminal arrive as Msg values after the issuing model has committed.
/// Every exec statement in one command commits atomically or all roll back.
db: {
query(sql: string, params: ReadonlyArray, route: DbRowsRoute): Cmd {
return {
op: "db_query",
key: route.key ?? "",
pageKind: route.page,
doneKind: route.done,
errKind: route.err,
sql,
params,
};
},
exec(statements: ReadonlyArray, route: WriteRoute): Cmd {
return { op: "db_exec", key: route.key ?? "", okKind: route.ok, errKind: route.err, statements };
},
},
// @native-sqlite-generated-cmds
fetch: fetchCmd,
/// Put bytes on the system clipboard, fire-and-forget (an over-bound or
/// refused write is dropped — there is no route to report on).
clipboardWrite(bytes: Uint8Array): Cmd {
return { op: "clip_write", bytes };
},
/// Read the system clipboard. Exactly one terminal Msg: the `ok` arm with
/// the text bytes, or the `err` arm with the reason bytes ("failed",
/// "rejected").
clipboardRead(route: RequestRoute): Cmd {
return { op: "clip_read", key: route.key ?? "", okKind: route.ok, errKind: route.err };
},
/// Ask the desktop host to show a system notification. Fire-and-forget:
/// platform acceptance cannot promise display while OS focus modes and user
/// notification settings remain authoritative. A stable id replaces the
/// earlier notification with that id; paired actionLabel/actionCommand bytes
/// dispatch through the normal app-command path while the process is live.
/// Empty or over-bound titles, over-bound optional fields, unpaired actions,
/// invalid command names, and unavailable services fail closed.
showNotification(spec: NotificationSpec): Cmd {
return {
op: "show_notification",
id: spec.id ?? new Uint8Array(0),
title: spec.title,
subtitle: spec.subtitle ?? new Uint8Array(0),
body: spec.body ?? new Uint8Array(0),
actionLabel: spec.actionLabel ?? new Uint8Array(0),
actionCommand: spec.actionCommand ?? new Uint8Array(0),
};
},
/// Open an HTTP(S) URL in the user's system browser. Fire-and-forget: the
/// runtime validates the URL and enforces `external_links`; invalid,
/// denied, or unavailable requests fail closed.
openExternalUrl(url: Uint8Array): Cmd {
return { op: "host_bytes", name: "native-sdk.os.openUrl", payload: url };
},
/// Reveal a path in the system file manager (Finder, Files, or Explorer).
/// Fire-and-forget; invalid or unavailable requests fail closed.
revealPath(path: Uint8Array): Cmd {
return { op: "host_bytes", name: "native-sdk.os.revealPath", payload: path };
},
/// Format an epoch timestamp (milliseconds, the same unit as `Cmd.now`) in
/// the host's current locale and local time zone. The localized UTF-8 text
/// arrives on ok; invalid timestamps or unavailable services route err.
formatLocalTime(timestampMs: number, style: LocalTimeStyle, route: RequestRoute): Cmd {
const styleCode = style === "date" ? 0 : style === "time" ? 1 : 2;
return Cmd.request("native-sdk.time.formatLocal", { style: styleCode, timestampMs }, route);
},
/// A keyed one-shot delay: dispatch the named Msg arm once, `ms` from now,
/// with the fire time (milliseconds) as its single number payload field.
/// Re-issuing a live key re-arms it from now (the debounce discipline);
/// `Cmd.cancel(key)` drops it silently.
delay(key: string, ms: number, msgKind: TimestampKind): Cmd {
return { op: "delay", key, afterMs: ms, msgKind };
},
/// Run a subprocess as a STREAM: each stdout line dispatches the `line`
/// arm as it arrives (line mode), or whole stdout buffers to the exit
/// (`collect: true`); exactly one terminal follows — the `exit` arm on a
/// clean exit, the `err` arm with the reason bytes on every other end.
/// The key stays live for the whole stream: `Cmd.cancel(key)` ends the
/// child mid-stream (err arm "cancelled" — loud on purpose: killing a
/// process is an observable event), and a spawn whose key is already
/// streaming is rejected, never replaced — a running subprocess is never
/// killed implicitly; cancel it first.
spawn(
argv: readonly Uint8Array[],
route: SpawnRoute | SpawnCollectRoute,
): Cmd {
const collect = "collect" in route && route.collect === true;
return {
op: "spawn",
key: route.key ?? "",
lineKind: collect ? "" : ((route as SpawnRoute).line ?? ""),
exitKind: route.exit,
errKind: route.err,
collect,
argv,
stdin: route.stdin ?? new Uint8Array(0),
};
},
/// Open (or replace — one player is the whole surface) the keyed audio
/// event stream: resolve the source cascade (local path, then url, cached
/// and integrity-gated) and start playback. Every playback event
/// dispatches the `event` arm until `Cmd.audioStop(key)` closes the
/// stream. Failure is never silent: an unplayable source arrives as a
/// "failed" event, a refused command as "rejected".
audioPlay(key: string, source: AudioSource, route: AudioRoute): Cmd {
return {
op: "audio_play",
key,
eventKind: route.event,
path: source.path ?? new Uint8Array(0),
url: source.url ?? new Uint8Array(0),
cachePath: source.cachePath ?? new Uint8Array(0),
expectedBytes: source.expectedBytes ?? 0,
};
},
/// Pause the keyed playback in place (no event echo — the caller
/// commanded it). A key with no open stream no-ops.
audioPause(key: string): Cmd {
return { op: "audio_ctl", key, verb: "pause", value: 0 };
},
/// Resume the keyed playback. A player that can no longer resume reports
/// one "failed" event on the stream instead of silence.
audioResume(key: string): Cmd {
return { op: "audio_ctl", key, verb: "resume", value: 0 };
},
/// Stop the keyed playback and CLOSE its event stream: no events for the
/// key after this. Stop is the audio stream's cancel.
audioStop(key: string): Cmd {
return { op: "audio_ctl", key, verb: "stop", value: 0 };
},
/// Jump the keyed playback to `ms` (the platform clamps to the duration).
/// No event echo — the next position tick reports from there.
audioSeek(key: string, ms: number): Cmd {
return { op: "audio_ctl", key, verb: "seek", value: ms };
},
/// Set playback volume, clamped to 0..1 and remembered across tracks: the
/// next audioPlay re-applies it.
audioSetVolume(key: string, volume: number): Cmd {
return { op: "audio_ctl", key, verb: "volume", value: volume };
},
/// Open (or replace — one player is the whole surface) the keyed video
/// event stream: claim the media-surface the source names, resolve the
/// source cascade (local path, then url) and start playback (autoplay,
/// the default). Every playback event dispatches the `event` arm until
/// `Cmd.videoStop(key)` closes the stream. Pixels never ride the
/// events: decoded frames flow platform-side into the bound surface.
/// Failure is never silent: an unplayable source arrives as a "failed"
/// event, a refused command as "rejected". Replacing is not stopping:
/// a replaced load still delivers the terminal it owes (its failure is
/// never silent either), routed to ITS OWN event arm — only
/// `Cmd.videoStop` cancels a stream's undelivered answers.
videoLoad(key: string, source: VideoSource, route: VideoRoute): Cmd {
return {
op: "video_load",
key,
eventKind: route.event,
surface: source.surface,
path: source.path ?? new Uint8Array(0),
url: source.url ?? new Uint8Array(0),
autoplay: source.autoplay ?? true,
loop: source.loop ?? false,
muted: source.muted ?? false,
};
},
/// Start or resume the loaded playback — the poster-frame counterpart
/// of `autoplay: false`, and un-pause. A key with no open stream
/// no-ops; a player that can no longer start reports one "failed"
/// event on the stream instead of silence.
videoPlay(key: string): Cmd {
return { op: "video_ctl", key, verb: "play", value: 0 };
},
/// Pause the keyed playback in place; the surface keeps its last frame
/// (no event echo — the caller commanded it). A key with no open
/// stream no-ops.
videoPause(key: string): Cmd {
return { op: "video_ctl", key, verb: "pause", value: 0 };
},
/// Stop the keyed playback, release the surface claim, and CLOSE its
/// event stream: no events for the key after this. Stop is the video
/// stream's cancel, exactly `Cmd.audioStop`'s discipline.
videoStop(key: string): Cmd {
return { op: "video_ctl", key, verb: "stop", value: 0 };
},
/// Jump the keyed playback to `ms` (the platform clamps to the
/// duration; a paused seek still pushes the sought frame, so scrubbing
/// is visible). No event echo — the next position tick reports from
/// there.
videoSeek(key: string, ms: number): Cmd {
return { op: "video_ctl", key, verb: "seek", value: ms };
},
/// Set playback volume, clamped to 0..1 and remembered across loads:
/// the next videoLoad re-applies it. Independent of mute.
videoSetVolume(key: string, volume: number): Cmd {
return { op: "video_ctl", key, verb: "volume", value: volume };
},
/// Mute or unmute the playback's audio track without touching the
/// remembered volume (a fresh load's `muted` option is the way to
/// start muted).
videoSetMuted(key: string, muted: boolean): Cmd {
return { op: "video_ctl", key, verb: "muted", value: muted ? 1 : 0 };
},
/// Enable or disable looping on the open playback (a fresh load's
/// `loop` option covers the start-looping case). A looping playback
/// never delivers "completed".
videoSetLoop(key: string, loop: boolean): Cmd {
return { op: "video_ctl", key, verb: "loop", value: loop ? 1 : 0 };
},
/// Show the window with the declared `label`: un-hide + activate — the
/// counterpart to a `close_policy = "hide"` hide and the tray "Open"
/// consequence; also restores a minimized window. Fire-and-forget: no
/// result Msg (the window's own frame event carries the state), and an
/// unknown label is a no-op. The label is a string literal — window
/// labels are declarations.
showWindow(label: string): Cmd {
return { op: "window_show", label };
},
/// Hide a live window without closing it. Its views and native identity
/// remain intact; `showWindow` brings it back. Unknown labels no-op.
hideWindow(label: string): Cmd {
return { op: "window_hide", label };
},
/// Control whether the app appears in the macOS Dock and app switcher.
/// Unsupported hosts ignore the command.
setDockPresence(visible: boolean): Cmd {
return { op: "dock_presence", visible };
},
/// Query the installed bundle's launch-at-login registration. The ok
/// route receives UTF-8 bytes naming `enabled`, `disabled`,
/// `requires_approval`, or `not_found`; the err route receives a reason.
launchAtLoginStatus(route: RequestRoute): Cmd {
return Cmd.request("native-sdk.launch-at-login.status", new Uint8Array(0), route);
},
/// Register or unregister the installed bundle for launch at login. The
/// ok and err payloads follow `launchAtLoginStatus`.
setLaunchAtLogin(enabled: boolean, route: RequestRoute): Cmd {
return Cmd.request("native-sdk.launch-at-login.set", new Uint8Array([enabled ? 1 : 0]), route);
},
/// Quit the app for real — the graceful terminate, and the tray "Quit"
/// consequence. The host quits through the SAME shutdown path a
/// last-window close takes, so the stop hook runs exactly once and a
/// recording session seals its journal. Fire-and-forget.
quitApp(): Cmd {
return { op: "quit_app" };
},
/// Load an image at runtime under the model-owned numeric ImageId your
/// markup binds (``, ``):
/// resolve the source cascade (local path first, then a verified cache
/// entry, then the network), decode through the platform codec, register
/// the pixels under `id`, and dispatch exactly ONE `event` arm — state
/// "loaded" with the decoded width/height, or one failure class, always
/// echoing the requested id so concurrent loads sharing the arm stay
/// distinguishable. Failure is never silent, and views referencing the
/// id repaint on the next
/// frame. One load per id at a time: a duplicate live id dispatches state
/// "rejected" (finish or re-key instead — ids are model data). Ids are
/// positive integers below 2^53 outside the reserved bit-63 namespace;
/// 0 is the no-image sentinel and dispatches "rejected".
imageLoad(id: number, source: ImageSource, route: ImageRoute): Cmd {
return {
op: "image_load",
id,
eventKind: route.event,
path: source.path ?? new Uint8Array(0),
url: source.url ?? new Uint8Array(0),
cachePath: source.cachePath ?? new Uint8Array(0),
expectedBytes: source.expectedBytes ?? 0,
};
},
/// End the in-flight image load under `id`, if any — LOUDLY: the load's
/// one terminal arrives as its own `event` arm with state "cancelled"
/// (ending an in-flight load is an observable event, the spawn cancel
/// discipline), and the id is free for a fresh load once that terminal
/// lands. Aimed at an id with no live load it no-ops (the load it aimed
/// at already delivered its terminal). Image loads are keyed by their
/// numeric id, so the string-keyed `Cmd.cancel` never touches them —
/// this is their cancel, the way `Cmd.audioStop` is audio's.
imageCancel(id: number): Cmd {
return { op: "image_cancel", id };
},
/// Free the registry slot under `id`: the pixels are released, views
/// referencing the id draw their fallback (avatar initials) on the next
/// frame, and the slot — one of the registry's 16 — is open for another
/// load (the gallery eviction move: unregister the evictee, load the
/// newcomer under a fresh id). Like registration itself this is
/// synchronous registry surgery, not an effect: no Msg follows, and an
/// id with no registration no-ops (whatever it aimed at is already
/// gone, `Cmd.imageCancel`'s idle rule). It frees only the CURRENT
/// registration — a load IN FLIGHT under the id is untouched and its
/// terminal still registers the pixels; to keep the slot free, end the
/// load with `Cmd.imageCancel(id)` first.
imageUnregister(id: number): Cmd {
return { op: "image_unregister", id };
},
/// Open an external-source channel under your numeric `key`: the
/// host stages a long-lived, thread-safe posting seam its NATIVE
/// side feeds — an embedder or platform-services extension resolves
/// the handle (`Effects.channelHandle(key)`) and posts bytes from
/// its own threads (a socket reader, a file watcher, a worker), and
/// each accepted post arrives as one "data" event through the
/// `event` arm, waking the loop itself — no timer polling anywhere.
/// POSTING is deliberately not a TS verb: transpiled cores are
/// single-threaded, so the TS tier opens, closes, and receives while
/// the native side feeds. Back-pressure is part of the contract:
/// the native `post` answers a four-way `ChannelHandle.PostResult`
/// (accepted / dropped_full / dropped_oversized / closed), so a
/// producer tells transient back-pressure from closure, and refused
/// posts count into `droppedPending`/`droppedTotal` on the next
/// delivered event, never silence. The native post never blocks its
/// producer given a conforming host wake — the platform's `wake_fn`
/// is contractually a bounded, enqueue-only nudge (see
/// `PlatformServices.wake_fn`; every first-party host conforms), and
/// the runtime holds no channel lock across it. One channel per key at a
/// time — a duplicate live key dispatches state "rejected" — and the
/// key shares the engine's effect-key space (a same-key fetch is
/// blocked while the channel lives). Keys are positive integers
/// below 2^53; the events are journaled at the effect boundary, so
/// a recorded session replays the whole stream from the journal and
/// never NEEDS the source — under replay the open parks and the
/// native handle is inert (every post answers closed). The opening
/// update still re-executes, so a native producer launched
/// unconditionally really starts and is stopped at its first post;
/// one that consults the handle's live() before launching never
/// starts, keeping replay fully offline.
channelOpen(key: number, route: ChannelRoute): Cmd {
return { op: "channel_open", key, eventKind: route.event, maxPending: 64 };
},
/// Close the open channel under `key`: posts stop landing, the
/// staged backlog flushes, and exactly one "closed" event (final
/// drop totals aboard) dispatches the event arm — then the key is
/// free again. A key with no open channel no-ops. Channels are keyed
/// by their numeric key, so the string-keyed `Cmd.cancel` never
/// touches them — this is their close, the way `Cmd.audioStop` is
/// audio's.
channelClose(key: number): Cmd {
return { op: "channel_close", key };
},
/// Start microphone or system-output capture under a positive numeric
/// `key`. The stream is normalized to interleaved signed 16-bit LE PCM
/// at the requested rate/channels (48kHz mono by default). Microphone and
/// system capture may run concurrently; only one stream per source may
/// be live. The event stream is bounded and journal/replay aware.
audioCaptureStart(key: number, spec: AudioCaptureSpec, route: AudioCaptureRoute): Cmd {
return {
op: "audio_capture_start",
key,
source: spec.source,
sampleRate: spec.sampleRate ?? 48000,
channels: spec.channels ?? 1,
eventKind: route.event,
};
},
/// Stop capture under `key`. Native callbacks quiesce synchronously,
/// accepted backlog drains, then exactly one "stopped" event retires the
/// stream. A key with no live capture is a no-op.
audioCaptureStop(key: number): Cmd {
return { op: "audio_capture_stop", key };
},
/// Open a pseudo-terminal session — a spawn with a different
/// transport: run `argv` on a fresh pty (same argv budgets, same
/// child environment policy) whose initial grid is `cols` x `rows`
/// (80x24 by default) and whose TERM is `term` (omitted = the
/// engine's default). Every session event dispatches the `event` arm:
/// "output" events carry coalesced batches of child output across
/// dispatches — feed them to the terminal emulator and move on —
/// until the exactly-one "exit" terminal retires the session (a
/// refused spawn is one "exit" with reason "rejected"; a transport
/// that could not start, one with "spawn_failed" — failure is never
/// silent). One session per key at a time, never replaced implicitly:
/// a running terminal's child is a running subprocess — kill it
/// first, the spawn discipline.
ptySpawn(argv: readonly Uint8Array[], route: PtyRoute): Cmd {
return {
op: "pty_spawn",
key: route.key ?? "",
eventKind: route.event,
cols: route.cols ?? 80,
rows: route.rows ?? 24,
term: route.term ?? "",
argv,
};
},
/// Write bytes toward the keyed session's child — keystrokes and
/// pastes, fire-and-forget: a key with no open session no-ops (the
/// exit was already on its way), and refused payloads (over the
/// engine's per-write bound, or a child that stopped reading) count
/// into the exit event's droppedWrites — never silence.
ptyWrite(key: string, bytes: Uint8Array): Cmd