Appearance
Rich UI surfaces and transport
Use a rich UI surface for a scene library, character sheet or encounter view. The extension registers a mount function; a browser or native adapter supplies its typed rendering target. The SDK requires no React, DOM or authentication library. Existing semantic panels remain supported.
First-party features use this same registration and mount API. Keep routing, credentials and application composition at the edge instead of giving a leaf access to a private application registry.
Define a platform and register a surface
UiPlatform<TTarget> names the adapter protocol and validates its target at runtime. The platform's target can expose the rendering operations its adapter supports. The SDK does not prescribe a component framework or a UI description language.
This complete headless example uses a text target to make the lifecycle visible:
ts
import { createExtensionHost } from '@talerook/engine';
import type { Extension, UiPlatform } from '@talerook/sdk';
interface NotesTarget {
replace(text: string): void;
}
const notesPlatform: UiPlatform<NotesTarget> = {
id: 'example.text-ui',
accepts(value): value is NotesTarget {
return (
value !== null &&
typeof value === 'object' &&
typeof (value as NotesTarget).replace === 'function'
);
},
};
const notes: Extension = {
manifest: {
id: 'example.notes',
version: '0.1.0',
apiVersion: 1,
capabilities: ['ui.surfaces'],
},
activate(extension) {
extension.ui.registerSurface({
id: 'example.notes/editor',
title: 'Notes',
platform: notesPlatform,
mount(target, context) {
if (typeof context.input !== 'string') {
throw new TypeError('Notes require text.');
}
target.replace(context.input);
// Own additional listeners/subscriptions with context.resources.add().
return () => target.replace('');
},
});
},
};
let displayed = '';
const target: NotesTarget = {
replace: (text) => {
displayed = text;
},
};
const host = createExtensionHost();
try {
await host.load(notes);
const mount = host.mountSurface('example.notes/editor', notesPlatform, target, {
input: 'Meet at the north gate.',
});
try {
await mount.ready;
console.log(displayed); // Meet at the north gate.
} finally {
await mount.dispose();
}
console.log(displayed); // Empty after cleanup.
await host.unload('example.notes');
} finally {
await host.dispose();
}Registration requires the requested and granted ui.surfaces capability. Contribution IDs belong to their extension namespace. Enumerate host.contributions('surfaces') for { id, title, platformId } descriptors; mount through host.mountSurface, which checks the platform and target.
A real adapter owns its framework root, focus management and accessible controls. Return the root's cleanup from mount, and register other resources with context.resources.add. Multiple mounts of one surface have independent instance IDs, signals and resources.
Mount lifetime and errors
host.mountSurface(id, platform, target, options?) returns a handle immediately:
| Member | Meaning |
|---|---|
id | Unique mount instance within this host |
signal | Aborts when this mount's lifetime ends |
ready | Resolves after mounting; rejects on failure or pending cancellation |
dispose() | Idempotently aborts and awaits owned work and cleanup |
Options accept input?: JsonValue, transport?: SubjectHttpTransport and signal?: AbortSignal. The mount context provides that input, its own signal and resources, optional scoped transport, and execute(commandId, payload).
Closing one mount leaves its other instances running. Unregistering the surface, unloading its extension or disposing the host closes all affected mounts. Pending mount callbacks remain owned: a cleanup returned after cancellation is still released and awaited. Resources clean up in reverse acquisition order, including resources acquired while a pending callback is finishing after abort.
Observe ready and await dispose() even on a failed mount. Cleanup failures reject disposal after remaining cleanup runs; the host also reports errors through onError(error, extensionId). Async adapters must observe cancellation and settle their work. The host cannot forcibly stop arbitrary JavaScript.
Inject subject-scoped HTTP
SubjectHttpTransport is an application-edge port with a readonly subject: { accountId }, a lifetime signal, and three operations:
| Operation | Request | Result |
|---|---|---|
json(request, signal?) | { path, method, body? } | JsonValue |
readBytes(request, signal?) | { path } | Uint8Array |
writeBytes(request, signal?) | { path, bytes, mediaType, idempotencyKey } | JsonValue |
JSON methods are GET, POST, PUT, PATCH or DELETE. Paths are application-relative, such as /api/campaigns. The leaf validates the returned domain data; the port does not assert an arbitrary response type is correct. The byte-write retry key identifies an operation, not a session credential.
The application supplies the transport in the mount options. Inside a surface:
ts
async mount(target, context) {
const transport = context.transport;
if (!transport) throw new Error('This surface requires an HTTP adapter.');
const campaigns = await transport.json({ path: '/api/campaigns', method: 'GET' });
if (context.signal.aborted) return;
// Validate the response, then render through this platform's typed target.
// Return the target/root cleanup as in the complete example above.
}The host copies and freezes the expected account subject and captures the injected methods. It combines mount and caller cancellation, rejects stale completion, and keeps pending requests owned through disposal. A caller's request signal cancels that operation; it does not cancel another request or mount. Aborting the injected transport's lifetime closes the mount. Changing the source transport's subject or lifetime is rejected; create a new transport and mount for a new account or audience.
The edge owns credentials, allowed origins and routes, expected-account header policy, and truthful HTTP errors. It must bind every operation to the captured account and reject an account change. Server session, membership, control and entitlement checks remain authoritative. Leaves receive no reusable session token, mutable global auth client or global fetch function through this port.
Cancellation and durable writes
Cancellation stops local publication and asks the adapter to abort work. A server write already admitted may still commit. A rejected or cancelled local promise therefore does not prove the server rolled back. Retain the operation's retry identity and body where required, then refetch authoritative state before deciding to retry. This applies to both HTTP writes and context.execute commands that reach the host's authority adapter.