Skip to content
Docs

Command palette

Jump to a page or search the docs.

Changelog

ADR-0018: The copilot draws typed tool results as fixed cards (Plan B), not OpenUI

The repository's architectural decision, rationale and consequences.

Markdown

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-core 1.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 on zod/v4, with @modelcontextprotocol/sdk as an optional peer. Its postinstall sends installation telemetry to PostHog (pnpm 11 runs no dependency build script unless allowBuilds names it, so it would need an explicit false); runtime telemetry is opt-in and server-side only.
  • @openuidev/react-lang 1.0.1 has a react-native export condition, but that entry re-exports the same chunk as the web one: Renderer, its query loader and Retry render div and button and inject a <style> through document.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:

  1. 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.
  2. 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.
  3. A second read path. OpenUI's Query/@Run would 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.
  4. 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:

ToolCard
costs_summaryCopilot-Costs: total, change against the span before, monthly bars, top categories
costs_compareCopilot-Compare's table: cost per km or mile and per month, per car
sale_estimateCopilot-Compare's sale: estimated value with its source, loan, deal result (estimate)
expense_draftCopilot-Draft: the draft's fields and the tank's economy; Review and save, Discard
vehicle_tcototal, its components and the category ring
net_worthnet, assets, loans, each car's value with its source
upcomingthe alert list: services (overdue, soon) and documents due
garage_carscar 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_draft writes nothing; Review and save opens Add expense prefilled (amount, date, vendor, litres, full tank, reading) and the person saves there. sale_estimate is read-only; Prepare sale opens M-Vender.
  • ai.genui gates 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 sends cards: 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 Query would then have to call only our oRPC procedures (RLS and per-vehicle roles), as the spec requires.
Edit on GitHubDocumentation is in English.

We use essential cookies to make our app work. With your consent, we also use optional cookies to improve your experience. Read our Cookie Policy and Privacy Policy.