GemmaPoddocs
Guides

Headless mode

Drop the Preact widget, keep the runtime.

"Headless" means: load the runtime, the WebRTC transport, the WebGPU fallback, the DARTC envelope handling, the chat API, the state store, but skip the Preact chat widget. You render the UI.

The contract

const { runtime, destroy } = await GemmaPod.mountPod(null, config, {
  ui: "none",
  fallbackUi: "default",        // or "none" if you also handle prepare UX
  fallbackMountParent: yourEl,   // when fallbackUi is "default"
});
  • el is null because there's no chat widget to mount.
  • ui: "none" skips Preact entirely.
  • fallbackUi: "default" keeps the shim's WebGPU prepare panel — just tell it where to render via fallbackMountParent.
  • fallbackUi: "none" if you also want to roll your own prepare UI (rare; see attachBrowserFallbackPrepare for the lower-level hook).

Why two IIFEs

The runtime-only IIFE (gemmapod-runtime.iife.js) doesn't bundle Preact or the chat widget, saving ~22 KB before gzip. For headless embeds the runtime-only build is the right choice; the full build is for packed .html blobs that need their own UI.

<script src="https://cdn.jsdelivr.net/npm/@gemmapod/embed@0.1.0/dist/gemmapod-runtime.iife.js"></script>

Both IIFEs assign to window.GemmaPod and expose the same mountPod / create / mount / attachBrowserFallbackPrepare / quickTransportStatus / mapDartcUiEventToAgUi. Only the full build exposes boot, initCore, GemmaPodCore, wasmInit (the signing helpers).

What the runtime gives you

runtime.events.on("transport.ready", ...);
runtime.events.on("transport.fallback", ...);
runtime.events.on("ui.event", ({ event }) => { /* DartcUiEvent */ });
runtime.events.on("state.changed", ({ state }) => { /* RFC 6902-applied snapshot */ });
runtime.events.on("chat.history", ({ messages }) => { /* ChatMessage[] */ });
runtime.events.on("a2a.card", ({ card }) => { /* A2AAgentCard */ });

await runtime.chat.send("hello");
for await (const chunk of runtime.chat.stream("hello")) { /* … */ }

runtime.state.get();        // current snapshot
runtime.state.subscribe(fn); // bypass the event bus for direct subscriptions

runtime.capabilities.has("storage.local");
runtime.capabilities.list();

Reference: GemmaPodRuntime.

Patterns

Stream assistant text into your own transcript

runtime.events.on("ui.event", ({ event }) => {
  if (event.type === "TEXT_MESSAGE_CONTENT") {
    appendAssistantDelta(event.delta);
  }
});

for await (const _chunk of runtime.chat.stream(userInput)) {
  // drain — text rendered above
}

Project shared state

runtime.events.on("state.changed", ({ state }) => renderCart(state));

STATE_SNAPSHOT and STATE_DELTA are auto-applied. You just subscribe.

Bridge to AG-UI

runtime.events.on("ui.event", ({ event }) => {
  copilot.dispatch(GemmaPod.mapDartcUiEventToAgUi(event));
});

Full AG-UI bridge guide →

What's next