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.
- package/.claude/agents/i18n-translator.md +119 -0
- package/.claude/settings.json +8 -0
- package/.eas/workflows/ota.yaml +9 -0
- package/.eas/workflows/preview.yaml +12 -0
- package/.easignore +12 -0
- package/.github/workflows/eas-build.yml +24 -0
- package/CHANGELOG.md +117 -0
- package/CLAUDE.md +413 -0
- package/GoogleService-Info.plist +30 -0
- package/LICENSE +21 -0
- package/NasTechapp.md +383 -0
- package/README.md +75 -0
- package/Stores.md +85 -0
- package/TERMS.md +83 -0
- package/app.config.js +153 -0
- package/babel.config.js +28 -0
- package/deploy/nastech-app.yaml +51 -0
- package/docs/marketing/README-creators.md +73 -0
- package/eas-build-post-install.sh +11 -0
- package/eas-build-pre-install.sh +27 -0
- package/eas.json +78 -0
- package/google-services.json +67 -0
- package/index.ts +3 -0
- package/logo.png +0 -0
- package/metro.config.js +54 -0
- package/nativewind-env.d.ts +1 -0
- package/package.json +233 -0
- package/patches/.keep +0 -0
- package/plugins/withEinkCompatibility.js +156 -0
- package/public/.well-known/apple-app-site-association +22 -0
- package/public/.well-known/assetlinks.json +12 -0
- package/public/canvaskit.wasm +0 -0
- package/public/favicon-active.ico +0 -0
- package/release-dev.sh +7 -0
- package/release-production.sh +3 -0
- package/release.cjs +160 -0
- package/sources/-session/SessionView.tsx +944 -0
- package/sources/-session/sessionOverlayNav.ts +34 -0
- package/sources/app/(app)/_layout.tsx +321 -0
- package/sources/app/(app)/artifacts/[id].tsx +279 -0
- package/sources/app/(app)/artifacts/edit/[id].tsx +318 -0
- package/sources/app/(app)/artifacts/index.tsx +264 -0
- package/sources/app/(app)/artifacts/new.tsx +219 -0
- package/sources/app/(app)/changelog.tsx +113 -0
- package/sources/app/(app)/dev/colors.tsx +197 -0
- package/sources/app/(app)/dev/device-info.tsx +183 -0
- package/sources/app/(app)/dev/expo-constants.tsx +394 -0
- package/sources/app/(app)/dev/index.tsx +400 -0
- package/sources/app/(app)/dev/input-styles.tsx +1951 -0
- package/sources/app/(app)/dev/inverted-list.tsx +295 -0
- package/sources/app/(app)/dev/list-demo.tsx +125 -0
- package/sources/app/(app)/dev/logs.tsx +160 -0
- package/sources/app/(app)/dev/messages-demo-data.ts +479 -0
- package/sources/app/(app)/dev/messages-demo.tsx +45 -0
- package/sources/app/(app)/dev/modal-demo.tsx +211 -0
- package/sources/app/(app)/dev/multi-text-input.tsx +224 -0
- package/sources/app/(app)/dev/purchases.tsx +228 -0
- package/sources/app/(app)/dev/qr-test.tsx +168 -0
- package/sources/app/(app)/dev/session-composer.tsx +812 -0
- package/sources/app/(app)/dev/shimmer-demo.tsx +275 -0
- package/sources/app/(app)/dev/tests.tsx +203 -0
- package/sources/app/(app)/dev/tools2.tsx +556 -0
- package/sources/app/(app)/dev/typography.tsx +177 -0
- package/sources/app/(app)/dev/unistyles-demo.tsx +376 -0
- package/sources/app/(app)/friends/index.tsx +167 -0
- package/sources/app/(app)/friends/search.tsx +232 -0
- package/sources/app/(app)/inbox/index.tsx +124 -0
- package/sources/app/(app)/index.tsx +264 -0
- package/sources/app/(app)/machine/[id].tsx +646 -0
- package/sources/app/(app)/new/index.tsx +1611 -0
- package/sources/app/(app)/restore/index.tsx +167 -0
- package/sources/app/(app)/restore/manual.tsx +138 -0
- package/sources/app/(app)/server.tsx +234 -0
- package/sources/app/(app)/session/[id]/file.tsx +527 -0
- package/sources/app/(app)/session/[id]/files.tsx +442 -0
- package/sources/app/(app)/session/[id]/info.tsx +655 -0
- package/sources/app/(app)/session/[id]/message/[messageId].tsx +125 -0
- package/sources/app/(app)/session/[id].tsx +10 -0
- package/sources/app/(app)/session/recent.tsx +270 -0
- package/sources/app/(app)/settings/account.tsx +600 -0
- package/sources/app/(app)/settings/agents.tsx +180 -0
- package/sources/app/(app)/settings/appearance.tsx +259 -0
- package/sources/app/(app)/settings/connect/claude.tsx +178 -0
- package/sources/app/(app)/settings/features.tsx +177 -0
- package/sources/app/(app)/settings/index.tsx +3 -0
- package/sources/app/(app)/settings/language.tsx +106 -0
- package/sources/app/(app)/settings/usage.tsx +11 -0
- package/sources/app/(app)/settings/voice/language.tsx +114 -0
- package/sources/app/(app)/settings/voice.tsx +274 -0
- package/sources/app/(app)/terminal/connect.tsx +241 -0
- package/sources/app/(app)/terminal/index.tsx +184 -0
- package/sources/app/(app)/text-selection.tsx +149 -0
- package/sources/app/(app)/user/[id].tsx +314 -0
- package/sources/app/+html.tsx +39 -0
- package/sources/app/_layout.tsx +402 -0
- package/sources/assets/animations/game.json +1 -0
- package/sources/assets/animations/owl.json +1 -0
- package/sources/assets/animations/popcorn.json +1 -0
- package/sources/assets/animations/robot.json +1 -0
- package/sources/assets/animations/sparkles.json +1 -0
- package/sources/assets/animations/stone.json +1 -0
- package/sources/assets/fonts/BricolageGrotesque-Bold.ttf +0 -0
- package/sources/assets/fonts/IBMPlexMono-Italic.ttf +0 -0
- package/sources/assets/fonts/IBMPlexMono-Regular.ttf +0 -0
- package/sources/assets/fonts/IBMPlexMono-SemiBold.ttf +0 -0
- package/sources/assets/fonts/IBMPlexSans-Italic.ttf +0 -0
- package/sources/assets/fonts/IBMPlexSans-Regular.ttf +0 -0
- package/sources/assets/fonts/IBMPlexSans-SemiBold.ttf +0 -0
- package/sources/assets/fonts/SpaceMono-Regular.ttf +0 -0
- package/sources/assets/images/brutalist/Abstract-1.png +0 -0
- package/sources/assets/images/brutalist/Abstract-10.png +0 -0
- package/sources/assets/images/brutalist/Abstract-100.png +0 -0
- package/sources/assets/images/brutalist/Abstract-101.png +0 -0
- package/sources/assets/images/brutalist/Abstract-102.png +0 -0
- package/sources/assets/images/brutalist/Abstract-103.png +0 -0
- package/sources/assets/images/brutalist/Abstract-104.png +0 -0
- package/sources/assets/images/brutalist/Abstract-105.png +0 -0
- package/sources/assets/images/brutalist/Abstract-106.png +0 -0
- package/sources/assets/images/brutalist/Abstract-107.png +0 -0
- package/sources/assets/images/brutalist/Abstract-108.png +0 -0
- package/sources/assets/images/brutalist/Abstract-109.png +0 -0
- package/sources/assets/images/brutalist/Abstract-11.png +0 -0
- package/sources/assets/images/brutalist/Abstract-110.png +0 -0
- package/sources/assets/images/brutalist/Abstract-111.png +0 -0
- package/sources/assets/images/brutalist/Abstract-112.png +0 -0
- package/sources/assets/images/brutalist/Abstract-113.png +0 -0
- package/sources/assets/images/brutalist/Abstract-114.png +0 -0
- package/sources/assets/images/brutalist/Abstract-115.png +0 -0
- package/sources/assets/images/brutalist/Abstract-116.png +0 -0
- package/sources/assets/images/brutalist/Abstract-117.png +0 -0
- package/sources/assets/images/brutalist/Abstract-118.png +0 -0
- package/sources/assets/images/brutalist/Abstract-119.png +0 -0
- package/sources/assets/images/brutalist/Abstract-12.png +0 -0
- package/sources/assets/images/brutalist/Abstract-120.png +0 -0
- package/sources/assets/images/brutalist/Abstract-121.png +0 -0
- package/sources/assets/images/brutalist/Abstract-122.png +0 -0
- package/sources/assets/images/brutalist/Abstract-123.png +0 -0
- package/sources/assets/images/brutalist/Abstract-124.png +0 -0
- package/sources/assets/images/brutalist/Abstract-125.png +0 -0
- package/sources/assets/images/brutalist/Abstract-126.png +0 -0
- package/sources/assets/images/brutalist/Abstract-127.png +0 -0
- package/sources/assets/images/brutalist/Abstract-128.png +0 -0
- package/sources/assets/images/brutalist/Abstract-129.png +0 -0
- package/sources/assets/images/brutalist/Abstract-13.png +0 -0
- package/sources/assets/images/brutalist/Abstract-130.png +0 -0
- package/sources/assets/images/brutalist/Abstract-131.png +0 -0
- package/sources/assets/images/brutalist/Abstract-132.png +0 -0
- package/sources/assets/images/brutalist/Abstract-133.png +0 -0
- package/sources/assets/images/brutalist/Abstract-134.png +0 -0
- package/sources/assets/images/brutalist/Abstract-135.png +0 -0
- package/sources/assets/images/brutalist/Abstract-136.png +0 -0
- package/sources/assets/images/brutalist/Abstract-137.png +0 -0
- package/sources/assets/images/brutalist/Abstract-138.png +0 -0
- package/sources/assets/images/brutalist/Abstract-139.png +0 -0
- package/sources/assets/images/brutalist/Abstract-14.png +0 -0
- package/sources/assets/images/brutalist/Abstract-140.png +0 -0
- package/sources/assets/images/brutalist/Abstract-141.png +0 -0
- package/sources/assets/images/brutalist/Abstract-142.png +0 -0
- package/sources/assets/images/brutalist/Abstract-143.png +0 -0
- package/sources/assets/images/brutalist/Abstract-144.png +0 -0
- package/sources/assets/images/brutalist/Abstract-145.png +0 -0
- package/sources/assets/images/brutalist/Abstract-146.png +0 -0
- package/sources/assets/images/brutalist/Abstract-147.png +0 -0
- package/sources/assets/images/brutalist/Abstract-148.png +0 -0
- package/sources/assets/images/brutalist/Abstract-149.png +0 -0
- package/sources/assets/images/brutalist/Abstract-15.png +0 -0
- package/sources/assets/images/brutalist/Abstract-150.png +0 -0
- package/sources/assets/images/brutalist/Abstract-151.png +0 -0
- package/sources/assets/images/brutalist/Abstract-152.png +0 -0
- package/sources/assets/images/brutalist/Abstract-153.png +0 -0
- package/sources/assets/images/brutalist/Abstract-154.png +0 -0
- package/sources/assets/images/brutalist/Abstract-155.png +0 -0
- package/sources/assets/images/brutalist/Abstract-156.png +0 -0
- package/sources/assets/images/brutalist/Abstract-157.png +0 -0
- package/sources/assets/images/brutalist/Abstract-158.png +0 -0
- package/sources/assets/images/brutalist/Abstract-159.png +0 -0
- package/sources/assets/images/brutalist/Abstract-16.png +0 -0
- package/sources/assets/images/brutalist/Abstract-160.png +0 -0
- package/sources/assets/images/brutalist/Abstract-161.png +0 -0
- package/sources/assets/images/brutalist/Abstract-162.png +0 -0
- package/sources/assets/images/brutalist/Abstract-163.png +0 -0
- package/sources/assets/images/brutalist/Abstract-164.png +0 -0
- package/sources/assets/images/brutalist/Abstract-165.png +0 -0
- package/sources/assets/images/brutalist/Abstract-166.png +0 -0
- package/sources/assets/images/brutalist/Abstract-167.png +0 -0
- package/sources/assets/images/brutalist/Abstract-168.png +0 -0
- package/sources/assets/images/brutalist/Abstract-169.png +0 -0
- package/sources/assets/images/brutalist/Abstract-17.png +0 -0
- package/sources/assets/images/brutalist/Abstract-170.png +0 -0
- package/sources/assets/images/brutalist/Abstract-171.png +0 -0
- package/sources/assets/images/brutalist/Abstract-172.png +0 -0
- package/sources/assets/images/brutalist/Abstract-173.png +0 -0
- package/sources/assets/images/brutalist/Abstract-174.png +0 -0
- package/sources/assets/images/brutalist/Abstract-175.png +0 -0
- package/sources/assets/images/brutalist/Abstract-176.png +0 -0
- package/sources/assets/images/brutalist/Abstract-177.png +0 -0
- package/sources/assets/images/brutalist/Abstract-178.png +0 -0
- package/sources/assets/images/brutalist/Abstract-179.png +0 -0
- package/sources/assets/images/brutalist/Abstract-18.png +0 -0
- package/sources/assets/images/brutalist/Abstract-180.png +0 -0
- package/sources/assets/images/brutalist/Abstract-181.png +0 -0
- package/sources/assets/images/brutalist/Abstract-182.png +0 -0
- package/sources/assets/images/brutalist/Abstract-183.png +0 -0
- package/sources/assets/images/brutalist/Abstract-184.png +0 -0
- package/sources/assets/images/brutalist/Abstract-185.png +0 -0
- package/sources/assets/images/brutalist/Abstract-186.png +0 -0
- package/sources/assets/images/brutalist/Abstract-187.png +0 -0
- package/sources/assets/images/brutalist/Abstract-188.png +0 -0
- package/sources/assets/images/brutalist/Abstract-189.png +0 -0
- package/sources/assets/images/brutalist/Abstract-19.png +0 -0
- package/sources/assets/images/brutalist/Abstract-190.png +0 -0
- package/sources/assets/images/brutalist/Abstract-191.png +0 -0
- package/sources/assets/images/brutalist/Abstract-192.png +0 -0
- package/sources/assets/images/brutalist/Abstract-193.png +0 -0
- package/sources/assets/images/brutalist/Abstract-194.png +0 -0
- package/sources/assets/images/brutalist/Abstract-195.png +0 -0
- package/sources/assets/images/brutalist/Abstract-196.png +0 -0
- package/sources/assets/images/brutalist/Abstract-197.png +0 -0
- package/sources/assets/images/brutalist/Abstract-198.png +0 -0
- package/sources/assets/images/brutalist/Abstract-199.png +0 -0
- package/sources/assets/images/brutalist/Abstract-2.png +0 -0
- package/sources/assets/images/brutalist/Abstract-20.png +0 -0
- package/sources/assets/images/brutalist/Abstract-200.png +0 -0
- package/sources/assets/images/brutalist/Abstract-201.png +0 -0
- package/sources/assets/images/brutalist/Abstract-202.png +0 -0
- package/sources/assets/images/brutalist/Abstract-203.png +0 -0
- package/sources/assets/images/brutalist/Abstract-204.png +0 -0
- package/sources/assets/images/brutalist/Abstract-205.png +0 -0
- package/sources/assets/images/brutalist/Abstract-206.png +0 -0
- package/sources/assets/images/brutalist/Abstract-207.png +0 -0
- package/sources/assets/images/brutalist/Abstract-208.png +0 -0
- package/sources/assets/images/brutalist/Abstract-209.png +0 -0
- package/sources/assets/images/brutalist/Abstract-21.png +0 -0
- package/sources/assets/images/brutalist/Abstract-210.png +0 -0
- package/sources/assets/images/brutalist/Abstract-211.png +0 -0
- package/sources/assets/images/brutalist/Abstract-212.png +0 -0
- package/sources/assets/images/brutalist/Abstract-213.png +0 -0
- package/sources/assets/images/brutalist/Abstract-214.png +0 -0
- package/sources/assets/images/brutalist/Abstract-215.png +0 -0
- package/sources/assets/images/brutalist/Abstract-216.png +0 -0
- package/sources/assets/images/brutalist/Abstract-217.png +0 -0
- package/sources/assets/images/brutalist/Abstract-218.png +0 -0
- package/sources/assets/images/brutalist/Abstract-219.png +0 -0
- package/sources/assets/images/brutalist/Abstract-22.png +0 -0
- package/sources/assets/images/brutalist/Abstract-220.png +0 -0
- package/sources/assets/images/brutalist/Abstract-221.png +0 -0
- package/sources/assets/images/brutalist/Abstract-222.png +0 -0
- package/sources/assets/images/brutalist/Abstract-223.png +0 -0
- package/sources/assets/images/brutalist/Abstract-224.png +0 -0
- package/sources/assets/images/brutalist/Abstract-225.png +0 -0
- package/sources/assets/images/brutalist/Abstract-226.png +0 -0
- package/sources/assets/images/brutalist/Abstract-227.png +0 -0
- package/sources/assets/images/brutalist/Abstract-228.png +0 -0
- package/sources/assets/images/brutalist/Abstract-229.png +0 -0
- package/sources/assets/images/brutalist/Abstract-23.png +0 -0
- package/sources/assets/images/brutalist/Abstract-230.png +0 -0
- package/sources/assets/images/brutalist/Abstract-231.png +0 -0
- package/sources/assets/images/brutalist/Abstract-232.png +0 -0
- package/sources/assets/images/brutalist/Abstract-233.png +0 -0
- package/sources/assets/images/brutalist/Abstract-234.png +0 -0
- package/sources/assets/images/brutalist/Abstract-235.png +0 -0
- package/sources/assets/images/brutalist/Abstract-236.png +0 -0
- package/sources/assets/images/brutalist/Abstract-237.png +0 -0
- package/sources/assets/images/brutalist/Abstract-238.png +0 -0
- package/sources/assets/images/brutalist/Abstract-239.png +0 -0
- package/sources/assets/images/brutalist/Abstract-24.png +0 -0
- package/sources/assets/images/brutalist/Abstract-240.png +0 -0
- package/sources/assets/images/brutalist/Abstract-241.png +0 -0
- package/sources/assets/images/brutalist/Abstract-242.png +0 -0
- package/sources/assets/images/brutalist/Abstract-243.png +0 -0
- package/sources/assets/images/brutalist/Abstract-244.png +0 -0
- package/sources/assets/images/brutalist/Abstract-245.png +0 -0
- package/sources/assets/images/brutalist/Abstract-246.png +0 -0
- package/sources/assets/images/brutalist/Abstract-247.png +0 -0
- package/sources/assets/images/brutalist/Abstract-248.png +0 -0
- package/sources/assets/images/brutalist/Abstract-249.png +0 -0
- package/sources/assets/images/brutalist/Abstract-25.png +0 -0
- package/sources/assets/images/brutalist/Abstract-250.png +0 -0
- package/sources/assets/images/brutalist/Abstract-251.png +0 -0
- package/sources/assets/images/brutalist/Abstract-252.png +0 -0
- package/sources/assets/images/brutalist/Abstract-253.png +0 -0
- package/sources/assets/images/brutalist/Abstract-254.png +0 -0
- package/sources/assets/images/brutalist/Abstract-255.png +0 -0
- package/sources/assets/images/brutalist/Abstract-256.png +0 -0
- package/sources/assets/images/brutalist/Abstract-257.png +0 -0
- package/sources/assets/images/brutalist/Abstract-258.png +0 -0
- package/sources/assets/images/brutalist/Abstract-259.png +0 -0
- package/sources/assets/images/brutalist/Abstract-26.png +0 -0
- package/sources/assets/images/brutalist/Abstract-260.png +0 -0
- package/sources/assets/images/brutalist/Abstract-261.png +0 -0
- package/sources/assets/images/brutalist/Abstract-262.png +0 -0
- package/sources/assets/images/brutalist/Abstract-27.png +0 -0
- package/sources/assets/images/brutalist/Abstract-28.png +0 -0
- package/sources/assets/images/brutalist/Abstract-29.png +0 -0
- package/sources/assets/images/brutalist/Abstract-3.png +0 -0
- package/sources/assets/images/brutalist/Abstract-30.png +0 -0
- package/sources/assets/images/brutalist/Abstract-31.png +0 -0
- package/sources/assets/images/brutalist/Abstract-32.png +0 -0
- package/sources/assets/images/brutalist/Abstract-33.png +0 -0
- package/sources/assets/images/brutalist/Abstract-34.png +0 -0
- package/sources/assets/images/brutalist/Abstract-35.png +0 -0
- package/sources/assets/images/brutalist/Abstract-36.png +0 -0
- package/sources/assets/images/brutalist/Abstract-37.png +0 -0
- package/sources/assets/images/brutalist/Abstract-38.png +0 -0
- package/sources/assets/images/brutalist/Abstract-39.png +0 -0
- package/sources/assets/images/brutalist/Abstract-4.png +0 -0
- package/sources/assets/images/brutalist/Abstract-40.png +0 -0
- package/sources/assets/images/brutalist/Abstract-41.png +0 -0
- package/sources/assets/images/brutalist/Abstract-42.png +0 -0
- package/sources/assets/images/brutalist/Abstract-43.png +0 -0
- package/sources/assets/images/brutalist/Abstract-44.png +0 -0
- package/sources/assets/images/brutalist/Abstract-45.png +0 -0
- package/sources/assets/images/brutalist/Abstract-46.png +0 -0
- package/sources/assets/images/brutalist/Abstract-47.png +0 -0
- package/sources/assets/images/brutalist/Abstract-48.png +0 -0
- package/sources/assets/images/brutalist/Abstract-49.png +0 -0
- package/sources/assets/images/brutalist/Abstract-5.png +0 -0
- package/sources/assets/images/brutalist/Abstract-50.png +0 -0
- package/sources/assets/images/brutalist/Abstract-51.png +0 -0
- package/sources/assets/images/brutalist/Abstract-52.png +0 -0
- package/sources/assets/images/brutalist/Abstract-53.png +0 -0
- package/sources/assets/images/brutalist/Abstract-54.png +0 -0
- package/sources/assets/images/brutalist/Abstract-55.png +0 -0
- package/sources/assets/images/brutalist/Abstract-56.png +0 -0
- package/sources/assets/images/brutalist/Abstract-57.png +0 -0
- package/sources/assets/images/brutalist/Abstract-58.png +0 -0
- package/sources/assets/images/brutalist/Abstract-59.png +0 -0
- package/sources/assets/images/brutalist/Abstract-6.png +0 -0
- package/sources/assets/images/brutalist/Abstract-60.png +0 -0
- package/sources/assets/images/brutalist/Abstract-61.png +0 -0
- package/sources/assets/images/brutalist/Abstract-62.png +0 -0
- package/sources/assets/images/brutalist/Abstract-63.png +0 -0
- package/sources/assets/images/brutalist/Abstract-64.png +0 -0
- package/sources/assets/images/brutalist/Abstract-65.png +0 -0
- package/sources/assets/images/brutalist/Abstract-66.png +0 -0
- package/sources/assets/images/brutalist/Abstract-67.png +0 -0
- package/sources/assets/images/brutalist/Abstract-68.png +0 -0
- package/sources/assets/images/brutalist/Abstract-69.png +0 -0
- package/sources/assets/images/brutalist/Abstract-7.png +0 -0
- package/sources/assets/images/brutalist/Abstract-70.png +0 -0
- package/sources/assets/images/brutalist/Abstract-71.png +0 -0
- package/sources/assets/images/brutalist/Abstract-72.png +0 -0
- package/sources/assets/images/brutalist/Abstract-73.png +0 -0
- package/sources/assets/images/brutalist/Abstract-74.png +0 -0
- package/sources/assets/images/brutalist/Abstract-75.png +0 -0
- package/sources/assets/images/brutalist/Abstract-76.png +0 -0
- package/sources/assets/images/brutalist/Abstract-77.png +0 -0
- package/sources/assets/images/brutalist/Abstract-78.png +0 -0
- package/sources/assets/images/brutalist/Abstract-79.png +0 -0
- package/sources/assets/images/brutalist/Abstract-8.png +0 -0
- package/sources/assets/images/brutalist/Abstract-80.png +0 -0
- package/sources/assets/images/brutalist/Abstract-81.png +0 -0
- package/sources/assets/images/brutalist/Abstract-82.png +0 -0
- package/sources/assets/images/brutalist/Abstract-83.png +0 -0
- package/sources/assets/images/brutalist/Abstract-84.png +0 -0
- package/sources/assets/images/brutalist/Abstract-85.png +0 -0
- package/sources/assets/images/brutalist/Abstract-86.png +0 -0
- package/sources/assets/images/brutalist/Abstract-87.png +0 -0
- package/sources/assets/images/brutalist/Abstract-88.png +0 -0
- package/sources/assets/images/brutalist/Abstract-89.png +0 -0
- package/sources/assets/images/brutalist/Abstract-9.png +0 -0
- package/sources/assets/images/brutalist/Abstract-90.png +0 -0
- package/sources/assets/images/brutalist/Abstract-91.png +0 -0
- package/sources/assets/images/brutalist/Abstract-92.png +0 -0
- package/sources/assets/images/brutalist/Abstract-93.png +0 -0
- package/sources/assets/images/brutalist/Abstract-94.png +0 -0
- package/sources/assets/images/brutalist/Abstract-95.png +0 -0
- package/sources/assets/images/brutalist/Abstract-96.png +0 -0
- package/sources/assets/images/brutalist/Abstract-97.png +0 -0
- package/sources/assets/images/brutalist/Abstract-98.png +0 -0
- package/sources/assets/images/brutalist/Abstract-99.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-1.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-10.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-11.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-12.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-13.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-14.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-15.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-16.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-17.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-18.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-19.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-2.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-20.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-21.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-22.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-23.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-24.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-25.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-26.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-27.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-28.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-29.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-3.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-30.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-31.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-32.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-33.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-34.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-35.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-36.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-37.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-38.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-39.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-4.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-40.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-5.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-6.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-7.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-8.png +0 -0
- package/sources/assets/images/brutalist/Bauhaus-9.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-1.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-10.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-100.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-101.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-102.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-103.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-104.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-105.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-106.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-107.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-108.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-109.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-11.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-110.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-111.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-112.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-113.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-114.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-115.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-116.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-117.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-118.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-12.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-13.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-14.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-15.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-16.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-17.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-18.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-19.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-2.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-20.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-21.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-22.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-23.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-24.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-25.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-26.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-27.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-28.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-29.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-3.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-30.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-31.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-32.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-33.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-34.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-35.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-36.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-37.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-38.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-39.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-4.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-40.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-41.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-42.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-43.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-44.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-45.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-46.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-47.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-48.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-49.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-5.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-50.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-51.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-52.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-53.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-54.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-55.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-56.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-57.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-58.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-59.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-6.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-60.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-61.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-62.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-63.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-64.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-65.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-66.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-67.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-68.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-69.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-7.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-70.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-71.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-72.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-73.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-74.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-75.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-76.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-77.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-78.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-79.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-8.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-80.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-81.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-82.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-83.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-84.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-85.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-86.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-87.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-88.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-89.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-9.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-90.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-91.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-92.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-93.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-94.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-95.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-96.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-97.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-98.png +0 -0
- package/sources/assets/images/brutalist/Brutalism-99.png +0 -0
- package/sources/assets/images/favicon-active.png +0 -0
- package/sources/assets/images/favicon.png +0 -0
- package/sources/assets/images/gradients/01.png +0 -0
- package/sources/assets/images/gradients/02.png +0 -0
- package/sources/assets/images/gradients/03.png +0 -0
- package/sources/assets/images/gradients/04.png +0 -0
- package/sources/assets/images/gradients/05.png +0 -0
- package/sources/assets/images/gradients/06.png +0 -0
- package/sources/assets/images/gradients/07.png +0 -0
- package/sources/assets/images/gradients/08.png +0 -0
- package/sources/assets/images/gradients/09.png +0 -0
- package/sources/assets/images/gradients/10.png +0 -0
- package/sources/assets/images/gradients/100.png +0 -0
- package/sources/assets/images/gradients/11.png +0 -0
- package/sources/assets/images/gradients/12.png +0 -0
- package/sources/assets/images/gradients/13.png +0 -0
- package/sources/assets/images/gradients/14.png +0 -0
- package/sources/assets/images/gradients/15.png +0 -0
- package/sources/assets/images/gradients/16.png +0 -0
- package/sources/assets/images/gradients/17.png +0 -0
- package/sources/assets/images/gradients/18.png +0 -0
- package/sources/assets/images/gradients/19.png +0 -0
- package/sources/assets/images/gradients/20.png +0 -0
- package/sources/assets/images/gradients/21.png +0 -0
- package/sources/assets/images/gradients/22.png +0 -0
- package/sources/assets/images/gradients/23.png +0 -0
- package/sources/assets/images/gradients/24.png +0 -0
- package/sources/assets/images/gradients/25.png +0 -0
- package/sources/assets/images/gradients/26.png +0 -0
- package/sources/assets/images/gradients/27.png +0 -0
- package/sources/assets/images/gradients/28.png +0 -0
- package/sources/assets/images/gradients/29.png +0 -0
- package/sources/assets/images/gradients/30.png +0 -0
- package/sources/assets/images/gradients/31.png +0 -0
- package/sources/assets/images/gradients/32.png +0 -0
- package/sources/assets/images/gradients/33.png +0 -0
- package/sources/assets/images/gradients/34.png +0 -0
- package/sources/assets/images/gradients/35.png +0 -0
- package/sources/assets/images/gradients/36.png +0 -0
- package/sources/assets/images/gradients/37.png +0 -0
- package/sources/assets/images/gradients/38.png +0 -0
- package/sources/assets/images/gradients/39.png +0 -0
- package/sources/assets/images/gradients/40.png +0 -0
- package/sources/assets/images/gradients/41.png +0 -0
- package/sources/assets/images/gradients/42.png +0 -0
- package/sources/assets/images/gradients/43.png +0 -0
- package/sources/assets/images/gradients/44.png +0 -0
- package/sources/assets/images/gradients/45.png +0 -0
- package/sources/assets/images/gradients/46.png +0 -0
- package/sources/assets/images/gradients/47.png +0 -0
- package/sources/assets/images/gradients/48.png +0 -0
- package/sources/assets/images/gradients/49.png +0 -0
- package/sources/assets/images/gradients/50.png +0 -0
- package/sources/assets/images/gradients/51.png +0 -0
- package/sources/assets/images/gradients/52.png +0 -0
- package/sources/assets/images/gradients/53.png +0 -0
- package/sources/assets/images/gradients/54.png +0 -0
- package/sources/assets/images/gradients/55.png +0 -0
- package/sources/assets/images/gradients/56.png +0 -0
- package/sources/assets/images/gradients/57.png +0 -0
- package/sources/assets/images/gradients/58.png +0 -0
- package/sources/assets/images/gradients/59.png +0 -0
- package/sources/assets/images/gradients/60.png +0 -0
- package/sources/assets/images/gradients/61.png +0 -0
- package/sources/assets/images/gradients/62.png +0 -0
- package/sources/assets/images/gradients/63.png +0 -0
- package/sources/assets/images/gradients/64.png +0 -0
- package/sources/assets/images/gradients/65.png +0 -0
- package/sources/assets/images/gradients/66.png +0 -0
- package/sources/assets/images/gradients/67.png +0 -0
- package/sources/assets/images/gradients/68.png +0 -0
- package/sources/assets/images/gradients/69.png +0 -0
- package/sources/assets/images/gradients/70.png +0 -0
- package/sources/assets/images/gradients/71.png +0 -0
- package/sources/assets/images/gradients/72.png +0 -0
- package/sources/assets/images/gradients/73.png +0 -0
- package/sources/assets/images/gradients/74.png +0 -0
- package/sources/assets/images/gradients/75.png +0 -0
- package/sources/assets/images/gradients/76.png +0 -0
- package/sources/assets/images/gradients/77.png +0 -0
- package/sources/assets/images/gradients/78.png +0 -0
- package/sources/assets/images/gradients/79.png +0 -0
- package/sources/assets/images/gradients/80.png +0 -0
- package/sources/assets/images/gradients/81.png +0 -0
- package/sources/assets/images/gradients/82.png +0 -0
- package/sources/assets/images/gradients/83.png +0 -0
- package/sources/assets/images/gradients/84.png +0 -0
- package/sources/assets/images/gradients/85.png +0 -0
- package/sources/assets/images/gradients/86.png +0 -0
- package/sources/assets/images/gradients/87.png +0 -0
- package/sources/assets/images/gradients/88.png +0 -0
- package/sources/assets/images/gradients/89.png +0 -0
- package/sources/assets/images/gradients/90.png +0 -0
- package/sources/assets/images/gradients/91.png +0 -0
- package/sources/assets/images/gradients/92.png +0 -0
- package/sources/assets/images/gradients/93.png +0 -0
- package/sources/assets/images/gradients/94.png +0 -0
- package/sources/assets/images/gradients/95.png +0 -0
- package/sources/assets/images/gradients/96.png +0 -0
- package/sources/assets/images/gradients/97.png +0 -0
- package/sources/assets/images/gradients/98.png +0 -0
- package/sources/assets/images/gradients/99.png +0 -0
- package/sources/assets/images/icon-adaptive.png +0 -0
- package/sources/assets/images/icon-claude.png +0 -0
- package/sources/assets/images/icon-claude@2x.png +0 -0
- package/sources/assets/images/icon-claude@3x.png +0 -0
- package/sources/assets/images/icon-gemini.png +0 -0
- package/sources/assets/images/icon-gemini@2x.png +0 -0
- package/sources/assets/images/icon-gemini@3x.png +0 -0
- package/sources/assets/images/icon-gpt.png +0 -0
- package/sources/assets/images/icon-gpt@2x.png +0 -0
- package/sources/assets/images/icon-gpt@3x.png +0 -0
- package/sources/assets/images/icon-monochrome.png +0 -0
- package/sources/assets/images/icon-notification.png +0 -0
- package/sources/assets/images/icon-openclaw.png +0 -0
- package/sources/assets/images/icon-openclaw@2x.png +0 -0
- package/sources/assets/images/icon-openclaw@3x.png +0 -0
- package/sources/assets/images/icon-tauri.png +0 -0
- package/sources/assets/images/icon-voice-white.png +0 -0
- package/sources/assets/images/icon-voice.png +0 -0
- package/sources/assets/images/icon-voice@2x.png +0 -0
- package/sources/assets/images/icon-voice@3x.png +0 -0
- package/sources/assets/images/icon.png +0 -0
- package/sources/assets/images/logo-black.png +0 -0
- package/sources/assets/images/logo-white.png +0 -0
- package/sources/assets/images/logotype-dark.png +0 -0
- package/sources/assets/images/logotype-dark@2x.png +0 -0
- package/sources/assets/images/logotype-dark@3x.png +0 -0
- package/sources/assets/images/logotype-light.png +0 -0
- package/sources/assets/images/logotype-light@2x.png +0 -0
- package/sources/assets/images/logotype-light@3x.png +0 -0
- package/sources/assets/images/logotype.png +0 -0
- package/sources/assets/images/logotype@2x.png +0 -0
- package/sources/assets/images/logotype@3x.png +0 -0
- package/sources/assets/images/splash-android-dark.png +0 -0
- package/sources/assets/images/splash-android-light.png +0 -0
- package/sources/assets/images/transparent.png +0 -0
- package/sources/assets/images/zen-icon.png +0 -0
- package/sources/auth/AuthContext.tsx +100 -0
- package/sources/auth/authAccountApprove.ts +2 -0
- package/sources/auth/authApprove.ts +2 -0
- package/sources/auth/authChallenge.ts +8 -0
- package/sources/auth/authGetToken.ts +4 -0
- package/sources/auth/authQRStart.ts +17 -0
- package/sources/auth/authQRWait.ts +13 -0
- package/sources/auth/secretKeyBackup.spec.ts +465 -0
- package/sources/auth/secretKeyBackup.ts +179 -0
- package/sources/auth/tokenStorage.ts +27 -0
- package/sources/changelog/changelog.json +60 -0
- package/sources/changelog/index.ts +3 -0
- package/sources/changelog/parser.ts +23 -0
- package/sources/changelog/storage.ts +17 -0
- package/sources/changelog/types.ts +10 -0
- package/sources/components/ActiveSessionsGroupCompact.tsx +567 -0
- package/sources/components/AgentContentView.ios.tsx +70 -0
- package/sources/components/AgentContentView.tsx +48 -0
- package/sources/components/AgentInput.tsx +1468 -0
- package/sources/components/AgentInputAttachmentStrip.tsx +122 -0
- package/sources/components/AgentInputAutocomplete.tsx +96 -0
- package/sources/components/AgentInputSuggestionView.tsx +106 -0
- package/sources/components/AllFilesDiffView.tsx +515 -0
- package/sources/components/Avatar.tsx +149 -0
- package/sources/components/AvatarBrutalist.tsx +501 -0
- package/sources/components/AvatarGradient.tsx +147 -0
- package/sources/components/AvatarSkia.tsx +111 -0
- package/sources/components/AvatarSkia.web.tsx +113 -0
- package/sources/components/ChatFooter.tsx +50 -0
- package/sources/components/ChatHeaderView.tsx +180 -0
- package/sources/components/ChatList.tsx +283 -0
- package/sources/components/CodeEditor.tsx +22 -0
- package/sources/components/CodeEditor.web.tsx +180 -0
- package/sources/components/CodeView.tsx +33 -0
- package/sources/components/CommandPalette/CommandPalette.tsx +72 -0
- package/sources/components/CommandPalette/CommandPaletteInput.tsx +65 -0
- package/sources/components/CommandPalette/CommandPaletteItem.tsx +141 -0
- package/sources/components/CommandPalette/CommandPaletteModal.tsx +148 -0
- package/sources/components/CommandPalette/CommandPaletteProvider.tsx +141 -0
- package/sources/components/CommandPalette/CommandPaletteResults.tsx +129 -0
- package/sources/components/CommandPalette/index.ts +3 -0
- package/sources/components/CommandPalette/types.ts +15 -0
- package/sources/components/CommandPalette/useCommandPalette.ts +107 -0
- package/sources/components/CommandView.tsx +135 -0
- package/sources/components/CompactGitStatus.tsx +88 -0
- package/sources/components/ConnectButton.tsx +117 -0
- package/sources/components/Deferred.tsx +18 -0
- package/sources/components/DuplicateSheet.tsx +295 -0
- package/sources/components/EmptyMainScreen.tsx +171 -0
- package/sources/components/EmptyMessages.tsx +123 -0
- package/sources/components/EmptySessionsTablet.tsx +111 -0
- package/sources/components/ExternalLink.tsx +22 -0
- package/sources/components/FAB.tsx +53 -0
- package/sources/components/FABWide.tsx +59 -0
- package/sources/components/FeedItemCard.tsx +98 -0
- package/sources/components/FileIcon.tsx +63 -0
- package/sources/components/FileViewPanel.tsx +673 -0
- package/sources/components/FilesSidebar.tsx +739 -0
- package/sources/components/FloatingOverlay.tsx +48 -0
- package/sources/components/GitStatusBadge.tsx +82 -0
- package/sources/components/HeaderLogo.tsx +28 -0
- package/sources/components/HomeHeader.tsx +243 -0
- package/sources/components/HorizontalScrollView.tsx +88 -0
- package/sources/components/InboxView.tsx +260 -0
- package/sources/components/InlineFileDiff.tsx +277 -0
- package/sources/components/Item.tsx +315 -0
- package/sources/components/ItemGroup.tsx +147 -0
- package/sources/components/ItemList.tsx +102 -0
- package/sources/components/MainView.tsx +324 -0
- package/sources/components/MessageView.tsx +299 -0
- package/sources/components/MultiTextInput.tsx +285 -0
- package/sources/components/MultiTextInput.web.tsx +220 -0
- package/sources/components/OAuthView.tsx +374 -0
- package/sources/components/PermissionModeSelector.tsx +68 -0
- package/sources/components/PlaceholderContainerView.tsx +47 -0
- package/sources/components/PlusPlus.tsx +34 -0
- package/sources/components/PlusPlus.web.tsx +34 -0
- package/sources/components/ProjectGitStatus.tsx +102 -0
- package/sources/components/RoundButton.tsx +130 -0
- package/sources/components/SearchableListSelector.tsx +675 -0
- package/sources/components/SessionActionsNativeMenu.android.tsx +58 -0
- package/sources/components/SessionActionsNativeMenu.ios.tsx +55 -0
- package/sources/components/SessionActionsNativeMenu.tsx +20 -0
- package/sources/components/SessionActionsNativeMenu.web.tsx +13 -0
- package/sources/components/SessionActionsPopover.tsx +240 -0
- package/sources/components/SessionsList.tsx +471 -0
- package/sources/components/SessionsListWrapper.tsx +72 -0
- package/sources/components/SettingsView.tsx +470 -0
- package/sources/components/SettingsViewWrapper.tsx +21 -0
- package/sources/components/Shaker.tsx +42 -0
- package/sources/components/Shaker.web.tsx +46 -0
- package/sources/components/ShimmerView.tsx +106 -0
- package/sources/components/SidebarNavigator.tsx +218 -0
- package/sources/components/SidebarView.tsx +104 -0
- package/sources/components/SimpleSyntaxHighlighter.tsx +322 -0
- package/sources/components/StatusBarProvider.tsx +12 -0
- package/sources/components/StatusDot.tsx +49 -0
- package/sources/components/StyledText.tsx +35 -0
- package/sources/components/Switch.tsx +20 -0
- package/sources/components/TabBar.tsx +140 -0
- package/sources/components/ToolGroupView.tsx +101 -0
- package/sources/components/TransitionStack.tsx +14 -0
- package/sources/components/UpdateBanner.tsx +74 -0
- package/sources/components/UserCard.tsx +41 -0
- package/sources/components/UserSearchResult.tsx +129 -0
- package/sources/components/VoiceAssistantStatusBar.tsx +260 -0
- package/sources/components/VoiceBars.tsx +95 -0
- package/sources/components/autocomplete/applySuggestion.test.ts +194 -0
- package/sources/components/autocomplete/applySuggestion.ts +61 -0
- package/sources/components/autocomplete/findActiveWord.test.ts +365 -0
- package/sources/components/autocomplete/findActiveWord.ts +207 -0
- package/sources/components/autocomplete/suggestions.ts +79 -0
- package/sources/components/autocomplete/useActiveSuggestions.ts +130 -0
- package/sources/components/autocomplete/useActiveWord.ts +19 -0
- package/sources/components/diff/DiffView.tsx +188 -0
- package/sources/components/diff/PierreDiffView.tsx +253 -0
- package/sources/components/diff/calculateDiff.ts +317 -0
- package/sources/components/entityColor.ts +51 -0
- package/sources/components/haptics.ts +9 -0
- package/sources/components/haptics.web.ts +7 -0
- package/sources/components/layout.ts +44 -0
- package/sources/components/markdown/MarkdownView.tsx +670 -0
- package/sources/components/markdown/MermaidRenderer.tsx +233 -0
- package/sources/components/markdown/linkUtils.test.ts +17 -0
- package/sources/components/markdown/linkUtils.ts +5 -0
- package/sources/components/markdown/parseMarkdown.test.ts +64 -0
- package/sources/components/markdown/parseMarkdown.ts +46 -0
- package/sources/components/markdown/parseMarkdownBlock.test.ts +108 -0
- package/sources/components/markdown/parseMarkdownBlock.ts +200 -0
- package/sources/components/markdown/parseMarkdownSpans.ts +88 -0
- package/sources/components/modelModeOptions.test.ts +114 -0
- package/sources/components/modelModeOptions.ts +257 -0
- package/sources/components/navigation/Header.tsx +252 -0
- package/sources/components/parseLocalCommandMessage.spec.ts +96 -0
- package/sources/components/parseLocalCommandMessage.ts +97 -0
- package/sources/components/qr/QRCode.tsx +178 -0
- package/sources/components/qr/QRCode.web.tsx +229 -0
- package/sources/components/qr/index.ts +2 -0
- package/sources/components/qr/qrMatrix.ts +48 -0
- package/sources/components/tools/PermissionFooter.tsx +527 -0
- package/sources/components/tools/ToolDiffView.tsx +60 -0
- package/sources/components/tools/ToolError.tsx +49 -0
- package/sources/components/tools/ToolFullView.tsx +193 -0
- package/sources/components/tools/ToolHeader.tsx +93 -0
- package/sources/components/tools/ToolSectionView.tsx +42 -0
- package/sources/components/tools/ToolStatusIndicator.tsx +36 -0
- package/sources/components/tools/ToolView.tsx +340 -0
- package/sources/components/tools/knownTools.tsx +957 -0
- package/sources/components/tools/views/AskUserQuestionView.tsx +357 -0
- package/sources/components/tools/views/BashView.tsx +47 -0
- package/sources/components/tools/views/BashViewFull.tsx +81 -0
- package/sources/components/tools/views/CodexBashView.tsx +126 -0
- package/sources/components/tools/views/CodexDiffView.tsx +79 -0
- package/sources/components/tools/views/CodexPatchView.tsx +186 -0
- package/sources/components/tools/views/EditView.tsx +33 -0
- package/sources/components/tools/views/EditViewFull.tsx +38 -0
- package/sources/components/tools/views/ExitPlanToolView.tsx +21 -0
- package/sources/components/tools/views/FileView.tsx +118 -0
- package/sources/components/tools/views/GeminiEditView.tsx +75 -0
- package/sources/components/tools/views/GeminiExecuteView.tsx +92 -0
- package/sources/components/tools/views/MCPToolView.tsx +31 -0
- package/sources/components/tools/views/MultiEditView.tsx +41 -0
- package/sources/components/tools/views/MultiEditViewFull.tsx +84 -0
- package/sources/components/tools/views/TaskView.tsx +129 -0
- package/sources/components/tools/views/TodoView.tsx +90 -0
- package/sources/components/tools/views/WriteView.tsx +29 -0
- package/sources/components/tools/views/_all.tsx +88 -0
- package/sources/components/usage/UsageBar.tsx +80 -0
- package/sources/components/usage/UsageChart.tsx +164 -0
- package/sources/components/usage/UsagePanel.tsx +283 -0
- package/sources/components/web/FaviconPermissionIndicator.tsx +44 -0
- package/sources/config.ts +3 -0
- package/sources/constants/Languages.ts +116 -0
- package/sources/constants/Typography.ts +116 -0
- package/sources/dev/testRunner.ts +277 -0
- package/sources/docs/autocomplete-text-manipulation.md +224 -0
- package/sources/encryption/aes.appspec.ts +25 -0
- package/sources/encryption/aes.ts +21 -0
- package/sources/encryption/aes.web.test.ts +73 -0
- package/sources/encryption/aes.web.ts +79 -0
- package/sources/encryption/base64.appspec.ts +240 -0
- package/sources/encryption/base64.native.ts +12 -0
- package/sources/encryption/base64.ts +46 -0
- package/sources/encryption/blob.test.ts +120 -0
- package/sources/encryption/blob.ts +58 -0
- package/sources/encryption/deriveKey.appspec.ts +72 -0
- package/sources/encryption/deriveKey.ts +46 -0
- package/sources/encryption/hex.ts +17 -0
- package/sources/encryption/hmac_sha512.appspec.ts +40 -0
- package/sources/encryption/hmac_sha512.ts +42 -0
- package/sources/encryption/libsodium.lib.ts +2 -0
- package/sources/encryption/libsodium.lib.web.ts +2 -0
- package/sources/encryption/libsodium.ts +58 -0
- package/sources/encryption/text.test.ts +61 -0
- package/sources/encryption/text.ts +11 -0
- package/sources/hooks/useAsyncCommand.ts +25 -0
- package/sources/hooks/useAttachmentImage.ts +134 -0
- package/sources/hooks/useAutocomplete.ts +69 -0
- package/sources/hooks/useAutocompleteSession.ts +53 -0
- package/sources/hooks/useChangelog.ts +45 -0
- package/sources/hooks/useCheckCameraPermissions.ts +25 -0
- package/sources/hooks/useConnectAccount.ts +107 -0
- package/sources/hooks/useConnectTerminal.ts +112 -0
- package/sources/hooks/useDemoMessages.ts +48 -0
- package/sources/hooks/useDraft.ts +120 -0
- package/sources/hooks/useElapsedTime.ts +36 -0
- package/sources/hooks/useGetPath.ts +13 -0
- package/sources/hooks/useGitStatusFiles.ts +45 -0
- package/sources/hooks/useGlobalKeyboard.ts +29 -0
- package/sources/hooks/useGroupedMessages.ts +149 -0
- package/sources/hooks/useImagePicker.ts +135 -0
- package/sources/hooks/useInboxHasContent.ts +19 -0
- package/sources/hooks/useMultiClick.ts +56 -0
- package/sources/hooks/useNasTechAction.ts +45 -0
- package/sources/hooks/useNativeUpdate.ts +10 -0
- package/sources/hooks/useNavigateToSession.ts +20 -0
- package/sources/hooks/useNewSessionDraft.ts +70 -0
- package/sources/hooks/usePrefetchFileContents.ts +162 -0
- package/sources/hooks/useSearch.ts +120 -0
- package/sources/hooks/useSessionQuickActions.ts +325 -0
- package/sources/hooks/useTauriDrag.ts +76 -0
- package/sources/hooks/useTauriZoom.ts +67 -0
- package/sources/hooks/useUpdates.ts +84 -0
- package/sources/hooks/useVisibleSessionListViewData.ts +65 -0
- package/sources/hooks/useWorktreeCleanup.ts +69 -0
- package/sources/log.ts +108 -0
- package/sources/modal/ModalManager.ts +203 -0
- package/sources/modal/ModalProvider.tsx +103 -0
- package/sources/modal/components/BaseModal.tsx +122 -0
- package/sources/modal/components/CustomModal.tsx +42 -0
- package/sources/modal/components/WebAlertModal.tsx +139 -0
- package/sources/modal/components/WebPromptModal.tsx +185 -0
- package/sources/modal/index.ts +3 -0
- package/sources/modal/types.ts +79 -0
- package/sources/nastech-wire/index.ts +10 -0
- package/sources/nastech-wire/legacyProtocol.ts +27 -0
- package/sources/nastech-wire/messageMeta.ts +14 -0
- package/sources/nastech-wire/messages.ts +113 -0
- package/sources/nastech-wire/sessionProtocol.ts +134 -0
- package/sources/nastech-wire/voice.ts +34 -0
- package/sources/polyfills/screenOrientation.ts +33 -0
- package/sources/realtime/RealtimeProvider.tsx +20 -0
- package/sources/realtime/RealtimeProvider.web.tsx +15 -0
- package/sources/realtime/RealtimeSession.ts +200 -0
- package/sources/realtime/RealtimeVoiceSession.tsx +211 -0
- package/sources/realtime/RealtimeVoiceSession.web.tsx +209 -0
- package/sources/realtime/hooks/contextFormatters.ts +127 -0
- package/sources/realtime/hooks/voiceHooks.ts +232 -0
- package/sources/realtime/realtimeClientTools.ts +94 -0
- package/sources/realtime/types.ts +19 -0
- package/sources/realtime/voiceConfig.ts +31 -0
- package/sources/realtime/voiceExperiment.ts +91 -0
- package/sources/realtime/voiceSystemPrompt.ts +75 -0
- package/sources/scripts/compareTranslations.ts +217 -0
- package/sources/scripts/parseChangelog.ts +87 -0
- package/sources/sync/__testdata__/trace_0.json +3986 -0
- package/sources/sync/__testdata__/trace_1.json +1391 -0
- package/sources/sync/__testdata__/trace_2.json +182 -0
- package/sources/sync/agentDefaults.ts +108 -0
- package/sources/sync/apiArtifacts.ts +143 -0
- package/sources/sync/apiAttachments.ts +217 -0
- package/sources/sync/apiFeed.ts +60 -0
- package/sources/sync/apiFriends.ts +217 -0
- package/sources/sync/apiGithub.spec.ts +97 -0
- package/sources/sync/apiGithub.ts +103 -0
- package/sources/sync/apiKv.ts +270 -0
- package/sources/sync/apiPush.ts +83 -0
- package/sources/sync/apiServices.ts +64 -0
- package/sources/sync/apiSocket.ts +290 -0
- package/sources/sync/apiTypes.spec.ts +23 -0
- package/sources/sync/apiTypes.ts +213 -0
- package/sources/sync/apiUsage.ts +130 -0
- package/sources/sync/apiVoice.ts +57 -0
- package/sources/sync/appConfig.ts +95 -0
- package/sources/sync/artifactTypes.ts +85 -0
- package/sources/sync/attachmentTypes.ts +28 -0
- package/sources/sync/encryption/artifactEncryption.ts +83 -0
- package/sources/sync/encryption/encryption.ts +220 -0
- package/sources/sync/encryption/encryptionCache.ts +248 -0
- package/sources/sync/encryption/encryptor.appspec.ts +409 -0
- package/sources/sync/encryption/encryptor.ts +126 -0
- package/sources/sync/encryption/machineEncryption.ts +122 -0
- package/sources/sync/encryption/sessionEncryption.ts +207 -0
- package/sources/sync/feedTypes.ts +43 -0
- package/sources/sync/friendTypes.ts +92 -0
- package/sources/sync/git-parsers/LineParser.ts +62 -0
- package/sources/sync/git-parsers/parseBranch.ts +97 -0
- package/sources/sync/git-parsers/parseDiff.ts +180 -0
- package/sources/sync/git-parsers/parseStatus.ts +162 -0
- package/sources/sync/git-parsers/parseStatusV2.ts +307 -0
- package/sources/sync/gitStatusFiles.ts +185 -0
- package/sources/sync/gitStatusSync.ts +282 -0
- package/sources/sync/localSettings.ts +67 -0
- package/sources/sync/messageMeta.test.ts +87 -0
- package/sources/sync/messageMeta.ts +36 -0
- package/sources/sync/modeHacks.test.ts +29 -0
- package/sources/sync/modeHacks.ts +22 -0
- package/sources/sync/nastechApi.ts +124 -0
- package/sources/sync/ops.ts +776 -0
- package/sources/sync/persistence.ts +322 -0
- package/sources/sync/profile.ts +95 -0
- package/sources/sync/projectFiles.ts +54 -0
- package/sources/sync/prompt/systemPrompt.ts +20 -0
- package/sources/sync/purchases.ts +67 -0
- package/sources/sync/pushRegistration.ts +229 -0
- package/sources/sync/reducer/activityUpdateAccumulator.test.ts +492 -0
- package/sources/sync/reducer/activityUpdateAccumulator.ts +96 -0
- package/sources/sync/reducer/messageToEvent.ts +85 -0
- package/sources/sync/reducer/phase0-skipping.spec.ts +206 -0
- package/sources/sync/reducer/reducer.spec.ts +3169 -0
- package/sources/sync/reducer/reducer.ts +1214 -0
- package/sources/sync/reducer/reducerTracer.spec.ts +502 -0
- package/sources/sync/reducer/reducerTracer.ts +310 -0
- package/sources/sync/revenueCat/index.ts +21 -0
- package/sources/sync/revenueCat/revenueCat.ts +215 -0
- package/sources/sync/revenueCat/revenueCat.web.ts +238 -0
- package/sources/sync/revenueCat/types.ts +82 -0
- package/sources/sync/serverConfig.ts +72 -0
- package/sources/sync/settings.spec.ts +456 -0
- package/sources/sync/settings.ts +186 -0
- package/sources/sync/storage.ts +1653 -0
- package/sources/sync/storageTypes.spec.ts +24 -0
- package/sources/sync/storageTypes.ts +215 -0
- package/sources/sync/suggestionCommands.ts +149 -0
- package/sources/sync/suggestionFile.ts +206 -0
- package/sources/sync/sync.ts +2555 -0
- package/sources/sync/typesMessage.ts +69 -0
- package/sources/sync/typesMessageMeta.test.ts +14 -0
- package/sources/sync/typesMessageMeta.ts +17 -0
- package/sources/sync/typesRaw.spec.ts +2010 -0
- package/sources/sync/typesRaw.ts +1183 -0
- package/sources/sync/uploadFormFile.ts +29 -0
- package/sources/sync/uploadFormFile.web.ts +14 -0
- package/sources/sync/webTabTitle.ts +58 -0
- package/sources/text/README.md +223 -0
- package/sources/text/_all.ts +104 -0
- package/sources/text/_default.ts +1015 -0
- package/sources/text/index.ts +215 -0
- package/sources/text/translations/ca.ts +993 -0
- package/sources/text/translations/en.ts +1009 -0
- package/sources/text/translations/es.ts +995 -0
- package/sources/text/translations/it.ts +992 -0
- package/sources/text/translations/ja.ts +993 -0
- package/sources/text/translations/pl.ts +1024 -0
- package/sources/text/translations/pt.ts +992 -0
- package/sources/text/translations/ru.ts +1023 -0
- package/sources/text/translations/zh-Hans.ts +992 -0
- package/sources/text/translations/zh-Hant.ts +991 -0
- package/sources/theme.css +72 -0
- package/sources/theme.dark.json +31 -0
- package/sources/theme.figma.json +457 -0
- package/sources/theme.gen.ts +10 -0
- package/sources/theme.light.json +31 -0
- package/sources/theme.ts +452 -0
- package/sources/track/index.ts +171 -0
- package/sources/track/tracking.ts +12 -0
- package/sources/track/useTrackScreens.ts +10 -0
- package/sources/types/react-native-webrtc-web-shim.d.ts +6 -0
- package/sources/unistyles.ts +97 -0
- package/sources/utils/codexUnifiedDiff.spec.ts +43 -0
- package/sources/utils/codexUnifiedDiff.ts +70 -0
- package/sources/utils/consoleLogging.ts +145 -0
- package/sources/utils/copySessionMetadataToClipboard.ts +53 -0
- package/sources/utils/debounce.test.ts +646 -0
- package/sources/utils/debounce.ts +122 -0
- package/sources/utils/deviceCalculations.test.ts +318 -0
- package/sources/utils/deviceCalculations.ts +87 -0
- package/sources/utils/errors.ts +10 -0
- package/sources/utils/formatPermissionParams.ts +23 -0
- package/sources/utils/isTauri.ts +7 -0
- package/sources/utils/loadSkia.ts +3 -0
- package/sources/utils/loadSkia.web.ts +5 -0
- package/sources/utils/lock.ts +40 -0
- package/sources/utils/machineUtils.ts +6 -0
- package/sources/utils/messageUtils.ts +254 -0
- package/sources/utils/microphonePermissions.ts +109 -0
- package/sources/utils/notificationRouting.test.ts +51 -0
- package/sources/utils/notificationRouting.ts +81 -0
- package/sources/utils/oauth.ts +143 -0
- package/sources/utils/openExternalUrl.ts +19 -0
- package/sources/utils/parseToken.ts +23 -0
- package/sources/utils/pasteImages.web.ts +81 -0
- package/sources/utils/pathUtils.spec.ts +226 -0
- package/sources/utils/pathUtils.ts +75 -0
- package/sources/utils/platform.ts +19 -0
- package/sources/utils/readFileBytes.ts +11 -0
- package/sources/utils/readFileBytes.web.ts +12 -0
- package/sources/utils/requestReview.ts +135 -0
- package/sources/utils/responsive.ts +87 -0
- package/sources/utils/resumeCommand.test.ts +78 -0
- package/sources/utils/resumeCommand.ts +70 -0
- package/sources/utils/sessionFileLinks.test.ts +112 -0
- package/sources/utils/sessionFileLinks.ts +388 -0
- package/sources/utils/sessionUtils.ts +226 -0
- package/sources/utils/stringUtils.ts +41 -0
- package/sources/utils/sync.ts +164 -0
- package/sources/utils/thumbhash.ts +17 -0
- package/sources/utils/thumbhash.web.ts +85 -0
- package/sources/utils/time.ts +41 -0
- package/sources/utils/toSnakeCase.test.ts +182 -0
- package/sources/utils/toSnakeCase.ts +40 -0
- package/sources/utils/toolCommand.test.ts +23 -0
- package/sources/utils/toolCommand.ts +35 -0
- package/sources/utils/toolComparison.test.ts +101 -0
- package/sources/utils/toolComparison.ts +73 -0
- package/sources/utils/toolErrorParser.test.ts +126 -0
- package/sources/utils/toolErrorParser.ts +102 -0
- package/sources/utils/trimIdent.ts +27 -0
- package/sources/utils/truncateForLogs.ts +37 -0
- package/sources/utils/versionUtils.test.ts +58 -0
- package/sources/utils/versionUtils.ts +69 -0
- package/sources/utils/web/faviconGenerator.ts +39 -0
- package/sources/utils/worktree.ts +192 -0
- package/sources/wire/index.ts +97 -0
- package/src-tauri/Cargo.lock +5978 -0
- package/src-tauri/Cargo.toml +27 -0
- package/src-tauri/build.rs +3 -0
- package/src-tauri/capabilities/default.json +27 -0
- package/src-tauri/deny.toml +53 -0
- package/src-tauri/entitlements.plist +24 -0
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/64x64.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/lib.rs +18 -0
- package/src-tauri/src/main.rs +6 -0
- package/src-tauri/tauri.conf.json +51 -0
- package/src-tauri/tauri.dev.conf.json +20 -0
- package/src-tauri/tauri.preview.conf.json +20 -0
- package/tsconfig.json +45 -0
- 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
|
+
|