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,1653 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
3
|
+
import equal from 'fast-deep-equal'
|
|
4
|
+
|
|
5
|
+
function useDeepEqual<T>(selector: (state: StorageState) => T): (state: StorageState) => T {
|
|
6
|
+
const prev = React.useRef<T>(undefined);
|
|
7
|
+
return (state: StorageState) => {
|
|
8
|
+
const next = selector(state);
|
|
9
|
+
return equal(prev.current, next) ? prev.current! : (prev.current = next);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
import { Session, Machine, GitStatus } from "./storageTypes";
|
|
13
|
+
import type { GitStatusFiles } from "./gitStatusFiles";
|
|
14
|
+
import type { ProjectFilesList } from "./projectFiles";
|
|
15
|
+
import { createReducer, reducer, ReducerState } from "./reducer/reducer";
|
|
16
|
+
import { Message } from "./typesMessage";
|
|
17
|
+
import { NormalizedMessage } from "./typesRaw";
|
|
18
|
+
import { isMachineOnline } from '@/utils/machineUtils';
|
|
19
|
+
import { getSessionName, getSessionSubtitle, getSessionAvatarId, type SessionState } from '@/utils/sessionUtils';
|
|
20
|
+
import { applySettings, Settings } from "./settings";
|
|
21
|
+
import { LocalSettings, applyLocalSettings } from "./localSettings";
|
|
22
|
+
import { Purchases, customerInfoToPurchases } from "./purchases";
|
|
23
|
+
import { Profile } from "./profile";
|
|
24
|
+
import { UserProfile, RelationshipUpdatedEvent } from "./friendTypes";
|
|
25
|
+
import { loadSettings, loadLocalSettings, saveLocalSettings, saveSettings, loadPurchases, savePurchases, loadProfile, saveProfile, loadSessionDrafts, saveSessionDrafts, loadSessionPermissionModes, saveSessionPermissionModes, loadSessionModelModes, saveSessionModelModes, loadSessionEffortLevels, saveSessionEffortLevels } from "./persistence";
|
|
26
|
+
import type { CustomerInfo } from './revenueCat/types';
|
|
27
|
+
import React from "react";
|
|
28
|
+
import { sync } from "./sync";
|
|
29
|
+
import { getCurrentRealtimeSessionId, getVoiceSession } from '@/realtime/RealtimeSession';
|
|
30
|
+
import { isMutableTool } from "@/components/tools/knownTools";
|
|
31
|
+
import { DecryptedArtifact } from "./artifactTypes";
|
|
32
|
+
import { FeedItem } from "./feedTypes";
|
|
33
|
+
|
|
34
|
+
// Debounce timer for realtimeMode changes
|
|
35
|
+
let realtimeModeDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
36
|
+
const REALTIME_MODE_DEBOUNCE_MS = 150;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Centralized session online state resolver
|
|
40
|
+
* Returns either "online" (string) or a timestamp (number) for last seen
|
|
41
|
+
*/
|
|
42
|
+
function resolveSessionOnlineState(session: { active: boolean; activeAt: number }): "online" | number {
|
|
43
|
+
// Session is online if the active flag is true
|
|
44
|
+
return session.active ? "online" : session.activeAt;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Checks if a session should be shown in the active sessions group
|
|
49
|
+
*/
|
|
50
|
+
function isSessionActive(session: { active: boolean; activeAt: number }): boolean {
|
|
51
|
+
// Use the active flag directly, no timeout checks
|
|
52
|
+
return session.active;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Known entitlement IDs
|
|
56
|
+
export type KnownEntitlements = 'pro';
|
|
57
|
+
|
|
58
|
+
interface SessionMessages {
|
|
59
|
+
messages: Message[];
|
|
60
|
+
messagesMap: Record<string, Message>;
|
|
61
|
+
reducerState: ReducerState;
|
|
62
|
+
isLoaded: boolean;
|
|
63
|
+
// True when the server reported more older messages exist beyond the
|
|
64
|
+
// oldest one we currently have. Drives the "load older" affordance in
|
|
65
|
+
// the chat list. Defaults to false until the initial fetch resolves —
|
|
66
|
+
// the UI must not show a stale paginate-up spinner before that.
|
|
67
|
+
hasMoreOlder: boolean;
|
|
68
|
+
// True while a backward (older-history) page is in flight. Used by the
|
|
69
|
+
// chat list to render a loading footer at the top of the inverted list
|
|
70
|
+
// and to suppress duplicate triggers from FlatList onEndReached.
|
|
71
|
+
isLoadingOlder: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Machine type is now imported from storageTypes - represents persisted machine data
|
|
75
|
+
|
|
76
|
+
// Display-only row data — all primitives, cheap to deep-equal
|
|
77
|
+
export interface SessionRowData {
|
|
78
|
+
id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
subtitle: string;
|
|
81
|
+
avatarId: string;
|
|
82
|
+
flavor: string | null;
|
|
83
|
+
state: SessionState;
|
|
84
|
+
// Only present on inactive sessions — active sessions never show "last seen"
|
|
85
|
+
// and activeAt updates on every heartbeat, causing needless deep-equal diffs
|
|
86
|
+
activeAt?: number;
|
|
87
|
+
createdAt?: number;
|
|
88
|
+
hasDraft: boolean;
|
|
89
|
+
active: boolean;
|
|
90
|
+
machineId: string | null;
|
|
91
|
+
path: string | null;
|
|
92
|
+
homeDir: string | null;
|
|
93
|
+
completedTodosCount: number;
|
|
94
|
+
totalTodosCount: number;
|
|
95
|
+
hasUnread: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function buildSessionRowData(session: Session, unreadSessionIds?: Set<string>): SessionRowData {
|
|
99
|
+
const isOnline = session.presence === "online";
|
|
100
|
+
const hasPermissions = !!(session.agentState?.requests && Object.keys(session.agentState.requests).length > 0);
|
|
101
|
+
|
|
102
|
+
let state: SessionState;
|
|
103
|
+
if (!isOnline) {
|
|
104
|
+
state = 'disconnected';
|
|
105
|
+
} else if (hasPermissions) {
|
|
106
|
+
state = 'permission_required';
|
|
107
|
+
} else if (session.thinking) {
|
|
108
|
+
state = 'thinking';
|
|
109
|
+
} else {
|
|
110
|
+
state = 'waiting';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
id: session.id,
|
|
115
|
+
name: getSessionName(session),
|
|
116
|
+
subtitle: getSessionSubtitle(session),
|
|
117
|
+
avatarId: getSessionAvatarId(session),
|
|
118
|
+
flavor: session.metadata?.flavor ?? null,
|
|
119
|
+
state,
|
|
120
|
+
...(!session.active && { activeAt: session.activeAt, createdAt: session.createdAt }),
|
|
121
|
+
hasDraft: !!session.draft,
|
|
122
|
+
active: session.active,
|
|
123
|
+
machineId: session.metadata?.machineId ?? null,
|
|
124
|
+
path: session.metadata?.path ?? null,
|
|
125
|
+
homeDir: session.metadata?.homeDir ?? null,
|
|
126
|
+
completedTodosCount: session.todos?.filter(todo => todo.status === 'completed').length ?? 0,
|
|
127
|
+
totalTodosCount: session.todos?.length ?? 0,
|
|
128
|
+
hasUnread: unreadSessionIds?.has(session.id) ?? false,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Unified list item type for SessionsList component
|
|
133
|
+
export type SessionListViewItem =
|
|
134
|
+
| { type: 'header'; title: string }
|
|
135
|
+
| { type: 'active-sessions'; sessions: SessionRowData[] }
|
|
136
|
+
| { type: 'archive-toggle'; hidden: boolean }
|
|
137
|
+
| { type: 'project-group'; displayPath: string; machine: Machine }
|
|
138
|
+
| { type: 'session'; session: SessionRowData };
|
|
139
|
+
|
|
140
|
+
// Legacy type for backward compatibility - to be removed
|
|
141
|
+
export type SessionListItem = string | Session;
|
|
142
|
+
|
|
143
|
+
interface StorageState {
|
|
144
|
+
settings: Settings;
|
|
145
|
+
settingsVersion: number | null;
|
|
146
|
+
localSettings: LocalSettings;
|
|
147
|
+
purchases: Purchases;
|
|
148
|
+
profile: Profile;
|
|
149
|
+
sessions: Record<string, Session>;
|
|
150
|
+
sessionsData: SessionListItem[] | null; // Legacy - to be removed
|
|
151
|
+
sessionListViewData: SessionListViewItem[] | null;
|
|
152
|
+
sessionMessages: Record<string, SessionMessages>;
|
|
153
|
+
pathGitStatus: Record<string, GitStatus | null>; // keyed by "machineId:path"
|
|
154
|
+
pathGitStatusFiles: Record<string, GitStatusFiles | null>; // keyed by "machineId:path"
|
|
155
|
+
pathProjectFiles: Record<string, ProjectFilesList | null>; // keyed by "machineId:path"
|
|
156
|
+
sessionFileCache: Record<string, Record<string, { content: string | null; diff: string | null; isBinary: boolean; cachedAt: number }>>;
|
|
157
|
+
machines: Record<string, Machine>;
|
|
158
|
+
artifacts: Record<string, DecryptedArtifact>; // New artifacts storage
|
|
159
|
+
friends: Record<string, UserProfile>; // All relationships (friends, pending, requested, etc.)
|
|
160
|
+
users: Record<string, UserProfile | null>; // Global user cache, null = 404/failed fetch
|
|
161
|
+
feedItems: FeedItem[]; // Simple list of feed items
|
|
162
|
+
feedHead: string | null; // Newest cursor
|
|
163
|
+
feedTail: string | null; // Oldest cursor
|
|
164
|
+
feedHasMore: boolean;
|
|
165
|
+
feedLoaded: boolean; // True after initial feed fetch
|
|
166
|
+
friendsLoaded: boolean; // True after initial friends fetch
|
|
167
|
+
realtimeStatus: 'disconnected' | 'connecting' | 'connected' | 'error';
|
|
168
|
+
realtimeMode: 'idle' | 'agent-speaking' | 'user-speaking';
|
|
169
|
+
voiceSessionGeneration: number;
|
|
170
|
+
socketStatus: 'disconnected' | 'connecting' | 'connected' | 'error';
|
|
171
|
+
socketLastConnectedAt: number | null;
|
|
172
|
+
socketLastDisconnectedAt: number | null;
|
|
173
|
+
isDataReady: boolean;
|
|
174
|
+
nativeUpdateStatus: { available: boolean; updateUrl?: string } | null;
|
|
175
|
+
applySessions: (sessions: (Omit<Session, 'presence'> & { presence?: "online" | number })[]) => void;
|
|
176
|
+
applyMachines: (machines: Machine[], replace?: boolean) => void;
|
|
177
|
+
deleteMachine: (machineId: string) => void;
|
|
178
|
+
applyLoaded: () => void;
|
|
179
|
+
applyReady: () => void;
|
|
180
|
+
applyMessages: (sessionId: string, messages: NormalizedMessage[]) => { changed: string[], hasReadyEvent: boolean };
|
|
181
|
+
applyMessagesLoaded: (sessionId: string) => void;
|
|
182
|
+
applyOlderMessagesPagination: (sessionId: string, info: { hasMore: boolean }) => void;
|
|
183
|
+
applyOlderMessagesLoading: (sessionId: string, isLoading: boolean) => void;
|
|
184
|
+
applySettings: (settings: Settings, version: number) => void;
|
|
185
|
+
applySettingsLocal: (settings: Partial<Settings>) => void;
|
|
186
|
+
applyLocalSettings: (settings: Partial<LocalSettings>) => void;
|
|
187
|
+
applyPurchases: (customerInfo: CustomerInfo) => void;
|
|
188
|
+
applyProfile: (profile: Profile) => void;
|
|
189
|
+
applyGitStatus: (pathKey: string, status: GitStatus | null) => void;
|
|
190
|
+
applyGitStatusFiles: (pathKey: string, files: GitStatusFiles | null) => void;
|
|
191
|
+
applyProjectFiles: (pathKey: string, files: ProjectFilesList | null) => void;
|
|
192
|
+
getSessionPathKey: (sessionId: string) => string | null;
|
|
193
|
+
applyFileCache: (sessionId: string, filePath: string, content: string | null, diff: string | null, isBinary: boolean) => void;
|
|
194
|
+
applyNativeUpdateStatus: (status: { available: boolean; updateUrl?: string } | null) => void;
|
|
195
|
+
isMutableToolCall: (sessionId: string, callId: string) => boolean;
|
|
196
|
+
setRealtimeStatus: (status: 'disconnected' | 'connecting' | 'connected' | 'error') => void;
|
|
197
|
+
setRealtimeMode: (mode: 'idle' | 'agent-speaking' | 'user-speaking', immediate?: boolean) => void;
|
|
198
|
+
clearRealtimeModeDebounce: () => void;
|
|
199
|
+
incrementVoiceSessionGeneration: () => void;
|
|
200
|
+
setSocketStatus: (status: 'disconnected' | 'connecting' | 'connected' | 'error') => void;
|
|
201
|
+
getActiveSessions: () => Session[];
|
|
202
|
+
updateSessionDraft: (sessionId: string, draft: string | null) => void;
|
|
203
|
+
updateSessionPermissionMode: (sessionId: string, mode: string | null) => void;
|
|
204
|
+
updateSessionModelMode: (sessionId: string, mode: string | null) => void;
|
|
205
|
+
updateSessionEffortLevel: (sessionId: string, level: string | null) => void;
|
|
206
|
+
resetSessionAgentOverrides: (sessionId: string) => void;
|
|
207
|
+
// Artifact methods
|
|
208
|
+
applyArtifacts: (artifacts: DecryptedArtifact[]) => void;
|
|
209
|
+
addArtifact: (artifact: DecryptedArtifact) => void;
|
|
210
|
+
updateArtifact: (artifact: DecryptedArtifact) => void;
|
|
211
|
+
deleteArtifact: (artifactId: string) => void;
|
|
212
|
+
deleteSession: (sessionId: string) => void;
|
|
213
|
+
// Friend management methods
|
|
214
|
+
applyFriends: (friends: UserProfile[]) => void;
|
|
215
|
+
applyRelationshipUpdate: (event: RelationshipUpdatedEvent) => void;
|
|
216
|
+
getFriend: (userId: string) => UserProfile | undefined;
|
|
217
|
+
getAcceptedFriends: () => UserProfile[];
|
|
218
|
+
// User cache methods
|
|
219
|
+
applyUsers: (users: Record<string, UserProfile | null>) => void;
|
|
220
|
+
getUser: (userId: string) => UserProfile | null | undefined;
|
|
221
|
+
assumeUsers: (userIds: string[]) => Promise<void>;
|
|
222
|
+
// Feed methods
|
|
223
|
+
applyFeedItems: (items: FeedItem[]) => void;
|
|
224
|
+
clearFeed: () => void;
|
|
225
|
+
// Unread session tracking (memory-only)
|
|
226
|
+
unreadSessionIds: Set<string>;
|
|
227
|
+
currentViewingSessionId: string | null;
|
|
228
|
+
markSessionRead: (sessionId: string) => void;
|
|
229
|
+
markSessionUnread: (sessionId: string) => void;
|
|
230
|
+
setCurrentViewingSession: (sessionId: string | null) => void;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Helper function to build unified list view data from sessions and machines
|
|
234
|
+
function buildSessionListViewData(
|
|
235
|
+
sessions: Record<string, Session>,
|
|
236
|
+
unreadSessionIds?: Set<string>,
|
|
237
|
+
): SessionListViewItem[] {
|
|
238
|
+
// Separate active and inactive sessions
|
|
239
|
+
const activeSessions: Session[] = [];
|
|
240
|
+
const inactiveSessions: Session[] = [];
|
|
241
|
+
|
|
242
|
+
Object.values(sessions).forEach(session => {
|
|
243
|
+
if (isSessionActive(session)) {
|
|
244
|
+
activeSessions.push(session);
|
|
245
|
+
} else {
|
|
246
|
+
inactiveSessions.push(session);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Sort by creation date (newest first) — matches applySessions behavior
|
|
251
|
+
activeSessions.sort((a, b) => b.createdAt - a.createdAt);
|
|
252
|
+
inactiveSessions.sort((a, b) => b.createdAt - a.createdAt);
|
|
253
|
+
|
|
254
|
+
// Build unified list view data
|
|
255
|
+
const listData: SessionListViewItem[] = [];
|
|
256
|
+
|
|
257
|
+
// Add active sessions as a single item at the top (if any)
|
|
258
|
+
if (activeSessions.length > 0) {
|
|
259
|
+
listData.push({ type: 'active-sessions', sessions: activeSessions.map(s => buildSessionRowData(s, unreadSessionIds)) });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Group inactive sessions by date
|
|
263
|
+
const now = new Date();
|
|
264
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
265
|
+
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
|
266
|
+
|
|
267
|
+
let currentDateGroup: Session[] = [];
|
|
268
|
+
let currentDateString: string | null = null;
|
|
269
|
+
|
|
270
|
+
for (const session of inactiveSessions) {
|
|
271
|
+
const sessionDate = new Date(session.createdAt);
|
|
272
|
+
const dateString = sessionDate.toDateString();
|
|
273
|
+
|
|
274
|
+
if (currentDateString !== dateString) {
|
|
275
|
+
// Process previous group
|
|
276
|
+
if (currentDateGroup.length > 0 && currentDateString) {
|
|
277
|
+
const groupDate = new Date(currentDateString);
|
|
278
|
+
const sessionDateOnly = new Date(groupDate.getFullYear(), groupDate.getMonth(), groupDate.getDate());
|
|
279
|
+
|
|
280
|
+
let headerTitle: string;
|
|
281
|
+
if (sessionDateOnly.getTime() === today.getTime()) {
|
|
282
|
+
headerTitle = 'Today';
|
|
283
|
+
} else if (sessionDateOnly.getTime() === yesterday.getTime()) {
|
|
284
|
+
headerTitle = 'Yesterday';
|
|
285
|
+
} else {
|
|
286
|
+
const diffTime = today.getTime() - sessionDateOnly.getTime();
|
|
287
|
+
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
288
|
+
headerTitle = `${diffDays} days ago`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
listData.push({ type: 'header', title: headerTitle });
|
|
292
|
+
currentDateGroup.forEach(sess => {
|
|
293
|
+
listData.push({ type: 'session', session: buildSessionRowData(sess, unreadSessionIds) });
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Start new group
|
|
298
|
+
currentDateString = dateString;
|
|
299
|
+
currentDateGroup = [session];
|
|
300
|
+
} else {
|
|
301
|
+
currentDateGroup.push(session);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Process final group
|
|
306
|
+
if (currentDateGroup.length > 0 && currentDateString) {
|
|
307
|
+
const groupDate = new Date(currentDateString);
|
|
308
|
+
const sessionDateOnly = new Date(groupDate.getFullYear(), groupDate.getMonth(), groupDate.getDate());
|
|
309
|
+
|
|
310
|
+
let headerTitle: string;
|
|
311
|
+
if (sessionDateOnly.getTime() === today.getTime()) {
|
|
312
|
+
headerTitle = 'Today';
|
|
313
|
+
} else if (sessionDateOnly.getTime() === yesterday.getTime()) {
|
|
314
|
+
headerTitle = 'Yesterday';
|
|
315
|
+
} else {
|
|
316
|
+
const diffTime = today.getTime() - sessionDateOnly.getTime();
|
|
317
|
+
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
318
|
+
headerTitle = `${diffDays} days ago`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
listData.push({ type: 'header', title: headerTitle });
|
|
322
|
+
currentDateGroup.forEach(sess => {
|
|
323
|
+
listData.push({ type: 'session', session: buildSessionRowData(sess, unreadSessionIds) });
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return listData;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export const storage = create<StorageState>()((set, get) => {
|
|
331
|
+
let { settings, version } = loadSettings();
|
|
332
|
+
let localSettings = loadLocalSettings();
|
|
333
|
+
let purchases = loadPurchases();
|
|
334
|
+
let profile = loadProfile();
|
|
335
|
+
let sessionDrafts = loadSessionDrafts();
|
|
336
|
+
let sessionPermissionModes = loadSessionPermissionModes();
|
|
337
|
+
let sessionModelModes = loadSessionModelModes();
|
|
338
|
+
let sessionEffortLevels = loadSessionEffortLevels();
|
|
339
|
+
return {
|
|
340
|
+
settings,
|
|
341
|
+
settingsVersion: version,
|
|
342
|
+
localSettings,
|
|
343
|
+
purchases,
|
|
344
|
+
profile,
|
|
345
|
+
sessions: {},
|
|
346
|
+
machines: {},
|
|
347
|
+
artifacts: {}, // Initialize artifacts
|
|
348
|
+
friends: {}, // Initialize relationships cache
|
|
349
|
+
users: {}, // Initialize global user cache
|
|
350
|
+
feedItems: [], // Initialize feed items list
|
|
351
|
+
feedHead: null,
|
|
352
|
+
feedTail: null,
|
|
353
|
+
feedHasMore: false,
|
|
354
|
+
feedLoaded: false, // Initialize as false
|
|
355
|
+
friendsLoaded: false, // Initialize as false
|
|
356
|
+
sessionsData: null, // Legacy - to be removed
|
|
357
|
+
sessionListViewData: null,
|
|
358
|
+
sessionMessages: {},
|
|
359
|
+
pathGitStatus: {},
|
|
360
|
+
pathGitStatusFiles: {},
|
|
361
|
+
pathProjectFiles: {},
|
|
362
|
+
sessionFileCache: {},
|
|
363
|
+
realtimeStatus: 'disconnected',
|
|
364
|
+
realtimeMode: 'idle',
|
|
365
|
+
voiceSessionGeneration: 0,
|
|
366
|
+
socketStatus: 'disconnected',
|
|
367
|
+
socketLastConnectedAt: null,
|
|
368
|
+
socketLastDisconnectedAt: null,
|
|
369
|
+
isDataReady: false,
|
|
370
|
+
nativeUpdateStatus: null,
|
|
371
|
+
unreadSessionIds: new Set<string>(),
|
|
372
|
+
currentViewingSessionId: null,
|
|
373
|
+
isMutableToolCall: (sessionId: string, callId: string) => {
|
|
374
|
+
const sessionMessages = get().sessionMessages[sessionId];
|
|
375
|
+
if (!sessionMessages) {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
const toolCall = sessionMessages.reducerState.toolIdToMessageId.get(callId);
|
|
379
|
+
if (!toolCall) {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
const toolCallMessage = sessionMessages.messagesMap[toolCall];
|
|
383
|
+
if (!toolCallMessage || toolCallMessage.kind !== 'tool-call') {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
return toolCallMessage.tool?.name ? isMutableTool(toolCallMessage.tool?.name) : true;
|
|
387
|
+
},
|
|
388
|
+
getActiveSessions: () => {
|
|
389
|
+
const state = get();
|
|
390
|
+
return Object.values(state.sessions).filter(s => s.active);
|
|
391
|
+
},
|
|
392
|
+
applySessions: (sessions: (Omit<Session, 'presence'> & { presence?: "online" | number })[]) => set((state) => {
|
|
393
|
+
// Load drafts and permission modes if sessions are empty (initial load)
|
|
394
|
+
const isInitialLoad = Object.keys(state.sessions).length === 0;
|
|
395
|
+
const savedDrafts = isInitialLoad ? sessionDrafts : {};
|
|
396
|
+
const savedPermissionModes = isInitialLoad ? sessionPermissionModes : {};
|
|
397
|
+
const savedModelModes = isInitialLoad ? sessionModelModes : {};
|
|
398
|
+
const savedEffortLevels = isInitialLoad ? sessionEffortLevels : {};
|
|
399
|
+
|
|
400
|
+
// Merge new sessions with existing ones
|
|
401
|
+
const mergedSessions: Record<string, Session> = { ...state.sessions };
|
|
402
|
+
|
|
403
|
+
// Update sessions with calculated presence using centralized resolver
|
|
404
|
+
sessions.forEach(session => {
|
|
405
|
+
// Use centralized resolver for consistent state management
|
|
406
|
+
const presence = resolveSessionOnlineState(session);
|
|
407
|
+
|
|
408
|
+
// Preserve explicit local overrides if they exist, or load from
|
|
409
|
+
// saved data. Missing/null means "no user override"; the UI and
|
|
410
|
+
// CLI resolve code defaults later.
|
|
411
|
+
const existingDraft = state.sessions[session.id]?.draft;
|
|
412
|
+
const savedDraft = savedDrafts[session.id];
|
|
413
|
+
const savedPermissionMode = savedPermissionModes[session.id] ?? null;
|
|
414
|
+
const existingPermissionModeRaw = state.sessions[session.id]?.permissionMode ?? null;
|
|
415
|
+
const existingPermissionMode = existingPermissionModeRaw === 'default' && savedPermissionMode !== 'default'
|
|
416
|
+
? null
|
|
417
|
+
: existingPermissionModeRaw;
|
|
418
|
+
const resolvedPermissionMode = existingPermissionMode ?? savedPermissionMode ?? session.permissionMode ?? null;
|
|
419
|
+
|
|
420
|
+
// Restore model mode / effort level from MMKV on first load — server
|
|
421
|
+
// does not sync these, and they used to reset on every app restart (#1028).
|
|
422
|
+
const savedModelMode = savedModelModes[session.id] ?? null;
|
|
423
|
+
const existingModelModeRaw = state.sessions[session.id]?.modelMode ?? null;
|
|
424
|
+
const existingModelMode = existingModelModeRaw === 'default' && savedModelMode !== 'default'
|
|
425
|
+
? null
|
|
426
|
+
: existingModelModeRaw;
|
|
427
|
+
const resolvedModelMode = existingModelMode ?? savedModelMode ?? session.modelMode ?? null;
|
|
428
|
+
const existingEffortLevel = state.sessions[session.id]?.effortLevel ?? null;
|
|
429
|
+
const resolvedEffortLevel = existingEffortLevel ?? savedEffortLevels[session.id] ?? session.effortLevel ?? null;
|
|
430
|
+
|
|
431
|
+
mergedSessions[session.id] = {
|
|
432
|
+
...session,
|
|
433
|
+
presence,
|
|
434
|
+
draft: existingDraft || savedDraft || session.draft || null,
|
|
435
|
+
permissionMode: resolvedPermissionMode,
|
|
436
|
+
modelMode: resolvedModelMode,
|
|
437
|
+
effortLevel: resolvedEffortLevel,
|
|
438
|
+
};
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Build active set from all sessions (including existing ones)
|
|
442
|
+
const activeSet = new Set<string>();
|
|
443
|
+
Object.values(mergedSessions).forEach(session => {
|
|
444
|
+
if (isSessionActive(session)) {
|
|
445
|
+
activeSet.add(session.id);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Separate active and inactive sessions
|
|
450
|
+
const activeSessions: Session[] = [];
|
|
451
|
+
const inactiveSessions: Session[] = [];
|
|
452
|
+
|
|
453
|
+
// Process all sessions from merged set
|
|
454
|
+
Object.values(mergedSessions).forEach(session => {
|
|
455
|
+
if (activeSet.has(session.id)) {
|
|
456
|
+
activeSessions.push(session);
|
|
457
|
+
} else {
|
|
458
|
+
inactiveSessions.push(session);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Sort both arrays by creation date for stable ordering
|
|
463
|
+
activeSessions.sort((a, b) => b.createdAt - a.createdAt);
|
|
464
|
+
inactiveSessions.sort((a, b) => b.createdAt - a.createdAt);
|
|
465
|
+
|
|
466
|
+
// Build flat list data for FlashList
|
|
467
|
+
const listData: SessionListItem[] = [];
|
|
468
|
+
|
|
469
|
+
if (activeSessions.length > 0) {
|
|
470
|
+
listData.push('online');
|
|
471
|
+
listData.push(...activeSessions);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Legacy sessionsData - to be removed
|
|
475
|
+
// Machines are now integrated into sessionListViewData
|
|
476
|
+
|
|
477
|
+
if (inactiveSessions.length > 0) {
|
|
478
|
+
listData.push('offline');
|
|
479
|
+
listData.push(...inactiveSessions);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// console.log(`📊 Storage: applySessions called with ${sessions.length} sessions, active: ${activeSessions.length}, inactive: ${inactiveSessions.length}`);
|
|
483
|
+
|
|
484
|
+
// Process AgentState updates for sessions that already have messages loaded
|
|
485
|
+
const updatedSessionMessages = { ...state.sessionMessages };
|
|
486
|
+
|
|
487
|
+
sessions.forEach(session => {
|
|
488
|
+
const oldSession = state.sessions[session.id];
|
|
489
|
+
const newSession = mergedSessions[session.id];
|
|
490
|
+
|
|
491
|
+
// Check if sessionMessages exists AND agentStateVersion is newer
|
|
492
|
+
const existingSessionMessages = updatedSessionMessages[session.id];
|
|
493
|
+
if (existingSessionMessages && newSession.agentState &&
|
|
494
|
+
(!oldSession || newSession.agentStateVersion > (oldSession.agentStateVersion || 0))) {
|
|
495
|
+
|
|
496
|
+
// Check for NEW permission requests before processing
|
|
497
|
+
const currentRealtimeSessionId = getCurrentRealtimeSessionId();
|
|
498
|
+
const voiceSession = getVoiceSession();
|
|
499
|
+
|
|
500
|
+
// console.log('[REALTIME DEBUG] Permission check:', {
|
|
501
|
+
// currentRealtimeSessionId,
|
|
502
|
+
// sessionId: session.id,
|
|
503
|
+
// match: currentRealtimeSessionId === session.id,
|
|
504
|
+
// hasVoiceSession: !!voiceSession,
|
|
505
|
+
// oldRequests: Object.keys(oldSession?.agentState?.requests || {}),
|
|
506
|
+
// newRequests: Object.keys(newSession.agentState?.requests || {})
|
|
507
|
+
// });
|
|
508
|
+
|
|
509
|
+
if (currentRealtimeSessionId === session.id && voiceSession) {
|
|
510
|
+
const oldRequests = oldSession?.agentState?.requests || {};
|
|
511
|
+
const newRequests = newSession.agentState?.requests || {};
|
|
512
|
+
|
|
513
|
+
// Find NEW permission requests only
|
|
514
|
+
for (const [requestId, request] of Object.entries(newRequests)) {
|
|
515
|
+
if (!oldRequests[requestId]) {
|
|
516
|
+
// This is a NEW permission request
|
|
517
|
+
const toolName = request.tool;
|
|
518
|
+
// console.log('[REALTIME DEBUG] Sending permission notification for:', toolName);
|
|
519
|
+
voiceSession.sendTextMessage(
|
|
520
|
+
`Claude is requesting permission to use the ${toolName} tool`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Process new AgentState through reducer
|
|
527
|
+
const reducerResult = reducer(existingSessionMessages.reducerState, [], newSession.agentState);
|
|
528
|
+
const processedMessages = reducerResult.messages;
|
|
529
|
+
|
|
530
|
+
// Always update the session messages, even if no new messages were created
|
|
531
|
+
// This ensures the reducer state is updated with the new AgentState
|
|
532
|
+
const mergedMessagesMap = { ...existingSessionMessages.messagesMap };
|
|
533
|
+
processedMessages.forEach(message => {
|
|
534
|
+
mergedMessagesMap[message.id] = message;
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const messagesArray = Object.values(mergedMessagesMap)
|
|
538
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
539
|
+
|
|
540
|
+
updatedSessionMessages[session.id] = {
|
|
541
|
+
messages: messagesArray,
|
|
542
|
+
messagesMap: mergedMessagesMap,
|
|
543
|
+
reducerState: existingSessionMessages.reducerState, // The reducer modifies state in-place, so this has the updates
|
|
544
|
+
isLoaded: existingSessionMessages.isLoaded,
|
|
545
|
+
hasMoreOlder: existingSessionMessages.hasMoreOlder,
|
|
546
|
+
isLoadingOlder: existingSessionMessages.isLoadingOlder
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// IMPORTANT: Copy latestUsage from reducerState to Session for immediate availability
|
|
550
|
+
if (existingSessionMessages.reducerState.latestUsage) {
|
|
551
|
+
mergedSessions[session.id] = {
|
|
552
|
+
...mergedSessions[session.id],
|
|
553
|
+
latestUsage: { ...existingSessionMessages.reducerState.latestUsage }
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Track unread: detect when agent finishes all work for a request.
|
|
560
|
+
// "Was active" = thinking or had pending permission requests.
|
|
561
|
+
// "Now idle" = online, not thinking, no pending permissions.
|
|
562
|
+
let unreadSessionIds = state.unreadSessionIds;
|
|
563
|
+
sessions.forEach(session => {
|
|
564
|
+
const oldSession = state.sessions[session.id];
|
|
565
|
+
if (!oldSession) return;
|
|
566
|
+
const wasActive = oldSession.thinking === true
|
|
567
|
+
|| (oldSession.agentState?.requests && Object.keys(oldSession.agentState.requests).length > 0);
|
|
568
|
+
const newSession = mergedSessions[session.id];
|
|
569
|
+
if (!newSession || !wasActive) return;
|
|
570
|
+
const isNowIdle = newSession.thinking !== true
|
|
571
|
+
&& newSession.presence === 'online'
|
|
572
|
+
&& (!newSession.agentState?.requests || Object.keys(newSession.agentState.requests).length === 0);
|
|
573
|
+
if (isNowIdle && state.currentViewingSessionId !== session.id) {
|
|
574
|
+
if (!unreadSessionIds.has(session.id)) {
|
|
575
|
+
unreadSessionIds = new Set(unreadSessionIds);
|
|
576
|
+
unreadSessionIds.add(session.id);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// Build new unified list view data
|
|
582
|
+
const sessionListViewData = buildSessionListViewData(
|
|
583
|
+
mergedSessions,
|
|
584
|
+
unreadSessionIds,
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
...state,
|
|
589
|
+
sessions: mergedSessions,
|
|
590
|
+
sessionsData: listData, // Legacy - to be removed
|
|
591
|
+
sessionListViewData,
|
|
592
|
+
sessionMessages: updatedSessionMessages,
|
|
593
|
+
unreadSessionIds,
|
|
594
|
+
};
|
|
595
|
+
}),
|
|
596
|
+
applyLoaded: () => set((state) => {
|
|
597
|
+
const result = {
|
|
598
|
+
...state,
|
|
599
|
+
sessionsData: []
|
|
600
|
+
};
|
|
601
|
+
return result;
|
|
602
|
+
}),
|
|
603
|
+
applyReady: () => set((state) => ({
|
|
604
|
+
...state,
|
|
605
|
+
isDataReady: true
|
|
606
|
+
})),
|
|
607
|
+
applyMessages: (sessionId: string, messages: NormalizedMessage[]) => {
|
|
608
|
+
let changed = new Set<string>();
|
|
609
|
+
let hasReadyEvent = false;
|
|
610
|
+
|
|
611
|
+
// Track plan mode transitions through the batch in order.
|
|
612
|
+
// Set true on EnterPlanMode, false on ExitPlanMode. The final value
|
|
613
|
+
// tells us whether the batch ends with an unresolved plan entry.
|
|
614
|
+
// This prevents history replays (which contain both Enter + Exit) from
|
|
615
|
+
// re-triggering plan mode, while still catching real-time EnterPlanMode.
|
|
616
|
+
let shouldEnterPlanMode = false;
|
|
617
|
+
for (const msg of messages) {
|
|
618
|
+
if (msg.role === 'agent') {
|
|
619
|
+
for (const c of msg.content) {
|
|
620
|
+
if (c.type === 'tool-call') {
|
|
621
|
+
if (c.name === 'EnterPlanMode' || c.name === 'enter_plan_mode') {
|
|
622
|
+
shouldEnterPlanMode = true;
|
|
623
|
+
} else if (c.name === 'ExitPlanMode' || c.name === 'exit_plan_mode') {
|
|
624
|
+
shouldEnterPlanMode = false;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
set((state) => {
|
|
632
|
+
|
|
633
|
+
// Resolve session messages state
|
|
634
|
+
const existingSession: SessionMessages = state.sessionMessages[sessionId] || {
|
|
635
|
+
messages: [],
|
|
636
|
+
messagesMap: {},
|
|
637
|
+
reducerState: createReducer(),
|
|
638
|
+
isLoaded: false,
|
|
639
|
+
hasMoreOlder: false,
|
|
640
|
+
isLoadingOlder: false
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
// Get the session's agentState if available
|
|
644
|
+
const session = state.sessions[sessionId];
|
|
645
|
+
const agentState = session?.agentState;
|
|
646
|
+
|
|
647
|
+
// Messages are already normalized, no need to process them again
|
|
648
|
+
const normalizedMessages = messages;
|
|
649
|
+
|
|
650
|
+
// Run reducer with agentState
|
|
651
|
+
const reducerResult = reducer(existingSession.reducerState, normalizedMessages, agentState);
|
|
652
|
+
const processedMessages = reducerResult.messages;
|
|
653
|
+
for (let message of processedMessages) {
|
|
654
|
+
changed.add(message.id);
|
|
655
|
+
}
|
|
656
|
+
if (reducerResult.hasReadyEvent) {
|
|
657
|
+
hasReadyEvent = true;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Merge messages
|
|
661
|
+
const mergedMessagesMap = { ...existingSession.messagesMap };
|
|
662
|
+
processedMessages.forEach(message => {
|
|
663
|
+
mergedMessagesMap[message.id] = message;
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Convert to array and sort by createdAt
|
|
667
|
+
const messagesArray = Object.values(mergedMessagesMap)
|
|
668
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
669
|
+
|
|
670
|
+
// Update session with todos and latestUsage
|
|
671
|
+
// IMPORTANT: We extract latestUsage from the mutable reducerState and copy it to the Session object
|
|
672
|
+
// This ensures latestUsage is available immediately on load, even before messages are fully loaded
|
|
673
|
+
let updatedSessions = state.sessions;
|
|
674
|
+
const needsUpdate = (reducerResult.todos !== undefined || existingSession.reducerState.latestUsage || shouldEnterPlanMode) && session;
|
|
675
|
+
|
|
676
|
+
if (needsUpdate) {
|
|
677
|
+
updatedSessions = {
|
|
678
|
+
...state.sessions,
|
|
679
|
+
[sessionId]: {
|
|
680
|
+
...session,
|
|
681
|
+
...(reducerResult.todos !== undefined && { todos: reducerResult.todos }),
|
|
682
|
+
// Copy latestUsage from reducerState to make it immediately available
|
|
683
|
+
latestUsage: existingSession.reducerState.latestUsage ? {
|
|
684
|
+
...existingSession.reducerState.latestUsage
|
|
685
|
+
} : session.latestUsage,
|
|
686
|
+
// Auto-switch to plan mode when EnterPlanMode tool call is detected
|
|
687
|
+
...(shouldEnterPlanMode && { permissionMode: 'plan' })
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
...state,
|
|
694
|
+
sessions: updatedSessions,
|
|
695
|
+
sessionMessages: {
|
|
696
|
+
...state.sessionMessages,
|
|
697
|
+
[sessionId]: {
|
|
698
|
+
...existingSession,
|
|
699
|
+
messages: messagesArray,
|
|
700
|
+
messagesMap: mergedMessagesMap,
|
|
701
|
+
reducerState: existingSession.reducerState, // Explicitly include the mutated reducer state
|
|
702
|
+
isLoaded: true
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// Persist plan mode change
|
|
709
|
+
if (shouldEnterPlanMode) {
|
|
710
|
+
const allModes: Record<string, string> = {};
|
|
711
|
+
const currentState = get();
|
|
712
|
+
Object.entries(currentState.sessions).forEach(([id, sess]) => {
|
|
713
|
+
if (sess.permissionMode && sess.permissionMode !== 'default') {
|
|
714
|
+
allModes[id] = sess.permissionMode;
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
saveSessionPermissionModes(allModes);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return { changed: Array.from(changed), hasReadyEvent };
|
|
721
|
+
},
|
|
722
|
+
applyMessagesLoaded: (sessionId: string) => set((state) => {
|
|
723
|
+
const existingSession = state.sessionMessages[sessionId];
|
|
724
|
+
let result: StorageState;
|
|
725
|
+
|
|
726
|
+
if (!existingSession) {
|
|
727
|
+
// First time loading - check for AgentState
|
|
728
|
+
const session = state.sessions[sessionId];
|
|
729
|
+
const agentState = session?.agentState;
|
|
730
|
+
|
|
731
|
+
// Create new reducer state
|
|
732
|
+
const reducerState = createReducer();
|
|
733
|
+
|
|
734
|
+
// Process AgentState if it exists
|
|
735
|
+
let messages: Message[] = [];
|
|
736
|
+
let messagesMap: Record<string, Message> = {};
|
|
737
|
+
|
|
738
|
+
if (agentState) {
|
|
739
|
+
// Process AgentState through reducer to get initial permission messages
|
|
740
|
+
const reducerResult = reducer(reducerState, [], agentState);
|
|
741
|
+
const processedMessages = reducerResult.messages;
|
|
742
|
+
|
|
743
|
+
processedMessages.forEach(message => {
|
|
744
|
+
messagesMap[message.id] = message;
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
messages = Object.values(messagesMap)
|
|
748
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Extract latestUsage from reducerState if available and update session
|
|
752
|
+
let updatedSessions = state.sessions;
|
|
753
|
+
if (session && reducerState.latestUsage) {
|
|
754
|
+
updatedSessions = {
|
|
755
|
+
...state.sessions,
|
|
756
|
+
[sessionId]: {
|
|
757
|
+
...session,
|
|
758
|
+
latestUsage: { ...reducerState.latestUsage }
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
result = {
|
|
764
|
+
...state,
|
|
765
|
+
sessions: updatedSessions,
|
|
766
|
+
sessionMessages: {
|
|
767
|
+
...state.sessionMessages,
|
|
768
|
+
[sessionId]: {
|
|
769
|
+
reducerState,
|
|
770
|
+
messages,
|
|
771
|
+
messagesMap,
|
|
772
|
+
isLoaded: true,
|
|
773
|
+
hasMoreOlder: false,
|
|
774
|
+
isLoadingOlder: false
|
|
775
|
+
} satisfies SessionMessages
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
} else {
|
|
779
|
+
result = {
|
|
780
|
+
...state,
|
|
781
|
+
sessionMessages: {
|
|
782
|
+
...state.sessionMessages,
|
|
783
|
+
[sessionId]: {
|
|
784
|
+
...existingSession,
|
|
785
|
+
isLoaded: true
|
|
786
|
+
} satisfies SessionMessages
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return result;
|
|
792
|
+
}),
|
|
793
|
+
applyOlderMessagesPagination: (sessionId: string, info: { hasMore: boolean }) => set((state) => {
|
|
794
|
+
const existing = state.sessionMessages[sessionId];
|
|
795
|
+
if (!existing) {
|
|
796
|
+
// Pagination metadata is only meaningful once the session has
|
|
797
|
+
// a SessionMessages entry. The fetch path always creates one
|
|
798
|
+
// through applyMessages / applyMessagesLoaded before calling
|
|
799
|
+
// this — but if for any reason it hasn't, ignore the update
|
|
800
|
+
// rather than synthesize a partial entry.
|
|
801
|
+
return state;
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
...state,
|
|
805
|
+
sessionMessages: {
|
|
806
|
+
...state.sessionMessages,
|
|
807
|
+
[sessionId]: {
|
|
808
|
+
...existing,
|
|
809
|
+
hasMoreOlder: info.hasMore
|
|
810
|
+
} satisfies SessionMessages
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}),
|
|
814
|
+
applyOlderMessagesLoading: (sessionId: string, isLoading: boolean) => set((state) => {
|
|
815
|
+
const existing = state.sessionMessages[sessionId];
|
|
816
|
+
if (!existing) {
|
|
817
|
+
return state;
|
|
818
|
+
}
|
|
819
|
+
if (existing.isLoadingOlder === isLoading) {
|
|
820
|
+
return state;
|
|
821
|
+
}
|
|
822
|
+
return {
|
|
823
|
+
...state,
|
|
824
|
+
sessionMessages: {
|
|
825
|
+
...state.sessionMessages,
|
|
826
|
+
[sessionId]: {
|
|
827
|
+
...existing,
|
|
828
|
+
isLoadingOlder: isLoading
|
|
829
|
+
} satisfies SessionMessages
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
}),
|
|
833
|
+
applySettingsLocal: (settings: Partial<Settings>) => set((state) => {
|
|
834
|
+
saveSettings(applySettings(state.settings, settings), state.settingsVersion ?? 0);
|
|
835
|
+
return {
|
|
836
|
+
...state,
|
|
837
|
+
settings: applySettings(state.settings, settings)
|
|
838
|
+
};
|
|
839
|
+
}),
|
|
840
|
+
applySettings: (settings: Settings, version: number) => set((state) => {
|
|
841
|
+
if (state.settingsVersion === null || state.settingsVersion < version) {
|
|
842
|
+
saveSettings(settings, version);
|
|
843
|
+
return {
|
|
844
|
+
...state,
|
|
845
|
+
settings,
|
|
846
|
+
settingsVersion: version
|
|
847
|
+
};
|
|
848
|
+
} else {
|
|
849
|
+
return state;
|
|
850
|
+
}
|
|
851
|
+
}),
|
|
852
|
+
applyLocalSettings: (delta: Partial<LocalSettings>) => set((state) => {
|
|
853
|
+
const updatedLocalSettings = applyLocalSettings(state.localSettings, delta);
|
|
854
|
+
saveLocalSettings(updatedLocalSettings);
|
|
855
|
+
return {
|
|
856
|
+
...state,
|
|
857
|
+
localSettings: updatedLocalSettings
|
|
858
|
+
};
|
|
859
|
+
}),
|
|
860
|
+
applyPurchases: (customerInfo: CustomerInfo) => set((state) => {
|
|
861
|
+
// Transform CustomerInfo to our Purchases format
|
|
862
|
+
const purchases = customerInfoToPurchases(customerInfo);
|
|
863
|
+
|
|
864
|
+
// Always save and update - no need for version checks
|
|
865
|
+
savePurchases(purchases);
|
|
866
|
+
return {
|
|
867
|
+
...state,
|
|
868
|
+
purchases
|
|
869
|
+
};
|
|
870
|
+
}),
|
|
871
|
+
applyProfile: (profile: Profile) => set((state) => {
|
|
872
|
+
// Always save and update profile
|
|
873
|
+
saveProfile(profile);
|
|
874
|
+
return {
|
|
875
|
+
...state,
|
|
876
|
+
profile
|
|
877
|
+
};
|
|
878
|
+
}),
|
|
879
|
+
applyGitStatus: (pathKey: string, status: GitStatus | null) => set((state) => ({
|
|
880
|
+
...state,
|
|
881
|
+
pathGitStatus: {
|
|
882
|
+
...state.pathGitStatus,
|
|
883
|
+
[pathKey]: status
|
|
884
|
+
}
|
|
885
|
+
})),
|
|
886
|
+
applyGitStatusFiles: (pathKey: string, files: GitStatusFiles | null) => set((state) => {
|
|
887
|
+
// Short-circuit on no-op writes. gitStatusSync.invalidate fires on every
|
|
888
|
+
// mutable-tool message and on every update-session, but most of those
|
|
889
|
+
// don't actually change the file set. Without this guard, every fetch
|
|
890
|
+
// produces a fresh object reference, the useSessionGitStatusFiles
|
|
891
|
+
// subscription fires, and AllFilesDiffView nukes its scroll position
|
|
892
|
+
// and re-runs every git diff. fast-deep-equal handles arrays + nested
|
|
893
|
+
// objects so we don't have to enumerate fields.
|
|
894
|
+
if (equal(state.pathGitStatusFiles[pathKey] ?? null, files)) {
|
|
895
|
+
return state;
|
|
896
|
+
}
|
|
897
|
+
return {
|
|
898
|
+
...state,
|
|
899
|
+
pathGitStatusFiles: {
|
|
900
|
+
...state.pathGitStatusFiles,
|
|
901
|
+
[pathKey]: files
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
}),
|
|
905
|
+
applyProjectFiles: (pathKey: string, files: ProjectFilesList | null) => set((state) => ({
|
|
906
|
+
...state,
|
|
907
|
+
pathProjectFiles: {
|
|
908
|
+
...state.pathProjectFiles,
|
|
909
|
+
[pathKey]: files
|
|
910
|
+
}
|
|
911
|
+
})),
|
|
912
|
+
applyFileCache: (sessionId: string, filePath: string, content: string | null, diff: string | null, isBinary: boolean) => set((state) => ({
|
|
913
|
+
...state,
|
|
914
|
+
sessionFileCache: {
|
|
915
|
+
...state.sessionFileCache,
|
|
916
|
+
[sessionId]: {
|
|
917
|
+
...(state.sessionFileCache[sessionId] || {}),
|
|
918
|
+
[filePath]: { content, diff, isBinary, cachedAt: Date.now() }
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
})),
|
|
922
|
+
applyNativeUpdateStatus: (status: { available: boolean; updateUrl?: string } | null) => set((state) => ({
|
|
923
|
+
...state,
|
|
924
|
+
nativeUpdateStatus: status
|
|
925
|
+
})),
|
|
926
|
+
setRealtimeStatus: (status: 'disconnected' | 'connecting' | 'connected' | 'error') => set((state) => ({
|
|
927
|
+
...state,
|
|
928
|
+
realtimeStatus: status
|
|
929
|
+
})),
|
|
930
|
+
setRealtimeMode: (mode: 'idle' | 'agent-speaking' | 'user-speaking', immediate?: boolean) => {
|
|
931
|
+
if (immediate) {
|
|
932
|
+
// Clear any pending debounce and set immediately
|
|
933
|
+
if (realtimeModeDebounceTimer) {
|
|
934
|
+
clearTimeout(realtimeModeDebounceTimer);
|
|
935
|
+
realtimeModeDebounceTimer = null;
|
|
936
|
+
}
|
|
937
|
+
set((state) => ({ ...state, realtimeMode: mode }));
|
|
938
|
+
} else {
|
|
939
|
+
// Debounce mode changes to avoid flickering
|
|
940
|
+
if (realtimeModeDebounceTimer) {
|
|
941
|
+
clearTimeout(realtimeModeDebounceTimer);
|
|
942
|
+
}
|
|
943
|
+
realtimeModeDebounceTimer = setTimeout(() => {
|
|
944
|
+
realtimeModeDebounceTimer = null;
|
|
945
|
+
set((state) => ({ ...state, realtimeMode: mode }));
|
|
946
|
+
}, REALTIME_MODE_DEBOUNCE_MS);
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
clearRealtimeModeDebounce: () => {
|
|
950
|
+
if (realtimeModeDebounceTimer) {
|
|
951
|
+
clearTimeout(realtimeModeDebounceTimer);
|
|
952
|
+
realtimeModeDebounceTimer = null;
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
incrementVoiceSessionGeneration: () => set((state) => ({
|
|
956
|
+
...state,
|
|
957
|
+
voiceSessionGeneration: state.voiceSessionGeneration + 1
|
|
958
|
+
})),
|
|
959
|
+
setSocketStatus: (status: 'disconnected' | 'connecting' | 'connected' | 'error') => set((state) => {
|
|
960
|
+
const now = Date.now();
|
|
961
|
+
const updates: Partial<StorageState> = {
|
|
962
|
+
socketStatus: status
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// Update timestamp based on status
|
|
966
|
+
if (status === 'connected') {
|
|
967
|
+
updates.socketLastConnectedAt = now;
|
|
968
|
+
} else if (status === 'disconnected' || status === 'error') {
|
|
969
|
+
updates.socketLastDisconnectedAt = now;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
return {
|
|
973
|
+
...state,
|
|
974
|
+
...updates
|
|
975
|
+
};
|
|
976
|
+
}),
|
|
977
|
+
updateSessionDraft: (sessionId: string, draft: string | null) => set((state) => {
|
|
978
|
+
const session = state.sessions[sessionId];
|
|
979
|
+
if (!session) return state;
|
|
980
|
+
|
|
981
|
+
// Don't store empty strings, convert to null
|
|
982
|
+
const normalizedDraft = draft?.trim() ? draft : null;
|
|
983
|
+
|
|
984
|
+
// Collect all drafts for persistence
|
|
985
|
+
const allDrafts: Record<string, string> = {};
|
|
986
|
+
Object.entries(state.sessions).forEach(([id, sess]) => {
|
|
987
|
+
if (id === sessionId) {
|
|
988
|
+
if (normalizedDraft) {
|
|
989
|
+
allDrafts[id] = normalizedDraft;
|
|
990
|
+
}
|
|
991
|
+
} else if (sess.draft) {
|
|
992
|
+
allDrafts[id] = sess.draft;
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// Persist drafts
|
|
997
|
+
saveSessionDrafts(allDrafts);
|
|
998
|
+
|
|
999
|
+
const updatedSessions = {
|
|
1000
|
+
...state.sessions,
|
|
1001
|
+
[sessionId]: {
|
|
1002
|
+
...session,
|
|
1003
|
+
draft: normalizedDraft
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
return {
|
|
1008
|
+
...state,
|
|
1009
|
+
sessions: updatedSessions,
|
|
1010
|
+
sessionListViewData: buildSessionListViewData(updatedSessions)
|
|
1011
|
+
};
|
|
1012
|
+
}),
|
|
1013
|
+
updateSessionPermissionMode: (sessionId: string, mode: string | null) => set((state) => {
|
|
1014
|
+
const session = state.sessions[sessionId];
|
|
1015
|
+
if (!session) return state;
|
|
1016
|
+
|
|
1017
|
+
// Update the session with the new permission mode
|
|
1018
|
+
const updatedSessions = {
|
|
1019
|
+
...state.sessions,
|
|
1020
|
+
[sessionId]: {
|
|
1021
|
+
...session,
|
|
1022
|
+
permissionMode: mode
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
// Collect all permission modes for persistence
|
|
1027
|
+
const allModes: Record<string, string> = {};
|
|
1028
|
+
Object.entries(updatedSessions).forEach(([id, sess]) => {
|
|
1029
|
+
if (sess.permissionMode) {
|
|
1030
|
+
allModes[id] = sess.permissionMode;
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
// Persist only explicit overrides; null/missing means code default.
|
|
1035
|
+
saveSessionPermissionModes(allModes);
|
|
1036
|
+
|
|
1037
|
+
// No need to rebuild sessionListViewData since permission mode doesn't affect the list display
|
|
1038
|
+
return {
|
|
1039
|
+
...state,
|
|
1040
|
+
sessions: updatedSessions
|
|
1041
|
+
};
|
|
1042
|
+
}),
|
|
1043
|
+
updateSessionModelMode: (sessionId: string, mode: string | null) => set((state) => {
|
|
1044
|
+
const session = state.sessions[sessionId];
|
|
1045
|
+
if (!session) return state;
|
|
1046
|
+
|
|
1047
|
+
// Update the session with the new model mode
|
|
1048
|
+
const updatedSessions = {
|
|
1049
|
+
...state.sessions,
|
|
1050
|
+
[sessionId]: {
|
|
1051
|
+
...session,
|
|
1052
|
+
modelMode: mode
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
// Persist only explicit overrides; null/missing means code default.
|
|
1057
|
+
const allModes: Record<string, string> = {};
|
|
1058
|
+
Object.entries(updatedSessions).forEach(([id, sess]) => {
|
|
1059
|
+
if (sess.modelMode) {
|
|
1060
|
+
allModes[id] = sess.modelMode;
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
saveSessionModelModes(allModes);
|
|
1064
|
+
|
|
1065
|
+
// No need to rebuild sessionListViewData since model mode doesn't affect the list display
|
|
1066
|
+
return {
|
|
1067
|
+
...state,
|
|
1068
|
+
sessions: updatedSessions
|
|
1069
|
+
};
|
|
1070
|
+
}),
|
|
1071
|
+
updateSessionEffortLevel: (sessionId: string, level: string | null) => set((state) => {
|
|
1072
|
+
const session = state.sessions[sessionId];
|
|
1073
|
+
if (!session) return state;
|
|
1074
|
+
|
|
1075
|
+
const updatedSessions = {
|
|
1076
|
+
...state.sessions,
|
|
1077
|
+
[sessionId]: {
|
|
1078
|
+
...session,
|
|
1079
|
+
effortLevel: level
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
// Persist effort levels so the selection survives app restart (#1028).
|
|
1084
|
+
const allLevels: Record<string, string> = {};
|
|
1085
|
+
Object.entries(updatedSessions).forEach(([id, sess]) => {
|
|
1086
|
+
if (sess.effortLevel) {
|
|
1087
|
+
allLevels[id] = sess.effortLevel;
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
saveSessionEffortLevels(allLevels);
|
|
1091
|
+
|
|
1092
|
+
return {
|
|
1093
|
+
...state,
|
|
1094
|
+
sessions: updatedSessions
|
|
1095
|
+
};
|
|
1096
|
+
}),
|
|
1097
|
+
resetSessionAgentOverrides: (sessionId: string) => set((state) => {
|
|
1098
|
+
const session = state.sessions[sessionId];
|
|
1099
|
+
if (!session) return state;
|
|
1100
|
+
|
|
1101
|
+
const updatedSessions = {
|
|
1102
|
+
...state.sessions,
|
|
1103
|
+
[sessionId]: {
|
|
1104
|
+
...session,
|
|
1105
|
+
permissionMode: null,
|
|
1106
|
+
modelMode: null,
|
|
1107
|
+
effortLevel: null,
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
const permissionModes: Record<string, string> = {};
|
|
1112
|
+
const modelModes: Record<string, string> = {};
|
|
1113
|
+
const effortLevels: Record<string, string> = {};
|
|
1114
|
+
Object.entries(updatedSessions).forEach(([id, sess]) => {
|
|
1115
|
+
if (sess.permissionMode) permissionModes[id] = sess.permissionMode;
|
|
1116
|
+
if (sess.modelMode) modelModes[id] = sess.modelMode;
|
|
1117
|
+
if (sess.effortLevel) effortLevels[id] = sess.effortLevel;
|
|
1118
|
+
});
|
|
1119
|
+
saveSessionPermissionModes(permissionModes);
|
|
1120
|
+
saveSessionModelModes(modelModes);
|
|
1121
|
+
saveSessionEffortLevels(effortLevels);
|
|
1122
|
+
|
|
1123
|
+
return {
|
|
1124
|
+
...state,
|
|
1125
|
+
sessions: updatedSessions
|
|
1126
|
+
};
|
|
1127
|
+
}),
|
|
1128
|
+
getSessionPathKey: (sessionId: string): string | null => {
|
|
1129
|
+
const session = get().sessions[sessionId];
|
|
1130
|
+
if (!session?.metadata?.machineId || !session?.metadata?.path) return null;
|
|
1131
|
+
return `${session.metadata.machineId}:${session.metadata.path}`;
|
|
1132
|
+
},
|
|
1133
|
+
applyMachines: (machines: Machine[], replace: boolean = false) => set((state) => {
|
|
1134
|
+
// Either replace all machines or merge updates
|
|
1135
|
+
let mergedMachines: Record<string, Machine>;
|
|
1136
|
+
|
|
1137
|
+
if (replace) {
|
|
1138
|
+
// Replace entire machine state (used by fetchMachines)
|
|
1139
|
+
mergedMachines = {};
|
|
1140
|
+
machines.forEach(machine => {
|
|
1141
|
+
mergedMachines[machine.id] = machine;
|
|
1142
|
+
});
|
|
1143
|
+
} else {
|
|
1144
|
+
// Merge individual updates (used by update-machine)
|
|
1145
|
+
mergedMachines = { ...state.machines };
|
|
1146
|
+
machines.forEach(machine => {
|
|
1147
|
+
mergedMachines[machine.id] = machine;
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Rebuild sessionListViewData to reflect machine changes
|
|
1152
|
+
const sessionListViewData = buildSessionListViewData(
|
|
1153
|
+
state.sessions
|
|
1154
|
+
);
|
|
1155
|
+
|
|
1156
|
+
return {
|
|
1157
|
+
...state,
|
|
1158
|
+
machines: mergedMachines,
|
|
1159
|
+
sessionListViewData
|
|
1160
|
+
};
|
|
1161
|
+
}),
|
|
1162
|
+
deleteMachine: (machineId: string) => set((state) => {
|
|
1163
|
+
if (!state.machines[machineId]) {
|
|
1164
|
+
return state;
|
|
1165
|
+
}
|
|
1166
|
+
const { [machineId]: _removed, ...remaining } = state.machines;
|
|
1167
|
+
return {
|
|
1168
|
+
...state,
|
|
1169
|
+
machines: remaining,
|
|
1170
|
+
sessionListViewData: buildSessionListViewData(state.sessions)
|
|
1171
|
+
};
|
|
1172
|
+
}),
|
|
1173
|
+
// Artifact methods
|
|
1174
|
+
applyArtifacts: (artifacts: DecryptedArtifact[]) => set((state) => {
|
|
1175
|
+
console.log(`🗂️ Storage.applyArtifacts: Applying ${artifacts.length} artifacts`);
|
|
1176
|
+
const mergedArtifacts = { ...state.artifacts };
|
|
1177
|
+
artifacts.forEach(artifact => {
|
|
1178
|
+
mergedArtifacts[artifact.id] = artifact;
|
|
1179
|
+
});
|
|
1180
|
+
console.log(`🗂️ Storage.applyArtifacts: Total artifacts after merge: ${Object.keys(mergedArtifacts).length}`);
|
|
1181
|
+
|
|
1182
|
+
return {
|
|
1183
|
+
...state,
|
|
1184
|
+
artifacts: mergedArtifacts
|
|
1185
|
+
};
|
|
1186
|
+
}),
|
|
1187
|
+
addArtifact: (artifact: DecryptedArtifact) => set((state) => {
|
|
1188
|
+
const updatedArtifacts = {
|
|
1189
|
+
...state.artifacts,
|
|
1190
|
+
[artifact.id]: artifact
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
return {
|
|
1194
|
+
...state,
|
|
1195
|
+
artifacts: updatedArtifacts
|
|
1196
|
+
};
|
|
1197
|
+
}),
|
|
1198
|
+
updateArtifact: (artifact: DecryptedArtifact) => set((state) => {
|
|
1199
|
+
const updatedArtifacts = {
|
|
1200
|
+
...state.artifacts,
|
|
1201
|
+
[artifact.id]: artifact
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
return {
|
|
1205
|
+
...state,
|
|
1206
|
+
artifacts: updatedArtifacts
|
|
1207
|
+
};
|
|
1208
|
+
}),
|
|
1209
|
+
deleteArtifact: (artifactId: string) => set((state) => {
|
|
1210
|
+
const { [artifactId]: _, ...remainingArtifacts } = state.artifacts;
|
|
1211
|
+
|
|
1212
|
+
return {
|
|
1213
|
+
...state,
|
|
1214
|
+
artifacts: remainingArtifacts
|
|
1215
|
+
};
|
|
1216
|
+
}),
|
|
1217
|
+
deleteSession: (sessionId: string) => set((state) => {
|
|
1218
|
+
// Remove session from sessions
|
|
1219
|
+
const { [sessionId]: deletedSession, ...remainingSessions } = state.sessions;
|
|
1220
|
+
|
|
1221
|
+
// Remove session messages if they exist
|
|
1222
|
+
const { [sessionId]: deletedMessages, ...remainingSessionMessages } = state.sessionMessages;
|
|
1223
|
+
|
|
1224
|
+
const { [sessionId]: _fileCache, ...remainingFileCache } = state.sessionFileCache;
|
|
1225
|
+
|
|
1226
|
+
// Clear drafts, permission modes, model modes, effort levels from persistent storage
|
|
1227
|
+
const drafts = loadSessionDrafts();
|
|
1228
|
+
delete drafts[sessionId];
|
|
1229
|
+
saveSessionDrafts(drafts);
|
|
1230
|
+
|
|
1231
|
+
const modes = loadSessionPermissionModes();
|
|
1232
|
+
delete modes[sessionId];
|
|
1233
|
+
saveSessionPermissionModes(modes);
|
|
1234
|
+
|
|
1235
|
+
const modelModes = loadSessionModelModes();
|
|
1236
|
+
delete modelModes[sessionId];
|
|
1237
|
+
saveSessionModelModes(modelModes);
|
|
1238
|
+
|
|
1239
|
+
const effortLevels = loadSessionEffortLevels();
|
|
1240
|
+
delete effortLevels[sessionId];
|
|
1241
|
+
saveSessionEffortLevels(effortLevels);
|
|
1242
|
+
|
|
1243
|
+
// Rebuild sessionListViewData without the deleted session
|
|
1244
|
+
const sessionListViewData = buildSessionListViewData(remainingSessions);
|
|
1245
|
+
|
|
1246
|
+
return {
|
|
1247
|
+
...state,
|
|
1248
|
+
sessions: remainingSessions,
|
|
1249
|
+
sessionMessages: remainingSessionMessages,
|
|
1250
|
+
sessionFileCache: remainingFileCache,
|
|
1251
|
+
sessionListViewData
|
|
1252
|
+
};
|
|
1253
|
+
}),
|
|
1254
|
+
// Friend management methods
|
|
1255
|
+
applyFriends: (friends: UserProfile[]) => set((state) => {
|
|
1256
|
+
const mergedFriends = { ...state.friends };
|
|
1257
|
+
friends.forEach(friend => {
|
|
1258
|
+
mergedFriends[friend.id] = friend;
|
|
1259
|
+
});
|
|
1260
|
+
return {
|
|
1261
|
+
...state,
|
|
1262
|
+
friends: mergedFriends,
|
|
1263
|
+
friendsLoaded: true // Mark as loaded after first fetch
|
|
1264
|
+
};
|
|
1265
|
+
}),
|
|
1266
|
+
applyRelationshipUpdate: (event: RelationshipUpdatedEvent) => set((state) => {
|
|
1267
|
+
const { fromUserId, toUserId, status, action, fromUser, toUser } = event;
|
|
1268
|
+
const currentUserId = state.profile.id;
|
|
1269
|
+
|
|
1270
|
+
// Update friends cache
|
|
1271
|
+
const updatedFriends = { ...state.friends };
|
|
1272
|
+
|
|
1273
|
+
// Determine which user profile to update based on perspective
|
|
1274
|
+
const otherUserId = fromUserId === currentUserId ? toUserId : fromUserId;
|
|
1275
|
+
const otherUser = fromUserId === currentUserId ? toUser : fromUser;
|
|
1276
|
+
|
|
1277
|
+
if (action === 'deleted' || status === 'none') {
|
|
1278
|
+
// Remove from friends if deleted or status is none
|
|
1279
|
+
delete updatedFriends[otherUserId];
|
|
1280
|
+
} else if (otherUser) {
|
|
1281
|
+
// Update or add the user profile with current status
|
|
1282
|
+
updatedFriends[otherUserId] = otherUser;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
return {
|
|
1286
|
+
...state,
|
|
1287
|
+
friends: updatedFriends
|
|
1288
|
+
};
|
|
1289
|
+
}),
|
|
1290
|
+
getFriend: (userId: string) => {
|
|
1291
|
+
return get().friends[userId];
|
|
1292
|
+
},
|
|
1293
|
+
getAcceptedFriends: () => {
|
|
1294
|
+
const friends = get().friends;
|
|
1295
|
+
return Object.values(friends).filter(friend => friend.status === 'friend');
|
|
1296
|
+
},
|
|
1297
|
+
// User cache methods
|
|
1298
|
+
applyUsers: (users: Record<string, UserProfile | null>) => set((state) => ({
|
|
1299
|
+
...state,
|
|
1300
|
+
users: { ...state.users, ...users }
|
|
1301
|
+
})),
|
|
1302
|
+
getUser: (userId: string) => {
|
|
1303
|
+
return get().users[userId]; // Returns UserProfile | null | undefined
|
|
1304
|
+
},
|
|
1305
|
+
assumeUsers: async (userIds: string[]) => {
|
|
1306
|
+
// This will be implemented in sync.ts as it needs access to credentials
|
|
1307
|
+
// Just a placeholder here for the interface
|
|
1308
|
+
const { sync } = await import('./sync');
|
|
1309
|
+
return sync.assumeUsers(userIds);
|
|
1310
|
+
},
|
|
1311
|
+
// Feed methods
|
|
1312
|
+
applyFeedItems: (items: FeedItem[]) => set((state) => {
|
|
1313
|
+
// Always mark feed as loaded even if empty
|
|
1314
|
+
if (items.length === 0) {
|
|
1315
|
+
return {
|
|
1316
|
+
...state,
|
|
1317
|
+
feedLoaded: true // Mark as loaded even when empty
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Create a map of existing items for quick lookup
|
|
1322
|
+
const existingMap = new Map<string, FeedItem>();
|
|
1323
|
+
state.feedItems.forEach(item => {
|
|
1324
|
+
existingMap.set(item.id, item);
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
// Process new items
|
|
1328
|
+
const updatedItems = [...state.feedItems];
|
|
1329
|
+
let head = state.feedHead;
|
|
1330
|
+
let tail = state.feedTail;
|
|
1331
|
+
|
|
1332
|
+
items.forEach(newItem => {
|
|
1333
|
+
// Remove items with same repeatKey if it exists
|
|
1334
|
+
if (newItem.repeatKey) {
|
|
1335
|
+
const indexToRemove = updatedItems.findIndex(item =>
|
|
1336
|
+
item.repeatKey === newItem.repeatKey
|
|
1337
|
+
);
|
|
1338
|
+
if (indexToRemove !== -1) {
|
|
1339
|
+
updatedItems.splice(indexToRemove, 1);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Add new item if it doesn't exist
|
|
1344
|
+
if (!existingMap.has(newItem.id)) {
|
|
1345
|
+
updatedItems.push(newItem);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Update head/tail cursors
|
|
1349
|
+
if (!head || newItem.counter > parseInt(head.substring(2), 10)) {
|
|
1350
|
+
head = newItem.cursor;
|
|
1351
|
+
}
|
|
1352
|
+
if (!tail || newItem.counter < parseInt(tail.substring(2), 10)) {
|
|
1353
|
+
tail = newItem.cursor;
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
// Sort by counter (desc - newest first)
|
|
1358
|
+
updatedItems.sort((a, b) => b.counter - a.counter);
|
|
1359
|
+
|
|
1360
|
+
return {
|
|
1361
|
+
...state,
|
|
1362
|
+
feedItems: updatedItems,
|
|
1363
|
+
feedHead: head,
|
|
1364
|
+
feedTail: tail,
|
|
1365
|
+
feedLoaded: true // Mark as loaded after first fetch
|
|
1366
|
+
};
|
|
1367
|
+
}),
|
|
1368
|
+
clearFeed: () => set((state) => ({
|
|
1369
|
+
...state,
|
|
1370
|
+
feedItems: [],
|
|
1371
|
+
feedHead: null,
|
|
1372
|
+
feedTail: null,
|
|
1373
|
+
feedHasMore: false,
|
|
1374
|
+
feedLoaded: false, // Reset loading flag
|
|
1375
|
+
friendsLoaded: false // Reset loading flag
|
|
1376
|
+
})),
|
|
1377
|
+
markSessionRead: (sessionId: string) => set((state) => {
|
|
1378
|
+
if (!state.unreadSessionIds.has(sessionId)) return state;
|
|
1379
|
+
const next = new Set(state.unreadSessionIds);
|
|
1380
|
+
next.delete(sessionId);
|
|
1381
|
+
return {
|
|
1382
|
+
...state,
|
|
1383
|
+
unreadSessionIds: next,
|
|
1384
|
+
sessionListViewData: buildSessionListViewData(state.sessions, next),
|
|
1385
|
+
};
|
|
1386
|
+
}),
|
|
1387
|
+
markSessionUnread: (sessionId: string) => set((state) => {
|
|
1388
|
+
if (state.unreadSessionIds.has(sessionId)) return state;
|
|
1389
|
+
const next = new Set(state.unreadSessionIds);
|
|
1390
|
+
next.add(sessionId);
|
|
1391
|
+
return {
|
|
1392
|
+
...state,
|
|
1393
|
+
unreadSessionIds: next,
|
|
1394
|
+
sessionListViewData: buildSessionListViewData(state.sessions, next),
|
|
1395
|
+
};
|
|
1396
|
+
}),
|
|
1397
|
+
setCurrentViewingSession: (sessionId: string | null) => set((state) => {
|
|
1398
|
+
if (state.currentViewingSessionId === sessionId) return state;
|
|
1399
|
+
// If switching to a new session, mark it as read
|
|
1400
|
+
const next = sessionId && state.unreadSessionIds.has(sessionId)
|
|
1401
|
+
? (() => { const s = new Set(state.unreadSessionIds); s.delete(sessionId); return s; })()
|
|
1402
|
+
: state.unreadSessionIds;
|
|
1403
|
+
return {
|
|
1404
|
+
...state,
|
|
1405
|
+
currentViewingSessionId: sessionId,
|
|
1406
|
+
unreadSessionIds: next,
|
|
1407
|
+
...(next !== state.unreadSessionIds ? {
|
|
1408
|
+
sessionListViewData: buildSessionListViewData(state.sessions, next),
|
|
1409
|
+
} : {}),
|
|
1410
|
+
};
|
|
1411
|
+
}),
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
export function useSessions() {
|
|
1416
|
+
return storage(useShallow((state) => state.isDataReady ? state.sessionsData : null));
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
export function useSession(id: string): Session | null {
|
|
1420
|
+
return storage(useShallow((state) => state.sessions[id] ?? null));
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const emptyArray: unknown[] = [];
|
|
1424
|
+
|
|
1425
|
+
export function useSessionMessages(sessionId: string): {
|
|
1426
|
+
messages: Message[],
|
|
1427
|
+
isLoaded: boolean,
|
|
1428
|
+
hasMoreOlder: boolean,
|
|
1429
|
+
isLoadingOlder: boolean
|
|
1430
|
+
} {
|
|
1431
|
+
return storage(useShallow((state) => {
|
|
1432
|
+
const session = state.sessionMessages[sessionId];
|
|
1433
|
+
return {
|
|
1434
|
+
messages: session?.messages ?? emptyArray,
|
|
1435
|
+
isLoaded: session?.isLoaded ?? false,
|
|
1436
|
+
hasMoreOlder: session?.hasMoreOlder ?? false,
|
|
1437
|
+
isLoadingOlder: session?.isLoadingOlder ?? false
|
|
1438
|
+
};
|
|
1439
|
+
}));
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
export function useMessage(sessionId: string, messageId: string): Message | null {
|
|
1443
|
+
return storage(useShallow((state) => {
|
|
1444
|
+
const session = state.sessionMessages[sessionId];
|
|
1445
|
+
return session?.messagesMap[messageId] ?? null;
|
|
1446
|
+
}));
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
export function useSessionUsage(sessionId: string) {
|
|
1450
|
+
return storage(useShallow((state) => {
|
|
1451
|
+
const session = state.sessionMessages[sessionId];
|
|
1452
|
+
return session?.reducerState?.latestUsage ?? null;
|
|
1453
|
+
}));
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
export function useSettings(): Settings {
|
|
1457
|
+
return storage(useShallow((state) => state.settings));
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
export function useSettingMutable<K extends keyof Settings>(name: K): [Settings[K], (value: Settings[K]) => void] {
|
|
1461
|
+
const setValue = React.useCallback((value: Settings[K]) => {
|
|
1462
|
+
sync.applySettings({ [name]: value });
|
|
1463
|
+
}, [name]);
|
|
1464
|
+
const value = useSetting(name);
|
|
1465
|
+
return [value, setValue];
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
export function useSetting<K extends keyof Settings>(name: K): Settings[K] {
|
|
1469
|
+
return storage(useShallow((state) => state.settings[name]));
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
export function useLocalSettings(): LocalSettings {
|
|
1473
|
+
return storage(useShallow((state) => state.localSettings));
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
export function useAllMachines(options?: { includeOffline?: boolean }): Machine[] {
|
|
1477
|
+
const includeOffline = options?.includeOffline ?? false;
|
|
1478
|
+
return storage(useShallow((state) => {
|
|
1479
|
+
if (!state.isDataReady) return [];
|
|
1480
|
+
const machines = Object.values(state.machines).sort((a, b) => b.createdAt - a.createdAt);
|
|
1481
|
+
return includeOffline ? machines : machines.filter((v) => v.active);
|
|
1482
|
+
}));
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
export function useMachine(machineId: string): Machine | null {
|
|
1486
|
+
return storage(useShallow((state) => state.machines[machineId] ?? null));
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
export function useSessionListViewData(): SessionListViewItem[] | null {
|
|
1490
|
+
return storage(useDeepEqual((state) => state.isDataReady ? state.sessionListViewData : null));
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
export function useAllSessions(): Session[] {
|
|
1494
|
+
return storage(useShallow((state) => {
|
|
1495
|
+
if (!state.isDataReady) return [];
|
|
1496
|
+
return Object.values(state.sessions).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1497
|
+
}));
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
export function useLocalSettingMutable<K extends keyof LocalSettings>(name: K): [LocalSettings[K], (value: LocalSettings[K]) => void] {
|
|
1501
|
+
const setValue = React.useCallback((value: LocalSettings[K]) => {
|
|
1502
|
+
storage.getState().applyLocalSettings({ [name]: value });
|
|
1503
|
+
}, [name]);
|
|
1504
|
+
const value = useLocalSetting(name);
|
|
1505
|
+
return [value, setValue];
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
export function useLocalSetting<K extends keyof LocalSettings>(name: K): LocalSettings[K] {
|
|
1509
|
+
return storage(useShallow((state) => state.localSettings[name]));
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
export function useIsSessionUnread(sessionId: string): boolean {
|
|
1513
|
+
return storage((state) => state.unreadSessionIds.has(sessionId));
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// Artifact hooks
|
|
1517
|
+
export function useArtifacts(): DecryptedArtifact[] {
|
|
1518
|
+
return storage(useShallow((state) => {
|
|
1519
|
+
if (!state.isDataReady) return [];
|
|
1520
|
+
// Filter out draft artifacts from the main list
|
|
1521
|
+
return Object.values(state.artifacts)
|
|
1522
|
+
.filter(artifact => !artifact.draft)
|
|
1523
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1524
|
+
}));
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
export function useAllArtifacts(): DecryptedArtifact[] {
|
|
1528
|
+
return storage(useShallow((state) => {
|
|
1529
|
+
if (!state.isDataReady) return [];
|
|
1530
|
+
// Return all artifacts including drafts
|
|
1531
|
+
return Object.values(state.artifacts).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1532
|
+
}));
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
export function useDraftArtifacts(): DecryptedArtifact[] {
|
|
1536
|
+
return storage(useShallow((state) => {
|
|
1537
|
+
if (!state.isDataReady) return [];
|
|
1538
|
+
// Return only draft artifacts
|
|
1539
|
+
return Object.values(state.artifacts)
|
|
1540
|
+
.filter(artifact => artifact.draft === true)
|
|
1541
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1542
|
+
}));
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
export function useArtifact(artifactId: string): DecryptedArtifact | null {
|
|
1546
|
+
return storage(useShallow((state) => state.artifacts[artifactId] ?? null));
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
export function useArtifactsCount(): number {
|
|
1550
|
+
return storage(useShallow((state) => {
|
|
1551
|
+
// Count only non-draft artifacts
|
|
1552
|
+
return Object.values(state.artifacts).filter(a => !a.draft).length;
|
|
1553
|
+
}));
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
export function useEntitlement(id: KnownEntitlements): boolean {
|
|
1557
|
+
return storage(useShallow((state) => state.purchases.entitlements[id] ?? false));
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
export function useRealtimeStatus(): 'disconnected' | 'connecting' | 'connected' | 'error' {
|
|
1561
|
+
return storage(useShallow((state) => state.realtimeStatus));
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
export function useRealtimeMode(): 'idle' | 'agent-speaking' | 'user-speaking' {
|
|
1565
|
+
return storage(useShallow((state) => state.realtimeMode));
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
export function useVoiceSessionGeneration(): number {
|
|
1569
|
+
return storage(useShallow((state) => state.voiceSessionGeneration));
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
export function useSocketStatus() {
|
|
1573
|
+
return storage(useShallow((state) => ({
|
|
1574
|
+
status: state.socketStatus,
|
|
1575
|
+
lastConnectedAt: state.socketLastConnectedAt,
|
|
1576
|
+
lastDisconnectedAt: state.socketLastDisconnectedAt
|
|
1577
|
+
})));
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
export function useSessionGitStatus(sessionId: string): GitStatus | null {
|
|
1581
|
+
return storage(useShallow((state) => {
|
|
1582
|
+
const pathKey = state.getSessionPathKey(sessionId);
|
|
1583
|
+
return pathKey ? state.pathGitStatus[pathKey] ?? null : null;
|
|
1584
|
+
}));
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
export function useSessionGitStatusFiles(sessionId: string): GitStatusFiles | null {
|
|
1588
|
+
return storage(useShallow((state) => {
|
|
1589
|
+
const pathKey = state.getSessionPathKey(sessionId);
|
|
1590
|
+
return pathKey ? state.pathGitStatusFiles[pathKey] ?? null : null;
|
|
1591
|
+
}));
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
export function useSessionProjectFiles(sessionId: string): ProjectFilesList | null {
|
|
1595
|
+
return storage(useShallow((state) => {
|
|
1596
|
+
const pathKey = state.getSessionPathKey(sessionId);
|
|
1597
|
+
return pathKey ? state.pathProjectFiles[pathKey] ?? null : null;
|
|
1598
|
+
}));
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
export function useSessionFileCache(sessionId: string, filePath: string) {
|
|
1602
|
+
return storage(useShallow((state) => state.sessionFileCache[sessionId]?.[filePath] ?? null));
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
export function useIsDataReady(): boolean {
|
|
1606
|
+
return storage(useShallow((state) => state.isDataReady));
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
export function useProfile() {
|
|
1610
|
+
return storage(useShallow((state) => state.profile));
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
export function useFriends() {
|
|
1614
|
+
return storage(useShallow((state) => state.friends));
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
export function useFriendRequests() {
|
|
1618
|
+
return storage(useShallow((state) => {
|
|
1619
|
+
// Filter friends to get pending requests (where status is 'pending')
|
|
1620
|
+
return Object.values(state.friends).filter(friend => friend.status === 'pending');
|
|
1621
|
+
}));
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
export function useAcceptedFriends() {
|
|
1625
|
+
return storage(useShallow((state) => {
|
|
1626
|
+
return Object.values(state.friends).filter(friend => friend.status === 'friend');
|
|
1627
|
+
}));
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
export function useFeedItems() {
|
|
1631
|
+
return storage(useShallow((state) => state.feedItems));
|
|
1632
|
+
}
|
|
1633
|
+
export function useFeedLoaded() {
|
|
1634
|
+
return storage((state) => state.feedLoaded);
|
|
1635
|
+
}
|
|
1636
|
+
export function useFriendsLoaded() {
|
|
1637
|
+
return storage((state) => state.friendsLoaded);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
export function useFriend(userId: string | undefined) {
|
|
1641
|
+
return storage(useShallow((state) => userId ? state.friends[userId] : undefined));
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
export function useUser(userId: string | undefined) {
|
|
1645
|
+
return storage(useShallow((state) => userId ? state.users[userId] : undefined));
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
export function useRequestedFriends() {
|
|
1649
|
+
return storage(useShallow((state) => {
|
|
1650
|
+
// Filter friends to get sent requests (where status is 'requested')
|
|
1651
|
+
return Object.values(state.friends).filter(friend => friend.status === 'requested');
|
|
1652
|
+
}));
|
|
1653
|
+
}
|