@assistant-ui/mcp-docs-server 0.1.22 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.docs/organized/code-examples/waterfall.md +801 -0
  2. package/.docs/organized/code-examples/with-ag-ui.md +38 -26
  3. package/.docs/organized/code-examples/with-ai-sdk-v6.md +38 -28
  4. package/.docs/organized/code-examples/with-artifacts.md +467 -0
  5. package/.docs/organized/code-examples/with-assistant-transport.md +31 -24
  6. package/.docs/organized/code-examples/with-chain-of-thought.md +41 -32
  7. package/.docs/organized/code-examples/with-cloud-standalone.md +675 -0
  8. package/.docs/organized/code-examples/with-cloud.md +34 -27
  9. package/.docs/organized/code-examples/with-custom-thread-list.md +34 -27
  10. package/.docs/organized/code-examples/with-elevenlabs-scribe.md +41 -30
  11. package/.docs/organized/code-examples/with-expo.md +2031 -0
  12. package/.docs/organized/code-examples/with-external-store.md +32 -25
  13. package/.docs/organized/code-examples/with-ffmpeg.md +31 -27
  14. package/.docs/organized/code-examples/with-langgraph.md +96 -38
  15. package/.docs/organized/code-examples/with-parent-id-grouping.md +32 -25
  16. package/.docs/organized/code-examples/with-react-hook-form.md +63 -58
  17. package/.docs/organized/code-examples/with-react-router.md +38 -30
  18. package/.docs/organized/code-examples/with-store.md +16 -24
  19. package/.docs/organized/code-examples/with-tanstack.md +36 -26
  20. package/.docs/organized/code-examples/with-tap-runtime.md +10 -24
  21. package/.docs/raw/docs/(docs)/cli.mdx +13 -6
  22. package/.docs/raw/docs/(docs)/guides/attachments.mdx +26 -3
  23. package/.docs/raw/docs/(docs)/guides/chain-of-thought.mdx +5 -5
  24. package/.docs/raw/docs/(docs)/guides/context-api.mdx +53 -52
  25. package/.docs/raw/docs/(docs)/guides/dictation.mdx +0 -2
  26. package/.docs/raw/docs/(docs)/guides/message-timing.mdx +169 -0
  27. package/.docs/raw/docs/(docs)/guides/quoting.mdx +327 -0
  28. package/.docs/raw/docs/(docs)/guides/speech.mdx +0 -1
  29. package/.docs/raw/docs/(docs)/index.mdx +12 -2
  30. package/.docs/raw/docs/(docs)/installation.mdx +8 -2
  31. package/.docs/raw/docs/(docs)/llm.mdx +9 -7
  32. package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar-more.mdx +1 -1
  33. package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +2 -2
  34. package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-if.mdx +27 -27
  35. package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +60 -0
  36. package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +78 -4
  37. package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +32 -0
  38. package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +61 -0
  39. package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +1 -1
  40. package/.docs/raw/docs/(reference)/legacy/styled/assistant-modal.mdx +1 -6
  41. package/.docs/raw/docs/(reference)/legacy/styled/decomposition.mdx +2 -2
  42. package/.docs/raw/docs/(reference)/legacy/styled/markdown.mdx +1 -6
  43. package/.docs/raw/docs/(reference)/legacy/styled/thread.mdx +1 -5
  44. package/.docs/raw/docs/(reference)/migrations/v0-12.mdx +17 -17
  45. package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +205 -0
  46. package/.docs/raw/docs/cloud/ai-sdk.mdx +292 -0
  47. package/.docs/raw/docs/cloud/authorization.mdx +178 -79
  48. package/.docs/raw/docs/cloud/{persistence/langgraph.mdx → langgraph.mdx} +2 -2
  49. package/.docs/raw/docs/cloud/overview.mdx +29 -39
  50. package/.docs/raw/docs/react-native/adapters.mdx +118 -0
  51. package/.docs/raw/docs/react-native/custom-backend.mdx +210 -0
  52. package/.docs/raw/docs/react-native/hooks.mdx +364 -0
  53. package/.docs/raw/docs/react-native/index.mdx +332 -0
  54. package/.docs/raw/docs/react-native/primitives.mdx +653 -0
  55. package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +7 -15
  56. package/.docs/raw/docs/runtimes/assistant-transport.mdx +103 -0
  57. package/.docs/raw/docs/runtimes/custom/external-store.mdx +25 -2
  58. package/.docs/raw/docs/runtimes/data-stream.mdx +1 -3
  59. package/.docs/raw/docs/runtimes/langgraph/index.mdx +113 -9
  60. package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +1 -4
  61. package/.docs/raw/docs/ui/attachment.mdx +4 -2
  62. package/.docs/raw/docs/ui/message-timing.mdx +92 -0
  63. package/.docs/raw/docs/ui/part-grouping.mdx +1 -1
  64. package/.docs/raw/docs/ui/reasoning.mdx +4 -4
  65. package/.docs/raw/docs/ui/scrollbar.mdx +2 -2
  66. package/.docs/raw/docs/ui/syntax-highlighting.mdx +55 -50
  67. package/.docs/raw/docs/ui/thread.mdx +16 -9
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/package.json +3 -3
  71. package/src/tools/tests/integration.test.ts +2 -2
  72. package/src/tools/tests/json-parsing.test.ts +1 -1
  73. package/src/tools/tests/mcp-protocol.test.ts +1 -3
  74. package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +0 -108
