Card chrome & dialogs
The card’s header, overview tile and dialogs are drawn by the app, not by your page — so they look native, work while your card is suspended, and show on the phone. You set their content.
Call them as often as you like. The app throttles these updates (title 30 a minute; status,
badge and overview 120 a minute each; attention once every 10 seconds), and the SDK absorbs that for
you: for setTitle, setStatus, setBadge, setOverview and attention it sends one call at a
time per method, folds calls made meanwhile into one with the latest value, and retries the latest
value when the app says RATE_LIMITED. They never throw for rate — updating on every render is
fine. An update that changes nothing costs nothing.
Title
await card.setTitle('Checkout tests') // null clears itThe name shown in the header, the sidebar and the Squad card. If the user renamed the card, the
user’s name wins; without either, the manifest’s displayName shows. ≤ 120 characters; control,
bidi and invisible characters are stripped.
Status
await card.setStatus('Running tests…', { busy: true }) // spinner
await card.setStatus('3 failed', { tone: 'danger' })
await card.setStatus(null) // clearA chip in the header, ≤ 80 characters. Tones: default, accent, success, warning, danger.
Badge
await card.setBadge(3) // a count
await card.setBadge('NEW', { tone: 'accent' }) // or a short text, ≤ 12 characters
await card.setBadge(null) // clearOverview tile
When the user zooms out past the overview threshold, every card shows a tile with its main fact instead of its body. Yours shows what you set here — and keeps showing it while the card is suspended, and on the phone. The app remembers it.
await card.setOverview({
primary: '3 failing', // big line, ≤ 160
secondary: 'checkout, auth', // small line, ≤ 160
progress: 0.8, // 0..1, a progress bar; null hides it
tone: 'danger',
icon: 'bug-ant'
})Keep it current: it is the only thing of your card most people see on a busy canvas.
Icons the app can draw (heroicons, outline): academic-cap, arrow-path, beaker, bell,
bolt, book-open, bug-ant, calendar, camera, chart-bar, chart-pie,
chat-bubble-left-right, check-circle, clock, cloud, code-bracket, command-line,
cpu-chip, cube, currency-dollar, document-text, exclamation-triangle, film, fire,
flag, folder, globe-alt, heart, inbox, key, light-bulb, link, list-bullet, map,
megaphone, moon, musical-note, newspaper, paper-airplane, pause, photo, play,
puzzle-piece, rocket-launch, server, shield-check, signal, sparkles, star, stop,
sun, table-cells, tag, trash, trophy, users, wrench-screwdriver (the list is
HOST_ICON_NAMES in the SDK). Inside your own page, draw any icon you like.
Attention
await card.attention('needs-input', 'Pick a branch to deploy')
await card.attention('info') // a softer pulse
await card.attention('none') // clearneeds-input makes the card pulse like an agent waiting for the user and lists it in the
Inbox. At most once every 10 seconds. There is no sound and no OS notification in version 1.
What is showing now. The app keeps a card’s title, status, badge, overview and attention
across reloads, suspension and updates. card.context.chrome tells your page what it currently
holds, so a card that raised attention before a reload can clear a stale pulse:
const attention = card.context.chrome?.attention
if (attention && !stillNeedsTheUser()) await card.attention('none')
declare function stillNeedsTheUser(): booleanchrome is absent on older app versions — treat that as “unknown”.
Resize
const applied = await card.requestResize({ w: 640, h: 480 })
render(applied.w, applied.h)Clamped to the manifest’s minSize/maxSize; resolves with the size actually applied. It goes
into the canvas undo history like a resize by hand. At most every 500 ms. The user can always resize
too — listen with card.lifecycle.onResized.
Open a link
const opened = await card.openLink('https://github.com/acme/app/issues/42')The app shows the full address in its own window-wide dialog; the link opens in the user’s browser
only after Open link. https:// only, and only while the card is visible (NOT_VISIBLE otherwise).
Resolves false if the user declined. A card cannot navigate itself or open windows.
Fly to another card
await card.focusCard(cardId)Moves the camera to another card of the same workspace — for “show me the failing agent” buttons. Only while your card is visible, at most every 5 seconds.
Spawn a card
Needs canvas.spawn.
// A note next to this card, connected with an arrow from this card.
const noteId = await card.spawn('note', { title: 'Test report' })
await card.ports.send(noteId, 'summary', '# Report\n\nAll green.')
// Another copy of this card, with data for its first start.
await card.spawn('self', { init: { suite: 'e2e' }, connect: false })Kinds: self (another card of your package, which gets init as card.spawnInit on its first
start), note, todo, kanban, sticky. The new card is placed next to yours and connected with
an arrow from your card unless connect: false. At most 4 per card and 4 a minute.
Card info
const info = await card.getInfo() // CardInstanceInfo, fresh from the app
render(info.instanceId, info.packageId, info.commit ?? 'dev folder', info.size)Toasts
await card.ui.toast('Copied', { tone: 'success', durationMs: 2000 })A small toast inside the card’s box, ≤ 280 characters, 1–15 seconds, at most one every 2 seconds.
Confirm dialog
alert, confirm and prompt do nothing in a card’s sandbox. Ask the app instead:
const ok = await card.ui.confirm({
title: 'Delete all saved runs?',
message: 'This removes 42 runs from this card. It cannot be undone.',
confirmLabel: 'Delete',
cancelLabel: 'Keep',
tone: 'danger'
})
if (ok) await card.storage.clear()The app shows this as its own dialog over the whole window, not inside your card: a fixed
heading saying your card asks for confirmation, your title and message as a quote, your
confirmLabel (replaced with the app’s “Confirm” if it reads like cancel/no/deny, names NeuroSquad
or contains control characters) and the app’s own Cancel. Only while the card is visible, at most
10 a minute. Dialogs from several cards queue.
The card’s menu
Add up to 12 items to the card’s ⋯ menu, after the app’s own (Settings…, Reload, About…).
await card.ui.setMenu([
{ id: 'rerun', label: 'Run again', icon: 'arrow-path', onSelect: () => void rerun() },
{ id: 'export', label: 'Copy report', icon: 'document-text', onSelect: () => void copyReport() },
{ id: 'reset', label: 'Reset', tone: 'danger', disabled: true }
])
// Or handle every choice in one place:
card.ui.onMenu((id) => card.log.info('menu', id))
declare function rerun(): Promise<void>
declare function copyReport(): Promise<void>Each call replaces the previous items (at most 30 calls a minute). onSelect stays in your card —
it is not sent to the app.
Labels ≤ 60 characters; icon is one of the host icons.
Everything on this page except dialogs and links works while the card is not on screen. Dialogs,
links, camera focus and permission requests need the user to be looking: they fail with
NOT_VISIBLE otherwise.