nastech-app 1.0.0

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 (1111) hide show
  1. package/.claude/agents/i18n-translator.md +119 -0
  2. package/.claude/settings.json +8 -0
  3. package/.eas/workflows/ota.yaml +9 -0
  4. package/.eas/workflows/preview.yaml +12 -0
  5. package/.easignore +12 -0
  6. package/.github/workflows/eas-build.yml +24 -0
  7. package/CHANGELOG.md +117 -0
  8. package/CLAUDE.md +413 -0
  9. package/GoogleService-Info.plist +30 -0
  10. package/LICENSE +21 -0
  11. package/NasTechapp.md +383 -0
  12. package/README.md +75 -0
  13. package/Stores.md +85 -0
  14. package/TERMS.md +83 -0
  15. package/app.config.js +153 -0
  16. package/babel.config.js +28 -0
  17. package/deploy/nastech-app.yaml +51 -0
  18. package/docs/marketing/README-creators.md +73 -0
  19. package/eas-build-post-install.sh +11 -0
  20. package/eas-build-pre-install.sh +27 -0
  21. package/eas.json +78 -0
  22. package/google-services.json +67 -0
  23. package/index.ts +3 -0
  24. package/logo.png +0 -0
  25. package/metro.config.js +54 -0
  26. package/nativewind-env.d.ts +1 -0
  27. package/package.json +233 -0
  28. package/patches/.keep +0 -0
  29. package/plugins/withEinkCompatibility.js +156 -0
  30. package/public/.well-known/apple-app-site-association +22 -0
  31. package/public/.well-known/assetlinks.json +12 -0
  32. package/public/canvaskit.wasm +0 -0
  33. package/public/favicon-active.ico +0 -0
  34. package/release-dev.sh +7 -0
  35. package/release-production.sh +3 -0
  36. package/release.cjs +160 -0
  37. package/sources/-session/SessionView.tsx +944 -0
  38. package/sources/-session/sessionOverlayNav.ts +34 -0
  39. package/sources/app/(app)/_layout.tsx +321 -0
  40. package/sources/app/(app)/artifacts/[id].tsx +279 -0
  41. package/sources/app/(app)/artifacts/edit/[id].tsx +318 -0
  42. package/sources/app/(app)/artifacts/index.tsx +264 -0
  43. package/sources/app/(app)/artifacts/new.tsx +219 -0
  44. package/sources/app/(app)/changelog.tsx +113 -0
  45. package/sources/app/(app)/dev/colors.tsx +197 -0
  46. package/sources/app/(app)/dev/device-info.tsx +183 -0
  47. package/sources/app/(app)/dev/expo-constants.tsx +394 -0
  48. package/sources/app/(app)/dev/index.tsx +400 -0
  49. package/sources/app/(app)/dev/input-styles.tsx +1951 -0
  50. package/sources/app/(app)/dev/inverted-list.tsx +295 -0
  51. package/sources/app/(app)/dev/list-demo.tsx +125 -0
  52. package/sources/app/(app)/dev/logs.tsx +160 -0
  53. package/sources/app/(app)/dev/messages-demo-data.ts +479 -0
  54. package/sources/app/(app)/dev/messages-demo.tsx +45 -0
  55. package/sources/app/(app)/dev/modal-demo.tsx +211 -0
  56. package/sources/app/(app)/dev/multi-text-input.tsx +224 -0
  57. package/sources/app/(app)/dev/purchases.tsx +228 -0
  58. package/sources/app/(app)/dev/qr-test.tsx +168 -0
  59. package/sources/app/(app)/dev/session-composer.tsx +812 -0
  60. package/sources/app/(app)/dev/shimmer-demo.tsx +275 -0
  61. package/sources/app/(app)/dev/tests.tsx +203 -0
  62. package/sources/app/(app)/dev/tools2.tsx +556 -0
  63. package/sources/app/(app)/dev/typography.tsx +177 -0
  64. package/sources/app/(app)/dev/unistyles-demo.tsx +376 -0
  65. package/sources/app/(app)/friends/index.tsx +167 -0
  66. package/sources/app/(app)/friends/search.tsx +232 -0
  67. package/sources/app/(app)/inbox/index.tsx +124 -0
  68. package/sources/app/(app)/index.tsx +264 -0
  69. package/sources/app/(app)/machine/[id].tsx +646 -0
  70. package/sources/app/(app)/new/index.tsx +1611 -0
  71. package/sources/app/(app)/restore/index.tsx +167 -0
  72. package/sources/app/(app)/restore/manual.tsx +138 -0
  73. package/sources/app/(app)/server.tsx +234 -0
  74. package/sources/app/(app)/session/[id]/file.tsx +527 -0
  75. package/sources/app/(app)/session/[id]/files.tsx +442 -0
  76. package/sources/app/(app)/session/[id]/info.tsx +655 -0
  77. package/sources/app/(app)/session/[id]/message/[messageId].tsx +125 -0
  78. package/sources/app/(app)/session/[id].tsx +10 -0
  79. package/sources/app/(app)/session/recent.tsx +270 -0
  80. package/sources/app/(app)/settings/account.tsx +600 -0
  81. package/sources/app/(app)/settings/agents.tsx +180 -0
  82. package/sources/app/(app)/settings/appearance.tsx +259 -0
  83. package/sources/app/(app)/settings/connect/claude.tsx +178 -0
  84. package/sources/app/(app)/settings/features.tsx +177 -0
  85. package/sources/app/(app)/settings/index.tsx +3 -0
  86. package/sources/app/(app)/settings/language.tsx +106 -0
  87. package/sources/app/(app)/settings/usage.tsx +11 -0
  88. package/sources/app/(app)/settings/voice/language.tsx +114 -0
  89. package/sources/app/(app)/settings/voice.tsx +274 -0
  90. package/sources/app/(app)/terminal/connect.tsx +241 -0
  91. package/sources/app/(app)/terminal/index.tsx +184 -0
  92. package/sources/app/(app)/text-selection.tsx +149 -0
  93. package/sources/app/(app)/user/[id].tsx +314 -0
  94. package/sources/app/+html.tsx +39 -0
  95. package/sources/app/_layout.tsx +402 -0
  96. package/sources/assets/animations/game.json +1 -0
  97. package/sources/assets/animations/owl.json +1 -0
  98. package/sources/assets/animations/popcorn.json +1 -0
  99. package/sources/assets/animations/robot.json +1 -0
  100. package/sources/assets/animations/sparkles.json +1 -0
  101. package/sources/assets/animations/stone.json +1 -0
  102. package/sources/assets/fonts/BricolageGrotesque-Bold.ttf +0 -0
  103. package/sources/assets/fonts/IBMPlexMono-Italic.ttf +0 -0
  104. package/sources/assets/fonts/IBMPlexMono-Regular.ttf +0 -0
  105. package/sources/assets/fonts/IBMPlexMono-SemiBold.ttf +0 -0
  106. package/sources/assets/fonts/IBMPlexSans-Italic.ttf +0 -0
  107. package/sources/assets/fonts/IBMPlexSans-Regular.ttf +0 -0
  108. package/sources/assets/fonts/IBMPlexSans-SemiBold.ttf +0 -0
  109. package/sources/assets/fonts/SpaceMono-Regular.ttf +0 -0
  110. package/sources/assets/images/brutalist/Abstract-1.png +0 -0
  111. package/sources/assets/images/brutalist/Abstract-10.png +0 -0
  112. package/sources/assets/images/brutalist/Abstract-100.png +0 -0
  113. package/sources/assets/images/brutalist/Abstract-101.png +0 -0
  114. package/sources/assets/images/brutalist/Abstract-102.png +0 -0
  115. package/sources/assets/images/brutalist/Abstract-103.png +0 -0
  116. package/sources/assets/images/brutalist/Abstract-104.png +0 -0
  117. package/sources/assets/images/brutalist/Abstract-105.png +0 -0
  118. package/sources/assets/images/brutalist/Abstract-106.png +0 -0
  119. package/sources/assets/images/brutalist/Abstract-107.png +0 -0
  120. package/sources/assets/images/brutalist/Abstract-108.png +0 -0
  121. package/sources/assets/images/brutalist/Abstract-109.png +0 -0
  122. package/sources/assets/images/brutalist/Abstract-11.png +0 -0
  123. package/sources/assets/images/brutalist/Abstract-110.png +0 -0
  124. package/sources/assets/images/brutalist/Abstract-111.png +0 -0
  125. package/sources/assets/images/brutalist/Abstract-112.png +0 -0
  126. package/sources/assets/images/brutalist/Abstract-113.png +0 -0
  127. package/sources/assets/images/brutalist/Abstract-114.png +0 -0
  128. package/sources/assets/images/brutalist/Abstract-115.png +0 -0
  129. package/sources/assets/images/brutalist/Abstract-116.png +0 -0
  130. package/sources/assets/images/brutalist/Abstract-117.png +0 -0
  131. package/sources/assets/images/brutalist/Abstract-118.png +0 -0
  132. package/sources/assets/images/brutalist/Abstract-119.png +0 -0
  133. package/sources/assets/images/brutalist/Abstract-12.png +0 -0
  134. package/sources/assets/images/brutalist/Abstract-120.png +0 -0
  135. package/sources/assets/images/brutalist/Abstract-121.png +0 -0
  136. package/sources/assets/images/brutalist/Abstract-122.png +0 -0
  137. package/sources/assets/images/brutalist/Abstract-123.png +0 -0
  138. package/sources/assets/images/brutalist/Abstract-124.png +0 -0
  139. package/sources/assets/images/brutalist/Abstract-125.png +0 -0
  140. package/sources/assets/images/brutalist/Abstract-126.png +0 -0
  141. package/sources/assets/images/brutalist/Abstract-127.png +0 -0
  142. package/sources/assets/images/brutalist/Abstract-128.png +0 -0
  143. package/sources/assets/images/brutalist/Abstract-129.png +0 -0
  144. package/sources/assets/images/brutalist/Abstract-13.png +0 -0
  145. package/sources/assets/images/brutalist/Abstract-130.png +0 -0
  146. package/sources/assets/images/brutalist/Abstract-131.png +0 -0
  147. package/sources/assets/images/brutalist/Abstract-132.png +0 -0
  148. package/sources/assets/images/brutalist/Abstract-133.png +0 -0
  149. package/sources/assets/images/brutalist/Abstract-134.png +0 -0
  150. package/sources/assets/images/brutalist/Abstract-135.png +0 -0
  151. package/sources/assets/images/brutalist/Abstract-136.png +0 -0
  152. package/sources/assets/images/brutalist/Abstract-137.png +0 -0
  153. package/sources/assets/images/brutalist/Abstract-138.png +0 -0
  154. package/sources/assets/images/brutalist/Abstract-139.png +0 -0
  155. package/sources/assets/images/brutalist/Abstract-14.png +0 -0
  156. package/sources/assets/images/brutalist/Abstract-140.png +0 -0
  157. package/sources/assets/images/brutalist/Abstract-141.png +0 -0
  158. package/sources/assets/images/brutalist/Abstract-142.png +0 -0
  159. package/sources/assets/images/brutalist/Abstract-143.png +0 -0
  160. package/sources/assets/images/brutalist/Abstract-144.png +0 -0
  161. package/sources/assets/images/brutalist/Abstract-145.png +0 -0
  162. package/sources/assets/images/brutalist/Abstract-146.png +0 -0
  163. package/sources/assets/images/brutalist/Abstract-147.png +0 -0
  164. package/sources/assets/images/brutalist/Abstract-148.png +0 -0
  165. package/sources/assets/images/brutalist/Abstract-149.png +0 -0
  166. package/sources/assets/images/brutalist/Abstract-15.png +0 -0
  167. package/sources/assets/images/brutalist/Abstract-150.png +0 -0
  168. package/sources/assets/images/brutalist/Abstract-151.png +0 -0
  169. package/sources/assets/images/brutalist/Abstract-152.png +0 -0
  170. package/sources/assets/images/brutalist/Abstract-153.png +0 -0
  171. package/sources/assets/images/brutalist/Abstract-154.png +0 -0
  172. package/sources/assets/images/brutalist/Abstract-155.png +0 -0
  173. package/sources/assets/images/brutalist/Abstract-156.png +0 -0
  174. package/sources/assets/images/brutalist/Abstract-157.png +0 -0
  175. package/sources/assets/images/brutalist/Abstract-158.png +0 -0
  176. package/sources/assets/images/brutalist/Abstract-159.png +0 -0
  177. package/sources/assets/images/brutalist/Abstract-16.png +0 -0
  178. package/sources/assets/images/brutalist/Abstract-160.png +0 -0
  179. package/sources/assets/images/brutalist/Abstract-161.png +0 -0
  180. package/sources/assets/images/brutalist/Abstract-162.png +0 -0
  181. package/sources/assets/images/brutalist/Abstract-163.png +0 -0
  182. package/sources/assets/images/brutalist/Abstract-164.png +0 -0
  183. package/sources/assets/images/brutalist/Abstract-165.png +0 -0
  184. package/sources/assets/images/brutalist/Abstract-166.png +0 -0
  185. package/sources/assets/images/brutalist/Abstract-167.png +0 -0
  186. package/sources/assets/images/brutalist/Abstract-168.png +0 -0
  187. package/sources/assets/images/brutalist/Abstract-169.png +0 -0
  188. package/sources/assets/images/brutalist/Abstract-17.png +0 -0
  189. package/sources/assets/images/brutalist/Abstract-170.png +0 -0
  190. package/sources/assets/images/brutalist/Abstract-171.png +0 -0
  191. package/sources/assets/images/brutalist/Abstract-172.png +0 -0
  192. package/sources/assets/images/brutalist/Abstract-173.png +0 -0
  193. package/sources/assets/images/brutalist/Abstract-174.png +0 -0
  194. package/sources/assets/images/brutalist/Abstract-175.png +0 -0
  195. package/sources/assets/images/brutalist/Abstract-176.png +0 -0
  196. package/sources/assets/images/brutalist/Abstract-177.png +0 -0
  197. package/sources/assets/images/brutalist/Abstract-178.png +0 -0
  198. package/sources/assets/images/brutalist/Abstract-179.png +0 -0
  199. package/sources/assets/images/brutalist/Abstract-18.png +0 -0
  200. package/sources/assets/images/brutalist/Abstract-180.png +0 -0
  201. package/sources/assets/images/brutalist/Abstract-181.png +0 -0
  202. package/sources/assets/images/brutalist/Abstract-182.png +0 -0
  203. package/sources/assets/images/brutalist/Abstract-183.png +0 -0
  204. package/sources/assets/images/brutalist/Abstract-184.png +0 -0
  205. package/sources/assets/images/brutalist/Abstract-185.png +0 -0
  206. package/sources/assets/images/brutalist/Abstract-186.png +0 -0
  207. package/sources/assets/images/brutalist/Abstract-187.png +0 -0
  208. package/sources/assets/images/brutalist/Abstract-188.png +0 -0
  209. package/sources/assets/images/brutalist/Abstract-189.png +0 -0
  210. package/sources/assets/images/brutalist/Abstract-19.png +0 -0
  211. package/sources/assets/images/brutalist/Abstract-190.png +0 -0
  212. package/sources/assets/images/brutalist/Abstract-191.png +0 -0
  213. package/sources/assets/images/brutalist/Abstract-192.png +0 -0
  214. package/sources/assets/images/brutalist/Abstract-193.png +0 -0
  215. package/sources/assets/images/brutalist/Abstract-194.png +0 -0
  216. package/sources/assets/images/brutalist/Abstract-195.png +0 -0
  217. package/sources/assets/images/brutalist/Abstract-196.png +0 -0
  218. package/sources/assets/images/brutalist/Abstract-197.png +0 -0
  219. package/sources/assets/images/brutalist/Abstract-198.png +0 -0
  220. package/sources/assets/images/brutalist/Abstract-199.png +0 -0
  221. package/sources/assets/images/brutalist/Abstract-2.png +0 -0
  222. package/sources/assets/images/brutalist/Abstract-20.png +0 -0
  223. package/sources/assets/images/brutalist/Abstract-200.png +0 -0
  224. package/sources/assets/images/brutalist/Abstract-201.png +0 -0
  225. package/sources/assets/images/brutalist/Abstract-202.png +0 -0
  226. package/sources/assets/images/brutalist/Abstract-203.png +0 -0
  227. package/sources/assets/images/brutalist/Abstract-204.png +0 -0
  228. package/sources/assets/images/brutalist/Abstract-205.png +0 -0
  229. package/sources/assets/images/brutalist/Abstract-206.png +0 -0
  230. package/sources/assets/images/brutalist/Abstract-207.png +0 -0
  231. package/sources/assets/images/brutalist/Abstract-208.png +0 -0
  232. package/sources/assets/images/brutalist/Abstract-209.png +0 -0
  233. package/sources/assets/images/brutalist/Abstract-21.png +0 -0
  234. package/sources/assets/images/brutalist/Abstract-210.png +0 -0
  235. package/sources/assets/images/brutalist/Abstract-211.png +0 -0
  236. package/sources/assets/images/brutalist/Abstract-212.png +0 -0
  237. package/sources/assets/images/brutalist/Abstract-213.png +0 -0
  238. package/sources/assets/images/brutalist/Abstract-214.png +0 -0
  239. package/sources/assets/images/brutalist/Abstract-215.png +0 -0
  240. package/sources/assets/images/brutalist/Abstract-216.png +0 -0
  241. package/sources/assets/images/brutalist/Abstract-217.png +0 -0
  242. package/sources/assets/images/brutalist/Abstract-218.png +0 -0
  243. package/sources/assets/images/brutalist/Abstract-219.png +0 -0
  244. package/sources/assets/images/brutalist/Abstract-22.png +0 -0
  245. package/sources/assets/images/brutalist/Abstract-220.png +0 -0
  246. package/sources/assets/images/brutalist/Abstract-221.png +0 -0
  247. package/sources/assets/images/brutalist/Abstract-222.png +0 -0
  248. package/sources/assets/images/brutalist/Abstract-223.png +0 -0
  249. package/sources/assets/images/brutalist/Abstract-224.png +0 -0
  250. package/sources/assets/images/brutalist/Abstract-225.png +0 -0
  251. package/sources/assets/images/brutalist/Abstract-226.png +0 -0
  252. package/sources/assets/images/brutalist/Abstract-227.png +0 -0
  253. package/sources/assets/images/brutalist/Abstract-228.png +0 -0
  254. package/sources/assets/images/brutalist/Abstract-229.png +0 -0
  255. package/sources/assets/images/brutalist/Abstract-23.png +0 -0
  256. package/sources/assets/images/brutalist/Abstract-230.png +0 -0
  257. package/sources/assets/images/brutalist/Abstract-231.png +0 -0
  258. package/sources/assets/images/brutalist/Abstract-232.png +0 -0
  259. package/sources/assets/images/brutalist/Abstract-233.png +0 -0
  260. package/sources/assets/images/brutalist/Abstract-234.png +0 -0
  261. package/sources/assets/images/brutalist/Abstract-235.png +0 -0
  262. package/sources/assets/images/brutalist/Abstract-236.png +0 -0
  263. package/sources/assets/images/brutalist/Abstract-237.png +0 -0
  264. package/sources/assets/images/brutalist/Abstract-238.png +0 -0
  265. package/sources/assets/images/brutalist/Abstract-239.png +0 -0
  266. package/sources/assets/images/brutalist/Abstract-24.png +0 -0
  267. package/sources/assets/images/brutalist/Abstract-240.png +0 -0
  268. package/sources/assets/images/brutalist/Abstract-241.png +0 -0
  269. package/sources/assets/images/brutalist/Abstract-242.png +0 -0
  270. package/sources/assets/images/brutalist/Abstract-243.png +0 -0
  271. package/sources/assets/images/brutalist/Abstract-244.png +0 -0
  272. package/sources/assets/images/brutalist/Abstract-245.png +0 -0
  273. package/sources/assets/images/brutalist/Abstract-246.png +0 -0
  274. package/sources/assets/images/brutalist/Abstract-247.png +0 -0
  275. package/sources/assets/images/brutalist/Abstract-248.png +0 -0
  276. package/sources/assets/images/brutalist/Abstract-249.png +0 -0
  277. package/sources/assets/images/brutalist/Abstract-25.png +0 -0
  278. package/sources/assets/images/brutalist/Abstract-250.png +0 -0
  279. package/sources/assets/images/brutalist/Abstract-251.png +0 -0
  280. package/sources/assets/images/brutalist/Abstract-252.png +0 -0
  281. package/sources/assets/images/brutalist/Abstract-253.png +0 -0
  282. package/sources/assets/images/brutalist/Abstract-254.png +0 -0
  283. package/sources/assets/images/brutalist/Abstract-255.png +0 -0
  284. package/sources/assets/images/brutalist/Abstract-256.png +0 -0
  285. package/sources/assets/images/brutalist/Abstract-257.png +0 -0
  286. package/sources/assets/images/brutalist/Abstract-258.png +0 -0
  287. package/sources/assets/images/brutalist/Abstract-259.png +0 -0
  288. package/sources/assets/images/brutalist/Abstract-26.png +0 -0
  289. package/sources/assets/images/brutalist/Abstract-260.png +0 -0
  290. package/sources/assets/images/brutalist/Abstract-261.png +0 -0
  291. package/sources/assets/images/brutalist/Abstract-262.png +0 -0
  292. package/sources/assets/images/brutalist/Abstract-27.png +0 -0
  293. package/sources/assets/images/brutalist/Abstract-28.png +0 -0
  294. package/sources/assets/images/brutalist/Abstract-29.png +0 -0
  295. package/sources/assets/images/brutalist/Abstract-3.png +0 -0
  296. package/sources/assets/images/brutalist/Abstract-30.png +0 -0
  297. package/sources/assets/images/brutalist/Abstract-31.png +0 -0
  298. package/sources/assets/images/brutalist/Abstract-32.png +0 -0
  299. package/sources/assets/images/brutalist/Abstract-33.png +0 -0
  300. package/sources/assets/images/brutalist/Abstract-34.png +0 -0
  301. package/sources/assets/images/brutalist/Abstract-35.png +0 -0
  302. package/sources/assets/images/brutalist/Abstract-36.png +0 -0
  303. package/sources/assets/images/brutalist/Abstract-37.png +0 -0
  304. package/sources/assets/images/brutalist/Abstract-38.png +0 -0
  305. package/sources/assets/images/brutalist/Abstract-39.png +0 -0
  306. package/sources/assets/images/brutalist/Abstract-4.png +0 -0
  307. package/sources/assets/images/brutalist/Abstract-40.png +0 -0
  308. package/sources/assets/images/brutalist/Abstract-41.png +0 -0
  309. package/sources/assets/images/brutalist/Abstract-42.png +0 -0
  310. package/sources/assets/images/brutalist/Abstract-43.png +0 -0
  311. package/sources/assets/images/brutalist/Abstract-44.png +0 -0
  312. package/sources/assets/images/brutalist/Abstract-45.png +0 -0
  313. package/sources/assets/images/brutalist/Abstract-46.png +0 -0
  314. package/sources/assets/images/brutalist/Abstract-47.png +0 -0
  315. package/sources/assets/images/brutalist/Abstract-48.png +0 -0
  316. package/sources/assets/images/brutalist/Abstract-49.png +0 -0
  317. package/sources/assets/images/brutalist/Abstract-5.png +0 -0
  318. package/sources/assets/images/brutalist/Abstract-50.png +0 -0
  319. package/sources/assets/images/brutalist/Abstract-51.png +0 -0
  320. package/sources/assets/images/brutalist/Abstract-52.png +0 -0
  321. package/sources/assets/images/brutalist/Abstract-53.png +0 -0
  322. package/sources/assets/images/brutalist/Abstract-54.png +0 -0
  323. package/sources/assets/images/brutalist/Abstract-55.png +0 -0
  324. package/sources/assets/images/brutalist/Abstract-56.png +0 -0
  325. package/sources/assets/images/brutalist/Abstract-57.png +0 -0
  326. package/sources/assets/images/brutalist/Abstract-58.png +0 -0
  327. package/sources/assets/images/brutalist/Abstract-59.png +0 -0
  328. package/sources/assets/images/brutalist/Abstract-6.png +0 -0
  329. package/sources/assets/images/brutalist/Abstract-60.png +0 -0
  330. package/sources/assets/images/brutalist/Abstract-61.png +0 -0
  331. package/sources/assets/images/brutalist/Abstract-62.png +0 -0
  332. package/sources/assets/images/brutalist/Abstract-63.png +0 -0
  333. package/sources/assets/images/brutalist/Abstract-64.png +0 -0
  334. package/sources/assets/images/brutalist/Abstract-65.png +0 -0
  335. package/sources/assets/images/brutalist/Abstract-66.png +0 -0
  336. package/sources/assets/images/brutalist/Abstract-67.png +0 -0
  337. package/sources/assets/images/brutalist/Abstract-68.png +0 -0
  338. package/sources/assets/images/brutalist/Abstract-69.png +0 -0
  339. package/sources/assets/images/brutalist/Abstract-7.png +0 -0
  340. package/sources/assets/images/brutalist/Abstract-70.png +0 -0
  341. package/sources/assets/images/brutalist/Abstract-71.png +0 -0
  342. package/sources/assets/images/brutalist/Abstract-72.png +0 -0
  343. package/sources/assets/images/brutalist/Abstract-73.png +0 -0
  344. package/sources/assets/images/brutalist/Abstract-74.png +0 -0
  345. package/sources/assets/images/brutalist/Abstract-75.png +0 -0
  346. package/sources/assets/images/brutalist/Abstract-76.png +0 -0
  347. package/sources/assets/images/brutalist/Abstract-77.png +0 -0
  348. package/sources/assets/images/brutalist/Abstract-78.png +0 -0
  349. package/sources/assets/images/brutalist/Abstract-79.png +0 -0
  350. package/sources/assets/images/brutalist/Abstract-8.png +0 -0
  351. package/sources/assets/images/brutalist/Abstract-80.png +0 -0
  352. package/sources/assets/images/brutalist/Abstract-81.png +0 -0
  353. package/sources/assets/images/brutalist/Abstract-82.png +0 -0
  354. package/sources/assets/images/brutalist/Abstract-83.png +0 -0
  355. package/sources/assets/images/brutalist/Abstract-84.png +0 -0
  356. package/sources/assets/images/brutalist/Abstract-85.png +0 -0
  357. package/sources/assets/images/brutalist/Abstract-86.png +0 -0
  358. package/sources/assets/images/brutalist/Abstract-87.png +0 -0
  359. package/sources/assets/images/brutalist/Abstract-88.png +0 -0
  360. package/sources/assets/images/brutalist/Abstract-89.png +0 -0
  361. package/sources/assets/images/brutalist/Abstract-9.png +0 -0
  362. package/sources/assets/images/brutalist/Abstract-90.png +0 -0
  363. package/sources/assets/images/brutalist/Abstract-91.png +0 -0
  364. package/sources/assets/images/brutalist/Abstract-92.png +0 -0
  365. package/sources/assets/images/brutalist/Abstract-93.png +0 -0
  366. package/sources/assets/images/brutalist/Abstract-94.png +0 -0
  367. package/sources/assets/images/brutalist/Abstract-95.png +0 -0
  368. package/sources/assets/images/brutalist/Abstract-96.png +0 -0
  369. package/sources/assets/images/brutalist/Abstract-97.png +0 -0
  370. package/sources/assets/images/brutalist/Abstract-98.png +0 -0
  371. package/sources/assets/images/brutalist/Abstract-99.png +0 -0
  372. package/sources/assets/images/brutalist/Bauhaus-1.png +0 -0
  373. package/sources/assets/images/brutalist/Bauhaus-10.png +0 -0
  374. package/sources/assets/images/brutalist/Bauhaus-11.png +0 -0
  375. package/sources/assets/images/brutalist/Bauhaus-12.png +0 -0
  376. package/sources/assets/images/brutalist/Bauhaus-13.png +0 -0
  377. package/sources/assets/images/brutalist/Bauhaus-14.png +0 -0
  378. package/sources/assets/images/brutalist/Bauhaus-15.png +0 -0
  379. package/sources/assets/images/brutalist/Bauhaus-16.png +0 -0
  380. package/sources/assets/images/brutalist/Bauhaus-17.png +0 -0
  381. package/sources/assets/images/brutalist/Bauhaus-18.png +0 -0
  382. package/sources/assets/images/brutalist/Bauhaus-19.png +0 -0
  383. package/sources/assets/images/brutalist/Bauhaus-2.png +0 -0
  384. package/sources/assets/images/brutalist/Bauhaus-20.png +0 -0
  385. package/sources/assets/images/brutalist/Bauhaus-21.png +0 -0
  386. package/sources/assets/images/brutalist/Bauhaus-22.png +0 -0
  387. package/sources/assets/images/brutalist/Bauhaus-23.png +0 -0
  388. package/sources/assets/images/brutalist/Bauhaus-24.png +0 -0
  389. package/sources/assets/images/brutalist/Bauhaus-25.png +0 -0
  390. package/sources/assets/images/brutalist/Bauhaus-26.png +0 -0
  391. package/sources/assets/images/brutalist/Bauhaus-27.png +0 -0
  392. package/sources/assets/images/brutalist/Bauhaus-28.png +0 -0
  393. package/sources/assets/images/brutalist/Bauhaus-29.png +0 -0
  394. package/sources/assets/images/brutalist/Bauhaus-3.png +0 -0
  395. package/sources/assets/images/brutalist/Bauhaus-30.png +0 -0
  396. package/sources/assets/images/brutalist/Bauhaus-31.png +0 -0
  397. package/sources/assets/images/brutalist/Bauhaus-32.png +0 -0
  398. package/sources/assets/images/brutalist/Bauhaus-33.png +0 -0
  399. package/sources/assets/images/brutalist/Bauhaus-34.png +0 -0
  400. package/sources/assets/images/brutalist/Bauhaus-35.png +0 -0
  401. package/sources/assets/images/brutalist/Bauhaus-36.png +0 -0
  402. package/sources/assets/images/brutalist/Bauhaus-37.png +0 -0
  403. package/sources/assets/images/brutalist/Bauhaus-38.png +0 -0
  404. package/sources/assets/images/brutalist/Bauhaus-39.png +0 -0
  405. package/sources/assets/images/brutalist/Bauhaus-4.png +0 -0
  406. package/sources/assets/images/brutalist/Bauhaus-40.png +0 -0
  407. package/sources/assets/images/brutalist/Bauhaus-5.png +0 -0
  408. package/sources/assets/images/brutalist/Bauhaus-6.png +0 -0
  409. package/sources/assets/images/brutalist/Bauhaus-7.png +0 -0
  410. package/sources/assets/images/brutalist/Bauhaus-8.png +0 -0
  411. package/sources/assets/images/brutalist/Bauhaus-9.png +0 -0
  412. package/sources/assets/images/brutalist/Brutalism-1.png +0 -0
  413. package/sources/assets/images/brutalist/Brutalism-10.png +0 -0
  414. package/sources/assets/images/brutalist/Brutalism-100.png +0 -0
  415. package/sources/assets/images/brutalist/Brutalism-101.png +0 -0
  416. package/sources/assets/images/brutalist/Brutalism-102.png +0 -0
  417. package/sources/assets/images/brutalist/Brutalism-103.png +0 -0
  418. package/sources/assets/images/brutalist/Brutalism-104.png +0 -0
  419. package/sources/assets/images/brutalist/Brutalism-105.png +0 -0
  420. package/sources/assets/images/brutalist/Brutalism-106.png +0 -0
  421. package/sources/assets/images/brutalist/Brutalism-107.png +0 -0
  422. package/sources/assets/images/brutalist/Brutalism-108.png +0 -0
  423. package/sources/assets/images/brutalist/Brutalism-109.png +0 -0
  424. package/sources/assets/images/brutalist/Brutalism-11.png +0 -0
  425. package/sources/assets/images/brutalist/Brutalism-110.png +0 -0
  426. package/sources/assets/images/brutalist/Brutalism-111.png +0 -0
  427. package/sources/assets/images/brutalist/Brutalism-112.png +0 -0
  428. package/sources/assets/images/brutalist/Brutalism-113.png +0 -0
  429. package/sources/assets/images/brutalist/Brutalism-114.png +0 -0
  430. package/sources/assets/images/brutalist/Brutalism-115.png +0 -0
  431. package/sources/assets/images/brutalist/Brutalism-116.png +0 -0
  432. package/sources/assets/images/brutalist/Brutalism-117.png +0 -0
  433. package/sources/assets/images/brutalist/Brutalism-118.png +0 -0
  434. package/sources/assets/images/brutalist/Brutalism-12.png +0 -0
  435. package/sources/assets/images/brutalist/Brutalism-13.png +0 -0
  436. package/sources/assets/images/brutalist/Brutalism-14.png +0 -0
  437. package/sources/assets/images/brutalist/Brutalism-15.png +0 -0
  438. package/sources/assets/images/brutalist/Brutalism-16.png +0 -0
  439. package/sources/assets/images/brutalist/Brutalism-17.png +0 -0
  440. package/sources/assets/images/brutalist/Brutalism-18.png +0 -0
  441. package/sources/assets/images/brutalist/Brutalism-19.png +0 -0
  442. package/sources/assets/images/brutalist/Brutalism-2.png +0 -0
  443. package/sources/assets/images/brutalist/Brutalism-20.png +0 -0
  444. package/sources/assets/images/brutalist/Brutalism-21.png +0 -0
  445. package/sources/assets/images/brutalist/Brutalism-22.png +0 -0
  446. package/sources/assets/images/brutalist/Brutalism-23.png +0 -0
  447. package/sources/assets/images/brutalist/Brutalism-24.png +0 -0
  448. package/sources/assets/images/brutalist/Brutalism-25.png +0 -0
  449. package/sources/assets/images/brutalist/Brutalism-26.png +0 -0
  450. package/sources/assets/images/brutalist/Brutalism-27.png +0 -0
  451. package/sources/assets/images/brutalist/Brutalism-28.png +0 -0
  452. package/sources/assets/images/brutalist/Brutalism-29.png +0 -0
  453. package/sources/assets/images/brutalist/Brutalism-3.png +0 -0
  454. package/sources/assets/images/brutalist/Brutalism-30.png +0 -0
  455. package/sources/assets/images/brutalist/Brutalism-31.png +0 -0
  456. package/sources/assets/images/brutalist/Brutalism-32.png +0 -0
  457. package/sources/assets/images/brutalist/Brutalism-33.png +0 -0
  458. package/sources/assets/images/brutalist/Brutalism-34.png +0 -0
  459. package/sources/assets/images/brutalist/Brutalism-35.png +0 -0
  460. package/sources/assets/images/brutalist/Brutalism-36.png +0 -0
  461. package/sources/assets/images/brutalist/Brutalism-37.png +0 -0
  462. package/sources/assets/images/brutalist/Brutalism-38.png +0 -0
  463. package/sources/assets/images/brutalist/Brutalism-39.png +0 -0
  464. package/sources/assets/images/brutalist/Brutalism-4.png +0 -0
  465. package/sources/assets/images/brutalist/Brutalism-40.png +0 -0
  466. package/sources/assets/images/brutalist/Brutalism-41.png +0 -0
  467. package/sources/assets/images/brutalist/Brutalism-42.png +0 -0
  468. package/sources/assets/images/brutalist/Brutalism-43.png +0 -0
  469. package/sources/assets/images/brutalist/Brutalism-44.png +0 -0
  470. package/sources/assets/images/brutalist/Brutalism-45.png +0 -0
  471. package/sources/assets/images/brutalist/Brutalism-46.png +0 -0
  472. package/sources/assets/images/brutalist/Brutalism-47.png +0 -0
  473. package/sources/assets/images/brutalist/Brutalism-48.png +0 -0
  474. package/sources/assets/images/brutalist/Brutalism-49.png +0 -0
  475. package/sources/assets/images/brutalist/Brutalism-5.png +0 -0
  476. package/sources/assets/images/brutalist/Brutalism-50.png +0 -0
  477. package/sources/assets/images/brutalist/Brutalism-51.png +0 -0
  478. package/sources/assets/images/brutalist/Brutalism-52.png +0 -0
  479. package/sources/assets/images/brutalist/Brutalism-53.png +0 -0
  480. package/sources/assets/images/brutalist/Brutalism-54.png +0 -0
  481. package/sources/assets/images/brutalist/Brutalism-55.png +0 -0
  482. package/sources/assets/images/brutalist/Brutalism-56.png +0 -0
  483. package/sources/assets/images/brutalist/Brutalism-57.png +0 -0
  484. package/sources/assets/images/brutalist/Brutalism-58.png +0 -0
  485. package/sources/assets/images/brutalist/Brutalism-59.png +0 -0
  486. package/sources/assets/images/brutalist/Brutalism-6.png +0 -0
  487. package/sources/assets/images/brutalist/Brutalism-60.png +0 -0
  488. package/sources/assets/images/brutalist/Brutalism-61.png +0 -0
  489. package/sources/assets/images/brutalist/Brutalism-62.png +0 -0
  490. package/sources/assets/images/brutalist/Brutalism-63.png +0 -0
  491. package/sources/assets/images/brutalist/Brutalism-64.png +0 -0
  492. package/sources/assets/images/brutalist/Brutalism-65.png +0 -0
  493. package/sources/assets/images/brutalist/Brutalism-66.png +0 -0
  494. package/sources/assets/images/brutalist/Brutalism-67.png +0 -0
  495. package/sources/assets/images/brutalist/Brutalism-68.png +0 -0
  496. package/sources/assets/images/brutalist/Brutalism-69.png +0 -0
  497. package/sources/assets/images/brutalist/Brutalism-7.png +0 -0
  498. package/sources/assets/images/brutalist/Brutalism-70.png +0 -0
  499. package/sources/assets/images/brutalist/Brutalism-71.png +0 -0
  500. package/sources/assets/images/brutalist/Brutalism-72.png +0 -0
  501. package/sources/assets/images/brutalist/Brutalism-73.png +0 -0
  502. package/sources/assets/images/brutalist/Brutalism-74.png +0 -0
  503. package/sources/assets/images/brutalist/Brutalism-75.png +0 -0
  504. package/sources/assets/images/brutalist/Brutalism-76.png +0 -0
  505. package/sources/assets/images/brutalist/Brutalism-77.png +0 -0
  506. package/sources/assets/images/brutalist/Brutalism-78.png +0 -0
  507. package/sources/assets/images/brutalist/Brutalism-79.png +0 -0
  508. package/sources/assets/images/brutalist/Brutalism-8.png +0 -0
  509. package/sources/assets/images/brutalist/Brutalism-80.png +0 -0
  510. package/sources/assets/images/brutalist/Brutalism-81.png +0 -0
  511. package/sources/assets/images/brutalist/Brutalism-82.png +0 -0
  512. package/sources/assets/images/brutalist/Brutalism-83.png +0 -0
  513. package/sources/assets/images/brutalist/Brutalism-84.png +0 -0
  514. package/sources/assets/images/brutalist/Brutalism-85.png +0 -0
  515. package/sources/assets/images/brutalist/Brutalism-86.png +0 -0
  516. package/sources/assets/images/brutalist/Brutalism-87.png +0 -0
  517. package/sources/assets/images/brutalist/Brutalism-88.png +0 -0
  518. package/sources/assets/images/brutalist/Brutalism-89.png +0 -0
  519. package/sources/assets/images/brutalist/Brutalism-9.png +0 -0
  520. package/sources/assets/images/brutalist/Brutalism-90.png +0 -0
  521. package/sources/assets/images/brutalist/Brutalism-91.png +0 -0
  522. package/sources/assets/images/brutalist/Brutalism-92.png +0 -0
  523. package/sources/assets/images/brutalist/Brutalism-93.png +0 -0
  524. package/sources/assets/images/brutalist/Brutalism-94.png +0 -0
  525. package/sources/assets/images/brutalist/Brutalism-95.png +0 -0
  526. package/sources/assets/images/brutalist/Brutalism-96.png +0 -0
  527. package/sources/assets/images/brutalist/Brutalism-97.png +0 -0
  528. package/sources/assets/images/brutalist/Brutalism-98.png +0 -0
  529. package/sources/assets/images/brutalist/Brutalism-99.png +0 -0
  530. package/sources/assets/images/favicon-active.png +0 -0
  531. package/sources/assets/images/favicon.png +0 -0
  532. package/sources/assets/images/gradients/01.png +0 -0
  533. package/sources/assets/images/gradients/02.png +0 -0
  534. package/sources/assets/images/gradients/03.png +0 -0
  535. package/sources/assets/images/gradients/04.png +0 -0
  536. package/sources/assets/images/gradients/05.png +0 -0
  537. package/sources/assets/images/gradients/06.png +0 -0
  538. package/sources/assets/images/gradients/07.png +0 -0
  539. package/sources/assets/images/gradients/08.png +0 -0
  540. package/sources/assets/images/gradients/09.png +0 -0
  541. package/sources/assets/images/gradients/10.png +0 -0
  542. package/sources/assets/images/gradients/100.png +0 -0
  543. package/sources/assets/images/gradients/11.png +0 -0
  544. package/sources/assets/images/gradients/12.png +0 -0
  545. package/sources/assets/images/gradients/13.png +0 -0
  546. package/sources/assets/images/gradients/14.png +0 -0
  547. package/sources/assets/images/gradients/15.png +0 -0
  548. package/sources/assets/images/gradients/16.png +0 -0
  549. package/sources/assets/images/gradients/17.png +0 -0
  550. package/sources/assets/images/gradients/18.png +0 -0
  551. package/sources/assets/images/gradients/19.png +0 -0
  552. package/sources/assets/images/gradients/20.png +0 -0
  553. package/sources/assets/images/gradients/21.png +0 -0
  554. package/sources/assets/images/gradients/22.png +0 -0
  555. package/sources/assets/images/gradients/23.png +0 -0
  556. package/sources/assets/images/gradients/24.png +0 -0
  557. package/sources/assets/images/gradients/25.png +0 -0
  558. package/sources/assets/images/gradients/26.png +0 -0
  559. package/sources/assets/images/gradients/27.png +0 -0
  560. package/sources/assets/images/gradients/28.png +0 -0
  561. package/sources/assets/images/gradients/29.png +0 -0
  562. package/sources/assets/images/gradients/30.png +0 -0
  563. package/sources/assets/images/gradients/31.png +0 -0
  564. package/sources/assets/images/gradients/32.png +0 -0
  565. package/sources/assets/images/gradients/33.png +0 -0
  566. package/sources/assets/images/gradients/34.png +0 -0
  567. package/sources/assets/images/gradients/35.png +0 -0
  568. package/sources/assets/images/gradients/36.png +0 -0
  569. package/sources/assets/images/gradients/37.png +0 -0
  570. package/sources/assets/images/gradients/38.png +0 -0
  571. package/sources/assets/images/gradients/39.png +0 -0
  572. package/sources/assets/images/gradients/40.png +0 -0
  573. package/sources/assets/images/gradients/41.png +0 -0
  574. package/sources/assets/images/gradients/42.png +0 -0
  575. package/sources/assets/images/gradients/43.png +0 -0
  576. package/sources/assets/images/gradients/44.png +0 -0
  577. package/sources/assets/images/gradients/45.png +0 -0
  578. package/sources/assets/images/gradients/46.png +0 -0
  579. package/sources/assets/images/gradients/47.png +0 -0
  580. package/sources/assets/images/gradients/48.png +0 -0
  581. package/sources/assets/images/gradients/49.png +0 -0
  582. package/sources/assets/images/gradients/50.png +0 -0
  583. package/sources/assets/images/gradients/51.png +0 -0
  584. package/sources/assets/images/gradients/52.png +0 -0
  585. package/sources/assets/images/gradients/53.png +0 -0
  586. package/sources/assets/images/gradients/54.png +0 -0
  587. package/sources/assets/images/gradients/55.png +0 -0
  588. package/sources/assets/images/gradients/56.png +0 -0
  589. package/sources/assets/images/gradients/57.png +0 -0
  590. package/sources/assets/images/gradients/58.png +0 -0
  591. package/sources/assets/images/gradients/59.png +0 -0
  592. package/sources/assets/images/gradients/60.png +0 -0
  593. package/sources/assets/images/gradients/61.png +0 -0
  594. package/sources/assets/images/gradients/62.png +0 -0
  595. package/sources/assets/images/gradients/63.png +0 -0
  596. package/sources/assets/images/gradients/64.png +0 -0
  597. package/sources/assets/images/gradients/65.png +0 -0
  598. package/sources/assets/images/gradients/66.png +0 -0
  599. package/sources/assets/images/gradients/67.png +0 -0
  600. package/sources/assets/images/gradients/68.png +0 -0
  601. package/sources/assets/images/gradients/69.png +0 -0
  602. package/sources/assets/images/gradients/70.png +0 -0
  603. package/sources/assets/images/gradients/71.png +0 -0
  604. package/sources/assets/images/gradients/72.png +0 -0
  605. package/sources/assets/images/gradients/73.png +0 -0
  606. package/sources/assets/images/gradients/74.png +0 -0
  607. package/sources/assets/images/gradients/75.png +0 -0
  608. package/sources/assets/images/gradients/76.png +0 -0
  609. package/sources/assets/images/gradients/77.png +0 -0
  610. package/sources/assets/images/gradients/78.png +0 -0
  611. package/sources/assets/images/gradients/79.png +0 -0
  612. package/sources/assets/images/gradients/80.png +0 -0
  613. package/sources/assets/images/gradients/81.png +0 -0
  614. package/sources/assets/images/gradients/82.png +0 -0
  615. package/sources/assets/images/gradients/83.png +0 -0
  616. package/sources/assets/images/gradients/84.png +0 -0
  617. package/sources/assets/images/gradients/85.png +0 -0
  618. package/sources/assets/images/gradients/86.png +0 -0
  619. package/sources/assets/images/gradients/87.png +0 -0
  620. package/sources/assets/images/gradients/88.png +0 -0
  621. package/sources/assets/images/gradients/89.png +0 -0
  622. package/sources/assets/images/gradients/90.png +0 -0
  623. package/sources/assets/images/gradients/91.png +0 -0
  624. package/sources/assets/images/gradients/92.png +0 -0
  625. package/sources/assets/images/gradients/93.png +0 -0
  626. package/sources/assets/images/gradients/94.png +0 -0
  627. package/sources/assets/images/gradients/95.png +0 -0
  628. package/sources/assets/images/gradients/96.png +0 -0
  629. package/sources/assets/images/gradients/97.png +0 -0
  630. package/sources/assets/images/gradients/98.png +0 -0
  631. package/sources/assets/images/gradients/99.png +0 -0
  632. package/sources/assets/images/icon-adaptive.png +0 -0
  633. package/sources/assets/images/icon-claude.png +0 -0
  634. package/sources/assets/images/icon-claude@2x.png +0 -0
  635. package/sources/assets/images/icon-claude@3x.png +0 -0
  636. package/sources/assets/images/icon-gemini.png +0 -0
  637. package/sources/assets/images/icon-gemini@2x.png +0 -0
  638. package/sources/assets/images/icon-gemini@3x.png +0 -0
  639. package/sources/assets/images/icon-gpt.png +0 -0
  640. package/sources/assets/images/icon-gpt@2x.png +0 -0
  641. package/sources/assets/images/icon-gpt@3x.png +0 -0
  642. package/sources/assets/images/icon-monochrome.png +0 -0
  643. package/sources/assets/images/icon-notification.png +0 -0
  644. package/sources/assets/images/icon-openclaw.png +0 -0
  645. package/sources/assets/images/icon-openclaw@2x.png +0 -0
  646. package/sources/assets/images/icon-openclaw@3x.png +0 -0
  647. package/sources/assets/images/icon-tauri.png +0 -0
  648. package/sources/assets/images/icon-voice-white.png +0 -0
  649. package/sources/assets/images/icon-voice.png +0 -0
  650. package/sources/assets/images/icon-voice@2x.png +0 -0
  651. package/sources/assets/images/icon-voice@3x.png +0 -0
  652. package/sources/assets/images/icon.png +0 -0
  653. package/sources/assets/images/logo-black.png +0 -0
  654. package/sources/assets/images/logo-white.png +0 -0
  655. package/sources/assets/images/logotype-dark.png +0 -0
  656. package/sources/assets/images/logotype-dark@2x.png +0 -0
  657. package/sources/assets/images/logotype-dark@3x.png +0 -0
  658. package/sources/assets/images/logotype-light.png +0 -0
  659. package/sources/assets/images/logotype-light@2x.png +0 -0
  660. package/sources/assets/images/logotype-light@3x.png +0 -0
  661. package/sources/assets/images/logotype.png +0 -0
  662. package/sources/assets/images/logotype@2x.png +0 -0
  663. package/sources/assets/images/logotype@3x.png +0 -0
  664. package/sources/assets/images/splash-android-dark.png +0 -0
  665. package/sources/assets/images/splash-android-light.png +0 -0
  666. package/sources/assets/images/transparent.png +0 -0
  667. package/sources/assets/images/zen-icon.png +0 -0
  668. package/sources/auth/AuthContext.tsx +100 -0
  669. package/sources/auth/authAccountApprove.ts +2 -0
  670. package/sources/auth/authApprove.ts +2 -0
  671. package/sources/auth/authChallenge.ts +8 -0
  672. package/sources/auth/authGetToken.ts +4 -0
  673. package/sources/auth/authQRStart.ts +17 -0
  674. package/sources/auth/authQRWait.ts +13 -0
  675. package/sources/auth/secretKeyBackup.spec.ts +465 -0
  676. package/sources/auth/secretKeyBackup.ts +179 -0
  677. package/sources/auth/tokenStorage.ts +27 -0
  678. package/sources/changelog/changelog.json +60 -0
  679. package/sources/changelog/index.ts +3 -0
  680. package/sources/changelog/parser.ts +23 -0
  681. package/sources/changelog/storage.ts +17 -0
  682. package/sources/changelog/types.ts +10 -0
  683. package/sources/components/ActiveSessionsGroupCompact.tsx +567 -0
  684. package/sources/components/AgentContentView.ios.tsx +70 -0
  685. package/sources/components/AgentContentView.tsx +48 -0
  686. package/sources/components/AgentInput.tsx +1468 -0
  687. package/sources/components/AgentInputAttachmentStrip.tsx +122 -0
  688. package/sources/components/AgentInputAutocomplete.tsx +96 -0
  689. package/sources/components/AgentInputSuggestionView.tsx +106 -0
  690. package/sources/components/AllFilesDiffView.tsx +515 -0
  691. package/sources/components/Avatar.tsx +149 -0
  692. package/sources/components/AvatarBrutalist.tsx +501 -0
  693. package/sources/components/AvatarGradient.tsx +147 -0
  694. package/sources/components/AvatarSkia.tsx +111 -0
  695. package/sources/components/AvatarSkia.web.tsx +113 -0
  696. package/sources/components/ChatFooter.tsx +50 -0
  697. package/sources/components/ChatHeaderView.tsx +180 -0
  698. package/sources/components/ChatList.tsx +283 -0
  699. package/sources/components/CodeEditor.tsx +22 -0
  700. package/sources/components/CodeEditor.web.tsx +180 -0
  701. package/sources/components/CodeView.tsx +33 -0
  702. package/sources/components/CommandPalette/CommandPalette.tsx +72 -0
  703. package/sources/components/CommandPalette/CommandPaletteInput.tsx +65 -0
  704. package/sources/components/CommandPalette/CommandPaletteItem.tsx +141 -0
  705. package/sources/components/CommandPalette/CommandPaletteModal.tsx +148 -0
  706. package/sources/components/CommandPalette/CommandPaletteProvider.tsx +141 -0
  707. package/sources/components/CommandPalette/CommandPaletteResults.tsx +129 -0
  708. package/sources/components/CommandPalette/index.ts +3 -0
  709. package/sources/components/CommandPalette/types.ts +15 -0
  710. package/sources/components/CommandPalette/useCommandPalette.ts +107 -0
  711. package/sources/components/CommandView.tsx +135 -0
  712. package/sources/components/CompactGitStatus.tsx +88 -0
  713. package/sources/components/ConnectButton.tsx +117 -0
  714. package/sources/components/Deferred.tsx +18 -0
  715. package/sources/components/DuplicateSheet.tsx +295 -0
  716. package/sources/components/EmptyMainScreen.tsx +171 -0
  717. package/sources/components/EmptyMessages.tsx +123 -0
  718. package/sources/components/EmptySessionsTablet.tsx +111 -0
  719. package/sources/components/ExternalLink.tsx +22 -0
  720. package/sources/components/FAB.tsx +53 -0
  721. package/sources/components/FABWide.tsx +59 -0
  722. package/sources/components/FeedItemCard.tsx +98 -0
  723. package/sources/components/FileIcon.tsx +63 -0
  724. package/sources/components/FileViewPanel.tsx +673 -0
  725. package/sources/components/FilesSidebar.tsx +739 -0
  726. package/sources/components/FloatingOverlay.tsx +48 -0
  727. package/sources/components/GitStatusBadge.tsx +82 -0
  728. package/sources/components/HeaderLogo.tsx +28 -0
  729. package/sources/components/HomeHeader.tsx +243 -0
  730. package/sources/components/HorizontalScrollView.tsx +88 -0
  731. package/sources/components/InboxView.tsx +260 -0
  732. package/sources/components/InlineFileDiff.tsx +277 -0
  733. package/sources/components/Item.tsx +315 -0
  734. package/sources/components/ItemGroup.tsx +147 -0
  735. package/sources/components/ItemList.tsx +102 -0
  736. package/sources/components/MainView.tsx +324 -0
  737. package/sources/components/MessageView.tsx +299 -0
  738. package/sources/components/MultiTextInput.tsx +285 -0
  739. package/sources/components/MultiTextInput.web.tsx +220 -0
  740. package/sources/components/OAuthView.tsx +374 -0
  741. package/sources/components/PermissionModeSelector.tsx +68 -0
  742. package/sources/components/PlaceholderContainerView.tsx +47 -0
  743. package/sources/components/PlusPlus.tsx +34 -0
  744. package/sources/components/PlusPlus.web.tsx +34 -0
  745. package/sources/components/ProjectGitStatus.tsx +102 -0
  746. package/sources/components/RoundButton.tsx +130 -0
  747. package/sources/components/SearchableListSelector.tsx +675 -0
  748. package/sources/components/SessionActionsNativeMenu.android.tsx +58 -0
  749. package/sources/components/SessionActionsNativeMenu.ios.tsx +55 -0
  750. package/sources/components/SessionActionsNativeMenu.tsx +20 -0
  751. package/sources/components/SessionActionsNativeMenu.web.tsx +13 -0
  752. package/sources/components/SessionActionsPopover.tsx +240 -0
  753. package/sources/components/SessionsList.tsx +471 -0
  754. package/sources/components/SessionsListWrapper.tsx +72 -0
  755. package/sources/components/SettingsView.tsx +470 -0
  756. package/sources/components/SettingsViewWrapper.tsx +21 -0
  757. package/sources/components/Shaker.tsx +42 -0
  758. package/sources/components/Shaker.web.tsx +46 -0
  759. package/sources/components/ShimmerView.tsx +106 -0
  760. package/sources/components/SidebarNavigator.tsx +218 -0
  761. package/sources/components/SidebarView.tsx +104 -0
  762. package/sources/components/SimpleSyntaxHighlighter.tsx +322 -0
  763. package/sources/components/StatusBarProvider.tsx +12 -0
  764. package/sources/components/StatusDot.tsx +49 -0
  765. package/sources/components/StyledText.tsx +35 -0
  766. package/sources/components/Switch.tsx +20 -0
  767. package/sources/components/TabBar.tsx +140 -0
  768. package/sources/components/ToolGroupView.tsx +101 -0
  769. package/sources/components/TransitionStack.tsx +14 -0
  770. package/sources/components/UpdateBanner.tsx +74 -0
  771. package/sources/components/UserCard.tsx +41 -0
  772. package/sources/components/UserSearchResult.tsx +129 -0
  773. package/sources/components/VoiceAssistantStatusBar.tsx +260 -0
  774. package/sources/components/VoiceBars.tsx +95 -0
  775. package/sources/components/autocomplete/applySuggestion.test.ts +194 -0
  776. package/sources/components/autocomplete/applySuggestion.ts +61 -0
  777. package/sources/components/autocomplete/findActiveWord.test.ts +365 -0
  778. package/sources/components/autocomplete/findActiveWord.ts +207 -0
  779. package/sources/components/autocomplete/suggestions.ts +79 -0
  780. package/sources/components/autocomplete/useActiveSuggestions.ts +130 -0
  781. package/sources/components/autocomplete/useActiveWord.ts +19 -0
  782. package/sources/components/diff/DiffView.tsx +188 -0
  783. package/sources/components/diff/PierreDiffView.tsx +253 -0
  784. package/sources/components/diff/calculateDiff.ts +317 -0
  785. package/sources/components/entityColor.ts +51 -0
  786. package/sources/components/haptics.ts +9 -0
  787. package/sources/components/haptics.web.ts +7 -0
  788. package/sources/components/layout.ts +44 -0
  789. package/sources/components/markdown/MarkdownView.tsx +670 -0
  790. package/sources/components/markdown/MermaidRenderer.tsx +233 -0
  791. package/sources/components/markdown/linkUtils.test.ts +17 -0
  792. package/sources/components/markdown/linkUtils.ts +5 -0
  793. package/sources/components/markdown/parseMarkdown.test.ts +64 -0
  794. package/sources/components/markdown/parseMarkdown.ts +46 -0
  795. package/sources/components/markdown/parseMarkdownBlock.test.ts +108 -0
  796. package/sources/components/markdown/parseMarkdownBlock.ts +200 -0
  797. package/sources/components/markdown/parseMarkdownSpans.ts +88 -0
  798. package/sources/components/modelModeOptions.test.ts +114 -0
  799. package/sources/components/modelModeOptions.ts +257 -0
  800. package/sources/components/navigation/Header.tsx +252 -0
  801. package/sources/components/parseLocalCommandMessage.spec.ts +96 -0
  802. package/sources/components/parseLocalCommandMessage.ts +97 -0
  803. package/sources/components/qr/QRCode.tsx +178 -0
  804. package/sources/components/qr/QRCode.web.tsx +229 -0
  805. package/sources/components/qr/index.ts +2 -0
  806. package/sources/components/qr/qrMatrix.ts +48 -0
  807. package/sources/components/tools/PermissionFooter.tsx +527 -0
  808. package/sources/components/tools/ToolDiffView.tsx +60 -0
  809. package/sources/components/tools/ToolError.tsx +49 -0
  810. package/sources/components/tools/ToolFullView.tsx +193 -0
  811. package/sources/components/tools/ToolHeader.tsx +93 -0
  812. package/sources/components/tools/ToolSectionView.tsx +42 -0
  813. package/sources/components/tools/ToolStatusIndicator.tsx +36 -0
  814. package/sources/components/tools/ToolView.tsx +340 -0
  815. package/sources/components/tools/knownTools.tsx +957 -0
  816. package/sources/components/tools/views/AskUserQuestionView.tsx +357 -0
  817. package/sources/components/tools/views/BashView.tsx +47 -0
  818. package/sources/components/tools/views/BashViewFull.tsx +81 -0
  819. package/sources/components/tools/views/CodexBashView.tsx +126 -0
  820. package/sources/components/tools/views/CodexDiffView.tsx +79 -0
  821. package/sources/components/tools/views/CodexPatchView.tsx +186 -0
  822. package/sources/components/tools/views/EditView.tsx +33 -0
  823. package/sources/components/tools/views/EditViewFull.tsx +38 -0
  824. package/sources/components/tools/views/ExitPlanToolView.tsx +21 -0
  825. package/sources/components/tools/views/FileView.tsx +118 -0
  826. package/sources/components/tools/views/GeminiEditView.tsx +75 -0
  827. package/sources/components/tools/views/GeminiExecuteView.tsx +92 -0
  828. package/sources/components/tools/views/MCPToolView.tsx +31 -0
  829. package/sources/components/tools/views/MultiEditView.tsx +41 -0
  830. package/sources/components/tools/views/MultiEditViewFull.tsx +84 -0
  831. package/sources/components/tools/views/TaskView.tsx +129 -0
  832. package/sources/components/tools/views/TodoView.tsx +90 -0
  833. package/sources/components/tools/views/WriteView.tsx +29 -0
  834. package/sources/components/tools/views/_all.tsx +88 -0
  835. package/sources/components/usage/UsageBar.tsx +80 -0
  836. package/sources/components/usage/UsageChart.tsx +164 -0
  837. package/sources/components/usage/UsagePanel.tsx +283 -0
  838. package/sources/components/web/FaviconPermissionIndicator.tsx +44 -0
  839. package/sources/config.ts +3 -0
  840. package/sources/constants/Languages.ts +116 -0
  841. package/sources/constants/Typography.ts +116 -0
  842. package/sources/dev/testRunner.ts +277 -0
  843. package/sources/docs/autocomplete-text-manipulation.md +224 -0
  844. package/sources/encryption/aes.appspec.ts +25 -0
  845. package/sources/encryption/aes.ts +21 -0
  846. package/sources/encryption/aes.web.test.ts +73 -0
  847. package/sources/encryption/aes.web.ts +79 -0
  848. package/sources/encryption/base64.appspec.ts +240 -0
  849. package/sources/encryption/base64.native.ts +12 -0
  850. package/sources/encryption/base64.ts +46 -0
  851. package/sources/encryption/blob.test.ts +120 -0
  852. package/sources/encryption/blob.ts +58 -0
  853. package/sources/encryption/deriveKey.appspec.ts +72 -0
  854. package/sources/encryption/deriveKey.ts +46 -0
  855. package/sources/encryption/hex.ts +17 -0
  856. package/sources/encryption/hmac_sha512.appspec.ts +40 -0
  857. package/sources/encryption/hmac_sha512.ts +42 -0
  858. package/sources/encryption/libsodium.lib.ts +2 -0
  859. package/sources/encryption/libsodium.lib.web.ts +2 -0
  860. package/sources/encryption/libsodium.ts +58 -0
  861. package/sources/encryption/text.test.ts +61 -0
  862. package/sources/encryption/text.ts +11 -0
  863. package/sources/hooks/useAsyncCommand.ts +25 -0
  864. package/sources/hooks/useAttachmentImage.ts +134 -0
  865. package/sources/hooks/useAutocomplete.ts +69 -0
  866. package/sources/hooks/useAutocompleteSession.ts +53 -0
  867. package/sources/hooks/useChangelog.ts +45 -0
  868. package/sources/hooks/useCheckCameraPermissions.ts +25 -0
  869. package/sources/hooks/useConnectAccount.ts +107 -0
  870. package/sources/hooks/useConnectTerminal.ts +112 -0
  871. package/sources/hooks/useDemoMessages.ts +48 -0
  872. package/sources/hooks/useDraft.ts +120 -0
  873. package/sources/hooks/useElapsedTime.ts +36 -0
  874. package/sources/hooks/useGetPath.ts +13 -0
  875. package/sources/hooks/useGitStatusFiles.ts +45 -0
  876. package/sources/hooks/useGlobalKeyboard.ts +29 -0
  877. package/sources/hooks/useGroupedMessages.ts +149 -0
  878. package/sources/hooks/useImagePicker.ts +135 -0
  879. package/sources/hooks/useInboxHasContent.ts +19 -0
  880. package/sources/hooks/useMultiClick.ts +56 -0
  881. package/sources/hooks/useNasTechAction.ts +45 -0
  882. package/sources/hooks/useNativeUpdate.ts +10 -0
  883. package/sources/hooks/useNavigateToSession.ts +20 -0
  884. package/sources/hooks/useNewSessionDraft.ts +70 -0
  885. package/sources/hooks/usePrefetchFileContents.ts +162 -0
  886. package/sources/hooks/useSearch.ts +120 -0
  887. package/sources/hooks/useSessionQuickActions.ts +325 -0
  888. package/sources/hooks/useTauriDrag.ts +76 -0
  889. package/sources/hooks/useTauriZoom.ts +67 -0
  890. package/sources/hooks/useUpdates.ts +84 -0
  891. package/sources/hooks/useVisibleSessionListViewData.ts +65 -0
  892. package/sources/hooks/useWorktreeCleanup.ts +69 -0
  893. package/sources/log.ts +108 -0
  894. package/sources/modal/ModalManager.ts +203 -0
  895. package/sources/modal/ModalProvider.tsx +103 -0
  896. package/sources/modal/components/BaseModal.tsx +122 -0
  897. package/sources/modal/components/CustomModal.tsx +42 -0
  898. package/sources/modal/components/WebAlertModal.tsx +139 -0
  899. package/sources/modal/components/WebPromptModal.tsx +185 -0
  900. package/sources/modal/index.ts +3 -0
  901. package/sources/modal/types.ts +79 -0
  902. package/sources/nastech-wire/index.ts +10 -0
  903. package/sources/nastech-wire/legacyProtocol.ts +27 -0
  904. package/sources/nastech-wire/messageMeta.ts +14 -0
  905. package/sources/nastech-wire/messages.ts +113 -0
  906. package/sources/nastech-wire/sessionProtocol.ts +134 -0
  907. package/sources/nastech-wire/voice.ts +34 -0
  908. package/sources/polyfills/screenOrientation.ts +33 -0
  909. package/sources/realtime/RealtimeProvider.tsx +20 -0
  910. package/sources/realtime/RealtimeProvider.web.tsx +15 -0
  911. package/sources/realtime/RealtimeSession.ts +200 -0
  912. package/sources/realtime/RealtimeVoiceSession.tsx +211 -0
  913. package/sources/realtime/RealtimeVoiceSession.web.tsx +209 -0
  914. package/sources/realtime/hooks/contextFormatters.ts +127 -0
  915. package/sources/realtime/hooks/voiceHooks.ts +232 -0
  916. package/sources/realtime/realtimeClientTools.ts +94 -0
  917. package/sources/realtime/types.ts +19 -0
  918. package/sources/realtime/voiceConfig.ts +31 -0
  919. package/sources/realtime/voiceExperiment.ts +91 -0
  920. package/sources/realtime/voiceSystemPrompt.ts +75 -0
  921. package/sources/scripts/compareTranslations.ts +217 -0
  922. package/sources/scripts/parseChangelog.ts +87 -0
  923. package/sources/sync/__testdata__/trace_0.json +3986 -0
  924. package/sources/sync/__testdata__/trace_1.json +1391 -0
  925. package/sources/sync/__testdata__/trace_2.json +182 -0
  926. package/sources/sync/agentDefaults.ts +108 -0
  927. package/sources/sync/apiArtifacts.ts +143 -0
  928. package/sources/sync/apiAttachments.ts +217 -0
  929. package/sources/sync/apiFeed.ts +60 -0
  930. package/sources/sync/apiFriends.ts +217 -0
  931. package/sources/sync/apiGithub.spec.ts +97 -0
  932. package/sources/sync/apiGithub.ts +103 -0
  933. package/sources/sync/apiKv.ts +270 -0
  934. package/sources/sync/apiPush.ts +83 -0
  935. package/sources/sync/apiServices.ts +64 -0
  936. package/sources/sync/apiSocket.ts +290 -0
  937. package/sources/sync/apiTypes.spec.ts +23 -0
  938. package/sources/sync/apiTypes.ts +213 -0
  939. package/sources/sync/apiUsage.ts +130 -0
  940. package/sources/sync/apiVoice.ts +57 -0
  941. package/sources/sync/appConfig.ts +95 -0
  942. package/sources/sync/artifactTypes.ts +85 -0
  943. package/sources/sync/attachmentTypes.ts +28 -0
  944. package/sources/sync/encryption/artifactEncryption.ts +83 -0
  945. package/sources/sync/encryption/encryption.ts +220 -0
  946. package/sources/sync/encryption/encryptionCache.ts +248 -0
  947. package/sources/sync/encryption/encryptor.appspec.ts +409 -0
  948. package/sources/sync/encryption/encryptor.ts +126 -0
  949. package/sources/sync/encryption/machineEncryption.ts +122 -0
  950. package/sources/sync/encryption/sessionEncryption.ts +207 -0
  951. package/sources/sync/feedTypes.ts +43 -0
  952. package/sources/sync/friendTypes.ts +92 -0
  953. package/sources/sync/git-parsers/LineParser.ts +62 -0
  954. package/sources/sync/git-parsers/parseBranch.ts +97 -0
  955. package/sources/sync/git-parsers/parseDiff.ts +180 -0
  956. package/sources/sync/git-parsers/parseStatus.ts +162 -0
  957. package/sources/sync/git-parsers/parseStatusV2.ts +307 -0
  958. package/sources/sync/gitStatusFiles.ts +185 -0
  959. package/sources/sync/gitStatusSync.ts +282 -0
  960. package/sources/sync/localSettings.ts +67 -0
  961. package/sources/sync/messageMeta.test.ts +87 -0
  962. package/sources/sync/messageMeta.ts +36 -0
  963. package/sources/sync/modeHacks.test.ts +29 -0
  964. package/sources/sync/modeHacks.ts +22 -0
  965. package/sources/sync/nastechApi.ts +124 -0
  966. package/sources/sync/ops.ts +776 -0
  967. package/sources/sync/persistence.ts +322 -0
  968. package/sources/sync/profile.ts +95 -0
  969. package/sources/sync/projectFiles.ts +54 -0
  970. package/sources/sync/prompt/systemPrompt.ts +20 -0
  971. package/sources/sync/purchases.ts +67 -0
  972. package/sources/sync/pushRegistration.ts +229 -0
  973. package/sources/sync/reducer/activityUpdateAccumulator.test.ts +492 -0
  974. package/sources/sync/reducer/activityUpdateAccumulator.ts +96 -0
  975. package/sources/sync/reducer/messageToEvent.ts +85 -0
  976. package/sources/sync/reducer/phase0-skipping.spec.ts +206 -0
  977. package/sources/sync/reducer/reducer.spec.ts +3169 -0
  978. package/sources/sync/reducer/reducer.ts +1214 -0
  979. package/sources/sync/reducer/reducerTracer.spec.ts +502 -0
  980. package/sources/sync/reducer/reducerTracer.ts +310 -0
  981. package/sources/sync/revenueCat/index.ts +21 -0
  982. package/sources/sync/revenueCat/revenueCat.ts +215 -0
  983. package/sources/sync/revenueCat/revenueCat.web.ts +238 -0
  984. package/sources/sync/revenueCat/types.ts +82 -0
  985. package/sources/sync/serverConfig.ts +72 -0
  986. package/sources/sync/settings.spec.ts +456 -0
  987. package/sources/sync/settings.ts +186 -0
  988. package/sources/sync/storage.ts +1653 -0
  989. package/sources/sync/storageTypes.spec.ts +24 -0
  990. package/sources/sync/storageTypes.ts +215 -0
  991. package/sources/sync/suggestionCommands.ts +149 -0
  992. package/sources/sync/suggestionFile.ts +206 -0
  993. package/sources/sync/sync.ts +2555 -0
  994. package/sources/sync/typesMessage.ts +69 -0
  995. package/sources/sync/typesMessageMeta.test.ts +14 -0
  996. package/sources/sync/typesMessageMeta.ts +17 -0
  997. package/sources/sync/typesRaw.spec.ts +2010 -0
  998. package/sources/sync/typesRaw.ts +1183 -0
  999. package/sources/sync/uploadFormFile.ts +29 -0
  1000. package/sources/sync/uploadFormFile.web.ts +14 -0
  1001. package/sources/sync/webTabTitle.ts +58 -0
  1002. package/sources/text/README.md +223 -0
  1003. package/sources/text/_all.ts +104 -0
  1004. package/sources/text/_default.ts +1015 -0
  1005. package/sources/text/index.ts +215 -0
  1006. package/sources/text/translations/ca.ts +993 -0
  1007. package/sources/text/translations/en.ts +1009 -0
  1008. package/sources/text/translations/es.ts +995 -0
  1009. package/sources/text/translations/it.ts +992 -0
  1010. package/sources/text/translations/ja.ts +993 -0
  1011. package/sources/text/translations/pl.ts +1024 -0
  1012. package/sources/text/translations/pt.ts +992 -0
  1013. package/sources/text/translations/ru.ts +1023 -0
  1014. package/sources/text/translations/zh-Hans.ts +992 -0
  1015. package/sources/text/translations/zh-Hant.ts +991 -0
  1016. package/sources/theme.css +72 -0
  1017. package/sources/theme.dark.json +31 -0
  1018. package/sources/theme.figma.json +457 -0
  1019. package/sources/theme.gen.ts +10 -0
  1020. package/sources/theme.light.json +31 -0
  1021. package/sources/theme.ts +452 -0
  1022. package/sources/track/index.ts +171 -0
  1023. package/sources/track/tracking.ts +12 -0
  1024. package/sources/track/useTrackScreens.ts +10 -0
  1025. package/sources/types/react-native-webrtc-web-shim.d.ts +6 -0
  1026. package/sources/unistyles.ts +97 -0
  1027. package/sources/utils/codexUnifiedDiff.spec.ts +43 -0
  1028. package/sources/utils/codexUnifiedDiff.ts +70 -0
  1029. package/sources/utils/consoleLogging.ts +145 -0
  1030. package/sources/utils/copySessionMetadataToClipboard.ts +53 -0
  1031. package/sources/utils/debounce.test.ts +646 -0
  1032. package/sources/utils/debounce.ts +122 -0
  1033. package/sources/utils/deviceCalculations.test.ts +318 -0
  1034. package/sources/utils/deviceCalculations.ts +87 -0
  1035. package/sources/utils/errors.ts +10 -0
  1036. package/sources/utils/formatPermissionParams.ts +23 -0
  1037. package/sources/utils/isTauri.ts +7 -0
  1038. package/sources/utils/loadSkia.ts +3 -0
  1039. package/sources/utils/loadSkia.web.ts +5 -0
  1040. package/sources/utils/lock.ts +40 -0
  1041. package/sources/utils/machineUtils.ts +6 -0
  1042. package/sources/utils/messageUtils.ts +254 -0
  1043. package/sources/utils/microphonePermissions.ts +109 -0
  1044. package/sources/utils/notificationRouting.test.ts +51 -0
  1045. package/sources/utils/notificationRouting.ts +81 -0
  1046. package/sources/utils/oauth.ts +143 -0
  1047. package/sources/utils/openExternalUrl.ts +19 -0
  1048. package/sources/utils/parseToken.ts +23 -0
  1049. package/sources/utils/pasteImages.web.ts +81 -0
  1050. package/sources/utils/pathUtils.spec.ts +226 -0
  1051. package/sources/utils/pathUtils.ts +75 -0
  1052. package/sources/utils/platform.ts +19 -0
  1053. package/sources/utils/readFileBytes.ts +11 -0
  1054. package/sources/utils/readFileBytes.web.ts +12 -0
  1055. package/sources/utils/requestReview.ts +135 -0
  1056. package/sources/utils/responsive.ts +87 -0
  1057. package/sources/utils/resumeCommand.test.ts +78 -0
  1058. package/sources/utils/resumeCommand.ts +70 -0
  1059. package/sources/utils/sessionFileLinks.test.ts +112 -0
  1060. package/sources/utils/sessionFileLinks.ts +388 -0
  1061. package/sources/utils/sessionUtils.ts +226 -0
  1062. package/sources/utils/stringUtils.ts +41 -0
  1063. package/sources/utils/sync.ts +164 -0
  1064. package/sources/utils/thumbhash.ts +17 -0
  1065. package/sources/utils/thumbhash.web.ts +85 -0
  1066. package/sources/utils/time.ts +41 -0
  1067. package/sources/utils/toSnakeCase.test.ts +182 -0
  1068. package/sources/utils/toSnakeCase.ts +40 -0
  1069. package/sources/utils/toolCommand.test.ts +23 -0
  1070. package/sources/utils/toolCommand.ts +35 -0
  1071. package/sources/utils/toolComparison.test.ts +101 -0
  1072. package/sources/utils/toolComparison.ts +73 -0
  1073. package/sources/utils/toolErrorParser.test.ts +126 -0
  1074. package/sources/utils/toolErrorParser.ts +102 -0
  1075. package/sources/utils/trimIdent.ts +27 -0
  1076. package/sources/utils/truncateForLogs.ts +37 -0
  1077. package/sources/utils/versionUtils.test.ts +58 -0
  1078. package/sources/utils/versionUtils.ts +69 -0
  1079. package/sources/utils/web/faviconGenerator.ts +39 -0
  1080. package/sources/utils/worktree.ts +192 -0
  1081. package/sources/wire/index.ts +97 -0
  1082. package/src-tauri/Cargo.lock +5978 -0
  1083. package/src-tauri/Cargo.toml +27 -0
  1084. package/src-tauri/build.rs +3 -0
  1085. package/src-tauri/capabilities/default.json +27 -0
  1086. package/src-tauri/deny.toml +53 -0
  1087. package/src-tauri/entitlements.plist +24 -0
  1088. package/src-tauri/icons/128x128.png +0 -0
  1089. package/src-tauri/icons/128x128@2x.png +0 -0
  1090. package/src-tauri/icons/32x32.png +0 -0
  1091. package/src-tauri/icons/64x64.png +0 -0
  1092. package/src-tauri/icons/Square107x107Logo.png +0 -0
  1093. package/src-tauri/icons/Square142x142Logo.png +0 -0
  1094. package/src-tauri/icons/Square150x150Logo.png +0 -0
  1095. package/src-tauri/icons/Square284x284Logo.png +0 -0
  1096. package/src-tauri/icons/Square30x30Logo.png +0 -0
  1097. package/src-tauri/icons/Square310x310Logo.png +0 -0
  1098. package/src-tauri/icons/Square44x44Logo.png +0 -0
  1099. package/src-tauri/icons/Square71x71Logo.png +0 -0
  1100. package/src-tauri/icons/Square89x89Logo.png +0 -0
  1101. package/src-tauri/icons/StoreLogo.png +0 -0
  1102. package/src-tauri/icons/icon.icns +0 -0
  1103. package/src-tauri/icons/icon.ico +0 -0
  1104. package/src-tauri/icons/icon.png +0 -0
  1105. package/src-tauri/src/lib.rs +18 -0
  1106. package/src-tauri/src/main.rs +6 -0
  1107. package/src-tauri/tauri.conf.json +51 -0
  1108. package/src-tauri/tauri.dev.conf.json +20 -0
  1109. package/src-tauri/tauri.preview.conf.json +20 -0
  1110. package/tsconfig.json +45 -0
  1111. package/vitest.config.ts +26 -0