@@ -0,0 +1,364 @@
1
+ ---
2
+ title: Hooks
3
+ description: Reactive hooks for accessing runtime state in React Native.
4
+ ---
5
+
6
+ All hooks support an optional **selector** function for fine-grained re-renders. Without a selector, the component re-renders on every state change. With a selector, it only re-renders when the selected value changes (shallow equality).
7
+
8
+ ```tsx
9
+ // Re-renders on every thread state change
10
+ const thread = useThread();
11
+
12
+ // Re-renders only when isRunning changes
13
+ const isRunning = useThread((s) => s.isRunning);
14
+ ```
15
+
16
+ ## State Hooks
17
+
18
+ ### useThread
19
+
20
+ Access thread state.
21
+
22
+ ```tsx
23
+ import { useThread } from "@assistant-ui/react-native";
24
+
25
+ const messages = useThread((s) => s.messages);
26
+ const isRunning = useThread((s) => s.isRunning);
27
+ ```
28
+
29
+ **ThreadState fields:**
30
+
31
+ | Field | Type | Description |
32
+ |-------|------|-------------|
33
+ | `messages` | `ThreadMessage[]` | All messages in the thread |
34
+ | `isRunning` | `boolean` | Whether the model is generating |
35
+ | `isDisabled` | `boolean` | Whether the thread is disabled |
36
+ | `capabilities` | `RuntimeCapabilities` | What actions are supported |
37
+
38
+ ### useComposer
39
+
40
+ Access composer state.
41
+
42
+ ```tsx
43
+ import { useComposer } from "@assistant-ui/react-native";
44
+
45
+ const text = useComposer((s) => s.text);
46
+ const isEmpty = useComposer((s) => s.isEmpty);
47
+ ```
48
+
49
+ **ComposerState fields:**
50
+
51
+ | Field | Type | Description |
52
+ |-------|------|-------------|
53
+ | `text` | `string` | Current input text |
54
+ | `isEmpty` | `boolean` | Whether the input is empty |
55
+ | `attachments` | `Attachment[]` | Current attachments |
56
+ | `canCancel` | `boolean` | Whether a run can be cancelled |
57
+
58
+ ### useMessage
59
+
60
+ Access the current message state. Must be used inside a `MessageProvider` or a `renderMessage` / `renderItem` callback.
61
+
62
+ ```tsx
63
+ import { useMessage } from "@assistant-ui/react-native";
64
+
65
+ const role = useMessage((s) => s.role);
66
+ const isLast = useMessage((s) => s.isLast);
67
+ ```
68
+
69
+ **MessageState fields:**
70
+
71
+ | Field | Type | Description |
72
+ |-------|------|-------------|
73
+ | `message` | `ThreadMessage` | The message object |
74
+ | `role` | `MessageRole` | `"user"`, `"assistant"`, or `"system"` |
75
+ | `isLast` | `boolean` | Whether this is the last message |
76
+ | `branchNumber` | `number` | Current branch index |
77
+ | `branchCount` | `number` | Total number of branches |
78
+
79
+ ### useContentPart
80
+
81
+ Access a specific content part by index within a message.
82
+
83
+ ```tsx
84
+ import { useContentPart } from "@assistant-ui/react-native";
85
+
86
+ const part = useContentPart(0); // first content part
87
+ ```
88
+
89
+ ### useThreadList
90
+
91
+ Access the thread list state.
92
+
93
+ ```tsx
94
+ import { useThreadList } from "@assistant-ui/react-native";
95
+
96
+ const { threadIds, mainThreadId, threadItems } = useThreadList();
97
+
98
+ // With selector
99
+ const mainThreadId = useThreadList((s) => s.mainThreadId);
100
+ ```
101
+
102
+ **ThreadListState fields:**
103
+
104
+ | Field | Type | Description |
105
+ |-------|------|-------------|
106
+ | `threadIds` | `string[]` | All thread IDs |
107
+ | `mainThreadId` | `string` | Currently active thread ID |
108
+ | `threadItems` | `Record<string, ThreadListItemState>` | Thread metadata (title, etc.) |
109
+
110
+ ## Runtime Hooks
111
+
112
+ ### useAssistantRuntime
113
+
114
+ Get the `AssistantRuntime` from context. This is the top-level runtime with access to thread management.
115
+
116
+ ```tsx
117
+ import { useAssistantRuntime } from "@assistant-ui/react-native";
118
+
119
+ const runtime = useAssistantRuntime();
120
+
121
+ // Switch threads
122
+ runtime.threads.switchToThread(threadId);
123
+ runtime.threads.switchToNewThread();
124
+
125
+ // Thread management
126
+ runtime.threads.rename(threadId, "New Title");
127
+ runtime.threads.delete(threadId);
128
+
129
+ // Access the current thread runtime
130
+ runtime.thread; // ThreadRuntime
131
+ ```
132
+
133
+ ### useThreadRuntime
134
+
135
+ Get the `ThreadRuntime` for the current thread.
136
+
137
+ ```tsx
138
+ import { useThreadRuntime } from "@assistant-ui/react-native";
139
+
140
+ const threadRuntime = useThreadRuntime();
141
+
142
+ threadRuntime.cancelRun();
143
+ threadRuntime.appendMessage(message);
144
+ ```
145
+
146
+ ### useComposerRuntime
147
+
148
+ Get the `ComposerRuntime` for the current composer.
149
+
150
+ ```tsx
151
+ import { useComposerRuntime } from "@assistant-ui/react-native";
152
+
153
+ const composerRuntime = useComposerRuntime();
154
+
155
+ composerRuntime.setText("Hello");
156
+ composerRuntime.send();
157
+ ```
158
+
159
+ ### useMessageRuntime
160
+
161
+ Get the `MessageRuntime` for the current message.
162
+
163
+ ```tsx
164
+ import { useMessageRuntime } from "@assistant-ui/react-native";
165
+
166
+ const messageRuntime = useMessageRuntime();
167
+
168
+ messageRuntime.reload();
169
+ messageRuntime.switchToBranch({ position: "next" });
170
+ ```
171
+
172
+ ### useLocalRuntime
173
+
174
+ Create an `AssistantRuntime` with a `ChatModelAdapter`. Optionally pass `storage` for persistence.
175
+
176
+ ```tsx
177
+ import { useLocalRuntime } from "@assistant-ui/react-native";
178
+ import AsyncStorage from "@react-native-async-storage/async-storage";
179
+
180
+ const runtime = useLocalRuntime(chatModel, {
181
+ storage: AsyncStorage,
182
+ titleGenerator: createSimpleTitleAdapter(),
183
+ });
184
+ ```
185
+
186
+ | Option | Type | Description |
187
+ |--------|------|-------------|
188
+ | `initialMessages` | `ThreadMessageLike[]` | Messages to pre-populate |
189
+ | `storage` | `AsyncStorageLike` | Thread and message persistence |
190
+ | `storagePrefix` | `string` | Key prefix for storage (default `"@assistant-ui:"`) |
191
+ | `titleGenerator` | `TitleGenerationAdapter` | Auto-generate thread titles |
192
+
193
+ ## Primitive Hooks
194
+
195
+ Low-level hooks for building custom components.
196
+
197
+ ### useThreadMessages
198
+
199
+ ```tsx
200
+ import { useThreadMessages } from "@assistant-ui/react-native";
201
+
202
+ const messages = useThreadMessages(); // ThreadMessage[]
203
+ ```
204
+
205
+ ### useThreadIsRunning
206
+
207
+ ```tsx
208
+ import { useThreadIsRunning } from "@assistant-ui/react-native";
209
+
210
+ const isRunning = useThreadIsRunning(); // boolean
211
+ ```
212
+
213
+ ### useThreadIsEmpty
214
+
215
+ ```tsx
216
+ import { useThreadIsEmpty } from "@assistant-ui/react-native";
217
+
218
+ const isEmpty = useThreadIsEmpty(); // boolean
219
+ ```
220
+
221
+ ### useComposerSend
222
+
223
+ ```tsx
224
+ import { useComposerSend } from "@assistant-ui/react-native";
225
+
226
+ const { send, canSend } = useComposerSend();
227
+ ```
228
+
229
+ ### useComposerCancel
230
+
231
+ ```tsx
232
+ import { useComposerCancel } from "@assistant-ui/react-native";
233
+
234
+ const { cancel, canCancel } = useComposerCancel();
235
+ ```
236
+
237
+ ### useComposerAddAttachment
238
+
239
+ Add an attachment to the composer. Returns `{ addAttachment }` — call it with a `File` or `CreateAttachment` object. Pair with your own file picker (e.g. `expo-image-picker`).
240
+
241
+ ```tsx
242
+ import { useComposerAddAttachment } from "@assistant-ui/react-native";
243
+
244
+ const { addAttachment } = useComposerAddAttachment();
245
+
246
+ // With expo-image-picker
247
+ const pickImage = async () => {
248
+ const result = await ImagePicker.launchImageLibraryAsync({
249
+ base64: true,
250
+ quality: 0.8,
251
+ });
252
+ if (result.canceled) return;
253
+
254
+ for (const asset of result.assets) {
255
+ await addAttachment({
256
+ name: asset.fileName ?? "image.jpg",
257
+ type: "image",
258
+ content: [
259
+ { type: "image", image: `data:image/jpeg;base64,${asset.base64}` },
260
+ ],
261
+ });
262
+ }
263
+ };
264
+ ```
265
+
266
+ <Callout type="info">
267
+ You must configure an `AttachmentAdapter` (e.g. `SimpleImageAttachmentAdapter`) in your runtime options for attachments to work.
268
+ </Callout>
269
+
270
+ ### useMessageReload
271
+
272
+ ```tsx
273
+ import { useMessageReload } from "@assistant-ui/react-native";
274
+
275
+ const { reload, canReload } = useMessageReload();
276
+ ```
277
+
278
+ ### useMessageBranching
279
+
280
+ ```tsx
281
+ import { useMessageBranching } from "@assistant-ui/react-native";
282
+
283
+ const { branchNumber, branchCount, goToPrev, goToNext } =
284
+ useMessageBranching();
285
+ ```
286
+
287
+ ## Model Context Hooks
288
+
289
+ ### useAssistantTool
290
+
291
+ Register a tool with an optional UI renderer. The tool definition is forwarded to the model, and when the model calls it, the `execute` function runs and the `render` component displays the result.
292
+
293
+ ```tsx
294
+ import { useAssistantTool } from "@assistant-ui/react-native";
295
+
296
+ useAssistantTool({
297
+ toolName: "get_weather",
298
+ description: "Get the current weather for a city",
299
+ parameters: {
300
+ type: "object",
301
+ properties: {
302
+ city: { type: "string" },
303
+ },
304
+ required: ["city"],
305
+ },
306
+ execute: async ({ city }) => {
307
+ const res = await fetch(`https://api.weather.example/${city}`);
308
+ return res.json();
309
+ },
310
+ render: ({ args, result }) => (
311
+ <View>
312
+ <Text>{args.city}: {result?.temperature}°F</Text>
313
+ </View>
314
+ ),
315
+ });
316
+ ```
317
+
318
+ ### useAssistantToolUI
319
+
320
+ Register only a UI renderer for a tool (without tool definition or execute function).
321
+
322
+ ```tsx
323
+ import { useAssistantToolUI } from "@assistant-ui/react-native";
324
+
325
+ useAssistantToolUI({
326
+ toolName: "get_weather",
327
+ render: ({ args, result, status }) => (
328
+ <View>
329
+ {status?.type === "running"
330
+ ? <Text>Loading weather for {args.city}...</Text>
331
+ : <Text>{args.city}: {result?.temperature}°F</Text>}
332
+ </View>
333
+ ),
334
+ });
335
+ ```
336
+
337
+ ### useAssistantInstructions
338
+
339
+ Register system instructions in the model context.
340
+
341
+ ```tsx
342
+ import { useAssistantInstructions } from "@assistant-ui/react-native";
343
+
344
+ useAssistantInstructions("You are a helpful weather assistant.");
345
+ ```
346
+
347
+ ### makeAssistantTool
348
+
349
+ Create a component that registers a tool when mounted. Useful for declarative tool registration.
350
+
351
+ ```tsx
352
+ import { makeAssistantTool } from "@assistant-ui/react-native";
353
+
354
+ const WeatherTool = makeAssistantTool({
355
+ toolName: "get_weather",
356
+ description: "Get weather",
357
+ parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
358
+ execute: async ({ city }) => ({ temperature: 72 }),
359
+ render: ({ args, result }) => <Text>{args.city}: {result?.temperature}°F</Text>,
360
+ });
361
+
362
+ // Mount inside AssistantProvider to register
363
+ <WeatherTool />
364
+ ```
@@ -0,0 +1,332 @@
1
+ ---
2
+ title: Getting Started
3
+ description: Build AI chat interfaces for iOS and Android with @assistant-ui/react-native.
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ `@assistant-ui/react-native` brings assistant-ui to React Native. It provides composable primitives, reactive hooks, and a local runtime — the same layered architecture as the web package, built on native components (`View`, `TextInput`, `FlatList`, `Pressable`).
9
+
10
+ **Key features:**
11
+
12
+ - **Primitives** — `Thread`, `Composer`, `Message`, `ThreadList`, `ThreadListItem`, `ChainOfThought`, `Suggestion`, `ActionBar`, `BranchPicker`, `Attachment` components that compose with standard React Native props
13
+ - **Reactive state** — `useAuiState` with selector support for fine-grained re-renders
14
+ - **Local runtime** — `useLocalRuntime` with pluggable `ChatModelAdapter` for any LLM API
15
+ - **Thread management** — Multi-thread support with create, switch, rename, delete
16
+ - **Tool system** — `useAssistantTool`, `makeAssistantTool` for registering tools with custom UI renderers
17
+ - **Attachments** — Image and file attachments with `useComposerAddAttachment` and attachment primitives
18
+
19
+ <Callout type="info">
20
+ `@assistant-ui/react-native` shares its runtime core with `@assistant-ui/react` via `@assistant-ui/core`. The type system, state management, and runtime logic are identical — only the UI layer differs.
21
+ </Callout>
22
+
23
+ ## Getting Started
24
+
25
+ This guide uses [Expo](https://expo.dev) with the OpenAI API. You can substitute any LLM provider.
26
+
27
+ <Steps>
28
+ <Step>
29
+
30
+ ### Create an Expo project
31
+
32
+ ```sh
33
+ npx create-expo-app@latest my-chat-app
34
+ cd my-chat-app
35
+ ```
36
+
37
+ </Step>
38
+ <Step>
39
+
40
+ ### Install dependencies
41
+
42
+ ```sh
43
+ npx expo install @assistant-ui/react-native
44
+ ```
45
+
46
+ </Step>
47
+ <Step>
48
+
49
+ ### Create a ChatModelAdapter
50
+
51
+ The adapter connects your LLM API to the runtime. Here's an example for the OpenAI chat completions API with streaming:
52
+
53
+ ```tsx title="adapters/openai-chat-adapter.ts"
54
+ import type {
55
+ ChatModelAdapter,
56
+ ChatModelRunResult,
57
+ } from "@assistant-ui/react-native";
58
+
59
+ type OpenAIModelConfig = {
60
+ apiKey: string;
61
+ model?: string;
62
+ baseURL?: string;
63
+ fetch?: typeof globalThis.fetch;
64
+ };
65
+
66
+ export function createOpenAIChatModelAdapter(
67
+ config: OpenAIModelConfig,
68
+ ): ChatModelAdapter {
69
+ const {
70
+ apiKey,
71
+ model = "gpt-4o-mini",
72
+ baseURL = "https://api.openai.com/v1",
73
+ fetch: customFetch = globalThis.fetch,
74
+ } = config;
75
+
76
+ return {
77
+ async *run({ messages, abortSignal }) {
78
+ const openAIMessages = messages
79
+ .filter((m) => m.role !== "system")
80
+ .map((m) => ({
81
+ role: m.role as "user" | "assistant",
82
+ content: m.content
83
+ .filter((p) => p.type === "text")
84
+ .map((p) => ("text" in p ? p.text : ""))
85
+ .join("\n"),
86
+ }));
87
+
88
+ const response = await customFetch(
89
+ `${baseURL}/chat/completions`,
90
+ {
91
+ method: "POST",
92
+ headers: {
93
+ "Content-Type": "application/json",
94
+ Authorization: `Bearer ${apiKey}`,
95
+ },
96
+ body: JSON.stringify({
97
+ model,
98
+ messages: openAIMessages,
99
+ stream: true,
100
+ }),
101
+ signal: abortSignal,
102
+ },
103
+ );
104
+
105
+ if (!response.ok) {
106
+ const body = await response.text().catch(() => "");
107
+ throw new Error(
108
+ `OpenAI API error: ${response.status} ${body}`,
109
+ );
110
+ }
111
+
112
+ const reader = response.body?.getReader();
113
+ if (!reader) {
114
+ const json = await response.json();
115
+ const text = json.choices?.[0]?.message?.content ?? "";
116
+ yield {
117
+ content: [{ type: "text" as const, text }],
118
+ } satisfies ChatModelRunResult;
119
+ return;
120
+ }
121
+
122
+ const decoder = new TextDecoder();
123
+ let fullText = "";
124
+
125
+ try {
126
+ while (true) {
127
+ const { done, value } = await reader.read();
128
+ if (done) break;
129
+ const chunk = decoder.decode(value, { stream: true });
130
+ for (const line of chunk.split("\n")) {
131
+ if (!line.startsWith("data: ")) continue;
132
+ const data = line.slice(6);
133
+ if (data === "[DONE]") continue;
134
+ try {
135
+ const parsed = JSON.parse(data);
136
+ const content =
137
+ parsed.choices?.[0]?.delta?.content ?? "";
138
+ fullText += content;
139
+ yield {
140
+ content: [
141
+ { type: "text" as const, text: fullText },
142
+ ],
143
+ } satisfies ChatModelRunResult;
144
+ } catch {
145
+ // skip invalid JSON
146
+ }
147
+ }
148
+ }
149
+ } finally {
150
+ reader.releaseLock();
151
+ }
152
+ },
153
+ };
154
+ }
155
+ ```
156
+
157
+ <Callout type="tip">
158
+ On Expo, import `fetch` from `expo/fetch` for streaming support and pass it as the `fetch` option.
159
+ </Callout>
160
+
161
+ </Step>
162
+ <Step>
163
+
164
+ ### Set up the runtime
165
+
166
+ ```tsx title="hooks/use-app-runtime.ts"
167
+ import { useMemo } from "react";
168
+ import { fetch } from "expo/fetch";
169
+ import { useLocalRuntime } from "@assistant-ui/react-native";
170
+ import { createOpenAIChatModelAdapter } from "@/adapters/openai-chat-adapter";
171
+
172
+ export function useAppRuntime() {
173
+ const chatModel = useMemo(
174
+ () =>
175
+ createOpenAIChatModelAdapter({
176
+ apiKey: process.env.EXPO_PUBLIC_OPENAI_API_KEY ?? "",
177
+ model: "gpt-4o-mini",
178
+ fetch,
179
+ }),
180
+ [],
181
+ );
182
+
183
+ return useLocalRuntime(chatModel);
184
+ }
185
+ ```
186
+
187
+ </Step>
188
+ <Step>
189
+
190
+ ### Build the UI
191
+
192
+ Wrap your app with `AssistantProvider` — thread and composer contexts are set up automatically:
193
+
194
+ ```tsx title="app/index.tsx"
195
+ import {
196
+ AssistantProvider,
197
+ useAuiState,
198
+ useAui,
199
+ } from "@assistant-ui/react-native";
200
+ import type { ThreadMessage } from "@assistant-ui/react-native";
201
+ import {
202
+ View,
203
+ Text,
204
+ TextInput,
205
+ FlatList,
206
+ Pressable,
207
+ KeyboardAvoidingView,
208
+ Platform,
209
+ } from "react-native";
210
+ import { useAppRuntime } from "@/hooks/use-app-runtime";
211
+
212
+ function MessageBubble({ message }: { message: ThreadMessage }) {
213
+ const isUser = message.role === "user";
214
+ const text = message.content
215
+ .filter((p) => p.type === "text")
216
+ .map((p) => ("text" in p ? p.text : ""))
217
+ .join("\n");
218
+
219
+ return (
220
+ <View
221
+ style={{
222
+ alignSelf: isUser ? "flex-end" : "flex-start",
223
+ backgroundColor: isUser ? "#007aff" : "#f0f0f0",
224
+ borderRadius: 16,
225
+ padding: 12,
226
+ marginVertical: 4,
227
+ marginHorizontal: 16,
228
+ maxWidth: "80%",
229
+ }}
230
+ >
231
+ <Text style={{ color: isUser ? "#fff" : "#000" }}>{text}</Text>
232
+ </View>
233
+ );
234
+ }
235
+
236
+ function Composer() {
237
+ const aui = useAui();
238
+ const text = useAuiState((s) => s.composer.text);
239
+ const isEmpty = useAuiState((s) => s.composer.isEmpty);
240
+
241
+ return (
242
+ <View
243
+ style={{
244
+ flexDirection: "row",
245
+ padding: 12,
246
+ alignItems: "flex-end",
247
+ }}
248
+ >
249
+ <TextInput
250
+ value={text}
251
+ onChangeText={(t) => aui.composer().setText(t)}
252
+ placeholder="Message..."
253
+ multiline
254
+ style={{
255
+ flex: 1,
256
+ borderWidth: 1,
257
+ borderColor: "#ddd",
258
+ borderRadius: 20,
259
+ paddingHorizontal: 16,
260
+ paddingVertical: 10,
261
+ maxHeight: 120,
262
+ }}
263
+ />
264
+ <Pressable
265
+ onPress={() => aui.composer().send()}
266
+ disabled={isEmpty}
267
+ style={{
268
+ marginLeft: 8,
269
+ backgroundColor: !isEmpty ? "#007aff" : "#ccc",
270
+ borderRadius: 20,
271
+ width: 36,
272
+ height: 36,
273
+ justifyContent: "center",
274
+ alignItems: "center",
275
+ }}
276
+ >
277
+ <Text style={{ color: "#fff", fontWeight: "bold" }}>↑</Text>
278
+ </Pressable>
279
+ </View>
280
+ );
281
+ }
282
+
283
+ function ChatScreen() {
284
+ const messages = useAuiState(
285
+ (s) => s.thread.messages,
286
+ ) as ThreadMessage[];
287
+
288
+ return (
289
+ <KeyboardAvoidingView
290
+ style={{ flex: 1 }}
291
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
292
+ >
293
+ <FlatList
294
+ data={messages}
295
+ keyExtractor={(m) => m.id}
296
+ renderItem={({ item }) => <MessageBubble message={item} />}
297
+ />
298
+ <Composer />
299
+ </KeyboardAvoidingView>
300
+ );
301
+ }
302
+
303
+ export default function App() {
304
+ const runtime = useAppRuntime();
305
+
306
+ return (
307
+ <AssistantProvider runtime={runtime}>
308
+ <ChatScreen />
309
+ </AssistantProvider>
310
+ );
311
+ }
312
+ ```
313
+
314
+ </Step>
315
+ </Steps>
316
+
317
+ ## Architecture
318
+
319
+ ```
320
+ useLocalRuntime(chatModel, options?)
321
+ └─ AssistantProvider
322
+ ├─ useAuiState((s) => s.thread) → thread state (messages, isRunning, …)
323
+ ├─ useAuiState((s) => s.composer) → composer state (text, isEmpty, …)
324
+ ├─ useAuiState((s) => s.message) → single message state (inside renderItem)
325
+ └─ Primitives → ThreadRoot, ComposerInput, MessageContent, …
326
+ ```
327
+
328
+ The runtime core is shared with `@assistant-ui/react` — only the UI primitives are React Native-specific.
329
+
330
+ ## Example
331
+
332
+ For a complete Expo example with drawer navigation, thread list, and styled chat UI, see the [`with-expo` example](https://github.com/assistant-ui/assistant-ui/tree/main/examples/with-expo).