@assistant-ui/mcp-docs-server 0.1.24 → 0.1.25

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 (135) hide show
  1. package/.docs/organized/code-examples/waterfall.md +4 -2
  2. package/.docs/organized/code-examples/with-a2a.md +676 -0
  3. package/.docs/organized/code-examples/with-ag-ui.md +5 -6
  4. package/.docs/organized/code-examples/with-ai-sdk-v6.md +27 -15
  5. package/.docs/organized/code-examples/with-artifacts.md +4 -4
  6. package/.docs/organized/code-examples/with-assistant-transport.md +2 -2
  7. package/.docs/organized/code-examples/with-chain-of-thought.md +33 -25
  8. package/.docs/organized/code-examples/with-cloud-standalone.md +9 -6
  9. package/.docs/organized/code-examples/with-cloud.md +4 -4
  10. package/.docs/organized/code-examples/with-custom-thread-list.md +6 -6
  11. package/.docs/organized/code-examples/with-elevenlabs-scribe.md +7 -7
  12. package/.docs/organized/code-examples/with-expo.md +565 -514
  13. package/.docs/organized/code-examples/with-external-store.md +2 -2
  14. package/.docs/organized/code-examples/with-ffmpeg.md +4 -4
  15. package/.docs/organized/code-examples/with-google-adk.md +353 -0
  16. package/.docs/organized/code-examples/with-heat-graph.md +304 -0
  17. package/.docs/organized/code-examples/with-langgraph.md +24 -22
  18. package/.docs/organized/code-examples/with-parent-id-grouping.md +3 -3
  19. package/.docs/organized/code-examples/with-react-hook-form.md +4 -4
  20. package/.docs/organized/code-examples/with-react-ink.md +265 -0
  21. package/.docs/organized/code-examples/with-react-router.md +5 -5
  22. package/.docs/organized/code-examples/with-store.md +28 -17
  23. package/.docs/organized/code-examples/with-tanstack.md +7 -7
  24. package/.docs/organized/code-examples/with-tap-runtime.md +5 -3
  25. package/.docs/raw/blog/2025-01-31-changelog/index.mdx +1 -1
  26. package/.docs/raw/blog/2026-03-launch-week/index.mdx +227 -0
  27. package/.docs/raw/docs/(docs)/architecture.mdx +1 -1
  28. package/.docs/raw/docs/(docs)/cli.mdx +14 -9
  29. package/.docs/raw/docs/(docs)/copilots/make-assistant-tool-ui.mdx +8 -3
  30. package/.docs/raw/docs/(docs)/copilots/make-assistant-tool.mdx +5 -1
  31. package/.docs/raw/docs/(docs)/copilots/{make-assistant-readable.mdx → make-assistant-visible.mdx} +14 -5
  32. package/.docs/raw/docs/(docs)/copilots/model-context.mdx +11 -11
  33. package/.docs/raw/docs/(docs)/copilots/motivation.mdx +2 -2
  34. package/.docs/raw/docs/(docs)/devtools.mdx +3 -2
  35. package/.docs/raw/docs/(docs)/guides/attachments.mdx +9 -11
  36. package/.docs/raw/docs/(docs)/guides/branching.mdx +11 -6
  37. package/.docs/raw/docs/(docs)/guides/chain-of-thought.mdx +18 -16
  38. package/.docs/raw/docs/(docs)/guides/context-api.mdx +81 -43
  39. package/.docs/raw/docs/(docs)/guides/dictation.mdx +5 -5
  40. package/.docs/raw/docs/(docs)/guides/editing.mdx +16 -7
  41. package/.docs/raw/docs/(docs)/guides/latex.mdx +3 -0
  42. package/.docs/raw/docs/(docs)/guides/message-timing.mdx +2 -1
  43. package/.docs/raw/docs/(docs)/guides/multi-agent.mdx +173 -0
  44. package/.docs/raw/docs/(docs)/guides/quoting.mdx +55 -206
  45. package/.docs/raw/docs/(docs)/guides/speech.mdx +1 -4
  46. package/.docs/raw/docs/(docs)/guides/suggestions.mdx +9 -15
  47. package/.docs/raw/docs/(docs)/guides/tool-ui.mdx +17 -7
  48. package/.docs/raw/docs/(docs)/guides/tools.mdx +24 -9
  49. package/.docs/raw/docs/(docs)/index.mdx +3 -3
  50. package/.docs/raw/docs/(docs)/installation.mdx +69 -46
  51. package/.docs/raw/docs/(reference)/api-reference/context-providers/text-message-part-provider.mdx +20 -6
  52. package/.docs/raw/docs/(reference)/api-reference/integrations/react-data-stream.mdx +24 -4
  53. package/.docs/raw/docs/(reference)/api-reference/integrations/react-hook-form.mdx +1 -1
  54. package/.docs/raw/docs/(reference)/api-reference/integrations/vercel-ai-sdk.mdx +20 -19
  55. package/.docs/raw/docs/(reference)/api-reference/overview.mdx +28 -53
  56. package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +4 -4
  57. package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-modal.mdx +7 -1
  58. package/.docs/raw/docs/(reference)/api-reference/primitives/attachment.mdx +20 -14
  59. package/.docs/raw/docs/(reference)/api-reference/primitives/branch-picker.mdx +1 -1
  60. package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +99 -45
  61. package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +52 -40
  62. package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +343 -23
  63. package/.docs/raw/docs/(reference)/api-reference/primitives/suggestion.mdx +4 -6
  64. package/.docs/raw/docs/(reference)/api-reference/primitives/thread-list-item.mdx +4 -2
  65. package/.docs/raw/docs/(reference)/api-reference/primitives/thread-list.mdx +3 -5
  66. package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +169 -22
  67. package/.docs/raw/docs/(reference)/api-reference/runtimes/assistant-runtime.mdx +14 -4
  68. package/.docs/raw/docs/(reference)/api-reference/runtimes/attachment-runtime.mdx +15 -26
  69. package/.docs/raw/docs/(reference)/api-reference/runtimes/composer-runtime.mdx +39 -21
  70. package/.docs/raw/docs/(reference)/api-reference/runtimes/message-part-runtime.mdx +33 -9
  71. package/.docs/raw/docs/(reference)/api-reference/runtimes/message-runtime.mdx +48 -21
  72. package/.docs/raw/docs/(reference)/api-reference/runtimes/thread-list-item-runtime.mdx +36 -7
  73. package/.docs/raw/docs/(reference)/api-reference/runtimes/thread-list-runtime.mdx +30 -10
  74. package/.docs/raw/docs/(reference)/api-reference/runtimes/thread-runtime.mdx +12 -10
  75. package/.docs/raw/docs/(reference)/migrations/deprecation-policy.mdx +1 -1
  76. package/.docs/raw/docs/(reference)/migrations/react-langgraph-v0-7.mdx +9 -4
  77. package/.docs/raw/docs/(reference)/migrations/v0-11.mdx +7 -5
  78. package/.docs/raw/docs/(reference)/migrations/v0-12.mdx +9 -7
  79. package/.docs/raw/docs/(reference)/migrations/v0-14.mdx +159 -0
  80. package/.docs/raw/docs/(reference)/react-compatibility.mdx +5 -134
  81. package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +84 -6
  82. package/.docs/raw/docs/cloud/ai-sdk.mdx +14 -4
  83. package/.docs/raw/docs/cloud/langgraph.mdx +13 -3
  84. package/.docs/raw/docs/ink/adapters.mdx +41 -0
  85. package/.docs/raw/docs/ink/custom-backend.mdx +203 -0
  86. package/.docs/raw/docs/ink/hooks.mdx +448 -0
  87. package/.docs/raw/docs/ink/index.mdx +239 -0
  88. package/.docs/raw/docs/ink/migration.mdx +140 -0
  89. package/.docs/raw/docs/ink/primitives.mdx +699 -0
  90. package/.docs/raw/docs/react-native/adapters.mdx +63 -87
  91. package/.docs/raw/docs/react-native/custom-backend.mdx +11 -14
  92. package/.docs/raw/docs/react-native/hooks.mdx +214 -232
  93. package/.docs/raw/docs/react-native/index.mdx +118 -159
  94. package/.docs/raw/docs/react-native/migration.mdx +144 -0
  95. package/.docs/raw/docs/react-native/primitives.mdx +431 -302
  96. package/.docs/raw/docs/runtimes/a2a/index.mdx +294 -0
  97. package/.docs/raw/docs/runtimes/ai-sdk/v4-legacy.mdx +9 -9
  98. package/.docs/raw/docs/runtimes/ai-sdk/v5-legacy.mdx +14 -3
  99. package/.docs/raw/docs/runtimes/assistant-transport.mdx +59 -25
  100. package/.docs/raw/docs/runtimes/custom/custom-thread-list.mdx +13 -6
  101. package/.docs/raw/docs/runtimes/custom/external-store.mdx +138 -38
  102. package/.docs/raw/docs/runtimes/custom/local.mdx +184 -42
  103. package/.docs/raw/docs/runtimes/data-stream.mdx +92 -19
  104. package/.docs/raw/docs/runtimes/google-adk/index.mdx +624 -0
  105. package/.docs/raw/docs/runtimes/helicone.mdx +6 -6
  106. package/.docs/raw/docs/runtimes/langgraph/index.mdx +38 -27
  107. package/.docs/raw/docs/runtimes/langgraph/tutorial/introduction.mdx +1 -1
  108. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +15 -20
  109. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +7 -11
  110. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +8 -11
  111. package/.docs/raw/docs/runtimes/langserve.mdx +6 -7
  112. package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +18 -3
  113. package/.docs/raw/docs/ui/file.mdx +5 -4
  114. package/.docs/raw/docs/ui/image.mdx +5 -4
  115. package/.docs/raw/docs/ui/markdown.mdx +3 -1
  116. package/.docs/raw/docs/ui/model-selector.mdx +8 -8
  117. package/.docs/raw/docs/ui/part-grouping.mdx +7 -10
  118. package/.docs/raw/docs/ui/quote.mdx +210 -0
  119. package/.docs/raw/docs/ui/reasoning.mdx +12 -11
  120. package/.docs/raw/docs/ui/sources.mdx +88 -17
  121. package/.docs/raw/docs/ui/streamdown.mdx +16 -7
  122. package/.docs/raw/docs/ui/thread-list.mdx +11 -13
  123. package/.docs/raw/docs/ui/thread.mdx +28 -33
  124. package/.docs/raw/docs/ui/tool-fallback.mdx +5 -6
  125. package/.docs/raw/docs/ui/tool-group.mdx +9 -8
  126. package/.docs/raw/docs/utilities/heat-graph.mdx +236 -0
  127. package/.docs/raw/docs/utilities/tw-shimmer.mdx +211 -0
  128. package/package.json +4 -4
  129. package/.docs/raw/docs/(reference)/legacy/styled/assistant-modal.mdx +0 -77
  130. package/.docs/raw/docs/(reference)/legacy/styled/decomposition.mdx +0 -635
  131. package/.docs/raw/docs/(reference)/legacy/styled/markdown.mdx +0 -77
  132. package/.docs/raw/docs/(reference)/legacy/styled/scrollbar.mdx +0 -72
  133. package/.docs/raw/docs/(reference)/legacy/styled/thread-width.mdx +0 -22
  134. package/.docs/raw/docs/(reference)/legacy/styled/thread.mdx +0 -77
  135. /package/.docs/raw/docs/cloud/{overview.mdx → index.mdx} +0 -0
