npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

wireai-rn

v0.2.6

Published

Wire your AI agent to native mobile UI. Open-source React Native SDK for generative UI — render interactive native components from LLM responses.

Readme

wireai-rn

Wire your AI agent to native mobile UI.

Open-source React Native SDK for generative UI — render interactive native components from LLM responses. No custom parsers. No prompt engineering. Works with Ollama, LM Studio, OpenAI, A2A agents, or any HTTP agent endpoint.

npm version npm downloads license types stars

Created by Malik Chohra. Sponsored by AI Mobile Launcher and CasaInnov.

// Your agent returns JSON. Wire RN renders it as native components, validated.
<WireAIProvider llm={config} components={defaultComponents}>
  <ChatScreen />
</WireAIProvider>

Contents


Quick Start

npm install wireai-rn zod
import { WireAIProvider } from "wireai-rn";
import { defaultComponents } from "wireai-rn/components";

const config = { provider: "openai" as const, model: "gpt-4o-mini", apiKey: "..." };

export default function App() {
  return (
    <WireAIProvider llm={config} components={defaultComponents}>
      <ChatScreen />
    </WireAIProvider>
  );
}

That is the whole setup. Your agent returns { action, component, props }, Wire RN validates the props against the component's Zod schema, and renders a native React Native component. No WebView, no HTML, no hand-written parser. The full chat screen is in Step 3.


The Problem

AI agents speak text. Mobile users expect native UI.

Your agent works — it answers questions, follows instructions, produces useful output. But in React Native you have three bad options: a text chat that feels like 2018, a WebView wrapper that feels cheap, or a custom UI that takes months.

WireAI fills this gap. Register your components with a description and a Zod schema. The LLM picks which one to show. WireAI validates the props and renders it natively.


How It Works

flowchart TD
    A([User sends a message]) --> B[useWireAIThread]
    B --> C["Build context\n(trim history to budget,\nprepend system prompt)"]
    C --> D{LLM Adapter}
    D --> |Ollama| E1[Local model]
    D --> |LM Studio| E2[Local model]
    D --> |OpenAI| E3[Cloud API]
    D --> |Webhook| E4[Your backend]
    E1 & E2 & E3 & E4 --> F["Raw JSON string\n{ action, component, props }"]
    F --> G[validateLLMResponse]
    G --> H{Props valid?}
    H --> |Yes| I[ComponentRenderer]
    H --> |No| J["Turn dropped:\nuseWireAIThread sets error"]
    I --> K([Native component renders])

FallbackMessage is not on this path. It renders per node inside ComponentRenderer, when a node's props fail their schema or the tree runs past depth 8, and from ComponentErrorBoundary when a mounted component throws. A response that came through validateLLMResponse has already had both of those checks applied to every node, so on the main path above an invalid response surfaces as error and the turn is dropped instead.


How the LLM Chooses a Component

WireAI auto-generates a system prompt from your registered components. The LLM reads it and decides which component fits the conversation.

sequenceDiagram
    participant App
    participant WireAI
    participant LLM

    App->>WireAI: <WireAIProvider components={[MoodSelector, ActionCard, ...]}>
    WireAI->>WireAI: Build system prompt from registry
    Note over WireAI: "You have these components:\n- MoodSelector: use when...\n- ActionCard: use when..."

    App->>WireAI: sendMessage("I feel tired today")
    WireAI->>LLM: [system prompt] + [history] + [user message]
    LLM-->>WireAI: { "action": "render", "component": "MoodSelector", "props": { ... } }
    WireAI->>WireAI: Validate props with Zod schema
    WireAI-->>App: <MoodSelector question="..." options={[...]} />

Install

npm install wireai-rn zod
# or
yarn add wireai-rn zod

Peer dependencies:

{
  "react": ">=18.0.0",
  "react-native": ">=0.73.0",
  "zod": ">=3.22.0"
}

Zod v3 only. Zod v4 has breaking API changes.


Step-by-Step Guide

Step 1 — Pick your LLM provider

