@assistant-ui/mcp-docs-server 0.1.34 → 0.1.36

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