vibefast-cli 0.1.1

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 (250) hide show
  1. package/FINAL-STATUS.md +144 -0
  2. package/HOW-IT-WORKS.md +559 -0
  3. package/PLAN.md +453 -0
  4. package/README.md +129 -0
  5. package/RECIPES-READY.md +172 -0
  6. package/STATUS.md +199 -0
  7. package/SUCCESS.md +259 -0
  8. package/TESTING-CHECKLIST.md +450 -0
  9. package/cloudflare-worker/.wrangler/state/v3/kv/64907821e2634080acce34618d2f3d4c/blobs/11f2769953c717e188062bc644da97c1fd1e4d6d0813a226ce7567dba759afab0000019a736fb8d4 +1 -0
  10. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite +0 -0
  11. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-shm +0 -0
  12. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-wal +0 -0
  13. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite +0 -0
  14. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-shm +0 -0
  15. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-wal +0 -0
  16. package/cloudflare-worker/.wrangler/state/v3/r2/vibefast-recipes/blobs/620e8cf7c35d9806da25dee237e1d7e8b2432bd98f755b60e2c7f08a48d2c7b90000019a73736484 +0 -0
  17. package/cloudflare-worker/MIGRATION.md +160 -0
  18. package/cloudflare-worker/QUICKSTART.md +200 -0
  19. package/cloudflare-worker/README.md +242 -0
  20. package/cloudflare-worker/generate-token.js +32 -0
  21. package/cloudflare-worker/mini-native@latest.zip +0 -0
  22. package/cloudflare-worker/setup.sh +143 -0
  23. package/cloudflare-worker/test-recipe/apps/native/src/app/mini/index.tsx +15 -0
  24. package/cloudflare-worker/test-recipe/recipe.json +16 -0
  25. package/cloudflare-worker/worker.js +308 -0
  26. package/cloudflare-worker/wrangler.toml +13 -0
  27. package/dist/commands/add.d.ts +3 -0
  28. package/dist/commands/add.d.ts.map +1 -0
  29. package/dist/commands/add.js +149 -0
  30. package/dist/commands/add.js.map +1 -0
  31. package/dist/commands/devices.d.ts +3 -0
  32. package/dist/commands/devices.d.ts.map +1 -0
  33. package/dist/commands/devices.js +35 -0
  34. package/dist/commands/devices.js.map +1 -0
  35. package/dist/commands/doctor.d.ts +3 -0
  36. package/dist/commands/doctor.d.ts.map +1 -0
  37. package/dist/commands/doctor.js +67 -0
  38. package/dist/commands/doctor.js.map +1 -0
  39. package/dist/commands/list.d.ts +3 -0
  40. package/dist/commands/list.d.ts.map +1 -0
  41. package/dist/commands/list.js +40 -0
  42. package/dist/commands/list.js.map +1 -0
  43. package/dist/commands/login.d.ts +3 -0
  44. package/dist/commands/login.d.ts.map +1 -0
  45. package/dist/commands/login.js +23 -0
  46. package/dist/commands/login.js.map +1 -0
  47. package/dist/commands/logout.d.ts +3 -0
  48. package/dist/commands/logout.d.ts.map +1 -0
  49. package/dist/commands/logout.js +16 -0
  50. package/dist/commands/logout.js.map +1 -0
  51. package/dist/commands/remove.d.ts +3 -0
  52. package/dist/commands/remove.d.ts.map +1 -0
  53. package/dist/commands/remove.js +67 -0
  54. package/dist/commands/remove.js.map +1 -0
  55. package/dist/core/__tests__/journal.test.d.ts +2 -0
  56. package/dist/core/__tests__/journal.test.d.ts.map +1 -0
  57. package/dist/core/__tests__/journal.test.js +101 -0
  58. package/dist/core/__tests__/journal.test.js.map +1 -0
  59. package/dist/core/__tests__/validate.test.d.ts +2 -0
  60. package/dist/core/__tests__/validate.test.d.ts.map +1 -0
  61. package/dist/core/__tests__/validate.test.js +53 -0
  62. package/dist/core/__tests__/validate.test.js.map +1 -0
  63. package/dist/core/archive.d.ts +2 -0
  64. package/dist/core/archive.d.ts.map +1 -0
  65. package/dist/core/archive.js +59 -0
  66. package/dist/core/archive.js.map +1 -0
  67. package/dist/core/auth.d.ts +15 -0
  68. package/dist/core/auth.d.ts.map +1 -0
  69. package/dist/core/auth.js +76 -0
  70. package/dist/core/auth.js.map +1 -0
  71. package/dist/core/codemod.d.ts +20 -0
  72. package/dist/core/codemod.d.ts.map +1 -0
  73. package/dist/core/codemod.js +150 -0
  74. package/dist/core/codemod.js.map +1 -0
  75. package/dist/core/fsx.d.ts +12 -0
  76. package/dist/core/fsx.d.ts.map +1 -0
  77. package/dist/core/fsx.js +70 -0
  78. package/dist/core/fsx.js.map +1 -0
  79. package/dist/core/http.d.ts +30 -0
  80. package/dist/core/http.d.ts.map +1 -0
  81. package/dist/core/http.js +95 -0
  82. package/dist/core/http.js.map +1 -0
  83. package/dist/core/journal.d.ts +18 -0
  84. package/dist/core/journal.d.ts.map +1 -0
  85. package/dist/core/journal.js +34 -0
  86. package/dist/core/journal.js.map +1 -0
  87. package/dist/core/log.d.ts +8 -0
  88. package/dist/core/log.d.ts.map +1 -0
  89. package/dist/core/log.js +9 -0
  90. package/dist/core/log.js.map +1 -0
  91. package/dist/core/pathGuard.d.ts +3 -0
  92. package/dist/core/pathGuard.d.ts.map +1 -0
  93. package/dist/core/pathGuard.js +18 -0
  94. package/dist/core/pathGuard.js.map +1 -0
  95. package/dist/core/paths.d.ts +11 -0
  96. package/dist/core/paths.d.ts.map +1 -0
  97. package/dist/core/paths.js +22 -0
  98. package/dist/core/paths.js.map +1 -0
  99. package/dist/core/validate.d.ts +8 -0
  100. package/dist/core/validate.d.ts.map +1 -0
  101. package/dist/core/validate.js +27 -0
  102. package/dist/core/validate.js.map +1 -0
  103. package/dist/index.d.ts +3 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +23 -0
  106. package/dist/index.js.map +1 -0
  107. package/docs/decisions.md +55 -0
  108. package/package.json +39 -0
  109. package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
  110. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
  111. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
  112. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
  113. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/index.ts +4 -0
  114. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
  115. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
  116. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
  117. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
  118. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
  119. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
  120. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
  121. package/recipes/audio-recorder/recipe.json +22 -0
  122. package/recipes/audio-recorder@latest.zip +0 -0
  123. package/recipes/charts/apps/native/src/app/charts/index.tsx +3 -0
  124. package/recipes/charts/apps/native/src/features/charts/README.md +185 -0
  125. package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +223 -0
  126. package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +40 -0
  127. package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +143 -0
  128. package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +196 -0
  129. package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +65 -0
  130. package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +143 -0
  131. package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +246 -0
  132. package/recipes/charts/apps/native/src/features/charts/components/index.ts +10 -0
  133. package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +308 -0
  134. package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +180 -0
  135. package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +188 -0
  136. package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +265 -0
  137. package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +322 -0
  138. package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +183 -0
  139. package/recipes/charts/apps/native/src/features/charts/types/index.ts +66 -0
  140. package/recipes/charts/recipe.json +22 -0
  141. package/recipes/charts@latest.zip +0 -0
  142. package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
  143. package/recipes/chatbot/apps/native/src/features/chatbot/app/index.tsx +302 -0
  144. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
  145. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +469 -0
  146. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
  147. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +246 -0
  148. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
  149. package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +115 -0
  150. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
  151. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
  152. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
  153. package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
  154. package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +173 -0
  155. package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
  156. package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
  157. package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
  158. package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +20 -0
  159. package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
  160. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +143 -0
  161. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +664 -0
  162. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +359 -0
  163. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
  164. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +79 -0
  165. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
  166. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
  167. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +207 -0
  168. package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +86 -0
  169. package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +162 -0
  170. package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +62 -0
  171. package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +40 -0
  172. package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +238 -0
  173. package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +180 -0
  174. package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +60 -0
  175. package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
  176. package/recipes/chatbot/recipe.json +22 -0
  177. package/recipes/chatbot@latest.zip +0 -0
  178. package/recipes/image-generator/apps/native/src/app/image-generator/gallery.tsx +3 -0
  179. package/recipes/image-generator/apps/native/src/app/image-generator/index.tsx +3 -0
  180. package/recipes/image-generator/apps/native/src/features/image-generator/app/_layout.tsx +25 -0
  181. package/recipes/image-generator/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
  182. package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +237 -0
  183. package/recipes/image-generator/apps/native/src/features/image-generator/components/gallery-image.tsx +26 -0
  184. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
  185. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +210 -0
  186. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
  187. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
  188. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
  189. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +93 -0
  190. package/recipes/image-generator/apps/native/src/features/image-generator/models/models.ts +66 -0
  191. package/recipes/image-generator/apps/native/src/features/image-generator/services/image-gallery-service.ts +98 -0
  192. package/recipes/image-generator/apps/native/src/features/image-generator/services/image-save-service.ts +121 -0
  193. package/recipes/image-generator/recipe.json +22 -0
  194. package/recipes/image-generator@latest.zip +0 -0
  195. package/recipes/quiz/apps/native/src/app/quiz/index.tsx +47 -0
  196. package/recipes/quiz/apps/native/src/features/quiz/components/question.tsx +67 -0
  197. package/recipes/quiz/apps/native/src/features/quiz/config.ts +11 -0
  198. package/recipes/quiz/apps/native/src/features/quiz/index.tsx +133 -0
  199. package/recipes/quiz/recipe.json +22 -0
  200. package/recipes/quiz@latest.zip +0 -0
  201. package/recipes/tracker-app/apps/native/src/app/tracker-app/index.tsx +1 -0
  202. package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +108 -0
  203. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/animated-number.tsx +102 -0
  204. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/calorie-card.tsx +66 -0
  205. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/circular-progress.tsx +97 -0
  206. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/floating-add-button.tsx +27 -0
  207. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/macro-card.tsx +80 -0
  208. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/promo-banner.tsx +98 -0
  209. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/recently-logged.tsx +64 -0
  210. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/week-calendar.tsx +68 -0
  211. package/recipes/tracker-app/recipe.json +22 -0
  212. package/recipes/tracker-app@latest.zip +0 -0
  213. package/recipes/upload-all.sh +32 -0
  214. package/recipes/voice-bot/apps/native/src/app/voice-bot/index.tsx +27 -0
  215. package/recipes/voice-bot/apps/native/src/features/voice-bot/README.md +185 -0
  216. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/conversation-status.tsx +76 -0
  217. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/index.ts +4 -0
  218. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/message-input.tsx +98 -0
  219. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-bot-screen.tsx +173 -0
  220. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-controls.tsx +73 -0
  221. package/recipes/voice-bot/apps/native/src/features/voice-bot/index.ts +3 -0
  222. package/recipes/voice-bot/apps/native/src/features/voice-bot/services/index.ts +1 -0
  223. package/recipes/voice-bot/apps/native/src/features/voice-bot/services/use-voice-bot.ts +161 -0
  224. package/recipes/voice-bot/apps/native/src/features/voice-bot/types.ts +29 -0
  225. package/recipes/voice-bot/recipe.json +22 -0
  226. package/recipes/voice-bot@latest.zip +0 -0
  227. package/scripts/create-recipes.mjs +189 -0
  228. package/src/commands/add.ts +183 -0
  229. package/src/commands/devices.ts +38 -0
  230. package/src/commands/doctor.ts +67 -0
  231. package/src/commands/list.ts +45 -0
  232. package/src/commands/login.ts +24 -0
  233. package/src/commands/logout.ts +15 -0
  234. package/src/commands/remove.ts +78 -0
  235. package/src/core/__tests__/journal.test.ts +119 -0
  236. package/src/core/__tests__/validate.test.ts +64 -0
  237. package/src/core/archive.ts +69 -0
  238. package/src/core/auth.ts +103 -0
  239. package/src/core/codemod.ts +211 -0
  240. package/src/core/fsx.ts +80 -0
  241. package/src/core/http.ts +136 -0
  242. package/src/core/journal.ts +64 -0
  243. package/src/core/log.ts +9 -0
  244. package/src/core/pathGuard.ts +22 -0
  245. package/src/core/paths.ts +33 -0
  246. package/src/core/validate.ts +44 -0
  247. package/src/index.ts +27 -0
  248. package/test-critical-cases.mjs +258 -0
  249. package/tsconfig.json +21 -0
  250. package/vitest.config.mts +12 -0