flowchart LR
    Dev(["🧑‍💻 You are..."])
    Dev --> A{Environment}
    A --> |Local dev, no API cost| B[Ollama]
    A --> |Local dev, GUI preferred| C[LM Studio]
    A --> |Best quality, cloud| D[OpenAI]
    A --> |Production app| E[Webhook / Your backend]

    B --> B1["provider: 'ollama'\nbaseUrl: 'http://localhost:11434'\nmodel: 'llama3'"]
    C --> C1["provider: 'lmstudio'\nbaseUrl: 'http://localhost:1234'\nmodel: 'llama-3-8b-instruct'"]
    D --> D1["provider: 'openai'\nmodel: 'gpt-4o-mini'\napiKey: '...' ← dev only!"]
    E --> E1["provider: 'webhook'\nbaseUrl: 'https://api.yourapp.com/ai'\nmodel: 'gpt-4o'"]

| Provider | When to use | Cold start | Cost | |---|---|---|---| | Ollama | Local development, privacy | ~2s | Free | | LM Studio | Local development, GUI | ~2s | Free | | OpenAI | Best quality + zero setup | <1s | Pay per token | | Webhook | Production (server holds keys) | <1s | Your backend |


Step 2 — Wrap your app

import { WireAIProvider } from "wireai-rn";
import { defaultComponents } from "wireai-rn/components"; // 11 built-in components

const LLM_CONFIG = {
  provider: "ollama" as const,
  baseUrl: "http://localhost:11434",
  model: "llama3",
};

export default function App() {
  return (
    <WireAIProvider llm={LLM_CONFIG} components={defaultComponents}>
      <ChatScreen />
    </WireAIProvider>
  );
}

WireAIProvider does three things on mount:

  1. Registers all your components into an in-memory registry
  2. Builds the LLM system prompt from the registry (auto-generated, no manual prompt writing needed)
  3. Pings your LLM endpoint in __DEV__ to confirm connectivity

Step 3 — Build a chat screen

import {
  useWireAIThread,
  useWireAIInput,
  useWireAIAction,
  ComponentRenderer,
  LoadingState,
} from "wireai-rn";
import type { Message } from "wireai-rn";
import {
  FlatList,
  KeyboardAvoidingView,
  TextInput,
  Text,
  TouchableOpacity,
  View,
} from "react-native";
import { useRef, useEffect } from "react";

export function ChatScreen() {
  const { messages, isLoading, error, sendMessage, abort } = useWireAIThread();
  const { inputText, setInputText, handleSubmit } = useWireAIInput(sendMessage);
  const createCallbacks = useWireAIAction(sendMessage);
  const listRef = useRef<FlatList>(null);

  // Auto-scroll to newest message
  useEffect(() => {
    if (messages.length) {
      setTimeout(() => listRef.current?.scrollToEnd({ animated: true }), 100);
    }
  }, [messages.length]);

  const renderItem = ({ item }: { item: Message }) => {
    if (item.role === "user") {
      return (
        <View style={{ alignSelf: "flex-end", padding: 8, backgroundColor: "#6C47FF", borderRadius: 12, margin: 4 }}>
          <Text style={{ color: "#fff" }}>{item.content}</Text>
        </View>
      );
    }
    if (item.role === "assistant" && item.response) {
      return (
        <ComponentRenderer
          messageId={item.id}
          response={item.response}
          callbackOverrides={createCallbacks(item.id)}
          isStreaming={item.isStreaming ?? false}
        />
      );
    }
    return null;
  };

  return (
    <KeyboardAvoidingView style={{ flex: 1 }} behavior="padding">
      <FlatList
        ref={listRef}
        data={messages}
        keyExtractor={(m) => m.id}
        renderItem={renderItem}
        contentContainerStyle={{ padding: 16 }}
      />

      {isLoading && <LoadingState />}
      {error && <Text style={{ color: "red", padding: 8 }}>{error}</Text>}

      <View style={{ flexDirection: "row", padding: 8, gap: 8 }}>
        <TextInput
          style={{ flex: 1, borderWidth: 1, borderRadius: 8, padding: 8 }}
          value={inputText}
          onChangeText={setInputText}
          onSubmitEditing={handleSubmit}
          placeholder="Type a message..."
          returnKeyType="send"
        />
        <TouchableOpacity
          onPress={isLoading ? abort : handleSubmit}
          style={{ padding: 8, backgroundColor: "#6C47FF", borderRadius: 8 }}
        >
          <Text style={{ color: "#fff" }}>{isLoading ? "Stop" : "Send"}</Text>
        </TouchableOpacity>
      </View>
    </KeyboardAvoidingView>
  );
}

Hooks at a glance:

