Philosophy

Principles

taw-ui is built for the next generation of Human-AI Interaction. These are the rules we follow — and the rules we think every AI tool UI library should follow.

The AI returns JSON. The component renders it. The developer writes zero glue code. That's the entire contract.

01

The Schema Is the API

One Zod schema is the single source of truth. It validates on the server, validates on the client, infers TypeScript types, and drives rendering. There are no separate action definitions, surface configurations, or render instructions.

The AI doesn't need to know about React. It doesn't need to know about components. It returns data that matches a schema — the same schema it already uses as its tool's outputSchema. The rendering is the framework's job, not the model's job.

The entire integration
// Server: define tool output shape
const getMetrics = tool({
  outputSchema: KpiCardSchema,
  execute: async () => ({
    id: "revenue",
    label: "Revenue",
    value: 142580,
    confidence: 0.92,
  }),
})

// Client: render it
<KpiCard part={part} />
Separate schema for validation and separate config for rendering
One schema drives everything — validation, types, and UI
02

Zero Glue Code

Components handle their entire lifecycle from a single prop. Loading? Skeleton with shimmer. Streaming? Progressive render. Success? Animated entrance. Parse error? Helpful inline error with field-level suggestions. The developer never writes conditional rendering for tool states.

This is all you write
// Handles loading, streaming, error, AND success
<KpiCard part={part} />

// Not this:
if (part.state === "loading") return <Skeleton />
if (part.state === "error") return <Error msg={part.error} />
const data = validate(part.output)
if (!data) return <ParseError />
return <KpiCard data={data} />

Common affordances are built in, not opt-in. A DataTable sorts columns, formats currencies, and shows row counts by default. A KpiCard animates numbers and shows confidence badges by default. You don't configure these — you disable them if you don't want them.

Wire up loading states, error boundaries, and validation manually
Pass the part. The component handles the rest.
03

AI-Native Fields

Every schema supports two fields that don't exist in traditional UI libraries: confidence (0–1) and source (provenance). These are first-class because AI outputs are probabilistic — users deserve to know how certain the data is and where it came from.

ConfidenceBadgeSignal
≥ 0.8GreenHigh certainty — safe to act on
0.5 – 0.8AmberModerate — verify before acting
< 0.5RedLow certainty — treat as estimate

These fields are optional in every schema. If the AI doesn't provide them, the component renders cleanly without them. But when they're present, the user sees exactly how much to trust the output. No other library does this.

Show AI data without indicating certainty or source
Surface confidence and provenance so users can calibrate trust
04

Decisions Produce Receipts

When a component asks the user to decide (approve, select, confirm), the interactive controls must transform into a permanent record after the decision. This is the receipt pattern. It prevents stale buttons in chat history, saves vertical space, and gives the AI a clear signal of what was chosen.

Present
Select
Confirm
Receipt
Full decision lifecycle
const [receipt, setReceipt] = useState<TawReceipt>()

<OptionList
  part={part}
  onAction={(actionId, payload) => {
    // payload.receipt is ready to use
    setReceipt(payload.receipt)
  }}
  receipt={receipt}
/>

// Before decision: full interactive list
// After decision: compact "✓ Rolling Deploy" receipt

Receipt labels use past tense: Approved, Selected, Confirmed, Skipped. The action description stays imperative. Pattern: [Past-tense verb] [What was acted on]

Leave interactive buttons active after the user has decided
Collapse to a receipt — compact, permanent, referenceable
05

Everything Is Addressable

Every component, every option, every row has a stable id. These must be backend identifiers (database IDs, slugs, URLs) — never array indexes or random UUIDs generated at render time.

Addressability is what makes the AI useful after the first response. When the user says " tell me more about the Acme Corp row", the AI can reference row.name === "Acme Corp" because the data has stable keys. When the user picks an option, the receipt contains selectedIds: ["rolling"] — not selectedIndex: 0.

Rendered DOM<div data-taw-component="kpi-card" data-taw-id="revenue-q4">
Use array indexes or render-time UUIDs as identifiers
Use stable backend IDs that persist across re-renders and sessions
06

Chat-Native by Default

AI tool UIs live inside chat. Chat is a vertical feed, 400–600px wide, where attention is scarce. Every component must communicate its purpose within the first 300px of vertical space. If a component needs tabs, navigation, or horizontal scrolling to be understood — it's too complex.

400–600pxMax width
300pxFirst impression
44pxMin tap target

No input fields. The chat composer is the input. The only interactive elements are selection (pick from options the AI provides) and confirmation (approve/reject what the AI proposes). Limit visible choices to 5–7. If the AI needs to show more, it should say so — not render a paginated table.

Build components that need tabs, forms, or horizontal scroll
Single-column, scannable in 300px, actionable with one tap
07

Fail Helpfully, Never Silently

When the AI returns data that doesn't match the schema, the component must never render null, crash, or show a blank space. It renders a helpful inline error that shows exactly which fields failed validation and suggests corrections.

Parse errors show the expected type, the received value, and a" Did you mean? " suggestion when possible. This is critical for development — the AI model iterates faster when it can see what went wrong. And in production, a visible error is always better than a silent one.

!Schema validation failed

missing label (string) — received field "title"

missing value (number | string) — received field "amount"

Did you mean: label → title, value → amount?

Return null, throw, or console.error when data is invalid
Render inline errors with field-level details and suggestions

The HAI Contract

Human-AI Interaction has three participants. Each has a clear responsibility. When one oversteps, the experience breaks.

AI
Responsible for

Returns structured data matching a schema. Introduces the component with context. References it later by ID.

Never does

Describe what the component already shows. Make rendering decisions. Generate HTML or styles.

Component
Responsible for

Validates data. Renders all states. Provides built-in affordances. Produces receipts from decisions.

Never does

Fetch data. Make AI calls. Render outside its container. Assume context about the conversation.

Developer
Responsible for

Registers tools with schemas. Maps tool names to components. Passes the part prop.

Never does

Write conditional rendering. Build loading states. Validate data manually. Style individual tool results.

TL;DR

01One schema = validation + types + UI
02One prop = loading + error + success
03Confidence and source are first-class
04Decisions collapse into receipts
05Everything has a stable ID
06Chat-native, single-column, no forms
07Errors are visible, never silent