@assistant-ui/mcp-docs-server 0.1.35 → 0.1.38

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 (177) hide show
  1. package/.docs/organized/code-examples/waterfall.md +1 -1
  2. package/.docs/organized/code-examples/with-a2a.md +2 -2
  3. package/.docs/organized/code-examples/with-ag-ui.md +2 -2
  4. package/.docs/organized/code-examples/with-ai-sdk-v6.md +4 -4
  5. package/.docs/organized/code-examples/with-artifacts.md +4 -4
  6. package/.docs/organized/code-examples/with-assistant-transport.md +70 -54
  7. package/.docs/organized/code-examples/with-browser-extension.md +4 -4
  8. package/.docs/organized/code-examples/with-chain-of-thought.md +40 -89
  9. package/.docs/organized/code-examples/with-cloud-standalone.md +4 -4
  10. package/.docs/organized/code-examples/with-cloud.md +4 -4
  11. package/.docs/organized/code-examples/with-custom-thread-list.md +4 -4
  12. package/.docs/organized/code-examples/with-elevenlabs-conversational.md +7 -7
  13. package/.docs/organized/code-examples/with-elevenlabs-scribe.md +7 -7
  14. package/.docs/organized/code-examples/with-eve.md +343 -0
  15. package/.docs/organized/code-examples/with-expo.md +910 -933
  16. package/.docs/organized/code-examples/with-external-store.md +2 -2
  17. package/.docs/organized/code-examples/with-ffmpeg.md +5 -8
  18. package/.docs/organized/code-examples/with-generative-ui.md +6 -6
  19. package/.docs/organized/code-examples/with-google-adk.md +3 -3
  20. package/.docs/organized/code-examples/with-heat-graph.md +1 -1
  21. package/.docs/organized/code-examples/with-image-generation.md +4 -4
  22. package/.docs/organized/code-examples/with-interactables.md +166 -338
  23. package/.docs/organized/code-examples/with-langchain.md +4 -4
  24. package/.docs/organized/code-examples/with-langgraph.md +20 -157
  25. package/.docs/organized/code-examples/with-livekit.md +8 -8
  26. package/.docs/organized/code-examples/with-mcp.md +19 -10
  27. package/.docs/organized/code-examples/with-opencode.md +3 -3
  28. package/.docs/organized/code-examples/with-pi.md +9 -7
  29. package/.docs/organized/code-examples/with-react-hook-form.md +13 -6
  30. package/.docs/organized/code-examples/with-react-ink-web.md +5 -5
  31. package/.docs/organized/code-examples/with-react-ink.md +1 -1
  32. package/.docs/organized/code-examples/with-react-router.md +11 -11
  33. package/.docs/organized/code-examples/with-resumable-stream.md +5 -5
  34. package/.docs/organized/code-examples/with-store.md +1 -1
  35. package/.docs/organized/code-examples/with-tanstack.md +7 -7
  36. package/.docs/organized/code-examples/with-tap-runtime.md +2 -2
  37. package/.docs/organized/code-examples/with-virtualized-thread.md +44 -24
  38. package/.docs/raw/docs/(docs)/cli.mdx +5 -3
  39. package/.docs/raw/docs/(docs)/devtools.mdx +25 -2
  40. package/.docs/raw/docs/(docs)/index.mdx +5 -2
  41. package/.docs/raw/docs/(docs)/installation.mdx +5 -2
  42. package/.docs/raw/docs/(reference)/api-reference/external-store/message-conversion.mdx +16 -16
  43. package/.docs/raw/docs/(reference)/api-reference/hooks/composer-triggers.mdx +73 -42
  44. package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +3 -20
  45. package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +234 -166
  46. package/.docs/raw/docs/(reference)/api-reference/integrations/eve.mdx +51 -0
  47. package/.docs/raw/docs/(reference)/api-reference/integrations/index.mdx +4 -1
  48. package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +70 -38
  49. package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +30 -0
  50. package/.docs/raw/docs/(reference)/api-reference/primitives/attachment.mdx +7 -0
  51. package/.docs/raw/docs/(reference)/api-reference/primitives/branch-picker.mdx +26 -0
  52. package/.docs/raw/docs/(reference)/api-reference/primitives/chain-of-thought.mdx +15 -0
  53. package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +103 -0
  54. package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +16 -0
  55. package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +56 -0
  56. package/.docs/raw/docs/(reference)/api-reference/primitives/queue-item.mdx +12 -0
  57. package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +12 -0
  58. package/.docs/raw/docs/(reference)/api-reference/primitives/suggestion.mdx +14 -0
  59. package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +57 -1
  60. package/.docs/raw/docs/(reference)/api-reference/tools/index.mdx +6 -0
  61. package/.docs/raw/docs/(reference)/api-reference/tools/interactables-legacy.mdx +55 -0
  62. package/.docs/raw/docs/(reference)/api-reference/tools/interactables.mdx +151 -0
  63. package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +15 -15
  64. package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +20 -20
  65. package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +25 -9
  66. package/.docs/raw/docs/cloud/langgraph.mdx +1 -1
  67. package/.docs/raw/docs/guides/headless-composer-input.mdx +113 -0
  68. package/.docs/raw/docs/guides/latex.mdx +28 -22
  69. package/.docs/raw/docs/guides/mentions.mdx +32 -7
  70. package/.docs/raw/docs/guides/slash-commands.mdx +3 -6
  71. package/.docs/raw/docs/guides/speech.mdx +5 -7
  72. package/.docs/raw/docs/guides/virtualization.mdx +77 -7
  73. package/.docs/raw/docs/guides/voice.mdx +3 -2
  74. package/.docs/raw/docs/integrations/attachments/custom-adapter.mdx +4 -12
  75. package/.docs/raw/docs/integrations/auth/better-auth.mdx +7 -10
  76. package/.docs/raw/docs/integrations/auth/clerk.mdx +3 -9
  77. package/.docs/raw/docs/integrations/auth/next-auth.mdx +3 -8
  78. package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +11 -8
  79. package/.docs/raw/docs/integrations/index.mdx +6 -13
  80. package/.docs/raw/docs/integrations/observability/helicone.mdx +3 -4
  81. package/.docs/raw/docs/integrations/observability/langfuse.mdx +3 -5
  82. package/.docs/raw/docs/integrations/observability/langsmith.mdx +5 -4
  83. package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +4 -3
  84. package/.docs/raw/docs/migrations/react-langgraph-v0-7.mdx +2 -2
  85. package/.docs/raw/docs/primitives/composer.mdx +8 -0
  86. package/.docs/raw/docs/primitives/thread-list.mdx +30 -6
  87. package/.docs/raw/docs/primitives/thread.mdx +24 -0
  88. package/.docs/raw/docs/runtimes/a2a/quickstart.mdx +1 -1
  89. package/.docs/raw/docs/runtimes/ag-ui/quickstart.mdx +1 -1
  90. package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +24 -1
  91. package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +1 -1
  92. package/.docs/raw/docs/runtimes/custom/external-store.mdx +54 -0
  93. package/.docs/raw/docs/runtimes/eve/overview.mdx +100 -0
  94. package/.docs/raw/docs/runtimes/eve/quickstart.mdx +143 -0
  95. package/.docs/raw/docs/runtimes/google-adk/quickstart.mdx +1 -1
  96. package/.docs/raw/docs/runtimes/langchain.mdx +388 -20
  97. package/.docs/raw/docs/runtimes/langgraph/quickstart.mdx +1 -1
  98. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +1 -1
  99. package/.docs/raw/docs/runtimes/opencode/hooks.mdx +1 -1
  100. package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +6 -0
  101. package/.docs/raw/docs/tools/defining-tools.mdx +9 -0
  102. package/.docs/raw/docs/tools/generative-ui.mdx +37 -0
  103. package/.docs/raw/docs/tools/interactables-legacy.mdx +410 -0
  104. package/.docs/raw/docs/tools/interactables.mdx +892 -223
  105. package/.docs/raw/docs/tools/mcp-apps.mdx +12 -3
  106. package/.docs/raw/docs/tools/mcp.mdx +9 -4
  107. package/.docs/raw/docs/tools/multi-agent.mdx +5 -0
  108. package/.docs/raw/docs/tools/user-managed-mcp.mdx +17 -3
  109. package/.docs/raw/docs/ui/composer-trigger-popover.mdx +2 -0
  110. package/.docs/raw/docs/ui/file.mdx +1 -1
  111. package/.docs/raw/docs/ui/follow-up-suggestions.mdx +80 -0
  112. package/.docs/raw/docs/ui/model-selector.mdx +38 -5
  113. package/.docs/raw/docs/ui/part-grouping.mdx +38 -0
  114. package/.docs/raw/docs/utilities/react-o11y.mdx +92 -6
  115. package/README.md +1 -1
  116. package/dist/constants.d.ts +2 -1
  117. package/dist/constants.d.ts.map +1 -1
  118. package/dist/constants.js +2 -1
  119. package/dist/constants.js.map +1 -1
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +30 -2
  122. package/dist/index.js.map +1 -1
  123. package/dist/prepare-docs/copy-raw.d.ts.map +1 -1
  124. package/dist/prepare-docs/copy-raw.js +0 -4
  125. package/dist/prepare-docs/copy-raw.js.map +1 -1
  126. package/dist/tools/docs.d.ts.map +1 -1
  127. package/dist/tools/docs.js +18 -6
  128. package/dist/tools/docs.js.map +1 -1
  129. package/dist/tools/examples.d.ts +3 -1
  130. package/dist/tools/examples.d.ts.map +1 -1
  131. package/dist/tools/examples.js +1 -1
  132. package/dist/tools/examples.js.map +1 -1
  133. package/dist/tools/resources.d.ts +7 -0
  134. package/dist/tools/resources.d.ts.map +1 -0
  135. package/dist/tools/resources.js +74 -0
  136. package/dist/tools/resources.js.map +1 -0
  137. package/dist/tools/search.d.ts +33 -0
  138. package/dist/tools/search.d.ts.map +1 -0
  139. package/dist/tools/search.js +39 -0
  140. package/dist/tools/search.js.map +1 -0
  141. package/dist/tools/tests/test-setup.d.ts.map +1 -1
  142. package/dist/tools/tests/test-setup.js +3 -1
  143. package/dist/tools/tests/test-setup.js.map +1 -1
  144. package/dist/utils/mdx.d.ts +2 -1
  145. package/dist/utils/mdx.d.ts.map +1 -1
  146. package/dist/utils/mdx.js +19 -2
  147. package/dist/utils/mdx.js.map +1 -1
  148. package/dist/utils/paths.d.ts +2 -1
  149. package/dist/utils/paths.d.ts.map +1 -1
  150. package/dist/utils/paths.js +18 -1
  151. package/dist/utils/paths.js.map +1 -1
  152. package/dist/utils/search.d.ts +10 -0
  153. package/dist/utils/search.d.ts.map +1 -0
  154. package/dist/utils/search.js +97 -0
  155. package/dist/utils/search.js.map +1 -0
  156. package/package.json +4 -4
  157. package/src/constants.ts +2 -0
  158. package/src/index.ts +28 -6
  159. package/src/prepare-docs/copy-raw.ts +0 -5
  160. package/src/tools/docs.ts +30 -4
  161. package/src/tools/examples.ts +4 -2
  162. package/src/tools/resources.ts +114 -0
  163. package/src/tools/search.ts +46 -0
  164. package/src/tools/tests/completions.test.ts +46 -0
  165. package/src/tools/tests/directory-size-cap.test.ts +50 -0
  166. package/src/tools/tests/mcp-protocol.test.ts +21 -1
  167. package/src/tools/tests/resources.test.ts +102 -0
  168. package/src/tools/tests/search.test.ts +37 -0
  169. package/src/tools/tests/test-setup.ts +2 -0
  170. package/src/utils/mdx.ts +21 -1
  171. package/src/utils/paths.ts +22 -0
  172. package/src/utils/search.ts +131 -0
  173. package/.docs/raw/blog/2024-07-29-hello/index.mdx +0 -64
  174. package/.docs/raw/blog/2024-09-11/index.mdx +0 -10
  175. package/.docs/raw/blog/2024-12-15/index.mdx +0 -10
  176. package/.docs/raw/blog/2025-01-31-changelog/index.mdx +0 -127
  177. package/.docs/raw/blog/2026-03-launch-week/index.mdx +0 -258
@@ -61,7 +61,7 @@
61
61
  "resizeMode": "contain",
62
62
  "backgroundColor": "#ffffff",
63
63
  "dark": {
64
- "backgroundColor": "#000000"
64
+ "backgroundColor": "#18181b"
65
65
  }
66
66
  }
67
67
  ],
@@ -83,12 +83,13 @@ import {
83
83
  DarkTheme,
84
84
  DefaultTheme,
85
85
  ThemeProvider,
86
+ type Theme,
86
87
  } from "@react-navigation/native";
87
88
  import { Drawer } from "expo-router/drawer";
88
89
  import { StatusBar } from "expo-status-bar";
89
90
  import "react-native-reanimated";
