widgetKit.ts — that turns
structured content into polished widget HTML: correct theming, brand accents
with contrast repair, your integration’s mark on every card, and the whole
bridge runtime implemented for you. It’s the
exact system the Integration Studio uses, which
is why its output looks native in the notch.
Getting it: every Studio-generated folder ships with its own copy, with your
logo baked in. Building by hand, copy widgetKit.ts from a generated folder —
or src/widget/kit.ts from the SDK — next to your server.ts.
Composed cards: renderWidget
Build blocks, render, return:
renderWidget returns { html, height, label } — and the height is real:
every component reports its measured height, so the declared estimate lands
within a few percent of the rendered card.
The components
Each returns aBlock for the blocks array:
Tones are
neutral, good, bad, accent. Helpers: esc() (escape any
interpolated string — always), clip(value, max), and textEms() for CJK-aware
width estimates.
Accent and theme
The surface finish is drawn for you. Every kit card carries a liquid-glass
rim — a 1px inset hairline, brighter along the top, on the same
--k-radius arc
the host clips at, so it can neither gap nor get shaved. Declare an accent and
the card also gets a soft brand wash out of the top-left corner. The integration
mark stays the bare logo; the kit never puts a tile or colored backdrop behind
it, and neither should you.
Don’t add your own outer border, glow, or logo tile. The finish that belongs
on the card is already there.
Bespoke layouts: renderCustom
When the composed blocks can’t express your card, own the surface:
renderWidget:
- You own the padding and the declared
height. - It throws if the document exceeds the byte cap instead of degrading — an error during your test run beats a card the host silently drops.
markHtml(), the kit floats the mark over the top-left
anyway. No card ships unbranded.
Images and fonts
The sandbox has no network, so fetch assets in the handler and inline them:null —
design the card to survive a missing image.
Editable fields and links
The kit’s runtime wires interactivity declaratively — you write no bridge code:- Fields.
vField({ key: "body", label: "Message" })renders an input bound to the tool argumentbody. In confirmation cards it hydrates from the pending call’sargsand stages the user’s edits. In custom markup, any element withdata-voiceos-key="body"gets the same treatment. - Links.
vListrows withhref, elements withdata-k-link, and plain<a href="https://…">anchors all route through the bridge to the user’s browser. https only.
Staying under the caps
The HTML budget is 128 KB.renderWidget degrades gracefully when a card
exceeds it — content images are stripped first (never your mark), then trailing
blocks — so a card never silently vanishes. Confirmation-mode cards
(mode: "confirm") also reserve the bottom-right corner for VoiceOS’s floating
confirm button.
The design bar
The same rules the Studio’s design critic scores against — worth keeping even when you’re hand-rolling:- At most 3 type sizes;
tabular-numson every number. - Truncate, never wrap — and
text-overflow: ellipsisneedsoverflow: hiddento work. - Hairlines and spacing over nested boxes; spend one accent deliberately.
- Fit 420px without scrolling — show 4–5 rows and a “+N more”, not a scrollbar.
- Everything renders offline, on first paint, in both themes.
Widget confirmations
The same kit, one extra rule: VoiceOS owns the confirm button.

