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,3169 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { NormalizedMessage } from '../typesRaw';
|
|
3
|
+
import { createReducer } from './reducer';
|
|
4
|
+
import { reducer } from './reducer';
|
|
5
|
+
import { AgentState } from '../storageTypes';
|
|
6
|
+
|
|
7
|
+
describe('reducer', () => {
|
|
8
|
+
// it('should process golden cases', () => {
|
|
9
|
+
// for (let i = 0; i <= 3; i++) {
|
|
10
|
+
|
|
11
|
+
// // Load raw data
|
|
12
|
+
// const raw = require(`./__testdata__/log_${i}.json`) as any[];
|
|
13
|
+
// const rawParsed = raw.map((v: any) => RawRecordSchema.parse(v.content));
|
|
14
|
+
// for (let i = 0; i < rawParsed.length; i++) {
|
|
15
|
+
// expect(rawParsed[i]).not.toBeNull();
|
|
16
|
+
// }
|
|
17
|
+
// expect(rawParsed, `raw_${i}`).toMatchSnapshot();
|
|
18
|
+
|
|
19
|
+
// const normalized = rawParsed.map((v: any, i) => normalizeRawMessage(`${i}`, null, 0, v));
|
|
20
|
+
// for (let i = 0; i < normalized.length; i++) {
|
|
21
|
+
// if (rawParsed[i].role === 'agent' && ((rawParsed[i] as any).content.data.type === 'system' || (rawParsed[i] as any).content.data.type === 'result')) {
|
|
22
|
+
// continue;
|
|
23
|
+
// }
|
|
24
|
+
// expect(normalized[i]).not.toBeNull();
|
|
25
|
+
// }
|
|
26
|
+
// expect(normalized, `normalized_${i}`).toMatchSnapshot();
|
|
27
|
+
|
|
28
|
+
// const state = createReducer();
|
|
29
|
+
// const newMessages = reducer(state, normalized.filter(v => v !== null));
|
|
30
|
+
// expect(newMessages, `log_${i}`).toMatchSnapshot();
|
|
31
|
+
// }
|
|
32
|
+
// });
|
|
33
|
+
|
|
34
|
+
describe('user message handling', () => {
|
|
35
|
+
it('should process user messages with localId', () => {
|
|
36
|
+
const state = createReducer();
|
|
37
|
+
const messages: NormalizedMessage[] = [
|
|
38
|
+
{
|
|
39
|
+
id: 'msg1',
|
|
40
|
+
localId: 'local123',
|
|
41
|
+
createdAt: 1000,
|
|
42
|
+
role: 'user',
|
|
43
|
+
content: { type: 'text', text: 'Hello' },
|
|
44
|
+
isSidechain: false
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const result = reducer(state, messages);
|
|
49
|
+
expect(result.messages).toHaveLength(1);
|
|
50
|
+
expect(result.messages[0].kind).toBe('user-text');
|
|
51
|
+
if (result.messages[0].kind === 'user-text') {
|
|
52
|
+
expect(result.messages[0].text).toBe('Hello');
|
|
53
|
+
}
|
|
54
|
+
expect(state.localIds.has('local123')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should deduplicate user messages by localId', () => {
|
|
58
|
+
const state = createReducer();
|
|
59
|
+
|
|
60
|
+
// First message with localId
|
|
61
|
+
const messages1: NormalizedMessage[] = [
|
|
62
|
+
{
|
|
63
|
+
id: 'msg1',
|
|
64
|
+
localId: 'local123',
|
|
65
|
+
createdAt: 1000,
|
|
66
|
+
role: 'user',
|
|
67
|
+
content: { type: 'text', text: 'First' },
|
|
68
|
+
isSidechain: false
|
|
69
|
+
}
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const result1 = reducer(state, messages1);
|
|
73
|
+
expect(result1.messages).toHaveLength(1);
|
|
74
|
+
|
|
75
|
+
// Second message with same localId should be ignored
|
|
76
|
+
const messages2: NormalizedMessage[] = [
|
|
77
|
+
{
|
|
78
|
+
id: 'msg2',
|
|
79
|
+
localId: 'local123',
|
|
80
|
+
createdAt: 2000,
|
|
81
|
+
role: 'user',
|
|
82
|
+
content: { type: 'text', text: 'Second' },
|
|
83
|
+
isSidechain: false
|
|
84
|
+
}
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const result2 = reducer(state, messages2);
|
|
88
|
+
expect(result2.messages).toHaveLength(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should deduplicate user messages by message id when no localId', () => {
|
|
92
|
+
const state = createReducer();
|
|
93
|
+
|
|
94
|
+
// First message without localId
|
|
95
|
+
const messages1: NormalizedMessage[] = [
|
|
96
|
+
{
|
|
97
|
+
id: 'msg1',
|
|
98
|
+
localId: null,
|
|
99
|
+
createdAt: 1000,
|
|
100
|
+
role: 'user',
|
|
101
|
+
content: { type: 'text', text: 'First' },
|
|
102
|
+
isSidechain: false
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const result1 = reducer(state, messages1);
|
|
107
|
+
expect(result1.messages).toHaveLength(1);
|
|
108
|
+
|
|
109
|
+
// Second message with same id should be ignored
|
|
110
|
+
const messages2: NormalizedMessage[] = [
|
|
111
|
+
{
|
|
112
|
+
id: 'msg1',
|
|
113
|
+
localId: null,
|
|
114
|
+
createdAt: 2000,
|
|
115
|
+
role: 'user',
|
|
116
|
+
content: { type: 'text', text: 'Second' },
|
|
117
|
+
isSidechain: false
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const result2 = reducer(state, messages2);
|
|
122
|
+
expect(result2.messages).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should process multiple user messages with different localIds', () => {
|
|
126
|
+
const state = createReducer();
|
|
127
|
+
const messages: NormalizedMessage[] = [
|
|
128
|
+
{
|
|
129
|
+
id: 'msg1',
|
|
130
|
+
localId: 'local123',
|
|
131
|
+
createdAt: 1000,
|
|
132
|
+
role: 'user',
|
|
133
|
+
content: { type: 'text', text: 'First' },
|
|
134
|
+
isSidechain: false
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'msg2',
|
|
138
|
+
localId: 'local456',
|
|
139
|
+
createdAt: 2000,
|
|
140
|
+
role: 'user',
|
|
141
|
+
content: { type: 'text', text: 'Second' },
|
|
142
|
+
isSidechain: false
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'msg3',
|
|
146
|
+
localId: null,
|
|
147
|
+
createdAt: 3000,
|
|
148
|
+
role: 'user',
|
|
149
|
+
content: { type: 'text', text: 'Third' },
|
|
150
|
+
isSidechain: false
|
|
151
|
+
}
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
const result = reducer(state, messages);
|
|
155
|
+
expect(result.messages).toHaveLength(3);
|
|
156
|
+
if (result.messages[0].kind === 'user-text') {
|
|
157
|
+
expect(result.messages[0].text).toBe('First');
|
|
158
|
+
}
|
|
159
|
+
if (result.messages[1].kind === 'user-text') {
|
|
160
|
+
expect(result.messages[1].text).toBe('Second');
|
|
161
|
+
}
|
|
162
|
+
if (result.messages[2].kind === 'user-text') {
|
|
163
|
+
expect(result.messages[2].text).toBe('Third');
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('agent text message handling', () => {
|
|
169
|
+
it('should process agent text messages', () => {
|
|
170
|
+
const state = createReducer();
|
|
171
|
+
const messages: NormalizedMessage[] = [
|
|
172
|
+
{
|
|
173
|
+
id: 'agent1',
|
|
174
|
+
localId: null,
|
|
175
|
+
createdAt: 1000,
|
|
176
|
+
role: 'agent',
|
|
177
|
+
isSidechain: false,
|
|
178
|
+
content: [{
|
|
179
|
+
type: 'text',
|
|
180
|
+
text: 'Hello from Claude!',
|
|
181
|
+
uuid: 'test-uuid-1',
|
|
182
|
+
parentUUID: null
|
|
183
|
+
}]
|
|
184
|
+
}
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
const result = reducer(state, messages);
|
|
188
|
+
expect(result.messages).toHaveLength(1);
|
|
189
|
+
expect(result.messages[0].kind).toBe('agent-text');
|
|
190
|
+
if (result.messages[0].kind === 'agent-text') {
|
|
191
|
+
expect(result.messages[0].text).toBe('Hello from Claude!');
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should process multiple text blocks in one agent message', () => {
|
|
196
|
+
const state = createReducer();
|
|
197
|
+
const messages: NormalizedMessage[] = [
|
|
198
|
+
{
|
|
199
|
+
id: 'agent1',
|
|
200
|
+
localId: null,
|
|
201
|
+
createdAt: 1000,
|
|
202
|
+
role: 'agent',
|
|
203
|
+
isSidechain: false,
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: 'text',
|
|
207
|
+
text: 'Part 1',
|
|
208
|
+
uuid: 'test-uuid-2',
|
|
209
|
+
parentUUID: null
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
type: 'text',
|
|
213
|
+
text: 'Part 2',
|
|
214
|
+
uuid: 'test-uuid-2',
|
|
215
|
+
parentUUID: null
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const result = reducer(state, messages);
|
|
222
|
+
expect(result.messages).toHaveLength(2);
|
|
223
|
+
if (result.messages[0].kind === 'agent-text') {
|
|
224
|
+
expect(result.messages[0].text).toBe('Part 1');
|
|
225
|
+
}
|
|
226
|
+
if (result.messages[1].kind === 'agent-text') {
|
|
227
|
+
expect(result.messages[1].text).toBe('Part 2');
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('mixed message processing', () => {
|
|
233
|
+
it('should handle interleaved user and agent messages', () => {
|
|
234
|
+
const state = createReducer();
|
|
235
|
+
const messages: NormalizedMessage[] = [
|
|
236
|
+
{
|
|
237
|
+
id: 'user1',
|
|
238
|
+
localId: 'local1',
|
|
239
|
+
createdAt: 1000,
|
|
240
|
+
role: 'user',
|
|
241
|
+
content: { type: 'text', text: 'Question 1' },
|
|
242
|
+
isSidechain: false
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: 'agent1',
|
|
246
|
+
localId: null,
|
|
247
|
+
createdAt: 2000,
|
|
248
|
+
role: 'agent',
|
|
249
|
+
content: [{
|
|
250
|
+
type: 'text',
|
|
251
|
+
text: 'Answer 1',
|
|
252
|
+
uuid: 'test-uuid-3',
|
|
253
|
+
parentUUID: null
|
|
254
|
+
}],
|
|
255
|
+
isSidechain: false
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: 'user2',
|
|
259
|
+
localId: 'local2',
|
|
260
|
+
createdAt: 3000,
|
|
261
|
+
role: 'user',
|
|
262
|
+
content: { type: 'text', text: 'Question 2' },
|
|
263
|
+
isSidechain: false
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: 'agent2',
|
|
267
|
+
localId: null,
|
|
268
|
+
createdAt: 4000,
|
|
269
|
+
role: 'agent',
|
|
270
|
+
content: [{
|
|
271
|
+
type: 'text',
|
|
272
|
+
text: 'Answer 2',
|
|
273
|
+
uuid: 'test-uuid-4',
|
|
274
|
+
parentUUID: null
|
|
275
|
+
}],
|
|
276
|
+
isSidechain: false
|
|
277
|
+
}
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
const result = reducer(state, messages);
|
|
281
|
+
expect(result.messages).toHaveLength(4);
|
|
282
|
+
expect(result.messages[0].kind).toBe('user-text');
|
|
283
|
+
if (result.messages[0].kind === 'user-text') {
|
|
284
|
+
expect(result.messages[0].text).toBe('Question 1');
|
|
285
|
+
}
|
|
286
|
+
expect(result.messages[1].kind).toBe('agent-text');
|
|
287
|
+
if (result.messages[1].kind === 'agent-text') {
|
|
288
|
+
expect(result.messages[1].text).toBe('Answer 1');
|
|
289
|
+
}
|
|
290
|
+
expect(result.messages[2].kind).toBe('user-text');
|
|
291
|
+
if (result.messages[2].kind === 'user-text') {
|
|
292
|
+
expect(result.messages[2].text).toBe('Question 2');
|
|
293
|
+
}
|
|
294
|
+
expect(result.messages[3].kind).toBe('agent-text');
|
|
295
|
+
if (result.messages[3].kind === 'agent-text') {
|
|
296
|
+
expect(result.messages[3].text).toBe('Answer 2');
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('edge cases', () => {
|
|
302
|
+
it('should handle empty message array', () => {
|
|
303
|
+
const state = createReducer();
|
|
304
|
+
const result = reducer(state, []);
|
|
305
|
+
expect(result.messages).toHaveLength(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should not duplicate agent messages when applied multiple times', () => {
|
|
309
|
+
const state = createReducer();
|
|
310
|
+
const messages: NormalizedMessage[] = [
|
|
311
|
+
{
|
|
312
|
+
id: 'agent1',
|
|
313
|
+
localId: null,
|
|
314
|
+
createdAt: 1000,
|
|
315
|
+
role: 'agent',
|
|
316
|
+
content: [{
|
|
317
|
+
type: 'text',
|
|
318
|
+
text: 'Hello world!',
|
|
319
|
+
uuid: 'test-uuid-5',
|
|
320
|
+
parentUUID: null
|
|
321
|
+
}],
|
|
322
|
+
isSidechain: false
|
|
323
|
+
}
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
// Apply the same messages multiple times
|
|
327
|
+
const result1 = reducer(state, messages);
|
|
328
|
+
expect(result1.messages).toHaveLength(1);
|
|
329
|
+
|
|
330
|
+
const result2 = reducer(state, messages);
|
|
331
|
+
expect(result2.messages).toHaveLength(0); // Should not add duplicates
|
|
332
|
+
|
|
333
|
+
const result3 = reducer(state, messages);
|
|
334
|
+
expect(result3.messages).toHaveLength(0); // Still no duplicates
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should filter out null normalized messages', () => {
|
|
338
|
+
const state = createReducer();
|
|
339
|
+
const messages: NormalizedMessage[] = [
|
|
340
|
+
{
|
|
341
|
+
id: 'user1',
|
|
342
|
+
localId: 'local1',
|
|
343
|
+
createdAt: 1000,
|
|
344
|
+
role: 'user',
|
|
345
|
+
content: { type: 'text', text: 'Valid' },
|
|
346
|
+
isSidechain: false
|
|
347
|
+
}
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
const result = reducer(state, messages);
|
|
351
|
+
expect(result.messages).toHaveLength(1);
|
|
352
|
+
if (result.messages[0].kind === 'user-text') {
|
|
353
|
+
expect(result.messages[0].text).toBe('Valid');
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should handle summary messages', () => {
|
|
358
|
+
const state = createReducer();
|
|
359
|
+
const messages: NormalizedMessage[] = [
|
|
360
|
+
{
|
|
361
|
+
id: 'agent1',
|
|
362
|
+
localId: null,
|
|
363
|
+
createdAt: 1000,
|
|
364
|
+
role: 'event',
|
|
365
|
+
content: {
|
|
366
|
+
type: 'message',
|
|
367
|
+
message: 'This is a summary'
|
|
368
|
+
},
|
|
369
|
+
isSidechain: false
|
|
370
|
+
}
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
const result = reducer(state, messages);
|
|
374
|
+
// Summary messages should be processed but may not appear in output
|
|
375
|
+
expect(result).toBeDefined();
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('AgentState permissions', () => {
|
|
380
|
+
it('should create tool messages for pending permission requests', () => {
|
|
381
|
+
const state = createReducer();
|
|
382
|
+
const agentState: AgentState = {
|
|
383
|
+
requests: {
|
|
384
|
+
'tool-1': {
|
|
385
|
+
tool: 'Bash',
|
|
386
|
+
arguments: { command: 'ls -la' },
|
|
387
|
+
createdAt: 1000
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const result = reducer(state, [], agentState);
|
|
393
|
+
|
|
394
|
+
expect(result.messages).toHaveLength(1);
|
|
395
|
+
expect(result.messages[0].kind).toBe('tool-call');
|
|
396
|
+
if (result.messages[0].kind === 'tool-call') {
|
|
397
|
+
expect(result.messages[0].tool.name).toBe('Bash');
|
|
398
|
+
expect(result.messages[0].tool.state).toBe('running');
|
|
399
|
+
expect(result.messages[0].tool.permission).toEqual({
|
|
400
|
+
id: 'tool-1',
|
|
401
|
+
status: 'pending'
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should update permission status for completed requests', () => {
|
|
407
|
+
const state = createReducer();
|
|
408
|
+
|
|
409
|
+
// First create a pending permission
|
|
410
|
+
const agentState1: AgentState = {
|
|
411
|
+
requests: {
|
|
412
|
+
'tool-1': {
|
|
413
|
+
tool: 'Bash',
|
|
414
|
+
arguments: { command: 'ls -la' },
|
|
415
|
+
createdAt: 1000
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const result1 = reducer(state, [], agentState1);
|
|
421
|
+
expect(result1.messages).toHaveLength(1);
|
|
422
|
+
|
|
423
|
+
// Then mark it as completed
|
|
424
|
+
const agentState2: AgentState = {
|
|
425
|
+
completedRequests: {
|
|
426
|
+
'tool-1': {
|
|
427
|
+
tool: 'Bash',
|
|
428
|
+
arguments: { command: 'ls -la' },
|
|
429
|
+
createdAt: 1000,
|
|
430
|
+
completedAt: 2000,
|
|
431
|
+
status: 'denied',
|
|
432
|
+
reason: 'User denied permission'
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const result2 = reducer(state, [], agentState2);
|
|
438
|
+
expect(result2.messages).toHaveLength(1);
|
|
439
|
+
if (result2.messages[0].kind === 'tool-call') {
|
|
440
|
+
expect(result2.messages[0].tool.state).toBe('error');
|
|
441
|
+
expect(result2.messages[0].tool.permission?.status).toBe('denied');
|
|
442
|
+
expect(result2.messages[0].tool.permission?.reason).toBe('User denied permission');
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('should match incoming tool calls to approved permission messages', () => {
|
|
447
|
+
const state = createReducer();
|
|
448
|
+
|
|
449
|
+
// First create an approved permission
|
|
450
|
+
const agentState: AgentState = {
|
|
451
|
+
completedRequests: {
|
|
452
|
+
'tool-1': {
|
|
453
|
+
tool: 'Bash',
|
|
454
|
+
arguments: { command: 'ls -la' },
|
|
455
|
+
createdAt: 1000,
|
|
456
|
+
completedAt: 2000,
|
|
457
|
+
status: 'approved'
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const result1 = reducer(state, [], agentState);
|
|
463
|
+
expect(result1.messages).toHaveLength(1);
|
|
464
|
+
|
|
465
|
+
// Then receive the actual tool call from the agent
|
|
466
|
+
const messages: NormalizedMessage[] = [
|
|
467
|
+
{
|
|
468
|
+
id: 'msg-1',
|
|
469
|
+
localId: null,
|
|
470
|
+
createdAt: 3000,
|
|
471
|
+
role: 'agent',
|
|
472
|
+
isSidechain: false,
|
|
473
|
+
content: [{
|
|
474
|
+
type: 'tool-call',
|
|
475
|
+
id: 'tool-1',
|
|
476
|
+
name: 'Bash',
|
|
477
|
+
input: { command: 'ls -la' },
|
|
478
|
+
description: null,
|
|
479
|
+
uuid: 'msg-1-uuid',
|
|
480
|
+
parentUUID: null
|
|
481
|
+
}]
|
|
482
|
+
}
|
|
483
|
+
];
|
|
484
|
+
|
|
485
|
+
const result2 = reducer(state, messages, agentState);
|
|
486
|
+
|
|
487
|
+
// The tool call should be matched to the existing permission message
|
|
488
|
+
// So we should get an update to the existing message, not a new one
|
|
489
|
+
expect(result2.messages).toHaveLength(1);
|
|
490
|
+
if (result2.messages[0].kind === 'tool-call') {
|
|
491
|
+
expect(result2.messages[0].tool.permission?.status).toBe('approved');
|
|
492
|
+
expect(result2.messages[0].tool.state).toBe('running');
|
|
493
|
+
expect(result2.messages[0].tool.name).toBe('Bash');
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('should merge real tool-call patch args into matched permission messages', () => {
|
|
498
|
+
const state = createReducer();
|
|
499
|
+
const fileChanges = {
|
|
500
|
+
'src/example.ts': {
|
|
501
|
+
modify: {
|
|
502
|
+
old_content: 'before',
|
|
503
|
+
new_content: 'after'
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
const changes = {
|
|
508
|
+
'src/example.ts': {
|
|
509
|
+
modify: {
|
|
510
|
+
old_content: 'before',
|
|
511
|
+
new_content: 'after'
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
const agentState: AgentState = {
|
|
516
|
+
completedRequests: {
|
|
517
|
+
'tool-1': {
|
|
518
|
+
tool: 'CodexPatch',
|
|
519
|
+
arguments: { fileChanges },
|
|
520
|
+
createdAt: 1000,
|
|
521
|
+
completedAt: 2000,
|
|
522
|
+
status: 'approved'
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
reducer(state, [], agentState);
|
|
528
|
+
|
|
529
|
+
const messages: NormalizedMessage[] = [
|
|
530
|
+
{
|
|
531
|
+
id: 'msg-1',
|
|
532
|
+
localId: null,
|
|
533
|
+
createdAt: 3000,
|
|
534
|
+
role: 'agent',
|
|
535
|
+
isSidechain: false,
|
|
536
|
+
content: [{
|
|
537
|
+
type: 'tool-call',
|
|
538
|
+
id: 'tool-1',
|
|
539
|
+
name: 'CodexPatch',
|
|
540
|
+
input: {
|
|
541
|
+
auto_approved: false,
|
|
542
|
+
changes
|
|
543
|
+
},
|
|
544
|
+
description: 'Apply patch to 1 file',
|
|
545
|
+
uuid: 'msg-1-uuid',
|
|
546
|
+
parentUUID: null
|
|
547
|
+
}]
|
|
548
|
+
}
|
|
549
|
+
];
|
|
550
|
+
|
|
551
|
+
const result = reducer(state, messages, agentState);
|
|
552
|
+
|
|
553
|
+
expect(result.messages).toHaveLength(1);
|
|
554
|
+
expect(result.messages[0].kind).toBe('tool-call');
|
|
555
|
+
if (result.messages[0].kind === 'tool-call') {
|
|
556
|
+
expect(result.messages[0].tool.input).toEqual({
|
|
557
|
+
auto_approved: false,
|
|
558
|
+
changes,
|
|
559
|
+
fileChanges
|
|
560
|
+
});
|
|
561
|
+
expect(result.messages[0].tool.startedAt).toBe(3000);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('should match tool calls by ID regardless of arguments', () => {
|
|
566
|
+
const state = createReducer();
|
|
567
|
+
|
|
568
|
+
// Create multiple pending permission requests
|
|
569
|
+
const agentState1: AgentState = {
|
|
570
|
+
requests: {
|
|
571
|
+
'tool-1': {
|
|
572
|
+
tool: 'Bash',
|
|
573
|
+
arguments: { command: 'ls -la' },
|
|
574
|
+
createdAt: 1000
|
|
575
|
+
},
|
|
576
|
+
'tool-2': {
|
|
577
|
+
tool: 'Bash',
|
|
578
|
+
arguments: { command: 'pwd' },
|
|
579
|
+
createdAt: 2000
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const result1 = reducer(state, [], agentState1);
|
|
585
|
+
expect(result1.messages).toHaveLength(2);
|
|
586
|
+
|
|
587
|
+
// Approve both permissions
|
|
588
|
+
const agentState2: AgentState = {
|
|
589
|
+
completedRequests: {
|
|
590
|
+
'tool-1': {
|
|
591
|
+
tool: 'Bash',
|
|
592
|
+
arguments: { command: 'ls -la' },
|
|
593
|
+
createdAt: 1000,
|
|
594
|
+
completedAt: 3000,
|
|
595
|
+
status: 'approved'
|
|
596
|
+
},
|
|
597
|
+
'tool-2': {
|
|
598
|
+
tool: 'Bash',
|
|
599
|
+
arguments: { command: 'pwd' },
|
|
600
|
+
createdAt: 2000,
|
|
601
|
+
completedAt: 3000,
|
|
602
|
+
status: 'approved'
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
reducer(state, [], agentState2);
|
|
608
|
+
|
|
609
|
+
// Now receive a tool call from the agent
|
|
610
|
+
const messages: NormalizedMessage[] = [
|
|
611
|
+
{
|
|
612
|
+
id: 'msg-1',
|
|
613
|
+
localId: null,
|
|
614
|
+
createdAt: 4000,
|
|
615
|
+
role: 'agent',
|
|
616
|
+
isSidechain: false,
|
|
617
|
+
content: [{
|
|
618
|
+
type: 'tool-call',
|
|
619
|
+
id: 'tool-1',
|
|
620
|
+
name: 'Bash',
|
|
621
|
+
input: { command: 'pwd' },
|
|
622
|
+
description: null,
|
|
623
|
+
uuid: 'msg-2-uuid',
|
|
624
|
+
parentUUID: null
|
|
625
|
+
}]
|
|
626
|
+
}
|
|
627
|
+
];
|
|
628
|
+
|
|
629
|
+
// Pass agentState2 - it's always provided as current state
|
|
630
|
+
const result3 = reducer(state, messages, agentState2);
|
|
631
|
+
|
|
632
|
+
// Should return the updated permission message (ID match)
|
|
633
|
+
expect(result3.messages).toHaveLength(1);
|
|
634
|
+
expect(result3.messages[0].kind).toBe('tool-call');
|
|
635
|
+
if (result3.messages[0].kind === 'tool-call') {
|
|
636
|
+
// With ID matching, keeps original permission arguments
|
|
637
|
+
expect(result3.messages[0].tool.input).toEqual({ command: 'ls -la' });
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Verify that tool-1 is in the map
|
|
641
|
+
expect(state.toolIdToMessageId.has('tool-1')).toBe(true);
|
|
642
|
+
// Should have both tool IDs in the map
|
|
643
|
+
expect(state.toolIdToMessageId.size).toBe(2);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('should not create new message when tool can be matched to existing permission (priority to newest)', () => {
|
|
647
|
+
const state = createReducer();
|
|
648
|
+
|
|
649
|
+
// Create multiple approved permissions with same tool but different times
|
|
650
|
+
const agentState: AgentState = {
|
|
651
|
+
completedRequests: {
|
|
652
|
+
'tool-old': {
|
|
653
|
+
tool: 'Bash',
|
|
654
|
+
arguments: { command: 'ls' },
|
|
655
|
+
createdAt: 1000,
|
|
656
|
+
completedAt: 2000,
|
|
657
|
+
status: 'approved'
|
|
658
|
+
},
|
|
659
|
+
'tool-new': {
|
|
660
|
+
tool: 'Bash',
|
|
661
|
+
arguments: { command: 'ls' },
|
|
662
|
+
createdAt: 3000,
|
|
663
|
+
completedAt: 4000,
|
|
664
|
+
status: 'approved'
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const result1 = reducer(state, [], agentState);
|
|
670
|
+
expect(result1.messages).toHaveLength(2);
|
|
671
|
+
|
|
672
|
+
// Store the message IDs
|
|
673
|
+
const oldMessageId = state.toolIdToMessageId.get('tool-old');
|
|
674
|
+
const newMessageId = state.toolIdToMessageId.get('tool-new');
|
|
675
|
+
|
|
676
|
+
// Now receive a tool call that matches both
|
|
677
|
+
const messages: NormalizedMessage[] = [
|
|
678
|
+
{
|
|
679
|
+
id: 'msg-1',
|
|
680
|
+
localId: null,
|
|
681
|
+
createdAt: 5000,
|
|
682
|
+
role: 'agent',
|
|
683
|
+
isSidechain: false,
|
|
684
|
+
content: [{
|
|
685
|
+
type: 'tool-call',
|
|
686
|
+
id: 'tool-1',
|
|
687
|
+
name: 'Bash',
|
|
688
|
+
input: { command: 'ls' },
|
|
689
|
+
description: null,
|
|
690
|
+
uuid: 'msg-3-uuid',
|
|
691
|
+
parentUUID: null
|
|
692
|
+
}]
|
|
693
|
+
}
|
|
694
|
+
];
|
|
695
|
+
|
|
696
|
+
// Pass agentState - it's always provided as current state
|
|
697
|
+
const result2 = reducer(state, messages, agentState);
|
|
698
|
+
|
|
699
|
+
// Should only return the updated message that matched
|
|
700
|
+
expect(result2.messages).toHaveLength(1);
|
|
701
|
+
expect(result2.messages[0].kind).toBe('tool-call');
|
|
702
|
+
if (result2.messages[0].kind === 'tool-call') {
|
|
703
|
+
expect(result2.messages[0].tool.input).toEqual({ command: 'ls' });
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// With new design, tool-1 creates a new message since it doesn't match tool-old or tool-new
|
|
707
|
+
expect(state.toolIdToMessageId.has('tool-1')).toBe(true);
|
|
708
|
+
expect(state.toolIdToMessageId.has('tool-old')).toBe(true);
|
|
709
|
+
expect(state.toolIdToMessageId.has('tool-new')).toBe(true);
|
|
710
|
+
|
|
711
|
+
// Verify that old messages were not updated (tool-1 is different ID)
|
|
712
|
+
const newMessage = state.messages.get(newMessageId!);
|
|
713
|
+
expect(newMessage?.tool?.startedAt).toBeNull();
|
|
714
|
+
|
|
715
|
+
const oldMessage = state.messages.get(oldMessageId!);
|
|
716
|
+
expect(oldMessage?.tool?.startedAt).toBeNull();
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('should not create duplicate messages when called twice with same AgentState', () => {
|
|
720
|
+
const state = createReducer();
|
|
721
|
+
|
|
722
|
+
// AgentState with both pending and completed permissions
|
|
723
|
+
const agentState: AgentState = {
|
|
724
|
+
requests: {
|
|
725
|
+
'tool-pending': {
|
|
726
|
+
tool: 'Read',
|
|
727
|
+
arguments: { file: 'test.txt' },
|
|
728
|
+
createdAt: 1000
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
completedRequests: {
|
|
732
|
+
'tool-completed': {
|
|
733
|
+
tool: 'Write',
|
|
734
|
+
arguments: { file: 'output.txt', content: 'hello' },
|
|
735
|
+
createdAt: 2000,
|
|
736
|
+
completedAt: 3000,
|
|
737
|
+
status: 'approved'
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
// First call - should create messages
|
|
743
|
+
const result1 = reducer(state, [], agentState);
|
|
744
|
+
expect(result1.messages).toHaveLength(2);
|
|
745
|
+
|
|
746
|
+
// Verify the messages were created
|
|
747
|
+
expect(state.toolIdToMessageId.has('tool-pending')).toBe(true);
|
|
748
|
+
expect(state.toolIdToMessageId.has('tool-completed')).toBe(true);
|
|
749
|
+
|
|
750
|
+
// Second call with same AgentState - should not create duplicates
|
|
751
|
+
const result2 = reducer(state, [], agentState);
|
|
752
|
+
expect(result2.messages).toHaveLength(0); // No new messages
|
|
753
|
+
|
|
754
|
+
// Verify the mappings still exist and haven't changed
|
|
755
|
+
expect(state.toolIdToMessageId.size).toBe(2);
|
|
756
|
+
|
|
757
|
+
// Third call with a message and same AgentState - still no duplicates
|
|
758
|
+
const messages: NormalizedMessage[] = [
|
|
759
|
+
{
|
|
760
|
+
id: 'msg-1',
|
|
761
|
+
localId: null,
|
|
762
|
+
createdAt: 4000,
|
|
763
|
+
role: 'user',
|
|
764
|
+
content: { type: 'text', text: 'Hello' },
|
|
765
|
+
isSidechain: false
|
|
766
|
+
}
|
|
767
|
+
];
|
|
768
|
+
|
|
769
|
+
const result3 = reducer(state, messages, agentState);
|
|
770
|
+
expect(result3.messages).toHaveLength(1); // Only the user message
|
|
771
|
+
expect(result3.messages[0].kind).toBe('user-text');
|
|
772
|
+
|
|
773
|
+
// Verify permission messages weren't duplicated
|
|
774
|
+
expect(state.toolIdToMessageId.size).toBe(2);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it('should prioritize tool call over permission request when both provided simultaneously', () => {
|
|
778
|
+
const state = createReducer();
|
|
779
|
+
|
|
780
|
+
// AgentState with approved permission
|
|
781
|
+
const agentState: AgentState = {
|
|
782
|
+
completedRequests: {
|
|
783
|
+
'tool-1': {
|
|
784
|
+
tool: 'Bash',
|
|
785
|
+
arguments: { command: 'ls' },
|
|
786
|
+
createdAt: 1000,
|
|
787
|
+
completedAt: 2000,
|
|
788
|
+
status: 'approved'
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// Tool call message with different timestamp
|
|
794
|
+
const messages: NormalizedMessage[] = [
|
|
795
|
+
{
|
|
796
|
+
id: 'tool-msg-1',
|
|
797
|
+
localId: null,
|
|
798
|
+
createdAt: 5000,
|
|
799
|
+
role: 'agent',
|
|
800
|
+
content: [{
|
|
801
|
+
type: 'tool-call',
|
|
802
|
+
id: 'tool-1',
|
|
803
|
+
name: 'Bash',
|
|
804
|
+
input: { command: 'ls' },
|
|
805
|
+
description: null,
|
|
806
|
+
uuid: 'tool-uuid-1',
|
|
807
|
+
parentUUID: null
|
|
808
|
+
}],
|
|
809
|
+
isSidechain: false
|
|
810
|
+
}
|
|
811
|
+
];
|
|
812
|
+
|
|
813
|
+
// Process both simultaneously
|
|
814
|
+
const result = reducer(state, messages, agentState);
|
|
815
|
+
|
|
816
|
+
// Should create only one message (the tool call takes priority)
|
|
817
|
+
expect(result.messages).toHaveLength(1);
|
|
818
|
+
expect(result.messages[0].kind).toBe('tool-call');
|
|
819
|
+
if (result.messages[0].kind === 'tool-call') {
|
|
820
|
+
// Should use tool call's timestamp, not permission's
|
|
821
|
+
expect(result.messages[0].createdAt).toBe(5000);
|
|
822
|
+
expect(result.messages[0].id).toBeDefined();
|
|
823
|
+
|
|
824
|
+
// Should have permission info from AgentState (it was skipped in Phase 0 but attached in Phase 2)
|
|
825
|
+
expect(result.messages[0].tool.permission).toBeDefined();
|
|
826
|
+
expect(result.messages[0].tool.permission?.id).toBe('tool-1');
|
|
827
|
+
expect(result.messages[0].tool.permission?.status).toBe('approved');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Verify only the tool message was created, not a separate permission message
|
|
831
|
+
expect(state.toolIdToMessageId.has('tool-1')).toBe(true);
|
|
832
|
+
expect(state.toolIdToMessageId.has('tool-1')).toBe(true);
|
|
833
|
+
// Tool ID maps to message ID
|
|
834
|
+
const toolMsgId = state.toolIdToMessageId.get('tool-1');
|
|
835
|
+
expect(toolMsgId).toBeDefined();
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it('should preserve original timestamps when request received first, then tool call', () => {
|
|
839
|
+
const state = createReducer();
|
|
840
|
+
|
|
841
|
+
// First: Process permission request
|
|
842
|
+
const agentState1: AgentState = {
|
|
843
|
+
requests: {
|
|
844
|
+
'tool-1': {
|
|
845
|
+
tool: 'Bash',
|
|
846
|
+
arguments: { command: 'ls' },
|
|
847
|
+
createdAt: 1000
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
const result1 = reducer(state, [], agentState1);
|
|
853
|
+
expect(result1.messages).toHaveLength(1);
|
|
854
|
+
|
|
855
|
+
const permMessageId = state.toolIdToMessageId.get('tool-1');
|
|
856
|
+
const originalMessage = state.messages.get(permMessageId!);
|
|
857
|
+
expect(originalMessage?.createdAt).toBe(1000);
|
|
858
|
+
expect(originalMessage?.realID).toBeNull();
|
|
859
|
+
|
|
860
|
+
// Then: Approve the permission
|
|
861
|
+
const agentState2: AgentState = {
|
|
862
|
+
completedRequests: {
|
|
863
|
+
'tool-1': {
|
|
864
|
+
tool: 'Bash',
|
|
865
|
+
arguments: { command: 'ls' },
|
|
866
|
+
createdAt: 1000,
|
|
867
|
+
completedAt: 2000,
|
|
868
|
+
status: 'approved'
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
const result2 = reducer(state, [], agentState2);
|
|
874
|
+
expect(result2.messages).toHaveLength(1); // Same message, updated
|
|
875
|
+
|
|
876
|
+
// Finally: Receive the actual tool call
|
|
877
|
+
const messages: NormalizedMessage[] = [
|
|
878
|
+
{
|
|
879
|
+
id: 'tool-msg-1',
|
|
880
|
+
localId: null,
|
|
881
|
+
createdAt: 5000,
|
|
882
|
+
role: 'agent',
|
|
883
|
+
content: [{
|
|
884
|
+
type: 'tool-call',
|
|
885
|
+
id: 'tool-1',
|
|
886
|
+
name: 'Bash',
|
|
887
|
+
input: { command: 'ls' },
|
|
888
|
+
description: null,
|
|
889
|
+
uuid: 'tool-uuid-1',
|
|
890
|
+
parentUUID: null
|
|
891
|
+
}],
|
|
892
|
+
isSidechain: false
|
|
893
|
+
}
|
|
894
|
+
];
|
|
895
|
+
|
|
896
|
+
const result3 = reducer(state, messages, agentState2);
|
|
897
|
+
expect(result3.messages).toHaveLength(1); // Same message, updated
|
|
898
|
+
|
|
899
|
+
// Check the final state of the message
|
|
900
|
+
const finalMessage = state.messages.get(permMessageId!);
|
|
901
|
+
|
|
902
|
+
// Original timestamp should be preserved
|
|
903
|
+
expect(finalMessage?.createdAt).toBe(1000);
|
|
904
|
+
|
|
905
|
+
// But realID should be updated to the tool message's ID
|
|
906
|
+
expect(finalMessage?.realID).toBe('tool-msg-1');
|
|
907
|
+
|
|
908
|
+
// Tool should be updated with execution details
|
|
909
|
+
expect(finalMessage?.tool?.startedAt).toBe(5000);
|
|
910
|
+
expect(finalMessage?.tool?.permission?.status).toBe('approved');
|
|
911
|
+
|
|
912
|
+
// Verify the tool is properly linked
|
|
913
|
+
expect(state.toolIdToMessageId.get('tool-1')).toBe(permMessageId);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it('should create separate messages for same tool name with different arguments', () => {
|
|
917
|
+
const state = createReducer();
|
|
918
|
+
|
|
919
|
+
// AgentState with two approved permissions for same tool but different arguments
|
|
920
|
+
const agentState: AgentState = {
|
|
921
|
+
completedRequests: {
|
|
922
|
+
'tool-ls': {
|
|
923
|
+
tool: 'Bash',
|
|
924
|
+
arguments: { command: 'ls -la' },
|
|
925
|
+
createdAt: 1000,
|
|
926
|
+
completedAt: 2000,
|
|
927
|
+
status: 'approved'
|
|
928
|
+
},
|
|
929
|
+
'tool-pwd': {
|
|
930
|
+
tool: 'Bash',
|
|
931
|
+
arguments: { command: 'pwd' },
|
|
932
|
+
createdAt: 1500,
|
|
933
|
+
completedAt: 2000,
|
|
934
|
+
status: 'approved'
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
// Process permissions
|
|
940
|
+
const result1 = reducer(state, [], agentState);
|
|
941
|
+
expect(result1.messages).toHaveLength(2);
|
|
942
|
+
|
|
943
|
+
// Both should be separate messages
|
|
944
|
+
const lsMessageId = state.toolIdToMessageId.get('tool-ls');
|
|
945
|
+
const pwdMessageId = state.toolIdToMessageId.get('tool-pwd');
|
|
946
|
+
expect(lsMessageId).toBeDefined();
|
|
947
|
+
expect(pwdMessageId).toBeDefined();
|
|
948
|
+
expect(lsMessageId).not.toBe(pwdMessageId);
|
|
949
|
+
|
|
950
|
+
// Verify the messages have correct arguments
|
|
951
|
+
const lsMessage = state.messages.get(lsMessageId!);
|
|
952
|
+
const pwdMessage = state.messages.get(pwdMessageId!);
|
|
953
|
+
expect(lsMessage?.tool?.input).toEqual({ command: 'ls -la' });
|
|
954
|
+
expect(pwdMessage?.tool?.input).toEqual({ command: 'pwd' });
|
|
955
|
+
|
|
956
|
+
// Now receive the first tool call (pwd)
|
|
957
|
+
const messages1: NormalizedMessage[] = [
|
|
958
|
+
{
|
|
959
|
+
id: 'msg-1',
|
|
960
|
+
localId: null,
|
|
961
|
+
createdAt: 3000,
|
|
962
|
+
role: 'agent',
|
|
963
|
+
content: [{
|
|
964
|
+
type: 'tool-call',
|
|
965
|
+
id: 'tool-pwd',
|
|
966
|
+
name: 'Bash',
|
|
967
|
+
input: { command: 'pwd' },
|
|
968
|
+
description: null,
|
|
969
|
+
uuid: 'tool-uuid-1',
|
|
970
|
+
parentUUID: null
|
|
971
|
+
}],
|
|
972
|
+
isSidechain: false
|
|
973
|
+
}
|
|
974
|
+
];
|
|
975
|
+
|
|
976
|
+
const result2 = reducer(state, messages1, agentState);
|
|
977
|
+
expect(result2.messages).toHaveLength(1);
|
|
978
|
+
|
|
979
|
+
// Should match to the pwd permission (newer one, matching arguments)
|
|
980
|
+
expect(state.toolIdToMessageId.get('tool-pwd')).toBe(pwdMessageId);
|
|
981
|
+
// ls permission should have its own message
|
|
982
|
+
expect(state.toolIdToMessageId.has('tool-ls')).toBe(true);
|
|
983
|
+
|
|
984
|
+
// Now receive the second tool call (ls)
|
|
985
|
+
const messages2: NormalizedMessage[] = [
|
|
986
|
+
{
|
|
987
|
+
id: 'msg-2',
|
|
988
|
+
localId: null,
|
|
989
|
+
createdAt: 4000,
|
|
990
|
+
role: 'agent',
|
|
991
|
+
content: [{
|
|
992
|
+
type: 'tool-call',
|
|
993
|
+
id: 'tool-ls',
|
|
994
|
+
name: 'Bash',
|
|
995
|
+
input: { command: 'ls -la' },
|
|
996
|
+
description: null,
|
|
997
|
+
uuid: 'tool-uuid-2',
|
|
998
|
+
parentUUID: null
|
|
999
|
+
}],
|
|
1000
|
+
isSidechain: false
|
|
1001
|
+
}
|
|
1002
|
+
];
|
|
1003
|
+
|
|
1004
|
+
const result3 = reducer(state, messages2, agentState);
|
|
1005
|
+
expect(result3.messages).toHaveLength(1);
|
|
1006
|
+
|
|
1007
|
+
// Should match to the ls permission
|
|
1008
|
+
expect(state.toolIdToMessageId.get('tool-ls')).toBe(lsMessageId);
|
|
1009
|
+
|
|
1010
|
+
// Both tools should be in the map
|
|
1011
|
+
expect(state.toolIdToMessageId.size).toBe(2);
|
|
1012
|
+
|
|
1013
|
+
// Verify final states
|
|
1014
|
+
const finalLsMessage = state.messages.get(lsMessageId!);
|
|
1015
|
+
const finalPwdMessage = state.messages.get(pwdMessageId!);
|
|
1016
|
+
expect(finalLsMessage?.tool?.startedAt).toBe(4000);
|
|
1017
|
+
expect(finalPwdMessage?.tool?.startedAt).toBe(3000);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it('should update permission message when tool call has matching ID', () => {
|
|
1021
|
+
const state = createReducer();
|
|
1022
|
+
|
|
1023
|
+
// AgentState with a pending permission request
|
|
1024
|
+
const agentState: AgentState = {
|
|
1025
|
+
requests: {
|
|
1026
|
+
'tool-1': {
|
|
1027
|
+
tool: 'Bash',
|
|
1028
|
+
arguments: { command: 'ls -la' },
|
|
1029
|
+
createdAt: 1000
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
// Tool call with matching ID (arguments don't matter with ID matching)
|
|
1035
|
+
const messages: NormalizedMessage[] = [
|
|
1036
|
+
{
|
|
1037
|
+
id: 'tool-msg-1',
|
|
1038
|
+
localId: null,
|
|
1039
|
+
createdAt: 2000,
|
|
1040
|
+
role: 'agent',
|
|
1041
|
+
content: [{
|
|
1042
|
+
type: 'tool-call',
|
|
1043
|
+
id: 'tool-1',
|
|
1044
|
+
name: 'Bash',
|
|
1045
|
+
input: { command: 'pwd' },
|
|
1046
|
+
description: null,
|
|
1047
|
+
uuid: 'tool-uuid-1',
|
|
1048
|
+
parentUUID: null
|
|
1049
|
+
}],
|
|
1050
|
+
isSidechain: false
|
|
1051
|
+
}
|
|
1052
|
+
];
|
|
1053
|
+
|
|
1054
|
+
// Process both simultaneously
|
|
1055
|
+
const result = reducer(state, messages, agentState);
|
|
1056
|
+
|
|
1057
|
+
// Should update the existing permission message
|
|
1058
|
+
expect(result.messages).toHaveLength(1);
|
|
1059
|
+
|
|
1060
|
+
// Verify the message was updated with tool execution details
|
|
1061
|
+
if (result.messages[0].kind === 'tool-call') {
|
|
1062
|
+
// Should keep original permission data
|
|
1063
|
+
expect(result.messages[0].tool.permission?.id).toBe('tool-1');
|
|
1064
|
+
expect(result.messages[0].tool.permission?.status).toBe('pending');
|
|
1065
|
+
// Should keep original arguments from permission
|
|
1066
|
+
expect(result.messages[0].tool.input).toEqual({ command: 'ls -la' });
|
|
1067
|
+
// Should keep original timestamp
|
|
1068
|
+
expect(result.messages[0].createdAt).toBe(1000);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Verify internal state - should be the same message
|
|
1072
|
+
expect(state.toolIdToMessageId.has('tool-1')).toBe(true);
|
|
1073
|
+
expect(state.toolIdToMessageId.has('tool-1')).toBe(true);
|
|
1074
|
+
|
|
1075
|
+
// They should be the same message now
|
|
1076
|
+
const permMsgId = state.toolIdToMessageId.get('tool-1');
|
|
1077
|
+
const toolMsgId = state.toolIdToMessageId.get('tool-1');
|
|
1078
|
+
expect(permMsgId).toBe(toolMsgId);
|
|
1079
|
+
|
|
1080
|
+
// Now approve the permission and send its tool call
|
|
1081
|
+
const agentState2: AgentState = {
|
|
1082
|
+
completedRequests: {
|
|
1083
|
+
'tool-1': {
|
|
1084
|
+
tool: 'Bash',
|
|
1085
|
+
arguments: { command: 'ls -la' },
|
|
1086
|
+
createdAt: 1000,
|
|
1087
|
+
completedAt: 3000,
|
|
1088
|
+
status: 'approved'
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
const messages2: NormalizedMessage[] = [
|
|
1094
|
+
{
|
|
1095
|
+
id: 'tool-msg-2',
|
|
1096
|
+
localId: null,
|
|
1097
|
+
createdAt: 4000,
|
|
1098
|
+
role: 'agent',
|
|
1099
|
+
content: [{
|
|
1100
|
+
type: 'tool-call',
|
|
1101
|
+
id: 'tool-1', // Must match permission ID
|
|
1102
|
+
name: 'Bash',
|
|
1103
|
+
input: { command: 'ls -la' },
|
|
1104
|
+
description: null,
|
|
1105
|
+
uuid: 'tool-uuid-2',
|
|
1106
|
+
parentUUID: null
|
|
1107
|
+
}],
|
|
1108
|
+
isSidechain: false
|
|
1109
|
+
}
|
|
1110
|
+
];
|
|
1111
|
+
|
|
1112
|
+
const result2 = reducer(state, messages2, agentState2);
|
|
1113
|
+
|
|
1114
|
+
// Should update the permission message
|
|
1115
|
+
expect(result2.messages).toHaveLength(1);
|
|
1116
|
+
expect(result2.messages[0].kind).toBe('tool-call');
|
|
1117
|
+
if (result2.messages[0].kind === 'tool-call') {
|
|
1118
|
+
expect(result2.messages[0].tool.input).toEqual({ command: 'ls -la' });
|
|
1119
|
+
expect(result2.messages[0].tool.permission?.status).toBe('approved');
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Verify it matched to the correct permission (same ID now)
|
|
1123
|
+
// Should resolve to the permission message since it was created first
|
|
1124
|
+
expect(state.toolIdToMessageId.get('tool-1')).toBe(permMsgId);
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
it('should handle full permission lifecycle: pending -> approved -> tool execution -> completion', () => {
|
|
1128
|
+
const state = createReducer();
|
|
1129
|
+
|
|
1130
|
+
// Step 1: Create pending permission
|
|
1131
|
+
const agentState1: AgentState = {
|
|
1132
|
+
requests: {
|
|
1133
|
+
'tool-1': {
|
|
1134
|
+
tool: 'Read',
|
|
1135
|
+
arguments: { file: '/test.txt' },
|
|
1136
|
+
createdAt: 1000
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
const result1 = reducer(state, [], agentState1);
|
|
1142
|
+
expect(result1.messages).toHaveLength(1);
|
|
1143
|
+
expect(result1.messages[0].kind).toBe('tool-call');
|
|
1144
|
+
if (result1.messages[0].kind === 'tool-call') {
|
|
1145
|
+
expect(result1.messages[0].tool.state).toBe('running');
|
|
1146
|
+
expect(result1.messages[0].tool.permission?.status).toBe('pending');
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Step 2: Approve permission
|
|
1150
|
+
const agentState2: AgentState = {
|
|
1151
|
+
completedRequests: {
|
|
1152
|
+
'tool-1': {
|
|
1153
|
+
tool: 'Read',
|
|
1154
|
+
arguments: { file: '/test.txt' },
|
|
1155
|
+
createdAt: 1000,
|
|
1156
|
+
completedAt: 2000,
|
|
1157
|
+
status: 'approved'
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
const result2 = reducer(state, [], agentState2);
|
|
1163
|
+
expect(result2.messages).toHaveLength(1);
|
|
1164
|
+
if (result2.messages[0].kind === 'tool-call') {
|
|
1165
|
+
expect(result2.messages[0].tool.permission?.status).toBe('approved');
|
|
1166
|
+
expect(result2.messages[0].tool.state).toBe('running');
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Step 3: Tool call arrives
|
|
1170
|
+
const toolMessages: NormalizedMessage[] = [
|
|
1171
|
+
{
|
|
1172
|
+
id: 'msg-1',
|
|
1173
|
+
localId: null,
|
|
1174
|
+
createdAt: 3000,
|
|
1175
|
+
role: 'agent',
|
|
1176
|
+
content: [{
|
|
1177
|
+
type: 'tool-call',
|
|
1178
|
+
id: 'tool-1',
|
|
1179
|
+
name: 'Read',
|
|
1180
|
+
input: { file: '/test.txt' },
|
|
1181
|
+
description: null,
|
|
1182
|
+
uuid: 'tool-uuid-1',
|
|
1183
|
+
parentUUID: null
|
|
1184
|
+
}],
|
|
1185
|
+
isSidechain: false
|
|
1186
|
+
}
|
|
1187
|
+
];
|
|
1188
|
+
|
|
1189
|
+
const result3 = reducer(state, toolMessages, agentState2);
|
|
1190
|
+
expect(result3.messages).toHaveLength(1);
|
|
1191
|
+
if (result3.messages[0].kind === 'tool-call') {
|
|
1192
|
+
expect(result3.messages[0].tool.startedAt).toBe(3000);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Step 4: Tool result arrives
|
|
1196
|
+
const resultMessages: NormalizedMessage[] = [
|
|
1197
|
+
{
|
|
1198
|
+
id: 'msg-2',
|
|
1199
|
+
localId: null,
|
|
1200
|
+
createdAt: 4000,
|
|
1201
|
+
role: 'agent',
|
|
1202
|
+
content: [{
|
|
1203
|
+
type: 'tool-result',
|
|
1204
|
+
tool_use_id: 'tool-1',
|
|
1205
|
+
content: 'File contents',
|
|
1206
|
+
is_error: false,
|
|
1207
|
+
uuid: 'result-uuid-1',
|
|
1208
|
+
parentUUID: null
|
|
1209
|
+
}],
|
|
1210
|
+
isSidechain: false
|
|
1211
|
+
}
|
|
1212
|
+
];
|
|
1213
|
+
|
|
1214
|
+
const result4 = reducer(state, resultMessages, agentState2);
|
|
1215
|
+
expect(result4.messages).toHaveLength(1);
|
|
1216
|
+
if (result4.messages[0].kind === 'tool-call') {
|
|
1217
|
+
expect(result4.messages[0].tool.state).toBe('completed');
|
|
1218
|
+
expect(result4.messages[0].tool.result).toBe('File contents');
|
|
1219
|
+
expect(result4.messages[0].tool.completedAt).toBe(4000);
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
it('should handle denied and canceled permissions correctly', () => {
|
|
1224
|
+
const state = createReducer();
|
|
1225
|
+
|
|
1226
|
+
// Create two permissions
|
|
1227
|
+
const agentState1: AgentState = {
|
|
1228
|
+
requests: {
|
|
1229
|
+
'tool-deny': {
|
|
1230
|
+
tool: 'Write',
|
|
1231
|
+
arguments: { file: '/secure.txt', content: 'hack' },
|
|
1232
|
+
createdAt: 1000
|
|
1233
|
+
},
|
|
1234
|
+
'tool-cancel': {
|
|
1235
|
+
tool: 'Delete',
|
|
1236
|
+
arguments: { file: '/important.txt' },
|
|
1237
|
+
createdAt: 1500
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
const result1 = reducer(state, [], agentState1);
|
|
1243
|
+
expect(result1.messages).toHaveLength(2);
|
|
1244
|
+
|
|
1245
|
+
// Deny first, cancel second
|
|
1246
|
+
const agentState2: AgentState = {
|
|
1247
|
+
completedRequests: {
|
|
1248
|
+
'tool-deny': {
|
|
1249
|
+
tool: 'Write',
|
|
1250
|
+
arguments: { file: '/secure.txt', content: 'hack' },
|
|
1251
|
+
createdAt: 1000,
|
|
1252
|
+
completedAt: 2000,
|
|
1253
|
+
status: 'denied',
|
|
1254
|
+
reason: 'Unauthorized access'
|
|
1255
|
+
},
|
|
1256
|
+
'tool-cancel': {
|
|
1257
|
+
tool: 'Delete',
|
|
1258
|
+
arguments: { file: '/important.txt' },
|
|
1259
|
+
createdAt: 1500,
|
|
1260
|
+
completedAt: 2500,
|
|
1261
|
+
status: 'canceled',
|
|
1262
|
+
reason: 'User canceled'
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
const result2 = reducer(state, [], agentState2);
|
|
1268
|
+
expect(result2.messages).toHaveLength(2);
|
|
1269
|
+
|
|
1270
|
+
const deniedMsg = result2.messages.find(m =>
|
|
1271
|
+
m.kind === 'tool-call' && m.tool.name === 'Write'
|
|
1272
|
+
);
|
|
1273
|
+
const canceledMsg = result2.messages.find(m =>
|
|
1274
|
+
m.kind === 'tool-call' && m.tool.name === 'Delete'
|
|
1275
|
+
);
|
|
1276
|
+
|
|
1277
|
+
if (deniedMsg?.kind === 'tool-call') {
|
|
1278
|
+
expect(deniedMsg.tool.state).toBe('error');
|
|
1279
|
+
expect(deniedMsg.tool.permission?.status).toBe('denied');
|
|
1280
|
+
expect(deniedMsg.tool.permission?.reason).toBe('Unauthorized access');
|
|
1281
|
+
expect(deniedMsg.tool.result).toEqual({ error: 'Unauthorized access' });
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
if (canceledMsg?.kind === 'tool-call') {
|
|
1285
|
+
expect(canceledMsg.tool.state).toBe('error');
|
|
1286
|
+
expect(canceledMsg.tool.permission?.status).toBe('canceled');
|
|
1287
|
+
expect(canceledMsg.tool.permission?.reason).toBe('User canceled');
|
|
1288
|
+
expect(canceledMsg.tool.result).toEqual({ error: 'User canceled' });
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
it('should handle tool result arriving before tool call (race condition)', () => {
|
|
1293
|
+
const state = createReducer();
|
|
1294
|
+
|
|
1295
|
+
// Tool result arrives first
|
|
1296
|
+
const resultMessages: NormalizedMessage[] = [
|
|
1297
|
+
{
|
|
1298
|
+
id: 'msg-1',
|
|
1299
|
+
localId: null,
|
|
1300
|
+
createdAt: 1000,
|
|
1301
|
+
role: 'agent',
|
|
1302
|
+
content: [{
|
|
1303
|
+
type: 'tool-result',
|
|
1304
|
+
tool_use_id: 'tool-1',
|
|
1305
|
+
content: 'Success',
|
|
1306
|
+
is_error: false,
|
|
1307
|
+
uuid: 'result-uuid-1',
|
|
1308
|
+
parentUUID: null
|
|
1309
|
+
}],
|
|
1310
|
+
isSidechain: false
|
|
1311
|
+
}
|
|
1312
|
+
];
|
|
1313
|
+
|
|
1314
|
+
const result1 = reducer(state, resultMessages);
|
|
1315
|
+
expect(result1.messages).toHaveLength(0); // Should not create anything
|
|
1316
|
+
|
|
1317
|
+
// Tool call arrives later
|
|
1318
|
+
const toolMessages: NormalizedMessage[] = [
|
|
1319
|
+
{
|
|
1320
|
+
id: 'msg-2',
|
|
1321
|
+
localId: null,
|
|
1322
|
+
createdAt: 2000,
|
|
1323
|
+
role: 'agent',
|
|
1324
|
+
content: [{
|
|
1325
|
+
type: 'tool-call',
|
|
1326
|
+
id: 'tool-1',
|
|
1327
|
+
name: 'Test',
|
|
1328
|
+
input: { test: true },
|
|
1329
|
+
description: null,
|
|
1330
|
+
uuid: 'tool-uuid-1',
|
|
1331
|
+
parentUUID: null
|
|
1332
|
+
}],
|
|
1333
|
+
isSidechain: false
|
|
1334
|
+
}
|
|
1335
|
+
];
|
|
1336
|
+
|
|
1337
|
+
const result2 = reducer(state, toolMessages);
|
|
1338
|
+
expect(result2.messages).toHaveLength(1);
|
|
1339
|
+
if (result2.messages[0].kind === 'tool-call') {
|
|
1340
|
+
expect(result2.messages[0].tool.state).toBe('running'); // Result was ignored
|
|
1341
|
+
expect(result2.messages[0].tool.result).toBeUndefined();
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// Result arrives again (with different message ID since it's a new message)
|
|
1345
|
+
const resultMessages2: NormalizedMessage[] = [
|
|
1346
|
+
{
|
|
1347
|
+
id: 'msg-3',
|
|
1348
|
+
localId: null,
|
|
1349
|
+
createdAt: 3000,
|
|
1350
|
+
role: 'agent',
|
|
1351
|
+
content: [{
|
|
1352
|
+
type: 'tool-result',
|
|
1353
|
+
tool_use_id: 'tool-1',
|
|
1354
|
+
content: 'Success',
|
|
1355
|
+
is_error: false,
|
|
1356
|
+
uuid: 'result-uuid-2',
|
|
1357
|
+
parentUUID: null
|
|
1358
|
+
}],
|
|
1359
|
+
isSidechain: false
|
|
1360
|
+
}
|
|
1361
|
+
];
|
|
1362
|
+
|
|
1363
|
+
const result3 = reducer(state, resultMessages2, null);
|
|
1364
|
+
|
|
1365
|
+
// Debug: Check if tool was properly registered
|
|
1366
|
+
const toolId = 'tool-1';
|
|
1367
|
+
const msgId = state.toolIdToMessageId.get(toolId);
|
|
1368
|
+
const message = msgId ? state.messages.get(msgId) : null;
|
|
1369
|
+
|
|
1370
|
+
expect(result3.messages).toHaveLength(1);
|
|
1371
|
+
if (result3.messages[0].kind === 'tool-call') {
|
|
1372
|
+
expect(result3.messages[0].tool.state).toBe('completed');
|
|
1373
|
+
expect(result3.messages[0].tool.result).toBe('Success');
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
it('should handle interleaved messages from multiple sources correctly', () => {
|
|
1378
|
+
const state = createReducer();
|
|
1379
|
+
|
|
1380
|
+
// Mix of user messages, permissions, and tool calls
|
|
1381
|
+
const agentState: AgentState = {
|
|
1382
|
+
requests: {
|
|
1383
|
+
'tool-1': {
|
|
1384
|
+
tool: 'Bash',
|
|
1385
|
+
arguments: { command: 'echo "hello"' },
|
|
1386
|
+
createdAt: 1500
|
|
1387
|
+
}
|
|
1388
|
+
},
|
|
1389
|
+
completedRequests: {
|
|
1390
|
+
'tool-2': {
|
|
1391
|
+
tool: 'Read',
|
|
1392
|
+
arguments: { file: 'test.txt' },
|
|
1393
|
+
createdAt: 500,
|
|
1394
|
+
completedAt: 1000,
|
|
1395
|
+
status: 'approved'
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
const messages: NormalizedMessage[] = [
|
|
1401
|
+
// User message
|
|
1402
|
+
{
|
|
1403
|
+
id: 'user-1',
|
|
1404
|
+
localId: 'local-1',
|
|
1405
|
+
createdAt: 1000,
|
|
1406
|
+
role: 'user',
|
|
1407
|
+
content: { type: 'text', text: 'Do something' },
|
|
1408
|
+
isSidechain: false
|
|
1409
|
+
},
|
|
1410
|
+
// Agent text
|
|
1411
|
+
{
|
|
1412
|
+
id: 'agent-1',
|
|
1413
|
+
localId: null,
|
|
1414
|
+
createdAt: 2000,
|
|
1415
|
+
role: 'agent',
|
|
1416
|
+
content: [{
|
|
1417
|
+
type: 'text',
|
|
1418
|
+
text: 'I will help you',
|
|
1419
|
+
uuid: 'agent-uuid-1',
|
|
1420
|
+
parentUUID: null
|
|
1421
|
+
}],
|
|
1422
|
+
isSidechain: false
|
|
1423
|
+
},
|
|
1424
|
+
// Tool call
|
|
1425
|
+
{
|
|
1426
|
+
id: 'tool-1',
|
|
1427
|
+
localId: null,
|
|
1428
|
+
createdAt: 3000,
|
|
1429
|
+
role: 'agent',
|
|
1430
|
+
content: [{
|
|
1431
|
+
type: 'tool-call',
|
|
1432
|
+
id: 'tool-new',
|
|
1433
|
+
name: 'Write',
|
|
1434
|
+
input: { file: 'output.txt', content: 'data' },
|
|
1435
|
+
description: null,
|
|
1436
|
+
uuid: 'tool-uuid-1',
|
|
1437
|
+
parentUUID: null
|
|
1438
|
+
}],
|
|
1439
|
+
isSidechain: false
|
|
1440
|
+
}
|
|
1441
|
+
];
|
|
1442
|
+
|
|
1443
|
+
const result = reducer(state, messages, agentState);
|
|
1444
|
+
|
|
1445
|
+
// Should create: 1 user, 1 agent text, 1 tool from permission request,
|
|
1446
|
+
// 1 tool from completed permission, 1 new tool call
|
|
1447
|
+
expect(result.messages).toHaveLength(5);
|
|
1448
|
+
|
|
1449
|
+
const types = result.messages.map(m => m.kind).sort();
|
|
1450
|
+
expect(types).toEqual(['agent-text', 'tool-call', 'tool-call', 'tool-call', 'user-text']);
|
|
1451
|
+
|
|
1452
|
+
// Verify each has correct properties
|
|
1453
|
+
const userMsg = result.messages.find(m => m.kind === 'user-text');
|
|
1454
|
+
expect(userMsg?.createdAt).toBe(1000);
|
|
1455
|
+
|
|
1456
|
+
const pendingPerm = result.messages.find(m =>
|
|
1457
|
+
m.kind === 'tool-call' && m.tool.permission?.status === 'pending'
|
|
1458
|
+
);
|
|
1459
|
+
expect(pendingPerm).toBeDefined();
|
|
1460
|
+
|
|
1461
|
+
const approvedPerm = result.messages.find(m =>
|
|
1462
|
+
m.kind === 'tool-call' && m.tool.permission?.status === 'approved'
|
|
1463
|
+
);
|
|
1464
|
+
expect(approvedPerm).toBeDefined();
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
it('should not allow multiple tool results for the same tool ID', () => {
|
|
1468
|
+
const state = createReducer();
|
|
1469
|
+
|
|
1470
|
+
// Create a tool call
|
|
1471
|
+
const toolMessages: NormalizedMessage[] = [
|
|
1472
|
+
{
|
|
1473
|
+
id: 'msg-1',
|
|
1474
|
+
localId: null,
|
|
1475
|
+
createdAt: 1000,
|
|
1476
|
+
role: 'agent',
|
|
1477
|
+
content: [{
|
|
1478
|
+
type: 'tool-call',
|
|
1479
|
+
id: 'tool-1',
|
|
1480
|
+
name: 'Test',
|
|
1481
|
+
input: {},
|
|
1482
|
+
description: null,
|
|
1483
|
+
uuid: 'tool-uuid-1',
|
|
1484
|
+
parentUUID: null
|
|
1485
|
+
}],
|
|
1486
|
+
isSidechain: false
|
|
1487
|
+
}
|
|
1488
|
+
];
|
|
1489
|
+
|
|
1490
|
+
reducer(state, toolMessages);
|
|
1491
|
+
|
|
1492
|
+
// First result
|
|
1493
|
+
const result1Messages: NormalizedMessage[] = [
|
|
1494
|
+
{
|
|
1495
|
+
id: 'msg-2',
|
|
1496
|
+
localId: null,
|
|
1497
|
+
createdAt: 2000,
|
|
1498
|
+
role: 'agent',
|
|
1499
|
+
content: [{
|
|
1500
|
+
type: 'tool-result',
|
|
1501
|
+
tool_use_id: 'tool-1',
|
|
1502
|
+
content: 'First result',
|
|
1503
|
+
is_error: false,
|
|
1504
|
+
uuid: 'result-uuid-1',
|
|
1505
|
+
parentUUID: null
|
|
1506
|
+
}],
|
|
1507
|
+
isSidechain: false
|
|
1508
|
+
}
|
|
1509
|
+
];
|
|
1510
|
+
|
|
1511
|
+
const result1 = reducer(state, result1Messages);
|
|
1512
|
+
expect(result1.messages).toHaveLength(1);
|
|
1513
|
+
if (result1.messages[0].kind === 'tool-call') {
|
|
1514
|
+
expect(result1.messages[0].tool.state).toBe('completed');
|
|
1515
|
+
expect(result1.messages[0].tool.result).toBe('First result');
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// Second result (should be ignored)
|
|
1519
|
+
const result2Messages: NormalizedMessage[] = [
|
|
1520
|
+
{
|
|
1521
|
+
id: 'msg-3',
|
|
1522
|
+
localId: null,
|
|
1523
|
+
createdAt: 3000,
|
|
1524
|
+
role: 'agent',
|
|
1525
|
+
content: [{
|
|
1526
|
+
type: 'tool-result',
|
|
1527
|
+
tool_use_id: 'tool-1',
|
|
1528
|
+
content: 'Should not override',
|
|
1529
|
+
is_error: true,
|
|
1530
|
+
uuid: 'result-uuid-2',
|
|
1531
|
+
parentUUID: null
|
|
1532
|
+
}],
|
|
1533
|
+
isSidechain: false
|
|
1534
|
+
}
|
|
1535
|
+
];
|
|
1536
|
+
|
|
1537
|
+
const result2 = reducer(state, result2Messages);
|
|
1538
|
+
expect(result2.messages).toHaveLength(0); // No changes
|
|
1539
|
+
|
|
1540
|
+
// Verify original result is preserved
|
|
1541
|
+
const toolMsgId = state.toolIdToMessageId.get('tool-1');
|
|
1542
|
+
const toolMsg = state.messages.get(toolMsgId!);
|
|
1543
|
+
expect(toolMsg?.tool?.state).toBe('completed');
|
|
1544
|
+
expect(toolMsg?.tool?.result).toBe('First result');
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
it('should handle permission updates after tool execution started', () => {
|
|
1548
|
+
const state = createReducer();
|
|
1549
|
+
|
|
1550
|
+
// Create approved permission
|
|
1551
|
+
const agentState1: AgentState = {
|
|
1552
|
+
completedRequests: {
|
|
1553
|
+
'tool-1': {
|
|
1554
|
+
tool: 'Bash',
|
|
1555
|
+
arguments: { command: 'ls' },
|
|
1556
|
+
createdAt: 1000,
|
|
1557
|
+
completedAt: 2000,
|
|
1558
|
+
status: 'approved'
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
reducer(state, [], agentState1);
|
|
1564
|
+
|
|
1565
|
+
// Tool call arrives and matches
|
|
1566
|
+
const toolMessages: NormalizedMessage[] = [
|
|
1567
|
+
{
|
|
1568
|
+
id: 'msg-1',
|
|
1569
|
+
localId: null,
|
|
1570
|
+
createdAt: 3000,
|
|
1571
|
+
role: 'agent',
|
|
1572
|
+
content: [{
|
|
1573
|
+
type: 'tool-call',
|
|
1574
|
+
id: 'tool-1',
|
|
1575
|
+
name: 'Bash',
|
|
1576
|
+
input: { command: 'ls' },
|
|
1577
|
+
description: null,
|
|
1578
|
+
uuid: 'tool-uuid-1',
|
|
1579
|
+
parentUUID: null
|
|
1580
|
+
}],
|
|
1581
|
+
isSidechain: false
|
|
1582
|
+
}
|
|
1583
|
+
];
|
|
1584
|
+
|
|
1585
|
+
reducer(state, toolMessages, agentState1);
|
|
1586
|
+
|
|
1587
|
+
// Try to change permission status (should not affect running tool)
|
|
1588
|
+
const agentState2: AgentState = {
|
|
1589
|
+
completedRequests: {
|
|
1590
|
+
'tool-1': {
|
|
1591
|
+
tool: 'Bash',
|
|
1592
|
+
arguments: { command: 'ls' },
|
|
1593
|
+
createdAt: 1000,
|
|
1594
|
+
completedAt: 4000,
|
|
1595
|
+
status: 'denied',
|
|
1596
|
+
reason: 'Changed mind'
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
|
|
1601
|
+
const result = reducer(state, [], agentState2);
|
|
1602
|
+
expect(result.messages).toHaveLength(0); // No changes, tool already started
|
|
1603
|
+
|
|
1604
|
+
// Verify tool is still running
|
|
1605
|
+
const permMsgId = state.toolIdToMessageId.get('tool-1');
|
|
1606
|
+
const permMsg = state.messages.get(permMsgId!);
|
|
1607
|
+
expect(permMsg?.tool?.state).toBe('running');
|
|
1608
|
+
expect(permMsg?.tool?.permission?.status).toBe('approved'); // Status unchanged
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
it('should handle empty or null AgentState gracefully', () => {
|
|
1612
|
+
const state = createReducer();
|
|
1613
|
+
|
|
1614
|
+
// Test with null
|
|
1615
|
+
const result1 = reducer(state, [], null);
|
|
1616
|
+
expect(result1.messages).toHaveLength(0);
|
|
1617
|
+
|
|
1618
|
+
// Test with undefined
|
|
1619
|
+
const result2 = reducer(state, [], undefined);
|
|
1620
|
+
expect(result2.messages).toHaveLength(0);
|
|
1621
|
+
|
|
1622
|
+
// Test with empty AgentState
|
|
1623
|
+
const emptyState: AgentState = {};
|
|
1624
|
+
const result3 = reducer(state, [], emptyState);
|
|
1625
|
+
expect(result3.messages).toHaveLength(0);
|
|
1626
|
+
|
|
1627
|
+
// Test with null requests/completedRequests
|
|
1628
|
+
const partialState: AgentState = {
|
|
1629
|
+
requests: null,
|
|
1630
|
+
completedRequests: null
|
|
1631
|
+
};
|
|
1632
|
+
const result4 = reducer(state, [], partialState);
|
|
1633
|
+
expect(result4.messages).toHaveLength(0);
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
it('should match completed permissions and tool calls by ID even with different arguments', () => {
|
|
1637
|
+
const state = createReducer();
|
|
1638
|
+
|
|
1639
|
+
// AgentState has completed permission for Bash with 'ls' command
|
|
1640
|
+
const agentState: AgentState = {
|
|
1641
|
+
completedRequests: {
|
|
1642
|
+
'tool-1': {
|
|
1643
|
+
tool: 'Bash',
|
|
1644
|
+
arguments: { command: 'ls' },
|
|
1645
|
+
createdAt: 1000,
|
|
1646
|
+
completedAt: 1500,
|
|
1647
|
+
status: 'approved'
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
// Incoming messages have tool call for Bash with 'pwd' command
|
|
1653
|
+
const messages: NormalizedMessage[] = [
|
|
1654
|
+
{
|
|
1655
|
+
id: 'msg-1',
|
|
1656
|
+
localId: null,
|
|
1657
|
+
createdAt: 2000,
|
|
1658
|
+
role: 'agent',
|
|
1659
|
+
content: [{
|
|
1660
|
+
type: 'tool-call',
|
|
1661
|
+
id: 'tool-1',
|
|
1662
|
+
name: 'Bash',
|
|
1663
|
+
input: { command: 'pwd' },
|
|
1664
|
+
description: null,
|
|
1665
|
+
uuid: 'tool-uuid-1',
|
|
1666
|
+
parentUUID: null
|
|
1667
|
+
}],
|
|
1668
|
+
isSidechain: false
|
|
1669
|
+
}
|
|
1670
|
+
];
|
|
1671
|
+
|
|
1672
|
+
const result = reducer(state, messages, agentState);
|
|
1673
|
+
|
|
1674
|
+
// Should update the existing permission message (ID match)
|
|
1675
|
+
expect(result.messages).toHaveLength(1);
|
|
1676
|
+
|
|
1677
|
+
// The message should have the permission's arguments
|
|
1678
|
+
const toolMessage = result.messages[0];
|
|
1679
|
+
expect(toolMessage.kind).toBe('tool-call');
|
|
1680
|
+
if (toolMessage.kind === 'tool-call') {
|
|
1681
|
+
expect(toolMessage.tool.name).toBe('Bash');
|
|
1682
|
+
// Keeps original permission arguments
|
|
1683
|
+
expect(toolMessage.tool.input).toEqual({ command: 'ls' });
|
|
1684
|
+
expect(toolMessage.tool.permission?.status).toBe('approved');
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
it('should maintain correct state across many operations', () => {
|
|
1689
|
+
const state = createReducer();
|
|
1690
|
+
let totalMessages = 0;
|
|
1691
|
+
|
|
1692
|
+
// Simulate a long conversation with many operations
|
|
1693
|
+
for (let i = 0; i < 10; i++) {
|
|
1694
|
+
// Add user message
|
|
1695
|
+
const userMsg: NormalizedMessage[] = [
|
|
1696
|
+
{
|
|
1697
|
+
id: `user-${i}`,
|
|
1698
|
+
localId: `local-${i}`,
|
|
1699
|
+
createdAt: i * 1000,
|
|
1700
|
+
role: 'user',
|
|
1701
|
+
content: { type: 'text', text: `Message ${i}` },
|
|
1702
|
+
isSidechain: false
|
|
1703
|
+
}
|
|
1704
|
+
];
|
|
1705
|
+
|
|
1706
|
+
const userResult = reducer(state, userMsg);
|
|
1707
|
+
expect(userResult.messages).toHaveLength(1);
|
|
1708
|
+
totalMessages++;
|
|
1709
|
+
|
|
1710
|
+
// Add permission
|
|
1711
|
+
const agentState: AgentState = {
|
|
1712
|
+
requests: {
|
|
1713
|
+
[`perm-${i}`]: {
|
|
1714
|
+
tool: 'Test',
|
|
1715
|
+
arguments: { index: i },
|
|
1716
|
+
createdAt: i * 1000 + 100
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
const permResult = reducer(state, [], agentState);
|
|
1722
|
+
expect(permResult.messages).toHaveLength(1);
|
|
1723
|
+
totalMessages++;
|
|
1724
|
+
|
|
1725
|
+
// Approve permission
|
|
1726
|
+
const approvedState: AgentState = {
|
|
1727
|
+
completedRequests: {
|
|
1728
|
+
[`perm-${i}`]: {
|
|
1729
|
+
tool: 'Test',
|
|
1730
|
+
arguments: { index: i },
|
|
1731
|
+
createdAt: i * 1000 + 100,
|
|
1732
|
+
completedAt: i * 1000 + 200,
|
|
1733
|
+
status: 'approved'
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
|
|
1738
|
+
reducer(state, [], approvedState);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// Verify state integrity
|
|
1742
|
+
expect(state.messages.size).toBe(totalMessages);
|
|
1743
|
+
expect(state.toolIdToMessageId.size).toBe(10);
|
|
1744
|
+
expect(state.localIds.size).toBe(10);
|
|
1745
|
+
|
|
1746
|
+
// Try to add duplicates (should not increase count)
|
|
1747
|
+
const duplicateUser: NormalizedMessage[] = [
|
|
1748
|
+
{
|
|
1749
|
+
id: 'user-0',
|
|
1750
|
+
localId: 'local-0',
|
|
1751
|
+
createdAt: 0,
|
|
1752
|
+
role: 'user',
|
|
1753
|
+
content: { type: 'text', text: 'Duplicate' },
|
|
1754
|
+
isSidechain: false
|
|
1755
|
+
}
|
|
1756
|
+
];
|
|
1757
|
+
|
|
1758
|
+
const dupResult = reducer(state, duplicateUser);
|
|
1759
|
+
expect(dupResult.messages).toHaveLength(0);
|
|
1760
|
+
expect(state.messages.size).toBe(totalMessages); // No increase
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
it('should NOT create duplicate messages for pending permission requests', () => {
|
|
1764
|
+
const state = createReducer();
|
|
1765
|
+
|
|
1766
|
+
// AgentState with a pending permission request
|
|
1767
|
+
const agentState: AgentState = {
|
|
1768
|
+
requests: {
|
|
1769
|
+
'tool-pending-1': {
|
|
1770
|
+
tool: 'Bash',
|
|
1771
|
+
arguments: { command: 'ls -la' },
|
|
1772
|
+
createdAt: 1000
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
// Process the pending permission - should create exactly ONE message
|
|
1778
|
+
const result1 = reducer(state, [], agentState);
|
|
1779
|
+
expect(result1.messages).toHaveLength(1);
|
|
1780
|
+
expect(result1.messages[0].kind).toBe('tool-call');
|
|
1781
|
+
|
|
1782
|
+
// Verify only one message exists
|
|
1783
|
+
const pendingMessageId = state.toolIdToMessageId.get('tool-pending-1');
|
|
1784
|
+
expect(pendingMessageId).toBeDefined();
|
|
1785
|
+
expect(state.messages.size).toBe(1);
|
|
1786
|
+
|
|
1787
|
+
// Process again with same state - should not create duplicate
|
|
1788
|
+
const result2 = reducer(state, [], agentState);
|
|
1789
|
+
expect(result2.messages).toHaveLength(0); // No new messages
|
|
1790
|
+
expect(state.messages.size).toBe(1); // Still only one message
|
|
1791
|
+
|
|
1792
|
+
// Verify the message has correct permission status
|
|
1793
|
+
const message = state.messages.get(pendingMessageId!);
|
|
1794
|
+
expect(message?.tool?.permission?.status).toBe('pending');
|
|
1795
|
+
expect(message?.tool?.permission?.id).toBe('tool-pending-1');
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
it('should match permissions when tool messages are loaded BEFORE AgentState', () => {
|
|
1799
|
+
const state = createReducer();
|
|
1800
|
+
|
|
1801
|
+
// First, process the tool call message (as if loaded from storage)
|
|
1802
|
+
const toolMessage: NormalizedMessage = {
|
|
1803
|
+
id: 'msg-1',
|
|
1804
|
+
localId: null,
|
|
1805
|
+
createdAt: 1000,
|
|
1806
|
+
role: 'agent',
|
|
1807
|
+
content: [{
|
|
1808
|
+
type: 'tool-call',
|
|
1809
|
+
id: 'tool-1',
|
|
1810
|
+
name: 'Bash',
|
|
1811
|
+
input: { command: 'ls -la' },
|
|
1812
|
+
description: null,
|
|
1813
|
+
uuid: 'tool-uuid-1',
|
|
1814
|
+
parentUUID: null
|
|
1815
|
+
}],
|
|
1816
|
+
isSidechain: false
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1819
|
+
const messages = [toolMessage];
|
|
1820
|
+
const result1 = reducer(state, messages);
|
|
1821
|
+
|
|
1822
|
+
// Should create the tool message
|
|
1823
|
+
expect(result1.messages).toHaveLength(1);
|
|
1824
|
+
expect(state.messages.size).toBe(1);
|
|
1825
|
+
|
|
1826
|
+
// Now process the AgentState with pending permission
|
|
1827
|
+
const agentState: AgentState = {
|
|
1828
|
+
requests: {
|
|
1829
|
+
'tool-1': {
|
|
1830
|
+
tool: 'Bash',
|
|
1831
|
+
arguments: { command: 'ls -la' },
|
|
1832
|
+
createdAt: 900 // Permission requested before the tool call
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
|
|
1837
|
+
const result2 = reducer(state, [], agentState);
|
|
1838
|
+
|
|
1839
|
+
// Should NOT create a new message, but update the existing one
|
|
1840
|
+
expect(result2.messages).toHaveLength(1); // The updated message
|
|
1841
|
+
expect(state.messages.size).toBe(1); // Still only one message
|
|
1842
|
+
|
|
1843
|
+
// The existing tool message should now have the permission attached
|
|
1844
|
+
const messageId = state.toolIdToMessageId.get('tool-1');
|
|
1845
|
+
expect(messageId).toBeDefined();
|
|
1846
|
+
|
|
1847
|
+
const message = state.messages.get(messageId!);
|
|
1848
|
+
expect(message?.tool?.name).toBe('Bash');
|
|
1849
|
+
expect(message?.tool?.permission?.status).toBe('pending');
|
|
1850
|
+
expect(message?.tool?.permission?.id).toBe('tool-1');
|
|
1851
|
+
});
|
|
1852
|
+
|
|
1853
|
+
it('should match permissions when tool messages are loaded AFTER AgentState', () => {
|
|
1854
|
+
const state = createReducer();
|
|
1855
|
+
|
|
1856
|
+
// First, process the AgentState with pending permission
|
|
1857
|
+
const agentState: AgentState = {
|
|
1858
|
+
requests: {
|
|
1859
|
+
'tool-1': {
|
|
1860
|
+
tool: 'Bash',
|
|
1861
|
+
arguments: { command: 'ls -la' },
|
|
1862
|
+
createdAt: 900
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
|
|
1867
|
+
const result1 = reducer(state, [], agentState);
|
|
1868
|
+
|
|
1869
|
+
// Should create a permission message
|
|
1870
|
+
expect(result1.messages).toHaveLength(1);
|
|
1871
|
+
expect(state.messages.size).toBe(1);
|
|
1872
|
+
|
|
1873
|
+
// Now process the tool call message
|
|
1874
|
+
const toolMessage: NormalizedMessage = {
|
|
1875
|
+
id: 'msg-1',
|
|
1876
|
+
localId: null,
|
|
1877
|
+
createdAt: 1000,
|
|
1878
|
+
role: 'agent',
|
|
1879
|
+
content: [{
|
|
1880
|
+
type: 'tool-call',
|
|
1881
|
+
id: 'tool-1',
|
|
1882
|
+
name: 'Bash',
|
|
1883
|
+
input: { command: 'ls -la' },
|
|
1884
|
+
description: null,
|
|
1885
|
+
uuid: 'tool-uuid-1',
|
|
1886
|
+
parentUUID: null
|
|
1887
|
+
}],
|
|
1888
|
+
isSidechain: false
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1891
|
+
const messages = [toolMessage];
|
|
1892
|
+
const result2 = reducer(state, messages, agentState);
|
|
1893
|
+
|
|
1894
|
+
// Should NOT create a new message, but update the existing permission message
|
|
1895
|
+
expect(result2.messages).toHaveLength(1); // The updated message
|
|
1896
|
+
expect(state.messages.size).toBe(1); // Still only one message
|
|
1897
|
+
|
|
1898
|
+
// The permission message should now be linked to the tool
|
|
1899
|
+
const messageId = state.toolIdToMessageId.get('tool-1');
|
|
1900
|
+
expect(messageId).toBeDefined();
|
|
1901
|
+
|
|
1902
|
+
const message = state.messages.get(messageId!);
|
|
1903
|
+
expect(message?.tool?.name).toBe('Bash');
|
|
1904
|
+
expect(message?.tool?.permission?.status).toBe('pending');
|
|
1905
|
+
expect(message?.tool?.permission?.id).toBe('tool-1');
|
|
1906
|
+
expect(message?.tool?.startedAt).toBe(1000); // From the tool message
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
it('should not downgrade approved permission to pending when AgentState has both', () => {
|
|
1910
|
+
const state = createReducer();
|
|
1911
|
+
|
|
1912
|
+
// AgentState with both pending and completed for same permission
|
|
1913
|
+
// This can happen when server sends stale data
|
|
1914
|
+
const agentState: AgentState = {
|
|
1915
|
+
requests: {
|
|
1916
|
+
'tool-1': {
|
|
1917
|
+
tool: 'Bash',
|
|
1918
|
+
arguments: { command: 'ls -la' },
|
|
1919
|
+
createdAt: 1000
|
|
1920
|
+
}
|
|
1921
|
+
},
|
|
1922
|
+
completedRequests: {
|
|
1923
|
+
'tool-1': {
|
|
1924
|
+
tool: 'Bash',
|
|
1925
|
+
arguments: { command: 'ls -la' },
|
|
1926
|
+
createdAt: 1000,
|
|
1927
|
+
completedAt: 2000,
|
|
1928
|
+
status: 'approved'
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
|
|
1933
|
+
// Process tool message
|
|
1934
|
+
const toolMessage: NormalizedMessage = {
|
|
1935
|
+
id: 'msg-1',
|
|
1936
|
+
localId: null,
|
|
1937
|
+
createdAt: 1500,
|
|
1938
|
+
role: 'agent',
|
|
1939
|
+
content: [{
|
|
1940
|
+
type: 'tool-call',
|
|
1941
|
+
id: 'tool-1',
|
|
1942
|
+
name: 'Bash',
|
|
1943
|
+
input: { command: 'ls -la' },
|
|
1944
|
+
description: null,
|
|
1945
|
+
uuid: 'tool-uuid-1',
|
|
1946
|
+
parentUUID: null
|
|
1947
|
+
}],
|
|
1948
|
+
isSidechain: false
|
|
1949
|
+
};
|
|
1950
|
+
|
|
1951
|
+
const messages = [toolMessage];
|
|
1952
|
+
const result = reducer(state, messages, agentState);
|
|
1953
|
+
|
|
1954
|
+
// Should create one message
|
|
1955
|
+
expect(result.messages).toHaveLength(1);
|
|
1956
|
+
|
|
1957
|
+
// Permission should be approved, NOT pending
|
|
1958
|
+
const messageId = state.toolIdToMessageId.get('tool-1');
|
|
1959
|
+
expect(messageId).toBeDefined();
|
|
1960
|
+
const message = state.messages.get(messageId!);
|
|
1961
|
+
expect(message).toBeDefined();
|
|
1962
|
+
expect(message?.tool).toBeDefined();
|
|
1963
|
+
expect(message?.tool?.permission).toBeDefined();
|
|
1964
|
+
expect(message?.tool?.permission?.status).toBe('approved'); // Not 'pending'!
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
it('should update permission status when AgentState changes from pending to approved', () => {
|
|
1968
|
+
const state = createReducer();
|
|
1969
|
+
|
|
1970
|
+
// First, create a tool message with pending permission
|
|
1971
|
+
const agentState1: AgentState = {
|
|
1972
|
+
requests: {
|
|
1973
|
+
'tool-1': {
|
|
1974
|
+
tool: 'Bash',
|
|
1975
|
+
arguments: { command: 'ls -la' },
|
|
1976
|
+
createdAt: 1000
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
|
|
1981
|
+
const toolMessage: NormalizedMessage = {
|
|
1982
|
+
id: 'msg-1',
|
|
1983
|
+
localId: null,
|
|
1984
|
+
createdAt: 1500,
|
|
1985
|
+
role: 'agent',
|
|
1986
|
+
content: [{
|
|
1987
|
+
type: 'tool-call',
|
|
1988
|
+
id: 'tool-1',
|
|
1989
|
+
name: 'Bash',
|
|
1990
|
+
input: { command: 'ls -la' },
|
|
1991
|
+
description: null,
|
|
1992
|
+
uuid: 'tool-uuid-1',
|
|
1993
|
+
parentUUID: null
|
|
1994
|
+
}],
|
|
1995
|
+
isSidechain: false
|
|
1996
|
+
};
|
|
1997
|
+
|
|
1998
|
+
// Process with pending permission
|
|
1999
|
+
const messages = [toolMessage];
|
|
2000
|
+
const result1 = reducer(state, messages, agentState1);
|
|
2001
|
+
|
|
2002
|
+
// Should create one message with pending permission
|
|
2003
|
+
expect(result1.messages).toHaveLength(1);
|
|
2004
|
+
expect(state.messages.size).toBe(1);
|
|
2005
|
+
|
|
2006
|
+
const messageId = state.toolIdToMessageId.get('tool-1');
|
|
2007
|
+
expect(messageId).toBeDefined();
|
|
2008
|
+
|
|
2009
|
+
let message = state.messages.get(messageId!);
|
|
2010
|
+
expect(message?.tool?.permission?.status).toBe('pending');
|
|
2011
|
+
|
|
2012
|
+
// Now update AgentState to approved
|
|
2013
|
+
const agentState2: AgentState = {
|
|
2014
|
+
completedRequests: {
|
|
2015
|
+
'tool-1': {
|
|
2016
|
+
tool: 'Bash',
|
|
2017
|
+
arguments: { command: 'ls -la' },
|
|
2018
|
+
createdAt: 1000,
|
|
2019
|
+
completedAt: 2000,
|
|
2020
|
+
status: 'approved'
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
};
|
|
2024
|
+
|
|
2025
|
+
// Process only the new AgentState (simulating applySessions update)
|
|
2026
|
+
const result2 = reducer(state, [], agentState2);
|
|
2027
|
+
|
|
2028
|
+
// Should return the updated message
|
|
2029
|
+
expect(result2.messages).toHaveLength(1);
|
|
2030
|
+
expect(state.messages.size).toBe(1); // Still only one message
|
|
2031
|
+
|
|
2032
|
+
// Check that the permission status was updated
|
|
2033
|
+
message = state.messages.get(messageId!);
|
|
2034
|
+
expect(message?.tool?.permission?.status).toBe('approved');
|
|
2035
|
+
expect(message?.tool?.permission?.id).toBe('tool-1');
|
|
2036
|
+
});
|
|
2037
|
+
|
|
2038
|
+
it('should handle app loading flow: tool loaded first, then AgentState with approved permission', () => {
|
|
2039
|
+
const state = createReducer();
|
|
2040
|
+
|
|
2041
|
+
// Step 1: Load tool message first (without AgentState) - simulates messages loaded before sessions
|
|
2042
|
+
const toolMessage: NormalizedMessage = {
|
|
2043
|
+
id: 'msg-1',
|
|
2044
|
+
localId: null,
|
|
2045
|
+
createdAt: 1500,
|
|
2046
|
+
role: 'agent',
|
|
2047
|
+
content: [{
|
|
2048
|
+
type: 'tool-call',
|
|
2049
|
+
id: 'tool-1',
|
|
2050
|
+
name: 'Bash',
|
|
2051
|
+
input: { command: 'ls -la' },
|
|
2052
|
+
description: null,
|
|
2053
|
+
uuid: 'tool-uuid-1',
|
|
2054
|
+
parentUUID: null
|
|
2055
|
+
}],
|
|
2056
|
+
isSidechain: false
|
|
2057
|
+
};
|
|
2058
|
+
|
|
2059
|
+
const messages = [toolMessage];
|
|
2060
|
+
const result1 = reducer(state, messages); // No AgentState
|
|
2061
|
+
|
|
2062
|
+
// Tool should be created without permission
|
|
2063
|
+
expect(result1.messages).toHaveLength(1);
|
|
2064
|
+
const toolMsgId = state.toolIdToMessageId.get('tool-1');
|
|
2065
|
+
expect(toolMsgId).toBeDefined();
|
|
2066
|
+
let toolMsg = state.messages.get(toolMsgId!);
|
|
2067
|
+
expect(toolMsg?.tool?.permission).toBeUndefined();
|
|
2068
|
+
expect(toolMsg?.tool?.state).toBe('running');
|
|
2069
|
+
|
|
2070
|
+
// Step 2: AgentState arrives with both pending and approved (sessions loaded)
|
|
2071
|
+
const agentState: AgentState = {
|
|
2072
|
+
requests: {
|
|
2073
|
+
'tool-1': {
|
|
2074
|
+
tool: 'Bash',
|
|
2075
|
+
arguments: { command: 'ls -la' },
|
|
2076
|
+
createdAt: 1000
|
|
2077
|
+
}
|
|
2078
|
+
},
|
|
2079
|
+
completedRequests: {
|
|
2080
|
+
'tool-1': {
|
|
2081
|
+
tool: 'Bash',
|
|
2082
|
+
arguments: { command: 'ls -la' },
|
|
2083
|
+
createdAt: 1000,
|
|
2084
|
+
completedAt: 2000,
|
|
2085
|
+
status: 'approved'
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
const result2 = reducer(state, [], agentState);
|
|
2091
|
+
|
|
2092
|
+
// Should update the existing tool with approved permission
|
|
2093
|
+
expect(result2.messages).toHaveLength(1); // Updated message
|
|
2094
|
+
expect(state.messages.size).toBe(1); // Still only one message
|
|
2095
|
+
|
|
2096
|
+
toolMsg = state.messages.get(toolMsgId!);
|
|
2097
|
+
expect(toolMsg?.tool?.permission).toBeDefined();
|
|
2098
|
+
expect(toolMsg?.tool?.permission?.status).toBe('approved');
|
|
2099
|
+
expect(toolMsg?.tool?.permission?.id).toBe('tool-1');
|
|
2100
|
+
expect(toolMsg?.tool?.state).toBe('running'); // Should stay running for approved
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
it('should handle app loading flow: tool loaded first, then AgentState with denied permission', () => {
|
|
2104
|
+
const state = createReducer();
|
|
2105
|
+
|
|
2106
|
+
// Step 1: Load tool message first
|
|
2107
|
+
const toolMessage: NormalizedMessage = {
|
|
2108
|
+
id: 'msg-1',
|
|
2109
|
+
localId: null,
|
|
2110
|
+
createdAt: 1500,
|
|
2111
|
+
role: 'agent',
|
|
2112
|
+
content: [{
|
|
2113
|
+
type: 'tool-call',
|
|
2114
|
+
id: 'tool-1',
|
|
2115
|
+
name: 'Bash',
|
|
2116
|
+
input: { command: 'rm -rf /' },
|
|
2117
|
+
description: null,
|
|
2118
|
+
uuid: 'tool-uuid-1',
|
|
2119
|
+
parentUUID: null
|
|
2120
|
+
}],
|
|
2121
|
+
isSidechain: false
|
|
2122
|
+
};
|
|
2123
|
+
|
|
2124
|
+
const messages = [toolMessage];
|
|
2125
|
+
reducer(state, messages);
|
|
2126
|
+
|
|
2127
|
+
// Step 2: AgentState arrives with denied permission
|
|
2128
|
+
const agentState: AgentState = {
|
|
2129
|
+
requests: {
|
|
2130
|
+
'tool-1': {
|
|
2131
|
+
tool: 'Bash',
|
|
2132
|
+
arguments: { command: 'rm -rf /' },
|
|
2133
|
+
createdAt: 1000
|
|
2134
|
+
}
|
|
2135
|
+
},
|
|
2136
|
+
completedRequests: {
|
|
2137
|
+
'tool-1': {
|
|
2138
|
+
tool: 'Bash',
|
|
2139
|
+
arguments: { command: 'rm -rf /' },
|
|
2140
|
+
createdAt: 1000,
|
|
2141
|
+
completedAt: 2000,
|
|
2142
|
+
status: 'denied',
|
|
2143
|
+
reason: 'Dangerous command'
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2147
|
+
|
|
2148
|
+
const result2 = reducer(state, [], agentState);
|
|
2149
|
+
|
|
2150
|
+
// Should update the existing tool with denied permission
|
|
2151
|
+
expect(result2.messages).toHaveLength(1);
|
|
2152
|
+
|
|
2153
|
+
const toolMsgId = state.toolIdToMessageId.get('tool-1');
|
|
2154
|
+
const toolMsg = state.messages.get(toolMsgId!);
|
|
2155
|
+
expect(toolMsg?.tool?.permission?.status).toBe('denied');
|
|
2156
|
+
expect(toolMsg?.tool?.permission?.reason).toBe('Dangerous command');
|
|
2157
|
+
expect(toolMsg?.tool?.state).toBe('error'); // Should change to error
|
|
2158
|
+
expect(toolMsg?.tool?.completedAt).toBeDefined();
|
|
2159
|
+
expect(toolMsg?.tool?.result).toEqual({ error: 'Dangerous command' });
|
|
2160
|
+
});
|
|
2161
|
+
|
|
2162
|
+
it('should handle app loading flow: tool loaded first, then AgentState with canceled permission', () => {
|
|
2163
|
+
const state = createReducer();
|
|
2164
|
+
|
|
2165
|
+
// Step 1: Load tool message first
|
|
2166
|
+
const toolMessage: NormalizedMessage = {
|
|
2167
|
+
id: 'msg-1',
|
|
2168
|
+
localId: null,
|
|
2169
|
+
createdAt: 1500,
|
|
2170
|
+
role: 'agent',
|
|
2171
|
+
content: [{
|
|
2172
|
+
type: 'tool-call',
|
|
2173
|
+
id: 'tool-1',
|
|
2174
|
+
name: 'Bash',
|
|
2175
|
+
input: { command: 'sleep 3600' },
|
|
2176
|
+
description: null,
|
|
2177
|
+
uuid: 'tool-uuid-1',
|
|
2178
|
+
parentUUID: null
|
|
2179
|
+
}],
|
|
2180
|
+
isSidechain: false
|
|
2181
|
+
};
|
|
2182
|
+
|
|
2183
|
+
const messages = [toolMessage];
|
|
2184
|
+
reducer(state, messages);
|
|
2185
|
+
|
|
2186
|
+
// Step 2: AgentState arrives with canceled permission
|
|
2187
|
+
const agentState: AgentState = {
|
|
2188
|
+
requests: {
|
|
2189
|
+
'tool-1': {
|
|
2190
|
+
tool: 'Bash',
|
|
2191
|
+
arguments: { command: 'sleep 3600' },
|
|
2192
|
+
createdAt: 1000
|
|
2193
|
+
}
|
|
2194
|
+
},
|
|
2195
|
+
completedRequests: {
|
|
2196
|
+
'tool-1': {
|
|
2197
|
+
tool: 'Bash',
|
|
2198
|
+
arguments: { command: 'sleep 3600' },
|
|
2199
|
+
createdAt: 1000,
|
|
2200
|
+
completedAt: 2000,
|
|
2201
|
+
status: 'canceled',
|
|
2202
|
+
reason: 'User canceled'
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
|
|
2207
|
+
const result2 = reducer(state, [], agentState);
|
|
2208
|
+
|
|
2209
|
+
// Should update the existing tool with canceled permission
|
|
2210
|
+
expect(result2.messages).toHaveLength(1);
|
|
2211
|
+
|
|
2212
|
+
const toolMsgId = state.toolIdToMessageId.get('tool-1');
|
|
2213
|
+
const toolMsg = state.messages.get(toolMsgId!);
|
|
2214
|
+
expect(toolMsg?.tool?.permission?.status).toBe('canceled');
|
|
2215
|
+
expect(toolMsg?.tool?.permission?.reason).toBe('User canceled');
|
|
2216
|
+
expect(toolMsg?.tool?.state).toBe('error'); // Should change to error
|
|
2217
|
+
expect(toolMsg?.tool?.completedAt).toBeDefined();
|
|
2218
|
+
expect(toolMsg?.tool?.result).toEqual({ error: 'User canceled' });
|
|
2219
|
+
});
|
|
2220
|
+
|
|
2221
|
+
it('should handle permission state transitions correctly', () => {
|
|
2222
|
+
const state = createReducer();
|
|
2223
|
+
|
|
2224
|
+
// Start with pending permission
|
|
2225
|
+
const agentState1: AgentState = {
|
|
2226
|
+
requests: {
|
|
2227
|
+
'tool-1': {
|
|
2228
|
+
tool: 'Bash',
|
|
2229
|
+
arguments: { command: 'echo test' },
|
|
2230
|
+
createdAt: 1000
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
2234
|
+
|
|
2235
|
+
const result1 = reducer(state, [], agentState1);
|
|
2236
|
+
expect(result1.messages).toHaveLength(1);
|
|
2237
|
+
|
|
2238
|
+
const permMsgId = state.toolIdToMessageId.get('tool-1');
|
|
2239
|
+
let msg = state.messages.get(permMsgId!);
|
|
2240
|
+
expect(msg?.tool?.permission?.status).toBe('pending');
|
|
2241
|
+
expect(msg?.tool?.state).toBe('running');
|
|
2242
|
+
|
|
2243
|
+
// Transition to approved
|
|
2244
|
+
const agentState2: AgentState = {
|
|
2245
|
+
completedRequests: {
|
|
2246
|
+
'tool-1': {
|
|
2247
|
+
tool: 'Bash',
|
|
2248
|
+
arguments: { command: 'echo test' },
|
|
2249
|
+
createdAt: 1000,
|
|
2250
|
+
completedAt: 2000,
|
|
2251
|
+
status: 'approved'
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
};
|
|
2255
|
+
|
|
2256
|
+
const result2 = reducer(state, [], agentState2);
|
|
2257
|
+
expect(result2.messages).toHaveLength(1);
|
|
2258
|
+
|
|
2259
|
+
msg = state.messages.get(permMsgId!);
|
|
2260
|
+
expect(msg?.tool?.permission?.status).toBe('approved');
|
|
2261
|
+
expect(msg?.tool?.state).toBe('running'); // Should stay running
|
|
2262
|
+
expect(msg?.tool?.completedAt).toBeNull(); // Not completed yet
|
|
2263
|
+
|
|
2264
|
+
// Now simulate a different scenario: transition from pending to denied
|
|
2265
|
+
const state2 = createReducer();
|
|
2266
|
+
const agentState3: AgentState = {
|
|
2267
|
+
requests: {
|
|
2268
|
+
'tool-2': {
|
|
2269
|
+
tool: 'Bash',
|
|
2270
|
+
arguments: { command: 'echo denied' },
|
|
2271
|
+
createdAt: 3000
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
|
|
2276
|
+
reducer(state2, [], agentState3);
|
|
2277
|
+
|
|
2278
|
+
const agentState4: AgentState = {
|
|
2279
|
+
completedRequests: {
|
|
2280
|
+
'tool-2': {
|
|
2281
|
+
tool: 'Bash',
|
|
2282
|
+
arguments: { command: 'echo denied' },
|
|
2283
|
+
createdAt: 3000,
|
|
2284
|
+
completedAt: 4000,
|
|
2285
|
+
status: 'denied',
|
|
2286
|
+
reason: 'Not allowed'
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
};
|
|
2290
|
+
|
|
2291
|
+
const result4 = reducer(state2, [], agentState4);
|
|
2292
|
+
expect(result4.messages).toHaveLength(1);
|
|
2293
|
+
|
|
2294
|
+
const permMsgId2 = state2.toolIdToMessageId.get('tool-2');
|
|
2295
|
+
const msg2 = state2.messages.get(permMsgId2!);
|
|
2296
|
+
expect(msg2?.tool?.permission?.status).toBe('denied');
|
|
2297
|
+
expect(msg2?.tool?.state).toBe('error'); // Should change to error
|
|
2298
|
+
expect(msg2?.tool?.completedAt).toBe(4000);
|
|
2299
|
+
expect(msg2?.tool?.result).toEqual({ error: 'Not allowed' });
|
|
2300
|
+
});
|
|
2301
|
+
|
|
2302
|
+
it('should handle finished tool: completed successfully, then AgentState with approved permission', () => {
|
|
2303
|
+
const state = createReducer();
|
|
2304
|
+
|
|
2305
|
+
// Step 1: Load tool message that's already completed
|
|
2306
|
+
const toolMessage: NormalizedMessage = {
|
|
2307
|
+
id: 'msg-1',
|
|
2308
|
+
localId: null,
|
|
2309
|
+
createdAt: 1500,
|
|
2310
|
+
role: 'agent',
|
|
2311
|
+
isSidechain: false,
|
|
2312
|
+
content: [{
|
|
2313
|
+
type: 'tool-call',
|
|
2314
|
+
id: 'tool-1',
|
|
2315
|
+
name: 'Bash',
|
|
2316
|
+
input: { command: 'echo success' },
|
|
2317
|
+
description: null,
|
|
2318
|
+
uuid: 'tool-uuid-1',
|
|
2319
|
+
parentUUID: null
|
|
2320
|
+
}]
|
|
2321
|
+
};
|
|
2322
|
+
|
|
2323
|
+
// Tool result message
|
|
2324
|
+
const resultMessage: NormalizedMessage = {
|
|
2325
|
+
id: 'msg-2',
|
|
2326
|
+
localId: null,
|
|
2327
|
+
createdAt: 2000,
|
|
2328
|
+
role: 'agent',
|
|
2329
|
+
isSidechain: false,
|
|
2330
|
+
content: [{
|
|
2331
|
+
type: 'tool-result',
|
|
2332
|
+
tool_use_id: 'tool-1',
|
|
2333
|
+
content: 'success\n',
|
|
2334
|
+
is_error: false,
|
|
2335
|
+
uuid: 'tool-uuid-2',
|
|
2336
|
+
parentUUID: null
|
|
2337
|
+
}]
|
|
2338
|
+
};
|
|
2339
|
+
|
|
2340
|
+
const messages = [toolMessage, resultMessage];
|
|
2341
|
+
reducer(state, messages);
|
|
2342
|
+
|
|
2343
|
+
// Verify tool is completed
|
|
2344
|
+
const toolMsgId = state.toolIdToMessageId.get('tool-1');
|
|
2345
|
+
let toolMsg = state.messages.get(toolMsgId!);
|
|
2346
|
+
expect(toolMsg?.tool?.state).toBe('completed');
|
|
2347
|
+
expect(toolMsg?.tool?.result).toBe('success\n');
|
|
2348
|
+
expect(toolMsg?.tool?.permission).toBeUndefined();
|
|
2349
|
+
|
|
2350
|
+
// Step 2: AgentState arrives with approved permission
|
|
2351
|
+
const agentState: AgentState = {
|
|
2352
|
+
requests: {
|
|
2353
|
+
'tool-1': {
|
|
2354
|
+
tool: 'Bash',
|
|
2355
|
+
arguments: { command: 'echo success' },
|
|
2356
|
+
createdAt: 1000
|
|
2357
|
+
}
|
|
2358
|
+
},
|
|
2359
|
+
completedRequests: {
|
|
2360
|
+
'tool-1': {
|
|
2361
|
+
tool: 'Bash',
|
|
2362
|
+
arguments: { command: 'echo success' },
|
|
2363
|
+
createdAt: 1000,
|
|
2364
|
+
completedAt: 1400,
|
|
2365
|
+
status: 'approved'
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
};
|
|
2369
|
+
|
|
2370
|
+
const result = reducer(state, [], agentState);
|
|
2371
|
+
|
|
2372
|
+
// Permission should be attached but tool should remain completed
|
|
2373
|
+
expect(result.messages).toHaveLength(1);
|
|
2374
|
+
toolMsg = state.messages.get(toolMsgId!);
|
|
2375
|
+
expect(toolMsg?.tool?.permission?.status).toBe('approved');
|
|
2376
|
+
expect(toolMsg?.tool?.state).toBe('completed'); // Should stay completed
|
|
2377
|
+
expect(toolMsg?.tool?.result).toBe('success\n'); // Result unchanged
|
|
2378
|
+
});
|
|
2379
|
+
|
|
2380
|
+
it('should handle finished tool: completed successfully, then AgentState with denied permission', () => {
|
|
2381
|
+
const state = createReducer();
|
|
2382
|
+
|
|
2383
|
+
// Step 1: Load completed tool
|
|
2384
|
+
const toolMessage: NormalizedMessage = {
|
|
2385
|
+
id: 'msg-1',
|
|
2386
|
+
localId: null,
|
|
2387
|
+
createdAt: 1500,
|
|
2388
|
+
role: 'agent',
|
|
2389
|
+
isSidechain: false,
|
|
2390
|
+
content: [{
|
|
2391
|
+
type: 'tool-call',
|
|
2392
|
+
id: 'tool-1',
|
|
2393
|
+
name: 'Bash',
|
|
2394
|
+
input: { command: 'rm important.txt' },
|
|
2395
|
+
description: null,
|
|
2396
|
+
uuid: 'tool-uuid-1',
|
|
2397
|
+
parentUUID: null
|
|
2398
|
+
}]
|
|
2399
|
+
};
|
|
2400
|
+
|
|
2401
|
+
const resultMessage: NormalizedMessage = {
|
|
2402
|
+
id: 'msg-2',
|
|
2403
|
+
localId: null,
|
|
2404
|
+
createdAt: 2000,
|
|
2405
|
+
role: 'agent',
|
|
2406
|
+
isSidechain: false,
|
|
2407
|
+
content: [{
|
|
2408
|
+
type: 'tool-result',
|
|
2409
|
+
tool_use_id: 'tool-1',
|
|
2410
|
+
content: 'file removed',
|
|
2411
|
+
is_error: false,
|
|
2412
|
+
uuid: 'tool-uuid-2',
|
|
2413
|
+
parentUUID: null
|
|
2414
|
+
}]
|
|
2415
|
+
};
|
|
2416
|
+
|
|
2417
|
+
reducer(state, [toolMessage, resultMessage]);
|
|
2418
|
+
|
|
2419
|
+
const toolMsgId = state.toolIdToMessageId.get('tool-1');
|
|
2420
|
+
let toolMsg = state.messages.get(toolMsgId!);
|
|
2421
|
+
expect(toolMsg?.tool?.state).toBe('completed');
|
|
2422
|
+
|
|
2423
|
+
// Step 2: AgentState with denied permission (too late!)
|
|
2424
|
+
const agentState: AgentState = {
|
|
2425
|
+
requests: {
|
|
2426
|
+
'tool-1': {
|
|
2427
|
+
tool: 'Bash',
|
|
2428
|
+
arguments: { command: 'rm important.txt' },
|
|
2429
|
+
createdAt: 1000
|
|
2430
|
+
}
|
|
2431
|
+
},
|
|
2432
|
+
completedRequests: {
|
|
2433
|
+
'tool-1': {
|
|
2434
|
+
tool: 'Bash',
|
|
2435
|
+
arguments: { command: 'rm important.txt' },
|
|
2436
|
+
createdAt: 1000,
|
|
2437
|
+
completedAt: 1400,
|
|
2438
|
+
status: 'denied',
|
|
2439
|
+
reason: 'Dangerous operation'
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
};
|
|
2443
|
+
|
|
2444
|
+
reducer(state, [], agentState);
|
|
2445
|
+
|
|
2446
|
+
// Tool should NOT change to error (already executed)
|
|
2447
|
+
toolMsg = state.messages.get(toolMsgId!);
|
|
2448
|
+
expect(toolMsg?.tool?.permission?.status).toBe('denied');
|
|
2449
|
+
expect(toolMsg?.tool?.permission?.reason).toBe('Dangerous operation');
|
|
2450
|
+
expect(toolMsg?.tool?.state).toBe('completed'); // Should stay completed, not error
|
|
2451
|
+
expect(toolMsg?.tool?.result).toBe('file removed'); // Result unchanged
|
|
2452
|
+
});
|
|
2453
|
+
|
|
2454
|
+
it('should handle finished tool: errored, then AgentState with approved permission', () => {
|
|
2455
|
+
const state = createReducer();
|
|
2456
|
+
|
|
2457
|
+
// Step 1: Load tool that errored
|
|
2458
|
+
const toolMessage: NormalizedMessage = {
|
|
2459
|
+
id: 'msg-1',
|
|
2460
|
+
localId: null,
|
|
2461
|
+
createdAt: 1500,
|
|
2462
|
+
role: 'agent',
|
|
2463
|
+
isSidechain: false,
|
|
2464
|
+
content: [{
|
|
2465
|
+
type: 'tool-call',
|
|
2466
|
+
id: 'tool-1',
|
|
2467
|
+
name: 'Bash',
|
|
2468
|
+
input: { command: 'cat /nonexistent' },
|
|
2469
|
+
description: null,
|
|
2470
|
+
uuid: 'tool-uuid-1',
|
|
2471
|
+
parentUUID: null
|
|
2472
|
+
}]
|
|
2473
|
+
};
|
|
2474
|
+
|
|
2475
|
+
const errorMessage: NormalizedMessage = {
|
|
2476
|
+
id: 'msg-2',
|
|
2477
|
+
localId: null,
|
|
2478
|
+
createdAt: 2000,
|
|
2479
|
+
role: 'agent',
|
|
2480
|
+
isSidechain: false,
|
|
2481
|
+
content: [{
|
|
2482
|
+
type: 'tool-result',
|
|
2483
|
+
tool_use_id: 'tool-1',
|
|
2484
|
+
content: 'File not found',
|
|
2485
|
+
is_error: true,
|
|
2486
|
+
uuid: 'tool-uuid-2',
|
|
2487
|
+
parentUUID: null
|
|
2488
|
+
}]
|
|
2489
|
+
};
|
|
2490
|
+
|
|
2491
|
+
reducer(state, [toolMessage, errorMessage]);
|
|
2492
|
+
|
|
2493
|
+
const toolMsgId = state.toolIdToMessageId.get('tool-1');
|
|
2494
|
+
let toolMsg = state.messages.get(toolMsgId!);
|
|
2495
|
+
expect(toolMsg?.tool?.state).toBe('error');
|
|
2496
|
+
expect(toolMsg?.tool?.result).toBe('File not found');
|
|
2497
|
+
|
|
2498
|
+
// Step 2: AgentState with approved permission (too late to help)
|
|
2499
|
+
const agentState: AgentState = {
|
|
2500
|
+
completedRequests: {
|
|
2501
|
+
'tool-1': {
|
|
2502
|
+
tool: 'Bash',
|
|
2503
|
+
arguments: { command: 'cat /nonexistent' },
|
|
2504
|
+
createdAt: 1000,
|
|
2505
|
+
completedAt: 1400,
|
|
2506
|
+
status: 'approved'
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
};
|
|
2510
|
+
|
|
2511
|
+
reducer(state, [], agentState);
|
|
2512
|
+
|
|
2513
|
+
// Permission attached but error state maintained
|
|
2514
|
+
toolMsg = state.messages.get(toolMsgId!);
|
|
2515
|
+
expect(toolMsg?.tool?.permission?.status).toBe('approved');
|
|
2516
|
+
expect(toolMsg?.tool?.state).toBe('error'); // Should stay error
|
|
2517
|
+
expect(toolMsg?.tool?.result).toBe('File not found'); // Error unchanged
|
|
2518
|
+
});
|
|
2519
|
+
|
|
2520
|
+
it('should handle finished tool: errored, then AgentState with denied permission', () => {
|
|
2521
|
+
const state = createReducer();
|
|
2522
|
+
|
|
2523
|
+
// Step 1: Load tool that errored
|
|
2524
|
+
const toolMessage: NormalizedMessage = {
|
|
2525
|
+
id: 'msg-1',
|
|
2526
|
+
localId: null,
|
|
2527
|
+
createdAt: 1500,
|
|
2528
|
+
role: 'agent',
|
|
2529
|
+
isSidechain: false,
|
|
2530
|
+
content: [{
|
|
2531
|
+
type: 'tool-call',
|
|
2532
|
+
id: 'tool-1',
|
|
2533
|
+
name: 'Bash',
|
|
2534
|
+
input: { command: 'sudo rm -rf /' },
|
|
2535
|
+
description: null,
|
|
2536
|
+
uuid: 'tool-uuid-1',
|
|
2537
|
+
parentUUID: null
|
|
2538
|
+
}]
|
|
2539
|
+
};
|
|
2540
|
+
|
|
2541
|
+
const errorMessage: NormalizedMessage = {
|
|
2542
|
+
id: 'msg-2',
|
|
2543
|
+
localId: null,
|
|
2544
|
+
createdAt: 2000,
|
|
2545
|
+
role: 'agent',
|
|
2546
|
+
isSidechain: false,
|
|
2547
|
+
content: [{
|
|
2548
|
+
type: 'tool-result',
|
|
2549
|
+
tool_use_id: 'tool-1',
|
|
2550
|
+
content: 'Permission denied',
|
|
2551
|
+
is_error: true,
|
|
2552
|
+
uuid: 'tool-uuid-2',
|
|
2553
|
+
parentUUID: null
|
|
2554
|
+
}]
|
|
2555
|
+
};
|
|
2556
|
+
|
|
2557
|
+
reducer(state, [toolMessage, errorMessage]);
|
|
2558
|
+
|
|
2559
|
+
// Step 2: AgentState with denied permission
|
|
2560
|
+
const agentState: AgentState = {
|
|
2561
|
+
completedRequests: {
|
|
2562
|
+
'tool-1': {
|
|
2563
|
+
tool: 'Bash',
|
|
2564
|
+
arguments: { command: 'sudo rm -rf /' },
|
|
2565
|
+
createdAt: 1000,
|
|
2566
|
+
completedAt: 1400,
|
|
2567
|
+
status: 'denied',
|
|
2568
|
+
reason: 'Extremely dangerous'
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
};
|
|
2572
|
+
|
|
2573
|
+
reducer(state, [], agentState);
|
|
2574
|
+
|
|
2575
|
+
// Both permission and error should be present
|
|
2576
|
+
const toolMsgId = state.toolIdToMessageId.get('tool-1');
|
|
2577
|
+
const toolMsg = state.messages.get(toolMsgId!);
|
|
2578
|
+
expect(toolMsg?.tool?.permission?.status).toBe('denied');
|
|
2579
|
+
expect(toolMsg?.tool?.permission?.reason).toBe('Extremely dangerous');
|
|
2580
|
+
expect(toolMsg?.tool?.state).toBe('error');
|
|
2581
|
+
expect(toolMsg?.tool?.result).toBe('Permission denied'); // Original error
|
|
2582
|
+
});
|
|
2583
|
+
|
|
2584
|
+
it('should handle finished tool: with multiple messages in sequence', () => {
|
|
2585
|
+
const state = createReducer();
|
|
2586
|
+
|
|
2587
|
+
// Step 1: Tool call
|
|
2588
|
+
const toolMessage: NormalizedMessage = {
|
|
2589
|
+
id: 'msg-1',
|
|
2590
|
+
localId: null,
|
|
2591
|
+
createdAt: 1500,
|
|
2592
|
+
role: 'agent',
|
|
2593
|
+
isSidechain: false,
|
|
2594
|
+
content: [{
|
|
2595
|
+
type: 'tool-call',
|
|
2596
|
+
id: 'tool-1',
|
|
2597
|
+
name: 'Bash',
|
|
2598
|
+
input: { command: 'ls -la' },
|
|
2599
|
+
description: null,
|
|
2600
|
+
uuid: 'tool-uuid-1',
|
|
2601
|
+
parentUUID: null
|
|
2602
|
+
}]
|
|
2603
|
+
};
|
|
2604
|
+
|
|
2605
|
+
reducer(state, [toolMessage]);
|
|
2606
|
+
|
|
2607
|
+
// Step 2: Tool result arrives
|
|
2608
|
+
const resultMessage: NormalizedMessage = {
|
|
2609
|
+
id: 'msg-2',
|
|
2610
|
+
localId: null,
|
|
2611
|
+
createdAt: 2000,
|
|
2612
|
+
role: 'agent',
|
|
2613
|
+
isSidechain: false,
|
|
2614
|
+
content: [{
|
|
2615
|
+
type: 'tool-result',
|
|
2616
|
+
tool_use_id: 'tool-1',
|
|
2617
|
+
content: 'file1.txt\nfile2.txt',
|
|
2618
|
+
is_error: false,
|
|
2619
|
+
uuid: 'tool-uuid-2',
|
|
2620
|
+
parentUUID: null
|
|
2621
|
+
}]
|
|
2622
|
+
};
|
|
2623
|
+
|
|
2624
|
+
reducer(state, [resultMessage]);
|
|
2625
|
+
|
|
2626
|
+
const toolMsgId = state.toolIdToMessageId.get('tool-1');
|
|
2627
|
+
let toolMsg = state.messages.get(toolMsgId!);
|
|
2628
|
+
expect(toolMsg?.tool?.state).toBe('completed');
|
|
2629
|
+
expect(toolMsg?.tool?.result).toBe('file1.txt\nfile2.txt');
|
|
2630
|
+
|
|
2631
|
+
// Step 3: AgentState arrives later with permission info
|
|
2632
|
+
const agentState: AgentState = {
|
|
2633
|
+
requests: {
|
|
2634
|
+
'tool-1': {
|
|
2635
|
+
tool: 'Bash',
|
|
2636
|
+
arguments: { command: 'ls -la' },
|
|
2637
|
+
createdAt: 1000
|
|
2638
|
+
}
|
|
2639
|
+
},
|
|
2640
|
+
completedRequests: {
|
|
2641
|
+
'tool-1': {
|
|
2642
|
+
tool: 'Bash',
|
|
2643
|
+
arguments: { command: 'ls -la' },
|
|
2644
|
+
createdAt: 1000,
|
|
2645
|
+
completedAt: 1400,
|
|
2646
|
+
status: 'approved'
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
};
|
|
2650
|
+
|
|
2651
|
+
const result = reducer(state, [], agentState);
|
|
2652
|
+
|
|
2653
|
+
// Permission should be attached to completed tool
|
|
2654
|
+
expect(result.messages).toHaveLength(1);
|
|
2655
|
+
toolMsg = state.messages.get(toolMsgId!);
|
|
2656
|
+
expect(toolMsg?.tool?.permission?.status).toBe('approved');
|
|
2657
|
+
expect(toolMsg?.tool?.state).toBe('completed');
|
|
2658
|
+
expect(toolMsg?.tool?.result).toBe('file1.txt\nfile2.txt');
|
|
2659
|
+
});
|
|
2660
|
+
|
|
2661
|
+
it('should handle real-world scenario: messages and AgentState received simultaneously', () => {
|
|
2662
|
+
const state = createReducer();
|
|
2663
|
+
|
|
2664
|
+
// Simulate a tool call message from the agent
|
|
2665
|
+
const toolMessage: NormalizedMessage = {
|
|
2666
|
+
id: 'msg-1',
|
|
2667
|
+
localId: null,
|
|
2668
|
+
createdAt: 1000,
|
|
2669
|
+
role: 'agent',
|
|
2670
|
+
content: [{
|
|
2671
|
+
type: 'tool-call',
|
|
2672
|
+
id: 'tool-1',
|
|
2673
|
+
name: 'Bash',
|
|
2674
|
+
input: { command: 'ls -la' },
|
|
2675
|
+
description: null,
|
|
2676
|
+
uuid: 'tool-uuid-1',
|
|
2677
|
+
parentUUID: null
|
|
2678
|
+
}],
|
|
2679
|
+
isSidechain: false
|
|
2680
|
+
};
|
|
2681
|
+
|
|
2682
|
+
// AgentState with the pending permission for the same tool
|
|
2683
|
+
const agentState: AgentState = {
|
|
2684
|
+
requests: {
|
|
2685
|
+
'tool-1': {
|
|
2686
|
+
tool: 'Bash',
|
|
2687
|
+
arguments: { command: 'ls -la' },
|
|
2688
|
+
createdAt: 900 // Permission requested before the tool call
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
};
|
|
2692
|
+
|
|
2693
|
+
// Process both simultaneously (as would happen when loading from storage)
|
|
2694
|
+
const messages = [toolMessage];
|
|
2695
|
+
const result = reducer(state, messages, agentState);
|
|
2696
|
+
|
|
2697
|
+
// Should create exactly ONE message, not two
|
|
2698
|
+
expect(result.messages).toHaveLength(1);
|
|
2699
|
+
expect(state.messages.size).toBe(1);
|
|
2700
|
+
|
|
2701
|
+
// The message should be the tool call with the permission attached
|
|
2702
|
+
const messageId = state.toolIdToMessageId.get('tool-1');
|
|
2703
|
+
expect(messageId).toBeDefined();
|
|
2704
|
+
|
|
2705
|
+
const message = state.messages.get(messageId!);
|
|
2706
|
+
expect(message?.tool?.name).toBe('Bash');
|
|
2707
|
+
expect(message?.tool?.permission?.status).toBe('pending');
|
|
2708
|
+
expect(message?.tool?.permission?.id).toBe('tool-1');
|
|
2709
|
+
});
|
|
2710
|
+
|
|
2711
|
+
it('should retroactively match permissions when tools are processed without AgentState initially', () => {
|
|
2712
|
+
const state = createReducer();
|
|
2713
|
+
|
|
2714
|
+
// Step 1: Process tool messages WITHOUT AgentState (simulating messages loading before session)
|
|
2715
|
+
const toolMessage: NormalizedMessage = {
|
|
2716
|
+
id: 'msg-1',
|
|
2717
|
+
localId: null,
|
|
2718
|
+
createdAt: 2000,
|
|
2719
|
+
role: 'agent',
|
|
2720
|
+
isSidechain: false,
|
|
2721
|
+
content: [{
|
|
2722
|
+
type: 'tool-call',
|
|
2723
|
+
id: 'tool-1',
|
|
2724
|
+
name: 'Bash',
|
|
2725
|
+
input: { command: 'echo hello' },
|
|
2726
|
+
description: null,
|
|
2727
|
+
uuid: 'tool-uuid-1',
|
|
2728
|
+
parentUUID: null
|
|
2729
|
+
}]
|
|
2730
|
+
};
|
|
2731
|
+
|
|
2732
|
+
// Process WITHOUT AgentState (undefined)
|
|
2733
|
+
const result1 = reducer(state, [toolMessage], undefined);
|
|
2734
|
+
|
|
2735
|
+
// Should create a tool message WITHOUT permission
|
|
2736
|
+
expect(result1.messages).toHaveLength(1);
|
|
2737
|
+
expect(result1.messages[0].kind).toBe('tool-call');
|
|
2738
|
+
if (result1.messages[0].kind === 'tool-call') {
|
|
2739
|
+
expect(result1.messages[0].tool.permission).toBeUndefined();
|
|
2740
|
+
expect(result1.messages[0].tool.state).toBe('running');
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
// Verify tool is registered in state
|
|
2744
|
+
expect(state.toolIdToMessageId.has('tool-1')).toBe(true);
|
|
2745
|
+
const toolMsgId = state.toolIdToMessageId.get('tool-1');
|
|
2746
|
+
|
|
2747
|
+
// Step 2: Later, AgentState arrives with permission for this tool
|
|
2748
|
+
const agentState: AgentState = {
|
|
2749
|
+
requests: {
|
|
2750
|
+
'tool-1': {
|
|
2751
|
+
tool: 'Bash',
|
|
2752
|
+
arguments: { command: 'echo hello' },
|
|
2753
|
+
createdAt: 1000 // Permission was requested BEFORE the tool ran
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
};
|
|
2757
|
+
|
|
2758
|
+
// Process with AgentState but no new messages
|
|
2759
|
+
const result2 = reducer(state, [], agentState);
|
|
2760
|
+
|
|
2761
|
+
// The reducer SHOULD match the permission to the existing tool
|
|
2762
|
+
expect(result2.messages).toHaveLength(1);
|
|
2763
|
+
expect(result2.messages[0].kind).toBe('tool-call');
|
|
2764
|
+
if (result2.messages[0].kind === 'tool-call') {
|
|
2765
|
+
// The existing tool should now have the permission attached
|
|
2766
|
+
expect(result2.messages[0].tool.permission?.status).toBe('pending');
|
|
2767
|
+
expect(result2.messages[0].tool.permission?.id).toBe('tool-1');
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// Should still only have ONE message - the tool was updated
|
|
2771
|
+
expect(state.messages.size).toBe(1);
|
|
2772
|
+
|
|
2773
|
+
// The original tool message should now have permission
|
|
2774
|
+
const originalTool = state.messages.get(toolMsgId!);
|
|
2775
|
+
expect(originalTool?.tool?.permission).toBeDefined();
|
|
2776
|
+
expect(originalTool?.tool?.permission?.status).toBe('pending');
|
|
2777
|
+
|
|
2778
|
+
// The permission should be linked to the existing tool
|
|
2779
|
+
const permMsgId = state.toolIdToMessageId.get('tool-1');
|
|
2780
|
+
expect(permMsgId).toBeDefined();
|
|
2781
|
+
expect(permMsgId).toBe(toolMsgId); // Same message ID
|
|
2782
|
+
});
|
|
2783
|
+
|
|
2784
|
+
it('should handle the full race condition scenario: messages load, then session with AgentState, then new message', () => {
|
|
2785
|
+
const state = createReducer();
|
|
2786
|
+
|
|
2787
|
+
// Step 1: Messages load WITHOUT AgentState (session hasn't arrived yet)
|
|
2788
|
+
const existingMessages: NormalizedMessage[] = [
|
|
2789
|
+
// User message
|
|
2790
|
+
{
|
|
2791
|
+
id: 'user-1',
|
|
2792
|
+
localId: 'local-1',
|
|
2793
|
+
createdAt: 1000,
|
|
2794
|
+
role: 'user',
|
|
2795
|
+
isSidechain: false,
|
|
2796
|
+
content: {
|
|
2797
|
+
type: 'text',
|
|
2798
|
+
text: 'Please list files'
|
|
2799
|
+
}
|
|
2800
|
+
},
|
|
2801
|
+
// Tool call that should have permission
|
|
2802
|
+
{
|
|
2803
|
+
id: 'tool-msg-1',
|
|
2804
|
+
localId: null,
|
|
2805
|
+
createdAt: 2000,
|
|
2806
|
+
role: 'agent',
|
|
2807
|
+
isSidechain: false,
|
|
2808
|
+
content: [{
|
|
2809
|
+
type: 'tool-call',
|
|
2810
|
+
id: 'tool-1',
|
|
2811
|
+
name: 'Bash',
|
|
2812
|
+
input: { command: 'ls -la' },
|
|
2813
|
+
description: 'List files',
|
|
2814
|
+
uuid: 'tool-uuid-1',
|
|
2815
|
+
parentUUID: null
|
|
2816
|
+
}]
|
|
2817
|
+
},
|
|
2818
|
+
// Tool result
|
|
2819
|
+
{
|
|
2820
|
+
id: 'result-1',
|
|
2821
|
+
localId: null,
|
|
2822
|
+
createdAt: 3000,
|
|
2823
|
+
role: 'agent',
|
|
2824
|
+
isSidechain: false,
|
|
2825
|
+
content: [{
|
|
2826
|
+
type: 'tool-result',
|
|
2827
|
+
tool_use_id: 'tool-1',
|
|
2828
|
+
content: 'file1.txt\nfile2.txt',
|
|
2829
|
+
is_error: false,
|
|
2830
|
+
uuid: 'result-uuid-1',
|
|
2831
|
+
parentUUID: null
|
|
2832
|
+
}]
|
|
2833
|
+
}
|
|
2834
|
+
];
|
|
2835
|
+
|
|
2836
|
+
// Process messages WITHOUT AgentState
|
|
2837
|
+
const result1 = reducer(state, existingMessages, undefined);
|
|
2838
|
+
|
|
2839
|
+
// Should create user message and tool message
|
|
2840
|
+
expect(result1.messages.length).toBeGreaterThanOrEqual(2);
|
|
2841
|
+
|
|
2842
|
+
// Find the tool message
|
|
2843
|
+
const toolMsg = result1.messages.find(m => m.kind === 'tool-call');
|
|
2844
|
+
expect(toolMsg).toBeDefined();
|
|
2845
|
+
if (toolMsg?.kind === 'tool-call') {
|
|
2846
|
+
expect(toolMsg.tool.permission).toBeUndefined(); // No permission yet
|
|
2847
|
+
expect(toolMsg.tool.state).toBe('completed'); // Tool completed
|
|
2848
|
+
expect(toolMsg.tool.result).toBe('file1.txt\nfile2.txt');
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
// Step 2: Session arrives with AgentState containing permission info
|
|
2852
|
+
const agentState: AgentState = {
|
|
2853
|
+
completedRequests: {
|
|
2854
|
+
'tool-1': {
|
|
2855
|
+
tool: 'Bash',
|
|
2856
|
+
arguments: { command: 'ls -la' },
|
|
2857
|
+
createdAt: 1500,
|
|
2858
|
+
completedAt: 1800,
|
|
2859
|
+
status: 'approved'
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
};
|
|
2863
|
+
|
|
2864
|
+
// Process AgentState (simulating session arrival)
|
|
2865
|
+
const result2 = reducer(state, [], agentState);
|
|
2866
|
+
|
|
2867
|
+
// Should update the existing tool with permission info
|
|
2868
|
+
expect(result2.messages).toHaveLength(1);
|
|
2869
|
+
if (result2.messages[0].kind === 'tool-call') {
|
|
2870
|
+
expect(result2.messages[0].tool.permission?.status).toBe('approved');
|
|
2871
|
+
// The tool should still be completed
|
|
2872
|
+
expect(result2.messages[0].tool.state).toBe('completed');
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
// Step 3: User sends a new message, triggering a new reducer call
|
|
2876
|
+
const newUserMessage: NormalizedMessage = {
|
|
2877
|
+
id: 'user-2',
|
|
2878
|
+
localId: 'local-2',
|
|
2879
|
+
createdAt: 4000,
|
|
2880
|
+
role: 'user',
|
|
2881
|
+
isSidechain: false,
|
|
2882
|
+
content: {
|
|
2883
|
+
type: 'text',
|
|
2884
|
+
text: 'Thanks!'
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
|
|
2888
|
+
// Process new message WITH AgentState (as would happen in real app)
|
|
2889
|
+
const result3 = reducer(state, [newUserMessage], agentState);
|
|
2890
|
+
|
|
2891
|
+
// Should only create the new user message
|
|
2892
|
+
expect(result3.messages).toHaveLength(1);
|
|
2893
|
+
expect(result3.messages[0].kind).toBe('user-text');
|
|
2894
|
+
|
|
2895
|
+
// The tool and permission should be the SAME message (matched correctly)
|
|
2896
|
+
expect(state.toolIdToMessageId.has('tool-1')).toBe(true);
|
|
2897
|
+
expect(state.toolIdToMessageId.has('tool-1')).toBe(true);
|
|
2898
|
+
|
|
2899
|
+
const toolMsgId = state.toolIdToMessageId.get('tool-1');
|
|
2900
|
+
const permMsgId = state.toolIdToMessageId.get('tool-1');
|
|
2901
|
+
expect(toolMsgId).toBe(permMsgId); // Same message - properly matched!
|
|
2902
|
+
});
|
|
2903
|
+
});
|
|
2904
|
+
|
|
2905
|
+
describe('session protocol lifecycle and subagent sidechains', () => {
|
|
2906
|
+
it('sets hasReadyEvent for ready events without creating visible messages', () => {
|
|
2907
|
+
const state = createReducer();
|
|
2908
|
+
const result = reducer(state, [{
|
|
2909
|
+
id: 'ready-1',
|
|
2910
|
+
localId: null,
|
|
2911
|
+
createdAt: 1000,
|
|
2912
|
+
role: 'event',
|
|
2913
|
+
content: { type: 'ready' },
|
|
2914
|
+
isSidechain: false
|
|
2915
|
+
}]);
|
|
2916
|
+
|
|
2917
|
+
expect(result.messages).toHaveLength(0);
|
|
2918
|
+
expect(result.hasReadyEvent).toBe(true);
|
|
2919
|
+
});
|
|
2920
|
+
|
|
2921
|
+
it('hides turn-start lifecycle messages', () => {
|
|
2922
|
+
const state = createReducer();
|
|
2923
|
+
const result = reducer(state, [{
|
|
2924
|
+
id: 'turn-start-1',
|
|
2925
|
+
localId: null,
|
|
2926
|
+
createdAt: 1000,
|
|
2927
|
+
role: 'event',
|
|
2928
|
+
content: { type: 'message', message: 'Turn started' },
|
|
2929
|
+
isSidechain: false
|
|
2930
|
+
}]);
|
|
2931
|
+
|
|
2932
|
+
expect(result.messages).toHaveLength(0);
|
|
2933
|
+
});
|
|
2934
|
+
|
|
2935
|
+
it('nests subagent-linked sidechain messages under parent tool calls', () => {
|
|
2936
|
+
const state = createReducer();
|
|
2937
|
+
const result = reducer(state, [
|
|
2938
|
+
{
|
|
2939
|
+
id: 'parent-msg',
|
|
2940
|
+
localId: null,
|
|
2941
|
+
createdAt: 1000,
|
|
2942
|
+
role: 'agent',
|
|
2943
|
+
isSidechain: false,
|
|
2944
|
+
content: [{
|
|
2945
|
+
type: 'tool-call',
|
|
2946
|
+
id: 'tool-parent',
|
|
2947
|
+
name: 'Task',
|
|
2948
|
+
input: { prompt: 'Inspect auth flow' },
|
|
2949
|
+
description: null,
|
|
2950
|
+
uuid: 'parent-uuid',
|
|
2951
|
+
parentUUID: null
|
|
2952
|
+
}]
|
|
2953
|
+
},
|
|
2954
|
+
{
|
|
2955
|
+
id: 'child-msg',
|
|
2956
|
+
localId: null,
|
|
2957
|
+
createdAt: 1100,
|
|
2958
|
+
role: 'agent',
|
|
2959
|
+
isSidechain: true,
|
|
2960
|
+
content: [{
|
|
2961
|
+
type: 'text',
|
|
2962
|
+
text: 'Subagent output',
|
|
2963
|
+
uuid: 'child-uuid',
|
|
2964
|
+
parentUUID: 'tool-parent'
|
|
2965
|
+
}]
|
|
2966
|
+
}
|
|
2967
|
+
]);
|
|
2968
|
+
|
|
2969
|
+
expect(result.messages).toHaveLength(1);
|
|
2970
|
+
expect(result.messages[0].kind).toBe('tool-call');
|
|
2971
|
+
if (result.messages[0].kind === 'tool-call') {
|
|
2972
|
+
expect(result.messages[0].children).toHaveLength(1);
|
|
2973
|
+
expect(result.messages[0].children[0].kind).toBe('agent-text');
|
|
2974
|
+
if (result.messages[0].children[0].kind === 'agent-text') {
|
|
2975
|
+
expect(result.messages[0].children[0].text).toBe('Subagent output');
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
});
|
|
2979
|
+
|
|
2980
|
+
it('nests Agent sidechains via sessionSubagent and suppresses the duplicated prompt echo', () => {
|
|
2981
|
+
const state = createReducer();
|
|
2982
|
+
const result = reducer(state, [
|
|
2983
|
+
{
|
|
2984
|
+
id: 'agent-parent-msg',
|
|
2985
|
+
localId: null,
|
|
2986
|
+
createdAt: 1000,
|
|
2987
|
+
role: 'agent',
|
|
2988
|
+
isSidechain: false,
|
|
2989
|
+
content: [{
|
|
2990
|
+
type: 'tool-call',
|
|
2991
|
+
id: 'tool-agent-parent',
|
|
2992
|
+
name: 'Agent',
|
|
2993
|
+
input: {
|
|
2994
|
+
description: 'Add translations for switchMachinesHint',
|
|
2995
|
+
prompt: 'Add translations for switchMachinesHint',
|
|
2996
|
+
sessionSubagent: 'session-subagent-1',
|
|
2997
|
+
},
|
|
2998
|
+
description: 'Add translations for switchMachinesHint',
|
|
2999
|
+
uuid: 'agent-parent-uuid',
|
|
3000
|
+
parentUUID: null
|
|
3001
|
+
}]
|
|
3002
|
+
},
|
|
3003
|
+
{
|
|
3004
|
+
id: 'agent-prompt-echo',
|
|
3005
|
+
localId: null,
|
|
3006
|
+
createdAt: 1100,
|
|
3007
|
+
role: 'agent',
|
|
3008
|
+
isSidechain: true,
|
|
3009
|
+
content: [{
|
|
3010
|
+
type: 'text',
|
|
3011
|
+
text: 'Add translations for switchMachinesHint',
|
|
3012
|
+
uuid: 'agent-prompt-uuid',
|
|
3013
|
+
parentUUID: 'session-subagent-1'
|
|
3014
|
+
}]
|
|
3015
|
+
},
|
|
3016
|
+
{
|
|
3017
|
+
id: 'agent-child-tool',
|
|
3018
|
+
localId: null,
|
|
3019
|
+
createdAt: 1200,
|
|
3020
|
+
role: 'agent',
|
|
3021
|
+
isSidechain: true,
|
|
3022
|
+
content: [{
|
|
3023
|
+
type: 'tool-call',
|
|
3024
|
+
id: 'tool-read-child',
|
|
3025
|
+
name: 'Read',
|
|
3026
|
+
input: { file_path: '/tmp/example.ts' },
|
|
3027
|
+
description: null,
|
|
3028
|
+
uuid: 'agent-child-tool-uuid',
|
|
3029
|
+
parentUUID: 'session-subagent-1'
|
|
3030
|
+
}]
|
|
3031
|
+
}
|
|
3032
|
+
]);
|
|
3033
|
+
|
|
3034
|
+
expect(result.messages).toHaveLength(1);
|
|
3035
|
+
expect(result.messages[0].kind).toBe('tool-call');
|
|
3036
|
+
if (result.messages[0].kind === 'tool-call') {
|
|
3037
|
+
expect(result.messages[0].tool.name).toBe('Agent');
|
|
3038
|
+
expect(result.messages[0].children).toHaveLength(1);
|
|
3039
|
+
expect(result.messages[0].children[0].kind).toBe('tool-call');
|
|
3040
|
+
if (result.messages[0].children[0].kind === 'tool-call') {
|
|
3041
|
+
expect(result.messages[0].children[0].tool.name).toBe('Read');
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
});
|
|
3045
|
+
});
|
|
3046
|
+
|
|
3047
|
+
describe('TodoWrite latestTodos handling', () => {
|
|
3048
|
+
it('does not update todos from a running TodoWrite input', () => {
|
|
3049
|
+
const state = createReducer();
|
|
3050
|
+
const result = reducer(state, [{
|
|
3051
|
+
id: 'todo-call-only',
|
|
3052
|
+
localId: null,
|
|
3053
|
+
createdAt: 1000,
|
|
3054
|
+
role: 'agent',
|
|
3055
|
+
isSidechain: false,
|
|
3056
|
+
content: [{
|
|
3057
|
+
type: 'tool-call',
|
|
3058
|
+
id: 'tool-todos',
|
|
3059
|
+
name: 'TodoWrite',
|
|
3060
|
+
input: {
|
|
3061
|
+
todos: [{
|
|
3062
|
+
content: 'Do the thing',
|
|
3063
|
+
status: 'pending'
|
|
3064
|
+
}]
|
|
3065
|
+
},
|
|
3066
|
+
description: null,
|
|
3067
|
+
uuid: 'tool-uuid',
|
|
3068
|
+
parentUUID: null
|
|
3069
|
+
}]
|
|
3070
|
+
}]);
|
|
3071
|
+
|
|
3072
|
+
expect(result.todos).toBeUndefined();
|
|
3073
|
+
});
|
|
3074
|
+
|
|
3075
|
+
it('updates todos from successful TodoWrite result newTodos', () => {
|
|
3076
|
+
const state = createReducer();
|
|
3077
|
+
const result = reducer(state, [
|
|
3078
|
+
{
|
|
3079
|
+
id: 'todo-call',
|
|
3080
|
+
localId: null,
|
|
3081
|
+
createdAt: 1000,
|
|
3082
|
+
role: 'agent',
|
|
3083
|
+
isSidechain: false,
|
|
3084
|
+
content: [{
|
|
3085
|
+
type: 'tool-call',
|
|
3086
|
+
id: 'tool-success',
|
|
3087
|
+
name: 'TodoWrite',
|
|
3088
|
+
input: {
|
|
3089
|
+
todos: [{
|
|
3090
|
+
content: 'Old task state',
|
|
3091
|
+
status: 'pending'
|
|
3092
|
+
}]
|
|
3093
|
+
},
|
|
3094
|
+
description: null,
|
|
3095
|
+
uuid: 'tool-uuid-success',
|
|
3096
|
+
parentUUID: null
|
|
3097
|
+
}]
|
|
3098
|
+
},
|
|
3099
|
+
{
|
|
3100
|
+
id: 'todo-result',
|
|
3101
|
+
localId: null,
|
|
3102
|
+
createdAt: 1010,
|
|
3103
|
+
role: 'agent',
|
|
3104
|
+
isSidechain: false,
|
|
3105
|
+
content: [{
|
|
3106
|
+
type: 'tool-result',
|
|
3107
|
+
tool_use_id: 'tool-success',
|
|
3108
|
+
content: {
|
|
3109
|
+
oldTodos: [],
|
|
3110
|
+
newTodos: [{
|
|
3111
|
+
content: 'New authoritative task state',
|
|
3112
|
+
status: 'completed'
|
|
3113
|
+
}]
|
|
3114
|
+
},
|
|
3115
|
+
is_error: false,
|
|
3116
|
+
uuid: 'tool-uuid-success',
|
|
3117
|
+
parentUUID: null
|
|
3118
|
+
}]
|
|
3119
|
+
}
|
|
3120
|
+
]);
|
|
3121
|
+
|
|
3122
|
+
expect(result.todos).toEqual([{
|
|
3123
|
+
content: 'New authoritative task state',
|
|
3124
|
+
status: 'completed'
|
|
3125
|
+
}]);
|
|
3126
|
+
});
|
|
3127
|
+
|
|
3128
|
+
it('ignores malformed TodoWrite input that later fails validation', () => {
|
|
3129
|
+
const state = createReducer();
|
|
3130
|
+
const result = reducer(state, [
|
|
3131
|
+
{
|
|
3132
|
+
id: 'bad-todo-call',
|
|
3133
|
+
localId: null,
|
|
3134
|
+
createdAt: 1000,
|
|
3135
|
+
role: 'agent',
|
|
3136
|
+
isSidechain: false,
|
|
3137
|
+
content: [{
|
|
3138
|
+
type: 'tool-call',
|
|
3139
|
+
id: 'tool-bad',
|
|
3140
|
+
name: 'TodoWrite',
|
|
3141
|
+
input: {
|
|
3142
|
+
todos: '[{"content":"Broken","status":"pending"}]'
|
|
3143
|
+
},
|
|
3144
|
+
description: null,
|
|
3145
|
+
uuid: 'tool-uuid-bad',
|
|
3146
|
+
parentUUID: null
|
|
3147
|
+
}]
|
|
3148
|
+
},
|
|
3149
|
+
{
|
|
3150
|
+
id: 'bad-todo-result',
|
|
3151
|
+
localId: null,
|
|
3152
|
+
createdAt: 1010,
|
|
3153
|
+
role: 'agent',
|
|
3154
|
+
isSidechain: false,
|
|
3155
|
+
content: [{
|
|
3156
|
+
type: 'tool-result',
|
|
3157
|
+
tool_use_id: 'tool-bad',
|
|
3158
|
+
content: 'InputValidationError',
|
|
3159
|
+
is_error: true,
|
|
3160
|
+
uuid: 'tool-uuid-bad',
|
|
3161
|
+
parentUUID: null
|
|
3162
|
+
}]
|
|
3163
|
+
}
|
|
3164
|
+
]);
|
|
3165
|
+
|
|
3166
|
+
expect(result.todos).toBeUndefined();
|
|
3167
|
+
});
|
|
3168
|
+
});
|
|
3169
|
+
});
|