nanobot-voice-interface/CARD_RUNTIME.md
kacper 4dfb7ca3cc
Some checks failed
CI / Backend Checks (push) Failing after 36s
CI / Frontend Checks (push) Failing after 40s
feat: unify card runtime and event-driven web ui
2026-04-06 15:42:53 -04:00

3.5 KiB

Card Runtime

The app shell is responsible for layout, navigation, sessions, feed ordering, and workbench placement. Cards are responsible for their own UI and behavior through a small dynamic runtime contract.

Source Of Truth

  • Live card templates live under ~/.nanobot/cards/templates.
  • Repo examples under examples/cards/templates are mirrors for development/reference.
  • New cards must be added as manifest.json + template.html + card.js.
  • template.html is markup and styles only. Do not put executable <script> tags in it.

Module Contract

Each template must export:

export function mount({ root, state, host }) {
  // render and wire up DOM
  return {
    update?.({ root, item, state, host }) {},
    destroy?.() {},
  };
}

root

  • The card root element already contains the rendered template.html.

state

  • The current template_state object for this card/workbench item.

host

  • The runtime host API described below.

Lifecycle

  • mount() runs once when the card is attached.
  • update() runs when the card item or template_state changes.
  • destroy() must clean up timers, listeners, observers, and in-flight UI handlers.

Cards must not rely on:

  • document.currentScript
  • global window.__nanobot* helpers
  • inline script re-execution

Host API

State

  • host.getState()
  • host.replaceState(nextState)
  • host.patchState(patch)

Card context

  • host.setLiveContent(snapshot)
  • host.getLiveContent()
  • host.setSelection(selection)
  • host.getSelection()
  • host.clearSelection()

Refresh

  • host.setRefreshHandler(handler)
  • host.runRefresh()
  • host.requestFeedRefresh()

Tools

  • host.callTool(name, args?)
  • host.startToolCall(name, args?)
  • host.getToolJob(jobId)
  • host.callToolAsync(name, args?, options?)
  • host.listTools()

Utilities

  • host.renderMarkdown(markdown, { inline? })
  • host.copyText(text)
  • host.getThemeName()
  • host.getThemeValue("--theme-card-neutral-text")

Theme Tokens

Cards should inherit theme from shared CSS variables instead of hardcoding app-level colors.

Use these families first:

  • --theme-card-neutral-* for standard data cards
  • --theme-card-warm-* for timeline/planning cards
  • --theme-card-success-* for inbox/positive cards
  • --card-* for feed shell cards
  • --helper-card-* for helper cards
  • --theme-text*, --theme-border*, --theme-accent*, --theme-status-* for generic UI

Examples:

<div style="background:var(--theme-card-neutral-bg); color:var(--theme-card-neutral-text); border:1px solid var(--theme-card-neutral-border)">
statusEl.style.color = "var(--theme-status-live)";
const accent = host.getThemeValue("--theme-accent");

Rules

  • Persist all card-local durable state through replaceState() / patchState().
  • Use setLiveContent() for transient model-readable card context.
  • Register at most one refresh handler with setRefreshHandler().
  • Always clear timers/intervals in destroy().
  • Prefer standard DOM APIs and small helper functions over framework-specific assumptions.
  • Prefer shared theme tokens over hardcoded shell/surface colors.

Validation

Run:

python3 scripts/check_card_runtime.py
node scripts/check_card_runtime_fixture.mjs
python3 scripts/sync_card_templates.py --check

The runtime check enforces:

  • manifest.json, template.html, and card.js exist
  • template.html has no inline script
  • card.js parses cleanly
  • no legacy runtime globals remain
  • the lifecycle fixture contract can mount, update, and destroy cleanly