# Carduri (/ro/docs/agents/cards)

Carduri bogate și tipizate pe care agentul le arată în loc de un zid de text — schema cardului, tool-urile agentului, endpoint-urile de date /v1/cards, limite și facturare.

Un **card** este o bucată mică de interfață, tipizată, pe care agentul o pune în conversație: un tabel comparativ, un grafic, o prognoză meteo, o listă de locuri, previzualizări de linkuri, o alegere de făcut. Cardurile sunt definite o singură dată, în catalogul `@codai/cards` (Zod 4), și sunt desenate nativ de fiecare suprafață codai — desktop, consola web, telefon și ceas. Suprafețele care nu pot desena un card (Telegram, email, citirea cu voce, istoricul văzut de model) folosesc varianta text a cardului, așa că nu se pierde nimic.

## Drumul unui card [#drumul-unui-card]

1. Agentul apelează unul dintre [tool-urile de carduri](#tool-urile-agentului) (`show_card` sau o scurtătură tipizată).
2. Aplicația validează cardul după schemă, completează variantele text și îl desenează. Un card invalid este refuzat cu o eroare precisă (`blocks.0.rows: …`), ca modelul să-și poată repara apelul.
3. Modelul primește înapoi doar un rezumat scurt (`fallback.text`), niciodată cardul întreg, ca să nu repete conținutul în răspuns.

Ca rezervă tolerantă, un model care nu poate apela tool-uri poate scrie același JSON într-un bloc de cod cu limbajul `codai-card`; aplicațiile îl desenează la fel ca pe un card emis printr-un tool.

## Carduri live [#carduri-live]

Un card cu `live: { key }` poate fi actualizat după ce a fost afișat. Actualizarea este un eveniment de sesiune `card_update` cu `{ cardId, patch }`: `cardId` este `live.key` al cardului, iar `patch` este un JSON Merge Patch (RFC 7396) peste card — obiectele se combină, `null` șterge un câmp, iar listele precum `blocks` și `actions` se înlocuiesc întregi. Cardul rezultat este validat din nou (inclusiv limita de mărime) și variantele text sunt recalculate; o actualizare care schimbă cheia sau produce un card invalid este ignorată și cardul anterior rămâne.

Fiecare suprafață redesenează cardul acolo unde a apărut prima dată — fără mesaj nou, fără pâlpâire — iar cititoarele de ecran aud o singură linie scurtă de stare la fiecare schimbare. Ultimul card de pe ceas urmează actualizarea.

```json
{ "kind": "card_update", "payload": { "cardId": "job:12", "patch": { "subtitle": "Job în fundal · Gata · 42s", "actions": [] } } }
```

### Cardul live al rulării [#cardul-live-al-rulării]

Când agentul lucrează mai mult (trei sau mai mulți pași cu tool-uri, sau când așteaptă aprobarea ta) și pentru fiecare job în fundal pe care îl pornește, desktopul și telefonul arată un card live al rulării: pașii recenți, timpul scurs, costul de până acum și, cât timp așteaptă, aprobarea însăși. **Pauză**, **Reia** și **Oprește** acționează direct asupra rulării (nu trec niciodată prin model); **Permite / Refuză** răspunde la permisiunea în așteptare. În consola web și pe pagina sesiunii de pe alt dispozitiv, Oprește, Permite și Refuză sunt trimise ca acțiuni de control ale sesiunii; pauza și reluarea cer dispozitivul care rulează agentul.

## Plicul cardului [#plicul-cardului]

Schema JSON completă este publicată în `packages/cards/schemas/cards.schema.json` (generată din catalogul Zod; un test de drift le ține sincronizate, iar tipurile Kotlin folosite pe telefon și ceas sunt generate din aceeași sursă).

| Câmp | Tip | Semnificație |
| --- | --- | --- |
| `v` | `1` | Versiunea schemei (implicit `1`). |
| `id` | string | Id stabil opțional (litere, cifre, `_ . : -`, maximum 64 de caractere). |
| `title`, `subtitle` | string | Antetul afișat deasupra blocurilor. |
| `blocks` | listă, 1–24 | Conținutul, în ordine (vezi tabelul de mai jos). |
| `actions` | listă, maximum 6 | Butoane sub card (vezi [acțiuni](#acțiuni)). |
| `fallback` | `{ text, speak? }` | Variantele markdown și vorbită; completate din blocuri când lipsesc. |
| `a11y` | `{ label?, summary? }` | Numele accesibil și rezumatul; completate automat când lipsesc. |
| `provenance` | `{ source, url?, asOf?, licence? }` | De unde vin datele; afișat ca insignă. |
| `live` | `{ key, ttlS? }` | Card live: actualizările ulterioare cu aceeași cheie îl modifică pe loc. |
| `display` | `inline` \| `expanded` | Mărimea inițială (implicit `inline`). |

### Tipuri de blocuri [#tipuri-de-blocuri]

| `type` | Ce arată | Câmpuri principale |
| --- | --- | --- |
| `text` | Paragraf markdown | `md` |
| `callout` | Notă evidențiată | `tone` (`neutral`, `info`, `success`, `warning`, `danger`), `title`, `md` |
| `metric` | Un număr cu tendință | `label`, `value`, `unit`, `delta`, `higherIsBetter`, `sparkline` |
| `kv` | Listă cheie–valoare | `items[{ k, v }]` (maximum 40) |
| `table` | Tabel sortabil | `columns[{ key, label, format }]` (maximum 12), `rows` (maximum 200), `highlightRow` |
| `chart` | Grafic linie, bare, arie, plăcintă, puncte sau sparkline | `kind`, `x`, `series` (maximum 8), `data` (maximum 500), `summary` obligatoriu |
| `map` | Marcaje și un traseu opțional | `center`, `zoom`, `markers` (maximum 50), `route` (polilinie codată), `staticImage` |
| `places` | Locuri cu rating și program | `items[{ id, name, address, lat, lng, rating, openNow, … }]` (maximum 20) |
| `products` | Carusel de produse | `items[{ id, title, price, url, merchant, sponsored }]` (maximum 12) |
| `links` | Previzualizări de linkuri sau surse numerotate | `variant` (`preview` \| `sources`), `items[{ url, title, description, image }]` |
| `weather` | Vremea acum, pe ore și pe zile | `location`, `current`, `hourly` (maximum 48), `daily` (maximum 14) |
| `gallery` | Imagini sau clipuri cu lightbox | `items[{ url, alt, caption }]` (maximum 24, `alt` obligatoriu) |
| `choice` | Opțiuni ca butoane | `id`, `prompt`, `mode` (`single`, `multi`, `rank`), `options` (2–12) |
| `approval` | Confirmarea unei acțiuni riscante | `id`, `action`, `risk`, `diff`, `undoSeconds` |
| `steps` | Listă de verificare sau pași ghidați (rețete, configurări) | `items[{ title, md, done, durationS }]`, `guided` |
| `diagram` | Diagramă Mermaid | `source`, `summary` |
| `math` | Formulă KaTeX | `tex`, `display` |
| `code` | Cod evidențiat | `lang`, `code`, `path` |
| `file` | Un fișier cu previzualizare | `path`, `size`, `mime`, `url`, `preview` |
| `file_tree` | Arbore de fișiere cu starea modificărilor | `entries[{ path, kind, status }]` (maximum 300) |
| `diff` | Diff unificat | `unified`, `path` |
| `progress` | Progresul unei lucrări | `label`, `state`, `value` (0–1), `steps`, `costMicroUsd` |
| `timeline` | Evenimente datate | `items[{ t, title, md, tone }]` |
| `countdown` | Timpul rămas până la un moment | `label`, `target` |
| `qr` | Cod QR | `data`, `label` |
| `contact` | Date de contact | `name`, `org`, `phone`, `email`, `url`, `address` |
| `visual` | HTML sau SVG scris de agent (diagramă proprie, explicație interactivă), într-un sandbox | `kind` (`html` \| `svg`), `source` (≤ 64 KB), `summary` obligatoriu (≤ 500), `height` (80–1200), `title` |

Orice URL dintr-un card trebuie să fie `https://`; imaginile pot fi și URI-uri `data:image/*`. Un ecran mic (ceasul) declară blocurile pe care le poate desena; restul sunt înlocuite cu un bloc text construit din varianta lor markdown, iar cardurile lungi sunt tăiate la câteva blocuri plus un rezumat vorbit.

### Acțiuni [#acțiuni]

| `kind` | Câmp obligatoriu | Efect |
| --- | --- | --- |
| `prompt` | `prompt` | Trimite textul ca următorul mesaj al utilizatorului (click pentru continuare). |
| `tool` | `tool` (+ `args`) | Apelează un tool; se aplică lista de tool-uri permise a aplicației. |
| `open_url` | `url` | Deschide linkul în browser. |
| `copy` | `value` | Copiază valoarea în clipboard. |
| `handoff` | `device` (`phone`, `desktop`, `web`) | Continuă conversația pe alt dispozitiv. |

Id-urile acțiunilor trebuie să fie unice într-un card; `style` este `primary`, `secondary` sau `danger`, iar `confirm` cere confirmare înainte de rulare.

### Variante text, accesibilitate și proveniență [#variante-text-accesibilitate-și-proveniență]

- **Variante text**: când agentul nu trimite `fallback`, aplicația construiește `fallback.text` (markdown) și `fallback.speak` (una-două propoziții) din blocuri. Pe acestea le vede modelul și pe acestea le citesc suprafețele vocale.
- **Accesibilitate**: `a11y.label` ia implicit titlul (sau primul bloc), `a11y.summary` varianta vorbită. Graficele cer un `summary` în limbaj simplu; imaginile din galerie cer `alt`.
- **Proveniență**: cardurile construite din datele gateway-ului poartă sursa, atribuirea și momentul descărcării, afișate ca insignă pe card.

### Limita de mărime [#limita-de-mărime]

Un card are cel mult **32 KB** de JSON după completarea variantelor text. Un card mai mare este refuzat cu `card exceeds 32768 bytes`; scurtează datele sau împarte-le în mai multe carduri.

Imaginile inline `data:image/*` au un buget separat de 256 KB, iar `source` al unui bloc `visual` are bugetul lui de **64 KB**; niciunul nu intră în cei 32 KB. Un card are cel mult un bloc `visual`.

### Sandbox [#sandbox]

Un bloc `visual` rulează într-un cadru izolat cu origine opacă (`sandbox="allow-scripts"`, niciodată `allow-same-origin`): nu poate citi aplicația, cookie-urile sau stocarea, iar politica Content-Security-Policy nu permite deloc rețea (`default-src 'none'`, doar scripturi și stiluri inline, imagini și fonturi `data:`). Sursele care trimit la adrese `http(s)://` sau `//`, conțin `<base>` sau un meta refresh, ori un `svg` care nu începe cu `<svg` sunt refuzate cu o eroare pe care agentul o poate corecta. Desktopul servește documentul o singură dată din schema proprie `mcpapp:`, cu politica în antet; consola web folosește un cadru `srcdoc`; telefonul folosește un WebView securizat, fără punte către aplicație. Ceasul, modul `minimal` și suprafețele doar-text afișează `summary`.

### Hărți [#hărți]

Un bloc `map` se afișează la fel peste tot, iar cheia Google Maps nu părăsește gateway-ul:

- **Desktop și consola web**: o imagine statică a hărții din `GET /v1/cards/static-map`, descărcată cu autentificarea ta (consola trece printr-un proxy de server pe același domeniu). Pinii sunt marcați cu literele A–T, iar lista de sub hartă folosește aceleași litere. Cât se încarcă imaginea apare un spațiu gri. Fiecare hartă se descarcă o singură dată pe vizualizare (fiecare descărcare costă 0,002 USD).
- **Telefon**: o hartă Google interactivă (modul lite: pini și traseu, se derulează odată cu conversația) când aplicația a fost construită cu o cheie Maps; altfel aceeași imagine statică de la gateway.
- **Ceas**: numele primilor pini, ca text.
- **Mereu**: câte un rând pentru fiecare pin, care îl deschide în aplicația ta de hărți, plus **Deschide în Google Maps** și **Deschide în OpenStreetMap**. Textul alternativ al imaginii numește pinii.
- **Fără imagine** (offline, cotă epuizată, hărți neconfigurate): primești pinii cu coordonatele lor și o notă că imaginea hărții nu este disponibilă.

O hartă interactivă Google Maps JS pe desktop și web nu este încă activă: așteaptă o cheie JS restricționată pe referrer. Aplicațiile gazdă pot conecta una prin `renderInteractiveMap`.

## Tool-urile agentului [#tool-urile-agentului]

| Tool | La ce îl folosește agentul |
| --- | --- |
| `show_card` | Orice card: primește plicul complet. Folosit când datele sunt structurate (3+ elemente comparabile, o serie numerică, locuri, linkuri, o alegere, pași, un arbore de fișiere sau un diff). |
| `show_table` | Scurtătură pentru un singur card cu tabel (`columns`, `rows`, `caption`). |
| `show_chart` | Scurtătură pentru un card cu grafic; `summary` (concluzia) este obligatoriu. |
| `ask_choice` | Butoane de alegere (2–12 opțiuni, `mode` single / multi / rank). Nu blochează: alegerea vine ca următorul mesaj al utilizatorului. |
| `show_weather` | Card meteo pentru un loc, din `/v1/cards/weather`. |
| `find_places` | Card cu locuri din `/v1/cards/places` (facturat, vezi mai jos). |
| `show_link_previews` | Carduri de previzualizare a linkurilor din `/v1/cards/unfurl`. |
| `show_products` | Card de cumpărături pentru 1-6 pagini de produs din `/v1/cards/product`: nume, imagine, preț, preț vechi, rating, vânzător și stoc, citite din datele schema.org Product ale fiecărei pagini. |
| `show_fx` | Card cu cursuri de schimb (BNR sau BCE) din `/v1/cards/fx`. |
| `show_usage` | Contorul tău de cost și cote: cheltuiala de azi cu tendința pe 14 zile, bugetul și limitele de task-uri zilnice/lunare, planul și portofelul, din `/v1/account` și `/v1/usage`. |
| `ask_approval` | Card de aprobare înaintea unui pas riscant: acțiunea, detaliile, un diff sau fișierele afectate, riscul, Permite / Refuză. Cu `reversible`, Permite păstrează o fereastră de anulare (`undo_seconds`, implicit 10 s). Nu blochează: răspunsul vine ca următorul tău mesaj. Pe ceas e o aprobare dintr-o atingere, cu aceeași fereastră de anulare. |
| `start_timer` | Cronometru cu numărătoare inversă (`seconds`, `minutes` sau un `target` absolut, până la 7 zile): numărătoare live în conversație, o notificare permanentă cu cronometru pe telefon, o notificare „timpul a expirat” la final și ora de final pe tile-ul ceasului. |
| `show_comparison` | Tabel de comparație sortabil cu sursa fiecărui rând (o coloană cu linkuri plus sursele numerotate) și un rând recomandat. Export CSV și copiere markdown pe desktop și web; Distribuie CSV pe telefon. |
| `show_handoff` | „Continuă pe telefon” / „Continuă pe PC” / „Continuă în browser”: deschide aceeași sesiune pe celălalt dispozitiv (handoff-ul existent, cere o conversație partajată). |
| `show_share` | Cod QR (`kind: qr`), contact (`kind: contact`, cu un QR vCard, copiere și distribuire/salvare) sau locație (`kind: location`, cu pin pe hartă, un QR `geo:`, Deschide în Hărți și copiere). Generate pe dispozitiv. |
| `show_changes` | Cronologia pașilor și a fișierelor modificate de un task (plus un diff), cu un buton **Anulează modificările** care readuce fișierele prin anularea proprie a aplicației: restaurarea checkpoint-ului pe desktop (snapshot-uri) și pe telefon (jurnalul de anulare), după o confirmare. |
| `show_visual` | O vizualizare scrisă chiar de agent (`svg` sau `html`, cu `summary` obligatoriu) când niciun card tipizat nu se potrivește: o diagramă proprie sau o mică explicație interactivă. Rulează în sandboxul de mai sus, fără rețea. Desktop, consola web și telefon. |
| `show_device_status` | Bateria, stocarea, rețeaua, modelul și sistemul acestui dispozitiv (citite local), celelalte dispozitive codai cu starea online / văzut ultima dată (`/v1/devices`) și un link „găsește dispozitivul”. Bateria și stocarea altor dispozitive nu sunt partajate. |
| `show_fiscal_status` | Starea e-Factura lângă cursul BNR: documentele emise de codai către tine prin Brivio cu starea lor ANAF (`/v1/cards/efactura`) și `/v1/cards/fx`. |
| `show_transcript_highlights` | Rezumatul, deciziile, sarcinile și întrebările deschise ale unei înregistrări codai Transcripts, fiecare cu un link care pornește redarea din acel moment (`/v1/transcripts`). |
| `show_route` | Durata drumului, întârzierea din trafic și distanța între două locuri, cu traseul pe hartă (`/v1/cards/route`, facturat). |
| `show_agenda` | Doar pe telefon: următoarele zile din calendar, cu durata drumului de unde ești și o numărătoare „pleacă la” (`/v1/cards/route` pentru fiecare eveniment cu locație, cel mult 3). |
| `track_package` | Istoricul unui colet (Sameday, FAN Courier, Cargus, DPD) din `/v1/cards/track`, plus codul de ridicare easybox ca QR. |

Fiecare card cu date arată în subsol o etichetă de proveniență: sursa (cu link când are o pagină), cât de recente sunt datele („actualizat acum 2 ore”, data exactă la hover) și licența (de exemplu MET Norway, CC BY 4.0). Pe ceas apare compact `sursă · 2h`.

Cele șase tool-uri locale (de la `ask_approval` la `show_changes`) nu apelează gateway-ul: cardul este construit pe dispozitiv de un builder pur (`buildLocalCard` în `@codai/cards`, geamănul Kotlin `LocalCards`), cu etichetele în limba aplicației (engleză sau română).

Agentul este instruit să nu repete conținutul cardului în răspunsul text; cel mult o propoziție de comentariu.

### Preferință [#preferință]

Fiecare utilizator alege cât folosește agentul cardurile: **Setări → Chat → Carduri** pe desktop, **Setări → Agent → Carduri** pe telefon (ceasul urmează telefonul).

| Valoare | Ce primește agentul | Cum arată cardurile |
| --- | --- | --- |
| `auto` (implicit) | Toate tool-urile de carduri și indicațiile proactive de mai sus. | Carduri complete. |
| `minimal` | Toate tool-urile de carduri, dar e instruit să folosească tool-urile de afișare doar când ceri un card, un tabel, un grafic sau o alegere; tool-urile de date răspund în continuare la cererile de date live. | Carduri compacte; graficele și galeriile devin varianta text și tabel, cel mult 6 blocuri. |
| `off` | Niciun tool de carduri și instrucțiunea de a răspunde în text simplu. | Orice card deja existent în conversație apare ca varianta lui text. |

Preferința se citește la începutul conversației, așa că lista de tool-uri și promptul de sistem rămân aceleași pe toată conversația (cache-ul de prompt continuă să funcționeze); o schimbare se aplică conversațiilor noi, iar felul în care arată cardurile se schimbă imediat. Consola web nu are preferințe de afișare a chatului și arată mereu cardurile complete.

## Endpoint-uri de date [#endpoint-uri-de-date]

Tool-urile de date apelează endpoint-uri ale gateway-ului care întorc `{ block, provenance }`, unde `block` este deja un bloc de card valid, gata de pus neschimbat în `show_card` (sau `null` când nu s-a găsit nimic). Le poți apela direct cu orice cheie API codai; referința completă este pe [pagina API Carduri](../api/gateway/cards.mdx).

| Metodă | Cale | Întoarce | Sursă | Cost |
| --- | --- | --- | --- | --- |
| `GET` | `/v1/cards/weather?lat&lng&name` | bloc `weather` | MET Norway, cache 30–60 min | Gratuit |
| `GET` | `/v1/cards/places?q` sau `?lat&lng&radius&type` | bloc `places` | Google Places (New) | 0,032 USD pe apel |
| `GET` | `/v1/cards/geocode?address` | bloc `map` + `results` | Google Geocoding | 0,005 USD pe apel |
| `GET` | `/v1/cards/static-map?center\|markers&zoom&w&h` | PNG | Google Maps Static | 0,002 USD pe apel |
| `GET` | `/v1/cards/fx?source&from&to\|symbols` | bloc `metric` sau `table` | BNR / BCE, cache până la următoarea publicare | Gratuit |
| `POST` | `/v1/cards/unfurl` `{ url }` | bloc `links` (previzualizare) | Pagina însăși, cache 24 h | Gratuit |
| `POST` | `/v1/cards/product` `{ url, lang? }` | bloc `products` (1 element) | `Product` schema.org al paginii (sau etichetele OpenGraph de produs), cache 6 h | Gratuit, în limita de 500 de linkuri pe zi |
| `GET` | `/v1/cards/route?from\|origin&to\|destination&mode` | bloc `map` + `route` | Google Routes, cu trafic live la mașină | 0,010 USD cu mașina, 0,005 USD pe jos / bicicletă / transport în comun |
| `GET` | `/v1/cards/track?awb&carrier` | bloc `timeline` + `result` | Urmărirea publică Sameday; FAN Courier, Cargus, DPD doar cu contract de curier; cache 10 min | Gratuit |
| `GET` | `/v1/cards/efactura?limit` | bloc `table` + `documents` | Documentele tale emise prin Brivio, cu starea ANAF e-Factura raportată de Brivio | Gratuit |

Urmărirea coletelor: Sameday se urmărește live prin endpointul public de urmărire. FAN Courier, Cargus și DPD întorc doar coletele unui cont de client, deci urmărirea lor este live doar pe gateway-urile care au credențialele de contract ale curierului; în rest cardul spune că urmărirea live nu este disponibilă și trimite la pagina de urmărire a curierului. Codul easybox din SMS-ul curierului nu ajunge niciodată la gateway: aplicația îl desenează ca QR.

Redare de la moment: linkurile din transcrieri deschid `https://hub.codai.ro/transcripts/<id>?t=<secunde>`; pagina transcrierii derulează la replica din acea secundă și o evidențiază.

### Randarea unui card ca imagine [#randarea-unui-card-ca-imagine]

`POST /v1/cards/render` `{ block, format?, width?, height?, theme?, scale? }` transformă un bloc `chart` sau `qr` într-o imagine pentru suprafețele care nu îl pot desena — tile-uri și complicații pe ceas, notificări pe telefon, email, Telegram. Răspunsul este imaginea însăși: `image/png` (implicit) sau `image/svg+xml`, cu dimensiunea în pixeli în `x-codai-render-size`. Graficele sunt așezate cu Vega-Lite și rasterizate pe gateway cu un font inclus (fără rețea, fără fonturi de sistem); codurile QR rămân negru pe alb în ambele teme. `width` și `height` sunt între 64 și 1200 px CSS (implicit 480 × 300; un QR este pătrat), iar `scale` 1–3 înmulțește pixelii PNG-ului.

**Nu sunt încă suportate:** blocurile `diagram` (Mermaid) și `math` (KaTeX) — și orice alt tip de bloc — răspund cu `422 render_unsupported`; folosește atunci varianta text a blocului. Randarea este calcul local, deci este gratuită.

### Limite [#limite]

| Categorie | Pe utilizator pe zi UTC |
| --- | --- |
| Locuri (căutare text și în apropiere) | 200 de apeluri |
| Hărți (geocodare, hărți statice și trasee) | 500 de apeluri |
| Urmărire colete (interogări la curier) | 300; răspunsurile din cache nu se socotesc |
| Previzualizări de linkuri (unfurl) | 500 de descărcări; răspunsurile din cache nu se socotesc |
| Randări (`/v1/cards/render`) | 2000 de imagini |

Conturile `super_admin` sunt exceptate. O cotă epuizată răspunde cu `429`. Toate endpoint-urile trec și prin limita obișnuită de rată a cheii și prin [regulile](./tools.mdx) tale: unfurl este verificat ca `browser_fetch`, așa că o regulă care interzice descărcarea paginilor se aplică și aici, iar randarea ca `cards_render`. Când hărțile nu sunt configurate pe un gateway, endpoint-urile de hărți răspund cu `503 maps_not_configured` — răspunde atunci în text.

### Facturare [#facturare]

Apelurile plătite către Google sunt facturate în consumul tău la **prețul de listă Google**, ca rând în aceeași sesiune cu conversația: căutare de locuri text sau în apropiere **0,032 USD**, geocodare **0,005 USD**, hartă statică **0,002 USD**, traseu **0,010 USD** cu mașina (cu trafic) sau **0,005 USD** pe jos, cu bicicleta sau cu transportul în comun pe apel. Costul taxat apare în `provenance.costMicroUsd` (micro-USD) și în antetul de răspuns `x-codai-cost-micro-usd`. Un apel eșuat la furnizor nu este nici facturat, nici socotit. Vremea, cursurile de schimb, previzualizările de linkuri, urmărirea coletelor, starea e-Factura și randările sunt gratuite.

## Din SDK-uri și MCP [#din-sdk-uri-și-mcp]

- TypeScript: `client.cards.weather({ lat, lng })`, `.places({ q })`, `.geocode({ address })`, `.staticMap({ center })` (octeții PNG), `.fx()`, `.route({ from, to })`, `.track({ awb })`, `.efactura()`, `.unfurl(url)`, `.render({ block, format })` (octeții imaginii) — vezi [SDK-ul TypeScript](../sdks/typescript.mdx).
- Python: `client.cards.weather(lat, lng)`, `.places(q=…)`, `.geocode(address)`, `.static_map(center=…)`, `.fx(from_=…, to=…)`, `.route(from_=…, to=…)`, `.track(awb)`, `.efactura()`, `.unfurl(url)`, `.render(block, format=…)` (octeții imaginii) — vezi [SDK-ul Python](../sdks/python.mdx).
- MCP: [serverul MCP codai](../gateway/clients.mdx) expune `cards_weather`, `cards_places`, `cards_fx`, `cards_unfurl`, `cards_product`, `cards_route`, `cards_track` și `cards_efactura`; fiecare întoarce `{ block, provenance, card }` ca `structuredContent` (`card` este blocul ca un card gata făcut cu un singur bloc, `null` când nu s-a găsit nimic) și varianta markdown ca text. Randarea nu este un tool MCP: clienții MCP primesc JSON-ul cardului și îl desenează singuri sau folosesc aplicația MCP de mai jos.

```ts
import { Codai } from 'codai-sdk';

const codai = new Codai({ apiKey: process.env.CODAI_API_KEY! });
const { block, provenance } = await codai.cards.weather({ lat: 44.43, lng: 26.1, name: 'București' });
// block este un bloc de card `weather` gata de afișat; provenance.source === 'met_norway'
```

## Cardurile ca aplicație MCP [#cardurile-ca-aplicație-mcp]

Serverul MCP codai livrează cardurile și ca [aplicație MCP](https://modelcontextprotocol.io/extensions/apps) (MCP Apps 2026-01-26). Fiecare tool `cards_*` are `_meta.ui.resourceUri: "ui://codai/card"`, iar serverul listează această resursă (`text/html;profile=mcp-app`). Gazdele care suportă MCP Apps (VS Code, Claude, ChatGPT, Cursor, aplicațiile codai pentru desktop și telefon) o încarcă într-un cadru izolat și desenează cardul cu același renderer ca aplicațiile codai, în tema și limba gazdei. Gazdele fără MCP Apps ignoră câmpul și afișează varianta markdown.

- Vizualizarea este un singur fișier HTML autonom: **nu face nicio cerere de rețea** și declară liste CSP goale (connect, resurse, cadre, base URI).
- Este mică (circa 200 KB, 65 KB comprimat), iar `resources/list` și `resources/read` includ `_meta["codai/sha256"]`, astfel încât gazda o poate păstra în cache per versiune.
- Butoanele cardului trec prin gazdă: deschide un link (`ui/open-link`, gazda confirmă), trimite un prompt (`ui/message`), copiază o valoare. Vizualizarea nu apelează niciodată tool-uri singură.
- În VS Code setarea este `chat.mcp.apps.enabled`.

### Aplicații MCP de la alte servere în codai [#aplicații-mcp-de-la-alte-servere-în-codai]

Aplicațiile codai pentru desktop și telefon **găzduiesc** și aplicații MCP de la orice server MCP conectat: când un tool declară o resursă `ui://`, rezultatul apare sub pasul tool-ului, într-un cadru izolat.

- **Izolare**: cadrul are o origine opacă proprie (`sandbox="allow-scripts"`, niciodată same-origin). Nu poate ajunge la aplicația codai, la cheile tale sau la puntea către dispozitiv.
- **Rețea**: CSP-ul strict vine din `_meta.ui.csp` al serverului. Sunt acceptate doar origini `https`/`wss`, niciodată `localhost` sau adrese IP, iar implicit nu există deloc rețea.
- **Apeluri de tool-uri din aplicație**: permise doar pentru tool-urile aceluiași server și doar pentru cele pe care serverul le face disponibile aplicațiilor. Tool-urile doar-citire rulează direct. Restul urmează setările agentului: în **Autopilot** (desktop) sau **Permite tot** / **Autopilot** (telefon) rulează fără întrebare; altfel alegi Permite / **Permite mereu** / Refuză. „Permite mereu” se reține pentru acel tool la fel ca la apelurile agentului (desktop: pentru conversația curentă; telefon: în Setări → Agent, de unde îl poți reseta). Regula aceluiași server, regula de vizibilitate și limita de 10 apeluri la 10 secunde se aplică în orice mod.
- **Context pentru agent** (`ui/update-model-context`): o aplicație îi poate spune agentului ce vezi (de exemplu elementul selectat). Se păstrează doar ultima actualizare a fiecărei aplicații; agentul o vede o singură dată, la pasul următor, marcată ca date nesigure de la o aplicație terță. O actualizare nu pornește niciodată singură o tură.
- **Mesaje din aplicație** (`ui/message`): o aplicație poate propune un mesaj trimis în numele tău. Vezi textul exact și alegi Trimite sau Nu trimite; cel mult o propunere la 5 secunde. Mesajul trimis ajunge în conversație ca unul scris de tine (ca notă dacă agentul lucrează).
- **Permisiuni**: linkurile se deschid doar pentru adrese `https`. Camera, microfonul și locația nu sunt acordate niciodată.
- **Consola web**: vizualizarea sesiunii din consolă nu rulează aplicația. Afișează în schimb rezultatul tool-ului (text, imagini și datele structurate, ascunse după un buton), cu o notă că vizualizarea interactivă se deschide în aplicația pentru desktop sau telefon.

## A2UI [#a2ui]

codai citește [A2UI](https://a2ui.org) **v0.9** (specificația stabilă actuală) și **v1.0** (release candidate) și o transformă într-un card codai. Când rezultatul unui tool MCP conține mesaje A2UI, aplicațiile pentru desktop și telefon desenează cardul obținut sub pasul tool-ului, cu rendererul obișnuit de carduri, deci nu e nevoie de alt renderer.

Un rezultat este A2UI când are o resursă încorporată cu `mimeType: "application/a2ui+json"` (v1.0) sau `"application/json+a2ui"` (v0.9) (în `text` stă un array JSON de mesaje, un singur mesaj sau JSONL) sau un array `structuredContent.a2ui`. Adaptorul redă cele patru mesaje server→client (`createSurface`, `updateComponents`, `updateDataModel`, `deleteSurface`), păstrează câte un model de date pentru fiecare suprafață și rezolvă legăturile `{ "path": … }`: JSON Pointer absolut, plus căi relative în șabloanele de listă. Fiecare suprafață încă activă care are o componentă `root` devine un card. Dacă rezultatul nu avea niciun bloc text, modelul primește textul echivalent al cardului în loc de JSON brut.

O suprafață urmează versiunea mesajului care a creat-o. Într-o suprafață **v1.0** doar `{ "@path": … }` și `{ "@call": … }` sunt legături; un obiect cu o cheie simplă `path` sau `call` este afișat ca dată literală, cheile `@@` pierd un `@`, iar cheile necunoscute cu un singur `@` sunt respinse cu un avertisment. v1.0 mai permite ca `createSurface` să aducă `components` și `dataModel` într-un singur mesaj, șterge o cheie din date când `updateDataModel` are `value` egal cu `null`, rezolvă `@index` în șabloanele de listă, pune `Action.event.userMessage` la începutul promptului butonului și afișează `Video.posterUrl` ca miniatură. Mesajele v1.0 pentru apeluri de funcții (`callRendererFunction`, `agentFunctionResponse`) sunt ignorate: un card doar afișează.

Pentru dezvoltatori: `@codai/cards/a2ui` exportă `a2uiToCards(messages)`, care întoarce `{ cards, warnings }`, și `isA2uiPayload(x)`. Geamănul Kotlin este `ro.codai.cards.A2ui`. Ambele sunt verificate pe aceleași cazuri golden din `packages/cards/fixtures/a2ui/`.

| Componentă A2UI v0.9 | Card codai |
| --- | --- |
| `Text` | bloc `text`. Primul `h1`–`h3` de la nivelul de sus devine titlul cardului, celelalte titluri rămân titluri markdown |
| `Image`, `Video` | bloc `gallery` (doar URL-uri https sau `data:image`) |
| `AudioPlayer` | link într-un bloc `text` |
| `Row` | o singură linie de text unită cu ` · ` (imaginile merg în galerie) |
| `Column`, `List`, `Card` | aplatizate în ordine; copiii din șablon se repetă pentru fiecare element din lista legată |
| `Divider` | `---` într-un bloc `text` |
| `Button` cu `action.event` | acțiune de card de tip `prompt`: un clic trimite `A2UI action <name> <context JSON>`, cu legăturile din context rezolvate |
| `Button` cu `openUrl` | acțiune de card de tip `open_url` (doar https) |
| `ChoicePicker` | bloc `choice` (`mutuallyExclusive` = single, `multipleSelection` = multi) |
| `TextField`, `CheckBox`, `DateTimeInput`, `Slider` | rânduri `kv` doar-citire, cu valoarea curentă (câmpurile `obscured` sunt mascate) |
| `Tabs`, `Modal` | aplatizate: titlurile taburilor devin linii îngroșate, conținutul modalului urmează declanșatorului |
| `Icon` | omis |
| orice altă componentă | `text`/`label` ca text, apoi copiii ei, cu un avertisment |

**Limitări.** Câmpurile nu se pot edita într-un card: le vezi valorile, iar un buton trimite valorile pe care serverul le are deja. Funcțiile locale în afară de `openUrl`, verificările `checks`, temele și `sendDataModel` sunt ignorate. Dintre funcțiile de formatare sunt suportate `formatString`, `formatNumber`, `formatCurrency` și `pluralize`. Un card are cel mult 24 de blocuri, 6 acțiuni și 32 KB, deci ce trece de aceste limite este tăiat. Fiecare omisiune este raportată ca avertisment, iar adaptorul nu eșuează niciodată pe date necunoscute.

**v1.0.** Când A2UI v1.0 devine finală, adaptorul va accepta plicul ei alături de v0.9 (o singură comutare pe `version`). Tabelul de mai sus rămâne la fel dacă nu se schimbă catalogul de bază. Decizia este documentată în `docs/decisions/2026-10-14-a2ui-adapter.md`.