@@ -0,0 +1,302 @@
1
+ import { useAuthToken } from '@convex-dev/auth/react';
2
+ import { api } from '@vibefast/backend/_generated/api';
3
+ import type { Id } from '@vibefast/backend/_generated/dataModel';
4
+ import { useQuery } from 'convex/react';
5
+ import { Stack } from 'expo-router';
6
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
7
+ import { Keyboard, TouchableWithoutFeedback, View } from 'react-native';
8
+ import Animated from 'react-native-reanimated';
9
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
10
+
11
+ import { chatbotApi } from '@/api-client/chatbot';
12
+ import {
13
+ FocusAwareStatusBar,
14
+ NetworkConnectivityWrapper,
15
+ Pressable,
16
+ Spinner,
17
+ Text,
18
+ } from '@/components/ui';
19
+ import { Trash2 } from '@/components/ui/icons';
20
+ import { useConvexAuth } from '@/features/authentication/services';
21
+ import { ChatInputBar } from '@/features/chatbot/components/chat-input-bar';
22
+ import { MessageList } from '@/features/chatbot/components/message-list';
23
+ import { ReportContentModal } from '@/features/chatbot/components/report-content-modal';
24
+ import type { ModelType } from '@/features/chatbot/constants/models';
25
+ import {
26
+ DEFAULT_MODELS,
27
+ getModelById,
28
+ } from '@/features/chatbot/constants/models';
29
+ import { useChatHandlers } from '@/features/chatbot/hooks/use-chat-handlers';
30
+ import { useKeyboardCoordinator } from '@/features/chatbot/hooks/use-keyboard-coordinator';
31
+ import { translate } from '@/lib';
32
+ import { useThemeConfig } from '@/lib/use-theme-config';
33
+
34
+ import { useConversation } from '../hooks/use-conversation';
35
+
36
+ const TOOL_STATUS_LABELS: Record<string, string> = {
37
+ tavilySearch: 'Searching the web…',
38
+ userProfile: 'Checking your profile…',
39
+ };
40
+
41
+ export type LocalMessageContentPart =
42
+ | { type: 'text'; text: string }
43
+ | { type: 'image'; image: string | URL };
44
+
45
+ export default function ChatConversationScreen() {
46
+ const { userQuery, isLoading: isAuthLoading } = useConvexAuth();
47
+ const authToken = useAuthToken();
48
+ const theme = useThemeConfig();
49
+ const insets = useSafeAreaInsets();
50
+ const keyboardCoordinator = useKeyboardCoordinator();
51
+ const [selectedModel, setSelectedModel] = useState<ModelType>(
52
+ DEFAULT_MODELS.openai,
53
+ );
54
+
55
+ const { conversationId, initialMessages } = useConversation(userQuery?._id);
56
+
57
+ const agentSession = chatbotApi.useAgentSession(
58
+ conversationId ? (conversationId as Id<'conversations'>) : undefined,
59
+ );
60
+
61
+ const [hasManualModelSelection, setHasManualModelSelection] = useState(false);
62
+
63
+ const selectedModelConfig = useMemo(
64
+ () => getModelById(selectedModel),
65
+ [selectedModel],
66
+ );
67
+ const preferredProvider = selectedModelConfig?.provider ?? 'openai';
68
+
69
+ useEffect(() => {
70
+ setHasManualModelSelection(false);
71
+ if (!conversationId) {
72
+ setSelectedModel(DEFAULT_MODELS.openai);
73
+ }
74
+ }, [conversationId]);
75
+
76
+ useEffect(() => {
77
+ const sessionModel = agentSession?.preferredModel;
78
+ if (!sessionModel) {
79
+ return;
80
+ }
81
+ if (hasManualModelSelection) {
82
+ return;
83
+ }
84
+ const knownModel = getModelById(sessionModel);
85
+ if (!knownModel) {
86
+ return;
87
+ }
88
+ if (knownModel.id === selectedModel) {
89
+ return;
90
+ }
91
+ setSelectedModel(knownModel.id as ModelType);
92
+ }, [agentSession?.preferredModel, hasManualModelSelection, selectedModel]);
93
+
94
+ const handleModelChange = useCallback((model: ModelType) => {
95
+ setSelectedModel(model);
96
+ setHasManualModelSelection(true);
97
+ }, []);
98
+
99
+ const {
100
+ messages,
101
+ input,
102
+ isLoading: isChatLoading,
103
+ error,
104
+ currentStatus,
105
+ messageToReport,
106
+ handleReportMessage,
107
+ handleSubmitReport,
108
+ handleCancelReport,
109
+ handleTextInputChange,
110
+ handleMessageSubmit,
111
+ handleClearChat,
112
+ } = useChatHandlers({
113
+ conversationId,
114
+ authToken: authToken ?? undefined,
115
+ initialMessages,
116
+ preferredModel: selectedModel,
117
+ preferredProvider,
118
+ });
119
+
120
+ const handleSubmitWithKeyboardReset = useCallback(
121
+ async (
122
+ attachments?: {
123
+ type: 'image';
124
+ storageId: string;
125
+ fileName?: string;
126
+ mimeType: string;
127
+ }[],
128
+ ) => {
129
+ await handleMessageSubmit(attachments);
130
+ keyboardCoordinator.resetPadding();
131
+ },
132
+ [handleMessageSubmit, keyboardCoordinator],
133
+ );
134
+
135
+ // Stable storage ID tracking - only grows, never shrinks
136
+ const [knownStorageIds, setKnownStorageIds] = React.useState<
137
+ Id<'_storage'>[]
138
+ >([]);
139
+
140
+ React.useEffect(() => {
141
+ const set = new Set(knownStorageIds);
142
+ messages.forEach((m) =>
143
+ m.attachments?.forEach((a) => set.add(a.storageId)),
144
+ );
145
+ if (set.size !== knownStorageIds.length) {
146
+ setKnownStorageIds([...set]); // only grows
147
+ }
148
+ }, [messages, knownStorageIds]);
149
+
150
+ // Reset known storage IDs when conversation changes to avoid stale/foreign IDs
151
+ React.useEffect(() => {
152
+ setKnownStorageIds([]);
153
+ }, [conversationId]);
154
+
155
+ // Batch fetch URLs for all known storage IDs
156
+ const urlMap = useQuery(
157
+ api.shared.files.getUrlsForStorageIds,
158
+ knownStorageIds.length ? { storageIds: knownStorageIds } : 'skip',
159
+ );
160
+
161
+ // Never blank out an existing URL during transient renders
162
+ const messagesWithUrls = React.useMemo(() => {
163
+ if (!urlMap) return messages;
164
+ return messages.map((m) => ({
165
+ ...m,
166
+ attachments: m.attachments?.map((a) => ({
167
+ ...a,
168
+ url: urlMap[a.storageId] || a.url,
169
+ })),
170
+ }));
171
+ }, [messages, urlMap]);
172
+
173
+ const statusLabel = useMemo(() => {
174
+ if (!currentStatus) {
175
+ return null;
176
+ }
177
+
178
+ if (currentStatus.type === 'tool') {
179
+ return (
180
+ currentStatus.label ??
181
+ TOOL_STATUS_LABELS[currentStatus.toolName] ??
182
+ `Using ${currentStatus.toolName}`
183
+ );
184
+ }
185
+
186
+ return currentStatus.label ?? 'Thinking…';
187
+ }, [currentStatus]);
188
+
189
+ // Clear chat header button - only show when there are messages
190
+ const clearChatButton = useCallback(() => {
191
+ if (messages.length === 0) return null;
192
+
193
+ return (
194
+ <Pressable
195
+ onPress={handleClearChat}
196
+ className="px-2"
197
+ accessibilityLabel={translate('chatbot.clear_chat')}
198
+ accessibilityHint={translate('chatbot.clear_chat_hint')}
199
+ accessibilityRole="button"
200
+ testID="clear-chat-button"
201
+ >
202
+ <Trash2 color={theme.colors.foreground} />
203
+ </Pressable>
204
+ );
205
+ }, [messages.length, handleClearChat, theme.colors.foreground]);
206
+
207
+ useEffect(() => {
208
+ if (error) {
209
+ console.error('[CHATBOT_FRONTEND] Chat error:', error);
210
+ }
211
+ }, [error]);
212
+
213
+ if (isAuthLoading) {
214
+ return (
215
+ <View className="flex-1 items-center justify-center">
216
+ <Text className="text-muted-foreground">
217
+ {translate('chatbot.loading_session')}
218
+ </Text>
219
+ </View>
220
+ );
221
+ }
222
+
223
+ if (!userQuery) {
224
+ return (
225
+ <View className="flex-1 items-center justify-center">
226
+ <Text className="text-muted-foreground">
227
+ {translate('chatbot.please_sign_in')}
228
+ </Text>
229
+ </View>
230
+ );
231
+ }
232
+
233
+ // Show conversation loading only when conversation is not ready
234
+ const conversationLoading = conversationId === null && !isAuthLoading;
235
+
236
+ return (
237
+ <NetworkConnectivityWrapper>
238
+ <TouchableWithoutFeedback
239
+ onPress={Keyboard.dismiss}
240
+ accessible={false}
241
+ touchSoundDisabled
242
+ >
243
+ <View style={{ flex: 1 }}>
244
+ <Stack.Screen
245
+ options={{
246
+ title: translate('chatbot.title'),
247
+ headerRight: clearChatButton,
248
+ }}
249
+ />
250
+ <FocusAwareStatusBar />
251
+
252
+ <View style={{ flex: 1, paddingTop: insets.top + 12 }}>
253
+ <MessageList
254
+ messages={messagesWithUrls}
255
+ onReportMessage={handleReportMessage}
256
+ testID="chat-conversation-screen"
257
+ />
258
+ </View>
259
+
260
+ {statusLabel ? (
261
+ <View className="pb-6 pl-5 pr-4 pt-2">
262
+ <View className="flex-row gap-3">
263
+ <Spinner
264
+ size="sm"
265
+ variant="pulse"
266
+ color={theme.colors.foreground}
267
+ />
268
+ <Text className="text-base font-medium text-neutral-400 dark:text-neutral-500">
269
+ {statusLabel}
270
+ </Text>
271
+ </View>
272
+ </View>
273
+ ) : null}
274
+
275
+ <ChatInputBar
276
+ input={input}
277
+ onInputChange={handleTextInputChange}
278
+ onSubmit={handleSubmitWithKeyboardReset}
279
+ isLoading={conversationLoading || isChatLoading}
280
+ showSuggestedMessages={messages.length === 0}
281
+ selectedModel={selectedModel}
282
+ onModelChange={handleModelChange}
283
+ />
284
+
285
+ {/* Animated spacer for keyboard */}
286
+ <Animated.View
287
+ pointerEvents="none"
288
+ style={keyboardCoordinator.keyboardPadding}
289
+ />
290
+
291
+ {messageToReport && (
292
+ <ReportContentModal
293
+ message={messageToReport}
294
+ onSubmit={handleSubmitReport}
295
+ onCancel={handleCancelReport}
296
+ />
297
+ )}
298
+ </View>
299
+ </TouchableWithoutFeedback>
300
+ </NetworkConnectivityWrapper>
301
+ );
302
+ }
@@ -0,0 +1,59 @@
1
+ import type React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { Pressable, Text } from '@/components/ui';
5
+ import { Settings as SettingsIcon } from '@/components/ui/icons';
6
+
7
+ type ChatHeaderButtonsProps = {
8
+ messagesCount: number;
9
+ onSettingsPress: () => void;
10
+ onClearPress: () => void;
11
+ };
12
+
13
+ /**
14
+ * Header buttons for chat screen including settings and clear chat
15
+ */
16
+ export const ChatHeaderButtons: React.FC<ChatHeaderButtonsProps> = ({
17
+ messagesCount,
18
+ onSettingsPress,
19
+ onClearPress,
20
+ }) => {
21
+ const renderSettingsButton = () => (
22
+ <Pressable
23
+ onPress={onSettingsPress}
24
+ className="mr-2 rounded-lg p-2"
25
+ accessibilityLabel="Chat settings"
26
+ accessibilityHint="Open chat settings to change AI provider and model"
27
+ accessibilityRole="button"
28
+ testID="chat-settings-button"
29
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
30
+ >
31
+ <SettingsIcon color="#666" width={20} height={20} />
32
+ </Pressable>
33
+ );
34
+
35
+ const renderClearChatButton = () => {
36
+ if (messagesCount === 0) return null;
37
+
38
+ return (
39
+ <Pressable
40
+ onPress={onClearPress}
41
+ className="mr-2 rounded-lg bg-destructive/10 px-3 py-2"
42
+ accessibilityLabel="Clear chat"
43
+ accessibilityHint="Clear all messages in this conversation"
44
+ accessibilityRole="button"
45
+ testID="clear-chat-button"
46
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
47
+ >
48
+ <Text className="text-sm font-medium text-destructive">Clear</Text>
49
+ </Pressable>
50
+ );
51
+ };
52
+
53
+ return (
54
+ <View className="flex-row items-center">
55
+ {renderSettingsButton()}
56
+ {renderClearChatButton()}
57
+ </View>
58
+ );
59
+ };