@@ -20,336 +20,6 @@
20
20
 
21
21
  ```
22
22
 
23
- ## adapters/openai-chat-adapter.ts
24
-
25
- ```typescript
26
- import type { ChatModelAdapter } from "@assistant-ui/react-native";
27
-
28
- export type OpenAIModelConfig = {
29
- apiKey: string;
30
- model?: string;
31
- baseURL?: string;
32
- /** Custom fetch implementation — pass `fetch` from `expo/fetch` for streaming support */
33
- fetch?: typeof globalThis.fetch;
34
- };
35
-
36
- type OpenAIMessage = {
37
- role: string;
38
- content: string | any[] | null;
39
- tool_calls?: {
40
- id: string;
41
- type: string;
42
- function: { name: string; arguments: string };
43
- }[];
44
- tool_call_id?: string;
45
- };
46
-
47
- type ToolCallAccumulator = Record<
48
- number,
49
- { id: string; name: string; arguments: string }
50
- >;
51
-
52
- export function createOpenAIChatModelAdapter(
53
- config: OpenAIModelConfig,
54
- ): ChatModelAdapter {
55
- const {
56
- apiKey,
57
- model = "gpt-4o-mini",
58
- baseURL = "https://api.openai.com/v1",
59
- fetch: customFetch = globalThis.fetch,
60
- } = config;
61
-
62
- const callOpenAI = async (
63
- messages: OpenAIMessage[],
64
- openAITools: any[] | undefined,
65
- abortSignal: AbortSignal,
66
- ) => {
67
- const response = await customFetch(`${baseURL}/chat/completions`, {
68
- method: "POST",
69
- headers: {
70
- "Content-Type": "application/json",
71
- Authorization: `Bearer ${apiKey}`,
72
- },
73
- body: JSON.stringify({
74
- model,
75
- messages,
76
- stream: true,
77
- ...(openAITools ? { tools: openAITools } : {}),
78
- }),
79
- signal: abortSignal,
80
- });
81
-
82
- if (!response.ok) {
83
- const body = await response.text().catch(() => "");
84
- throw new Error(`OpenAI API error: ${response.status} ${body}`);
85
- }
86
-
87
- return response;
88
- };
89
-
90
- const streamResponse = async function* (
91
- response: Response,
92
- onUpdate: (text: string, toolCalls: ToolCallAccumulator) => any,
93
- ) {
94
- const reader = response.body?.getReader();
95
- if (!reader) {
96
- const json = await response.json();
97
- const choice = json.choices?.[0]?.message;
98
- return {
99
- text: (choice?.content as string) ?? "",
100
- toolCalls: {} as ToolCallAccumulator,
101
- rawToolCalls: choice?.tool_calls,
102
- };
103
- }
104
-
105
- const decoder = new TextDecoder();
106
- let fullText = "";
107
- const toolCalls: ToolCallAccumulator = {};
108
-
109
- try {
110
- while (true) {
111
- const { done, value } = await reader.read();
112
- if (done) break;
113
-
114
- const chunk = decoder.decode(value, { stream: true });
115
- for (const line of chunk.split("\n")) {
116
- if (!line.startsWith("data: ")) continue;
117
- const data = line.slice(6);
118
- if (data === "[DONE]") continue;
119
-
120
- try {
121
- const delta = JSON.parse(data).choices?.[0]?.delta;
122
- if (!delta) continue;
123
-
124
- if (delta.content) fullText += delta.content;
125
- if (delta.tool_calls) {
126
- for (const tc of delta.tool_calls) {
127
- if (!toolCalls[tc.index]) {
128
- toolCalls[tc.index] = {
129
- id: tc.id ?? "",
130
- name: tc.function?.name ?? "",
131
- arguments: "",
132
- };
133
- }
134
- if (tc.id) toolCalls[tc.index].id = tc.id;
135
- if (tc.function?.name)
136
- toolCalls[tc.index].name = tc.function.name;
137
- if (tc.function?.arguments)
138
- toolCalls[tc.index].arguments += tc.function.arguments;
139
- }
140
- }
141
-
142
- yield* onUpdate(fullText, toolCalls);
143
- } catch {
144
- // skip invalid JSON
145
- }
146
- }
147
- }
148
- } finally {
149
- reader.releaseLock();
150
- }
151
-
152
- return { text: fullText, toolCalls };
153
- };
154
-
155
- return {
156
- async *run({ messages, context, abortSignal }) {
157
- const tools = context.tools;
158
-
159
- // Convert messages to OpenAI format
160
- const openAIMessages: OpenAIMessage[] = messages
161
- .filter((m) => m.role !== "system")
162
- .flatMap((m) => {
163
- if (m.role === "user") {
164
- const textParts = m.content.filter((p) => p.type === "text");
165
- const text = textParts
166
- .map((p) => ("text" in p ? p.text : ""))
167
- .join("\n");
168
-
169
- // Check for image attachments
170
- const imageAttachments = (m.attachments ?? []).flatMap((a) =>
171
- (a.content ?? []).filter((c: any) => c.type === "image"),
172
- );
173
-
174
- if (imageAttachments.length > 0) {
175
- const content: any[] = [];
176
- if (text) content.push({ type: "text", text });
177
- for (const img of imageAttachments) {
178
- content.push({
179
- type: "image_url",
180
- image_url: { url: (img as any).image },
181
- });
182
- }
183
- return [{ role: "user", content }];
184
- }
185
-
186
- return [{ role: "user", content: text }];
187
- }
188
- if (m.role === "assistant") {
189
- const result: OpenAIMessage[] = [];
190
- const textParts = m.content.filter((p) => p.type === "text");
191
- const toolCallParts = m.content.filter(
192
- (p) => p.type === "tool-call",
193
- );
194
-
195
- if (toolCallParts.length > 0) {
196
- result.push({
197
- role: "assistant",
198
- content:
199
- textParts.length > 0
200
- ? textParts
201
- .map((p) => ("text" in p ? p.text : ""))
202
- .join("\n")
203
- : null,
204
- tool_calls: toolCallParts.map((p: any) => ({
205
- id: p.toolCallId,
206
- type: "function",
207
- function: {
208
- name: p.toolName,
209
- arguments: JSON.stringify(p.args),
210
- },
211
- })),
212
- });
213
- for (const tc of toolCallParts) {
214
- if ((tc as any).result !== undefined) {
215
- result.push({
216
- role: "tool",
217
- content: JSON.stringify((tc as any).result),
218
- tool_call_id: (tc as any).toolCallId,
219
- });
220
- }
221
- }
222
- } else if (textParts.length > 0) {
223
- result.push({
224
- role: "assistant",
225
- content: textParts
226
- .map((p) => ("text" in p ? p.text : ""))
227
- .join("\n"),
228
- });
229
- }
230
-
231
- return result;
232
- }
233
- return [];
234
- });
235
-
236
- const openAITools =
237
- tools && Object.keys(tools).length > 0
238
- ? Object.entries(tools).map(([name, t]) => ({
239
- type: "function" as const,
240
- function: {
241
- name,
242
- description: (t as any).description ?? "",
243
- parameters: (t as any).parameters ?? {},
244
- },
245
- }))
246
- : undefined;
247
-
248
- // Tool execution loop — keep calling OpenAI until we get a text response
249
- const maxToolRounds = 5;
250
- const priorParts: any[] = []; // accumulate tool-call parts across rounds
251
-
252
- for (let round = 0; round <= maxToolRounds; round++) {
253
- const response = await callOpenAI(
254
- openAIMessages,
255
- openAITools,
256
- abortSignal,
257
- );
258
-
259
- let lastText = "";
260
- const gen = streamResponse(response, function* (text, toolCalls) {
261
- lastText = text;
262
- const content: any[] = [...priorParts];
263
- if (text) content.push({ type: "text" as const, text });
264
- for (const tc of Object.values(toolCalls)) {
265
- let args = {};
266
- try {
267
- args = JSON.parse(tc.arguments);
268
- } catch {
269
- // still streaming
270
- }
271
- content.push({
272
- type: "tool-call" as const,
273
- toolCallId: tc.id,
274
- toolName: tc.name,
275
- args,
276
- });
277
- }
278
- if (content.length > 0) yield { content };
279
- });
280
-
281
- // Consume the stream
282
- let streamResult: any;
283
- while (true) {
284
- const { value, done } = await gen.next();
285
- if (done) {
286
- streamResult = value;
287
- break;
288
- }
289
- yield value;
290
- }
291
-
292
- const { toolCalls } = (streamResult as {
293
- toolCalls: ToolCallAccumulator;
294
- }) ?? { toolCalls: {} };
295
- const pendingToolCalls = Object.values(toolCalls) as {
296
- id: string;
297
- name: string;
298
- arguments: string;
299
- }[];
300
-
301
- // No tool calls — done
302
- if (pendingToolCalls.length === 0) break;
303
-
304
- // Execute tools and add results to messages for next round
305
- openAIMessages.push({
306
- role: "assistant",
307
- content: lastText || null,
308
- tool_calls: pendingToolCalls.map((tc) => ({
309
- id: tc.id,
310
- type: "function",
311
- function: { name: tc.name, arguments: tc.arguments },
312
- })),
313
- });
314
-
315
- const executedToolCalls: any[] = [];
316
- for (const tc of pendingToolCalls) {
317
- const args = JSON.parse(tc.arguments);
318
- const toolDef = tools?.[tc.name];
319
- let result: any;
320
- if (toolDef?.execute) {
321
- result = await (toolDef as any).execute(args);
322
- }
323
-
324
- executedToolCalls.push({
325
- type: "tool-call" as const,
326
- toolCallId: tc.id,
327
- toolName: tc.name,
328
- args,
329
- result,
330
- });
331
-
332
- // Yield with all prior parts + executed tool calls so far
333
- yield { content: [...priorParts, ...executedToolCalls] };
334
-
335
- openAIMessages.push({
336
- role: "tool",
337
- content: JSON.stringify(result),
338
- tool_call_id: tc.id,
339
- });
340
- }
341
-
342
- // Add executed tool calls to prior parts for next round
343
- priorParts.push(...executedToolCalls);
344
-
345
- // Next iteration will call OpenAI with tool results
346
- }
347
- },
348
- };
349
- }
350
-
351
- ```
352
-
353
23
  ## app.json
354
24
 
355
25
  ```json
