Status: accepted · Date: 2026-10-23 · Related: ADR-0007, ADR-0017, docs/specs/ai-features.md §3
Enforcement: packages/api/src/features/agent-tools/agent-tools.garage.test.ts (the results the cards draw, on the seeded garage), apps/mobile/src/features/assistant/assistant.model.test.ts (a result becomes a card only when its contract parses).
The spec asks the copilot to answer with small screens built from AutoFolio components (total, monthly bars, ring, comparison table, car card, service card, alert list, expense draft, action button) instead of text only, and to time-box a spike of OpenUI on Expo before building. Its Plan B: each assistant tool returns typed data and the app has a fixed component per result type.
The spike (2026-10-23, well inside the 3-day box)
Read from the published packages, not the docs alone:
@openuidev/lang-core1.0.1 (MIT, published 2026-10-15, so past the two-day quarantine) is the framework-free parser, prompt generator and runtime (Query,Mutation,$variables): about 180 KB of ESM onzod/v4, with@modelcontextprotocol/sdkas an optional peer. Itspostinstallsends installation telemetry to PostHog (pnpm 11 runs no dependency build script unlessallowBuildsnames it, so it would need an explicitfalse); runtime telemetry is opt-in and server-side only.@openuidev/react-lang1.0.1 has areact-nativeexport condition, but that entry re-exports the same chunk as the web one:Renderer, its query loader andRetryrenderdivandbuttonand inject a<style>throughdocument.createElement. None of that exists on React Native, so their renderer cannot run in the app. It also depends on@openuidev/devtools(react-dom,lucide-react).
So the two OpenUI options were their parser with our own React Native renderer of its element tree, or our own parser and renderer for the same line format. Both lose against Plan B here:
- Numbers (RN03, RN04). OpenUI Lang lets the model write props:
Total("R$ 48.320"). Every literal would need grounding against a tool result before it is drawn. With Plan B the numbers never pass through the model: the card draws the tool's own display text. - No streaming to win. The phone reads the finished answer and then the saved Thread (ADR-0007, amendment of 2026-10-23); OpenUI's main gain, drawing while the answer streams, does not apply.
- A second read path. OpenUI's
Query/@Runwould make the app run reads the tools already ran on the server under RLS and per-vehicle roles. Plan B has one path: the tool call. - Cost and weight. A component library spec in every system prompt, a new runtime and parser in the Hermes bundle, and a telemetry postinstall, for layouts the boards draw as fixed cards anyway.
Decision
Plan B. The garage tools' results are the copilot's result types. Their Zod schemas live in the leaf assistant/assistant.contract.ts (the app parses them without bundling TanStack AI), and the app has one card per result type:
| Tool | Card |
|---|---|
costs_summary | Copilot-Costs: total, change against the span before, monthly bars, top categories |
costs_compare | Copilot-Compare's table: cost per km or mile and per month, per car |
sale_estimate | Copilot-Compare's sale: estimated value with its source, loan, deal result (estimate) |
expense_draft | Copilot-Draft: the draft's fields and the tank's economy; Review and save, Discard |
vehicle_tco | total, its components and the category ring |
net_worth | net, assets, loans, each car's value with its source |
upcoming | the alert list: services (overdue, soon) and documents due |
garage_cars | car cards, only when they are the whole answer (it mostly finds a vehicleId) |
- Every number comes from a tool. A card shows only the result's display text (formatted on the server in the account's locale: totals in whole units, a cost per distance and a single expense to the cent). The model's words sit above the cards.
- Every card shows its source and opens its screen (
ASSISTANT_LINK, which now admits/prepare-sale/<id>). Estimates carry source · date · confidence (RN03); the sale card says "deal result (estimate)" and never "profit" (RN04). - Writes stay drafts (RN10).
expense_draftwrites nothing; Review and save opens Add expense prefilled (amount, date, vendor, litres, full tank, reading) and the person saves there.sale_estimateis read-only; Prepare sale opens M-Vender. ai.genuigates how the copilot renders (ADR-0017). With it off, the app draws the text bubbles of M-Assistente as before. With it on, the app draws the cards and sendscards: true; the server, with the flag on, then asks the model for one or two short sentences instead of repeating the card's numbers. The web assistant sends nothing and keeps full text answers.
New tool data this needed: costs_summary gives the car's name, the period's months and the change against the same span before (the year before for ytd); costs_compare a cost per month (none for all history or a car sold in the period); expense_draft takes a fill-up's volume, full tank and reading in the units the user said, and returns them in the account's units with the economy of the tank it closes (RF09); sale_estimate is new. The catalog has 23 tools.
Consequences
- A new copilot result is a tool result schema in the assistant contract and one card; the model cannot compose a layout the app does not have. When it needs two views, it calls two tools.
- No "line" card: no tool returns a value over time other than the months, which are bars.
- Discarding a draft hides it until the conversation is reopened. shortcut: store dismissals with the Thread if people mind.
- Revisit OpenUI when its React renderer runs on React Native, the phone streams answers, or the product needs layouts the model composes beyond tool results. Its
Querywould then have to call only our oRPC procedures (RLS and per-vehicle roles), as the spec requires.