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,161 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ import { Keyboard } from 'react-native';
3
+ import {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withTiming,
7
+ } from 'react-native-reanimated';
8
+ import { runOnUI } from 'react-native-worklets';
9
+
10
+ /**
11
+ * Hook to coordinate keyboard animations and padding.
12
+ * Eliminates race conditions in keyboard show/hide animations.
13
+ *
14
+ * Features:
15
+ * - Smooth 300ms animations using Reanimated
16
+ * - Cancels pending animations to prevent stacking
17
+ * - Synchronized timing with message submission
18
+ * - Automatic cleanup on unmount
19
+ * - 60fps animations using runOnUI
20
+ *
21
+ * Requirements addressed:
22
+ * - 3.1: Animate padding smoothly over 300ms
23
+ * - 3.2: Reset padding to zero with synchronized timing
24
+ * - 3.3: Dismiss keyboard and reset padding atomically on message send
25
+ * - 3.4: Cancel pending animations to prevent stacking
26
+ * - 3.5: Prioritize submission action over animation completion
27
+ *
28
+ * @returns Object containing animated padding value, reset function, and animated style
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * const keyboardCoordinator = useKeyboardCoordinator();
33
+ *
34
+ * // Use animated style for keyboard padding
35
+ * <Animated.View style={keyboardCoordinator.keyboardPadding} />
36
+ *
37
+ * // Reset padding when sending message
38
+ * const handleSubmit = async () => {
39
+ * await sendMessage();
40
+ * keyboardCoordinator.resetPadding();
41
+ * };
42
+ * ```
43
+ */
44
+ export function useKeyboardCoordinator() {
45
+ // Animated padding value for smooth transitions
46
+ const height = useSharedValue(0);
47
+
48
+ // Pending timeout reference for cancellation
49
+ const pendingTimeout = useRef<NodeJS.Timeout | null>(null);
50
+
51
+ /**
52
+ * Cancel any pending animations.
53
+ * Prevents animation stacking when rapid keyboard events occur.
54
+ */
55
+ const cancelPending = useCallback(() => {
56
+ if (pendingTimeout.current) {
57
+ clearTimeout(pendingTimeout.current);
58
+ pendingTimeout.current = null;
59
+ }
60
+ }, []);
61
+
62
+ /**
63
+ * Reset padding to zero with smooth animation.
64
+ * Uses runOnUI for 60fps performance.
65
+ * Cancels any pending animations before starting new one.
66
+ */
67
+ const resetPadding = useCallback(() => {
68
+ cancelPending();
69
+
70
+ runOnUI(() => {
71
+ 'worklet';
72
+ height.value = withTiming(0, { duration: 300 });
73
+ })();
74
+ }, [height, cancelPending]);
75
+
76
+ /**
77
+ * Handle keyboard show event.
78
+ * Animates padding to match keyboard height.
79
+ *
80
+ * @param keyboardHeight - Height of the keyboard in pixels
81
+ */
82
+ const onKeyboardShow = useCallback(
83
+ (keyboardHeight: number) => {
84
+ cancelPending();
85
+
86
+ runOnUI(() => {
87
+ 'worklet';
88
+ height.value = withTiming(keyboardHeight, { duration: 300 });
89
+ })();
90
+ },
91
+ [height, cancelPending],
92
+ );
93
+
94
+ /**
95
+ * Handle keyboard hide event.
96
+ * Resets padding to zero with animation.
97
+ */
98
+ const onKeyboardHide = useCallback(() => {
99
+ resetPadding();
100
+ }, [resetPadding]);
101
+
102
+ /**
103
+ * Animated style for keyboard padding.
104
+ * Apply this to a View component to create dynamic spacing.
105
+ */
106
+ const keyboardPadding = useAnimatedStyle(() => {
107
+ return {
108
+ height: height.value,
109
+ };
110
+ }, [height]);
111
+
112
+ /**
113
+ * Set up keyboard event listeners.
114
+ * Uses keyboardWillShow for instant response (iOS) and keyboardDidShow as fallback (Android).
115
+ * Automatically cleans up on unmount.
116
+ */
117
+ useEffect(() => {
118
+ // Use 'Will' events for instant response on iOS, 'Did' events as fallback for Android
119
+ const showListener = Keyboard.addListener('keyboardWillShow', (e) => {
120
+ onKeyboardShow(e.endCoordinates.height);
121
+ });
122
+
123
+ const hideListener = Keyboard.addListener(
124
+ 'keyboardWillHide',
125
+ onKeyboardHide,
126
+ );
127
+
128
+ // Fallback for Android (doesn't have 'Will' events)
129
+ const showListenerAndroid = Keyboard.addListener('keyboardDidShow', (e) => {
130
+ onKeyboardShow(e.endCoordinates.height);
131
+ });
132
+
133
+ const hideListenerAndroid = Keyboard.addListener(
134
+ 'keyboardDidHide',
135
+ onKeyboardHide,
136
+ );
137
+
138
+ return () => {
139
+ showListener.remove();
140
+ hideListener.remove();
141
+ showListenerAndroid.remove();
142
+ hideListenerAndroid.remove();
143
+ cancelPending();
144
+ };
145
+ }, [onKeyboardShow, onKeyboardHide, cancelPending]);
146
+
147
+ return {
148
+ // Animated values
149
+ height,
150
+ keyboardPadding,
151
+
152
+ // Methods
153
+ resetPadding,
154
+ cancelPending,
155
+ onKeyboardShow,
156
+ onKeyboardHide,
157
+
158
+ // Refs
159
+ pendingTimeout,
160
+ };
161
+ }
@@ -0,0 +1,207 @@
1
+ import type { FlashListRef } from '@shopify/flash-list';
2
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
3
+ import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
4
+
5
+ const NEAR_BOTTOM_THRESHOLD = 120;
6
+ const SCROLL_DEBOUNCE_MS = 32;
7
+
8
+ /**
9
+ * Smart scroll manager for chat message lists.
10
+ * Handles auto-scrolling based on user intent and scroll position.
11
+ *
12
+ * Features:
13
+ * - Auto-scroll when user is near bottom (within 120px threshold)
14
+ * - Cancel auto-scroll when user manually scrolls up
15
+ * - Always scroll on user message send
16
+ * - Track drag state to prevent scroll interruption
17
+ * - Debounced scroll scheduling (32ms) for performance
18
+ *
19
+ * Requirements addressed:
20
+ * - 4.1: Auto-scroll when near bottom (120px threshold)
21
+ * - 4.2: Don't auto-scroll when user scrolls up to read history
22
+ * - 4.3: Always scroll to bottom when user sends a message
23
+ * - 4.4: Maintain scroll position during streaming if reading history
24
+ * - 4.5: Cancel pending auto-scroll operations when user drags
25
+ *
26
+ * @param flashListRef - Reference to the FlashList component
27
+ * @returns Object containing scroll state refs, methods, and event handlers
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * const flashListRef = useRef<FlashListRef<Message> | null>(null);
32
+ * const scrollManager = useSmartScrollManager(flashListRef);
33
+ *
34
+ * // In useEffect for message updates
35
+ * useEffect(() => {
36
+ * scrollManager.handleMessagesUpdate(messages, previousMessageIds, { logTelemetry: true });
37
+ * previousMessageIds.current = messages.map(m => m.id);
38
+ * }, [messages, scrollManager]);
39
+ *
40
+ * // In FlashList props
41
+ * <FlashList
42
+ * onScroll={scrollManager.onScroll}
43
+ * onScrollBeginDrag={scrollManager.onScrollBeginDrag}
44
+ * onScrollEndDrag={scrollManager.onScrollEndDrag}
45
+ * onMomentumScrollBegin={scrollManager.onMomentumScrollBegin}
46
+ * onMomentumScrollEnd={scrollManager.onMomentumScrollEnd}
47
+ * />
48
+ * ```
49
+ */
50
+ export function useSmartScrollManager<T>(
51
+ flashListRef: React.RefObject<FlashListRef<T> | null>,
52
+ ) {
53
+ // Track if user is near bottom (within 120px)
54
+ const isNearBottom = useRef(true);
55
+
56
+ // Track if user is actively dragging
57
+ const isDragging = useRef(false);
58
+
59
+ // Pending scroll timeout for debouncing
60
+ const pendingScroll = useRef<ReturnType<typeof setTimeout> | null>(null);
61
+
62
+ /**
63
+ * Cancel any pending scroll operation
64
+ */
65
+ const cancelScroll = useCallback(() => {
66
+ if (pendingScroll.current) {
67
+ clearTimeout(pendingScroll.current);
68
+ pendingScroll.current = null;
69
+ }
70
+ }, []);
71
+
72
+ /**
73
+ * Scroll to bottom immediately
74
+ * Includes null check to prevent crashes when component unmounts
75
+ */
76
+ const scrollToBottom = useCallback(() => {
77
+ if (flashListRef.current) {
78
+ try {
79
+ flashListRef.current.scrollToEnd({ animated: true });
80
+ isNearBottom.current = true;
81
+ } catch (error) {
82
+ // Silently catch scroll errors (e.g., if list is unmounting)
83
+ console.warn('[ScrollManager] Scroll failed:', error);
84
+ }
85
+ }
86
+ }, [flashListRef]);
87
+
88
+ /**
89
+ * Schedule scroll to bottom with 32ms debounce
90
+ * Cancels any pending scroll before scheduling new one
91
+ * This prevents rapid scroll calls during streaming
92
+ */
93
+ const scheduleScroll = useCallback(() => {
94
+ // If already pending, don't schedule another one
95
+ if (pendingScroll.current) {
96
+ return;
97
+ }
98
+
99
+ pendingScroll.current = setTimeout(() => {
100
+ pendingScroll.current = null;
101
+ scrollToBottom();
102
+ }, SCROLL_DEBOUNCE_MS);
103
+ }, [scrollToBottom]);
104
+
105
+ /**
106
+ * Handle scroll events to track near-bottom state
107
+ * Updates isNearBottom based on 120px threshold
108
+ */
109
+ const onScroll = useCallback(
110
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
111
+ const { contentOffset, contentSize, layoutMeasurement } =
112
+ event.nativeEvent;
113
+ const visibleBottom = contentOffset.y + layoutMeasurement.height;
114
+ const distanceFromBottom = Math.max(
115
+ contentSize.height - visibleBottom,
116
+ 0,
117
+ );
118
+
119
+ isNearBottom.current = distanceFromBottom < NEAR_BOTTOM_THRESHOLD;
120
+ },
121
+ [],
122
+ );
123
+
124
+ /**
125
+ * Handle scroll begin drag - cancel pending scrolls
126
+ */
127
+ const onScrollBeginDrag = useCallback(() => {
128
+ isDragging.current = true;
129
+ cancelScroll();
130
+ }, [cancelScroll]);
131
+
132
+ /**
133
+ * Handle scroll end drag - mark dragging as complete
134
+ */
135
+ const onScrollEndDrag = useCallback(() => {
136
+ isDragging.current = false;
137
+ }, []);
138
+
139
+ /**
140
+ * Handle momentum scroll begin - treat as dragging
141
+ */
142
+ const onMomentumScrollBegin = useCallback(() => {
143
+ isDragging.current = true;
144
+ }, []);
145
+
146
+ /**
147
+ * Handle momentum scroll end - mark dragging as complete
148
+ */
149
+ const onMomentumScrollEnd = useCallback(() => {
150
+ isDragging.current = false;
151
+ }, []);
152
+
153
+ /**
154
+ * Determine if should auto-scroll based on current state
155
+ * Always scrolls on user message send
156
+ * Otherwise only scrolls if user is near bottom and not dragging
157
+ */
158
+ const shouldAutoScroll = useCallback((userSentMessage: boolean): boolean => {
159
+ if (userSentMessage) {
160
+ return true;
161
+ }
162
+ return !isDragging.current && isNearBottom.current;
163
+ }, []);
164
+
165
+ /**
166
+ * Cleanup pending scrolls on unmount
167
+ */
168
+ useEffect(() => {
169
+ return () => {
170
+ cancelScroll();
171
+ };
172
+ }, [cancelScroll]);
173
+
174
+ // Memoize the return object to prevent unnecessary re-renders
175
+ return useMemo(
176
+ () => ({
177
+ // State refs
178
+ isNearBottom,
179
+ isDragging,
180
+ pendingScroll,
181
+
182
+ // Methods
183
+ scheduleScroll,
184
+ cancelScroll,
185
+ scrollToBottom,
186
+ shouldAutoScroll,
187
+
188
+ // Event handlers
189
+ onScroll,
190
+ onScrollBeginDrag,
191
+ onScrollEndDrag,
192
+ onMomentumScrollBegin,
193
+ onMomentumScrollEnd,
194
+ }),
195
+ [
196
+ scheduleScroll,
197
+ cancelScroll,
198
+ scrollToBottom,
199
+ shouldAutoScroll,
200
+ onScroll,
201
+ onScrollBeginDrag,
202
+ onScrollEndDrag,
203
+ onMomentumScrollBegin,
204
+ onMomentumScrollEnd,
205
+ ],
206
+ );
207
+ }
@@ -0,0 +1,86 @@
1
+ // Import directly to avoid circular dependencies
2
+ import { DEFAULT_MODELS, MODELS } from './models';
3
+ import type { ModelConfig, ModelId, Provider } from './types';
4
+
5
+ // Re-export models and model functions
6
+ export { DEFAULT_MODELS, getModelDescription, MODELS } from './models';
7
+ // Re-export provider functions
8
+ export {
9
+ getAllProviders,
10
+ getProviderConfig,
11
+ getProviderDisplayName,
12
+ getProviderIcon,
13
+ PROVIDER_CONFIGS,
14
+ } from './providers';
15
+ // Re-export types
16
+ export type {
17
+ ModelCategory,
18
+ ModelConfig,
19
+ ModelId,
20
+ ModelOptions,
21
+ ModelSpeed,
22
+ Provider,
23
+ ProviderConfig,
24
+ } from './types';
25
+
26
+ // Additional helper functions
27
+ export const getModelById = (id: string): ModelConfig | undefined => {
28
+ return MODELS.find((model) => model.id === id);
29
+ };
30
+
31
+ export const getModelsByProvider = (provider: Provider): ModelConfig[] => {
32
+ return MODELS.filter((model) => model.provider === provider);
33
+ };
34
+
35
+ export const getPremiumModels = (): ModelConfig[] => {
36
+ return MODELS.filter((model) => model.category === 'premium');
37
+ };
38
+
39
+ export const getFreeModels = (): ModelConfig[] => {
40
+ return MODELS.filter((model) => model.category === 'free');
41
+ };
42
+
43
+ export const getDefaultModel = (provider: Provider): ModelConfig => {
44
+ const defaultId = DEFAULT_MODELS[provider];
45
+ const model = getModelById(defaultId);
46
+ if (!model) {
47
+ throw new Error(
48
+ `Default model ${defaultId} not found for provider ${provider}`,
49
+ );
50
+ }
51
+ return model;
52
+ };
53
+
54
+ export const isPremiumModel = (modelId: string): boolean => {
55
+ const model = getModelById(modelId);
56
+ return model?.category === 'premium';
57
+ };
58
+
59
+ export const getAllModelIds = (): string[] => {
60
+ return MODELS.map((model: any) => model.id);
61
+ };
62
+
63
+ export const getModelsByCategory = (
64
+ category: 'premium' | 'free' | 'specialized',
65
+ ) => {
66
+ return MODELS.filter((model: any) => model.category === category);
67
+ };
68
+
69
+ export const getModelProvider = (modelId: string): string | undefined => {
70
+ const model = getModelById(modelId);
71
+ return model?.provider;
72
+ };
73
+
74
+ // Backward compatibility exports (to match old API) - lazy evaluation
75
+ export const ALL_MODELS = (): string[] => {
76
+ return MODELS.map((model: any) => model.id);
77
+ };
78
+
79
+ export const PREMIUM_MODELS = (): string[] => {
80
+ return MODELS.filter((model: any) => model.category === 'premium').map(
81
+ (model: any) => model.id,
82
+ );
83
+ };
84
+
85
+ // Legacy type for backward compatibility
86
+ export type ModelType = ModelId;
@@ -0,0 +1,162 @@
1
+ import type { ModelConfig, Provider } from './types';
2
+
3
+ export const MODELS: ModelConfig[] = [
4
+ // Claude Models
5
+ {
6
+ id: 'claude-4-sonnet',
7
+ name: 'Claude 4 Sonnet',
8
+ provider: 'claude',
9
+ description: 'Most capable • Great at coding',
10
+ category: 'premium',
11
+ capabilities: ['coding', 'reasoning', 'analysis', 'writing'],
12
+ contextWindow: 200000,
13
+ speed: 'medium',
14
+ released: '2024',
15
+ tags: ['latest', 'most-capable'],
16
+ },
17
+ {
18
+ id: 'claude-4.5-sonnet',
19
+ name: 'Claude 4.5 Sonnet',
20
+ provider: 'claude',
21
+ description: 'Fast & efficient • Perfect for chat',
22
+ category: 'premium',
23
+ capabilities: ['chat', 'reasoning', 'coding', 'analysis'],
24
+ contextWindow: 200000,
25
+ speed: 'fast',
26
+ released: '2024',
27
+ tags: ['balanced', 'chat-optimized'],
28
+ },
29
+
30
+ // OpenAI Models
31
+ {
32
+ id: 'gpt-5',
33
+ name: 'GPT-5',
34
+ provider: 'openai',
35
+ description: 'For complex tasks and thinking',
36
+ category: 'premium',
37
+ capabilities: ['reasoning', 'coding', 'analysis', 'mathematics'],
38
+ contextWindow: 200000,
39
+ speed: 'medium',
40
+ released: '2024',
41
+ tags: ['most-capable', 'complex-tasks'],
42
+ },
43
+ {
44
+ id: 'gpt-5-mini',
45
+ name: 'GPT-5 Mini',
46
+ provider: 'openai',
47
+ description: 'Quick thinking & reliable',
48
+ category: 'free',
49
+ capabilities: ['chat', 'reasoning', 'coding', 'analysis'],
50
+ contextWindow: 128000,
51
+ speed: 'fast',
52
+ released: '2024',
53
+ tags: ['balanced', 'efficient'],
54
+ },
55
+ {
56
+ id: 'gpt-5-nano',
57
+ name: 'GPT-5 Nano',
58
+ provider: 'openai',
59
+ description: 'Very fast • Great for basic work',
60
+ category: 'free',
61
+ capabilities: ['chat', 'basic-tasks', 'quick-responses'],
62
+ contextWindow: 16000,
63
+ speed: 'fast',
64
+ released: '2024',
65
+ tags: ['fastest', 'basic-tasks'],
66
+ },
67
+ {
68
+ id: 'gpt-4o-mini',
69
+ name: 'GPT-4o Mini',
70
+ provider: 'openai',
71
+ description: 'Very fast • Great for basic work',
72
+ category: 'free',
73
+ capabilities: ['chat', 'basic-tasks', 'quick-responses'],
74
+ contextWindow: 16000,
75
+ speed: 'fast',
76
+ released: '2024',
77
+ tags: ['fastest', 'basic-tasks'],
78
+ },
79
+
80
+ // Gemini Models
81
+ {
82
+ id: 'gemini-2.0-flash',
83
+ name: 'Gemini 2.0 Flash',
84
+ provider: 'gemini',
85
+ description: 'Latest model • Ultra fast',
86
+ category: 'free',
87
+ capabilities: ['chat', 'reasoning', 'multimodal', 'coding'],
88
+ contextWindow: 1000000,
89
+ speed: 'fast',
90
+ released: '2024',
91
+ tags: ['latest', 'multimodal', 'fast'],
92
+ },
93
+ {
94
+ id: 'gemini-2.5-flash',
95
+ name: 'Gemini 2.5 Flash',
96
+ provider: 'gemini',
97
+ description: 'Balanced • Great performance',
98
+ category: 'free',
99
+ capabilities: ['chat', 'reasoning', 'multimodal', 'analysis'],
100
+ contextWindow: 1000000,
101
+ speed: 'fast',
102
+ released: '2024',
103
+ tags: ['balanced', 'multimodal'],
104
+ },
105
+ {
106
+ id: 'gemini-2.5-pro',
107
+ name: 'Gemini 2.5 Pro',
108
+ provider: 'gemini',
109
+ description: 'Advanced reasoning',
110
+ category: 'premium',
111
+ capabilities: ['reasoning', 'coding', 'mathematics', 'analysis'],
112
+ contextWindow: 2000000,
113
+ speed: 'medium',
114
+ released: '2024',
115
+ tags: ['most-capable', 'advanced-reasoning'],
116
+ },
117
+ ];
118
+
119
+ // Helper to get models by provider
120
+ export const getModelsByProvider = (provider: Provider): ModelConfig[] => {
121
+ return MODELS.filter((model) => model.provider === provider);
122
+ };
123
+
124
+ // Helper to get premium models
125
+ export const getPremiumModels = (): ModelConfig[] => {
126
+ return MODELS.filter((model) => model.category === 'premium');
127
+ };
128
+
129
+ // Helper to get free models
130
+ export const getFreeModels = (): ModelConfig[] => {
131
+ return MODELS.filter((model) => model.category === 'free');
132
+ };
133
+
134
+ // Helper to get model by ID
135
+ export const getModelById = (id: string): ModelConfig | undefined => {
136
+ return MODELS.find((model) => model.id === id);
137
+ };
138
+
139
+ // Default models for each provider
140
+ export const DEFAULT_MODELS: Record<Provider, string> = {
141
+ claude: 'claude-4.5-sonnet',
142
+ openai: 'gpt-5-nano',
143
+ gemini: 'gemini-2.0-flash',
144
+ };
145
+
146
+ // Get default model for a provider
147
+ export const getDefaultModel = (provider: Provider): ModelConfig => {
148
+ const defaultId = DEFAULT_MODELS[provider];
149
+ const model = getModelById(defaultId);
150
+ if (!model) {
151
+ throw new Error(
152
+ `Default model ${defaultId} not found for provider ${provider}`,
153
+ );
154
+ }
155
+ return model;
156
+ };
157
+
158
+ // Get model description (single source of truth)
159
+ export const getModelDescription = (modelId: string): string => {
160
+ const model = getModelById(modelId);
161
+ return model?.description || 'AI model';
162
+ };
@@ -0,0 +1,62 @@
1
+ import type React from 'react';
2
+
3
+ import { ClaudeIcon } from '@/components/ui/icons/claude';
4
+ import { GeminiIcon } from '@/components/ui/icons/gemini';
5
+ import { OpenaiIcon } from '@/components/ui/icons/openai';
6
+
7
+ import type { Provider, ProviderConfig } from './types';
8
+
9
+ export const PROVIDER_CONFIGS: Record<Provider, ProviderConfig> = {
10
+ claude: {
11
+ id: 'claude',
12
+ name: 'claude',
13
+ displayName: 'Anthropic Claude',
14
+ icon: ClaudeIcon,
15
+ defaultModel: 'claude-4.5-sonnet',
16
+ description: 'Advanced AI assistant with strong reasoning capabilities',
17
+ website: 'https://anthropic.com',
18
+ color: '#D97706',
19
+ },
20
+ openai: {
21
+ id: 'openai',
22
+ name: 'openai',
23
+ displayName: 'OpenAI',
24
+ icon: OpenaiIcon,
25
+ defaultModel: 'gpt-5-nano',
26
+ description: 'Leading AI research lab and ChatGPT creator',
27
+ website: 'https://openai.com',
28
+ color: '#10A37F',
29
+ },
30
+ gemini: {
31
+ id: 'gemini',
32
+ name: 'gemini',
33
+ displayName: 'Google Gemini',
34
+ icon: GeminiIcon,
35
+ defaultModel: 'gemini-2.0-flash',
36
+ description: "Google's multimodal AI models",
37
+ website: 'https://gemini.google.com',
38
+ color: '#4285F4',
39
+ },
40
+ };
41
+
42
+ // Get provider config by ID
43
+ export const getProviderConfig = (provider: Provider): ProviderConfig => {
44
+ return PROVIDER_CONFIGS[provider];
45
+ };
46
+
47
+ // Get all providers
48
+ export const getAllProviders = (): Provider[] => {
49
+ return Object.keys(PROVIDER_CONFIGS) as Provider[];
50
+ };
51
+
52
+ // Get provider icon component
53
+ export const getProviderIcon = (
54
+ provider: Provider,
55
+ ): React.ComponentType<any> => {
56
+ return PROVIDER_CONFIGS[provider].icon;
57
+ };
58
+
59
+ // Get provider display name
60
+ export const getProviderDisplayName = (provider: Provider): string => {
61
+ return PROVIDER_CONFIGS[provider].displayName;
62
+ };