flowchart LR
    T[useWireAIThread] --> |messages, isLoading, error| UI
    T --> |sendMessage, abort| UI
    I[useWireAIInput] --> |inputText, setInputText, handleSubmit| UI
    A[useWireAIAction] --> |createCallbacks| CR[ComponentRenderer]
    UI --> CR

Step 4 — Register a custom component

Custom components give the LLM new capabilities specific to your app.

flowchart TD
    S1["1. Define Zod schema\n(what props can the LLM send?)"]
    S2["2. Write the React component\n(what does it render?)"]
    S3["3. Write the description\n(when should the LLM use it?)"]
    S4["4. Export as WireAIComponent\n(name + schema + component + description)"]
    S5["5. Pass to WireAIProvider\ncomponents={[MyComponent, ...defaultComponents]}"]
    S1 --> S2 --> S3 --> S4 --> S5

Full example:

import React, { useCallback, useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { z } from "zod";
import { colors, spacing, radii, textStyles } from "wireai-rn";
import type { WireAIComponent } from "wireai-rn";

// 1. Schema — every field needs .describe() so the LLM knows what to fill in
const schema = z.object({
  question: z.string().describe("The mood check-in question, e.g. 'How are you feeling right now?'"),
  options: z.array(z.string()).describe("4-6 mood labels, warm and non-clinical"),
});

// 2. React component
const MoodSelectorView: React.FC<z.infer<typeof schema> & { onSelect?: (v: string) => void }> =
  React.memo(({ question, options, onSelect }) => {
    const [selected, setSelected] = useState<string | null>(null);

    const handlePress = useCallback((opt: string) => {
      if (selected) return; // one-shot: lock after first tap
      setSelected(opt);
      onSelect?.(opt);
    }, [selected, onSelect]);

    return (
      <View style={styles.card}>
        <Text style={styles.question}>{question}</Text>
        {options.map((opt) => (
          <Pressable
            key={opt}
            style={[styles.chip, selected === opt && styles.chipSelected]}
            onPress={() => handlePress(opt)}
            disabled={!!selected}
          >
            <Text style={[styles.chipText, selected === opt && styles.chipTextSelected]}>
              {opt}
            </Text>
          </Pressable>
        ))}
      </View>
    );
  });

// 3 + 4. Export definition — description is a routing instruction for the LLM
export const MoodSelector: WireAIComponent<typeof schema> = {
  name: "MoodSelector",
  description:
    "Use when checking the user's current mood or emotional state at the start of a session. " +
    "Provide 4-6 warm, non-clinical mood labels as options. " +
    "Do NOT use for yes/no questions — use ConfirmPrompt instead.",
  component: MoodSelectorView,
  propsSchema: schema,
};

const styles = StyleSheet.create({
  card: { padding: spacing.md, backgroundColor: colors.backgroundSecondary, borderRadius: radii.lg, gap: spacing.sm },
  question: { ...textStyles.h4, color: colors.text },
  chip: { padding: spacing.sm, borderRadius: radii.full, borderWidth: 1, borderColor: colors.border, alignItems: "center" },
  chipSelected: { backgroundColor: colors.primary, borderColor: colors.primary },
  chipText: { color: colors.textSecondary },
  chipTextSelected: { color: "#fff", fontWeight: "600" },
});

Then pass it to the provider alongside the built-ins:

<WireAIProvider components={[MoodSelector, ...defaultComponents]}>

Component description writing rules:

| Do | Don't | |---|---| | "Use when the user needs to pick a mood" | "A mood picker card" | | "Do NOT use for yes/no — use ConfirmPrompt" | (no negative routing) | | "Provide 4-6 options, warm and non-clinical" | (no output guidelines) | | Reference other components by name | Reference your app's business logic |


Step 5 — Add persistence (optional)

Without persistence, conversations reset on every app restart. WireAI supports any storage backend through the StorageBackend interface.

flowchart TD
    subgraph "On App Start"
        A1[Load config from SecureStore] --> A2[Load history from MMKV]
        A2 --> A3["<WireAIProvider initialMessages={history}>"]
    end

    subgraph "During Session"
        B1[User sends message] --> B2[LLM responds]
        B2 --> B3["onThreadUpdate(messages)"]
        B3 --> B4[Save to MMKV]
    end

    subgraph "Config Change"
        C1[User updates API key] --> C2["saveConfig(newConfig)"]
        C2 --> C3[Saved to SecureStore]
    end

Install storage libraries:

# API key + LLM config → iOS Keychain / Android Keystore
npx expo install expo-secure-store

# Conversation history → high-performance key-value store
yarn add react-native-mmkv

Create storage backends:

// storage/secureStorageBackend.ts
import * as SecureStore from "expo-secure-store";
import type { StorageBackend } from "wireai-rn";

export const secureStorageBackend: StorageBackend = {
  getItem: (key) => SecureStore.getItemAsync(key),
  setItem: (key, value) => SecureStore.setItemAsync(key, value),
  deleteItem: (key) => SecureStore.deleteItemAsync(key),
};
// storage/mmkvHistoryStorage.ts
import { MMKV } from "react-native-mmkv";
import type { StorageBackend } from "wireai-rn";

const store = new MMKV({ id: "chat-history" });

export const mmkvHistoryStorage: StorageBackend = {
  getItem: async (key) => store.getString(key) ?? null,
  setItem: async (key, value) => store.set(key, value),
  deleteItem: async (key) => store.delete(key),
};

Wire it all together:

import { useLLMConfigStorage, WireAIProvider } from "wireai-rn";
import type { Message } from "wireai-rn";
import { useCallback, useEffect, useState } from "react";
import { secureStorageBackend } from "./storage/secureStorageBackend";
import { mmkvHistoryStorage } from "./storage/mmkvHistoryStorage";

const DEFAULT_CONFIG = { provider: "openai" as const, model: "gpt-4o-mini" };
const HISTORY_KEY = "chat_history_v1";

export function AppRoot() {
  // Config — persisted in SecureStore
  const { config, isLoaded, saveConfig } = useLLMConfigStorage(
    secureStorageBackend,
    DEFAULT_CONFIG
  );

  // History — loaded from MMKV on first mount
  const [initialMessages, setInitialMessages] = useState<Message[]>([]);
  const [historyLoaded, setHistoryLoaded] = useState(false);

  useEffect(() => {
    mmkvHistoryStorage.getItem(HISTORY_KEY).then((raw) => {
      if (raw) {
        try { setInitialMessages(JSON.parse(raw)); } catch {}
      }
      setHistoryLoaded(true);
    });
  }, []);

  // Stable callback — called with the full message array on every update
  const handleThreadUpdate = useCallback((messages: Message[]) => {
    mmkvHistoryStorage.setItem(HISTORY_KEY, JSON.stringify(messages));
  }, []);

  if (!isLoaded || !historyLoaded) return null; // wait for storage reads

  return (
    <WireAIProvider
      llm={config}
      components={defaultComponents}
      initialMessages={initialMessages}
      onThreadUpdate={handleThreadUpdate}
    >
      <ChatScreen />
    </WireAIProvider>
  );
}

Composition — nested components

A container component can declare a children slot in its Zod schema using the exported NodeRefSchema. The renderer recurses into the tree, validates each node against the registered component's schema, and hands the container ready-to-mount React elements (pre-rendered in place of the raw NodeRef[]).

1. Declare a container:

import { NodeRefSchema } from "wireai-rn";
import type { WireAIComponent } from "wireai-rn";
import { z } from "zod";
import React from "react";
import { View, Text, StyleSheet } from "react-native";

const cardSchema = z.object({
  title: z.string().describe("Card heading"),
  children: z
    .array(NodeRefSchema)
    .optional()
    .describe("Nested child components — text, charts, status cards, etc."),
});

// The SDK pre-renders the children array into ReactNode[] before passing it
// down, so the container just lays them out.
const CardView: React.FC<z.infer<typeof cardSchema>> = ({ title, children }) => (
  <View style={styles.card}>
    <Text style={styles.title}>{title}</Text>
    {children as unknown as React.ReactNode}
  </View>
);

export const Card: WireAIComponent<typeof cardSchema> = {
  name: "Card",
  description:
    "A container holding a heading plus a list of nested components. Use when you need to group related child components under a single title.",
  component: CardView as React.ComponentType<z.infer<typeof cardSchema> & { messageId: string }>,
  propsSchema: cardSchema,
};

const styles = StyleSheet.create({
  card: { padding: 16, borderRadius: 12, backgroundColor: "#fff", gap: 8 },
  title: { fontSize: 17, fontWeight: "600" },
});

2. The LLM emits a tree:

{
  "action": "render",
  "component": "Card",
  "props": {
    "title": "Trip Summary",
    "children": [
      { "component": "MessageBubble", "props": { "message": "Lisbon, Sept 12–18" } },
      { "component": "StatusCard",    "props": { "status": "success", "title": "Booked", "ctaLabel": "What's next?" } }
    ]
  }
}

The renderer walks the tree, validates each node's props against its registered schema, and substitutes the children array with ReactNode[] before Card mounts.

Notes:

  • Max nesting depth is 8. Beyond that, the offending subtree renders FallbackMessage.
  • One bad subtree never kills the whole message — each level is wrapped in ComponentErrorBoundary, and per-node validation failures degrade to a localized fallback.
  • For dynamic layouts where the SDK can't auto-locate children, the component receives an injected renderNode(child) helper that turns a single NodeRef into a ReactNode.

Streaming — progressive UI as tokens arrive

Adapters that implement chatStream (OpenAIAdapter, OllamaAdapter, LMStudioAdapter and WebhookAdapter, which is every adapter except A2AAdapter) push tokens through an XHR-based stream. The thread hook parses partial JSON on each chunk and publishes the best-effort partial response to a small pub/sub store keyed by messageId. Subscribe with useWireAIStream to render only the bubble that's streaming — the rest of the conversation doesn't re-render.

Render a streaming bubble:

import { ComponentRenderer, LoadingState, useWireAIStream } from "wireai-rn";
import type { Message } from "wireai-rn";

function AssistantBubble({ message }: { message: Message }) {
  const stream = useWireAIStream(message.id);
  const response = stream?.response ?? message.response;
  if (!response) return <LoadingState />;
  return (
    <ComponentRenderer
      messageId={message.id}
      response={response}
      isStreaming={stream?.isStreaming ?? message.isStreaming ?? false}
    />
  );
}

Why XHR (not fetch)? React Native's fetch does not expose response.body as a ReadableStream, so true progressive reads are only available through XMLHttpRequest.onprogress. The adapter accumulates xhr.responseText and forwards new bytes to your onChunk callback.

Webhook server contract. When using WebhookAdapter.chatStream, your server must respond with Transfer-Encoding: chunked and stream the assistant's raw JSON text progressively. If your server wraps responses in { content: ... }, either strip the wrapper before streaming or use the non-streaming chat() method.

Behavior during a stream:

  • A placeholder assistant Message is inserted immediately when you call sendMessage. It carries isStreaming: true and an empty content until the final chunk arrives.
  • The stream store holds the latest partial. Nodes whose props don't yet pass strict Zod validation silently render null until the next chunk fills them in (no fallback flicker).
  • On the final chunk, the strict validator runs against the full buffer. On success, the placeholder is replaced with the finalized message and the stream store entry is cleared. On failure, the placeholder is removed and error is set.
  • Aborting (e.g. user taps Stop) tears down the XHR; no late onChunk callbacks fire.

Falls back automatically. A2AAdapter is the only adapter without chatStream, so an A2A turn keeps the one-shot chat() path even with streaming left on. No code change needed in your app to support the mix. If you configure provider: "custom", it resolves to the LM Studio adapter, so it streams like the rest.

No offline or cold-start lane. If the configured provider, server or agent is unreachable, the turn rejects: the assistant placeholder is removed and error is set. That holds for the very first turn as much as any later one. The SDK ships no cached first question, no canned offline flow and no automatic retry. What it gives you is errorKind and retry() on useWireAIThread, so your app can show its own affordance and re-run the last user message on a tap. Seeding initialMessages on WireAIProvider puts your own content on screen before the first turn, but it does not make that turn succeed.


Built-in Components (11)

Import from wireai-rn/components. All are Zod-validated, React.memo-wrapped, and include submitted-state protection (no double-submit).

| Component | Use When | Key Props | |---|---|---| | ActionCard | Offering 1–3 next-step CTA buttons | title, actions[{ label, value }] | | ChipSelectCard | Quick selection from compact labels (moods, tags, activities) | question, options[], multiSelect? | | ConfirmPrompt | Binary yes/no decision before an action | question, confirmLabel?, cancelLabel? | | ContentSelectCard | Selecting from items with title + description | title, items[{ title, description, value }] | | InfoList | Displaying read-only key/value summary data | title, items[{ label, value }] | | MessageBubble | Standard text chat response | message, sender? | | NumberStepperCard | Picking a number within a range | label, min, max, step?, defaultValue? | | SelectionCard | Choosing one option from a list with longer labels | title, options[{ label, value, description? }] | | StatusCard | Success, error, or informational status display | status, title, message? | | StepList | Ordered steps or itinerary | title, steps[] | | TextInputCard | Collecting free-text input (name, goal, note) | label, placeholder?, submitLabel? |


LLM Provider Reference

const config: LocalLLMConfig = {
  provider: "ollama" | "lmstudio" | "openai" | "webhook",
  baseUrl: "...",
  model: "...",
  apiKey?: "...",           // OpenAI / Webhook auth
  timeoutMs?: 30000,        // default: 30s
  temperature?: 0.7,        // default: 0.7
  maxTokens?: 1024,         // default: 1024
};

| Provider | Default base URL | Ping endpoint | Notes | |---|---|---|---| | ollama | http://localhost:11434 | GET /api/tags | Physical device: use LAN IP | | lmstudio | http://localhost:1234 | GET /v1/models | Start server in LM Studio first | | openai | https://api.openai.com | GET /v1/models | Strips /v1 suffix if present in baseUrl | | webhook | (required) | skipped | POST / with { messages, model } |


WireAIProvider Props

<WireAIProvider
  llm={config}                      // Required — LLM connection config
  components={components}            // Required — WireAIComponent[] registry
  maxContextMessages={20}            // Max messages kept in context (default: 20)
  maxContextChars={12000}            // Max chars in context (~3k tokens, default: 12000)
  systemPromptSuffix="You are..."   // Append app-specific instructions to auto-generated prompt
  initialMessages={[]}               // Pre-populate thread (e.g. from storage)
  onMessage={(msg) => {}}            // Called with each new message (analytics hook)
  onThreadUpdate={(msgs) => {}}      // Called with full thread on every update (persistence hook)
>
  {children}
</WireAIProvider>

Hooks Reference

useWireAIStream(messageId)

const stream = useWireAIStream(messageId);
// stream?.response   — latest partial WireAIResponse (or undefined)
// stream?.isStreaming — true until the final chunk arrives

Used inside per-message bubbles when the adapter supports chatStream. Re-renders are coalesced via requestAnimationFrame so a fast token stream doesn't flood React.

useWireAIThread()

const {
  messages,     // Message[] — full conversation history
  isLoading,    // boolean — LLM request in flight
  error,        // string | null — last error message
  errorKind,    // "interrupted" | "failed" | null — why the last turn ended unanswered
  sendMessage,  // (text: string, options?: SendMessageOptions) => void
  retry,        // () => void — re-run the last user message
  reset,        // () => void — clear history, return to initial state
  abort,        // () => void — cancel in-flight request
} = useWireAIThread();

errorKind says why the thread is sitting without an answer, and it is null when the thread is healthy. "failed" means the request errored and error carries the message. "interrupted" means the app went to background mid-turn and the request was aborted: nothing failed, error stays null, and the user's message is still in the thread unanswered. Show a retry affordance and call retry(). The SDK never resends by itself.

retry() re-runs the last user message without appending a second copy of it. It is a no-op while a send is in flight, and a no-op unless the newest message is an unanswered user message, so calling it twice cannot double-send.

useWireAIInput(sendMessage)

const {
  inputText,    // string — controlled input value
  setInputText, // Dispatch<SetStateAction<string>>
  handleSubmit, // () => void — trims + sends + clears input
} = useWireAIInput(sendMessage);

useWireAIAction(sendMessage)

Returns a factory that creates callback props for interactive components.

const createCallbacks = useWireAIAction(sendMessage);

// In renderItem:
<ComponentRenderer
  messageId={item.id}
  response={item.response}
  callbackOverrides={createCallbacks(item.id)}
  // ^ injects onSelect, onConfirm, onSubmit etc. that send messages back to the thread
/>

useLLMConfigStorage(storage, defaultConfig)

const {
  config,       // LocalLLMConfig — current resolved config
  isLoaded,     // boolean — false until storage read completes
  saveConfig,   // (config: LocalLLMConfig) => Promise<void>
  clearConfig,  // () => Promise<void> — resets to defaultConfig
} = useLLMConfigStorage(storage, defaultConfig);

| Param | Type | Description | |---|---|---| | storage | StorageBackend | Any { getItem, setItem, deleteItem } implementation | | defaultConfig | LocalLLMConfig | Fallback config if nothing is stored yet |


Context Budget

WireAI automatically trims old messages when the conversation grows:

maxContextMessages = 20   ← drop oldest messages beyond this count
maxContextChars = 12000   ← drop oldest messages beyond this char limit

The system prompt is always prepended fresh and is never trimmed. A dev warning fires at 80% utilization so you can tune before hitting limits.

<WireAIProvider
  maxContextMessages={30}  // increase for long sessions
  maxContextChars={20000}  // increase for GPT-4o (128k context)
>

Security

flowchart LR
    subgraph "❌ Never do this in production"
        A["apiKey in app bundle"] --> B["Extractable from .ipa / .apk"]
    end
    subgraph "✅ Production pattern"
        C["User authenticates to your server"] --> D["Your server holds the LLM API key"]
        D --> E["provider: 'webhook'\nbaseUrl: 'https://api.yourapp.com/ai'"]
    end
  • Local LLMs (Ollama, LM Studio): no API key needed — safe for production if the model runs on the device or a private server
  • Cloud LLMs in dev: apiKey is fine in __DEV__ mode, but WireAI logs a warning
  • Cloud LLMs in production: always use WebhookAdapter with a server-side proxy

Acceptable use and the EU AI Act

Position as of 2026-08-06, against Regulation (EU) 2024/1689 as amended by Regulation (EU) 2026/1744. This is our reading, not legal advice, and a review by a qualified lawyer is pending.

This system is not intended to be put into service as, or changed into, a high-risk AI system. Do not build Wire-powered surfaces into an Annex III high-risk area: biometrics, critical infrastructure, education and vocational training, employment, access to essential services and benefits, law enforcement, migration, or administration of justice. Two Article 5(1) prohibitions have been live since 2 February 2025: no subliminal, manipulative or deceptive techniques that materially distort behaviour, and no exploiting vulnerabilities due to age, disability, or a specific social or economic situation.

The app that installs this SDK is the deployer of the surface it ships. Wire runs no model and hosts no service here, so you pick the model, the prompt and the flow.

On Article 50(1), applicable since 2 August 2026 with no legacy grace period: our reading is that generated UI made of cards, buttons and form fields is not a conversational agent, and a user tapping through it is not being led to believe there is a human on the other end. If you build a chat-like or free-text-responding surface instead, that reading stops and the disclosure that the user is interacting with an AI system is your app's to place. This SDK ships MessageBubble and a ChatScreen boilerplate, so that path is a short one from here.

On Article 50(2): the text a user sees comes from a model you chose, and provider-level marking of a model's output sits upstream with that model's provider. For the render layer, we rely on the carve-out in the article itself, which "shall not apply to the extent the AI systems perform an assistive function for standard editing or do not substantially alter the input data provided by the deployer or the semantics thereof."

The full position, including Article 50(3), Article 25 and what the MIT licence changes, is in EU-AI-ACT.md.


Design Tokens

Use these in your custom components to stay visually consistent with the built-ins:

import { colors, spacing, radii, textStyles } from "wireai-rn";

// Colors
colors.primary          // #6C47FF — violet
colors.text             // #0D0D14 — near-black
colors.textSecondary    // #6B7280 — muted
colors.backgroundSecondary // surface cards
colors.border           // subtle stroke

// Spacing
spacing.xs   // 4
spacing.sm   // 8
spacing.md   // 16
spacing.lg   // 24
spacing.xl   // 32

// Radii
radii.sm     // 6
radii.md     // 10
radii.lg     // 16
radii.full   // 999 (pill)

// Text styles (spread into StyleSheet)
textStyles.h3    // 20px semibold
textStyles.h4    // 17px semibold
textStyles.body  // 15px regular
textStyles.small // 13px regular

Contributing

git clone [email protected]:chohra-med/wireai-rn.git
cd wireai-rn/packages/core
yarn install
yarn build      # ESM + CJS + .d.ts (tsup)
yarn test       # jest — run the test suites
yarn typecheck

See CONTRIBUTING.md for pull request guidelines.


Sponsors

Wire RN is open source and free. Its development is backed by:

  • AI Mobile Launcher — the AI-native React Native boilerplate. Ship an AI mobile app with local + cloud LLMs, generative UI, and a paywall already wired.
  • CasaInnov — AI-native mobile product studio. Done-for-you AI mobile builds and fractional CTO work.

Want your product here? Open an issue or reach out at getwireai.com.


License

MIT — see LICENSE for details.


Created by Malik Chohra — React Native engineer, building AI-native mobile. Website · Newsletter · X / @malik_chohra

Sponsored by AI Mobile Launcher and CasaInnov.