Surfaces ↓
Webapp
client.webapp Daemon -> webapp replies and events for the webapp-management surface. ListReply,
CurrentReply, ActiveReply, and IconReply answer the matching ClientToBridgeWebappMsg
request; WebappError replaces the reply on failure. WebappInstalled is a live event
broadcast to every connected webapp whenever an installed-webapp transfer completes.
Requests
you ask, the daemon answers; await the tagged result and check .ok
list(): Promise<TypedRequestResult<WebappListReply, never>>
push form: onListReply (subscribe instead of awaiting)
const res = await client.webapp.list();
if (res.ok) {
console.log(res.response.webapps);
} type Result =
| { ok: true; response: WebappListReply }
| { ok: false; kind: 'domain'; error: never }
| { ok: false; kind: 'protocol'; error: WireError }; current(): Promise<TypedRequestResult<WebappCurrentReply, never>>
push form: onCurrentReply (subscribe instead of awaiting)
Webapp asks which webapp (if any) is currently active in the kiosk.
const res = await client.webapp.current();
if (res.ok) {
console.log(res.response.id);
} type Result =
| { ok: true; response: WebappCurrentReply }
| { ok: false; kind: 'domain'; error: never }
| { ok: false; kind: 'protocol'; error: WireError }; activate(WebappActivate): Promise<TypedRequestResult<WebappActiveReply, WebappError>>
push form: onActiveReply (subscribe instead of awaiting)
Payload for the activate request: switch the kiosk to the given webapp. The kiosk runs
exactly one webapp at a time, so the daemon navigates it away from whatever was previously
active.
const res = await client.webapp.activate({ id: '...' });
if (res.ok) {
console.log(res.response.id);
} else {
console.warn(res.kind, res.error);
} type Result =
| { ok: true; response: WebappActiveReply }
| { ok: false; kind: 'domain'; error: WebappError }
| { ok: false; kind: 'protocol'; error: WireError }; icon(WebappIcon): Promise<TypedRequestResult<WebappIconReply, WebappError>>
push form: onIconReply (subscribe instead of awaiting)
Icon byte fetch. Daemon reads the icon declared by the manifest from the bundle directory and returns the bytes + mime.
const res = await client.webapp.icon({ id: '...' });
if (res.ok) {
console.log(res.response.bytes);
} else {
console.warn(res.kind, res.error);
} type Result =
| { ok: true; response: WebappIconReply }
| { ok: false; kind: 'domain'; error: WebappError }
| { ok: false; kind: 'protocol'; error: WireError }; Events
the daemon pushes these unprompted; subscribing returns an unsubscribe function
onActiveChanged(handler: (WebappActiveChanged) => void): () => void
const off = client.webapp.onActiveChanged((webappActiveChanged) => {
console.log(webappActiveChanged.id);
});
// call off() to unsubscribe onWebappInstalled(handler: (WebappInfo) => void): () => void
event: a webapp install completed successfully
const off = client.webapp.onWebappInstalled((webappInfo) => {
console.log(webappInfo.id);
});
// call off() to unsubscribe onWebappUninstalled(handler: (WebappUninstalled) => void): () => void
const off = client.webapp.onWebappUninstalled((webappUninstalled) => {
console.log(webappUninstalled.name);
});
// call off() to unsubscribe Types
shapes referenced above, as the SDK types them
Event payload for an active-webapp change (any initiator). Distinct from
WebappActive (a request response) so it carries the new app's declared
art profile; the companion reads art directly to size its pushes.
type WebappActiveChanged = {
id?: string;
name?: string;
art?: ArtProfile;
}; type WebappInfo = {
id: string;
name: string;
source: WebappSource;
role: WebappRole;
version: string;
description?: string;
iconAvailable: boolean;
iconMime?: string;
icon?: number[]; // icon bytes, inlined on the gateway list so the companion never round-trips a separate fetch per app. omitted on the on-device client list.
config: ConfigField[];
permissions: string[];
voiceGrammar?: string; // Plain-English description of the voice intents the webapp wants WEBAPP_INTENT routing for. Companion-side NLU folds this into the "currently active extensions" section of the system prompt at inference, which is what makes WEBAPP_INTENT emission context-aware. `None` opts the webapp out of voice integration.
art?: ArtProfile; // Declared art render sizes; the companion warms exactly these. `None` means the canonical `{248, 96}` default applies.
}; Broadcast when an installed webapp is removed; carries the removed webapp's name.
type WebappUninstalled = {
name: string;
}; type WebappListReply = {
webapps: WebappInfo[]; // Excludes `Launcher`-role bundles used as alternate home screens.
}; type WebappCurrentReply = {
id?: string; // `None` when no webapp is currently active in the kiosk.
name?: string;
}; Payload for the activate request: switch the kiosk to the given webapp. The kiosk runs
exactly one webapp at a time, so the daemon navigates it away from whatever was previously
active.
type WebappActivate = {
id: string; // Id of an installed webapp, from `webapp.list`.
}; type WebappActiveReply = {
id?: string; // Id of the webapp that was just activated.
name?: string;
}; Domain errors emitted by any webapp surface (gateway- or client-side). Single catalog: both protocols speak the same variant set.
type WebappError =
| 'webappNotFound' // No installed webapp matches this id (uninstall / activate / icon / config target).
| 'cannotUninstallBuiltin' // Built-in webapps cannot be uninstalled.
| 'idReserved' // Install rejected: the manifest's id is in the reserved-uuid set (stock, hub, launcher, etc).
| 'extractedTooLarge' // Extracted bundle exceeds the 1 GiB disk-protection cap.
| 'zipMalformed' // Zip extraction failed: corrupt archive, unsafe entry names, etc.
| 'missingIndexHtml' // Bundle has no index.html at its root.
| 'invalidManifest' // manifest.json missing, unparseable, or failed schema validation.
| 'iconNotAvailable' // The webapp's manifest doesn't declare an icon (or the icon file is missing on disk).
| 'unknownConfigKey' // Config key is not declared in the webapp's manifest schema.
| 'invalidConfigValue' // Value failed schema validation (out of range, regex mismatch, not in enum).
| 'internal' // Catch-all for genuinely-unexpected failures (io errors, daemon-side bugs). Reason is human-readable; not a stable wire contract.; Icon byte fetch. Daemon reads the icon declared by the manifest from the bundle directory and returns the bytes + mime.
type WebappIcon = {
id: string;
}; type WebappIconReply = {
bytes: number[];
mime?: string;
}; Art render sizes a webapp declares so the companion warms exactly the
pixels it renders: hero (now-playing / detail views) and thumb (queue /
grid). Omitted in a manifest falls back to the canonical {248, 96},
which is also the stock webapp's profile.
type ArtProfile = {
heroPx: number;
thumbPx: number;
}; type WebappSource = 'builtin' | 'installed'; A webapp's launcher visibility. Standard shows up in user-facing
listings (the hub grid, etc); Launcher is itself a launcher and is
hidden from those listings. The daemon filters Launcher bundles
out of client.webapp.list; the gateway list keeps everything.
type WebappRole = 'standard' | 'launcher'; One declared user-tunable setting. Adjacent-tagged on the wire to
stay typeshare-compatible: {"type":"string","data":{"key":"zip",...}}.
type ConfigField =
| { type: 'string'; data: StringField }
| { type: 'number'; data: NumberField }
| { type: 'boolean'; data: BoolField }
| { type: 'enum'; data: EnumField }
| { type: 'secret'; data: StringField } // String semantics, masked in companion UI. No actual secure storage.; type StringField = {
key: string;
label: string;
pattern?: string;
minLength?: number;
maxLength?: number;
default?: string;
}; type NumberField = {
key: string;
label: string;
min?: number;
max?: number;
step?: number;
default?: number;
}; type BoolField = {
key: string;
label: string;
default?: boolean;
}; type EnumField = {
key: string;
label: string;
choices: string[];
default?: string;
};