@@ -0,0 +1,2555 @@
1
+ import Constants from 'expo-constants';
2
+ import { apiSocket, getCurrentAppState, getNasTechClientId } from '@/sync/apiSocket';
3
+ import { notifyUnreadMessage } from '@/sync/webTabTitle';
4
+ import { AuthCredentials } from '@/auth/tokenStorage';
5
+ import { Encryption } from '@/sync/encryption/encryption';
6
+ import { decodeBase64, encodeBase64 } from '@/encryption/base64';
7
+ import { storage } from './storage';
8
+ import { ApiEphemeralUpdateSchema, ApiMessage, ApiUpdateContainerSchema } from './apiTypes';
9
+ import type { ApiEphemeralActivityUpdate } from './apiTypes';
10
+ import { Session, Machine } from './storageTypes';
11
+ import { InvalidateSync } from '@/utils/sync';
12
+ import { ActivityUpdateAccumulator } from './reducer/activityUpdateAccumulator';
13
+ import { randomUUID } from 'expo-crypto';
14
+ import * as Notifications from 'expo-notifications';
15
+ import { syncCurrentPushToken } from './pushRegistration';
16
+ import { Platform, AppState, type AppStateStatus } from 'react-native';
17
+ import { isRunningOnMac } from '@/utils/platform';
18
+ import { NormalizedMessage, normalizeRawMessage, RawRecord } from './typesRaw';
19
+ import { applySettings, Settings, settingsDefaults, settingsParse, settingsToSyncPayload, SUPPORTED_SCHEMA_VERSION } from './settings';
20
+ import { Profile, profileParse } from './profile';
21
+ import { loadPendingSettings, savePendingSettings } from './persistence';
22
+ import {
23
+ initializeTracking,
24
+ trackGitHubConnected,
25
+ trackMessageSent,
26
+ tracking,
27
+ trackPaywallCancelled,
28
+ trackPaywallError,
29
+ trackPaywallPresented,
30
+ trackPaywallPurchased,
31
+ trackPaywallRestored,
32
+ } from '@/track';
33
+ import type { MessageSentSource } from '@/track';
34
+ import { parseToken } from '@/utils/parseToken';
35
+ import { RevenueCat, LogLevel, PaywallResult } from './revenueCat';
36
+ import { getServerUrl } from './serverConfig';
37
+ import { config } from '@/config';
38
+ import { log } from '@/log';
39
+ import { gitStatusSync } from './gitStatusSync';
40
+ import { AsyncLock } from '@/utils/lock';
41
+ import { voiceHooks } from '@/realtime/hooks/voiceHooks';
42
+ import { Message } from './typesMessage';
43
+ import { EncryptionCache } from './encryption/encryptionCache';
44
+ import { systemPrompt } from './prompt/systemPrompt';
45
+ import { fetchArtifact, fetchArtifacts, createArtifact, updateArtifact } from './apiArtifacts';
46
+ import { DecryptedArtifact, Artifact, ArtifactCreateRequest, ArtifactUpdateRequest } from './artifactTypes';
47
+ import { ArtifactEncryption } from './encryption/artifactEncryption';
48
+ import { getFriendsList, getUserProfile } from './apiFriends';
49
+ import { fetchFeed } from './apiFeed';
50
+ import { FeedItem } from './feedTypes';
51
+ import { UserProfile } from './friendTypes';
52
+ import { resolveMessageModeMeta } from './messageMeta';
53
+ import type { AttachmentPreview, UploadedAttachment } from './attachmentTypes';
54
+ import { requestAttachmentUpload, uploadEncryptedBlob } from './apiAttachments';
55
+ import { encryptBlob } from '@/encryption/blob';
56
+ import { readFileBytes } from '@/utils/readFileBytes';
57
+ import { Modal } from '@/modal';
58
+ import { t } from '@/text';
59
+
60
+ type V3GetSessionMessagesResponse = {
61
+ messages: ApiMessage[];
62
+ hasMore: boolean;
63
+ };
64
+
65
+ // Sentinel used as `before_seq` for the very first backward fetch of a
66
+ // session. It must exceed any real `seq` value the server can produce.
67
+ // `seq` is stored as Postgres int4 on the server, so the maximum is
68
+ // 2_147_483_647. We use that exact upper bound to keep the request safely
69
+ // within int4 while still being effectively "infinite" for any session.
70
+ const SEQ_BACKWARD_INITIAL_SENTINEL = 2_147_483_647;
71
+
72
+ type V3PostSessionMessagesResponse = {
73
+ messages: Array<{
74
+ id: string;
75
+ seq: number;
76
+ localId: string | null;
77
+ createdAt: number;
78
+ updatedAt: number;
79
+ }>;
80
+ };
81
+
82
+ type OutboxMessage = {
83
+ localId: string;
84
+ content: string;
85
+ };
86
+
87
+ type SendMessageOptions = {
88
+ displayText?: string;
89
+ source?: MessageSentSource;
90
+ /** Optional image attachments to send before the text message. */
91
+ attachments?: AttachmentPreview[];
92
+ };
93
+
94
+ class Sync {
95
+ private static readonly BACKGROUND_SEND_TIMEOUT_MS = 30_000;
96
+ encryption!: Encryption;
97
+ serverID!: string;
98
+ anonID!: string;
99
+ private credentials!: AuthCredentials;
100
+ public encryptionCache = new EncryptionCache();
101
+ private sessionsSync: InvalidateSync;
102
+ private messagesSync = new Map<string, InvalidateSync>();
103
+ private sendSync = new Map<string, InvalidateSync>();
104
+ private sendAbortControllers = new Map<string, AbortController>();
105
+ private sessionLastSeq = new Map<string, number>();
106
+ // Lowest seq value we have already fetched and applied for a session.
107
+ // Used as the cursor for backward pagination when the user scrolls up to
108
+ // load older history. Set after the initial latest-page fetch and
109
+ // advanced downward by loadOlderMessages.
110
+ private sessionOldestSeq = new Map<string, number>();
111
+ private pendingOutbox = new Map<string, OutboxMessage[]>();
112
+ private sessionMessageQueue = new Map<string, NormalizedMessage[]>();
113
+ private sessionQueueProcessing = new Set<string>();
114
+ private sessionMessageLocks = new Map<string, AsyncLock>();
115
+ private sessionDataKeys = new Map<string, Uint8Array>(); // Store session data encryption keys internally
116
+ private machineDataKeys = new Map<string, Uint8Array>(); // Store machine data encryption keys internally
117
+ private artifactDataKeys = new Map<string, Uint8Array>(); // Store artifact data encryption keys internally
118
+ private settingsSync: InvalidateSync;
119
+ private profileSync: InvalidateSync;
120
+ private purchasesSync: InvalidateSync;
121
+ private machinesSync: InvalidateSync;
122
+ private pushTokenSync: InvalidateSync;
123
+ private nativeUpdateSync: InvalidateSync;
124
+ private artifactsSync: InvalidateSync;
125
+ private friendsSync: InvalidateSync;
126
+ private friendRequestsSync: InvalidateSync;
127
+ private feedSync: InvalidateSync;
128
+ private activityAccumulator: ActivityUpdateAccumulator;
129
+ private pendingSettings: Partial<Settings> = loadPendingSettings();
130
+ private appState: AppStateStatus = AppState.currentState;
131
+ private backgroundSendTimeout: ReturnType<typeof setTimeout> | null = null;
132
+ private backgroundSendNotificationId: string | null = null;
133
+ private backgroundSendStartedAt: number | null = null;
134
+ revenueCatInitialized = false;
135
+
136
+ // Generic locking mechanism
137
+ private recalculationLockCount = 0;
138
+ private lastRecalculationTime = 0;
139
+
140
+ constructor() {
141
+ this.sessionsSync = new InvalidateSync(this.fetchSessions);
142
+ this.settingsSync = new InvalidateSync(this.syncSettings);
143
+ this.profileSync = new InvalidateSync(this.fetchProfile);
144
+ this.purchasesSync = new InvalidateSync(this.syncPurchases);
145
+ this.machinesSync = new InvalidateSync(this.fetchMachines);
146
+ this.nativeUpdateSync = new InvalidateSync(this.fetchNativeUpdate);
147
+ this.artifactsSync = new InvalidateSync(this.fetchArtifactsList);
148
+ this.friendsSync = new InvalidateSync(this.fetchFriends);
149
+ this.friendRequestsSync = new InvalidateSync(this.fetchFriendRequests);
150
+ this.feedSync = new InvalidateSync(this.fetchFeed);
151
+
152
+ const registerPushToken = async () => {
153
+ await this.registerPushToken();
154
+ }
155
+ this.pushTokenSync = new InvalidateSync(registerPushToken);
156
+ this.activityAccumulator = new ActivityUpdateAccumulator(this.flushActivityUpdates.bind(this), 2000);
157
+
158
+ // Listen for app state changes to refresh purchases
159
+ AppState.addEventListener('change', (nextAppState) => {
160
+ this.appState = nextAppState;
161
+
162
+ // Notify server of focus state for push notification routing.
163
+ // Mobile: AppState.currentState reflects fg/bg directly.
164
+ // Web/desktop: visibilitychange/focus listeners below drive this same path
165
+ // by updating this.appState too — re-derive via getCurrentAppState() so
166
+ // the wire value matches what the server uses for suppression.
167
+ apiSocket.sendAppState(getCurrentAppState());
168
+
169
+ if (nextAppState === 'active') {
170
+ const shouldFailAfterResume = this.backgroundSendStartedAt !== null
171
+ && this.hasPendingOutboxMessages()
172
+ && (Date.now() - this.backgroundSendStartedAt) >= Sync.BACKGROUND_SEND_TIMEOUT_MS;
173
+ void this.cancelBackgroundSendTimeoutNotification();
174
+ this.clearBackgroundSendWatchdog();
175
+ if (shouldFailAfterResume) {
176
+ void this.notifyMessageSendFailed();
177
+ this.failPendingOutboxMessages('Message failed to send in background after 30s. Please retry.');
178
+ }
179
+ log.log('📱 App became active');
180
+ this.purchasesSync.invalidate();
181
+ this.profileSync.invalidate();
182
+ this.machinesSync.invalidate();
183
+ this.pushTokenSync.invalidate();
184
+ this.sessionsSync.invalidate();
185
+ this.nativeUpdateSync.invalidate();
186
+ log.log('📱 App became active: Invalidating artifacts sync');
187
+ this.artifactsSync.invalidate();
188
+ this.friendsSync.invalidate();
189
+ this.friendRequestsSync.invalidate();
190
+ this.feedSync.invalidate();
191
+ } else {
192
+ log.log(`📱 App state changed to: ${nextAppState}`);
193
+ this.maybeStartBackgroundSendWatchdog();
194
+ }
195
+ });
196
+
197
+ // Web/desktop: AppState alone doesn't capture tab focus/visibility.
198
+ // Notify server when the tab becomes hidden, regains visibility,
199
+ // or window focus changes — so push routing can suppress only when
200
+ // the user is actually looking at this client.
201
+ if (Platform.OS === 'web' && typeof document !== 'undefined') {
202
+ const broadcast = () => {
203
+ apiSocket.sendAppState(getCurrentAppState());
204
+ };
205
+ document.addEventListener('visibilitychange', broadcast);
206
+ window.addEventListener('focus', broadcast);
207
+ window.addEventListener('blur', broadcast);
208
+ }
209
+ }
210
+
211
+ async create(credentials: AuthCredentials, encryption: Encryption) {
212
+ this.credentials = credentials;
213
+ this.encryption = encryption;
214
+ this.anonID = encryption.anonID;
215
+ this.serverID = credentials.token ? parseToken(credentials.token) : 'local';
216
+ await this.#init();
217
+
218
+ // Await settings sync to have fresh settings
219
+ await this.settingsSync.awaitQueue();
220
+
221
+ // Await profile sync to have fresh profile
222
+ await this.profileSync.awaitQueue();
223
+
224
+ // Await purchases sync to have fresh purchases
225
+ await this.purchasesSync.awaitQueue();
226
+ }
227
+
228
+ async restore(credentials: AuthCredentials, encryption: Encryption) {
229
+ // NOTE: No awaiting anything here, we're restoring from a disk (ie app restarted)
230
+ // Purchases sync is invalidated in #init() and will complete asynchronously
231
+ this.credentials = credentials;
232
+ this.encryption = encryption;
233
+ this.anonID = encryption.anonID;
234
+ this.serverID = credentials.token ? parseToken(credentials.token) : 'local';
235
+ await this.#init();
236
+ }
237
+
238
+ async #init() {
239
+
240
+ // Subscribe to updates
241
+ this.subscribeToUpdates();
242
+
243
+ // Sync initial PostHog opt-out state with stored settings
244
+ if (tracking) {
245
+ const currentSettings = storage.getState().settings;
246
+ if (currentSettings.analyticsOptOut) {
247
+ tracking.optOut();
248
+ } else {
249
+ tracking.optIn();
250
+ }
251
+ }
252
+
253
+ // Invalidate sync
254
+ log.log('🔄 #init: Invalidating all syncs');
255
+ this.sessionsSync.invalidate();
256
+ this.settingsSync.invalidate();
257
+ this.profileSync.invalidate();
258
+ this.purchasesSync.invalidate();
259
+ this.machinesSync.invalidate();
260
+ this.pushTokenSync.invalidate();
261
+ this.nativeUpdateSync.invalidate();
262
+ this.friendsSync.invalidate();
263
+ this.friendRequestsSync.invalidate();
264
+ this.artifactsSync.invalidate();
265
+ this.feedSync.invalidate();
266
+ log.log('🔄 #init: All syncs invalidated, including artifacts');
267
+
268
+ // Mark UI ready as soon as sessions load. Machines sync may hang
269
+ // when encryption keys are unavailable (e.g. V1 auth fallback) —
270
+ // let it resolve in the background instead of blocking the UI.
271
+ this.sessionsSync.awaitQueue().then(() => {
272
+ storage.getState().applyReady();
273
+ }).catch((error) => {
274
+ console.error('Failed to load sessions:', error);
275
+ // Still mark ready so the UI doesn't stay on a blank screen forever
276
+ storage.getState().applyReady();
277
+ });
278
+ }
279
+
280
+
281
+ onSessionVisible = (sessionId: string) => {
282
+ this.getMessagesSync(sessionId).invalidate();
283
+
284
+ // Also invalidate git status sync for this session
285
+ gitStatusSync.getSync(sessionId).invalidate();
286
+
287
+ // Notify voice assistant about session visibility
288
+ const session = storage.getState().sessions[sessionId];
289
+ if (session) {
290
+ voiceHooks.onSessionFocus(sessionId, session.metadata || undefined);
291
+ }
292
+ }
293
+
294
+ private getMessagesSync(sessionId: string): InvalidateSync {
295
+ let sync = this.messagesSync.get(sessionId);
296
+ if (!sync) {
297
+ sync = new InvalidateSync(() => this.fetchMessages(sessionId));
298
+ this.messagesSync.set(sessionId, sync);
299
+ }
300
+ return sync;
301
+ }
302
+
303
+ private getSendSync(sessionId: string): InvalidateSync {
304
+ let sync = this.sendSync.get(sessionId);
305
+ if (!sync) {
306
+ sync = new InvalidateSync(() => this.flushOutbox(sessionId));
307
+ this.sendSync.set(sessionId, sync);
308
+ }
309
+ return sync;
310
+ }
311
+
312
+ private enqueueMessages(sessionId: string, messages: NormalizedMessage[]) {
313
+ if (messages.length === 0) {
314
+ return;
315
+ }
316
+
317
+ let queue = this.sessionMessageQueue.get(sessionId);
318
+ if (!queue) {
319
+ queue = [];
320
+ this.sessionMessageQueue.set(sessionId, queue);
321
+ }
322
+ queue.push(...messages);
323
+
324
+ this.scheduleQueuedMessagesProcessing(sessionId);
325
+ }
326
+
327
+ private getSessionMessageLock(sessionId: string): AsyncLock {
328
+ let lock = this.sessionMessageLocks.get(sessionId);
329
+ if (!lock) {
330
+ lock = new AsyncLock();
331
+ this.sessionMessageLocks.set(sessionId, lock);
332
+ }
333
+ return lock;
334
+ }
335
+
336
+ private scheduleQueuedMessagesProcessing(sessionId: string) {
337
+ if (this.sessionQueueProcessing.has(sessionId)) {
338
+ return;
339
+ }
340
+
341
+ this.sessionQueueProcessing.add(sessionId);
342
+ const lock = this.getSessionMessageLock(sessionId);
343
+ void lock.inLock(() => {
344
+ while (true) {
345
+ const pending = this.sessionMessageQueue.get(sessionId);
346
+ if (!pending || pending.length === 0) {
347
+ break;
348
+ }
349
+ const batch = pending.splice(0, pending.length);
350
+ this.applyMessages(sessionId, batch);
351
+ }
352
+ }).finally(() => {
353
+ this.sessionQueueProcessing.delete(sessionId);
354
+ const pending = this.sessionMessageQueue.get(sessionId);
355
+ if (pending && pending.length > 0) {
356
+ this.scheduleQueuedMessagesProcessing(sessionId);
357
+ }
358
+ });
359
+ }
360
+
361
+ private hasPendingOutboxMessages() {
362
+ if (this.sendAbortControllers.size > 0) {
363
+ return true;
364
+ }
365
+ for (const messages of this.pendingOutbox.values()) {
366
+ if (messages.length > 0) {
367
+ return true;
368
+ }
369
+ }
370
+ return false;
371
+ }
372
+
373
+ private maybeStartBackgroundSendWatchdog() {
374
+ if (Platform.OS === 'web' || this.appState === 'active') {
375
+ return;
376
+ }
377
+ if (!this.hasPendingOutboxMessages() || this.backgroundSendTimeout) {
378
+ return;
379
+ }
380
+
381
+ log.log('📨 Pending messages detected in background. Starting 30s send watchdog.');
382
+ this.backgroundSendStartedAt = Date.now();
383
+ this.backgroundSendTimeout = setTimeout(() => {
384
+ this.backgroundSendTimeout = null;
385
+ void this.handleBackgroundSendTimeout();
386
+ }, Sync.BACKGROUND_SEND_TIMEOUT_MS);
387
+ void this.scheduleBackgroundSendTimeoutNotification();
388
+ }
389
+
390
+ private clearBackgroundSendWatchdog() {
391
+ if (this.backgroundSendTimeout) {
392
+ clearTimeout(this.backgroundSendTimeout);
393
+ this.backgroundSendTimeout = null;
394
+ }
395
+ this.backgroundSendStartedAt = null;
396
+ }
397
+
398
+ private async scheduleBackgroundSendTimeoutNotification() {
399
+ if (Platform.OS === 'web' || this.backgroundSendNotificationId) {
400
+ return;
401
+ }
402
+ try {
403
+ this.backgroundSendNotificationId = await Notifications.scheduleNotificationAsync({
404
+ content: {
405
+ title: 'Message not sent',
406
+ body: 'A message is still sending in the background. It will fail in 30 seconds if not delivered.',
407
+ sound: true
408
+ },
409
+ trigger: {
410
+ type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
411
+ seconds: Math.ceil(Sync.BACKGROUND_SEND_TIMEOUT_MS / 1000)
412
+ }
413
+ });
414
+ } catch (error) {
415
+ log.log(`Failed to schedule background send timeout notification: ${error}`);
416
+ }
417
+ }
418
+
419
+ private async cancelBackgroundSendTimeoutNotification() {
420
+ if (!this.backgroundSendNotificationId) {
421
+ return;
422
+ }
423
+ try {
424
+ await Notifications.cancelScheduledNotificationAsync(this.backgroundSendNotificationId);
425
+ } catch (error) {
426
+ log.log(`Failed to cancel background send timeout notification: ${error}`);
427
+ } finally {
428
+ this.backgroundSendNotificationId = null;
429
+ }
430
+ }
431
+
432
+ private async notifyMessageSendFailed() {
433
+ if (Platform.OS === 'web') {
434
+ return;
435
+ }
436
+ try {
437
+ await Notifications.scheduleNotificationAsync({
438
+ content: {
439
+ title: 'Message failed',
440
+ body: 'A message failed to send while the app was in background. Open NasTech and retry.',
441
+ sound: true
442
+ },
443
+ trigger: null
444
+ });
445
+ } catch (error) {
446
+ log.log(`Failed to schedule message failure notification: ${error}`);
447
+ }
448
+ }
449
+
450
+ private failPendingOutboxMessages(reasonText: string) {
451
+ for (const controller of this.sendAbortControllers.values()) {
452
+ controller.abort();
453
+ }
454
+ this.sendAbortControllers.clear();
455
+
456
+ const now = Date.now();
457
+ const sessionIds: string[] = [];
458
+ for (const [sessionId, pending] of this.pendingOutbox) {
459
+ if (pending.length === 0) {
460
+ continue;
461
+ }
462
+ pending.length = 0;
463
+ this.pendingOutbox.delete(sessionId);
464
+ sessionIds.push(sessionId);
465
+ }
466
+
467
+ for (const sessionId of sessionIds) {
468
+ this.enqueueMessages(sessionId, [{
469
+ id: randomUUID(),
470
+ localId: null,
471
+ createdAt: now,
472
+ role: 'event',
473
+ isSidechain: false,
474
+ content: {
475
+ type: 'message',
476
+ message: reasonText
477
+ }
478
+ }]);
479
+ }
480
+ }
481
+
482
+ private async handleBackgroundSendTimeout() {
483
+ if (!this.hasPendingOutboxMessages()) {
484
+ await this.cancelBackgroundSendTimeoutNotification();
485
+ this.backgroundSendStartedAt = null;
486
+ return;
487
+ }
488
+
489
+ await this.cancelBackgroundSendTimeoutNotification();
490
+ await this.notifyMessageSendFailed();
491
+ this.failPendingOutboxMessages('Message failed to send in background after 30s. Please retry.');
492
+ this.backgroundSendStartedAt = null;
493
+ }
494
+
495
+ /**
496
+ * Upload image attachments for a session: read bytes → encrypt → upload to server.
497
+ * Returns UploadedAttachment records to embed as file events before the text message.
498
+ * Failures are logged and skipped rather than aborting the whole message send.
499
+ */
500
+ private async uploadAttachmentsForSession(
501
+ sessionId: string,
502
+ attachments: AttachmentPreview[],
503
+ ): Promise<{ uploaded: UploadedAttachment[]; failed: number }> {
504
+ if (!this.credentials) return { uploaded: [], failed: attachments.length };
505
+
506
+ const blobKey = this.encryption.getSessionBlobKey(sessionId);
507
+ if (!blobKey) {
508
+ console.error(`[attachments] No blob key for session ${sessionId}`);
509
+ return { uploaded: [], failed: attachments.length };
510
+ }
511
+
512
+ const uploaded: UploadedAttachment[] = [];
513
+ let failed = 0;
514
+
515
+ for (const attachment of attachments) {
516
+ try {
517
+ const bytes = await readFileBytes(attachment.uri);
518
+ const encrypted = encryptBlob(bytes, blobKey);
519
+
520
+ const upload = await requestAttachmentUpload(
521
+ this.credentials,
522
+ sessionId,
523
+ attachment.name,
524
+ encrypted.length,
525
+ );
526
+
527
+ await uploadEncryptedBlob(upload, encrypted, this.credentials);
528
+ const { ref } = upload;
529
+
530
+ uploaded.push({
531
+ ref,
532
+ name: attachment.name,
533
+ size: attachment.size,
534
+ width: attachment.width,
535
+ height: attachment.height,
536
+ thumbhash: attachment.thumbhash,
537
+ });
538
+ } catch (err) {
539
+ console.error(`[attachments] Failed to upload ${attachment.name}:`, err);
540
+ failed++;
541
+ // Skip this attachment; do not abort the whole message send.
542
+ }
543
+ }
544
+
545
+ return { uploaded, failed };
546
+ }
547
+
548
+ async sendMessage(sessionId: string, text: string, options?: SendMessageOptions) {
549
+
550
+ // Get encryption — may not be ready yet if sessions are still syncing
551
+ let encryption = this.encryption.getSessionEncryption(sessionId);
552
+ if (!encryption) {
553
+ // Wait for sessions sync to complete (initializes encryption keys)
554
+ await this.sessionsSync.awaitQueue();
555
+ encryption = this.encryption.getSessionEncryption(sessionId);
556
+ if (!encryption) {
557
+ console.error(`Session ${sessionId} not found after sync`);
558
+ return;
559
+ }
560
+ }
561
+
562
+ // Get session data from storage
563
+ let session = storage.getState().sessions[sessionId];
564
+ if (!session) {
565
+ await this.sessionsSync.awaitQueue();
566
+ session = storage.getState().sessions[sessionId];
567
+ if (!session) {
568
+ console.error(`Session ${sessionId} not found in storage after sync`);
569
+ return;
570
+ }
571
+ }
572
+
573
+ const modeMeta = resolveMessageModeMeta(session, storage.getState().settings);
574
+ const { displayText, source = 'chat', attachments } = options ?? {};
575
+
576
+ // Image attachments are wired into the Claude pipeline only; Codex /
577
+ // Gemini / OpenClaw runners read message.content.text and ignore
578
+ // file events, so dropping attachments silently would leave the user
579
+ // wondering why the image was skipped. Warn and send text only.
580
+ const flavor = session.metadata?.flavor;
581
+ const supportsAttachments = !flavor || flavor === 'claude';
582
+ const effectiveAttachments = supportsAttachments ? attachments : undefined;
583
+
584
+ if (attachments && attachments.length > 0 && !supportsAttachments) {
585
+ Modal.alert(
586
+ t('imageUpload.notSupportedTitle'),
587
+ t('imageUpload.notSupportedMessage'),
588
+ [{ text: t('common.ok'), style: 'cancel' }],
589
+ );
590
+ }
591
+
592
+ // Upload attachments and queue file events before the text message.
593
+ if (effectiveAttachments && effectiveAttachments.length > 0) {
594
+ const { uploaded, failed } = await this.uploadAttachmentsForSession(sessionId, effectiveAttachments);
595
+
596
+ if (failed > 0) {
597
+ Modal.alert(
598
+ t('imageUpload.uploadFailedTitle'),
599
+ t('imageUpload.uploadFailedMessage', { count: failed }),
600
+ [{ text: t('common.ok'), style: 'cancel' }],
601
+ );
602
+ }
603
+
604
+ if (uploaded.length > 0) {
605
+ let pending = this.pendingOutbox.get(sessionId);
606
+ if (!pending) {
607
+ pending = [];
608
+ this.pendingOutbox.set(sessionId, pending);
609
+ }
610
+
611
+ for (const att of uploaded) {
612
+ const fileRecord: RawRecord = {
613
+ role: 'session',
614
+ content: {
615
+ type: 'session',
616
+ data: {
617
+ id: randomUUID(),
618
+ time: Date.now(),
619
+ role: 'user',
620
+ ev: {
621
+ t: 'file',
622
+ ref: att.ref,
623
+ name: att.name,
624
+ size: att.size,
625
+ // Include image metadata when we have dimensions; thumbhash is
626
+ // optional. The native iOS picker can't generate a thumbhash
627
+ // without Canvas, so requiring it here would reduce the chat
628
+ // bubble to a compact filename row instead of an inline picture.
629
+ // FileView only needs w/h to size the inline render — placeholder
630
+ // is absent, but the real image is decrypted on mount.
631
+ ...(att.width > 0 && att.height > 0
632
+ ? {
633
+ image: {
634
+ width: att.width,
635
+ height: att.height,
636
+ ...(att.thumbhash ? { thumbhash: att.thumbhash } : {}),
637
+ },
638
+ }
639
+ : {}),
640
+ },
641
+ },
642
+ },
643
+ };
644
+ const encryptedFileRecord = await encryption.encryptRawRecord(fileRecord);
645
+ const fileLocalId = randomUUID();
646
+ const fileNormalized = normalizeRawMessage(fileLocalId, fileLocalId, Date.now(), fileRecord);
647
+ if (fileNormalized) {
648
+ this.enqueueMessages(sessionId, [fileNormalized]);
649
+ }
650
+ pending.push({ localId: fileLocalId, content: encryptedFileRecord });
651
+ }
652
+ }
653
+ }
654
+
655
+ // Generate local ID
656
+ const localId = randomUUID();
657
+
658
+ // Determine sentFrom based on platform
659
+ let sentFrom: string;
660
+ if (Platform.OS === 'web') {
661
+ sentFrom = 'web';
662
+ } else if (Platform.OS === 'android') {
663
+ sentFrom = 'android';
664
+ } else if (Platform.OS === 'ios') {
665
+ // Check if running on Mac (Catalyst or Designed for iPad on Mac)
666
+ if (isRunningOnMac()) {
667
+ sentFrom = 'mac';
668
+ } else {
669
+ sentFrom = 'ios';
670
+ }
671
+ } else {
672
+ sentFrom = 'web'; // fallback
673
+ }
674
+
675
+ // Create user message content with metadata
676
+ const content: RawRecord = {
677
+ role: 'user',
678
+ content: {
679
+ type: 'text',
680
+ text
681
+ },
682
+ meta: {
683
+ sentFrom,
684
+ appendSystemPrompt: systemPrompt,
685
+ ...(modeMeta.permissionMode !== undefined ? { permissionMode: modeMeta.permissionMode } : {}),
686
+ ...(modeMeta.model !== undefined ? { model: modeMeta.model } : {}),
687
+ ...(modeMeta.effort !== undefined ? { effort: modeMeta.effort } : {}),
688
+ ...(displayText && { displayText }) // Add displayText if provided
689
+ }
690
+ };
691
+ const encryptedRawRecord = await encryption.encryptRawRecord(content);
692
+
693
+ // Add to messages - normalize the raw record
694
+ const createdAt = Date.now();
695
+ const normalizedMessage = normalizeRawMessage(localId, localId, createdAt, content);
696
+ if (normalizedMessage) {
697
+ this.enqueueMessages(sessionId, [normalizedMessage]);
698
+ }
699
+
700
+ let pending = this.pendingOutbox.get(sessionId);
701
+ if (!pending) {
702
+ pending = [];
703
+ this.pendingOutbox.set(sessionId, pending);
704
+ }
705
+ pending.push({
706
+ localId,
707
+ content: encryptedRawRecord
708
+ });
709
+ trackMessageSent(source, session.metadata);
710
+
711
+ this.getSendSync(sessionId).invalidate();
712
+ this.maybeStartBackgroundSendWatchdog();
713
+ }
714
+
715
+ /** Server sent us settings — merge any pending local changes on top, then apply as one update. */
716
+ private applyServerSettings = (serverSettings: Settings, version: number) => {
717
+ const merged = Object.keys(this.pendingSettings).length > 0
718
+ ? applySettings(serverSettings, this.pendingSettings)
719
+ : serverSettings;
720
+ storage.getState().applySettings(merged, version);
721
+ }
722
+
723
+ applySettings = (delta: Partial<Settings>) => {
724
+ storage.getState().applySettingsLocal(delta);
725
+
726
+ // Save pending settings
727
+ this.pendingSettings = { ...this.pendingSettings, ...delta };
728
+ savePendingSettings(this.pendingSettings);
729
+
730
+ // Sync PostHog opt-out state if it was changed
731
+ if (tracking && 'analyticsOptOut' in delta) {
732
+ const currentSettings = storage.getState().settings;
733
+ if (currentSettings.analyticsOptOut) {
734
+ tracking.optOut();
735
+ } else {
736
+ tracking.optIn();
737
+ }
738
+ }
739
+
740
+ // Invalidate settings sync
741
+ this.settingsSync.invalidate();
742
+ }
743
+
744
+ refreshPurchases = () => {
745
+ this.purchasesSync.invalidate();
746
+ }
747
+
748
+ refreshProfile = async () => {
749
+ await this.profileSync.invalidateAndAwait();
750
+ }
751
+
752
+ purchaseProduct = async (productId: string): Promise<{ success: boolean; error?: string }> => {
753
+ try {
754
+ // Check if RevenueCat is initialized
755
+ if (!this.revenueCatInitialized) {
756
+ return { success: false, error: 'RevenueCat not initialized' };
757
+ }
758
+
759
+ // Fetch the product
760
+ const products = await RevenueCat.getProducts([productId]);
761
+ if (products.length === 0) {
762
+ return { success: false, error: `Product '${productId}' not found` };
763
+ }
764
+
765
+ // Purchase the product
766
+ const product = products[0];
767
+ const { customerInfo } = await RevenueCat.purchaseStoreProduct(product);
768
+
769
+ // Update local purchases data
770
+ storage.getState().applyPurchases(customerInfo);
771
+
772
+ return { success: true };
773
+ } catch (error: any) {
774
+ // Check if user cancelled
775
+ if (error.userCancelled) {
776
+ return { success: false, error: 'Purchase cancelled' };
777
+ }
778
+
779
+ // Return the error message
780
+ return { success: false, error: error.message || 'Purchase failed' };
781
+ }
782
+ }
783
+
784
+ getOfferings = async (): Promise<{ success: boolean; offerings?: any; error?: string }> => {
785
+ try {
786
+ // Check if RevenueCat is initialized
787
+ if (!this.revenueCatInitialized) {
788
+ return { success: false, error: 'RevenueCat not initialized' };
789
+ }
790
+
791
+ // Fetch offerings
792
+ const offerings = await RevenueCat.getOfferings();
793
+
794
+ // Return the offerings data
795
+ return {
796
+ success: true,
797
+ offerings: {
798
+ current: offerings.current,
799
+ all: offerings.all
800
+ }
801
+ };
802
+ } catch (error: any) {
803
+ return { success: false, error: error.message || 'Failed to fetch offerings' };
804
+ }
805
+ }
806
+
807
+ presentPaywall = async (flow?: string): Promise<{ success: boolean; purchased?: boolean; error?: string }> => {
808
+ try {
809
+ // Check if RevenueCat is initialized
810
+ if (!this.revenueCatInitialized) {
811
+ const error = 'RevenueCat not initialized';
812
+ trackPaywallError(error, flow);
813
+ return { success: false, error };
814
+ }
815
+
816
+ // Track paywall presentation
817
+ trackPaywallPresented(flow);
818
+
819
+ // Present the paywall (with flow custom variable if specified)
820
+ const result = await RevenueCat.presentPaywall(
821
+ flow ? { customVariables: { flow } } : undefined
822
+ );
823
+
824
+ // Handle the result
825
+ switch (result) {
826
+ case PaywallResult.PURCHASED:
827
+ trackPaywallPurchased(flow);
828
+ // Refresh customer info after purchase
829
+ await this.syncPurchases();
830
+ return { success: true, purchased: true };
831
+ case PaywallResult.RESTORED:
832
+ trackPaywallRestored(flow);
833
+ // Refresh customer info after restore
834
+ await this.syncPurchases();
835
+ return { success: true, purchased: true };
836
+ case PaywallResult.CANCELLED:
837
+ trackPaywallCancelled(flow);
838
+ return { success: true, purchased: false };
839
+ case PaywallResult.NOT_PRESENTED:
840
+ trackPaywallError('Paywall not presented', flow);
841
+ return { success: false, error: 'Paywall not available on this platform' };
842
+ case PaywallResult.ERROR:
843
+ default:
844
+ const errorMsg = 'Failed to present paywall';
845
+ trackPaywallError(errorMsg, flow);
846
+ return { success: false, error: errorMsg };
847
+ }
848
+ } catch (error: any) {
849
+ const errorMessage = error.message || 'Failed to present paywall';
850
+ trackPaywallError(errorMessage, flow);
851
+ return { success: false, error: errorMessage };
852
+ }
853
+ }
854
+
855
+ async assumeUsers(userIds: string[]): Promise<void> {
856
+ if (!this.credentials || userIds.length === 0) return;
857
+
858
+ const state = storage.getState();
859
+ // Filter out users we already have in cache (including null for 404s)
860
+ const missingIds = userIds.filter(id => !(id in state.users));
861
+
862
+ if (missingIds.length === 0) return;
863
+
864
+ log.log(`👤 Fetching ${missingIds.length} missing users...`);
865
+
866
+ // Fetch missing users in parallel
867
+ const results = await Promise.all(
868
+ missingIds.map(async (id) => {
869
+ try {
870
+ const profile = await getUserProfile(this.credentials!, id);
871
+ return { id, profile }; // profile is null if 404
872
+ } catch (error) {
873
+ console.error(`Failed to fetch user ${id}:`, error);
874
+ return { id, profile: null }; // Treat errors as 404
875
+ }
876
+ })
877
+ );
878
+
879
+ // Convert to Record<string, UserProfile | null>
880
+ const usersMap: Record<string, UserProfile | null> = {};
881
+ results.forEach(({ id, profile }) => {
882
+ usersMap[id] = profile;
883
+ });
884
+
885
+ storage.getState().applyUsers(usersMap);
886
+ log.log(`👤 Applied ${results.length} users to cache (${results.filter(r => r.profile).length} found, ${results.filter(r => !r.profile).length} not found)`);
887
+ }
888
+
889
+ //
890
+ // Private
891
+ //
892
+
893
+ private fetchSessions = async () => {
894
+ const API_ENDPOINT = getServerUrl();
895
+ const response = await fetch(`${API_ENDPOINT}/api/sessions`, {
896
+ headers: { 'Content-Type': 'application/json' }
897
+ });
898
+
899
+ if (!response.ok) {
900
+ throw new Error(`Failed to fetch sessions: ${response.status}`);
901
+ }
902
+
903
+ const data = await response.json();
904
+ const nasSessions = data.sessions as Array<{
905
+ id: string;
906
+ title?: string;
907
+ started_at?: number;
908
+ last_active?: number;
909
+ ended_at?: number | null;
910
+ is_active?: boolean;
911
+ model?: string;
912
+ preview?: string;
913
+ }>;
914
+
915
+ // Initialize null encryption for all sessions (NasTech has no per-session keys)
916
+ const sessionKeys = new Map<string, Uint8Array | null>();
917
+ for (const s of nasSessions) {
918
+ sessionKeys.set(s.id, null);
919
+ }
920
+ await this.encryption.initializeSessions(sessionKeys);
921
+
922
+ const now = Date.now();
923
+ const decryptedSessions: (Omit<Session, 'presence'> & { presence?: "online" | number })[] = nasSessions.map(s => ({
924
+ id: s.id,
925
+ tag: '',
926
+ seq: 0,
927
+ thinking: false,
928
+ thinkingAt: 0,
929
+ active: s.is_active ?? false,
930
+ activeAt: s.last_active ? s.last_active * 1000 : now,
931
+ createdAt: s.started_at ? s.started_at * 1000 : now,
932
+ updatedAt: s.last_active ? s.last_active * 1000 : now,
933
+ metadata: {
934
+ path: '/',
935
+ host: 'localhost',
936
+ name: s.title ?? '',
937
+ version: '1.0',
938
+ summary: s.preview ? { text: s.preview, updatedAt: now } : undefined,
939
+ },
940
+ agentState: null,
941
+ lastMessage: null,
942
+ dataEncryptionKey: null,
943
+ metadataVersion: 0,
944
+ agentStateVersion: 0,
945
+ }));
946
+
947
+ this.applySessions(decryptedSessions);
948
+ log.log(`📥 fetchSessions completed - processed ${decryptedSessions.length} sessions`);
949
+ }
950
+
951
+ public refreshMachines = async () => {
952
+ return this.fetchMachines();
953
+ }
954
+
955
+ public refreshSessions = async () => {
956
+ return this.sessionsSync.invalidateAndAwait();
957
+ }
958
+
959
+ public getCredentials() {
960
+ return this.credentials;
961
+ }
962
+
963
+ // Artifact methods
964
+ public fetchArtifactsList = async (): Promise<void> => {
965
+ log.log('📦 fetchArtifactsList: Starting artifact sync');
966
+ if (!this.credentials) {
967
+ log.log('📦 fetchArtifactsList: No credentials, skipping');
968
+ return;
969
+ }
970
+
971
+ try {
972
+ log.log('📦 fetchArtifactsList: Fetching artifacts from server');
973
+ const artifacts = await fetchArtifacts(this.credentials);
974
+ log.log(`📦 fetchArtifactsList: Received ${artifacts.length} artifacts from server`);
975
+ const decryptedArtifacts: DecryptedArtifact[] = [];
976
+
977
+ for (const artifact of artifacts) {
978
+ try {
979
+ // Decrypt the data encryption key
980
+ const decryptedKey = await this.encryption.decryptEncryptionKey(artifact.dataEncryptionKey);
981
+ if (!decryptedKey) {
982
+ console.error(`Failed to decrypt key for artifact ${artifact.id}`);
983
+ continue;
984
+ }
985
+
986
+ // Store the decrypted key in memory
987
+ this.artifactDataKeys.set(artifact.id, decryptedKey);
988
+
989
+ // Create artifact encryption instance
990
+ const artifactEncryption = new ArtifactEncryption(decryptedKey);
991
+
992
+ // Decrypt header
993
+ const header = await artifactEncryption.decryptHeader(artifact.header);
994
+
995
+ decryptedArtifacts.push({
996
+ id: artifact.id,
997
+ title: header?.title || null,
998
+ sessions: header?.sessions, // Include sessions from header
999
+ draft: header?.draft, // Include draft flag from header
1000
+ body: undefined, // Body not loaded in list
1001
+ headerVersion: artifact.headerVersion,
1002
+ bodyVersion: artifact.bodyVersion,
1003
+ seq: artifact.seq,
1004
+ createdAt: artifact.createdAt,
1005
+ updatedAt: artifact.updatedAt,
1006
+ isDecrypted: !!header,
1007
+ });
1008
+ } catch (err) {
1009
+ console.error(`Failed to decrypt artifact ${artifact.id}:`, err);
1010
+ // Add with decryption failed flag
1011
+ decryptedArtifacts.push({
1012
+ id: artifact.id,
1013
+ title: null,
1014
+ body: undefined,
1015
+ headerVersion: artifact.headerVersion,
1016
+ seq: artifact.seq,
1017
+ createdAt: artifact.createdAt,
1018
+ updatedAt: artifact.updatedAt,
1019
+ isDecrypted: false,
1020
+ });
1021
+ }
1022
+ }
1023
+
1024
+ log.log(`📦 fetchArtifactsList: Successfully decrypted ${decryptedArtifacts.length} artifacts`);
1025
+ storage.getState().applyArtifacts(decryptedArtifacts);
1026
+ log.log('📦 fetchArtifactsList: Artifacts applied to storage');
1027
+ } catch (error) {
1028
+ log.log(`📦 fetchArtifactsList: Error fetching artifacts: ${error}`);
1029
+ console.error('Failed to fetch artifacts:', error);
1030
+ throw error;
1031
+ }
1032
+ }
1033
+
1034
+ public async fetchArtifactWithBody(artifactId: string): Promise<DecryptedArtifact | null> {
1035
+ if (!this.credentials) return null;
1036
+
1037
+ try {
1038
+ const artifact = await fetchArtifact(this.credentials, artifactId);
1039
+
1040
+ // Decrypt the data encryption key
1041
+ const decryptedKey = await this.encryption.decryptEncryptionKey(artifact.dataEncryptionKey);
1042
+ if (!decryptedKey) {
1043
+ console.error(`Failed to decrypt key for artifact ${artifactId}`);
1044
+ return null;
1045
+ }
1046
+
1047
+ // Store the decrypted key in memory
1048
+ this.artifactDataKeys.set(artifact.id, decryptedKey);
1049
+
1050
+ // Create artifact encryption instance
1051
+ const artifactEncryption = new ArtifactEncryption(decryptedKey);
1052
+
1053
+ // Decrypt header and body
1054
+ const header = await artifactEncryption.decryptHeader(artifact.header);
1055
+ const body = artifact.body ? await artifactEncryption.decryptBody(artifact.body) : null;
1056
+
1057
+ return {
1058
+ id: artifact.id,
1059
+ title: header?.title || null,
1060
+ sessions: header?.sessions, // Include sessions from header
1061
+ draft: header?.draft, // Include draft flag from header
1062
+ body: body?.body || null,
1063
+ headerVersion: artifact.headerVersion,
1064
+ bodyVersion: artifact.bodyVersion,
1065
+ seq: artifact.seq,
1066
+ createdAt: artifact.createdAt,
1067
+ updatedAt: artifact.updatedAt,
1068
+ isDecrypted: !!header,
1069
+ };
1070
+ } catch (error) {
1071
+ console.error(`Failed to fetch artifact ${artifactId}:`, error);
1072
+ return null;
1073
+ }
1074
+ }
1075
+
1076
+ public async createArtifact(
1077
+ title: string | null,
1078
+ body: string | null,
1079
+ sessions?: string[],
1080
+ draft?: boolean
1081
+ ): Promise<string> {
1082
+ if (!this.credentials) {
1083
+ throw new Error('Not authenticated');
1084
+ }
1085
+
1086
+ try {
1087
+ // Generate unique artifact ID
1088
+ const artifactId = this.encryption.generateId();
1089
+
1090
+ // Generate data encryption key
1091
+ const dataEncryptionKey = ArtifactEncryption.generateDataEncryptionKey();
1092
+
1093
+ // Store the decrypted key in memory
1094
+ this.artifactDataKeys.set(artifactId, dataEncryptionKey);
1095
+
1096
+ // Encrypt the data encryption key with user's key
1097
+ const encryptedKey = await this.encryption.encryptEncryptionKey(dataEncryptionKey);
1098
+
1099
+ // Create artifact encryption instance
1100
+ const artifactEncryption = new ArtifactEncryption(dataEncryptionKey);
1101
+
1102
+ // Encrypt header and body
1103
+ const encryptedHeader = await artifactEncryption.encryptHeader({ title, sessions, draft });
1104
+ const encryptedBody = await artifactEncryption.encryptBody({ body });
1105
+
1106
+ // Create the request
1107
+ const request: ArtifactCreateRequest = {
1108
+ id: artifactId,
1109
+ header: encryptedHeader,
1110
+ body: encryptedBody,
1111
+ dataEncryptionKey: encodeBase64(encryptedKey, 'base64'),
1112
+ };
1113
+
1114
+ // Send to server
1115
+ const artifact = await createArtifact(this.credentials, request);
1116
+
1117
+ // Add to local storage
1118
+ const decryptedArtifact: DecryptedArtifact = {
1119
+ id: artifact.id,
1120
+ title,
1121
+ sessions,
1122
+ draft,
1123
+ body,
1124
+ headerVersion: artifact.headerVersion,
1125
+ bodyVersion: artifact.bodyVersion,
1126
+ seq: artifact.seq,
1127
+ createdAt: artifact.createdAt,
1128
+ updatedAt: artifact.updatedAt,
1129
+ isDecrypted: true,
1130
+ };
1131
+
1132
+ storage.getState().addArtifact(decryptedArtifact);
1133
+
1134
+ return artifactId;
1135
+ } catch (error) {
1136
+ console.error('Failed to create artifact:', error);
1137
+ throw error;
1138
+ }
1139
+ }
1140
+
1141
+ public async updateArtifact(
1142
+ artifactId: string,
1143
+ title: string | null,
1144
+ body: string | null,
1145
+ sessions?: string[],
1146
+ draft?: boolean
1147
+ ): Promise<void> {
1148
+ if (!this.credentials) {
1149
+ throw new Error('Not authenticated');
1150
+ }
1151
+
1152
+ try {
1153
+ // Get current artifact to get versions and encryption key
1154
+ const currentArtifact = storage.getState().artifacts[artifactId];
1155
+ if (!currentArtifact) {
1156
+ throw new Error('Artifact not found');
1157
+ }
1158
+
1159
+ // Get the data encryption key from memory or fetch it
1160
+ let dataEncryptionKey = this.artifactDataKeys.get(artifactId);
1161
+
1162
+ // Fetch full artifact if we don't have version info or encryption key
1163
+ let headerVersion = currentArtifact.headerVersion;
1164
+ let bodyVersion = currentArtifact.bodyVersion;
1165
+
1166
+ if (headerVersion === undefined || bodyVersion === undefined || !dataEncryptionKey) {
1167
+ const fullArtifact = await fetchArtifact(this.credentials, artifactId);
1168
+ headerVersion = fullArtifact.headerVersion;
1169
+ bodyVersion = fullArtifact.bodyVersion;
1170
+
1171
+ // Decrypt and store the data encryption key if we don't have it
1172
+ if (!dataEncryptionKey) {
1173
+ const decryptedKey = await this.encryption.decryptEncryptionKey(fullArtifact.dataEncryptionKey);
1174
+ if (!decryptedKey) {
1175
+ throw new Error('Failed to decrypt encryption key');
1176
+ }
1177
+ this.artifactDataKeys.set(artifactId, decryptedKey);
1178
+ dataEncryptionKey = decryptedKey;
1179
+ }
1180
+ }
1181
+
1182
+ // Create artifact encryption instance
1183
+ const artifactEncryption = new ArtifactEncryption(dataEncryptionKey);
1184
+
1185
+ // Prepare update request
1186
+ const updateRequest: ArtifactUpdateRequest = {};
1187
+
1188
+ // Check if header needs updating (title, sessions, or draft changed)
1189
+ if (title !== currentArtifact.title ||
1190
+ JSON.stringify(sessions) !== JSON.stringify(currentArtifact.sessions) ||
1191
+ draft !== currentArtifact.draft) {
1192
+ const encryptedHeader = await artifactEncryption.encryptHeader({
1193
+ title,
1194
+ sessions,
1195
+ draft
1196
+ });
1197
+ updateRequest.header = encryptedHeader;
1198
+ updateRequest.expectedHeaderVersion = headerVersion;
1199
+ }
1200
+
1201
+ // Only update body if it changed
1202
+ if (body !== currentArtifact.body) {
1203
+ const encryptedBody = await artifactEncryption.encryptBody({ body });
1204
+ updateRequest.body = encryptedBody;
1205
+ updateRequest.expectedBodyVersion = bodyVersion;
1206
+ }
1207
+
1208
+ // Skip if no changes
1209
+ if (Object.keys(updateRequest).length === 0) {
1210
+ return;
1211
+ }
1212
+
1213
+ // Send update to server
1214
+ const response = await updateArtifact(this.credentials, artifactId, updateRequest);
1215
+
1216
+ if (!response.success) {
1217
+ // Handle version mismatch
1218
+ if (response.error === 'version-mismatch') {
1219
+ throw new Error('Artifact was modified by another client. Please refresh and try again.');
1220
+ }
1221
+ throw new Error('Failed to update artifact');
1222
+ }
1223
+
1224
+ // Update local storage
1225
+ const updatedArtifact: DecryptedArtifact = {
1226
+ ...currentArtifact,
1227
+ title,
1228
+ sessions,
1229
+ draft,
1230
+ body,
1231
+ headerVersion: response.headerVersion !== undefined ? response.headerVersion : headerVersion,
1232
+ bodyVersion: response.bodyVersion !== undefined ? response.bodyVersion : bodyVersion,
1233
+ updatedAt: Date.now(),
1234
+ };
1235
+
1236
+ storage.getState().updateArtifact(updatedArtifact);
1237
+ } catch (error) {
1238
+ console.error('Failed to update artifact:', error);
1239
+ throw error;
1240
+ }
1241
+ }
1242
+
1243
+ private fetchMachines = async () => {
1244
+ // NasTech has no machines concept — apply empty list and return immediately
1245
+ storage.getState().applyMachines([], true);
1246
+ log.log('🖥️ fetchMachines: no machines in NasTech mode');
1247
+ return;
1248
+
1249
+ // Dead code below kept for type-checking; never executed
1250
+ const API_ENDPOINT = getServerUrl();
1251
+ const response = await fetch(`${API_ENDPOINT}/v1/machines`, {
1252
+ headers: {
1253
+ 'Content-Type': 'application/json',
1254
+ 'X-NasTech-Client': getNasTechClientId(),
1255
+ }
1256
+ });
1257
+
1258
+ if (!response.ok) {
1259
+ console.error(`Failed to fetch machines: ${response.status}`);
1260
+ return;
1261
+ }
1262
+
1263
+ const data = await response.json();
1264
+ console.log(`📊 Sync: Fetched ${Array.isArray(data) ? data.length : 0} machines from server`);
1265
+ const machines = data as Array<{
1266
+ id: string;
1267
+ metadata: string;
1268
+ metadataVersion: number;
1269
+ daemonState?: string | null;
1270
+ daemonStateVersion?: number;
1271
+ dataEncryptionKey?: string | null; // Add support for per-machine encryption keys
1272
+ seq: number;
1273
+ active: boolean;
1274
+ activeAt: number; // Changed from lastActiveAt
1275
+ createdAt: number;
1276
+ updatedAt: number;
1277
+ }>;
1278
+
1279
+ // First, collect and decrypt encryption keys for all machines
1280
+ const machineKeysMap = new Map<string, Uint8Array | null>();
1281
+ for (const machine of machines) {
1282
+ if (machine.dataEncryptionKey) {
1283
+ const decryptedKey = await this.encryption.decryptEncryptionKey(machine.dataEncryptionKey as string);
1284
+ if (!decryptedKey) {
1285
+ console.error(`Failed to decrypt data encryption key for machine ${machine.id}`);
1286
+ continue;
1287
+ }
1288
+ machineKeysMap.set(machine.id, decryptedKey);
1289
+ this.machineDataKeys.set(machine.id, decryptedKey as Uint8Array);
1290
+ } else {
1291
+ machineKeysMap.set(machine.id, null);
1292
+ }
1293
+ }
1294
+
1295
+ // Initialize machine encryptions
1296
+ await this.encryption.initializeMachines(machineKeysMap);
1297
+
1298
+ // Process all machines first, then update state once
1299
+ const decryptedMachines: Machine[] = [];
1300
+
1301
+ for (const machine of machines) {
1302
+ // Get machine-specific encryption (might exist from previous initialization)
1303
+ const machineEncryption = this.encryption.getMachineEncryption(machine.id);
1304
+ if (!machineEncryption) {
1305
+ console.error(`Machine encryption not found for ${machine.id} - this should never happen`);
1306
+ continue;
1307
+ }
1308
+
1309
+ try {
1310
+
1311
+ // Use machine-specific encryption (which handles fallback internally)
1312
+ const metadata = machine.metadata
1313
+ ? await machineEncryption!.decryptMetadata(machine.metadataVersion, machine.metadata)
1314
+ : null;
1315
+
1316
+ const daemonState = machine.daemonState
1317
+ ? await machineEncryption!.decryptDaemonState(machine.daemonStateVersion || 0, machine.daemonState)
1318
+ : null;
1319
+
1320
+ decryptedMachines.push({
1321
+ id: machine.id,
1322
+ seq: machine.seq,
1323
+ createdAt: machine.createdAt,
1324
+ updatedAt: machine.updatedAt,
1325
+ active: machine.active,
1326
+ activeAt: machine.activeAt,
1327
+ metadata,
1328
+ metadataVersion: machine.metadataVersion,
1329
+ daemonState,
1330
+ daemonStateVersion: machine.daemonStateVersion || 0
1331
+ });
1332
+ } catch (error) {
1333
+ console.error(`Failed to decrypt machine ${machine.id}:`, error);
1334
+ // Still add the machine with null metadata
1335
+ decryptedMachines.push({
1336
+ id: machine.id,
1337
+ seq: machine.seq,
1338
+ createdAt: machine.createdAt,
1339
+ updatedAt: machine.updatedAt,
1340
+ active: machine.active,
1341
+ activeAt: machine.activeAt,
1342
+ metadata: null,
1343
+ metadataVersion: machine.metadataVersion,
1344
+ daemonState: null,
1345
+ daemonStateVersion: 0
1346
+ });
1347
+ }
1348
+ }
1349
+
1350
+ // Replace entire machine state with fetched machines
1351
+ storage.getState().applyMachines(decryptedMachines, true);
1352
+ log.log(`🖥️ fetchMachines completed - processed ${decryptedMachines.length} machines`);
1353
+ }
1354
+
1355
+ private fetchFriends = async () => {
1356
+ if (!this.credentials) return;
1357
+
1358
+ try {
1359
+ log.log('👥 Fetching friends list...');
1360
+ const friendsList = await getFriendsList(this.credentials);
1361
+ storage.getState().applyFriends(friendsList);
1362
+ log.log(`👥 fetchFriends completed - processed ${friendsList.length} friends`);
1363
+ } catch (error) {
1364
+ console.error('Failed to fetch friends:', error);
1365
+ // Silently handle error - UI will show appropriate state
1366
+ }
1367
+ }
1368
+
1369
+ private fetchFriendRequests = async () => {
1370
+ // Friend requests are now included in the friends list with status='pending'
1371
+ // This method is kept for backward compatibility but does nothing
1372
+ log.log('👥 fetchFriendRequests called - now handled by fetchFriends');
1373
+ }
1374
+
1375
+ private fetchFeed = async () => {
1376
+ if (!this.credentials) return;
1377
+
1378
+ try {
1379
+ log.log('📰 Fetching feed...');
1380
+ const state = storage.getState();
1381
+ const existingItems = state.feedItems;
1382
+ const head = state.feedHead;
1383
+
1384
+ // Load feed items - if we have a head, load newer items
1385
+ let allItems: FeedItem[] = [];
1386
+ let hasMore = true;
1387
+ let cursor = head ? { after: head } : undefined;
1388
+ let loadedCount = 0;
1389
+ const maxItems = 500;
1390
+
1391
+ // Keep loading until we reach known items or hit max limit
1392
+ while (hasMore && loadedCount < maxItems) {
1393
+ const response = await fetchFeed(this.credentials, {
1394
+ limit: 100,
1395
+ ...cursor
1396
+ });
1397
+
1398
+ // Check if we reached known items
1399
+ const foundKnown = response.items.some(item =>
1400
+ existingItems.some(existing => existing.id === item.id)
1401
+ );
1402
+
1403
+ allItems.push(...response.items);
1404
+ loadedCount += response.items.length;
1405
+ hasMore = response.hasMore && !foundKnown;
1406
+
1407
+ // Update cursor for next page
1408
+ if (response.items.length > 0) {
1409
+ const lastItem = response.items[response.items.length - 1];
1410
+ cursor = { after: lastItem.cursor };
1411
+ }
1412
+ }
1413
+
1414
+ // If this is initial load (no head), also load older items
1415
+ if (!head && allItems.length < 100) {
1416
+ const response = await fetchFeed(this.credentials, {
1417
+ limit: 100
1418
+ });
1419
+ allItems.push(...response.items);
1420
+ }
1421
+
1422
+ // Collect user IDs from friend-related feed items
1423
+ const userIds = new Set<string>();
1424
+ allItems.forEach(item => {
1425
+ if (item.body && (item.body.kind === 'friend_request' || item.body.kind === 'friend_accepted')) {
1426
+ userIds.add(item.body.uid);
1427
+ }
1428
+ });
1429
+
1430
+ // Fetch missing users
1431
+ if (userIds.size > 0) {
1432
+ await this.assumeUsers(Array.from(userIds));
1433
+ }
1434
+
1435
+ // Filter out items where user is not found (404)
1436
+ const users = storage.getState().users;
1437
+ const compatibleItems = allItems.filter(item => {
1438
+ // Keep text items
1439
+ if (item.body.kind === 'text') return true;
1440
+
1441
+ // For friend-related items, check if user exists and is not null (404)
1442
+ if (item.body.kind === 'friend_request' || item.body.kind === 'friend_accepted') {
1443
+ const userProfile = users[item.body.uid];
1444
+ // Keep item only if user exists and is not null
1445
+ return userProfile !== null && userProfile !== undefined;
1446
+ }
1447
+
1448
+ return true;
1449
+ });
1450
+
1451
+ // Apply only compatible items to storage
1452
+ storage.getState().applyFeedItems(compatibleItems);
1453
+ log.log(`📰 fetchFeed completed - loaded ${compatibleItems.length} compatible items (${allItems.length - compatibleItems.length} filtered)`);
1454
+ } catch (error) {
1455
+ console.error('Failed to fetch feed:', error);
1456
+ }
1457
+ }
1458
+
1459
+ private syncSettings = async () => {
1460
+ // NasTech has no remote settings server — manage settings locally only.
1461
+ // Flush any pending changes into the local store and return.
1462
+ if (Object.keys(this.pendingSettings).length > 0) {
1463
+ const current = storage.getState().settings;
1464
+ const merged = applySettings(current, this.pendingSettings);
1465
+ this.pendingSettings = {};
1466
+ savePendingSettings(this.pendingSettings);
1467
+ const nextVersion = (storage.getState().settingsVersion ?? 0) + 1;
1468
+ this.applyServerSettings(merged, nextVersion);
1469
+ }
1470
+ }
1471
+
1472
+ private fetchProfile = async () => {
1473
+ // NasTech has no profile endpoint — skip silently
1474
+ }
1475
+
1476
+ private fetchNativeUpdate = async () => {
1477
+ try {
1478
+ const serverUrl = getServerUrl();
1479
+ const response = await fetch(`${serverUrl}/api/status`);
1480
+ if (!response.ok) {
1481
+ storage.getState().applyNativeUpdateStatus(null);
1482
+ return;
1483
+ }
1484
+ // NasTech does not push native update URLs — always report no update available
1485
+ storage.getState().applyNativeUpdateStatus({ available: false });
1486
+ } catch (error) {
1487
+ console.log('[fetchNativeUpdate] Error:', error);
1488
+ storage.getState().applyNativeUpdateStatus(null);
1489
+ }
1490
+ }
1491
+
1492
+ private syncPurchases = async () => {
1493
+ try {
1494
+ // Initialize RevenueCat if not already done
1495
+ if (!this.revenueCatInitialized) {
1496
+ // Get the appropriate API key based on platform
1497
+ let apiKey: string | undefined;
1498
+
1499
+ if (Platform.OS === 'ios') {
1500
+ apiKey = config.revenueCatAppleKey;
1501
+ } else if (Platform.OS === 'android') {
1502
+ apiKey = config.revenueCatGoogleKey;
1503
+ } else if (Platform.OS === 'web') {
1504
+ apiKey = config.revenueCatStripeKey;
1505
+ }
1506
+
1507
+ if (!apiKey) {
1508
+ console.log(`RevenueCat: No API key found for platform ${Platform.OS}`);
1509
+ return;
1510
+ }
1511
+
1512
+ // Configure RevenueCat
1513
+ if (__DEV__) {
1514
+ RevenueCat.setLogLevel(LogLevel.DEBUG);
1515
+ }
1516
+
1517
+ // Initialize with the public ID as user ID
1518
+ RevenueCat.configure({
1519
+ apiKey,
1520
+ appUserID: this.serverID, // In server this is a CUID, which we can assume is globaly unique even between servers
1521
+ useAmazon: false,
1522
+ });
1523
+
1524
+ this.revenueCatInitialized = true;
1525
+ console.log('RevenueCat initialized successfully');
1526
+ }
1527
+
1528
+ // Sync purchases
1529
+ await RevenueCat.syncPurchases();
1530
+
1531
+ // Fetch customer info
1532
+ const customerInfo = await RevenueCat.getCustomerInfo();
1533
+
1534
+ // Apply to storage (storage handles the transformation)
1535
+ storage.getState().applyPurchases(customerInfo);
1536
+
1537
+ } catch (error) {
1538
+ console.error('Failed to sync purchases:', error);
1539
+ // Don't throw - purchases are optional
1540
+ }
1541
+ }
1542
+
1543
+ private flushOutbox = async (sessionId: string) => {
1544
+ const pending = this.pendingOutbox.get(sessionId);
1545
+ if (!pending || pending.length === 0) {
1546
+ if (!this.hasPendingOutboxMessages()) {
1547
+ this.clearBackgroundSendWatchdog();
1548
+ await this.cancelBackgroundSendTimeoutNotification();
1549
+ this.backgroundSendStartedAt = null;
1550
+ }
1551
+ return;
1552
+ }
1553
+
1554
+ const batch = pending.slice();
1555
+ const controller = new AbortController();
1556
+ this.sendAbortControllers.set(sessionId, controller);
1557
+ try {
1558
+ const response = await apiSocket.request(`/v3/sessions/${sessionId}/messages`, {
1559
+ method: 'POST',
1560
+ body: JSON.stringify({
1561
+ messages: batch.map((message) => ({
1562
+ localId: message.localId,
1563
+ content: message.content
1564
+ }))
1565
+ }),
1566
+ headers: {
1567
+ 'Content-Type': 'application/json'
1568
+ },
1569
+ signal: controller.signal
1570
+ });
1571
+ if (!response.ok) {
1572
+ throw new Error(`Failed to send messages for ${sessionId}: ${response.status}`);
1573
+ }
1574
+
1575
+ const data = await response.json() as V3PostSessionMessagesResponse;
1576
+ pending.splice(0, batch.length);
1577
+ if (Array.isArray(data.messages) && data.messages.length > 0) {
1578
+ const currentLastSeq = this.sessionLastSeq.get(sessionId) ?? 0;
1579
+ let maxSeq = currentLastSeq;
1580
+ for (const message of data.messages) {
1581
+ if (message.seq > maxSeq) {
1582
+ maxSeq = message.seq;
1583
+ }
1584
+ }
1585
+ this.sessionLastSeq.set(sessionId, maxSeq);
1586
+ }
1587
+ } catch (error) {
1588
+ this.maybeStartBackgroundSendWatchdog();
1589
+ throw error;
1590
+ } finally {
1591
+ this.sendAbortControllers.delete(sessionId);
1592
+ }
1593
+
1594
+ if (pending.length === 0) {
1595
+ this.pendingOutbox.delete(sessionId);
1596
+ }
1597
+ if (!this.hasPendingOutboxMessages()) {
1598
+ this.clearBackgroundSendWatchdog();
1599
+ await this.cancelBackgroundSendTimeoutNotification();
1600
+ this.backgroundSendStartedAt = null;
1601
+ } else if (this.appState !== 'active') {
1602
+ this.maybeStartBackgroundSendWatchdog();
1603
+ }
1604
+ }
1605
+
1606
+ private fetchMessages = async (sessionId: string) => {
1607
+ log.log(`💬 fetchMessages starting for session ${sessionId} - acquiring lock`);
1608
+ const lock = this.getSessionMessageLock(sessionId);
1609
+ await lock.inLock(async () => {
1610
+ const encryption = this.encryption.getSessionEncryption(sessionId);
1611
+ if (!encryption) {
1612
+ log.log(`💬 fetchMessages: Session encryption not ready for ${sessionId}, will retry`);
1613
+ throw new Error(`Session encryption not ready for ${sessionId}`);
1614
+ }
1615
+
1616
+ const knownLastSeq = this.sessionLastSeq.get(sessionId);
1617
+ const isInitialLoad = knownLastSeq === undefined;
1618
+ if (isInitialLoad) {
1619
+ // Initial load. Pull only the most recent page so the user can
1620
+ // start chatting immediately. Older history streams in lazily
1621
+ // through loadOlderMessages() when the user scrolls up — and
1622
+ // also through a background prefetch kicked off below, so the
1623
+ // history fills in even when the user doesn't scroll.
1624
+ //
1625
+ // Previously this method walked forward from seq=0 until every
1626
+ // page had been fetched and decrypted, which blocked the chat
1627
+ // from displaying anything for sessions with thousands of
1628
+ // messages. The user's reported pain point was "opening a long
1629
+ // session feels frozen" — this is the fix.
1630
+ await this.fetchInitialLatestPage(sessionId, encryption);
1631
+ } else {
1632
+ // Forward incremental sync. Used after reconnect, invalidate,
1633
+ // or any subsequent visit. Only pulls messages newer than what
1634
+ // we already have, so it's bounded and fast in normal use.
1635
+ await this.fetchForwardSince(sessionId, encryption, knownLastSeq);
1636
+ }
1637
+
1638
+ storage.getState().applyMessagesLoaded(sessionId);
1639
+ log.log(`💬 fetchMessages completed for session ${sessionId}`);
1640
+
1641
+ if (isInitialLoad) {
1642
+ // Fire-and-forget. The chat is interactive at this point;
1643
+ // background pages stream in without blocking either the
1644
+ // surrounding lock or the UI. loadOlderMessages takes the
1645
+ // same lock internally, so the loop naturally serialises
1646
+ // with on-scroll triggers and live socket updates.
1647
+ void this.prefetchOlderMessagesInBackground(sessionId);
1648
+ }
1649
+ });
1650
+ }
1651
+
1652
+ private prefetchOlderMessagesInBackground = async (sessionId: string) => {
1653
+ const SLEEP_BETWEEN_PAGES_MS = 250;
1654
+ // While loadOlderMessages handles the actual work, this loop is what
1655
+ // keeps it going without user input. We keep stepping until either:
1656
+ // - the server says there is no more older history, or
1657
+ // - the session is no longer present in the store (user navigated
1658
+ // away and the session was unloaded), or
1659
+ // - we hit seq = 1 (the very first message), or
1660
+ // - the encryption key is gone (logged out).
1661
+ // The loop yields between pages to keep the UI thread responsive
1662
+ // and to spread out server load.
1663
+ while (true) {
1664
+ const sessionMessages = storage.getState().sessionMessages[sessionId];
1665
+ if (!sessionMessages || !sessionMessages.hasMoreOlder) {
1666
+ return;
1667
+ }
1668
+ if (!this.encryption.getSessionEncryption(sessionId)) {
1669
+ return;
1670
+ }
1671
+ const oldestSeq = this.sessionOldestSeq.get(sessionId);
1672
+ if (oldestSeq === undefined || oldestSeq <= 1) {
1673
+ return;
1674
+ }
1675
+
1676
+ try {
1677
+ await this.loadOlderMessages(sessionId);
1678
+ } catch (error) {
1679
+ log.log(`💬 prefetchOlderMessagesInBackground: error for ${sessionId}, stopping: ${String(error)}`);
1680
+ return;
1681
+ }
1682
+
1683
+ await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_PAGES_MS));
1684
+ }
1685
+ }
1686
+
1687
+ private fetchInitialLatestPage = async (
1688
+ sessionId: string,
1689
+ _encryption: ReturnType<Encryption['getSessionEncryption']> & {}
1690
+ ) => {
1691
+ const API_ENDPOINT = getServerUrl();
1692
+ const response = await fetch(`${API_ENDPOINT}/api/sessions/${sessionId}/messages`);
1693
+ if (!response.ok) {
1694
+ throw new Error(`Failed to fetch initial page for ${sessionId}: ${response.status}`);
1695
+ }
1696
+ const data = await response.json();
1697
+ const messages: Array<{ id: string | number; role: string; content: unknown; created_at?: string | number }> =
1698
+ Array.isArray(data.messages) ? data.messages : [];
1699
+
1700
+ const normalized = messages
1701
+ .map((msg, idx) => this.mapNasTechRawMsg(msg, idx))
1702
+ .filter(Boolean) as NormalizedMessage[];
1703
+
1704
+ if (normalized.length > 0) {
1705
+ this.applyMessages(sessionId, normalized);
1706
+ }
1707
+
1708
+ // NasTech returns all messages at once — no pagination needed
1709
+ this.sessionLastSeq.set(sessionId, messages.length);
1710
+ if (messages.length > 0) {
1711
+ this.sessionOldestSeq.set(sessionId, 1);
1712
+ }
1713
+ storage.getState().applyOlderMessagesPagination(sessionId, { hasMore: false });
1714
+ }
1715
+
1716
+ private fetchForwardSince = async (
1717
+ sessionId: string,
1718
+ _encryption: ReturnType<Encryption['getSessionEncryption']> & {},
1719
+ fromSeq: number
1720
+ ) => {
1721
+ const API_ENDPOINT = getServerUrl();
1722
+ const response = await fetch(`${API_ENDPOINT}/api/sessions/${sessionId}/messages`);
1723
+ if (!response.ok) {
1724
+ throw new Error(`Failed to forward-sync ${sessionId}: ${response.status}`);
1725
+ }
1726
+ const data = await response.json();
1727
+ const allMessages: Array<{ id: string | number; role: string; content: unknown; created_at?: string | number }> =
1728
+ Array.isArray(data.messages) ? data.messages : [];
1729
+
1730
+ // Only apply messages newer than what we already have (by index)
1731
+ const newMessages = allMessages.slice(fromSeq);
1732
+ if (newMessages.length > 0) {
1733
+ const normalized = newMessages
1734
+ .map((msg, idx) => this.mapNasTechRawMsg(msg, fromSeq + idx))
1735
+ .filter(Boolean) as NormalizedMessage[];
1736
+ if (normalized.length > 0) {
1737
+ this.applyMessages(sessionId, normalized);
1738
+ }
1739
+ }
1740
+ this.sessionLastSeq.set(sessionId, allMessages.length);
1741
+ }
1742
+
1743
+ private mapNasTechRawMsg = (
1744
+ msg: { id: string | number; role: string; content: unknown; created_at?: string | number },
1745
+ fallbackSeq: number
1746
+ ): NormalizedMessage | null => {
1747
+ const id = String(msg.id ?? fallbackSeq);
1748
+ const rawTs = msg.created_at;
1749
+ const createdAt = rawTs
1750
+ ? (typeof rawTs === 'number' ? rawTs * 1000 : new Date(rawTs as string).getTime())
1751
+ : Date.now();
1752
+ const text = typeof msg.content === 'string'
1753
+ ? msg.content
1754
+ : msg.content != null ? JSON.stringify(msg.content) : '';
1755
+ if (!text) return null;
1756
+
1757
+ if (msg.role === 'user') {
1758
+ return {
1759
+ id,
1760
+ localId: null,
1761
+ createdAt,
1762
+ isSidechain: false,
1763
+ role: 'user',
1764
+ content: { type: 'text', text },
1765
+ };
1766
+ }
1767
+ // assistant / tool / system → render as agent text bubble
1768
+ return {
1769
+ id,
1770
+ localId: null,
1771
+ createdAt,
1772
+ isSidechain: false,
1773
+ role: 'agent',
1774
+ content: [{ type: 'text', text, uuid: id, parentUUID: null }],
1775
+ };
1776
+ }
1777
+
1778
+ private applyFetchedMessages = async (
1779
+ sessionId: string,
1780
+ encryption: ReturnType<Encryption['getSessionEncryption']> & {},
1781
+ messages: ApiMessage[]
1782
+ ) => {
1783
+ if (messages.length === 0) return;
1784
+ const decryptedMessages = await encryption.decryptMessages(messages);
1785
+ const normalizedMessages: NormalizedMessage[] = [];
1786
+ for (let i = 0; i < decryptedMessages.length; i++) {
1787
+ const decrypted = decryptedMessages[i];
1788
+ if (!decrypted) continue;
1789
+ const normalized = normalizeRawMessage(decrypted.id, decrypted.localId, decrypted.createdAt, decrypted.content);
1790
+ if (normalized) {
1791
+ normalizedMessages.push(normalized);
1792
+ }
1793
+ }
1794
+ if (normalizedMessages.length > 0) {
1795
+ this.applyMessages(sessionId, normalizedMessages);
1796
+ }
1797
+ }
1798
+
1799
+ /**
1800
+ * Fetch one page of older messages for a session and prepend them to the
1801
+ * store. Called from the chat UI when the user scrolls past the top of
1802
+ * the currently loaded history. No-op when we have already fetched the
1803
+ * earliest message, when no initial fetch has happened yet, or when an
1804
+ * older-fetch is already in flight for this session.
1805
+ */
1806
+ loadOlderMessages = async (sessionId: string) => {
1807
+ const oldestSeq = this.sessionOldestSeq.get(sessionId);
1808
+ if (oldestSeq === undefined || oldestSeq <= 1) {
1809
+ return;
1810
+ }
1811
+ const sessionMessages = storage.getState().sessionMessages[sessionId];
1812
+ if (!sessionMessages || sessionMessages.isLoadingOlder || !sessionMessages.hasMoreOlder) {
1813
+ return;
1814
+ }
1815
+
1816
+ storage.getState().applyOlderMessagesLoading(sessionId, true);
1817
+ const lock = this.getSessionMessageLock(sessionId);
1818
+ try {
1819
+ await lock.inLock(async () => {
1820
+ const encryption = this.encryption.getSessionEncryption(sessionId);
1821
+ if (!encryption) {
1822
+ log.log(`💬 loadOlderMessages: encryption not ready for ${sessionId}`);
1823
+ return;
1824
+ }
1825
+ // Re-read the cursor inside the lock. A concurrent
1826
+ // socket-pushed update or reload could have changed it.
1827
+ const beforeSeq = this.sessionOldestSeq.get(sessionId);
1828
+ if (beforeSeq === undefined || beforeSeq <= 1) {
1829
+ return;
1830
+ }
1831
+ // NasTech loads all messages at once — no older-page pagination needed
1832
+ storage.getState().applyOlderMessagesPagination(sessionId, { hasMore: false });
1833
+ });
1834
+ } finally {
1835
+ storage.getState().applyOlderMessagesLoading(sessionId, false);
1836
+ }
1837
+ }
1838
+
1839
+ private registerPushToken = async () => {
1840
+ log.log('registerPushToken');
1841
+ try {
1842
+ const result = await syncCurrentPushToken(this.credentials);
1843
+ log.log('Push token sync result: ' + JSON.stringify({
1844
+ registered: result.registered,
1845
+ hasToken: !!result.token,
1846
+ permission: result.permission.status,
1847
+ }));
1848
+ if (!result.permission.granted) {
1849
+ console.log('Failed to get push token for push notification!');
1850
+ }
1851
+ } catch (error) {
1852
+ log.log('Failed to register push token: ' + JSON.stringify(error));
1853
+ }
1854
+ }
1855
+
1856
+ private subscribeToUpdates = () => {
1857
+ // Connect to NasTech backend WebSocket for real-time session/message updates
1858
+ const connectNasTechWs = () => {
1859
+ const serverUrl = getServerUrl().replace(/^http/, 'ws');
1860
+ let ws: WebSocket | null = null;
1861
+ try {
1862
+ ws = new WebSocket(`${serverUrl}/api/ws`);
1863
+ ws.onmessage = (_e) => {
1864
+ this.sessionsSync.invalidate();
1865
+ };
1866
+ ws.onclose = () => {
1867
+ setTimeout(connectNasTechWs, 3000);
1868
+ };
1869
+ ws.onerror = () => {
1870
+ ws?.close();
1871
+ };
1872
+ } catch {
1873
+ setTimeout(connectNasTechWs, 5000);
1874
+ }
1875
+ };
1876
+ connectNasTechWs();
1877
+
1878
+ // Polling fallback: refresh session list every 10 seconds
1879
+ setInterval(() => {
1880
+ this.sessionsSync.invalidate();
1881
+ }, 10000);
1882
+ }
1883
+
1884
+ private handleUpdate = async (update: unknown) => {
1885
+ const validatedUpdate = ApiUpdateContainerSchema.safeParse(update);
1886
+ if (!validatedUpdate.success) {
1887
+ console.log('❌ Sync: Invalid update received:', validatedUpdate.error);
1888
+ console.error('❌ Sync: Invalid update data:', update);
1889
+ return;
1890
+ }
1891
+ const updateData = validatedUpdate.data;
1892
+ console.log(`🔄 Sync: Validated update type: ${updateData.body.t}`);
1893
+
1894
+ if (updateData.body.t === 'new-message') {
1895
+
1896
+ // Get encryption — may not be ready if sessions are still syncing
1897
+ let encryption = this.encryption.getSessionEncryption(updateData.body.sid);
1898
+ if (!encryption) {
1899
+ await this.sessionsSync.awaitQueue();
1900
+ encryption = this.encryption.getSessionEncryption(updateData.body.sid);
1901
+ if (!encryption) {
1902
+ console.error(`Session ${updateData.body.sid} not found after sync`);
1903
+ this.fetchSessions();
1904
+ return;
1905
+ }
1906
+ }
1907
+
1908
+ // Decrypt message
1909
+ let lastMessage: NormalizedMessage | null = null;
1910
+ if (updateData.body.message) {
1911
+ const decrypted = await encryption.decryptMessage(updateData.body.message);
1912
+ if (decrypted) {
1913
+ lastMessage = normalizeRawMessage(decrypted.id, decrypted.localId, decrypted.createdAt, decrypted.content);
1914
+
1915
+ // Check for task lifecycle events to update thinking state
1916
+ // This ensures UI updates even if volatile activity updates are lost
1917
+ const rawContent = decrypted.content as {
1918
+ role?: string;
1919
+ content?: {
1920
+ type?: string;
1921
+ data?: {
1922
+ type?: string;
1923
+ ev?: { t?: string };
1924
+ }
1925
+ }
1926
+ } | null;
1927
+ const contentType = rawContent?.content?.type;
1928
+ const dataType = rawContent?.content?.data?.type;
1929
+ const sessionEventType = rawContent?.content?.data?.ev?.t;
1930
+
1931
+ // Debug logging to trace lifecycle events
1932
+ if (dataType === 'task_complete' || dataType === 'turn_aborted' || dataType === 'task_started' || sessionEventType === 'turn-start' || sessionEventType === 'turn-end') {
1933
+ console.log(`🔄 [Sync] Lifecycle event detected: contentType=${contentType}, dataType=${dataType}, sessionEventType=${sessionEventType}`);
1934
+ }
1935
+
1936
+ const isTaskComplete =
1937
+ ((contentType === 'acp' || contentType === 'codex') &&
1938
+ (dataType === 'task_complete' || dataType === 'turn_aborted')) ||
1939
+ (contentType === 'session' && sessionEventType === 'turn-end');
1940
+
1941
+ const isTaskStarted =
1942
+ ((contentType === 'acp' || contentType === 'codex') && dataType === 'task_started') ||
1943
+ (contentType === 'session' && sessionEventType === 'turn-start');
1944
+
1945
+ if (isTaskComplete || isTaskStarted) {
1946
+ console.log(`🔄 [Sync] Updating thinking state: isTaskComplete=${isTaskComplete}, isTaskStarted=${isTaskStarted}`);
1947
+ }
1948
+
1949
+ // Update session
1950
+ const session = storage.getState().sessions[updateData.body.sid];
1951
+ if (session) {
1952
+ this.applySessions([{
1953
+ ...session,
1954
+ updatedAt: updateData.createdAt,
1955
+ seq: updateData.seq,
1956
+ // Update thinking state based on task lifecycle events
1957
+ ...(isTaskComplete ? { thinking: false } : {}),
1958
+ ...(isTaskStarted ? { thinking: true } : {})
1959
+ }])
1960
+ } else {
1961
+ // Fetch sessions again if we don't have this session
1962
+ this.fetchSessions();
1963
+ }
1964
+
1965
+ // Fast-path only on consecutive seq values, otherwise fetch from server.
1966
+ const currentLastSeq = this.sessionLastSeq.get(updateData.body.sid);
1967
+ const incomingSeq = updateData.body.message.seq;
1968
+ if (lastMessage && currentLastSeq !== undefined && incomingSeq === currentLastSeq + 1) {
1969
+ this.enqueueMessages(updateData.body.sid, [lastMessage]);
1970
+ this.sessionLastSeq.set(updateData.body.sid, incomingSeq);
1971
+ let hasMutableTool = false;
1972
+ if (lastMessage.role === 'agent' && lastMessage.content[0] && lastMessage.content[0].type === 'tool-result') {
1973
+ hasMutableTool = storage.getState().isMutableToolCall(updateData.body.sid, lastMessage.content[0].tool_use_id);
1974
+ }
1975
+ if (hasMutableTool) {
1976
+ gitStatusSync.invalidate(updateData.body.sid);
1977
+ }
1978
+ } else {
1979
+ this.getMessagesSync(updateData.body.sid).invalidate();
1980
+ }
1981
+ }
1982
+ }
1983
+
1984
+ // Ping session
1985
+ this.onSessionVisible(updateData.body.sid);
1986
+
1987
+ } else if (updateData.body.t === 'new-session') {
1988
+ log.log('🆕 New session update received');
1989
+ this.sessionsSync.invalidate();
1990
+ } else if (updateData.body.t === 'delete-session') {
1991
+ log.log('🗑️ Delete session update received');
1992
+ const sessionId = updateData.body.sid;
1993
+
1994
+ // Remove session from storage
1995
+ storage.getState().deleteSession(sessionId);
1996
+
1997
+ // Remove encryption keys from memory
1998
+ this.encryption.removeSessionEncryption(sessionId);
1999
+
2000
+ // Clear any cached git status
2001
+ gitStatusSync.clearForSession(sessionId);
2002
+ this.messagesSync.delete(sessionId);
2003
+ this.sendSync.delete(sessionId);
2004
+ this.pendingOutbox.delete(sessionId);
2005
+ this.sessionLastSeq.delete(sessionId);
2006
+ this.sessionOldestSeq.delete(sessionId);
2007
+ this.sessionMessageLocks.delete(sessionId);
2008
+ this.sessionMessageQueue.delete(sessionId);
2009
+ this.sessionQueueProcessing.delete(sessionId);
2010
+
2011
+ log.log(`🗑️ Session ${sessionId} deleted from local storage`);
2012
+ } else if (updateData.body.t === 'update-session') {
2013
+ // Session + encryption may not be initialized yet if sessions are
2014
+ // still syncing on startup. Mirror the new-message path: await the
2015
+ // sessions sync queue and re-check before giving up — dropping here
2016
+ // silently loses the metadata update that carries the chat title
2017
+ // (#1251: every chat stuck on "New chat" after the lazy-load change).
2018
+ let session = storage.getState().sessions[updateData.body.id];
2019
+ let sessionEncryption = this.encryption.getSessionEncryption(updateData.body.id);
2020
+ if (!session || !sessionEncryption) {
2021
+ await this.sessionsSync.awaitQueue();
2022
+ session = storage.getState().sessions[updateData.body.id];
2023
+ sessionEncryption = this.encryption.getSessionEncryption(updateData.body.id);
2024
+ }
2025
+ if (session) {
2026
+ if (!sessionEncryption) {
2027
+ console.error(`Session encryption not found for ${updateData.body.id} after sync`);
2028
+ this.fetchSessions();
2029
+ return;
2030
+ }
2031
+
2032
+ const agentState = updateData.body.agentState && sessionEncryption
2033
+ ? await sessionEncryption.decryptAgentState(updateData.body.agentState.version, updateData.body.agentState.value)
2034
+ : session.agentState;
2035
+ const metadata = updateData.body.metadata && sessionEncryption
2036
+ ? await sessionEncryption.decryptMetadata(updateData.body.metadata.version, updateData.body.metadata.value)
2037
+ : session.metadata;
2038
+
2039
+ this.applySessions([{
2040
+ ...session,
2041
+ agentState,
2042
+ agentStateVersion: updateData.body.agentState
2043
+ ? updateData.body.agentState.version
2044
+ : session.agentStateVersion,
2045
+ metadata,
2046
+ metadataVersion: updateData.body.metadata
2047
+ ? updateData.body.metadata.version
2048
+ : session.metadataVersion,
2049
+ updatedAt: updateData.createdAt,
2050
+ seq: updateData.seq
2051
+ }]);
2052
+
2053
+ // Invalidate git status when agent state changes (files may have been modified)
2054
+ if (updateData.body.agentState) {
2055
+ gitStatusSync.invalidate(updateData.body.id);
2056
+
2057
+ // Check for new permission requests and notify voice assistant
2058
+ if (agentState?.requests && Object.keys(agentState.requests).length > 0) {
2059
+ const requestIds = Object.keys(agentState.requests);
2060
+ const firstRequest = agentState.requests[requestIds[0]];
2061
+ const toolName = firstRequest?.tool;
2062
+ voiceHooks.onPermissionRequested(updateData.body.id, requestIds[0], toolName, firstRequest?.arguments);
2063
+ }
2064
+
2065
+ // Re-fetch messages when control returns to mobile (local -> remote mode switch)
2066
+ // This catches up on any messages that were exchanged while desktop had control
2067
+ const wasControlledByUser = session.agentState?.controlledByUser;
2068
+ const isNowControlledByUser = agentState?.controlledByUser;
2069
+ if (!wasControlledByUser && isNowControlledByUser) {
2070
+ log.log(`🔄 Control returned to mobile for session ${updateData.body.id}, re-fetching messages`);
2071
+ this.onSessionVisible(updateData.body.id);
2072
+ }
2073
+ }
2074
+ }
2075
+ } else if (updateData.body.t === 'update-account') {
2076
+ const accountUpdate = updateData.body;
2077
+ const currentProfile = storage.getState().profile;
2078
+ const hadGitHub = !!currentProfile.github?.login;
2079
+
2080
+ // Build updated profile with new data
2081
+ const updatedProfile: Profile = {
2082
+ ...currentProfile,
2083
+ firstName: accountUpdate.firstName !== undefined ? accountUpdate.firstName : currentProfile.firstName,
2084
+ lastName: accountUpdate.lastName !== undefined ? accountUpdate.lastName : currentProfile.lastName,
2085
+ avatar: accountUpdate.avatar !== undefined ? accountUpdate.avatar : currentProfile.avatar,
2086
+ github: accountUpdate.github !== undefined ? accountUpdate.github : currentProfile.github,
2087
+ timestamp: updateData.createdAt // Update timestamp to latest
2088
+ };
2089
+
2090
+ // Apply the updated profile to storage
2091
+ storage.getState().applyProfile(updatedProfile);
2092
+
2093
+ if (!hadGitHub && updatedProfile.github?.login) {
2094
+ trackGitHubConnected();
2095
+ }
2096
+
2097
+ // Handle settings updates (new for profile sync)
2098
+ if (accountUpdate.settings?.value) {
2099
+ try {
2100
+ const decryptedSettings = await this.encryption.decryptRaw(accountUpdate.settings.value);
2101
+ const parsedSettings = settingsParse(decryptedSettings);
2102
+
2103
+ // Version compatibility check
2104
+ const settingsSchemaVersion = parsedSettings.schemaVersion ?? 1;
2105
+ if (settingsSchemaVersion > SUPPORTED_SCHEMA_VERSION) {
2106
+ console.warn(
2107
+ `⚠️ Received settings schema v${settingsSchemaVersion}, ` +
2108
+ `we support v${SUPPORTED_SCHEMA_VERSION}. Update app for full functionality.`
2109
+ );
2110
+ }
2111
+
2112
+ this.applyServerSettings(parsedSettings, accountUpdate.settings.version);
2113
+ log.log(`📋 Settings synced from server (schema v${settingsSchemaVersion}, version ${accountUpdate.settings.version})`);
2114
+ } catch (error) {
2115
+ console.error('❌ Failed to process settings update:', error);
2116
+ // Don't crash on settings sync errors, just log
2117
+ }
2118
+ }
2119
+ } else if (updateData.body.t === 'update-machine') {
2120
+ const machineUpdate = updateData.body;
2121
+ const machineId = machineUpdate.machineId; // Changed from .id to .machineId
2122
+ const machine = storage.getState().machines[machineId];
2123
+
2124
+ // Create or update machine with all required fields
2125
+ const updatedMachine: Machine = {
2126
+ id: machineId,
2127
+ seq: updateData.seq,
2128
+ createdAt: machine?.createdAt ?? updateData.createdAt,
2129
+ updatedAt: updateData.createdAt,
2130
+ active: machineUpdate.active ?? true,
2131
+ activeAt: machineUpdate.activeAt ?? updateData.createdAt,
2132
+ metadata: machine?.metadata ?? null,
2133
+ metadataVersion: machine?.metadataVersion ?? 0,
2134
+ daemonState: machine?.daemonState ?? null,
2135
+ daemonStateVersion: machine?.daemonStateVersion ?? 0
2136
+ };
2137
+
2138
+ // Get machine-specific encryption (might not exist if machine wasn't initialized)
2139
+ const machineEncryption = this.encryption.getMachineEncryption(machineId);
2140
+ if (!machineEncryption) {
2141
+ console.error(`Machine encryption not found for ${machineId} - cannot decrypt updates`);
2142
+ return;
2143
+ }
2144
+
2145
+ // If metadata is provided, decrypt and update it
2146
+ const metadataUpdate = machineUpdate.metadata;
2147
+ if (metadataUpdate) {
2148
+ try {
2149
+ const metadata = await machineEncryption.decryptMetadata(metadataUpdate.version, metadataUpdate.value);
2150
+ updatedMachine.metadata = metadata;
2151
+ updatedMachine.metadataVersion = metadataUpdate.version;
2152
+ } catch (error) {
2153
+ console.error(`Failed to decrypt machine metadata for ${machineId}:`, error);
2154
+ }
2155
+ }
2156
+
2157
+ // If daemonState is provided, decrypt and update it
2158
+ const daemonStateUpdate = machineUpdate.daemonState;
2159
+ if (daemonStateUpdate) {
2160
+ try {
2161
+ const daemonState = await machineEncryption.decryptDaemonState(daemonStateUpdate.version, daemonStateUpdate.value);
2162
+ updatedMachine.daemonState = daemonState;
2163
+ updatedMachine.daemonStateVersion = daemonStateUpdate.version;
2164
+ } catch (error) {
2165
+ console.error(`Failed to decrypt machine daemonState for ${machineId}:`, error);
2166
+ }
2167
+ }
2168
+
2169
+ // Update storage using applyMachines which rebuilds sessionListViewData
2170
+ storage.getState().applyMachines([updatedMachine]);
2171
+ } else if (updateData.body.t === 'delete-machine') {
2172
+ const machineId = updateData.body.machineId;
2173
+ log.log(`🗑️ Delete machine update received for ${machineId}`);
2174
+ if (!storage.getState().machines[machineId]) {
2175
+ log.log(`Machine ${machineId} not in storage, skipping delete`);
2176
+ } else {
2177
+ storage.getState().deleteMachine(machineId);
2178
+ this.encryption.removeMachineEncryption(machineId);
2179
+ this.machineDataKeys.delete(machineId);
2180
+ }
2181
+ } else if (updateData.body.t === 'relationship-updated') {
2182
+ log.log('👥 Received relationship-updated update');
2183
+ const relationshipUpdate = updateData.body;
2184
+
2185
+ // Apply the relationship update to storage
2186
+ storage.getState().applyRelationshipUpdate({
2187
+ fromUserId: relationshipUpdate.fromUserId,
2188
+ toUserId: relationshipUpdate.toUserId,
2189
+ status: relationshipUpdate.status,
2190
+ action: relationshipUpdate.action,
2191
+ fromUser: relationshipUpdate.fromUser,
2192
+ toUser: relationshipUpdate.toUser,
2193
+ timestamp: relationshipUpdate.timestamp
2194
+ });
2195
+
2196
+ // Invalidate friends data to refresh with latest changes
2197
+ this.friendsSync.invalidate();
2198
+ this.friendRequestsSync.invalidate();
2199
+ this.feedSync.invalidate();
2200
+ } else if (updateData.body.t === 'new-artifact') {
2201
+ log.log('📦 Received new-artifact update');
2202
+ const artifactUpdate = updateData.body;
2203
+ const artifactId = artifactUpdate.artifactId;
2204
+
2205
+ try {
2206
+ // Decrypt the data encryption key
2207
+ const decryptedKey = await this.encryption.decryptEncryptionKey(artifactUpdate.dataEncryptionKey);
2208
+ if (!decryptedKey) {
2209
+ console.error(`Failed to decrypt key for new artifact ${artifactId}`);
2210
+ return;
2211
+ }
2212
+
2213
+ // Store the decrypted key in memory
2214
+ this.artifactDataKeys.set(artifactId, decryptedKey);
2215
+
2216
+ // Create artifact encryption instance
2217
+ const artifactEncryption = new ArtifactEncryption(decryptedKey);
2218
+
2219
+ // Decrypt header
2220
+ const header = await artifactEncryption.decryptHeader(artifactUpdate.header);
2221
+
2222
+ // Decrypt body if provided
2223
+ let decryptedBody: string | null | undefined = undefined;
2224
+ if (artifactUpdate.body && artifactUpdate.bodyVersion !== undefined) {
2225
+ const body = await artifactEncryption.decryptBody(artifactUpdate.body);
2226
+ decryptedBody = body?.body || null;
2227
+ }
2228
+
2229
+ // Add to storage
2230
+ const decryptedArtifact: DecryptedArtifact = {
2231
+ id: artifactId,
2232
+ title: header?.title || null,
2233
+ body: decryptedBody,
2234
+ headerVersion: artifactUpdate.headerVersion,
2235
+ bodyVersion: artifactUpdate.bodyVersion,
2236
+ seq: artifactUpdate.seq,
2237
+ createdAt: artifactUpdate.createdAt,
2238
+ updatedAt: artifactUpdate.updatedAt,
2239
+ isDecrypted: !!header,
2240
+ };
2241
+
2242
+ storage.getState().addArtifact(decryptedArtifact);
2243
+ log.log(`📦 Added new artifact ${artifactId} to storage`);
2244
+ } catch (error) {
2245
+ console.error(`Failed to process new artifact ${artifactId}:`, error);
2246
+ }
2247
+ } else if (updateData.body.t === 'update-artifact') {
2248
+ log.log('📦 Received update-artifact update');
2249
+ const artifactUpdate = updateData.body;
2250
+ const artifactId = artifactUpdate.artifactId;
2251
+
2252
+ // Get existing artifact
2253
+ const existingArtifact = storage.getState().artifacts[artifactId];
2254
+ if (!existingArtifact) {
2255
+ console.error(`Artifact ${artifactId} not found in storage`);
2256
+ // Fetch all artifacts to sync
2257
+ this.artifactsSync.invalidate();
2258
+ return;
2259
+ }
2260
+
2261
+ try {
2262
+ // Get the data encryption key from memory
2263
+ let dataEncryptionKey = this.artifactDataKeys.get(artifactId);
2264
+ if (!dataEncryptionKey) {
2265
+ console.error(`Encryption key not found for artifact ${artifactId}, fetching artifacts`);
2266
+ this.artifactsSync.invalidate();
2267
+ return;
2268
+ }
2269
+
2270
+ // Create artifact encryption instance
2271
+ const artifactEncryption = new ArtifactEncryption(dataEncryptionKey);
2272
+
2273
+ // Update artifact with new data
2274
+ const updatedArtifact: DecryptedArtifact = {
2275
+ ...existingArtifact,
2276
+ seq: updateData.seq,
2277
+ updatedAt: updateData.createdAt,
2278
+ };
2279
+
2280
+ // Decrypt and update header if provided
2281
+ if (artifactUpdate.header) {
2282
+ const header = await artifactEncryption.decryptHeader(artifactUpdate.header.value);
2283
+ updatedArtifact.title = header?.title || null;
2284
+ updatedArtifact.sessions = header?.sessions;
2285
+ updatedArtifact.draft = header?.draft;
2286
+ updatedArtifact.headerVersion = artifactUpdate.header.version;
2287
+ }
2288
+
2289
+ // Decrypt and update body if provided
2290
+ if (artifactUpdate.body) {
2291
+ const body = await artifactEncryption.decryptBody(artifactUpdate.body.value);
2292
+ updatedArtifact.body = body?.body || null;
2293
+ updatedArtifact.bodyVersion = artifactUpdate.body.version;
2294
+ }
2295
+
2296
+ storage.getState().updateArtifact(updatedArtifact);
2297
+ log.log(`📦 Updated artifact ${artifactId} in storage`);
2298
+ } catch (error) {
2299
+ console.error(`Failed to process artifact update ${artifactId}:`, error);
2300
+ }
2301
+ } else if (updateData.body.t === 'delete-artifact') {
2302
+ log.log('📦 Received delete-artifact update');
2303
+ const artifactUpdate = updateData.body;
2304
+ const artifactId = artifactUpdate.artifactId;
2305
+
2306
+ // Remove from storage
2307
+ storage.getState().deleteArtifact(artifactId);
2308
+
2309
+ // Remove encryption key from memory
2310
+ this.artifactDataKeys.delete(artifactId);
2311
+ } else if (updateData.body.t === 'new-feed-post') {
2312
+ log.log('📰 Received new-feed-post update');
2313
+ const feedUpdate = updateData.body;
2314
+
2315
+ // Convert to FeedItem with counter from cursor
2316
+ const feedItem: FeedItem = {
2317
+ id: feedUpdate.id,
2318
+ body: feedUpdate.body,
2319
+ cursor: feedUpdate.cursor,
2320
+ createdAt: feedUpdate.createdAt,
2321
+ repeatKey: feedUpdate.repeatKey,
2322
+ counter: parseInt(feedUpdate.cursor.substring(2), 10)
2323
+ };
2324
+
2325
+ // Check if we need to fetch user for friend-related items
2326
+ if (feedItem.body && (feedItem.body.kind === 'friend_request' || feedItem.body.kind === 'friend_accepted')) {
2327
+ await this.assumeUsers([feedItem.body.uid]);
2328
+
2329
+ // Check if user fetch failed (404) - don't store item if user not found
2330
+ const users = storage.getState().users;
2331
+ const userProfile = users[feedItem.body.uid];
2332
+ if (userProfile === null || userProfile === undefined) {
2333
+ // User was not found or 404, don't store this item
2334
+ log.log(`📰 Skipping feed item ${feedItem.id} - user ${feedItem.body.uid} not found`);
2335
+ return;
2336
+ }
2337
+ }
2338
+
2339
+ // Apply to storage (will handle repeatKey replacement)
2340
+ storage.getState().applyFeedItems([feedItem]);
2341
+ }
2342
+ }
2343
+
2344
+ private flushActivityUpdates = (updates: Map<string, ApiEphemeralActivityUpdate>) => {
2345
+ // log.log(`🔄 Flushing activity updates for ${updates.size} sessions - acquiring lock`);
2346
+
2347
+
2348
+ const sessions: Session[] = [];
2349
+
2350
+ for (const [sessionId, update] of updates) {
2351
+ const session = storage.getState().sessions[sessionId];
2352
+ if (session) {
2353
+ sessions.push({
2354
+ ...session,
2355
+ active: update.active,
2356
+ activeAt: update.activeAt,
2357
+ thinking: update.thinking ?? false,
2358
+ thinkingAt: update.activeAt // Always use activeAt for consistency
2359
+ });
2360
+ }
2361
+ }
2362
+
2363
+ if (sessions.length > 0) {
2364
+ // console.log('flushing activity updates ' + sessions.length);
2365
+ this.applySessions(sessions);
2366
+ // log.log(`🔄 Activity updates flushed - updated ${sessions.length} sessions`);
2367
+ }
2368
+ }
2369
+
2370
+ private handleEphemeralUpdate = (update: unknown) => {
2371
+ const validatedUpdate = ApiEphemeralUpdateSchema.safeParse(update);
2372
+ if (!validatedUpdate.success) {
2373
+ console.log('Invalid ephemeral update received:', validatedUpdate.error);
2374
+ console.error('Invalid ephemeral update received:', update);
2375
+ return;
2376
+ } else {
2377
+ // console.log('Ephemeral update received:', update);
2378
+ }
2379
+ const updateData = validatedUpdate.data;
2380
+
2381
+ // Process activity updates through smart debounce accumulator
2382
+ if (updateData.type === 'activity') {
2383
+ // console.log('adding activity update ' + updateData.id);
2384
+ this.activityAccumulator.addUpdate(updateData);
2385
+ }
2386
+
2387
+ // Handle machine activity updates
2388
+ if (updateData.type === 'machine-activity') {
2389
+ // Update machine's active status and lastActiveAt
2390
+ const machine = storage.getState().machines[updateData.id];
2391
+ if (machine) {
2392
+ const updatedMachine: Machine = {
2393
+ ...machine,
2394
+ active: updateData.active,
2395
+ activeAt: updateData.activeAt
2396
+ };
2397
+ storage.getState().applyMachines([updatedMachine]);
2398
+ }
2399
+ }
2400
+
2401
+ // Session-level lifecycle event (Claude finished, needs permission, asks question).
2402
+ // This is the same signal that triggers the mobile push — bump browser-tab
2403
+ // unread counter on these only, ignore the noisy per-message stream.
2404
+ if (updateData.type === 'session-event') {
2405
+ notifyUnreadMessage();
2406
+ }
2407
+
2408
+ // daemon-status ephemeral updates are deprecated, machine status is handled via machine-activity
2409
+ }
2410
+
2411
+ //
2412
+ // Apply store
2413
+ //
2414
+
2415
+ private applyMessages = (sessionId: string, messages: NormalizedMessage[]) => {
2416
+ const result = storage.getState().applyMessages(sessionId, messages);
2417
+ let m: Message[] = [];
2418
+ for (let messageId of result.changed) {
2419
+ const message = storage.getState().sessionMessages[sessionId].messagesMap[messageId];
2420
+ if (message) {
2421
+ m.push(message);
2422
+ }
2423
+ }
2424
+ if (m.length > 0) {
2425
+ voiceHooks.onMessages(sessionId, m);
2426
+ }
2427
+ if (result.hasReadyEvent) {
2428
+ voiceHooks.onReady(sessionId);
2429
+ }
2430
+ }
2431
+
2432
+ private applySessions = (sessions: (Omit<Session, "presence"> & {
2433
+ presence?: "online" | number;
2434
+ })[]) => {
2435
+ const active = storage.getState().getActiveSessions();
2436
+ storage.getState().applySessions(sessions);
2437
+ const newActive = storage.getState().getActiveSessions();
2438
+ this.applySessionDiff(active, newActive);
2439
+ }
2440
+
2441
+ private applySessionDiff = (active: Session[], newActive: Session[]) => {
2442
+ let wasActive = new Set(active.map(s => s.id));
2443
+ let isActive = new Set(newActive.map(s => s.id));
2444
+ for (let s of active) {
2445
+ if (!isActive.has(s.id)) {
2446
+ voiceHooks.onSessionOffline(s.id, s.metadata ?? undefined);
2447
+ }
2448
+ }
2449
+ for (let s of newActive) {
2450
+ if (!wasActive.has(s.id)) {
2451
+ voiceHooks.onSessionOnline(s.id, s.metadata ?? undefined);
2452
+ }
2453
+ }
2454
+ }
2455
+
2456
+ }
2457
+
2458
+ // Global singleton instance
2459
+ export const sync = new Sync();
2460
+
2461
+ //
2462
+ // Init sequence
2463
+ //
2464
+
2465
+ let isInitialized = false;
2466
+ export async function syncCreate(credentials: AuthCredentials) {
2467
+ if (isInitialized) {
2468
+ console.warn('Sync already initialized: ignoring');
2469
+ return;
2470
+ }
2471
+ isInitialized = true;
2472
+ await syncInit(credentials, false);
2473
+ }
2474
+
2475
+ export async function syncRestore(credentials: AuthCredentials) {
2476
+ if (isInitialized) {
2477
+ console.warn('Sync already initialized: ignoring');
2478
+ return;
2479
+ }
2480
+ isInitialized = true;
2481
+ await syncInit(credentials, true);
2482
+ }
2483
+
2484
+ async function syncInit(credentials: AuthCredentials, restore: boolean) {
2485
+
2486
+ // Initialize sync engine
2487
+ const secretKey = decodeBase64(credentials.secret, 'base64url');
2488
+ if (secretKey.length !== 32) {
2489
+ throw new Error(`Invalid secret key length: ${secretKey.length}, expected 32`);
2490
+ }
2491
+ const encryption = await Encryption.create(secretKey);
2492
+
2493
+ // Initialize tracking
2494
+ initializeTracking(encryption.anonID);
2495
+
2496
+ // Initialize socket connection
2497
+ const API_ENDPOINT = getServerUrl();
2498
+ apiSocket.initialize({ endpoint: API_ENDPOINT }, encryption);
2499
+
2500
+ // Wire socket status to storage
2501
+ apiSocket.onStatusChange((status) => {
2502
+ storage.getState().setSocketStatus(status);
2503
+ });
2504
+
2505
+ // Initialize sessions engine
2506
+ if (restore) {
2507
+ await sync.restore(credentials, encryption);
2508
+ } else {
2509
+ await sync.create(credentials, encryption);
2510
+ }
2511
+ }
2512
+
2513
+ /**
2514
+ * Initialize sync without authentication.
2515
+ * Generates or loads a persistent local device key from MMKV.
2516
+ * Used when the NasTech backend runs in open/insecure mode.
2517
+ */
2518
+ export async function syncStartNoAuth() {
2519
+ if (isInitialized) {
2520
+ console.warn('Sync already initialized: ignoring');
2521
+ return;
2522
+ }
2523
+ isInitialized = true;
2524
+
2525
+ // Load or generate a persistent 32-byte device key
2526
+ const { MMKV } = await import('react-native-mmkv');
2527
+ const keyStore = new MMKV({ id: 'nastech-device-key' });
2528
+ const DEVICE_KEY = 'device-secret';
2529
+ let secretB64 = keyStore.getString(DEVICE_KEY);
2530
+ if (!secretB64) {
2531
+ const sodium = (await import('@/encryption/libsodium.lib')).default;
2532
+ await sodium.ready;
2533
+ const key = sodium.randombytes_buf(32);
2534
+ const { encodeBase64 } = await import('@/encryption/base64');
2535
+ secretB64 = encodeBase64(key, 'base64url');
2536
+ keyStore.set(DEVICE_KEY, secretB64);
2537
+ }
2538
+
2539
+ const { decodeBase64 } = await import('@/encryption/base64');
2540
+ const secretKey = decodeBase64(secretB64, 'base64url');
2541
+ const encryption = await Encryption.create(secretKey);
2542
+
2543
+ initializeTracking(encryption.anonID);
2544
+
2545
+ const API_ENDPOINT = getServerUrl();
2546
+ apiSocket.initialize({ endpoint: API_ENDPOINT }, encryption);
2547
+
2548
+ apiSocket.onStatusChange((status) => {
2549
+ storage.getState().setSocketStatus(status);
2550
+ });
2551
+
2552
+ const fakeCredentials: AuthCredentials = { token: '', secret: secretB64 };
2553
+ await sync.restore(fakeCredentials, encryption);
2554
+ }
2555
+