90
- import { Pressable, useColorScheme } from "react-native";
91
- import { Ionicons } from "@expo/vector-icons";
91
+ import { Platform, Pressable } from "react-native";
92
+ import MaterialIcons from "@expo/vector-icons/MaterialIcons";
92
93
  import { useFonts } from "expo-font";
93
94
  import { GestureHandlerRootView } from "react-native-gesture-handler";
94
95
 
@@ -98,42 +99,60 @@ import {
98
99
  Tools,
99
100
  } from "@assistant-ui/react-native";
100
101
  import { useAppRuntime } from "@/hooks/use-app-runtime";
102
+ import { useTheme } from "@/hooks/use-theme";
103
+ import { Icon } from "@/components/ui/icon";
101
104
  import { ThreadListDrawer } from "@/components/thread-list/ThreadListDrawer";
105
+ import { haptics } from "@/lib/haptics";
102
106
  import toolkit from "@/components/assistant-ui/tools";
103
107
 
104
108
  function NewChatButton() {
105
109
  const aui = useAui();
106
- const colorScheme = useColorScheme();
107
- const isDark = colorScheme === "dark";
110
+ const { colors } = useTheme();
108
111
 
109
112
  return (
110
113
  <Pressable
114
+ accessibilityLabel="New chat"
115
+ hitSlop={8}
111
116
  onPress={() => {
117
+ haptics.selection();
112
118
  aui.threads().switchToNewThread();
113
119
  }}
114
120
  style={{ marginRight: 16 }}
115
121
  >
116
- <Ionicons
117
- name="create-outline"
118
- size={24}
119
- color={isDark ? "#ffffff" : "#000000"}
120
- />
122
+ <Icon name="compose" size={22} color={colors.foreground} />
121
123
  </Pressable>
122
124
  );
123
125
  }
124
126
 