@@ -418,25 +88,27 @@ import { StatusBar } from "expo-status-bar";
418
88
  import "react-native-reanimated";
419
89
  import { Pressable, useColorScheme } from "react-native";
420
90
  import { Ionicons } from "@expo/vector-icons";
91
+ import { useFonts } from "expo-font";
421
92
  import { GestureHandlerRootView } from "react-native-gesture-handler";
422
93
 
423
94
  import {
424
- AssistantProvider,
425
- useAssistantRuntime,
95
+ AssistantRuntimeProvider,
96
+ useAui,
97
+ Tools,
426
98
  } from "@assistant-ui/react-native";
427
99
  import { useAppRuntime } from "@/hooks/use-app-runtime";
428
100
  import { ThreadListDrawer } from "@/components/thread-list/ThreadListDrawer";
429
- import { WeatherTool } from "@/components/assistant-ui/tools";
101
+ import { expoToolkit } from "@/components/assistant-ui/tools";
430
102
 
431
103
  function NewChatButton() {
432
- const runtime = useAssistantRuntime();
104
+ const aui = useAui();
433
105
  const colorScheme = useColorScheme();
434
106
  const isDark = colorScheme === "dark";
435
107
 
436
108
  return (
437
109
  <Pressable
438
110
  onPress={() => {
439
- runtime.threads.switchToNewThread();
111
+ aui.threads().switchToNewThread();
440
112
  }}
441
113
  style={{ marginRight: 16 }}
442
114
  >
@@ -471,20 +143,61 @@ function DrawerLayout() {
471
143
  }
472
144
 
473
145
  export default function RootLayout() {
146
+ const [fontsLoaded] = useFonts(Ionicons.font);
474
147
  const runtime = useAppRuntime();
148
+ const aui = useAui({
149
+ tools: Tools({ toolkit: expoToolkit }),
150
+ });
151
+
152
+ if (!fontsLoaded) return null;
475
153
 
476
154
  return (
477
155
  <GestureHandlerRootView style={{ flex: 1 }}>
478
- <AssistantProvider runtime={runtime}>
479
- <WeatherTool />
156
+ <AssistantRuntimeProvider runtime={runtime} aui={aui}>
480
157
  <DrawerLayout />
481
- </AssistantProvider>
158
+ </AssistantRuntimeProvider>
482
159
  </GestureHandlerRootView>
483
160
  );
484
161
  }
485
162
 
486
163
  ```
487
164
 
165
+ ## app/api/chat+api.ts
166
+
167
+ ```typescript
168
+ import { frontendTools } from "@assistant-ui/react-ai-sdk";
169
+ import { openai } from "@ai-sdk/openai";
170
+ import {
171
+ convertToModelMessages,
172
+ pruneMessages,
173
+ stepCountIs,
174
+ streamText,
175
+ } from "ai";
176
+
177
+ export async function POST(req: Request) {
178
+ const body = await req.json();
179
+ const { messages, tools } = body;
180
+
181
+ const model = openai("gpt-4o-mini");
182
+
183
+ const prunedMessages = pruneMessages({
184
+ messages: await convertToModelMessages(messages),
185
+ reasoning: "none",
186
+ });
187
+
188
+ const result = streamText({
189
+ model,
190
+ messages: prunedMessages,
191
+ maxOutputTokens: 15000,
192
+ stopWhen: stepCountIs(10),
193
+ tools: frontendTools(tools),
194
+ });
195
+
196
+ return result.toUIMessageStreamResponse();
197
+ }
198
+
199
+ ```
200
+
488
201
  ## app/index.tsx
489
202
 
490
203
  ```tsx
@@ -501,9 +214,9 @@ export default function ChatPage() {
501
214
  ```tsx
502
215
  import {
503
216
  View,
504
- TextInput,
505
217
  Pressable,
506
218
  Image,
219
+ Platform,
507
220
  StyleSheet,
508
221
  useColorScheme,
509
222
  } from "react-native";
@@ -512,12 +225,8 @@ import * as ImagePicker from "expo-image-picker";
512
225
  import {
513
226
  useAui,
514
227
  useAuiState,
515
- useComposerSend,
516
- useComposerCancel,
517
- useComposerAddAttachment,
518
- ComposerAttachments,
519
- AttachmentRoot,
520
- AttachmentRemove,
228
+ ComposerPrimitive,
229
+ AttachmentPrimitive,
521
230
  } from "@assistant-ui/react-native";
522
231
 
523
232
  function AttachmentPreview() {
@@ -529,27 +238,25 @@ function AttachmentPreview() {
529
238
  const uri = (imageContent as any)?.image;
530
239
 
531
240
  return (
532
- <AttachmentRoot style={styles.attachmentItem}>
241
+ <AttachmentPrimitive.Root style={styles.attachmentItem}>
533
242
  {uri ? <Image source={{ uri }} style={styles.attachmentImage} /> : null}
534
- <AttachmentRemove style={styles.attachmentRemoveButton}>
243
+ <AttachmentPrimitive.Remove style={styles.attachmentRemoveButton}>
535
244
  <Ionicons name="close-circle" size={20} color="#ff453a" />
536
- </AttachmentRemove>
537
- </AttachmentRoot>
245
+ </AttachmentPrimitive.Remove>
246
+ </AttachmentPrimitive.Root>
538
247
  );
539
248
  }
540
249
 
541
- const attachmentComponents = { Attachment: AttachmentPreview };
542
-
543
250
  export function Composer() {
544
251
  const colorScheme = useColorScheme();
545
252
  const isDark = colorScheme === "dark";
546
253
 
547
254
  const aui = useAui();
548
- const text = useAuiState((s) => s.composer.text);
549
255
  const attachmentsCount = useAuiState((s) => s.composer.attachments.length);
550
- const { send, canSend } = useComposerSend();
551
- const { cancel, canCancel } = useComposerCancel();
552
- const { addAttachment } = useComposerAddAttachment();
256
+ const canCancel = useAuiState((s) => s.composer.canCancel);
257
+ const canSend = useAuiState(
258
+ (s) => !s.thread.isRunning && s.composer.isEditing && !s.composer.isEmpty,
259
+ );
553
260
 
554
261
  const pickImage = async () => {
555
262
  const result = await ImagePicker.launchImageLibraryAsync({
@@ -565,7 +272,7 @@ export function Composer() {
565
272
  // Force JPEG mime type — iOS may report HEIC which OpenAI doesn't support
566
273
  const dataUrl = `data:image/jpeg;base64,${asset.base64}`;
567
274
 
568
- await addAttachment({
275
+ await aui.composer().addAttachment({
569
276
  name: asset.fileName ?? "image.jpg",
570
277
  contentType: "image/jpeg",
571
278
  type: "image",
@@ -587,7 +294,9 @@ export function Composer() {
587
294
  >
588
295
  {attachmentsCount > 0 && (
589
296
  <View style={styles.attachmentsList}>
590
- <ComposerAttachments components={attachmentComponents} />
297
+ <ComposerPrimitive.Attachments>
298
+ {() => <AttachmentPreview />}
299
+ </ComposerPrimitive.Attachments>
591
300
  </View>
592
301
  )}
593
302
  <View
@@ -610,25 +319,20 @@ export function Composer() {
610
319
  color={isDark ? "#8e8e93" : "#6e6e73"}
611
320
  />
612
321
  </Pressable>
613
- <TextInput
322
+ <ComposerPrimitive.Input
614
323
  style={[styles.input, { color: isDark ? "#ffffff" : "#000000" }]}
615
324
  placeholder="Message..."
616
325
  placeholderTextColor="#8e8e93"
617
- value={text}
618
- onChangeText={(newText) => aui.composer().setText(newText)}
619
326
  multiline
620
327
  maxLength={4000}
621
328
  editable={!canCancel}
622
329
  />
623
330
  {canCancel ? (
624
- <Pressable
625
- style={[styles.button, styles.stopButton]}
626
- onPress={cancel}
627
- >
331
+ <ComposerPrimitive.Cancel style={[styles.button, styles.stopButton]}>
628
332
  <View style={styles.stopIcon} />
629
- </Pressable>
333
+ </ComposerPrimitive.Cancel>
630
334
  ) : (
631
- <Pressable
335
+ <ComposerPrimitive.Send
632
336
  style={[
633
337
  styles.button,
634
338
  styles.sendButton,
@@ -642,15 +346,13 @@ export function Composer() {
642
346
  : "#e5e5ea",
643
347
  },
644
348
  ]}
645
- onPress={send}
646
- disabled={!canSend}
647
349
  >
648
350
  <Ionicons
649
351
  name="arrow-up"
650
352
  size={20}
651
353
  color={canSend ? "#ffffff" : "#8e8e93"}
652
354
  />
653
- </Pressable>
355
+ </ComposerPrimitive.Send>
654
356
  )}
655
357
  </View>
656
358
  </View>
@@ -660,8 +362,7 @@ export function Composer() {
660
362
  const styles = StyleSheet.create({
661
363
  container: {
662
364
  paddingHorizontal: 16,
663
- paddingTop: 12,
664
- paddingBottom: 8,
365
+ paddingVertical: 8,
665
366
  },
666
367
  attachmentsList: {
667
368
  flexDirection: "row",
@@ -685,36 +386,35 @@ const styles = StyleSheet.create({
685
386
  inputWrapper: {
686
387
  flexDirection: "row",
687
388
  alignItems: "flex-end",
688
- borderRadius: 24,
389
+ borderRadius: 20,
689
390
  borderWidth: 1,
690
- paddingLeft: 6,
691
- paddingRight: 6,
692
- paddingVertical: 6,
693
- minHeight: 48,
391
+ padding: 6,
694
392
  },
695
393
  attachButton: {
696
- width: 34,
697
- height: 34,
394
+ width: 30,
395
+ height: 30,
698
396
  justifyContent: "center",
699
397
  alignItems: "center",
700
398
  },
701
399
  input: {
702
400
  flex: 1,
703
401
  fontSize: 16,
704
- lineHeight: 22,
705
402
  maxHeight: 120,
706
- paddingVertical: 6,
707
- letterSpacing: -0.2,
403
+ alignSelf: "center",
404
+ paddingVertical: 0,
405
+ ...Platform.select({
406
+ web: { paddingHorizontal: 4, outlineStyle: "none" },
407
+ default: {},
408
+ }),
708
409
  },
709
410
  button: {
710
- width: 34,
711
- height: 34,
712
- borderRadius: 17,
411
+ width: 30,
412
+ height: 30,
413
+ borderRadius: 15,
713
414
  justifyContent: "center",
714
415
  alignItems: "center",
715
- marginLeft: 8,
416
+ marginLeft: 6,
716
417
  },
717
- sendButton: {},
718
418
  stopButton: {
719
419
  backgroundColor: "#ff453a",
720
420
  },
@@ -731,33 +431,29 @@ const styles = StyleSheet.create({
731
431
  ## components/assistant-ui/message-action-bar.tsx
732
432
 
733
433
  ```tsx
734
- import { Pressable, View, StyleSheet, useColorScheme } from "react-native";
434
+ import { View, StyleSheet, useColorScheme } from "react-native";
735
435
  import { Ionicons } from "@expo/vector-icons";
736
- import {
737
- useActionBarCopy,
738
- useActionBarReload,
739
- } from "@assistant-ui/react-native";
436
+ import { ActionBarPrimitive } from "@assistant-ui/react-native";
740
437
 
741
438
  export function MessageActionBar() {
742
439
  const colorScheme = useColorScheme();
743
440
  const isDark = colorScheme === "dark";
744
441
  const iconColor = isDark ? "#8e8e93" : "#6e6e73";
745
442
 
746
- const { copy, isCopied } = useActionBarCopy();
747
- const { reload } = useActionBarReload();
748
-
749
443
  return (
750
444
  <View style={styles.container}>
751
- <Pressable style={styles.button} onPress={copy}>
752
- <Ionicons
753
- name={isCopied ? "checkmark" : "copy-outline"}
754
- size={16}
755
- color={isCopied ? "#34c759" : iconColor}
756
- />
757
- </Pressable>
758
- <Pressable style={styles.button} onPress={reload}>
445
+ <ActionBarPrimitive.Copy style={styles.button}>
446
+ {({ isCopied }) => (
447
+ <Ionicons
448
+ name={isCopied ? "checkmark" : "copy-outline"}
449
+ size={16}
450
+ color={isCopied ? "#34c759" : iconColor}
451
+ />
452
+ )}
453
+ </ActionBarPrimitive.Copy>
454
+ <ActionBarPrimitive.Reload style={styles.button}>
759
455
  <Ionicons name="refresh-outline" size={16} color={iconColor} />
760
- </Pressable>
456
+ </ActionBarPrimitive.Reload>
761
457
  </View>
762
458
  );
763
459
  }
@@ -779,14 +475,14 @@ const styles = StyleSheet.create({
779
475
  ## components/assistant-ui/message-branch-picker.tsx
780
476
 
781
477
  ```tsx
782
- import { Pressable, View, StyleSheet, useColorScheme } from "react-native";
478
+ import { View, StyleSheet, useColorScheme } from "react-native";
783
479
  import { Ionicons } from "@expo/vector-icons";
784
480
  import { ThemedText } from "@/components/themed-text";
785
- import { useMessageBranching } from "@assistant-ui/react-native";
481
+ import { BranchPickerPrimitive, useAuiState } from "@assistant-ui/react-native";
786
482
 
787
483
  export function MessageBranchPicker() {
788
- const { branchNumber, branchCount, goToPrev, goToNext } =
789
- useMessageBranching();
484
+ const branchNumber = useAuiState((s) => s.message.branchNumber);
485
+ const branchCount = useAuiState((s) => s.message.branchCount);
790
486
 
791
487
  const colorScheme = useColorScheme();
792
488
  const isDark = colorScheme === "dark";
@@ -796,11 +492,7 @@ export function MessageBranchPicker() {
796
492
 
797
493
  return (
798
494
  <View style={styles.container}>
799
- <Pressable
800
- style={styles.button}
801
- onPress={goToPrev}
802
- disabled={branchNumber <= 1}
803
- >
495
+ <BranchPickerPrimitive.Previous style={styles.button}>
804
496
  <Ionicons
805
497
  name="chevron-back"
806
498
  size={14}
@@ -808,15 +500,11 @@ export function MessageBranchPicker() {
808
500
  branchNumber <= 1 ? (isDark ? "#3a3a3c" : "#d1d1d6") : iconColor
809
501
  }
810
502
  />
811
- </Pressable>
503
+ </BranchPickerPrimitive.Previous>
812
504
  <ThemedText style={styles.label} lightColor="#6e6e73" darkColor="#8e8e93">
813
505
  {branchNumber} / {branchCount}
814
506
  </ThemedText>
815
- <Pressable
816
- style={styles.button}
817
- onPress={goToNext}
818
- disabled={branchNumber >= branchCount}
819
- >
507
+ <BranchPickerPrimitive.Next style={styles.button}>
820
508
  <Ionicons
821
509
  name="chevron-forward"
822
510
  size={14}
@@ -828,7 +516,7 @@ export function MessageBranchPicker() {
828
516
  : iconColor
829
517
  }
830
518
  />
831
- </Pressable>
519
+ </BranchPickerPrimitive.Next>
832
520
  </View>
833
521
  );
834
522
  }
@@ -856,11 +544,7 @@ const styles = StyleSheet.create({
856
544
  ```tsx
857
545
  import { View, Image, StyleSheet, useColorScheme } from "react-native";
858
546
  import { ThemedText } from "@/components/themed-text";
859
- import {
860
- useAuiState,
861
- MessageContent,
862
- MessageAttachments,
863
- } from "@assistant-ui/react-native";
547
+ import { useAuiState, MessagePrimitive } from "@assistant-ui/react-native";
864
548
  import { MessageActionBar } from "./message-action-bar";
865
549
  import { MessageBranchPicker } from "./message-branch-picker";
866
550
 
@@ -915,8 +599,6 @@ function MessageImageAttachment() {
915
599
  return <Image source={{ uri }} style={styles.messageImage} />;
916
600
  }
917
601
 
918
- const messageAttachmentComponents = { Attachment: MessageImageAttachment };
919
-
920
602
  export function MessageBubble() {
921
603
  const colorScheme = useColorScheme();
922
604
  const isDark = colorScheme === "dark";
@@ -927,7 +609,9 @@ export function MessageBubble() {
927
609
  if (isUser) {
928
610
  return (
929
611
  <View style={[styles.container, styles.userContainer]}>
930
- <MessageAttachments components={messageAttachmentComponents} />
612
+ <MessagePrimitive.Attachments>
613
+ {() => <MessageImageAttachment />}
614
+ </MessagePrimitive.Attachments>
931
615
  <View
932
616
  style={[
933
617
  styles.bubble,
@@ -935,7 +619,9 @@ export function MessageBubble() {
935
619
  { backgroundColor: isDark ? "#0a84ff" : "#007aff" },
936
620
  ]}
937
621
  >
938
- <MessageContent renderText={({ part }) => <TextPart part={part} />} />
622
+ <MessagePrimitive.Content
623
+ renderText={({ part }) => <TextPart part={part} />}
624
+ />
939
625
  </View>
940
626
  <MessageBranchPicker />
941
627
  </View>
@@ -955,7 +641,9 @@ export function MessageBubble() {
955
641
  },
956
642
  ]}
957
643
  >
958
- <MessageContent renderText={({ part }) => <TextPart part={part} />} />
644
+ <MessagePrimitive.Content
645
+ renderText={({ part }) => <TextPart part={part} />}
646
+ />
959
647
  <MessageError />
960
648
  </View>
961
649
  {!isRunning && (
@@ -1040,11 +728,7 @@ import {
1040
728
  import { useSafeAreaInsets } from "react-native-safe-area-context";
1041
729
  import { MessageBubble } from "./message";
1042
730
  import { Composer } from "./composer";
1043
- import {
1044
- ThreadMessages,
1045
- useThreadIsEmpty,
1046
- useAui,
1047
- } from "@assistant-ui/react-native";
731
+ import { ThreadPrimitive, useAui } from "@assistant-ui/react-native";
1048
732
 
1049
733
  function SuggestionChip({ title, prompt }: { title: string; prompt: string }) {
1050
734
  const colorScheme = useColorScheme();
@@ -1126,21 +810,21 @@ function EmptyState() {
1126
810
  );
1127
811
  }
1128
812
 
1129
- const renderMessage = () => <MessageBubble />;
1130
-
1131
813
  function ChatMessages() {
1132
- const isEmpty = useThreadIsEmpty();
1133
-
1134
- if (isEmpty) {
1135
- return <EmptyState />;
1136
- }
1137
-
1138
814
  return (
1139
- <ThreadMessages
1140
- renderMessage={renderMessage}
1141
- contentContainerStyle={styles.messageList}
1142
- showsVerticalScrollIndicator={false}
1143
- />
815
+ <>
816
+ <ThreadPrimitive.Empty>
817
+ <EmptyState />
818
+ </ThreadPrimitive.Empty>
819
+ <ThreadPrimitive.If empty={false}>
820
+ <ThreadPrimitive.Messages
821
+ contentContainerStyle={styles.messageList}
822
+ showsVerticalScrollIndicator={false}
823
+ >
824
+ {() => <MessageBubble />}
825
+ </ThreadPrimitive.Messages>
826
+ </ThreadPrimitive.If>
827
+ </>
1144
828
  );
1145
829
  }
1146
830
 
@@ -1238,14 +922,123 @@ const styles = StyleSheet.create({
1238
922
 
1239
923
  ```tsx
1240
924
  import { View, Text, StyleSheet, useColorScheme } from "react-native";
1241
- import {
1242
- makeAssistantTool,
1243
- type ToolCallMessagePartProps,
925
+ import type {
926
+ Toolkit,
927
+ ToolCallMessagePartProps,
1244
928
  } from "@assistant-ui/react-native";
929
+ import { z } from "zod";
930
+
931
+ // Open-Meteo API adapters (free, no API key needed)
932
+
933
+ const geocodeLocationWithOpenMeteo = async (query: string) => {
934
+ try {
935
+ const response = await fetch(
936
+ `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=1`,
937
+ );
938
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
939
+ const data = await response.json();
940
+ if (!data.results || data.results.length === 0)
941
+ throw new Error("No results found");
942
+ return { success: true as const, result: data.results[0] };
943
+ } catch (error) {
944
+ return {
945
+ success: false as const,
946
+ error:
947
+ error instanceof Error ? error.message : "Failed to geocode location",
948
+ };
949
+ }
950
+ };
951
+
952
+ const mapWeatherCode = (code: number): string => {
953
+ if (code === 0) return "Clear";
954
+ if (code <= 3) return "Partly Cloudy";
955
+ if (code <= 48) return "Foggy";
956
+ if (code <= 57) return "Drizzle";
957
+ if (code <= 67) return "Rain";
958
+ if (code <= 77) return "Snow";
959
+ if (code <= 82) return "Showers";
960
+ if (code <= 86) return "Snow Showers";
961
+ if (code === 95) return "Thunderstorm";
962
+ return "Stormy";
963
+ };
964
+
965
+ const mapWeatherEmoji = (code: number): string => {
966
+ if (code === 0) return "\u2600\uFE0F";
967
+ if (code <= 3) return "\u26C5";
968
+ if (code <= 48) return "\uD83C\uDF2B\uFE0F";
969
+ if (code <= 57) return "\uD83C\uDF26\uFE0F";
970
+ if (code <= 67) return "\uD83C\uDF27\uFE0F";
971
+ if (code <= 77) return "\u2744\uFE0F";
972
+ if (code <= 82) return "\uD83C\uDF26\uFE0F";
973
+ if (code <= 86) return "\uD83C\uDF28\uFE0F";
974
+ if (code === 95) return "\u26C8\uFE0F";
975
+ return "\uD83C\uDF29\uFE0F";
976
+ };
977
+
978
+ const fetchWeatherFromOpenMeteo = async ({
979
+ query,
980
+ longitude,
981
+ latitude,
982
+ }: {
983
+ query: string;
984
+ longitude: number;
985
+ latitude: number;
986
+ }) => {
987
+ try {
988
+ const response = await fetch(
989
+ `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&timezone=auto&temperature_unit=fahrenheit&current=temperature_2m,weather_code,wind_speed_10m&daily=weather_code,temperature_2m_max,temperature_2m_min&forecast_days=5`,
990
+ );
991
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
992
+ const data = await response.json();
993
+ const current = data.current;
994
+ const daily = data.daily;
995
+ if (!current || !daily?.time) throw new Error("Invalid API response");
996
+
997
+ const forecast = daily.time.slice(0, 5).map((date: string, i: number) => {
998
+ const d = new Date(`${date}T12:00:00Z`);
999
+ const label =
1000
+ i === 0
1001
+ ? "Today"
1002
+ : new Intl.DateTimeFormat("en-US", {
1003
+ weekday: "short",
1004
+ timeZone: "UTC",
1005
+ }).format(d);
1006
+ return {
1007
+ label,
1008
+ code: daily.weather_code[i],
1009
+ min: Math.round(daily.temperature_2m_min[i]),
1010
+ max: Math.round(daily.temperature_2m_max[i]),
1011
+ };
1012
+ });
1013
+
1014
+ return {
1015
+ success: true as const,
1016
+ location: query,
1017
+ temperature: Math.round(current.temperature_2m),
1018
+ weatherCode: current.weather_code,
1019
+ windSpeed: Math.round(current.wind_speed_10m),
1020
+ forecast,
1021
+ };
1022
+ } catch (error) {
1023
+ return {
1024
+ success: false as const,
1025
+ error: error instanceof Error ? error.message : "Failed to fetch weather",
1026
+ };
1027
+ }
1028
+ };
1245
1029
 
1246
- const WeatherToolUI = (
1247
- props: ToolCallMessagePartProps<{ city: string }, { temperature: number }>,
1248
- ) => {
1030
+ // Tool UI Components
1031
+
1032
+ function GeocodeToolUI(
1033
+ props: ToolCallMessagePartProps<
1034
+ { query: string },
1035
+ {
1036
+ success: boolean;
1037
+ result?: { name: string; latitude: number; longitude: number };
1038
+ error?: string;
1039
+ }
1040
+ >,
1041
+ ) {
1249
1042
  const colorScheme = useColorScheme();
1250
1043
  const isDark = colorScheme === "dark";
1251
1044
 
@@ -1258,65 +1051,315 @@ const WeatherToolUI = (
1258
1051
  ]}
1259
1052
  >
1260
1053
  <Text style={[styles.label, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
1261
- Looking up weather for {props.args.city}...
1054
+ Finding location...
1055
+ </Text>
1056
+ </View>
1057
+ );
1058
+ }
1059
+
1060
+ if (props.result?.error) {
1061
+ return (
1062
+ <View
1063
+ style={[
1064
+ styles.card,
1065
+ { backgroundColor: isDark ? "#3a1c1c" : "#fff0f0" },
1066
+ ]}
1067
+ >
1068
+ <Text style={[styles.label, { color: "#ff453a" }]}>
1069
+ Geocoding failed: {props.result.error}
1262
1070
  </Text>
1263
1071
  </View>
1264
1072
  );
1265
1073
  }
1266
1074
 
1075
+ const result = props.result?.result;
1076
+ if (!result) return null;
1077
+
1267
1078
  return (
1268
1079
  <View
1269
1080
  style={[styles.card, { backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" }]}
1270
1081
  >
1271
- <Text style={[styles.city, { color: isDark ? "#ffffff" : "#000000" }]}>
1272
- {props.args.city}
1273
- </Text>
1274
- <Text style={styles.temp}>{props.result?.temperature ?? "—"}°F</Text>
1275
- <Text style={[styles.label, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
1276
- Current Weather
1082
+ <View style={styles.row}>
1083
+ <Text style={styles.pin}>{"\uD83D\uDCCD"}</Text>
1084
+ <View>
1085
+ <Text
1086
+ style={[
1087
+ styles.locationName,
1088
+ { color: isDark ? "#ffffff" : "#000000" },
1089
+ ]}
1090
+ >
1091
+ {result.name}
1092
+ </Text>
1093
+ <Text
1094
+ style={[styles.coords, { color: isDark ? "#8e8e93" : "#6e6e73" }]}
1095
+ >
1096
+ {Math.abs(result.latitude).toFixed(2)}
1097
+ {"\u00B0"}
1098
+ {result.latitude >= 0 ? "N" : "S"},{" "}
1099
+ {Math.abs(result.longitude).toFixed(2)}
1100
+ {"\u00B0"}
1101
+ {result.longitude >= 0 ? "E" : "W"}
1102
+ </Text>
1103
+ </View>
1104
+ </View>
1105
+ </View>
1106
+ );
1107
+ }
1108
+
1109
+ function WeatherToolUI(
1110
+ props: ToolCallMessagePartProps<
1111
+ { query: string; longitude: number; latitude: number },
1112
+ {
1113
+ success: boolean;
1114
+ location?: string;
1115
+ temperature?: number;
1116
+ weatherCode?: number;
1117
+ windSpeed?: number;
1118
+ forecast?: Array<{
1119
+ label: string;
1120
+ code: number;
1121
+ min: number;
1122
+ max: number;
1123
+ }>;
1124
+ error?: string;
1125
+ }
1126
+ >,
1127
+ ) {
1128
+ const colorScheme = useColorScheme();
1129
+ const isDark = colorScheme === "dark";
1130
+
1131
+ if (props.status?.type === "running") {
1132
+ return (
1133
+ <View
1134
+ style={[
1135
+ styles.card,
1136
+ { backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" },
1137
+ ]}
1138
+ >
1139
+ <Text style={[styles.label, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
1140
+ Fetching weather for {props.args.query}...
1141
+ </Text>
1142
+ </View>
1143
+ );
1144
+ }
1145
+
1146
+ if (!props.result?.success) {
1147
+ return (
1148
+ <View
1149
+ style={[
1150
+ styles.card,
1151
+ { backgroundColor: isDark ? "#3a1c1c" : "#fff0f0" },
1152
+ ]}
1153
+ >
1154
+ <Text style={[styles.label, { color: "#ff453a" }]}>
1155
+ Weather unavailable: {props.result?.error ?? "Unknown error"}
1156
+ </Text>
1157
+ </View>
1158
+ );
1159
+ }
1160
+
1161
+ const { location, temperature, weatherCode, windSpeed, forecast } =
1162
+ props.result;
1163
+
1164
+ return (
1165
+ <View
1166
+ style={[
1167
+ styles.weatherCard,
1168
+ { backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" },
1169
+ ]}
1170
+ >
1171
+ <View style={styles.weatherHeader}>
1172
+ <Text style={styles.weatherEmoji}>
1173
+ {mapWeatherEmoji(weatherCode ?? 0)}
1174
+ </Text>
1175
+ <View>
1176
+ <Text
1177
+ style={[
1178
+ styles.locationName,
1179
+ { color: isDark ? "#ffffff" : "#000000" },
1180
+ ]}
1181
+ >
1182
+ {location}
1183
+ </Text>
1184
+ <Text
1185
+ style={[
1186
+ styles.condition,
1187
+ { color: isDark ? "#8e8e93" : "#6e6e73" },
1188
+ ]}
1189
+ >
1190
+ {mapWeatherCode(weatherCode ?? 0)}
1191
+ </Text>
1192
+ </View>
1193
+ </View>
1194
+
1195
+ <Text style={styles.tempLarge}>
1196
+ {temperature ?? "--"}
1197
+ {"\u00B0"}F
1277
1198
  </Text>
1199
+
1200
+ {windSpeed != null && (
1201
+ <Text style={[styles.wind, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
1202
+ Wind: {windSpeed} mph
1203
+ </Text>
1204
+ )}
1205
+
1206
+ {forecast && forecast.length > 0 && (
1207
+ <View
1208
+ style={[
1209
+ styles.forecastRow,
1210
+ {
1211
+ borderTopColor: isDark
1212
+ ? "rgba(255,255,255,0.1)"
1213
+ : "rgba(0,0,0,0.1)",
1214
+ },
1215
+ ]}
1216
+ >
1217
+ {forecast.map((day, i) => (
1218
+ <View key={i} style={styles.forecastDay}>
1219
+ <Text
1220
+ style={[
1221
+ styles.forecastLabel,
1222
+ { color: isDark ? "#8e8e93" : "#6e6e73" },
1223
+ ]}
1224
+ >
1225
+ {day.label}
1226
+ </Text>
1227
+ <Text style={styles.forecastEmoji}>
1228
+ {mapWeatherEmoji(day.code)}
1229
+ </Text>
1230
+ <Text
1231
+ style={[
1232
+ styles.forecastTemp,
1233
+ { color: isDark ? "#ffffff" : "#000000" },
1234
+ ]}
1235
+ >
1236
+ {day.max}
1237
+ {"\u00B0"}
1238
+ </Text>
1239
+ <Text
1240
+ style={[
1241
+ styles.forecastTempLow,
1242
+ { color: isDark ? "#8e8e93" : "#6e6e73" },
1243
+ ]}
1244
+ >
1245
+ {day.min}
1246
+ {"\u00B0"}
1247
+ </Text>
1248
+ </View>
1249
+ ))}
1250
+ </View>
1251
+ )}
1278
1252
  </View>
1279
1253
  );
1280
- };
1254
+ }
1281
1255
 
1282
- export const WeatherTool = makeAssistantTool({
1283
- toolName: "get_weather",
1284
- description: "Get the current weather for a city",
1285
- parameters: {
1286
- type: "object",
1287
- properties: {
1288
- city: { type: "string", description: "The city name" },
1289
- },
1290
- required: ["city"],
1256
+ // Toolkit definition
1257
+
1258
+ export const expoToolkit: Toolkit = {
1259
+ geocode_location: {
1260
+ description: "Geocode a location using Open-Meteo's geocoding API",
1261
+ parameters: z.object({
1262
+ query: z.string(),
1263
+ }),
1264
+ execute: async (args: { query: string }) =>
1265
+ geocodeLocationWithOpenMeteo(args.query),
1266
+ render: GeocodeToolUI,
1267
+ },
1268
+ weather_search: {
1269
+ description:
1270
+ "Find the weather in a location given a longitude and latitude",
1271
+ parameters: z.object({
1272
+ query: z.string(),
1273
+ longitude: z.number(),
1274
+ latitude: z.number(),
1275
+ }),
1276
+ execute: async (args: {
1277
+ query: string;
1278
+ longitude: number;
1279
+ latitude: number;
1280
+ }) => fetchWeatherFromOpenMeteo(args),
1281
+ render: WeatherToolUI,
1291
1282
  },
1292
- execute: async ({ city }) => {
1293
- // Simulated weather API — use city to vary seed
1294
- await new Promise((r) => setTimeout(r, 1000));
1295
- const seed = city.length;
1296
- const temperature = Math.round(50 + ((seed * 17) % 40));
1297
- return { temperature };
1298
- },
1299
- render: WeatherToolUI,
1300
- });
1283
+ };
1301
1284
 
1302
1285
  const styles = StyleSheet.create({
1303
1286
  card: {
1287
+ padding: 12,
1288
+ borderRadius: 12,
1289
+ marginVertical: 4,
1290
+ },
1291
+ weatherCard: {
1304
1292
  padding: 16,
1305
1293
  borderRadius: 12,
1306
1294
  marginVertical: 4,
1307
1295
  gap: 4,
1308
1296
  },
1309
- city: {
1297
+ row: {
1298
+ flexDirection: "row",
1299
+ alignItems: "center",
1300
+ gap: 10,
1301
+ },
1302
+ pin: {
1303
+ fontSize: 20,
1304
+ },
1305
+ locationName: {
1310
1306
  fontSize: 15,
1311
1307
  fontWeight: "600",
1312
1308
  },
1313
- temp: {
1309
+ coords: {
1310
+ fontSize: 13,
1311
+ marginTop: 2,
1312
+ },
1313
+ label: {
1314
+ fontSize: 13,
1315
+ },
1316
+ weatherHeader: {
1317
+ flexDirection: "row",
1318
+ alignItems: "center",
1319
+ gap: 10,
1320
+ marginBottom: 4,
1321
+ },
1322
+ weatherEmoji: {
1314
1323
  fontSize: 32,
1324
+ },
1325
+ condition: {
1326
+ fontSize: 13,
1327
+ marginTop: 2,
1328
+ },
1329
+ tempLarge: {
1330
+ fontSize: 40,
1315
1331
  fontWeight: "700",
1316
1332
  color: "#007aff",
1317
1333
  },
1318
- label: {
1334
+ wind: {
1335
+ fontSize: 13,
1336
+ marginTop: 2,
1337
+ },
1338
+ forecastRow: {
1339
+ flexDirection: "row",
1340
+ justifyContent: "space-between",
1341
+ marginTop: 12,
1342
+ paddingTop: 12,
1343
+ borderTopWidth: StyleSheet.hairlineWidth,
1344
+ },
1345
+ forecastDay: {
1346
+ alignItems: "center",
1347
+ flex: 1,
1348
+ gap: 4,
1349
+ },
1350
+ forecastLabel: {
1351
+ fontSize: 11,
1352
+ fontWeight: "500",
1353
+ },
1354
+ forecastEmoji: {
1355
+ fontSize: 18,
1356
+ },
1357
+ forecastTemp: {
1319
1358
  fontSize: 13,
1359
+ fontWeight: "600",
1360
+ },
1361
+ forecastTempLow: {
1362
+ fontSize: 12,
1320
1363
  },
1321
1364
  });
1322
1365
 
@@ -1421,12 +1464,12 @@ export function ThemedView({
1421
1464
  ```tsx
1422
1465
  import { FlatList, View, StyleSheet, useColorScheme } from "react-native";
1423
1466
  import { useSafeAreaInsets } from "react-native-safe-area-context";
1424
- import { useAssistantRuntime, useAuiState } from "@assistant-ui/react-native";
1467
+ import { useAui, useAuiState } from "@assistant-ui/react-native";
1425
1468
  import { ThreadListItem } from "./ThreadListItem";
1426
1469
  import type { DrawerContentComponentProps } from "@react-navigation/drawer";
1427
1470
 
1428
1471
  export function ThreadListDrawer({ navigation }: DrawerContentComponentProps) {
1429
- const runtime = useAssistantRuntime();
1472
+ const aui = useAui();
1430
1473
  const threadIds = useAuiState((s) => s.threads.threadIds);
1431
1474
  const mainThreadId = useAuiState((s) => s.threads.mainThreadId);
1432
1475
  const threadItems = useAuiState((s) => s.threads.threadItems);
@@ -1455,7 +1498,7 @@ export function ThreadListDrawer({ navigation }: DrawerContentComponentProps) {
1455
1498
  title={threadItem?.title ?? "New Chat"}
1456
1499
  isActive={threadId === mainThreadId}
1457
1500
  onPress={() => {
1458
- runtime.threads.switchToThread(threadId);
1501
+ aui.threads().switchToThread(threadId);
1459
1502
  navigation.closeDrawer();
1460
1503
  }}
1461
1504
  />
@@ -1752,32 +1795,22 @@ export const Fonts = Platform.select({
1752
1795
 
1753
1796
  ```typescript
1754
1797
  import { useMemo } from "react";
1755
- import { fetch } from "expo/fetch";
1756
1798
  import {
1757
- useLocalRuntime,
1758
- createSimpleTitleAdapter,
1759
- SimpleImageAttachmentAdapter,
1760
- } from "@assistant-ui/react-native";
1761
- import { createOpenAIChatModelAdapter } from "@/adapters/openai-chat-adapter";
1799
+ useChatRuntime,
1800
+ AssistantChatTransport,
1801
+ } from "@assistant-ui/react-ai-sdk";
1802
+ import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
1803
+
1804
+ const CHAT_API = process.env.EXPO_PUBLIC_CHAT_ENDPOINT_URL ?? "/api/chat";
1762
1805
 
1763
1806
  export function useAppRuntime() {
1764
- const chatModel = useMemo(
1765
- () =>
1766
- createOpenAIChatModelAdapter({
1767
- apiKey: process.env.EXPO_PUBLIC_OPENAI_API_KEY ?? "",
1768
- model: "gpt-4o-mini",
1769
- fetch,
1770
- }),
1807
+ const transport = useMemo(
1808
+ () => new AssistantChatTransport({ api: CHAT_API }),
1771
1809
  [],
1772
1810
  );
1773
-
1774
- const titleGenerator = useMemo(() => createSimpleTitleAdapter(), []);
1775
-
1776
- return useLocalRuntime(chatModel, {
1777
- titleGenerator,
1778
- adapters: {
1779
- attachments: new SimpleImageAttachmentAdapter(),
1780
- },
1811
+ return useChatRuntime({
1812
+ transport,
1813
+ sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
1781
1814
  });
1782
1815
  }
1783
1816
 
@@ -1903,19 +1936,25 @@ module.exports = config;
1903
1936
  "start": "expo start",
1904
1937
  "android": "expo run:android",
1905
1938
  "ios": "expo run:ios",
1906
- "web": "expo start --web"
1939
+ "web": "expo start --web",
1940
+ "export:web": "expo export --platform web && node scripts/flatten-assets.mjs"
1907
1941
  },
1908
1942
  "dependencies": {
1943
+ "@ai-sdk/openai": "^3.0.41",
1944
+ "@ai-sdk/react": "^3.0.118",
1945
+ "@assistant-ui/react-ai-sdk": "workspace:*",
1909
1946
  "@assistant-ui/react-native": "workspace:*",
1910
1947
  "@expo/vector-icons": "^15.1.1",
1911
1948
  "@react-navigation/drawer": "^7.7.2",
1912
1949
  "@react-navigation/native": "^7.1.28",
1913
- "expo": "~55.0.4",
1950
+ "ai": "^6.0.116",
1951
+ "expo": "~55.0.6",
1914
1952
  "expo-constants": "~55.0.7",
1915
1953
  "expo-font": "~55.0.4",
1916
- "expo-image-picker": "~55.0.10",
1954
+ "expo-image-picker": "~55.0.12",
1917
1955
  "expo-linking": "~55.0.7",
1918
- "expo-router": "~55.0.3",
1956
+ "expo-router": "~55.0.5",
1957
+ "expo-server": "~55.0.6",
1919
1958
  "expo-splash-screen": "~55.0.10",
1920
1959
  "expo-status-bar": "~55.0.4",
1921
1960
  "expo-system-ui": "~55.0.9",
@@ -1923,11 +1962,12 @@ module.exports = config;
1923
1962
  "react-dom": "19.2.0",
1924
1963
  "react-native": "0.83.2",
1925
1964
  "react-native-gesture-handler": "~2.30.0",
1926
- "react-native-reanimated": "~4.2.1",
1927
- "react-native-safe-area-context": "~5.6.2",
1928
- "react-native-screens": "~4.23.0",
1965
+ "react-native-reanimated": "~4.2.2",
1966
+ "react-native-safe-area-context": "~5.7.0",
1967
+ "react-native-screens": "~4.24.0",
1929
1968
  "react-native-web": "~0.21.2",
1930
- "react-native-worklets": "0.7.2"
1969
+ "react-native-worklets": "0.7.2",
1970
+ "zod": "^4.3.6"
1931
1971
  },
1932
1972
  "devDependencies": {
1933
1973
  "@types/react": "~19.2.14",
@@ -2010,3 +2050,14 @@ Join our community of developers creating universal apps.
2010
2050
 
2011
2051
  ```
2012
2052
 
2053
+ ## vercel.json
2054
+
2055
+ ```json
2056
+ {
2057
+ "buildCommand": "cd ../.. && pnpm turbo build --filter=@assistant-ui/react-native --filter=@assistant-ui/react-ai-sdk && cd examples/with-expo && pnpm run export:web",
2058
+ "outputDirectory": "dist",
2059
+ "framework": null
2060
+ }
2061
+
2062
+ ```
2063
+