Skip to Content
Card SDKAPI referenceCard chrome & dialogs

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 it

The 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) // clear

A 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) // clear

Overview 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') // clear

needs-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(): boolean

chrome 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.

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.

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.