125
127
  function DrawerLayout() {
126
- const colorScheme = useColorScheme();
128
+ const { isDark, colors } = useTheme();
129
+
130
+ const base = isDark ? DarkTheme : DefaultTheme;
131
+ const navTheme: Theme = {
132
+ ...base,
133
+ colors: {
134
+ ...base.colors,
135
+ background: colors.background,
136
+ card: colors.background,
137
+ text: colors.foreground,
138
+ border: colors.border,
139
+ primary: colors.foreground,
140
+ },
141
+ };
127
142
 
128
143
  return (
129
- <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
144
+ <ThemeProvider value={navTheme}>
130
145
  <Drawer
131
146
  drawerContent={(props) => <ThreadListDrawer {...props} />}
132
147
  screenOptions={{
133
148
  headerRight: () => <NewChatButton />,
149
+ headerShadowVisible: false,
150
+ headerTintColor: colors.foreground,
151
+ headerStyle: { backgroundColor: colors.background },
152
+ headerTitleStyle: { fontWeight: "600" },
134
153
  drawerType: "front",
135
154
  swipeEnabled: true,
136
- drawerStyle: { backgroundColor: "transparent" },
155
+ drawerStyle: { backgroundColor: colors.background },
137
156
  }}
138
157
  >
139
158
  <Drawer.Screen name="index" options={{ title: "Chat" }} />
@@ -144,7 +163,10 @@ function DrawerLayout() {
144
163
  }
145
164
 
146
165
  export default function RootLayout() {
147
- const [fontsLoaded] = useFonts(Ionicons.font);
166
+ // iOS renders SF Symbols, so it never needs the MaterialIcons glyph font.
167
+ const [fontsLoaded] = useFonts(
168
+ Platform.OS === "ios" ? {} : MaterialIcons.font,
169
+ );
148
170
  const runtime = useAppRuntime();
149
171
  const aui = useAui({
150
172
  tools: Tools({ toolkit }),
@@ -239,66 +261,105 @@ module.exports = (api) => {
239
261
  ## components/assistant-ui/composer.tsx
240
262
 
241
263
  ```tsx
242
- import {
243
- View,
244
- Pressable,
245
- Image,
246
- Platform,
247
- StyleSheet,
248
- useColorScheme,
249
- } from "react-native";
250
- import { Ionicons } from "@expo/vector-icons";
264
+ import { View, Pressable, Platform, StyleSheet } from "react-native";
265
+ import { Image } from "expo-image";
251
266
  import * as ImagePicker from "expo-image-picker";
252
267
  import {
253
268
  useAui,
254
269
  useAuiState,
270
+ AuiIf,
255
271
  ComposerPrimitive,
256
272
  AttachmentPrimitive,
257
273
  } from "@assistant-ui/react-native";
274
+ import { Icon } from "@/components/ui/icon";
275
+ import { useTheme } from "@/hooks/use-theme";
276
+ import { Radius, Spacing } from "@/constants/theme";
277
+ import { haptics } from "@/lib/haptics";
258
278
 
259
279
  function AttachmentPreview() {
280
+ const { colors } = useTheme();
260
281
  const attachment = useAuiState((s) => s.attachment);
261
282
  if (!attachment) return null;
262
283
 
263
- // Find image content for preview URI
264
284
  const imageContent = attachment.content?.find((c: any) => c.type === "image");
265
285
  const uri = (imageContent as any)?.image;
266
286
 
267
287
  return (
268
288
  <AttachmentPrimitive.Root style={styles.attachmentItem}>
269
- {uri ? <Image source={{ uri }} style={styles.attachmentImage} /> : null}
270
- <AttachmentPrimitive.Remove style={styles.attachmentRemoveButton}>
271
- <Ionicons name="close-circle" size={20} color="#ff453a" />
289
+ {uri ? (
290
+ <Image
291
+ source={{ uri }}
292
+ style={[styles.attachmentImage, { backgroundColor: colors.muted }]}
293
+ contentFit="cover"
294
+ transition={120}
295
+ />
296
+ ) : null}
297
+ <AttachmentPrimitive.Remove
298
+ onPressIn={haptics.light}
299
+ style={styles.attachmentRemove}
300
+ >
301
+ <View
302
+ style={[styles.removeBadge, { backgroundColor: colors.foreground }]}
303
+ >
304
+ <Icon name="remove" size={16} color={colors.background} />
305
+ </View>
272
306
  </AttachmentPrimitive.Remove>
273
307
  </AttachmentPrimitive.Root>
274
308
  );
275
309
  }
276
310
 
277
- export function Composer() {
278
- const colorScheme = useColorScheme();
279
- const isDark = colorScheme === "dark";
311
+ function SendButton() {
312
+ const { colors } = useTheme();
313
+ const canSend = useAuiState((s) => s.composer.canSend);
280
314
 
281
- const aui = useAui();
282
- const attachmentsCount = useAuiState((s) => s.composer.attachments.length);
283
- const canCancel = useAuiState((s) => s.composer.canCancel);
284
- const canSend = useAuiState(
285
- (s) => !s.thread.isRunning && s.composer.isEditing && !s.composer.isEmpty,
315
+ return (
316
+ <ComposerPrimitive.Send
317
+ accessibilityLabel="Send message"
318
+ onPressIn={() => canSend && haptics.success()}
319
+ style={[
320
+ styles.actionButton,
321
+ { backgroundColor: canSend ? colors.primary : colors.muted },
322
+ ]}
323
+ >
324
+ <Icon
325
+ name="send"
326
+ size={18}
327
+ color={canSend ? colors.primaryForeground : colors.mutedForeground}
328
+ />
329
+ </ComposerPrimitive.Send>
330
+ );
331
+ }
332
+
333
+ function CancelButton() {
334
+ const { colors } = useTheme();
335
+ return (
336
+ <ComposerPrimitive.Cancel
337
+ accessibilityLabel="Stop generating"
338
+ onPressIn={haptics.light}
339
+ style={[styles.actionButton, { backgroundColor: colors.foreground }]}
340
+ >
341
+ <Icon name="stop" size={15} color={colors.background} />
342
+ </ComposerPrimitive.Cancel>
286
343
  );
344
+ }
345
+
346
+ function AttachButton() {
347
+ const { colors } = useTheme();
348
+ const aui = useAui();
287
349
 
288
350
  const pickImage = async () => {
351
+ haptics.selection();
289
352
  const result = await ImagePicker.launchImageLibraryAsync({
290
353
  mediaTypes: ["images"],
291
354
  allowsMultipleSelection: true,
292
355
  quality: 0.8,
293
356
  base64: true,
294
357
  });
295
-
296
358
  if (result.canceled) return;
297
359
 
298
360
  for (const asset of result.assets) {
299
- // Force JPEG mime type — iOS may report HEIC which OpenAI doesn't support
361
+ // iOS may report HEIC, which OpenAI rejects; normalize to JPEG.
300
362
  const dataUrl = `data:image/jpeg;base64,${asset.base64}`;
301
-
302
363
  await aui.composer().addAttachment({
303
364
  name: asset.fileName ?? "image.jpg",
304
365
  contentType: "image/jpeg",
@@ -309,78 +370,57 @@ export function Composer() {
309
370
  };
310
371
 
311
372
  return (
312
- <View
313
- style={[
314
- styles.container,
315
- {
316
- backgroundColor: isDark
317
- ? "rgba(28, 28, 30, 0.8)"
318
- : "rgba(242, 242, 247, 0.8)",
319
- },
373
+ <Pressable
374
+ accessibilityLabel="Add image"
375
+ onPress={pickImage}
376
+ hitSlop={6}
377
+ style={({ pressed }) => [
378
+ styles.attachButton,
379
+ { backgroundColor: pressed ? colors.muted : "transparent" },
320
380
  ]}
321
381
  >
322
- {attachmentsCount > 0 && (
323
- <View style={styles.attachmentsList}>
324
- <ComposerPrimitive.Attachments>
325
- {() => <AttachmentPreview />}
326
- </ComposerPrimitive.Attachments>
327
- </View>
328
- )}
382
+ <Icon name="add" size={20} color={colors.mutedForeground} />
383
+ </Pressable>
384
+ );
385
+ }
386
+
387
+ export function Composer() {
388
+ const { colors } = useTheme();
389
+ const hasAttachments = useAuiState((s) => s.composer.attachments.length > 0);
390
+
391
+ return (
392
+ <View style={styles.container}>
329
393
  <View
330
394
  style={[
331
- styles.inputWrapper,
332
- {
333
- backgroundColor: isDark ? "#1c1c1e" : "#ffffff",
334
- borderColor: isDark ? "#3a3a3c" : "#e5e5ea",
335
- },
395
+ styles.shell,
396
+ { backgroundColor: colors.composer, borderColor: colors.border },
336
397
  ]}
337
398
  >
338
- <Pressable
339
- style={styles.attachButton}
340
- onPress={pickImage}
341
- disabled={canCancel}
342
- >
343
- <Ionicons
344
- name="add-circle-outline"
345
- size={24}
346
- color={isDark ? "#8e8e93" : "#6e6e73"}
347
- />
348
- </Pressable>
399
+ {hasAttachments && (
400
+ <View style={styles.attachmentsRow}>
401
+ <ComposerPrimitive.Attachments>
402
+ {() => <AttachmentPreview />}
403
+ </ComposerPrimitive.Attachments>
404
+ </View>
405
+ )}
406
+
349
407
  <ComposerPrimitive.Input
350
- style={[styles.input, { color: isDark ? "#ffffff" : "#000000" }]}
351
- placeholder="Message..."
352
- placeholderTextColor="#8e8e93"
408
+ style={[styles.input, { color: colors.foreground }]}
409
+ placeholder="Message…"
410
+ placeholderTextColor={colors.mutedForeground}
353
411
  multiline
354
412
  maxLength={4000}
355
- editable={!canCancel}
356
413
  />
357
- {canCancel ? (
358
- <ComposerPrimitive.Cancel style={[styles.button, styles.stopButton]}>
359
- <View style={styles.stopIcon} />
360
- </ComposerPrimitive.Cancel>
361
- ) : (
362
- <ComposerPrimitive.Send
363
- style={[
364
- styles.button,
365
- styles.sendButton,
366
- {
367
- backgroundColor: canSend
368
- ? isDark
369
- ? "#0a84ff"
370
- : "#007aff"
371
- : isDark
372
- ? "#3a3a3c"
373
- : "#e5e5ea",
374
- },
375
- ]}
376
- >
377
- <Ionicons
378
- name="arrow-up"
379
- size={20}
380
- color={canSend ? "#ffffff" : "#8e8e93"}
381
- />
382
- </ComposerPrimitive.Send>
383
- )}
414
+
415
+ <View style={styles.actionRow}>
416
+ <AttachButton />
417
+ <AuiIf condition={(s) => !s.thread.isRunning}>
418
+ <SendButton />
419
+ </AuiIf>
420
+ <AuiIf condition={(s) => s.thread.isRunning}>
421
+ <CancelButton />
422
+ </AuiIf>
423
+ </View>
384
424
  </View>
385
425
  </View>
386
426
  );
@@ -388,69 +428,77 @@ export function Composer() {
388
428
 
389
429
  const styles = StyleSheet.create({
390
430
  container: {
391
- paddingHorizontal: 16,
392
- paddingVertical: 8,
431
+ width: "100%",
432
+ maxWidth: Spacing.threadMaxWidth + 24,
433
+ alignSelf: "center",
434
+ paddingHorizontal: 12,
435
+ paddingTop: 8,
436
+ },
437
+ shell: {
438
+ flexDirection: "column",
439
+ gap: 6,
440
+ borderRadius: Radius.composer,
441
+ borderWidth: StyleSheet.hairlineWidth,
442
+ padding: 8,
393
443
  },
394
- attachmentsList: {
444
+ attachmentsRow: {
395
445
  flexDirection: "row",
396
446
  flexWrap: "wrap",
397
447
  gap: 8,
398
- paddingBottom: 8,
448
+ paddingHorizontal: 4,
449
+ paddingTop: 4,
399
450
  },
400
451
  attachmentItem: {
401
452
  position: "relative",
402
453
  },
403
454
  attachmentImage: {
404
- width: 60,
405
- height: 60,
406
- borderRadius: 8,
455
+ width: 56,
456
+ height: 56,
457
+ borderRadius: Radius.attachment,
407
458
  },
408
- attachmentRemoveButton: {
459
+ attachmentRemove: {
409
460
  position: "absolute",
410
461
  top: -6,
411
462
  right: -6,
412
463
  },
413
- inputWrapper: {
414
- flexDirection: "row",
415
- alignItems: "flex-end",
416
- borderRadius: 20,
417
- borderWidth: 1,
418
- padding: 6,
419
- },
420
- attachButton: {
421
- width: 30,
422
- height: 30,
423
- justifyContent: "center",
464
+ removeBadge: {
465
+ width: 20,
466
+ height: 20,
467
+ borderRadius: 10,
424
468
  alignItems: "center",
469
+ justifyContent: "center",
425
470
  },
426
471
  input: {
427
- flex: 1,
428
472
  fontSize: 16,
429
- maxHeight: 120,
430
- alignSelf: "center",
431
- paddingVertical: 0,
473
+ lineHeight: 22,
474
+ minHeight: 28,
475
+ maxHeight: 132,
476
+ paddingHorizontal: 10,
477
+ paddingTop: 6,
478
+ paddingBottom: 2,
432
479
  ...Platform.select({
433
- web: { paddingHorizontal: 4, outlineStyle: "none" } as any,
480
+ web: { outlineStyle: "none" } as any,
434
481
  default: {},
435
482
  }),
436
483
  },
437
- button: {
438
- width: 30,
439
- height: 30,
440
- borderRadius: 15,
441
- justifyContent: "center",
484
+ actionRow: {
485
+ flexDirection: "row",
442
486
  alignItems: "center",
443
- marginLeft: 6,
487
+ justifyContent: "space-between",
444
488
  },
445
- stopButton: {
446
- backgroundColor: "#ff453a",
489
+ attachButton: {
490
+ width: 32,
491
+ height: 32,
492
+ borderRadius: Radius.pill,
493
+ alignItems: "center",
494
+ justifyContent: "center",
447
495
  },
448
- sendButton: {},
449
- stopIcon: {
450
- width: 12,
451
- height: 12,
452
- borderRadius: 2,
453
- backgroundColor: "#ffffff",
496
+ actionButton: {
497
+ width: 32,
498
+ height: 32,
499
+ borderRadius: Radius.pill,
500
+ alignItems: "center",
501
+ justifyContent: "center",
454
502
  },
455
503
  });
456
504
 
@@ -459,28 +507,46 @@ const styles = StyleSheet.create({
459
507
  ## components/assistant-ui/message-action-bar.tsx
460
508
 
461
509
  ```tsx
462
- import { View, StyleSheet, useColorScheme } from "react-native";
463
- import { Ionicons } from "@expo/vector-icons";
510
+ import { View, StyleSheet } from "react-native";
511
+ import * as Clipboard from "expo-clipboard";
512
+ import { Icon } from "@/components/ui/icon";
464
513
  import { ActionBarPrimitive } from "@assistant-ui/react-native";
514
+ import { useTheme } from "@/hooks/use-theme";
515
+ import { haptics } from "@/lib/haptics";
516
+ import { Radius } from "@/constants/theme";
517
+
518
+ const copyToClipboard = async (text: string) => {
519
+ await Clipboard.setStringAsync(text);
520
+ };
465
521
 
466
522
  export function MessageActionBar() {
467
- const colorScheme = useColorScheme();
468
- const isDark = colorScheme === "dark";
469
- const iconColor = isDark ? "#8e8e93" : "#6e6e73";
523
+ const { colors } = useTheme();
524
+
525
+ const buttonStyle = ({ pressed }: { pressed: boolean }) => [
526
+ styles.button,
527
+ pressed && { backgroundColor: colors.muted },
528
+ ];
470
529
 
471
530
  return (
472
531
  <View style={styles.container}>
473
- <ActionBarPrimitive.Copy style={styles.button}>
532
+ <ActionBarPrimitive.Copy
533
+ copyToClipboard={copyToClipboard}
534
+ onPressIn={haptics.selection}
535
+ style={buttonStyle}
536
+ >
474
537
  {({ isCopied }) => (
475
- <Ionicons
476
- name={isCopied ? "checkmark" : "copy-outline"}
538
+ <Icon
539
+ name={isCopied ? "check" : "copy"}
477
540
  size={16}
478
- color={isCopied ? "#34c759" : iconColor}
541
+ color={isCopied ? colors.foreground : colors.mutedForeground}
479
542
  />
480
543
  )}
481
544
  </ActionBarPrimitive.Copy>
482
- <ActionBarPrimitive.Reload style={styles.button}>
483
- <Ionicons name="refresh-outline" size={16} color={iconColor} />
545
+ <ActionBarPrimitive.Reload
546
+ onPressIn={haptics.selection}
547
+ style={buttonStyle}
548
+ >
549
+ <Icon name="reload" size={16} color={colors.mutedForeground} />
484
550
  </ActionBarPrimitive.Reload>
485
551
  </View>
486
552
  );
@@ -489,12 +555,12 @@ export function MessageActionBar() {
489
555
  const styles = StyleSheet.create({
490
556
  container: {
491
557
  flexDirection: "row",
492
- gap: 4,
493
- marginTop: 4,
558
+ alignItems: "center",
559
+ gap: 2,
494
560
  },
495
561
  button: {
496
562
  padding: 6,
497
- borderRadius: 8,
563
+ borderRadius: Radius.sm,
498
564
  },
499
565
  });
500
566
 
@@ -503,47 +569,41 @@ const styles = StyleSheet.create({
503
569
  ## components/assistant-ui/message-branch-picker.tsx
504
570
 
505
571
  ```tsx
506
- import { View, StyleSheet, useColorScheme } from "react-native";
507
- import { Ionicons } from "@expo/vector-icons";
508
- import { ThemedText } from "@/components/themed-text";
572
+ import { View, Text, StyleSheet } from "react-native";
573
+ import { Icon } from "@/components/ui/icon";
509
574
  import { BranchPickerPrimitive, useAuiState } from "@assistant-ui/react-native";
575
+ import { useTheme } from "@/hooks/use-theme";
510
576
 
511
- export function MessageBranchPicker() {
577
+ export function MessageBranchPicker({
578
+ align = "flex-start",
579
+ }: {
580
+ align?: "flex-start" | "flex-end";
581
+ }) {
582
+ const { colors } = useTheme();
512
583
  const branchNumber = useAuiState((s) => s.message.branchNumber);
513
584
  const branchCount = useAuiState((s) => s.message.branchCount);
514
585
 
515
- const colorScheme = useColorScheme();
516
- const isDark = colorScheme === "dark";
517
- const iconColor = isDark ? "#8e8e93" : "#6e6e73";
518
-
519
586
  if (branchCount <= 1) return null;
520
587
 
521
588
  return (
522
- <View style={styles.container}>
523
- <BranchPickerPrimitive.Previous style={styles.button}>
524
- <Ionicons
525
- name="chevron-back"
526
- size={14}
527
- color={
528
- branchNumber <= 1 ? (isDark ? "#3a3a3c" : "#d1d1d6") : iconColor
529
- }
530
- />
589
+ <View style={[styles.container, { justifyContent: align }]}>
590
+ <BranchPickerPrimitive.Previous
591
+ style={[styles.button, { opacity: branchNumber <= 1 ? 0.35 : 1 }]}
592
+ hitSlop={4}
593
+ >
594
+ <Icon name="chevron-left" size={16} color={colors.mutedForeground} />
531
595
  </BranchPickerPrimitive.Previous>
532
- <ThemedText style={styles.label} lightColor="#6e6e73" darkColor="#8e8e93">
533
- {branchNumber} / {branchCount}
534
- </ThemedText>
535
- <BranchPickerPrimitive.Next style={styles.button}>
536
- <Ionicons
537
- name="chevron-forward"
538
- size={14}
539
- color={
540
- branchNumber >= branchCount
541
- ? isDark
542
- ? "#3a3a3c"
543
- : "#d1d1d6"
544
- : iconColor
545
- }
546
- />
596
+ <Text style={[styles.label, { color: colors.mutedForeground }]}>
597
+ <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
598
+ </Text>
599
+ <BranchPickerPrimitive.Next
600
+ style={[
601
+ styles.button,
602
+ { opacity: branchNumber >= branchCount ? 0.35 : 1 },
603
+ ]}
604
+ hitSlop={4}
605
+ >
606
+ <Icon name="chevron-right" size={16} color={colors.mutedForeground} />
547
607
  </BranchPickerPrimitive.Next>
548
608
  </View>
549
609
  );
@@ -557,7 +617,6 @@ const styles = StyleSheet.create({
557
617
  },
558
618
  button: {
559
619
  padding: 4,
560
- borderRadius: 6,
561
620
  },
562
621
  label: {
563
622
  fontSize: 12,
@@ -570,53 +629,82 @@ const styles = StyleSheet.create({
570
629
  ## components/assistant-ui/message.tsx
571
630
 
572
631
  ```tsx
573
- import { View, Image, StyleSheet, useColorScheme } from "react-native";
574
- import { ThemedText } from "@/components/themed-text";
575
- import { useAuiState, MessagePrimitive } from "@assistant-ui/react-native";
632
+ import { useEffect, useRef } from "react";
633
+ import { View, Text, Animated, Platform, StyleSheet } from "react-native";
634
+ import { Image } from "expo-image";
635
+ import {
636
+ useAuiState,
637
+ MessagePrimitive,
638
+ ErrorPrimitive,
639
+ type TextMessagePartComponent,
640
+ } from "@assistant-ui/react-native";
641
+ import { useTheme } from "@/hooks/use-theme";
642
+ import { Radius } from "@/constants/theme";
576
643
  import { MessageActionBar } from "./message-action-bar";
577
644
  import { MessageBranchPicker } from "./message-branch-picker";
578
645
 
579
- function MessageError() {
580
- const error = useAuiState((s) => {
581
- const status = s.message.status;
582
- if (status?.type === "incomplete" && status.reason === "error") {
583
- return status.error ?? "An error occurred";
584
- }
585
- return null;
586
- });
646
+ const UserText: TextMessagePartComponent = ({ text }) => {
647
+ const { colors } = useTheme();
648
+ return (
649
+ <Text style={[styles.userText, { color: colors.foreground }]}>{text}</Text>
650
+ );
651
+ };
652
+
653
+ const AssistantText: TextMessagePartComponent = ({ text }) => {
654
+ const { colors } = useTheme();
655
+ return (
656
+ <Text style={[styles.assistantText, { color: colors.foreground }]}>
657
+ {text}
658
+ </Text>
659
+ );
660
+ };
661
+
662
+ function TypingDot({ delay }: { delay: number }) {
663
+ const { colors } = useTheme();
664
+ const opacity = useRef(new Animated.Value(0.3)).current;
587
665
 
588
- if (!error) return null;
666
+ useEffect(() => {
667
+ const anim = Animated.loop(
668
+ Animated.sequence([
669
+ Animated.timing(opacity, {
670
+ toValue: 1,
671
+ duration: 400,
672
+ delay,
673
+ useNativeDriver: Platform.OS !== "web",
674
+ }),
675
+ Animated.timing(opacity, {
676
+ toValue: 0.3,
677
+ duration: 400,
678
+ useNativeDriver: Platform.OS !== "web",
679
+ }),
680
+ ]),
681
+ );
682
+ anim.start();
683
+ return () => anim.stop();
684
+ }, [opacity, delay]);
589
685
 
590
686
  return (
591
- <View style={styles.errorContainer}>
592
- <ThemedText
593
- style={styles.errorText}
594
- lightColor="#ff453a"
595
- darkColor="#ff6961"
596
- >
597
- {typeof error === "string" ? error : "An error occurred"}
598
- </ThemedText>
599
- </View>
687
+ <Animated.View
688
+ style={[styles.dot, { opacity, backgroundColor: colors.mutedForeground }]}
689
+ />
600
690
  );
601
691
  }
602
692
 
603
- function TextPart({ part }: { part: { type: "text"; text: string } }) {
604
- const role = useAuiState((s) => s.message.role);
605
- if (role === "user") {
606
- return <ThemedText style={styles.userText}>{part.text}</ThemedText>;
607
- }
693
+ function TypingIndicator() {
694
+ const isRunning = useAuiState((s) => s.message.status?.type === "running");
695
+ if (!isRunning) return null;
696
+
608
697
  return (
609
- <ThemedText
610
- style={styles.assistantText}
611
- lightColor="#000000"
612
- darkColor="#ffffff"
613
- >
614
- {part.text}
615
- </ThemedText>
698
+ <View style={styles.typing}>
699
+ <TypingDot delay={0} />
700
+ <TypingDot delay={160} />
701
+ <TypingDot delay={320} />
702
+ </View>
616
703
  );
617
704
  }
618
705
 
619
706
  function MessageImageAttachment() {
707
+ const { colors } = useTheme();
620
708
  const attachment = useAuiState((s) => s.attachment);
621
709
  if (!attachment) return null;
622
710
 
@@ -624,119 +712,138 @@ function MessageImageAttachment() {
624
712
  const uri = (imageContent as any)?.image;
625
713
  if (!uri) return null;
626
714
 
627
- return <Image source={{ uri }} style={styles.messageImage} />;
715
+ return (
716
+ <Image
717
+ source={{ uri }}
718
+ style={[styles.messageImage, { backgroundColor: colors.muted }]}
719
+ contentFit="cover"
720
+ transition={120}
721
+ />
722
+ );
628
723
  }
629
724
 
630
- export function MessageBubble() {
631
- const colorScheme = useColorScheme();
632
- const isDark = colorScheme === "dark";
633
- const role = useAuiState((s) => s.message.role);
634
- const isRunning = useAuiState((s) => s.message.status?.type === "running");
635
- const isUser = role === "user";
636
-
637
- if (isUser) {
638
- return (
639
- <View style={[styles.container, styles.userContainer]}>
725
+ function UserMessage() {
726
+ const { colors } = useTheme();
727
+ return (
728
+ <MessagePrimitive.Root style={styles.userContainer}>
729
+ <View style={styles.userAttachments}>
640
730
  <MessagePrimitive.Attachments>
641
731
  {() => <MessageImageAttachment />}
642
732
  </MessagePrimitive.Attachments>
643
- <View
644
- style={[
645
- styles.bubble,
646
- styles.userBubble,
647
- { backgroundColor: isDark ? "#0a84ff" : "#007aff" },
648
- ]}
649
- >
650
- <MessagePrimitive.Content
651
- renderText={({ part }) => <TextPart part={part} />}
652
- />
653
- </View>
654
- <MessageBranchPicker />
655
733
  </View>
656
- );
657
- }
734
+ <View style={[styles.userBubble, { backgroundColor: colors.muted }]}>
735
+ <MessagePrimitive.Parts components={{ Text: UserText }} />
736
+ </View>
737
+ <MessageBranchPicker align="flex-end" />
738
+ </MessagePrimitive.Root>
739
+ );
740
+ }
658
741
 
742
+ function AssistantMessage() {
743
+ const { colors } = useTheme();
659
744
  return (
660
- <View style={[styles.container, styles.assistantContainer]}>
661
- <View
662
- style={[
663
- styles.bubble,
664
- styles.assistantBubble,
665
- {
666
- backgroundColor: isDark
667
- ? "rgba(44, 44, 46, 0.8)"
668
- : "rgba(229, 229, 234, 0.8)",
669
- },
670
- ]}
671
- >
672
- <MessagePrimitive.Content
673
- renderText={({ part }) => <TextPart part={part} />}
745
+ <MessagePrimitive.Root style={styles.assistantContainer}>
746
+ <View style={styles.assistantContent}>
747
+ <MessagePrimitive.Parts
748
+ components={{ Text: AssistantText, Empty: TypingIndicator }}
674
749
  />
675
- <MessageError />
750
+ <ErrorPrimitive.Root
751
+ style={[
752
+ styles.error,
753
+ {
754
+ backgroundColor: colors.destructiveSurface,
755
+ borderColor: colors.destructive,
756
+ },
757
+ ]}
758
+ >
759
+ <ErrorPrimitive.Message
760
+ style={[styles.errorText, { color: colors.destructive }]}
761
+ />
762
+ </ErrorPrimitive.Root>
676
763
  </View>
677
- {!isRunning && (
764
+ <MessagePrimitive.If running={false}>
678
765
  <View style={styles.actionsRow}>
679
- <MessageBranchPicker />
766
+ <MessageBranchPicker align="flex-start" />
680
767
  <MessageActionBar />
681
768
  </View>
682
- )}
683
- </View>
769
+ </MessagePrimitive.If>
770
+ </MessagePrimitive.Root>
684
771
  );
685
772
  }
686
773
 
774
+ export function MessageBubble() {
775
+ const role = useAuiState((s) => s.message.role);
776
+ if (role === "user") return <UserMessage />;
777
+ return <AssistantMessage />;
778
+ }
779
+
687
780
  const styles = StyleSheet.create({
688
- container: {
689
- paddingHorizontal: 16,
690
- paddingVertical: 6,
691
- },
692
781
  userContainer: {
693
782
  alignItems: "flex-end",
694
783
  },
695
- assistantContainer: {
696
- alignItems: "flex-start",
784
+ userAttachments: {
785
+ flexDirection: "row",
786
+ flexWrap: "wrap",
787
+ justifyContent: "flex-end",
788
+ gap: 6,
697
789
  },
698
- bubble: {
790
+ userBubble: {
699
791
  maxWidth: "85%",
700
792
  paddingHorizontal: 16,
701
- paddingVertical: 12,
702
- borderRadius: 20,
703
- },
704
- userBubble: {
705
- borderBottomRightRadius: 6,
706
- },
707
- actionsRow: {
708
- flexDirection: "row",
709
- alignItems: "center",
710
- gap: 4,
711
- marginTop: 4,
712
- },
713
- assistantBubble: {
714
- borderBottomLeftRadius: 6,
715
- },
716
- errorContainer: {
717
- paddingTop: 4,
793
+ paddingVertical: 10,
794
+ borderRadius: Radius.bubble,
718
795
  },
719
- errorText: {
720
- fontSize: 14,
721
- lineHeight: 20,
796
+ assistantContainer: {
797
+ alignItems: "flex-start",
722
798
  },
723
- messageImage: {
724
- width: 200,
725
- height: 200,
726
- borderRadius: 12,
727
- marginBottom: 4,
799
+ assistantContent: {
800
+ paddingHorizontal: 2,
728
801
  },
729
802
  userText: {
730
803
  fontSize: 16,
731
804
  lineHeight: 22,
732
- color: "#ffffff",
733
805
  letterSpacing: -0.2,
734
806
  },
735
807
  assistantText: {
736
808
  fontSize: 16,
737
- lineHeight: 24,
809
+ lineHeight: 25,
738
810
  letterSpacing: -0.2,
739
811
  },
812
+ typing: {
813
+ flexDirection: "row",
814
+ alignItems: "center",
815
+ gap: 5,
816
+ paddingVertical: 8,
817
+ },
818
+ dot: {
819
+ width: 7,
820
+ height: 7,
821
+ borderRadius: 3.5,
822
+ },
823
+ messageImage: {
824
+ width: 200,
825
+ height: 200,
826
+ borderRadius: Radius.card,
827
+ marginBottom: 6,
828
+ },
829
+ actionsRow: {
830
+ flexDirection: "row",
831
+ alignItems: "center",
832
+ gap: 4,
833
+ marginTop: 6,
834
+ marginLeft: -4,
835
+ },
836
+ error: {
837
+ marginTop: 8,
838
+ borderRadius: Radius.md,
839
+ borderWidth: StyleSheet.hairlineWidth,
840
+ paddingHorizontal: 12,
841
+ paddingVertical: 10,
842
+ },
843
+ errorText: {
844
+ fontSize: 14,
845
+ lineHeight: 20,
846
+ },
740
847
  });
741
848
 
742
849
  ```
@@ -744,101 +851,71 @@ const styles = StyleSheet.create({
744
851
  ## components/assistant-ui/thread.tsx
745
852
 
746
853
  ```tsx
854
+ import { useRef } from "react";
747
855
  import {
748
856
  View,
749
857
  Text,
750
- Pressable,
751
858
  StyleSheet,
752
859
  KeyboardAvoidingView,
753
860
  Platform,
754
- useColorScheme,
861
+ type FlatList,
755
862
  } from "react-native";
756
863
  import { useSafeAreaInsets } from "react-native-safe-area-context";
757
864
  import { MessageBubble } from "./message";
758
865
  import { Composer } from "./composer";
759
- import { ThreadPrimitive, useAui } from "@assistant-ui/react-native";
760
-
761
- function SuggestionChip({ title, prompt }: { title: string; prompt: string }) {
762
- const colorScheme = useColorScheme();
763
- const isDark = colorScheme === "dark";
764
- const aui = useAui();
866
+ import { ThreadPrimitive } from "@assistant-ui/react-native";
867
+ import { useTheme } from "@/hooks/use-theme";
868
+ import { Radius, Spacing } from "@/constants/theme";
869
+ import { haptics } from "@/lib/haptics";
870
+
871
+ const suggestions = [
872
+ "What's the weather in Tokyo?",
873
+ "Tell me a joke",
874
+ "Help me write an email",
875
+ ];
765
876
 
877
+ function SuggestionChip({ prompt }: { prompt: string }) {
878
+ const { colors } = useTheme();
766
879
  return (
767
- <Pressable
768
- onPress={() => aui.thread().append(prompt)}
769
- style={[
770
- styles.suggestionChip,
880
+ <ThreadPrimitive.Suggestion
881
+ prompt={prompt}
882
+ send
883
+ onPressIn={haptics.selection}
884
+ style={({ pressed }: { pressed: boolean }) => [
885
+ styles.chip,
771
886
  {
772
- backgroundColor: isDark
773
- ? "rgba(44, 44, 46, 0.8)"
774
- : "rgba(229, 229, 234, 0.8)",
887
+ borderColor: colors.border,
888
+ backgroundColor: pressed ? colors.muted : colors.background,
775
889
  },
776
890
  ]}
777
891
  >
778
- <Text
779
- style={[
780
- styles.suggestionText,
781
- { color: isDark ? "#ffffff" : "#000000" },
782
- ]}
783
- >
784
- {title}
892
+ <Text style={[styles.chipText, { color: colors.foreground }]}>
893
+ {prompt}
785
894
  </Text>
786
- </Pressable>
787
- );
788
- }
789
-
790
- const defaultSuggestions = [
791
- {
792
- title: "What's the weather in Tokyo?",
793
- prompt: "What's the weather in Tokyo?",
794
- },
795
- { title: "Tell me a joke", prompt: "Tell me a joke" },
796
- { title: "Help me write an email", prompt: "Help me write an email" },
797
- ];
798
-
799
- function Suggestions() {
800
- return (
801
- <View style={styles.suggestionsContainer}>
802
- {defaultSuggestions.map((s, i) => (
803
- <SuggestionChip key={i} title={s.title} prompt={s.prompt} />
804
- ))}
805
- </View>
895
+ </ThreadPrimitive.Suggestion>
806
896
  );
807
897
  }
808
898
 
809
899
  function EmptyState() {
810
- const colorScheme = useColorScheme();
811
- const isDark = colorScheme === "dark";
812
-
900
+ const { colors } = useTheme();
813
901
  return (
814
- <View
815
- style={[
816
- styles.emptyContainer,
817
- { backgroundColor: isDark ? "#000000" : "#ffffff" },
818
- ]}
819
- >
820
- <View style={styles.emptyIconContainer}>
821
- <Text style={styles.emptyIcon}>💭</Text>
822
- </View>
823
- <Text
824
- style={[styles.emptyTitle, { color: isDark ? "#ffffff" : "#000000" }]}
825
- >
826
- How can I help?
827
- </Text>
828
- <Text
829
- style={[
830
- styles.emptySubtitle,
831
- { color: isDark ? "#8e8e93" : "#6e6e73" },
832
- ]}
833
- >
834
- Send a message to start chatting
902
+ <View style={styles.empty}>
903
+ <Text style={[styles.welcome, { color: colors.foreground }]}>
904
+ How can I help you today?
835
905
  </Text>
836
- <Suggestions />
906
+ <View style={styles.chips}>
907
+ {suggestions.map((prompt) => (
908
+ <SuggestionChip key={prompt} prompt={prompt} />
909
+ ))}
910
+ </View>
837
911
  </View>
838
912
  );
839
913
  }
840
914
 
841
915
  function ChatMessages() {
916
+ const listRef = useRef<FlatList>(null);
917
+ const nearBottom = useRef(true);
918
+
842
919
  return (
843
920
  <>
844
921
  <ThreadPrimitive.Empty>
@@ -846,8 +923,24 @@ function ChatMessages() {
846
923
  </ThreadPrimitive.Empty>
847
924
  <ThreadPrimitive.If empty={false}>
848
925
  <ThreadPrimitive.Messages
926
+ // Messages isn't a forwardRef component, but it spreads extra props
927
+ // onto its FlatList, so React 19 still forwards this ref to the list.
928
+ {...({ ref: listRef } as object)}
929
+ style={styles.flex}
849
930
  contentContainerStyle={styles.messageList}
850
931
  showsVerticalScrollIndicator={false}
932
+ scrollEventThrottle={32}
933
+ onScroll={(e) => {
934
+ const { contentOffset, contentSize, layoutMeasurement } =
935
+ e.nativeEvent;
936
+ nearBottom.current =
937
+ contentOffset.y + layoutMeasurement.height >=
938
+ contentSize.height - 48;
939
+ }}
940
+ onContentSizeChange={() => {
941
+ if (nearBottom.current)
942
+ listRef.current?.scrollToEnd({ animated: true });
943
+ }}
851
944
  >
852
945
  {() => <MessageBubble />}
853
946
  </ThreadPrimitive.Messages>
@@ -858,23 +951,18 @@ function ChatMessages() {
858
951
 
859
952
  export function Thread() {
860
953
  const insets = useSafeAreaInsets();
861
- const colorScheme = useColorScheme();
862
- const isDark = colorScheme === "dark";
954
+ const { colors } = useTheme();
955
+
863
956
  return (
864
- <View
865
- style={[
866
- styles.container,
867
- { backgroundColor: isDark ? "#000000" : "#ffffff" },
868
- ]}
869
- >
957
+ <View style={[styles.container, { backgroundColor: colors.background }]}>
870
958
  <KeyboardAvoidingView
871
- style={styles.keyboardAvoid}
872
- behavior={Platform.OS === "ios" ? "padding" : "height"}
959
+ style={styles.flex}
960
+ behavior={Platform.OS === "ios" ? "padding" : undefined}
873
961
  >
874
- <View style={styles.messagesContainer}>
962
+ <View style={styles.flex}>
875
963
  <ChatMessages />
876
964
  </View>
877
- <View style={{ paddingBottom: insets.bottom }}>
965
+ <View style={{ paddingBottom: insets.bottom + 8 }}>
878
966
  <Composer />
879
967
  </View>
880
968
  </KeyboardAvoidingView>
@@ -886,59 +974,43 @@ const styles = StyleSheet.create({
886
974
  container: {
887
975
  flex: 1,
888
976
  },
889
- keyboardAvoid: {
890
- flex: 1,
891
- },
892
- messagesContainer: {
977
+ flex: {
893
978
  flex: 1,
894
979
  },
895
980
  messageList: {
981
+ width: "100%",
982
+ maxWidth: Spacing.threadMaxWidth,
983
+ marginHorizontal: "auto",
896
984
  paddingVertical: 20,
897
- paddingHorizontal: 4,
985
+ paddingHorizontal: 12,
986
+ gap: 18,
898
987
  },
899
- emptyContainer: {
988
+ empty: {
900
989
  flex: 1,
901
990
  justifyContent: "center",
902
991
  alignItems: "center",
903
- padding: 40,
992
+ paddingHorizontal: 24,
904
993
  },
905
- emptyIconContainer: {
906
- width: 72,
907
- height: 72,
908
- borderRadius: 36,
909
- backgroundColor: "rgba(0, 122, 255, 0.1)",
910
- justifyContent: "center",
911
- alignItems: "center",
912
- marginBottom: 20,
913
- },
914
- emptyIcon: {
915
- fontSize: 32,
916
- },
917
- emptyTitle: {
918
- fontSize: 22,
994
+ welcome: {
995
+ fontSize: 24,
919
996
  fontWeight: "600",
920
- marginBottom: 8,
921
997
  letterSpacing: -0.4,
922
- },
923
- emptySubtitle: {
924
- fontSize: 15,
925
998
  textAlign: "center",
926
- letterSpacing: -0.2,
999
+ marginBottom: 24,
927
1000
  },
928
- suggestionsContainer: {
1001
+ chips: {
929
1002
  flexDirection: "row",
930
1003
  flexWrap: "wrap",
931
1004
  justifyContent: "center",
932
1005
  gap: 8,
933
- marginTop: 20,
934
- paddingHorizontal: 16,
935
1006
  },
936
- suggestionChip: {
937
- paddingHorizontal: 16,
938
- paddingVertical: 10,
939
- borderRadius: 18,
1007
+ chip: {
1008
+ paddingHorizontal: 14,
1009
+ paddingVertical: 8,
1010
+ borderRadius: Radius.pill,
1011
+ borderWidth: StyleSheet.hairlineWidth,
940
1012
  },
941
- suggestionText: {
1013
+ chipText: {
942
1014
  fontSize: 14,
943
1015
  letterSpacing: -0.2,
944
1016
  },
@@ -951,12 +1023,15 @@ const styles = StyleSheet.create({
951
1023
  ```tsx
952
1024
  "use generative";
953
1025
 
954
- import { View, Text, StyleSheet, useColorScheme } from "react-native";
1026
+ import type { ReactNode } from "react";
1027
+ import { View, Text, StyleSheet } from "react-native";
955
1028
  import {
956
1029
  defineToolkit,
957
1030
  type ToolCallMessagePartProps,
958
1031
  } from "@assistant-ui/react-native";
959
1032
  import { z } from "zod";
1033
+ import { useTheme } from "@/hooks/use-theme";
1034
+ import { Radius } from "@/constants/theme";
960
1035
 
961
1036
  // Open-Meteo API adapters (free, no API key needed)
962
1037
 
@@ -993,16 +1068,16 @@ const mapWeatherCode = (code: number): string => {
993
1068
  };
994
1069
 
995
1070
  const mapWeatherEmoji = (code: number): string => {
996
- if (code === 0) return "\u2600\uFE0F";
997
- if (code <= 3) return "\u26C5";
998
- if (code <= 48) return "\uD83C\uDF2B\uFE0F";
999
- if (code <= 57) return "\uD83C\uDF26\uFE0F";
1000
- if (code <= 67) return "\uD83C\uDF27\uFE0F";
1001
- if (code <= 77) return "\u2744\uFE0F";
1002
- if (code <= 82) return "\uD83C\uDF26\uFE0F";
1003
- if (code <= 86) return "\uD83C\uDF28\uFE0F";
1004
- if (code === 95) return "\u26C8\uFE0F";
1005
- return "\uD83C\uDF29\uFE0F";
1071
+ if (code === 0) return "☀️";
1072
+ if (code <= 3) return "⛅";
1073
+ if (code <= 48) return "🌫️";
1074
+ if (code <= 57) return "🌦️";
1075
+ if (code <= 67) return "🌧️";
1076
+ if (code <= 77) return "❄️";
1077
+ if (code <= 82) return "🌦️";
1078
+ if (code <= 86) return "🌨️";
1079
+ if (code === 95) return "⛈️";
1080
+ return "🌩️";
1006
1081
  };
1007
1082
 
1008
1083
  const fetchWeatherFromOpenMeteo = async ({
@@ -1059,6 +1134,50 @@ const fetchWeatherFromOpenMeteo = async ({
1059
1134
 
1060
1135
  // Tool UI Components
1061
1136
 
1137
+ function ToolCard({ children }: { children: ReactNode }) {
1138
+ const { colors } = useTheme();
1139
+ return (
1140
+ <View
1141
+ style={[
1142
+ styles.card,
1143
+ { backgroundColor: colors.muted, borderColor: colors.border },
1144
+ ]}
1145
+ >
1146
+ {children}
1147
+ </View>
1148
+ );
1149
+ }
1150
+
1151
+ function ToolStatus({ label }: { label: string }) {
1152
+ const { colors } = useTheme();
1153
+ return (
1154
+ <ToolCard>
1155
+ <Text style={[styles.statusText, { color: colors.mutedForeground }]}>
1156
+ {label}
1157
+ </Text>
1158
+ </ToolCard>
1159
+ );
1160
+ }
1161
+
1162
+ function ToolError({ label }: { label: string }) {
1163
+ const { colors } = useTheme();
1164
+ return (
1165
+ <View
1166
+ style={[
1167
+ styles.card,
1168
+ {
1169
+ backgroundColor: colors.destructiveSurface,
1170
+ borderColor: colors.destructive,
1171
+ },
1172
+ ]}
1173
+ >
1174
+ <Text style={[styles.statusText, { color: colors.destructive }]}>
1175
+ {label}
1176
+ </Text>
1177
+ </View>
1178
+ );
1179
+ }
1180
+
1062
1181
  function GeocodeToolUI(
1063
1182
  props: ToolCallMessagePartProps<
1064
1183
  { query: string },
@@ -1069,70 +1188,38 @@ function GeocodeToolUI(
1069
1188
  }
1070
1189
  >,
1071
1190
  ) {
1072
- const colorScheme = useColorScheme();
1073
- const isDark = colorScheme === "dark";
1074
-
1075
- if (props.status?.type === "running") {
1076
- return (
1077
- <View
1078
- style={[
1079
- styles.card,
1080
- { backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" },
1081
- ]}
1082
- >
1083
- <Text style={[styles.label, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
1084
- Finding location...
1085
- </Text>
1086
- </View>
1087
- );
1088
- }
1089
-
1090
- if (props.result?.error) {
1091
- return (
1092
- <View
1093
- style={[
1094
- styles.card,
1095
- { backgroundColor: isDark ? "#3a1c1c" : "#fff0f0" },
1096
- ]}
1097
- >
1098
- <Text style={[styles.label, { color: "#ff453a" }]}>
1099
- Geocoding failed: {props.result.error}
1100
- </Text>
1101
- </View>
1102
- );
1191
+ const { colors } = useTheme();
1192
+
1193
+ if (props.status?.type === "running") {
1194
+ return <ToolStatus label="Finding location…" />;
1195
+ }
1196
+
1197
+ if (props.result?.error) {
1198
+ return <ToolError label={`Geocoding failed: ${props.result.error}`} />;
1103
1199
  }
1104
1200
 
1105
1201
  const result = props.result?.result;
1106
1202
  if (!result) return null;
1107
1203
 
1108
1204
  return (
1109
- <View
1110
- style={[styles.card, { backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" }]}
1111
- >
1205
+ <ToolCard>
1112
1206
  <View style={styles.row}>
1113
- <Text style={styles.pin}>{"\uD83D\uDCCD"}</Text>
1207
+ <Text style={styles.pin}>{"📍"}</Text>
1114
1208
  <View>
1115
- <Text
1116
- style={[
1117
- styles.locationName,
1118
- { color: isDark ? "#ffffff" : "#000000" },
1119
- ]}
1120
- >
1209
+ <Text style={[styles.locationName, { color: colors.foreground }]}>
1121
1210
  {result.name}
1122
1211
  </Text>
1123
- <Text
1124
- style={[styles.coords, { color: isDark ? "#8e8e93" : "#6e6e73" }]}
1125
- >
1212
+ <Text style={[styles.coords, { color: colors.mutedForeground }]}>
1126
1213
  {Math.abs(result.latitude).toFixed(2)}
1127
- {"\u00B0"}
1214
+ {"°"}
1128
1215
  {result.latitude >= 0 ? "N" : "S"},{" "}
1129
1216
  {Math.abs(result.longitude).toFixed(2)}
1130
- {"\u00B0"}
1217
+ {"°"}
1131
1218
  {result.longitude >= 0 ? "E" : "W"}
1132
1219
  </Text>
1133
1220
  </View>
1134
1221
  </View>
1135
- </View>
1222
+ </ToolCard>
1136
1223
  );
1137
1224
  }
1138
1225
 
@@ -1155,36 +1242,17 @@ function WeatherToolUI(
1155
1242
  }
1156
1243
  >,
1157
1244
  ) {
1158
- const colorScheme = useColorScheme();
1159
- const isDark = colorScheme === "dark";
1245
+ const { colors } = useTheme();
1160
1246
 
1161
1247
  if (props.status?.type === "running") {
1162
- return (
1163
- <View
1164
- style={[
1165
- styles.card,
1166
- { backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" },
1167
- ]}
1168
- >
1169
- <Text style={[styles.label, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
1170
- Fetching weather for {props.args.query}...
1171
- </Text>
1172
- </View>
1173
- );
1248
+ return <ToolStatus label={`Fetching weather for ${props.args.query}…`} />;
1174
1249
  }
1175
1250
 
1176
1251
  if (!props.result?.success) {
1177
1252
  return (
1178
- <View
1179
- style={[
1180
- styles.card,
1181
- { backgroundColor: isDark ? "#3a1c1c" : "#fff0f0" },
1182
- ]}
1183
- >
1184
- <Text style={[styles.label, { color: "#ff453a" }]}>
1185
- Weather unavailable: {props.result?.error ?? "Unknown error"}
1186
- </Text>
1187
- </View>
1253
+ <ToolError
1254
+ label={`Weather unavailable: ${props.result?.error ?? "Unknown error"}`}
1255
+ />
1188
1256
  );
1189
1257
  }
1190
1258
 
@@ -1192,64 +1260,40 @@ function WeatherToolUI(
1192
1260
  props.result;
1193
1261
 
1194
1262
  return (
1195
- <View
1196
- style={[
1197
- styles.weatherCard,
1198
- { backgroundColor: isDark ? "#1c1c1e" : "#f2f2f7" },
1199
- ]}
1200
- >
1263
+ <ToolCard>
1201
1264
  <View style={styles.weatherHeader}>
1202
1265
  <Text style={styles.weatherEmoji}>
1203
1266
  {mapWeatherEmoji(weatherCode ?? 0)}
1204
1267
  </Text>
1205
1268
  <View>
1206
- <Text
1207
- style={[
1208
- styles.locationName,
1209
- { color: isDark ? "#ffffff" : "#000000" },
1210
- ]}
1211
- >
1269
+ <Text style={[styles.locationName, { color: colors.foreground }]}>
1212
1270
  {location}
1213
1271
  </Text>
1214
- <Text
1215
- style={[
1216
- styles.condition,
1217
- { color: isDark ? "#8e8e93" : "#6e6e73" },
1218
- ]}
1219
- >
1272
+ <Text style={[styles.condition, { color: colors.mutedForeground }]}>
1220
1273
  {mapWeatherCode(weatherCode ?? 0)}
1221
1274
  </Text>
1222
1275
  </View>
1223
1276
  </View>
1224
1277
 
1225
- <Text style={styles.tempLarge}>
1278
+ <Text style={[styles.tempLarge, { color: colors.foreground }]}>
1226
1279
  {temperature ?? "--"}
1227
- {"\u00B0"}F
1280
+ {"°"}F
1228
1281
  </Text>
1229
1282
 
1230
1283
  {windSpeed != null && (
1231
- <Text style={[styles.wind, { color: isDark ? "#8e8e93" : "#6e6e73" }]}>
1284
+ <Text style={[styles.wind, { color: colors.mutedForeground }]}>
1232
1285
  Wind: {windSpeed} mph
1233
1286
  </Text>
1234
1287
  )}
1235
1288
 
1236
1289
  {forecast && forecast.length > 0 && (
1237
- <View
1238
- style={[
1239
- styles.forecastRow,
1240
- {
1241
- borderTopColor: isDark
1242
- ? "rgba(255,255,255,0.1)"
1243
- : "rgba(0,0,0,0.1)",
1244
- },
1245
- ]}
1246
- >
1290
+ <View style={[styles.forecastRow, { borderTopColor: colors.border }]}>
1247
1291
  {forecast.map((day, i) => (
1248
1292
  <View key={i} style={styles.forecastDay}>
1249
1293
  <Text
1250
1294
  style={[
1251
1295
  styles.forecastLabel,
1252
- { color: isDark ? "#8e8e93" : "#6e6e73" },
1296
+ { color: colors.mutedForeground },
1253
1297
  ]}
1254
1298
  >
1255
1299
  {day.label}
@@ -1257,29 +1301,24 @@ function WeatherToolUI(
1257
1301
  <Text style={styles.forecastEmoji}>
1258
1302
  {mapWeatherEmoji(day.code)}
1259
1303
  </Text>
1260
- <Text
1261
- style={[
1262
- styles.forecastTemp,
1263
- { color: isDark ? "#ffffff" : "#000000" },
1264
- ]}
1265
- >
1304
+ <Text style={[styles.forecastTemp, { color: colors.foreground }]}>
1266
1305
  {day.max}
1267
- {"\u00B0"}
1306
+ {"°"}
1268
1307
  </Text>
1269
1308
  <Text
1270
1309
  style={[
1271
1310
  styles.forecastTempLow,
1272
- { color: isDark ? "#8e8e93" : "#6e6e73" },
1311
+ { color: colors.mutedForeground },
1273
1312
  ]}
1274
1313
  >
1275
1314
  {day.min}
1276
- {"\u00B0"}
1315
+ {"°"}
1277
1316
  </Text>
1278
1317
  </View>
1279
1318
  ))}
1280
1319
  </View>
1281
1320
  )}
1282
- </View>
1321
+ </ToolCard>
1283
1322
  );
1284
1323
  }
1285
1324
 
@@ -1319,16 +1358,15 @@ export default defineToolkit({
1319
1358
 
1320
1359
  const styles = StyleSheet.create({
1321
1360
  card: {
1322
- padding: 12,
1323
- borderRadius: 12,
1324
- marginVertical: 4,
1325
- },
1326
- weatherCard: {
1327
- padding: 16,
1328
- borderRadius: 12,
1361
+ padding: 14,
1362
+ borderRadius: Radius.card,
1363
+ borderWidth: StyleSheet.hairlineWidth,
1329
1364
  marginVertical: 4,
1330
1365
  gap: 4,
1331
1366
  },
1367
+ statusText: {
1368
+ fontSize: 13,
1369
+ },
1332
1370
  row: {
1333
1371
  flexDirection: "row",
1334
1372
  alignItems: "center",
@@ -1345,9 +1383,6 @@ const styles = StyleSheet.create({
1345
1383
  fontSize: 13,
1346
1384
  marginTop: 2,
1347
1385
  },
1348
- label: {
1349
- fontSize: 13,
1350
- },
1351
1386
  weatherHeader: {
1352
1387
  flexDirection: "row",
1353
1388
  alignItems: "center",
@@ -1364,7 +1399,7 @@ const styles = StyleSheet.create({
1364
1399
  tempLarge: {
1365
1400
  fontSize: 40,
1366
1401
  fontWeight: "700",
1367
- color: "#007aff",
1402
+ letterSpacing: -1,
1368
1403
  },
1369
1404
  wind: {
1370
1405
  fontSize: 13,
@@ -1400,148 +1435,68 @@ const styles = StyleSheet.create({
1400
1435
 
1401
1436
  ```
1402
1437
 
1403
- ## components/themed-text.tsx
1404
-
1405
- ```tsx
1406
- import { StyleSheet, Text, type TextProps } from "react-native";
1407
-
1408
- import { useThemeColor } from "@/hooks/use-theme-color";
1409
-
1410
- export type ThemedTextProps = TextProps & {
1411
- lightColor?: string;
1412
- darkColor?: string;
1413
- type?: "default" | "title" | "defaultSemiBold" | "subtitle" | "link";
1414
- };
1415
-
1416
- export function ThemedText({
1417
- style,
1418
- lightColor,
1419
- darkColor,
1420
- type = "default",
1421
- ...rest
1422
- }: ThemedTextProps) {
1423
- const color = useThemeColor({ light: lightColor, dark: darkColor }, "text");
1424
-
1425
- return (
1426
- <Text
1427
- style={[
1428
- { color },
1429
- type === "default" ? styles.default : undefined,
1430
- type === "title" ? styles.title : undefined,
1431
- type === "defaultSemiBold" ? styles.defaultSemiBold : undefined,
1432
- type === "subtitle" ? styles.subtitle : undefined,
1433
- type === "link" ? styles.link : undefined,
1434
- style,
1435
- ]}
1436
- {...rest}
1437
- />
1438
- );
1439
- }
1440
-
1441
- const styles = StyleSheet.create({
1442
- default: {
1443
- fontSize: 16,
1444
- lineHeight: 24,
1445
- },
1446
- defaultSemiBold: {
1447
- fontSize: 16,
1448
- lineHeight: 24,
1449
- fontWeight: "600",
1450
- },
1451
- title: {
1452
- fontSize: 32,
1453
- fontWeight: "bold",
1454
- lineHeight: 32,
1455
- },
1456
- subtitle: {
1457
- fontSize: 20,
1458
- fontWeight: "bold",
1459
- },
1460
- link: {
1461
- lineHeight: 30,
1462
- fontSize: 16,
1463
- color: "#0a7ea4",
1464
- },
1465
- });
1466
-
1467
- ```
1468
-
1469
- ## components/themed-view.tsx
1470
-
1471
- ```tsx
1472
- import { View, type ViewProps } from "react-native";
1473
-
1474
- import { useThemeColor } from "@/hooks/use-theme-color";
1475
-
1476
- export type ThemedViewProps = ViewProps & {
1477
- lightColor?: string;
1478
- darkColor?: string;
1479
- };
1480
-
1481
- export function ThemedView({
1482
- style,
1483
- lightColor,
1484
- darkColor,
1485
- ...otherProps
1486
- }: ThemedViewProps) {
1487
- const backgroundColor = useThemeColor(
1488
- { light: lightColor, dark: darkColor },
1489
- "background",
1490
- );
1491
-
1492
- return <View style={[{ backgroundColor }, style]} {...otherProps} />;
1493
- }
1494
-
1495
- ```
1496
-
1497
1438
  ## components/thread-list/ThreadListDrawer.tsx
1498
1439
 
1499
1440
  ```tsx
1500
- import { FlatList, View, StyleSheet, useColorScheme } from "react-native";
1441
+ import { View, Text, Pressable, StyleSheet } from "react-native";
1501
1442
  import { useSafeAreaInsets } from "react-native-safe-area-context";
1502
- import { useAui, useAuiState } from "@assistant-ui/react-native";
1503
- import { ThreadListItem } from "./ThreadListItem";
1443
+ import {
1444
+ ThreadListPrimitive,
1445
+ ThreadListItemByIndexProvider,
1446
+ useAui,
1447
+ } from "@assistant-ui/react-native";
1504
1448
  import type { DrawerContentComponentProps } from "expo-router/build/react-navigation/drawer/types";
1449
+ import { ThreadListItem } from "./ThreadListItem";
1450
+ import { Icon } from "@/components/ui/icon";
1451
+ import { useTheme } from "@/hooks/use-theme";
1452
+ import { Radius } from "@/constants/theme";
1453
+ import { haptics } from "@/lib/haptics";
1505
1454
 
1506
1455
  export function ThreadListDrawer({ navigation }: DrawerContentComponentProps) {
1456
+ const { colors } = useTheme();
1507
1457
  const aui = useAui();
1508
- const threadIds = useAuiState((s) => s.threads.threadIds);
1509
- const mainThreadId = useAuiState((s) => s.threads.mainThreadId);
1510
- const threadItems = useAuiState((s) => s.threads.threadItems);
1511
1458
  const insets = useSafeAreaInsets();
1512
- const isDark = useColorScheme() === "dark";
1513
1459
 
1514
1460
  return (
1515
1461
  <View
1516
1462
  style={[
1517
1463
  styles.container,
1518
- {
1519
- backgroundColor: isDark
1520
- ? "rgba(28, 28, 30, 0.85)"
1521
- : "rgba(242, 242, 247, 0.85)",
1522
- paddingTop: insets.top,
1523
- },
1464
+ { backgroundColor: colors.background, paddingTop: insets.top + 12 },
1524
1465
  ]}
1525
1466
  >
1526
- <FlatList
1527
- data={threadIds}
1528
- keyExtractor={(item) => item}
1529
- renderItem={({ item: threadId }) => {
1530
- const threadItem = threadItems.find((t) => t.id === threadId);
1531
- return (
1532
- <ThreadListItem
1533
- title={threadItem?.title ?? "New Chat"}
1534
- isActive={threadId === mainThreadId}
1535
- onPress={() => {
1536
- aui.threads().switchToThread(threadId);
1537
- navigation.closeDrawer();
1538
- }}
1539
- />
1540
- );
1541
- }}
1542
- contentContainerStyle={styles.list}
1543
- showsVerticalScrollIndicator={false}
1544
- />
1467
+ <ThreadListPrimitive.Root style={styles.root}>
1468
+ <Pressable
1469
+ onPressIn={haptics.selection}
1470
+ onPress={() => {
1471
+ aui.threads().switchToNewThread();
1472
+ navigation.closeDrawer();
1473
+ }}
1474
+ style={({ pressed }) => [
1475
+ styles.newButton,
1476
+ { backgroundColor: pressed ? colors.muted : "transparent" },
1477
+ ]}
1478
+ >
1479
+ <Icon name="compose" size={18} color={colors.foreground} />
1480
+ <Text style={[styles.newLabel, { color: colors.foreground }]}>
1481
+ New chat
1482
+ </Text>
1483
+ </Pressable>
1484
+
1485
+ <Text style={[styles.sectionLabel, { color: colors.mutedForeground }]}>
1486
+ Recent
1487
+ </Text>
1488
+
1489
+ <ThreadListPrimitive.Items
1490
+ renderItem={({ index }) => (
1491
+ <ThreadListItemByIndexProvider index={index} archived={false}>
1492
+ <ThreadListItem onSelect={() => navigation.closeDrawer()} />
1493
+ </ThreadListItemByIndexProvider>
1494
+ )}
1495
+ style={styles.list}
1496
+ contentContainerStyle={styles.listContent}
1497
+ showsVerticalScrollIndicator={false}
1498
+ />
1499
+ </ThreadListPrimitive.Root>
1545
1500
  </View>
1546
1501
  );
1547
1502
  }
@@ -1550,8 +1505,36 @@ const styles = StyleSheet.create({
1550
1505
  container: {
1551
1506
  flex: 1,
1552
1507
  },
1508
+ root: {
1509
+ flex: 1,
1510
+ },
1511
+ newButton: {
1512
+ flexDirection: "row",
1513
+ alignItems: "center",
1514
+ gap: 10,
1515
+ height: 40,
1516
+ paddingHorizontal: 12,
1517
+ marginHorizontal: 8,
1518
+ marginBottom: 4,
1519
+ borderRadius: Radius.md,
1520
+ },
1521
+ newLabel: {
1522
+ fontSize: 15,
1523
+ fontWeight: "500",
1524
+ letterSpacing: -0.2,
1525
+ },
1526
+ sectionLabel: {
1527
+ fontSize: 12,
1528
+ fontWeight: "500",
1529
+ paddingHorizontal: 20,
1530
+ paddingTop: 12,
1531
+ paddingBottom: 6,
1532
+ },
1553
1533
  list: {
1554
- paddingVertical: 8,
1534
+ flex: 1,
1535
+ },
1536
+ listContent: {
1537
+ paddingBottom: 8,
1555
1538
  },
1556
1539
  });
1557
1540
 
@@ -1560,211 +1543,168 @@ const styles = StyleSheet.create({
1560
1543
  ## components/thread-list/ThreadListItem.tsx
1561
1544
 
1562
1545
  ```tsx
1546
+ import { Text, Pressable, StyleSheet } from "react-native";
1563
1547
  import {
1564
- Pressable,
1565
- Text,
1566
- View,
1567
- StyleSheet,
1568
- useColorScheme,
1569
- } from "react-native";
1570
-
1571
- type ThreadListItemProps = {
1572
- title: string;
1573
- isActive: boolean;
1574
- onPress: () => void;
1575
- };
1548
+ ThreadListItemPrimitive,
1549
+ useAui,
1550
+ useAuiState,
1551
+ } from "@assistant-ui/react-native";
1552
+ import { useTheme } from "@/hooks/use-theme";
1553
+ import { Radius } from "@/constants/theme";
1554
+ import { haptics } from "@/lib/haptics";
1576
1555
 
1577
- export function ThreadListItem({
1578
- title,
1579
- isActive,
1580
- onPress,
1581
- }: ThreadListItemProps) {
1582
- const isDark = useColorScheme() === "dark";
1556
+ export function ThreadListItem({ onSelect }: { onSelect: () => void }) {
1557
+ const { colors } = useTheme();
1558
+ const aui = useAui();
1559
+ const isActive = useAuiState(
1560
+ (s) => s.threads.mainThreadId === s.threadListItem.id,
1561
+ );
1583
1562
 
1584
- const content = (
1585
- <View style={styles.row}>
1586
- {isActive && (
1587
- <View style={[styles.indicator, { backgroundColor: "#007AFF" }]} />
1588
- )}
1589
- <Text
1590
- numberOfLines={1}
1591
- style={[
1592
- styles.title,
1593
- { color: isDark ? "#ffffff" : "#000000" },
1594
- isActive && styles.titleActive,
1563
+ return (
1564
+ <ThreadListItemPrimitive.Root>
1565
+ <Pressable
1566
+ onPressIn={haptics.selection}
1567
+ onPress={() => {
1568
+ aui.threadListItem().switchTo();
1569
+ onSelect();
1570
+ }}
1571
+ style={({ pressed }) => [
1572
+ styles.item,
1573
+ (isActive || pressed) && { backgroundColor: colors.muted },
1595
1574
  ]}
1596
1575
  >
1597
- {title}
1598
- </Text>
1599
- </View>
1600
- );
1601
-
1602
- if (isActive) {
1603
- return (
1604
- <Pressable onPress={onPress} style={styles.itemOuter}>
1605
- <View
1576
+ <Text
1577
+ numberOfLines={1}
1606
1578
  style={[
1607
- styles.glassItem,
1579
+ styles.title,
1608
1580
  {
1609
- backgroundColor: isDark
1610
- ? "rgba(44, 44, 46, 0.6)"
1611
- : "rgba(209, 209, 214, 0.6)",
1581
+ color: colors.foreground,
1582
+ fontWeight: isActive ? "600" : "400",
1612
1583
  },
1613
1584
  ]}
1614
1585
  >
1615
- {content}
1616
- </View>
1586
+ <ThreadListItemPrimitive.Title fallback="New chat" />
1587
+ </Text>
1617
1588
  </Pressable>
1618
- );
1619
- }
1620
-
1621
- return (
1622
- <Pressable
1623
- onPress={onPress}
1624
- style={({ pressed }) => [
1625
- styles.itemOuter,
1626
- styles.itemPadding,
1627
- pressed && {
1628
- backgroundColor: isDark ? "#3a3a3c" : "#d1d1d6",
1629
- borderRadius: 10,
1630
- },
1631
- ]}
1632
- >
1633
- {content}
1634
- </Pressable>
1589
+ </ThreadListItemPrimitive.Root>
1635
1590
  );
1636
1591
  }
1637
1592
 
1638
1593
  const styles = StyleSheet.create({
1639
- itemOuter: {
1594
+ item: {
1595
+ height: 38,
1596
+ justifyContent: "center",
1597
+ paddingHorizontal: 12,
1640
1598
  marginHorizontal: 8,
1641
- marginVertical: 2,
1642
- },
1643
- itemPadding: {
1644
- paddingVertical: 12,
1645
- paddingHorizontal: 16,
1646
- },
1647
- glassItem: {
1648
- paddingVertical: 12,
1649
- paddingHorizontal: 16,
1650
- borderRadius: 10,
1651
- overflow: "hidden",
1652
- },
1653
- row: {
1654
- flexDirection: "row",
1655
- alignItems: "center",
1656
- },
1657
- indicator: {
1658
- width: 4,
1659
- height: 20,
1660
- borderRadius: 2,
1661
- marginRight: 10,
1599
+ borderRadius: Radius.md,
1662
1600
  },
1663
1601
  title: {
1664
1602
  fontSize: 15,
1665
- flex: 1,
1666
- },
1667
- titleActive: {
1668
- fontWeight: "600",
1603
+ letterSpacing: -0.2,
1669
1604
  },
1670
1605
  });
1671
1606
 
1672
1607
  ```
1673
1608
 
1674
- ## components/ui/icon-symbol.ios.tsx
1609
+ ## components/ui/icon-mappings.ts
1610
+
1611
+ ```typescript
1612
+ import type { ComponentProps } from "react";
1613
+ import type MaterialIcons from "@expo/vector-icons/MaterialIcons";
1614
+ import type { SymbolViewProps, SymbolWeight } from "expo-symbols";
1615
+
1616
+ export type IconName =
1617
+ | "compose"
1618
+ | "send"
1619
+ | "stop"
1620
+ | "add"
1621
+ | "remove"
1622
+ | "copy"
1623
+ | "check"
1624
+ | "reload"
1625
+ | "chevron-left"
1626
+ | "chevron-right"
1627
+ | "menu"
1628
+ | "location";
1629
+
1630
+ export type IconProps = {
1631
+ name: IconName;
1632
+ size?: number;
1633
+ color: string;
1634
+ weight?: SymbolWeight;
1635
+ };
1636
+
1637
+ /** SF Symbols rendered natively on iOS via `expo-symbols`. */
1638
+ export const SF_SYMBOLS: Record<IconName, SymbolViewProps["name"]> = {
1639
+ compose: "square.and.pencil",
1640
+ send: "arrow.up",
1641
+ stop: "stop.fill",
1642
+ add: "plus",
1643
+ remove: "xmark",
1644
+ copy: "doc.on.doc",
1645
+ check: "checkmark",
1646
+ reload: "arrow.clockwise",
1647
+ "chevron-left": "chevron.left",
1648
+ "chevron-right": "chevron.right",
1649
+ menu: "line.3.horizontal",
1650
+ location: "location.fill",
1651
+ };
1652
+
1653
+ /** Material Icons fallback for Android and web. */
1654
+ export const MATERIAL_ICONS: Record<
1655
+ IconName,
1656
+ ComponentProps<typeof MaterialIcons>["name"]
1657
+ > = {
1658
+ compose: "edit",
1659
+ send: "arrow-upward",
1660
+ stop: "stop",
1661
+ add: "add",
1662
+ remove: "close",
1663
+ copy: "content-copy",
1664
+ check: "check",
1665
+ reload: "refresh",
1666
+ "chevron-left": "chevron-left",
1667
+ "chevron-right": "chevron-right",
1668
+ menu: "menu",
1669
+ location: "place",
1670
+ };
1671
+
1672
+ ```
1673
+
1674
+ ## components/ui/icon.ios.tsx
1675
1675
 
1676
1676
  ```tsx
1677
- import {
1678
- SymbolView,
1679
- type SymbolViewProps,
1680
- type SymbolWeight,
1681
- } from "expo-symbols";
1682
- import type { StyleProp, ViewStyle } from "react-native";
1677
+ import { SymbolView } from "expo-symbols";
1678
+ import { SF_SYMBOLS, type IconProps } from "./icon-mappings";
1683
1679
 
1684
- export function IconSymbol({
1680
+ export function Icon({
1685
1681
  name,
1686
1682
  size = 24,
1687
1683
  color,
1688
- style,
1689
1684
  weight = "regular",
1690
- }: {
1691
- name: SymbolViewProps["name"];
1692
- size?: number;
1693
- color: string;
1694
- style?: StyleProp<ViewStyle>;
1695
- weight?: SymbolWeight;
1696
- }) {
1685
+ }: IconProps) {
1697
1686
  return (
1698
1687
  <SymbolView
1699
- weight={weight}
1688
+ name={SF_SYMBOLS[name]}
1700
1689
  tintColor={color}
1690
+ weight={weight}
1701
1691
  resizeMode="scaleAspectFit"
1702
- name={name}
1703
- style={[
1704
- {
1705
- width: size,
1706
- height: size,
1707
- },
1708
- style,
1709
- ]}
1692
+ style={{ width: size, height: size }}
1710
1693
  />
1711
1694
  );
1712
1695
  }
1713
1696
 
1714
1697
  ```
1715
1698
 
1716
- ## components/ui/icon-symbol.tsx
1699
+ ## components/ui/icon.tsx
1717
1700
 
1718
1701
  ```tsx
1719
- // Fallback for using MaterialIcons on Android and web.
1720
-
1721
1702
  import MaterialIcons from "@expo/vector-icons/MaterialIcons";
1722
- import type { SymbolWeight, SFSymbol } from "expo-symbols";
1723
- import type { ComponentProps } from "react";
1724
- import type { OpaqueColorValue, StyleProp, TextStyle } from "react-native";
1725
-
1726
- type IconMapping = Record<
1727
- SFSymbol,
1728
- ComponentProps<typeof MaterialIcons>["name"]
1729
- >;
1730
- type IconSymbolName = keyof typeof MAPPING;
1731
-
1732
- /**
1733
- * Add your SF Symbols to Material Icons mappings here.
1734
- * - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
1735
- * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
1736
- */
1737
- const MAPPING = {
1738
- "house.fill": "home",
1739
- "paperplane.fill": "send",
1740
- "chevron.left.forwardslash.chevron.right": "code",
1741
- "chevron.right": "chevron-right",
1742
- } as IconMapping;
1703
+ import { MATERIAL_ICONS, type IconProps } from "./icon-mappings";
1743
1704
 
1744
- /**
1745
- * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.
1746
- * This ensures a consistent look across platforms, and optimal resource usage.
1747
- * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.
1748
- */
1749
- export function IconSymbol({
1750
- name,
1751
- size = 24,
1752
- color,
1753
- style,
1754
- }: {
1755
- name: IconSymbolName;
1756
- size?: number;
1757
- color: string | OpaqueColorValue;
1758
- style?: StyleProp<TextStyle>;
1759
- weight?: SymbolWeight;
1760
- }) {
1705
+ export function Icon({ name, size = 24, color }: IconProps) {
1761
1706
  return (
1762
- <MaterialIcons
1763
- color={color}
1764
- size={size}
1765
- name={MAPPING[name]}
1766
- style={style}
1767
- />
1707
+ <MaterialIcons name={MATERIAL_ICONS[name]} size={size} color={color} />
1768
1708
  );
1769
1709
  }
1770
1710
 
@@ -1774,59 +1714,83 @@ export function IconSymbol({
1774
1714
 
1775
1715
  ```typescript
1776
1716
  /**
1777
- * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
1778
- * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
1717
+ * Design tokens for the assistant-ui Expo example.
1718
+ *
1719
+ * These mirror the canonical web kit (`packages/ui`): a neutral zinc palette,
1720
+ * a clean ChatGPT-grade look with subtle hairline borders and no heavy glass or
1721
+ * shadow. Values are the sRGB equivalents of the web `oklch` tokens.
1779
1722
  */
1780
1723
 
1781
- import { Platform } from "react-native";
1782
-
1783
- const tintColorLight = "#0a7ea4";
1784
- const tintColorDark = "#fff";
1724
+ export type Palette = {
1725
+ background: string;
1726
+ foreground: string;
1727
+ card: string;
1728
+ cardForeground: string;
1729
+ muted: string;
1730
+ mutedForeground: string;
1731
+ accent: string;
1732
+ border: string;
1733
+ primary: string;
1734
+ primaryForeground: string;
1735
+ composer: string;
1736
+ destructive: string;
1737
+ destructiveForeground: string;
1738
+ destructiveSurface: string;
1739
+ ring: string;
1740
+ };
1785
1741
 
1786
- export const Colors = {
1742
+ export const Colors: { light: Palette; dark: Palette } = {
1787
1743
  light: {
1788
- text: "#11181C",
1789
- background: "#fff",
1790
- tint: tintColorLight,
1791
- icon: "#687076",
1792
- tabIconDefault: "#687076",
1793
- tabIconSelected: tintColorLight,
1744
+ background: "#ffffff",
1745
+ foreground: "#18181b",
1746
+ card: "#ffffff",
1747
+ cardForeground: "#18181b",
1748
+ muted: "#f4f4f5",
1749
+ mutedForeground: "#71717a",
1750
+ accent: "#f4f4f5",
1751
+ border: "#e4e4e7",
1752
+ primary: "#18181b",
1753
+ primaryForeground: "#fafafa",
1754
+ composer: "#fafafa",
1755
+ destructive: "#dc2626",
1756
+ destructiveForeground: "#ffffff",
1757
+ destructiveSurface: "rgba(220, 38, 38, 0.08)",
1758
+ ring: "#a1a1aa",
1794
1759
  },
1795
1760
  dark: {
1796
- text: "#ECEDEE",
1797
- background: "#151718",
1798
- tint: tintColorDark,
1799
- icon: "#9BA1A6",
1800
- tabIconDefault: "#9BA1A6",
1801
- tabIconSelected: tintColorDark,
1761
+ background: "#18181b",
1762
+ foreground: "#fafafa",
1763
+ card: "#27272a",
1764
+ cardForeground: "#fafafa",
1765
+ muted: "#3f3f46",
1766
+ mutedForeground: "#a1a1aa",
1767
+ accent: "#3f3f46",
1768
+ border: "rgba(255, 255, 255, 0.1)",
1769
+ primary: "#e4e4e7",
1770
+ primaryForeground: "#18181b",
1771
+ composer: "#232326",
1772
+ destructive: "#f87171",
1773
+ destructiveForeground: "#18181b",
1774
+ destructiveSurface: "rgba(248, 113, 113, 0.12)",
1775
+ ring: "#52525b",
1802
1776
  },
1803
1777
  };
1804
1778
 
1805
- export const Fonts = Platform.select({
1806
- ios: {
1807
- /** iOS `UIFontDescriptorSystemDesignDefault` */
1808
- sans: "system-ui",
1809
- /** iOS `UIFontDescriptorSystemDesignSerif` */
1810
- serif: "ui-serif",
1811
- /** iOS `UIFontDescriptorSystemDesignRounded` */
1812
- rounded: "ui-rounded",
1813
- /** iOS `UIFontDescriptorSystemDesignMonospaced` */
1814
- mono: "ui-monospace",
1815
- },
1816
- default: {
1817
- sans: "normal",
1818
- serif: "serif",
1819
- rounded: "normal",
1820
- mono: "monospace",
1821
- },
1822
- web: {
1823
- sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
1824
- serif: "Georgia, 'Times New Roman', serif",
1825
- rounded:
1826
- "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif",
1827
- mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
1828
- },
1829
- });
1779
+ export const Radius = {
1780
+ sm: 6,
1781
+ md: 8,
1782
+ lg: 10,
1783
+ card: 16,
1784
+ bubble: 18,
1785
+ composer: 24,
1786
+ attachment: 14,
1787
+ pill: 999,
1788
+ } as const;
1789
+
1790
+ export const Spacing = {
1791
+ threadMaxWidth: 768,
1792
+ gutter: 16,
1793
+ } as const;
1830
1794
 
1831
1795
  ```
1832
1796
 
@@ -1889,34 +1853,42 @@ export function useColorScheme() {
1889
1853
 
1890
1854
  ```
1891
1855
 
1892
- ## hooks/use-theme-color.ts
1856
+ ## hooks/use-theme.ts
1893
1857
 
1894
1858
  ```typescript
1895
- /**
1896
- * Learn more about light and dark modes:
1897
- * https://docs.expo.dev/guides/color-schemes/
1898
- */
1899
-
1900
- import { Colors } from "@/constants/theme";
1859
+ import { Colors, type Palette } from "@/constants/theme";
1901
1860
  import { useColorScheme } from "@/hooks/use-color-scheme";
1902
1861
 
1903
- export function useThemeColor(
1904
- props: { light?: string; dark?: string },
1905
- colorName: keyof typeof Colors.light & keyof typeof Colors.dark,
1906
- ) {
1907
- const scheme = useColorScheme();
1908
- const theme: "light" | "dark" = scheme === "dark" ? "dark" : "light";
1909
- const colorFromProps = props[theme];
1910
-
1911
- if (colorFromProps) {
1912
- return colorFromProps;
1913
- } else {
1914
- return Colors[theme][colorName];
1915
- }
1862
+ export function useTheme(): { isDark: boolean; colors: Palette } {
1863
+ const isDark = useColorScheme() === "dark";
1864
+ return { isDark, colors: isDark ? Colors.dark : Colors.light };
1916
1865
  }
1917
1866
 
1918
1867
  ```
1919
1868
 
1869
+ ## lib/haptics.ts
1870
+
1871
+ ```typescript
1872
+ import { Platform } from "react-native";
1873
+ import * as Haptics from "expo-haptics";
1874
+
1875
+ const enabled = Platform.OS === "ios" || Platform.OS === "android";
1876
+
1877
+ export const haptics = {
1878
+ selection() {
1879
+ if (enabled) void Haptics.selectionAsync();
1880
+ },
1881
+ light() {
1882
+ if (enabled) void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
1883
+ },
1884
+ success() {
1885
+ if (enabled)
1886
+ void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
1887
+ },
1888
+ };
1889
+
1890
+ ```
1891
+
1920
1892
  ## metro.config.js
1921
1893
 
1922
1894
  ```javascript
@@ -1981,20 +1953,23 @@ module.exports = withAui(config);
1981
1953
  "export:web": "expo export --platform web && node scripts/flatten-assets.mjs"
1982
1954
  },
1983
1955
  "dependencies": {
1984
- "@ai-sdk/openai": "^3.0.71",
1985
- "@ai-sdk/react": "^3.0.206",
1956
+ "@ai-sdk/openai": "^3.0.74",
1957
+ "@ai-sdk/react": "^3.0.211",
1986
1958
  "@assistant-ui/react-ai-sdk": "workspace:*",
1987
1959
  "@assistant-ui/react-native": "workspace:*",
1988
1960
  "@expo/vector-icons": "^15.1.1",
1989
1961
  "@react-navigation/drawer": "7.9.4",
1990
1962
  "@react-navigation/native": "7.1.33",
1991
- "ai": "^6.0.204",
1992
- "expo": "~56.0.11",
1963
+ "ai": "^6.0.209",
1964
+ "expo": "~56.0.12",
1965
+ "expo-clipboard": "~56.0.4",
1993
1966
  "expo-constants": "~56.0.18",
1994
- "expo-font": "~56.0.6",
1995
- "expo-image-picker": "~56.0.17",
1967
+ "expo-font": "~56.0.7",
1968
+ "expo-haptics": "~56.0.3",
1969
+ "expo-image": "~56.0.11",
1970
+ "expo-image-picker": "~56.0.18",
1996
1971
  "expo-linking": "~56.0.14",
1997
- "expo-router": "~56.2.10",
1972
+ "expo-router": "~56.2.11",
1998
1973
  "expo-server": "~56.0.5",
1999
1974
  "expo-splash-screen": "~56.0.10",
2000
1975
  "expo-status-bar": "~56.0.4",
@@ -2003,9 +1978,9 @@ module.exports = withAui(config);
2003
1978
  "react": "19.2.3",
2004
1979
  "react-dom": "19.2.3",
2005
1980
  "react-native": "0.85.3",
2006
- "react-native-gesture-handler": "~3.0.1",
1981
+ "react-native-gesture-handler": "~2.31.2",
2007
1982
  "react-native-reanimated": "4.3.1",
2008
- "react-native-safe-area-context": "~5.8.0",
1983
+ "react-native-safe-area-context": "~5.7.0",
2009
1984
  "react-native-screens": "4.25.2",
2010
1985
  "react-native-web": "~0.21.2",
2011
1986
  "react-native-worklets": "0.8.3",
@@ -2013,6 +1988,7 @@ module.exports = withAui(config);
2013
1988
  },
2014
1989
  "devDependencies": {
2015
1990
  "@assistant-ui/metro": "workspace:*",
1991
+ "@babel/core": "^7.29.0",
2016
1992
  "@types/react": "~19.2.17",
2017
1993
  "typescript": "~6.0.3"
2018
1994
  },
@@ -2024,56 +2000,57 @@ module.exports = withAui(config);
2024
2000
  ## README.md
2025
2001
 
2026
2002
  ```markdown
2027
- # Welcome to your Expo app 👋
2003
+ # assistant-ui + Expo
2004
+
2005
+ A native chat app built with [assistant-ui](https://www.assistant-ui.com) and [Expo](https://expo.dev). It runs on iOS, Android, and the web from a single codebase, and is styled to match the assistant-ui web kit: a clean, neutral, ChatGPT-grade look with subtle hairline borders.
2006
+
2007
+ The chat is powered by `@assistant-ui/react-native` with the AI SDK runtime (`@assistant-ui/react-ai-sdk`). The example leans on native Expo APIs throughout:
2008
+
2009
+ - **SF Symbols** (`expo-symbols`) for native iconography on iOS, with a Material Icons fallback on Android and the web.
2010
+ - **Haptics** (`expo-haptics`) for tactile feedback on send, stop, and selection.
2011
+ - **Image picker** (`expo-image-picker`) and **expo-image** for attachments.
2012
+ - **Clipboard** (`expo-clipboard`) so the copy action works natively.
2013
+ - A native **drawer** (`@react-navigation/drawer`) for the thread list, with a swipe gesture to switch conversations.
2028
2014
 
2029
- This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
2015
+ It follows the assistant-ui component conventions: `MessagePrimitive.Parts` for message rendering, `AuiIf` for declarative state-driven UI, the `ThreadList` / `ThreadListItem` primitives for the drawer, and a `"use generative"` toolkit (`components/assistant-ui/tools.tsx`) that renders weather cards inline.
2030
2016
 
2031
2017
  ## Get started
2032
2018
 
2033
- 1. Install dependencies
2019
+ 1. Install dependencies from the repository root:
2034
2020
 
2035
2021
  ```bash
2036
- npm install
2022
+ pnpm install
2037
2023
  ```
2038
2024
 
2039
- 2. Start the app
2025
+ 2. Configure the chat backend. Copy `.env.example` to `.env` and set your key:
2040
2026
 
2041
2027
  ```bash
2042
- npx expo start
2028
+ cp .env.example .env
2043
2029
  ```
2044
2030
 
2045
- In the output, you'll find options to open the app in a
2031
+ The bundled API route (`app/api/chat+api.ts`) needs `OPENAI_API_KEY`. To point the app at a separately hosted backend instead, set `EXPO_PUBLIC_CHAT_ENDPOINT_URL`.
2046
2032
 
2047
- - [development build](https://docs.expo.dev/develop/development-builds/introduction/)
2048
- - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
2049
- - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
2050
- - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
2033
+ 3. Start the app:
2051
2034
 
2052
- You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
2053
-
2054
- ## Get a fresh project
2035
+ ```bash
2036
+ pnpm --filter with-expo start
2037
+ ```
2055
2038
 
2056
- When you're ready, run:
2039
+ From there you can open it in the iOS simulator, an Android emulator, or the browser.
2057
2040
 
2058
- ```bash
2059
- npm run reset-project
2060
- ```
2041
+ ## Project structure
2061
2042
 
2062
- This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
2043
+ - `app/_layout.tsx` wires the runtime, the toolkit, and the drawer navigation.
2044
+ - `app/index.tsx` renders the `Thread`.
2045
+ - `components/assistant-ui/` holds the composer, message, action bar, branch picker, and tool UIs.
2046
+ - `components/thread-list/` holds the drawer thread list.
2047
+ - `constants/theme.ts` and `hooks/use-theme.ts` define the shared design tokens.
2048
+ - `components/ui/icon.tsx` is the cross-platform icon (SF Symbols on iOS, Material Icons elsewhere).
2063
2049
 
2064
2050
  ## Learn more
2065
2051
 
2066
- To learn more about developing your project with Expo, look at the following resources:
2067
-
2068
- - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
2069
- - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
2070
-
2071
- ## Join the community
2072
-
2073
- Join our community of developers creating universal apps.
2074
-
2075
- - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
2076
- - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
2052
+ - [assistant-ui documentation](https://www.assistant-ui.com/docs)
2053
+ - [Expo documentation](https://docs.expo.dev)
2077
2054
 
2078
2055
  ```
2079
2056