nastech-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1111) hide show
  1. package/.claude/agents/i18n-translator.md +119 -0
  2. package/.claude/settings.json +8 -0
  3. package/.eas/workflows/ota.yaml +9 -0
  4. package/.eas/workflows/preview.yaml +12 -0
  5. package/.easignore +12 -0
  6. package/.github/workflows/eas-build.yml +24 -0
  7. package/CHANGELOG.md +117 -0
  8. package/CLAUDE.md +413 -0
  9. package/GoogleService-Info.plist +30 -0
  10. package/LICENSE +21 -0
  11. package/NasTechapp.md +383 -0
  12. package/README.md +75 -0
  13. package/Stores.md +85 -0
  14. package/TERMS.md +83 -0
  15. package/app.config.js +153 -0
  16. package/babel.config.js +28 -0
  17. package/deploy/nastech-app.yaml +51 -0
  18. package/docs/marketing/README-creators.md +73 -0
  19. package/eas-build-post-install.sh +11 -0
  20. package/eas-build-pre-install.sh +27 -0
  21. package/eas.json +78 -0
  22. package/google-services.json +67 -0
  23. package/index.ts +3 -0
  24. package/logo.png +0 -0
  25. package/metro.config.js +54 -0
  26. package/nativewind-env.d.ts +1 -0
  27. package/package.json +233 -0
  28. package/patches/.keep +0 -0
  29. package/plugins/withEinkCompatibility.js +156 -0
  30. package/public/.well-known/apple-app-site-association +22 -0
  31. package/public/.well-known/assetlinks.json +12 -0
  32. package/public/canvaskit.wasm +0 -0
  33. package/public/favicon-active.ico +0 -0
  34. package/release-dev.sh +7 -0
  35. package/release-production.sh +3 -0
  36. package/release.cjs +160 -0
  37. package/sources/-session/SessionView.tsx +944 -0
  38. package/sources/-session/sessionOverlayNav.ts +34 -0
  39. package/sources/app/(app)/_layout.tsx +321 -0
  40. package/sources/app/(app)/artifacts/[id].tsx +279 -0
  41. package/sources/app/(app)/artifacts/edit/[id].tsx +318 -0
  42. package/sources/app/(app)/artifacts/index.tsx +264 -0
  43. package/sources/app/(app)/artifacts/new.tsx +219 -0
  44. package/sources/app/(app)/changelog.tsx +113 -0
  45. package/sources/app/(app)/dev/colors.tsx +197 -0
  46. package/sources/app/(app)/dev/device-info.tsx +183 -0
  47. package/sources/app/(app)/dev/expo-constants.tsx +394 -0
  48. package/sources/app/(app)/dev/index.tsx +400 -0
  49. package/sources/app/(app)/dev/input-styles.tsx +1951 -0
  50. package/sources/app/(app)/dev/inverted-list.tsx +295 -0
  51. package/sources/app/(app)/dev/list-demo.tsx +125 -0
  52. package/sources/app/(app)/dev/logs.tsx +160 -0
  53. package/sources/app/(app)/dev/messages-demo-data.ts +479 -0
  54. package/sources/app/(app)/dev/messages-demo.tsx +45 -0
  55. package/sources/app/(app)/dev/modal-demo.tsx +211 -0
  56. package/sources/app/(app)/dev/multi-text-input.tsx +224 -0
  57. package/sources/app/(app)/dev/purchases.tsx +228 -0
  58. package/sources/app/(app)/dev/qr-test.tsx +168 -0
  59. package/sources/app/(app)/dev/session-composer.tsx +812 -0
  60. package/sources/app/(app)/dev/shimmer-demo.tsx +275 -0
  61. package/sources/app/(app)/dev/tests.tsx +203 -0
  62. package/sources/app/(app)/dev/tools2.tsx +556 -0
  63. package/sources/app/(app)/dev/typography.tsx +177 -0
  64. package/sources/app/(app)/dev/unistyles-demo.tsx +376 -0
  65. package/sources/app/(app)/friends/index.tsx +167 -0
  66. package/sources/app/(app)/friends/search.tsx +232 -0
  67. package/sources/app/(app)/inbox/index.tsx +124 -0
  68. package/sources/app/(app)/index.tsx +264 -0
  69. package/sources/app/(app)/machine/[id].tsx +646 -0
  70. package/sources/app/(app)/new/index.tsx +1611 -0
  71. package/sources/app/(app)/restore/index.tsx +167 -0
  72. package/sources/app/(app)/restore/manual.tsx +138 -0
  73. package/sources/app/(app)/server.tsx +234 -0
  74. package/sources/app/(app)/session/[id]/file.tsx +527 -0
  75. package/sources/app/(app)/session/[id]/files.tsx +442 -0
  76. package/sources/app/(app)/session/[id]/info.tsx +655 -0
  77. package/sources/app/(app)/session/[id]/message/[messageId].tsx +125 -0
  78. package/sources/app/(app)/session/[id].tsx +10 -0
  79. package/sources/app/(app)/session/recent.tsx +270 -0
  80. package/sources/app/(app)/settings/account.tsx +600 -0
  81. package/sources/app/(app)/settings/agents.tsx +180 -0
  82. package/sources/app/(app)/settings/appearance.tsx +259 -0
  83. package/sources/app/(app)/settings/connect/claude.tsx +178 -0
  84. package/sources/app/(app)/settings/features.tsx +177 -0
  85. package/sources/app/(app)/settings/index.tsx +3 -0
  86. package/sources/app/(app)/settings/language.tsx +106 -0
  87. package/sources/app/(app)/settings/usage.tsx +11 -0
  88. package/sources/app/(app)/settings/voice/language.tsx +114 -0
  89. package/sources/app/(app)/settings/voice.tsx +274 -0
  90. package/sources/app/(app)/terminal/connect.tsx +241 -0
  91. package/sources/app/(app)/terminal/index.tsx +184 -0
  92. package/sources/app/(app)/text-selection.tsx +149 -0
  93. package/sources/app/(app)/user/[id].tsx +314 -0
  94. package/sources/app/+html.tsx +39 -0
  95. package/sources/app/_layout.tsx +402 -0
  96. package/sources/assets/animations/game.json +1 -0
  97. package/sources/assets/animations/owl.json +1 -0
  98. package/sources/assets/animations/popcorn.json +1 -0
  99. package/sources/assets/animations/robot.json +1 -0
  100. package/sources/assets/animations/sparkles.json +1 -0
  101. package/sources/assets/animations/stone.json +1 -0
  102. package/sources/assets/fonts/BricolageGrotesque-Bold.ttf +0 -0
  103. package/sources/assets/fonts/IBMPlexMono-Italic.ttf +0 -0
  104. package/sources/assets/fonts/IBMPlexMono-Regular.ttf +0 -0
  105. package/sources/assets/fonts/IBMPlexMono-SemiBold.ttf +0 -0
  106. package/sources/assets/fonts/IBMPlexSans-Italic.ttf +0 -0
  107. package/sources/assets/fonts/IBMPlexSans-Regular.ttf +0 -0
  108. package/sources/assets/fonts/IBMPlexSans-SemiBold.ttf +0 -0
  109. package/sources/assets/fonts/SpaceMono-Regular.ttf +0 -0
  110. package/sources/assets/images/brutalist/Abstract-1.png +0 -0
  111. package/sources/assets/images/brutalist/Abstract-10.png +0 -0
  112. package/sources/assets/images/brutalist/Abstract-100.png +0 -0
  113. package/sources/assets/images/brutalist/Abstract-101.png +0 -0
  114. package/sources/assets/images/brutalist/Abstract-102.png +0 -0
  115. package/sources/assets/images/brutalist/Abstract-103.png +0 -0
  116. package/sources/assets/images/brutalist/Abstract-104.png +0 -0
  117. package/sources/assets/images/brutalist/Abstract-105.png +0 -0
  118. package/sources/assets/images/brutalist/Abstract-106.png +0 -0
  119. package/sources/assets/images/brutalist/Abstract-107.png +0 -0
  120. package/sources/assets/images/brutalist/Abstract-108.png +0 -0
  121. package/sources/assets/images/brutalist/Abstract-109.png +0 -0
  122. package/sources/assets/images/brutalist/Abstract-11.png +0 -0
  123. package/sources/assets/images/brutalist/Abstract-110.png +0 -0
  124. package/sources/assets/images/brutalist/Abstract-111.png +0 -0
  125. package/sources/assets/images/brutalist/Abstract-112.png +0 -0
  126. package/sources/assets/images/brutalist/Abstract-113.png +0 -0
  127. package/sources/assets/images/brutalist/Abstract-114.png +0 -0
  128. package/sources/assets/images/brutalist/Abstract-115.png +0 -0
  129. package/sources/assets/images/brutalist/Abstract-116.png +0 -0
  130. package/sources/assets/images/brutalist/Abstract-117.png +0 -0
  131. package/sources/assets/images/brutalist/Abstract-118.png +0 -0
  132. package/sources/assets/images/brutalist/Abstract-119.png +0 -0
  133. package/sources/assets/images/brutalist/Abstract-12.png +0 -0
  134. package/sources/assets/images/brutalist/Abstract-120.png +0 -0
  135. package/sources/assets/images/brutalist/Abstract-121.png +0 -0
  136. package/sources/assets/images/brutalist/Abstract-122.png +0 -0
  137. package/sources/assets/images/brutalist/Abstract-123.png +0 -0
  138. package/sources/assets/images/brutalist/Abstract-124.png +0 -0
  139. package/sources/assets/images/brutalist/Abstract-125.png +0 -0
  140. package/sources/assets/images/brutalist/Abstract-126.png +0 -0
  141. package/sources/assets/images/brutalist/Abstract-127.png +0 -0
  142. package/sources/assets/images/brutalist/Abstract-128.png +0 -0
  143. package/sources/assets/images/brutalist/Abstract-129.png +0 -0
  144. package/sources/assets/images/brutalist/Abstract-13.png +0 -0
  145. package/sources/assets/images/brutalist/Abstract-130.png +0 -0
  146. package/sources/assets/images/brutalist/Abstract-131.png +0 -0
  147. package/sources/assets/images/brutalist/Abstract-132.png +0 -0
  148. package/sources/assets/images/brutalist/Abstract-133.png +0 -0
  149. package/sources/assets/images/brutalist/Abstract-134.png +0 -0
  150. package/sources/assets/images/brutalist/Abstract-135.png +0 -0
  151. package/sources/assets/images/brutalist/Abstract-136.png +0 -0
  152. package/sources/assets/images/brutalist/Abstract-137.png +0 -0
  153. package/sources/assets/images/brutalist/Abstract-138.png +0 -0
  154. package/sources/assets/images/brutalist/Abstract-139.png +0 -0
  155. package/sources/assets/images/brutalist/Abstract-14.png +0 -0
  156. package/sources/assets/images/brutalist/Abstract-140.png +0 -0
  157. package/sources/assets/images/brutalist/Abstract-141.png +0 -0
  158. package/sources/assets/images/brutalist/Abstract-142.png +0 -0
  159. package/sources/assets/images/brutalist/Abstract-143.png +0 -0
  160. package/sources/assets/images/brutalist/Abstract-144.png +0 -0
  161. package/sources/assets/images/brutalist/Abstract-145.png +0 -0
  162. package/sources/assets/images/brutalist/Abstract-146.png +0 -0
  163. package/sources/assets/images/brutalist/Abstract-147.png +0 -0
  164. package/sources/assets/images/brutalist/Abstract-148.png +0 -0
  165. package/sources/assets/images/brutalist/Abstract-149.png +0 -0
  166. package/sources/assets/images/brutalist/Abstract-15.png +0 -0
  167. package/sources/assets/images/brutalist/Abstract-150.png +0 -0
  168. package/sources/assets/images/brutalist/Abstract-151.png +0 -0
  169. package/sources/assets/images/brutalist/Abstract-152.png +0 -0
  170. package/sources/assets/images/brutalist/Abstract-153.png +0 -0
  171. package/sources/assets/images/brutalist/Abstract-154.png +0 -0
  172. package/sources/assets/images/brutalist/Abstract-155.png +0 -0
  173. package/sources/assets/images/brutalist/Abstract-156.png +0 -0
  174. package/sources/assets/images/brutalist/Abstract-157.png +0 -0
  175. package/sources/assets/images/brutalist/Abstract-158.png +0 -0
  176. package/sources/assets/images/brutalist/Abstract-159.png +0 -0
  177. package/sources/assets/images/brutalist/Abstract-16.png +0 -0
  178. package/sources/assets/images/brutalist/Abstract-160.png +0 -0
  179. package/sources/assets/images/brutalist/Abstract-161.png +0 -0
  180. package/sources/assets/images/brutalist/Abstract-162.png +0 -0
  181. package/sources/assets/images/brutalist/Abstract-163.png +0 -0
  182. package/sources/assets/images/brutalist/Abstract-164.png +0 -0
  183. package/sources/assets/images/brutalist/Abstract-165.png +0 -0
  184. package/sources/assets/images/brutalist/Abstract-166.png +0 -0
  185. package/sources/assets/images/brutalist/Abstract-167.png +0 -0
  186. package/sources/assets/images/brutalist/Abstract-168.png +0 -0
  187. package/sources/assets/images/brutalist/Abstract-169.png +0 -0
  188. package/sources/assets/images/brutalist/Abstract-17.png +0 -0
  189. package/sources/assets/images/brutalist/Abstract-170.png +0 -0
  190. package/sources/assets/images/brutalist/Abstract-171.png +0 -0
  191. package/sources/assets/images/brutalist/Abstract-172.png +0 -0
  192. package/sources/assets/images/brutalist/Abstract-173.png +0 -0
  193. package/sources/assets/images/brutalist/Abstract-174.png +0 -0
  194. package/sources/assets/images/brutalist/Abstract-175.png +0 -0
  195. package/sources/assets/images/brutalist/Abstract-176.png +0 -0
  196. package/sources/assets/images/brutalist/Abstract-177.png +0 -0
  197. package/sources/assets/images/brutalist/Abstract-178.png +0 -0
  198. package/sources/assets/images/brutalist/Abstract-179.png +0 -0
  199. package/sources/assets/images/brutalist/Abstract-18.png +0 -0
  200. package/sources/assets/images/brutalist/Abstract-180.png +0 -0
  201. package/sources/assets/images/brutalist/Abstract-181.png +0 -0
  202. package/sources/assets/images/brutalist/Abstract-182.png +0 -0
  203. package/sources/assets/images/brutalist/Abstract-183.png +0 -0
  204. package/sources/assets/images/brutalist/Abstract-184.png +0 -0
  205. package/sources/assets/images/brutalist/Abstract-185.png +0 -0
  206. package/sources/assets/images/brutalist/Abstract-186.png +0 -0
  207. package/sources/assets/images/brutalist/Abstract-187.png +0 -0
  208. package/sources/assets/images/brutalist/Abstract-188.png +0 -0
  209. package/sources/assets/images/brutalist/Abstract-189.png +0 -0
  210. package/sources/assets/images/brutalist/Abstract-19.png +0 -0
  211. package/sources/assets/images/brutalist/Abstract-190.png +0 -0
  212. package/sources/assets/images/brutalist/Abstract-191.png +0 -0
  213. package/sources/assets/images/brutalist/Abstract-192.png +0 -0
  214. package/sources/assets/images/brutalist/Abstract-193.png +0 -0
  215. package/sources/assets/images/brutalist/Abstract-194.png +0 -0
  216. package/sources/assets/images/brutalist/Abstract-195.png +0 -0
  217. package/sources/assets/images/brutalist/Abstract-196.png +0 -0
  218. package/sources/assets/images/brutalist/Abstract-197.png +0 -0
  219. package/sources/assets/images/brutalist/Abstract-198.png +0 -0
  220. package/sources/assets/images/brutalist/Abstract-199.png +0 -0
  221. package/sources/assets/images/brutalist/Abstract-2.png +0 -0
  222. package/sources/assets/images/brutalist/Abstract-20.png +0 -0
  223. package/sources/assets/images/brutalist/Abstract-200.png +0 -0
  224. package/sources/assets/images/brutalist/Abstract-201.png +0 -0
  225. package/sources/assets/images/brutalist/Abstract-202.png +0 -0
  226. package/sources/assets/images/brutalist/Abstract-203.png +0 -0
  227. package/sources/assets/images/brutalist/Abstract-204.png +0 -0
  228. package/sources/assets/images/brutalist/Abstract-205.png +0 -0
  229. package/sources/assets/images/brutalist/Abstract-206.png +0 -0
  230. package/sources/assets/images/brutalist/Abstract-207.png +0 -0
  231. package/sources/assets/images/brutalist/Abstract-208.png +0 -0
  232. package/sources/assets/images/brutalist/Abstract-209.png +0 -0
  233. package/sources/assets/images/brutalist/Abstract-21.png +0 -0
  234. package/sources/assets/images/brutalist/Abstract-210.png +0 -0
  235. package/sources/assets/images/brutalist/Abstract-211.png +0 -0
  236. package/sources/assets/images/brutalist/Abstract-212.png +0 -0
  237. package/sources/assets/images/brutalist/Abstract-213.png +0 -0
  238. package/sources/assets/images/brutalist/Abstract-214.png +0 -0
  239. package/sources/assets/images/brutalist/Abstract-215.png +0 -0
  240. package/sources/assets/images/brutalist/Abstract-216.png +0 -0
  241. package/sources/assets/images/brutalist/Abstract-217.png +0 -0
  242. package/sources/assets/images/brutalist/Abstract-218.png +0 -0
  243. package/sources/assets/images/brutalist/Abstract-219.png +0 -0
  244. package/sources/assets/images/brutalist/Abstract-22.png +0 -0
  245. package/sources/assets/images/brutalist/Abstract-220.png +0 -0
  246. package/sources/assets/images/brutalist/Abstract-221.png +0 -0
  247. package/sources/assets/images/brutalist/Abstract-222.png +0 -0
  248. package/sources/assets/images/brutalist/Abstract-223.png +0 -0
  249. package/sources/assets/images/brutalist/Abstract-224.png +0 -0
  250. package/sources/assets/images/brutalist/Abstract-225.png +0 -0
  251. package/sources/assets/images/brutalist/Abstract-226.png +0 -0
  252. package/sources/assets/images/brutalist/Abstract-227.png +0 -0
  253. package/sources/assets/images/brutalist/Abstract-228.png +0 -0
  254. package/sources/assets/images/brutalist/Abstract-229.png +0 -0
  255. package/sources/assets/images/brutalist/Abstract-23.png +0 -0
  256. package/sources/assets/images/brutalist/Abstract-230.png +0 -0
  257. package/sources/assets/images/brutalist/Abstract-231.png +0 -0
  258. package/sources/assets/images/brutalist/Abstract-232.png +0 -0
  259. package/sources/assets/images/brutalist/Abstract-233.png +0 -0
  260. package/sources/assets/images/brutalist/Abstract-234.png +0 -0
  261. package/sources/assets/images/brutalist/Abstract-235.png +0 -0
  262. package/sources/assets/images/brutalist/Abstract-236.png +0 -0
  263. package/sources/assets/images/brutalist/Abstract-237.png +0 -0
  264. package/sources/assets/images/brutalist/Abstract-238.png +0 -0
  265. package/sources/assets/images/brutalist/Abstract-239.png +0 -0
  266. package/sources/assets/images/brutalist/Abstract-24.png +0 -0
  267. package/sources/assets/images/brutalist/Abstract-240.png +0 -0
  268. package/sources/assets/images/brutalist/Abstract-241.png +0 -0
  269. package/sources/assets/images/brutalist/Abstract-242.png +0 -0
  270. package/sources/assets/images/brutalist/Abstract-243.png +0 -0
  271. package/sources/assets/images/brutalist/Abstract-244.png +0 -0
  272. package/sources/assets/images/brutalist/Abstract-245.png +0 -0
  273. package/sources/assets/images/brutalist/Abstract-246.png +0 -0
  274. package/sources/assets/images/brutalist/Abstract-247.png +0 -0
  275. package/sources/assets/images/brutalist/Abstract-248.png +0 -0
  276. package/sources/assets/images/brutalist/Abstract-249.png +0 -0
  277. package/sources/assets/images/brutalist/Abstract-25.png +0 -0
  278. package/sources/assets/images/brutalist/Abstract-250.png +0 -0
  279. package/sources/assets/images/brutalist/Abstract-251.png +0 -0
  280. package/sources/assets/images/brutalist/Abstract-252.png +0 -0
  281. package/sources/assets/images/brutalist/Abstract-253.png +0 -0
  282. package/sources/assets/images/brutalist/Abstract-254.png +0 -0
  283. package/sources/assets/images/brutalist/Abstract-255.png +0 -0
  284. package/sources/assets/images/brutalist/Abstract-256.png +0 -0
  285. package/sources/assets/images/brutalist/Abstract-257.png +0 -0
  286. package/sources/assets/images/brutalist/Abstract-258.png +0 -0
  287. package/sources/assets/images/brutalist/Abstract-259.png +0 -0
  288. package/sources/assets/images/brutalist/Abstract-26.png +0 -0
  289. package/sources/assets/images/brutalist/Abstract-260.png +0 -0
  290. package/sources/assets/images/brutalist/Abstract-261.png +0 -0
  291. package/sources/assets/images/brutalist/Abstract-262.png +0 -0
  292. package/sources/assets/images/brutalist/Abstract-27.png +0 -0
  293. package/sources/assets/images/brutalist/Abstract-28.png +0 -0
  294. package/sources/assets/images/brutalist/Abstract-29.png +0 -0
  295. package/sources/assets/images/brutalist/Abstract-3.png +0 -0
  296. package/sources/assets/images/brutalist/Abstract-30.png +0 -0
  297. package/sources/assets/images/brutalist/Abstract-31.png +0 -0
  298. package/sources/assets/images/brutalist/Abstract-32.png +0 -0
  299. package/sources/assets/images/brutalist/Abstract-33.png +0 -0
  300. package/sources/assets/images/brutalist/Abstract-34.png +0 -0
  301. package/sources/assets/images/brutalist/Abstract-35.png +0 -0
  302. package/sources/assets/images/brutalist/Abstract-36.png +0 -0
  303. package/sources/assets/images/brutalist/Abstract-37.png +0 -0
  304. package/sources/assets/images/brutalist/Abstract-38.png +0 -0
  305. package/sources/assets/images/brutalist/Abstract-39.png +0 -0
  306. package/sources/assets/images/brutalist/Abstract-4.png +0 -0
  307. package/sources/assets/images/brutalist/Abstract-40.png +0 -0
  308. package/sources/assets/images/brutalist/Abstract-41.png +0 -0
  309. package/sources/assets/images/brutalist/Abstract-42.png +0 -0
  310. package/sources/assets/images/brutalist/Abstract-43.png +0 -0
  311. package/sources/assets/images/brutalist/Abstract-44.png +0 -0
  312. package/sources/assets/images/brutalist/Abstract-45.png +0 -0
  313. package/sources/assets/images/brutalist/Abstract-46.png +0 -0
  314. package/sources/assets/images/brutalist/Abstract-47.png +0 -0
  315. package/sources/assets/images/brutalist/Abstract-48.png +0 -0
  316. package/sources/assets/images/brutalist/Abstract-49.png +0 -0
  317. package/sources/assets/images/brutalist/Abstract-5.png +0 -0
  318. package/sources/assets/images/brutalist/Abstract-50.png +0 -0
  319. package/sources/assets/images/brutalist/Abstract-51.png +0 -0
  320. package/sources/assets/images/brutalist/Abstract-52.png +0 -0
  321. package/sources/assets/images/brutalist/Abstract-53.png +0 -0
  322. package/sources/assets/images/brutalist/Abstract-54.png +0 -0
  323. package/sources/assets/images/brutalist/Abstract-55.png +0 -0
  324. package/sources/assets/images/brutalist/Abstract-56.png +0 -0
  325. package/sources/assets/images/brutalist/Abstract-57.png +0 -0
  326. package/sources/assets/images/brutalist/Abstract-58.png +0 -0
  327. package/sources/assets/images/brutalist/Abstract-59.png +0 -0
  328. package/sources/assets/images/brutalist/Abstract-6.png +0 -0
  329. package/sources/assets/images/brutalist/Abstract-60.png +0 -0
  330. package/sources/assets/images/brutalist/Abstract-61.png +0 -0
  331. package/sources/assets/images/brutalist/Abstract-62.png +0 -0
  332. package/sources/assets/images/brutalist/Abstract-63.png +0 -0
  333. package/sources/assets/images/brutalist/Abstract-64.png +0 -0
  334. package/sources/assets/images/brutalist/Abstract-65.png +0 -0
  335. package/sources/assets/images/brutalist/Abstract-66.png +0 -0
  336. package/sources/assets/images/brutalist/Abstract-67.png +0 -0
  337. package/sources/assets/images/brutalist/Abstract-68.png +0 -0
  338. package/sources/assets/images/brutalist/Abstract-69.png +0 -0
  339. package/sources/assets/images/brutalist/Abstract-7.png +0 -0
  340. package/sources/assets/images/brutalist/Abstract-70.png +0 -0
  341. package/sources/assets/images/brutalist/Abstract-71.png +0 -0
  342. package/sources/assets/images/brutalist/Abstract-72.png +0 -0
  343. package/sources/assets/images/brutalist/Abstract-73.png +0 -0
  344. package/sources/assets/images/brutalist/Abstract-74.png +0 -0
  345. package/sources/assets/images/brutalist/Abstract-75.png +0 -0
  346. package/sources/assets/images/brutalist/Abstract-76.png +0 -0
  347. package/sources/assets/images/brutalist/Abstract-77.png +0 -0
  348. package/sources/assets/images/brutalist/Abstract-78.png +0 -0
  349. package/sources/assets/images/brutalist/Abstract-79.png +0 -0
  350. package/sources/assets/images/brutalist/Abstract-8.png +0 -0
  351. package/sources/assets/images/brutalist/Abstract-80.png +0 -0
  352. package/sources/assets/images/brutalist/Abstract-81.png +0 -0
  353. package/sources/assets/images/brutalist/Abstract-82.png +0 -0
  354. package/sources/assets/images/brutalist/Abstract-83.png +0 -0
  355. package/sources/assets/images/brutalist/Abstract-84.png +0 -0
  356. package/sources/assets/images/brutalist/Abstract-85.png +0 -0
  357. package/sources/assets/images/brutalist/Abstract-86.png +0 -0
  358. package/sources/assets/images/brutalist/Abstract-87.png +0 -0
  359. package/sources/assets/images/brutalist/Abstract-88.png +0 -0
  360. package/sources/assets/images/brutalist/Abstract-89.png +0 -0
  361. package/sources/assets/images/brutalist/Abstract-9.png +0 -0
  362. package/sources/assets/images/brutalist/Abstract-90.png +0 -0
  363. package/sources/assets/images/brutalist/Abstract-91.png +0 -0
  364. package/sources/assets/images/brutalist/Abstract-92.png +0 -0
  365. package/sources/assets/images/brutalist/Abstract-93.png +0 -0
  366. package/sources/assets/images/brutalist/Abstract-94.png +0 -0
  367. package/sources/assets/images/brutalist/Abstract-95.png +0 -0
  368. package/sources/assets/images/brutalist/Abstract-96.png +0 -0
  369. package/sources/assets/images/brutalist/Abstract-97.png +0 -0
  370. package/sources/assets/images/brutalist/Abstract-98.png +0 -0
  371. package/sources/assets/images/brutalist/Abstract-99.png +0 -0
  372. package/sources/assets/images/brutalist/Bauhaus-1.png +0 -0
  373. package/sources/assets/images/brutalist/Bauhaus-10.png +0 -0
  374. package/sources/assets/images/brutalist/Bauhaus-11.png +0 -0
  375. package/sources/assets/images/brutalist/Bauhaus-12.png +0 -0
  376. package/sources/assets/images/brutalist/Bauhaus-13.png +0 -0
  377. package/sources/assets/images/brutalist/Bauhaus-14.png +0 -0
  378. package/sources/assets/images/brutalist/Bauhaus-15.png +0 -0
  379. package/sources/assets/images/brutalist/Bauhaus-16.png +0 -0
  380. package/sources/assets/images/brutalist/Bauhaus-17.png +0 -0
  381. package/sources/assets/images/brutalist/Bauhaus-18.png +0 -0
  382. package/sources/assets/images/brutalist/Bauhaus-19.png +0 -0
  383. package/sources/assets/images/brutalist/Bauhaus-2.png +0 -0
  384. package/sources/assets/images/brutalist/Bauhaus-20.png +0 -0
  385. package/sources/assets/images/brutalist/Bauhaus-21.png +0 -0
  386. package/sources/assets/images/brutalist/Bauhaus-22.png +0 -0
  387. package/sources/assets/images/brutalist/Bauhaus-23.png +0 -0
  388. package/sources/assets/images/brutalist/Bauhaus-24.png +0 -0
  389. package/sources/assets/images/brutalist/Bauhaus-25.png +0 -0
  390. package/sources/assets/images/brutalist/Bauhaus-26.png +0 -0
  391. package/sources/assets/images/brutalist/Bauhaus-27.png +0 -0
  392. package/sources/assets/images/brutalist/Bauhaus-28.png +0 -0
  393. package/sources/assets/images/brutalist/Bauhaus-29.png +0 -0
  394. package/sources/assets/images/brutalist/Bauhaus-3.png +0 -0
  395. package/sources/assets/images/brutalist/Bauhaus-30.png +0 -0
  396. package/sources/assets/images/brutalist/Bauhaus-31.png +0 -0
  397. package/sources/assets/images/brutalist/Bauhaus-32.png +0 -0
  398. package/sources/assets/images/brutalist/Bauhaus-33.png +0 -0
  399. package/sources/assets/images/brutalist/Bauhaus-34.png +0 -0
  400. package/sources/assets/images/brutalist/Bauhaus-35.png +0 -0
  401. package/sources/assets/images/brutalist/Bauhaus-36.png +0 -0
  402. package/sources/assets/images/brutalist/Bauhaus-37.png +0 -0
  403. package/sources/assets/images/brutalist/Bauhaus-38.png +0 -0
  404. package/sources/assets/images/brutalist/Bauhaus-39.png +0 -0
  405. package/sources/assets/images/brutalist/Bauhaus-4.png +0 -0
  406. package/sources/assets/images/brutalist/Bauhaus-40.png +0 -0
  407. package/sources/assets/images/brutalist/Bauhaus-5.png +0 -0
  408. package/sources/assets/images/brutalist/Bauhaus-6.png +0 -0
  409. package/sources/assets/images/brutalist/Bauhaus-7.png +0 -0
  410. package/sources/assets/images/brutalist/Bauhaus-8.png +0 -0
  411. package/sources/assets/images/brutalist/Bauhaus-9.png +0 -0
  412. package/sources/assets/images/brutalist/Brutalism-1.png +0 -0
  413. package/sources/assets/images/brutalist/Brutalism-10.png +0 -0
  414. package/sources/assets/images/brutalist/Brutalism-100.png +0 -0
  415. package/sources/assets/images/brutalist/Brutalism-101.png +0 -0
  416. package/sources/assets/images/brutalist/Brutalism-102.png +0 -0
  417. package/sources/assets/images/brutalist/Brutalism-103.png +0 -0
  418. package/sources/assets/images/brutalist/Brutalism-104.png +0 -0
  419. package/sources/assets/images/brutalist/Brutalism-105.png +0 -0
  420. package/sources/assets/images/brutalist/Brutalism-106.png +0 -0
  421. package/sources/assets/images/brutalist/Brutalism-107.png +0 -0
  422. package/sources/assets/images/brutalist/Brutalism-108.png +0 -0
  423. package/sources/assets/images/brutalist/Brutalism-109.png +0 -0
  424. package/sources/assets/images/brutalist/Brutalism-11.png +0 -0
  425. package/sources/assets/images/brutalist/Brutalism-110.png +0 -0
  426. package/sources/assets/images/brutalist/Brutalism-111.png +0 -0
  427. package/sources/assets/images/brutalist/Brutalism-112.png +0 -0
  428. package/sources/assets/images/brutalist/Brutalism-113.png +0 -0
  429. package/sources/assets/images/brutalist/Brutalism-114.png +0 -0
  430. package/sources/assets/images/brutalist/Brutalism-115.png +0 -0
  431. package/sources/assets/images/brutalist/Brutalism-116.png +0 -0
  432. package/sources/assets/images/brutalist/Brutalism-117.png +0 -0
  433. package/sources/assets/images/brutalist/Brutalism-118.png +0 -0
  434. package/sources/assets/images/brutalist/Brutalism-12.png +0 -0
  435. package/sources/assets/images/brutalist/Brutalism-13.png +0 -0
  436. package/sources/assets/images/brutalist/Brutalism-14.png +0 -0
  437. package/sources/assets/images/brutalist/Brutalism-15.png +0 -0
  438. package/sources/assets/images/brutalist/Brutalism-16.png +0 -0
  439. package/sources/assets/images/brutalist/Brutalism-17.png +0 -0
  440. package/sources/assets/images/brutalist/Brutalism-18.png +0 -0
  441. package/sources/assets/images/brutalist/Brutalism-19.png +0 -0
  442. package/sources/assets/images/brutalist/Brutalism-2.png +0 -0
  443. package/sources/assets/images/brutalist/Brutalism-20.png +0 -0
  444. package/sources/assets/images/brutalist/Brutalism-21.png +0 -0
  445. package/sources/assets/images/brutalist/Brutalism-22.png +0 -0
  446. package/sources/assets/images/brutalist/Brutalism-23.png +0 -0
  447. package/sources/assets/images/brutalist/Brutalism-24.png +0 -0
  448. package/sources/assets/images/brutalist/Brutalism-25.png +0 -0
  449. package/sources/assets/images/brutalist/Brutalism-26.png +0 -0
  450. package/sources/assets/images/brutalist/Brutalism-27.png +0 -0
  451. package/sources/assets/images/brutalist/Brutalism-28.png +0 -0
  452. package/sources/assets/images/brutalist/Brutalism-29.png +0 -0
  453. package/sources/assets/images/brutalist/Brutalism-3.png +0 -0
  454. package/sources/assets/images/brutalist/Brutalism-30.png +0 -0
  455. package/sources/assets/images/brutalist/Brutalism-31.png +0 -0
  456. package/sources/assets/images/brutalist/Brutalism-32.png +0 -0
  457. package/sources/assets/images/brutalist/Brutalism-33.png +0 -0
  458. package/sources/assets/images/brutalist/Brutalism-34.png +0 -0
  459. package/sources/assets/images/brutalist/Brutalism-35.png +0 -0
  460. package/sources/assets/images/brutalist/Brutalism-36.png +0 -0
  461. package/sources/assets/images/brutalist/Brutalism-37.png +0 -0
  462. package/sources/assets/images/brutalist/Brutalism-38.png +0 -0
  463. package/sources/assets/images/brutalist/Brutalism-39.png +0 -0
  464. package/sources/assets/images/brutalist/Brutalism-4.png +0 -0
  465. package/sources/assets/images/brutalist/Brutalism-40.png +0 -0
  466. package/sources/assets/images/brutalist/Brutalism-41.png +0 -0
  467. package/sources/assets/images/brutalist/Brutalism-42.png +0 -0
  468. package/sources/assets/images/brutalist/Brutalism-43.png +0 -0
  469. package/sources/assets/images/brutalist/Brutalism-44.png +0 -0
  470. package/sources/assets/images/brutalist/Brutalism-45.png +0 -0
  471. package/sources/assets/images/brutalist/Brutalism-46.png +0 -0
  472. package/sources/assets/images/brutalist/Brutalism-47.png +0 -0
  473. package/sources/assets/images/brutalist/Brutalism-48.png +0 -0
  474. package/sources/assets/images/brutalist/Brutalism-49.png +0 -0
  475. package/sources/assets/images/brutalist/Brutalism-5.png +0 -0
  476. package/sources/assets/images/brutalist/Brutalism-50.png +0 -0
  477. package/sources/assets/images/brutalist/Brutalism-51.png +0 -0
  478. package/sources/assets/images/brutalist/Brutalism-52.png +0 -0
  479. package/sources/assets/images/brutalist/Brutalism-53.png +0 -0
  480. package/sources/assets/images/brutalist/Brutalism-54.png +0 -0
  481. package/sources/assets/images/brutalist/Brutalism-55.png +0 -0
  482. package/sources/assets/images/brutalist/Brutalism-56.png +0 -0
  483. package/sources/assets/images/brutalist/Brutalism-57.png +0 -0
  484. package/sources/assets/images/brutalist/Brutalism-58.png +0 -0
  485. package/sources/assets/images/brutalist/Brutalism-59.png +0 -0
  486. package/sources/assets/images/brutalist/Brutalism-6.png +0 -0
  487. package/sources/assets/images/brutalist/Brutalism-60.png +0 -0
  488. package/sources/assets/images/brutalist/Brutalism-61.png +0 -0
  489. package/sources/assets/images/brutalist/Brutalism-62.png +0 -0
  490. package/sources/assets/images/brutalist/Brutalism-63.png +0 -0
  491. package/sources/assets/images/brutalist/Brutalism-64.png +0 -0
  492. package/sources/assets/images/brutalist/Brutalism-65.png +0 -0
  493. package/sources/assets/images/brutalist/Brutalism-66.png +0 -0
  494. package/sources/assets/images/brutalist/Brutalism-67.png +0 -0
  495. package/sources/assets/images/brutalist/Brutalism-68.png +0 -0
  496. package/sources/assets/images/brutalist/Brutalism-69.png +0 -0
  497. package/sources/assets/images/brutalist/Brutalism-7.png +0 -0
  498. package/sources/assets/images/brutalist/Brutalism-70.png +0 -0
  499. package/sources/assets/images/brutalist/Brutalism-71.png +0 -0
  500. package/sources/assets/images/brutalist/Brutalism-72.png +0 -0
  501. package/sources/assets/images/brutalist/Brutalism-73.png +0 -0
  502. package/sources/assets/images/brutalist/Brutalism-74.png +0 -0
  503. package/sources/assets/images/brutalist/Brutalism-75.png +0 -0
  504. package/sources/assets/images/brutalist/Brutalism-76.png +0 -0
  505. package/sources/assets/images/brutalist/Brutalism-77.png +0 -0
  506. package/sources/assets/images/brutalist/Brutalism-78.png +0 -0
  507. package/sources/assets/images/brutalist/Brutalism-79.png +0 -0
  508. package/sources/assets/images/brutalist/Brutalism-8.png +0 -0
  509. package/sources/assets/images/brutalist/Brutalism-80.png +0 -0
  510. package/sources/assets/images/brutalist/Brutalism-81.png +0 -0
  511. package/sources/assets/images/brutalist/Brutalism-82.png +0 -0
  512. package/sources/assets/images/brutalist/Brutalism-83.png +0 -0
  513. package/sources/assets/images/brutalist/Brutalism-84.png +0 -0
  514. package/sources/assets/images/brutalist/Brutalism-85.png +0 -0
  515. package/sources/assets/images/brutalist/Brutalism-86.png +0 -0
  516. package/sources/assets/images/brutalist/Brutalism-87.png +0 -0
  517. package/sources/assets/images/brutalist/Brutalism-88.png +0 -0
  518. package/sources/assets/images/brutalist/Brutalism-89.png +0 -0
  519. package/sources/assets/images/brutalist/Brutalism-9.png +0 -0
  520. package/sources/assets/images/brutalist/Brutalism-90.png +0 -0
  521. package/sources/assets/images/brutalist/Brutalism-91.png +0 -0
  522. package/sources/assets/images/brutalist/Brutalism-92.png +0 -0
  523. package/sources/assets/images/brutalist/Brutalism-93.png +0 -0
  524. package/sources/assets/images/brutalist/Brutalism-94.png +0 -0
  525. package/sources/assets/images/brutalist/Brutalism-95.png +0 -0
  526. package/sources/assets/images/brutalist/Brutalism-96.png +0 -0
  527. package/sources/assets/images/brutalist/Brutalism-97.png +0 -0
  528. package/sources/assets/images/brutalist/Brutalism-98.png +0 -0
  529. package/sources/assets/images/brutalist/Brutalism-99.png +0 -0
  530. package/sources/assets/images/favicon-active.png +0 -0
  531. package/sources/assets/images/favicon.png +0 -0
  532. package/sources/assets/images/gradients/01.png +0 -0
  533. package/sources/assets/images/gradients/02.png +0 -0
  534. package/sources/assets/images/gradients/03.png +0 -0
  535. package/sources/assets/images/gradients/04.png +0 -0
  536. package/sources/assets/images/gradients/05.png +0 -0
  537. package/sources/assets/images/gradients/06.png +0 -0
  538. package/sources/assets/images/gradients/07.png +0 -0
  539. package/sources/assets/images/gradients/08.png +0 -0
  540. package/sources/assets/images/gradients/09.png +0 -0
  541. package/sources/assets/images/gradients/10.png +0 -0
  542. package/sources/assets/images/gradients/100.png +0 -0
  543. package/sources/assets/images/gradients/11.png +0 -0
  544. package/sources/assets/images/gradients/12.png +0 -0
  545. package/sources/assets/images/gradients/13.png +0 -0
  546. package/sources/assets/images/gradients/14.png +0 -0
  547. package/sources/assets/images/gradients/15.png +0 -0
  548. package/sources/assets/images/gradients/16.png +0 -0
  549. package/sources/assets/images/gradients/17.png +0 -0
  550. package/sources/assets/images/gradients/18.png +0 -0
  551. package/sources/assets/images/gradients/19.png +0 -0
  552. package/sources/assets/images/gradients/20.png +0 -0
  553. package/sources/assets/images/gradients/21.png +0 -0
  554. package/sources/assets/images/gradients/22.png +0 -0
  555. package/sources/assets/images/gradients/23.png +0 -0
  556. package/sources/assets/images/gradients/24.png +0 -0
  557. package/sources/assets/images/gradients/25.png +0 -0
  558. package/sources/assets/images/gradients/26.png +0 -0
  559. package/sources/assets/images/gradients/27.png +0 -0
  560. package/sources/assets/images/gradients/28.png +0 -0
  561. package/sources/assets/images/gradients/29.png +0 -0
  562. package/sources/assets/images/gradients/30.png +0 -0
  563. package/sources/assets/images/gradients/31.png +0 -0
  564. package/sources/assets/images/gradients/32.png +0 -0
  565. package/sources/assets/images/gradients/33.png +0 -0
  566. package/sources/assets/images/gradients/34.png +0 -0
  567. package/sources/assets/images/gradients/35.png +0 -0
  568. package/sources/assets/images/gradients/36.png +0 -0
  569. package/sources/assets/images/gradients/37.png +0 -0
  570. package/sources/assets/images/gradients/38.png +0 -0
  571. package/sources/assets/images/gradients/39.png +0 -0
  572. package/sources/assets/images/gradients/40.png +0 -0
  573. package/sources/assets/images/gradients/41.png +0 -0
  574. package/sources/assets/images/gradients/42.png +0 -0
  575. package/sources/assets/images/gradients/43.png +0 -0
  576. package/sources/assets/images/gradients/44.png +0 -0
  577. package/sources/assets/images/gradients/45.png +0 -0
  578. package/sources/assets/images/gradients/46.png +0 -0
  579. package/sources/assets/images/gradients/47.png +0 -0
  580. package/sources/assets/images/gradients/48.png +0 -0
  581. package/sources/assets/images/gradients/49.png +0 -0
  582. package/sources/assets/images/gradients/50.png +0 -0
  583. package/sources/assets/images/gradients/51.png +0 -0
  584. package/sources/assets/images/gradients/52.png +0 -0
  585. package/sources/assets/images/gradients/53.png +0 -0
  586. package/sources/assets/images/gradients/54.png +0 -0
  587. package/sources/assets/images/gradients/55.png +0 -0
  588. package/sources/assets/images/gradients/56.png +0 -0
  589. package/sources/assets/images/gradients/57.png +0 -0
  590. package/sources/assets/images/gradients/58.png +0 -0
  591. package/sources/assets/images/gradients/59.png +0 -0
  592. package/sources/assets/images/gradients/60.png +0 -0
  593. package/sources/assets/images/gradients/61.png +0 -0
  594. package/sources/assets/images/gradients/62.png +0 -0
  595. package/sources/assets/images/gradients/63.png +0 -0
  596. package/sources/assets/images/gradients/64.png +0 -0
  597. package/sources/assets/images/gradients/65.png +0 -0
  598. package/sources/assets/images/gradients/66.png +0 -0
  599. package/sources/assets/images/gradients/67.png +0 -0
  600. package/sources/assets/images/gradients/68.png +0 -0
  601. package/sources/assets/images/gradients/69.png +0 -0
  602. package/sources/assets/images/gradients/70.png +0 -0
  603. package/sources/assets/images/gradients/71.png +0 -0
  604. package/sources/assets/images/gradients/72.png +0 -0
  605. package/sources/assets/images/gradients/73.png +0 -0
  606. package/sources/assets/images/gradients/74.png +0 -0
  607. package/sources/assets/images/gradients/75.png +0 -0
  608. package/sources/assets/images/gradients/76.png +0 -0
  609. package/sources/assets/images/gradients/77.png +0 -0
  610. package/sources/assets/images/gradients/78.png +0 -0
  611. package/sources/assets/images/gradients/79.png +0 -0
  612. package/sources/assets/images/gradients/80.png +0 -0
  613. package/sources/assets/images/gradients/81.png +0 -0
  614. package/sources/assets/images/gradients/82.png +0 -0
  615. package/sources/assets/images/gradients/83.png +0 -0
  616. package/sources/assets/images/gradients/84.png +0 -0
  617. package/sources/assets/images/gradients/85.png +0 -0
  618. package/sources/assets/images/gradients/86.png +0 -0
  619. package/sources/assets/images/gradients/87.png +0 -0
  620. package/sources/assets/images/gradients/88.png +0 -0
  621. package/sources/assets/images/gradients/89.png +0 -0
  622. package/sources/assets/images/gradients/90.png +0 -0
  623. package/sources/assets/images/gradients/91.png +0 -0
  624. package/sources/assets/images/gradients/92.png +0 -0
  625. package/sources/assets/images/gradients/93.png +0 -0
  626. package/sources/assets/images/gradients/94.png +0 -0
  627. package/sources/assets/images/gradients/95.png +0 -0
  628. package/sources/assets/images/gradients/96.png +0 -0
  629. package/sources/assets/images/gradients/97.png +0 -0
  630. package/sources/assets/images/gradients/98.png +0 -0
  631. package/sources/assets/images/gradients/99.png +0 -0
  632. package/sources/assets/images/icon-adaptive.png +0 -0
  633. package/sources/assets/images/icon-claude.png +0 -0
  634. package/sources/assets/images/icon-claude@2x.png +0 -0
  635. package/sources/assets/images/icon-claude@3x.png +0 -0
  636. package/sources/assets/images/icon-gemini.png +0 -0
  637. package/sources/assets/images/icon-gemini@2x.png +0 -0
  638. package/sources/assets/images/icon-gemini@3x.png +0 -0
  639. package/sources/assets/images/icon-gpt.png +0 -0
  640. package/sources/assets/images/icon-gpt@2x.png +0 -0
  641. package/sources/assets/images/icon-gpt@3x.png +0 -0
  642. package/sources/assets/images/icon-monochrome.png +0 -0
  643. package/sources/assets/images/icon-notification.png +0 -0
  644. package/sources/assets/images/icon-openclaw.png +0 -0
  645. package/sources/assets/images/icon-openclaw@2x.png +0 -0
  646. package/sources/assets/images/icon-openclaw@3x.png +0 -0
  647. package/sources/assets/images/icon-tauri.png +0 -0
  648. package/sources/assets/images/icon-voice-white.png +0 -0
  649. package/sources/assets/images/icon-voice.png +0 -0
  650. package/sources/assets/images/icon-voice@2x.png +0 -0
  651. package/sources/assets/images/icon-voice@3x.png +0 -0
  652. package/sources/assets/images/icon.png +0 -0
  653. package/sources/assets/images/logo-black.png +0 -0
  654. package/sources/assets/images/logo-white.png +0 -0
  655. package/sources/assets/images/logotype-dark.png +0 -0
  656. package/sources/assets/images/logotype-dark@2x.png +0 -0
  657. package/sources/assets/images/logotype-dark@3x.png +0 -0
  658. package/sources/assets/images/logotype-light.png +0 -0
  659. package/sources/assets/images/logotype-light@2x.png +0 -0
  660. package/sources/assets/images/logotype-light@3x.png +0 -0
  661. package/sources/assets/images/logotype.png +0 -0
  662. package/sources/assets/images/logotype@2x.png +0 -0
  663. package/sources/assets/images/logotype@3x.png +0 -0
  664. package/sources/assets/images/splash-android-dark.png +0 -0
  665. package/sources/assets/images/splash-android-light.png +0 -0
  666. package/sources/assets/images/transparent.png +0 -0
  667. package/sources/assets/images/zen-icon.png +0 -0
  668. package/sources/auth/AuthContext.tsx +100 -0
  669. package/sources/auth/authAccountApprove.ts +2 -0
  670. package/sources/auth/authApprove.ts +2 -0
  671. package/sources/auth/authChallenge.ts +8 -0
  672. package/sources/auth/authGetToken.ts +4 -0
  673. package/sources/auth/authQRStart.ts +17 -0
  674. package/sources/auth/authQRWait.ts +13 -0
  675. package/sources/auth/secretKeyBackup.spec.ts +465 -0
  676. package/sources/auth/secretKeyBackup.ts +179 -0
  677. package/sources/auth/tokenStorage.ts +27 -0
  678. package/sources/changelog/changelog.json +60 -0
  679. package/sources/changelog/index.ts +3 -0
  680. package/sources/changelog/parser.ts +23 -0
  681. package/sources/changelog/storage.ts +17 -0
  682. package/sources/changelog/types.ts +10 -0
  683. package/sources/components/ActiveSessionsGroupCompact.tsx +567 -0
  684. package/sources/components/AgentContentView.ios.tsx +70 -0
  685. package/sources/components/AgentContentView.tsx +48 -0
  686. package/sources/components/AgentInput.tsx +1468 -0
  687. package/sources/components/AgentInputAttachmentStrip.tsx +122 -0
  688. package/sources/components/AgentInputAutocomplete.tsx +96 -0
  689. package/sources/components/AgentInputSuggestionView.tsx +106 -0
  690. package/sources/components/AllFilesDiffView.tsx +515 -0
  691. package/sources/components/Avatar.tsx +149 -0
  692. package/sources/components/AvatarBrutalist.tsx +501 -0
  693. package/sources/components/AvatarGradient.tsx +147 -0
  694. package/sources/components/AvatarSkia.tsx +111 -0
  695. package/sources/components/AvatarSkia.web.tsx +113 -0
  696. package/sources/components/ChatFooter.tsx +50 -0
  697. package/sources/components/ChatHeaderView.tsx +180 -0
  698. package/sources/components/ChatList.tsx +283 -0
  699. package/sources/components/CodeEditor.tsx +22 -0
  700. package/sources/components/CodeEditor.web.tsx +180 -0
  701. package/sources/components/CodeView.tsx +33 -0
  702. package/sources/components/CommandPalette/CommandPalette.tsx +72 -0
  703. package/sources/components/CommandPalette/CommandPaletteInput.tsx +65 -0
  704. package/sources/components/CommandPalette/CommandPaletteItem.tsx +141 -0
  705. package/sources/components/CommandPalette/CommandPaletteModal.tsx +148 -0
  706. package/sources/components/CommandPalette/CommandPaletteProvider.tsx +141 -0
  707. package/sources/components/CommandPalette/CommandPaletteResults.tsx +129 -0
  708. package/sources/components/CommandPalette/index.ts +3 -0
  709. package/sources/components/CommandPalette/types.ts +15 -0
  710. package/sources/components/CommandPalette/useCommandPalette.ts +107 -0
  711. package/sources/components/CommandView.tsx +135 -0
  712. package/sources/components/CompactGitStatus.tsx +88 -0
  713. package/sources/components/ConnectButton.tsx +117 -0
  714. package/sources/components/Deferred.tsx +18 -0
  715. package/sources/components/DuplicateSheet.tsx +295 -0
  716. package/sources/components/EmptyMainScreen.tsx +171 -0
  717. package/sources/components/EmptyMessages.tsx +123 -0
  718. package/sources/components/EmptySessionsTablet.tsx +111 -0
  719. package/sources/components/ExternalLink.tsx +22 -0
  720. package/sources/components/FAB.tsx +53 -0
  721. package/sources/components/FABWide.tsx +59 -0
  722. package/sources/components/FeedItemCard.tsx +98 -0
  723. package/sources/components/FileIcon.tsx +63 -0
  724. package/sources/components/FileViewPanel.tsx +673 -0
  725. package/sources/components/FilesSidebar.tsx +739 -0
  726. package/sources/components/FloatingOverlay.tsx +48 -0
  727. package/sources/components/GitStatusBadge.tsx +82 -0
  728. package/sources/components/HeaderLogo.tsx +28 -0
  729. package/sources/components/HomeHeader.tsx +243 -0
  730. package/sources/components/HorizontalScrollView.tsx +88 -0
  731. package/sources/components/InboxView.tsx +260 -0
  732. package/sources/components/InlineFileDiff.tsx +277 -0
  733. package/sources/components/Item.tsx +315 -0
  734. package/sources/components/ItemGroup.tsx +147 -0
  735. package/sources/components/ItemList.tsx +102 -0
  736. package/sources/components/MainView.tsx +324 -0
  737. package/sources/components/MessageView.tsx +299 -0
  738. package/sources/components/MultiTextInput.tsx +285 -0
  739. package/sources/components/MultiTextInput.web.tsx +220 -0
  740. package/sources/components/OAuthView.tsx +374 -0
  741. package/sources/components/PermissionModeSelector.tsx +68 -0
  742. package/sources/components/PlaceholderContainerView.tsx +47 -0
  743. package/sources/components/PlusPlus.tsx +34 -0
  744. package/sources/components/PlusPlus.web.tsx +34 -0
  745. package/sources/components/ProjectGitStatus.tsx +102 -0
  746. package/sources/components/RoundButton.tsx +130 -0
  747. package/sources/components/SearchableListSelector.tsx +675 -0
  748. package/sources/components/SessionActionsNativeMenu.android.tsx +58 -0
  749. package/sources/components/SessionActionsNativeMenu.ios.tsx +55 -0
  750. package/sources/components/SessionActionsNativeMenu.tsx +20 -0
  751. package/sources/components/SessionActionsNativeMenu.web.tsx +13 -0
  752. package/sources/components/SessionActionsPopover.tsx +240 -0
  753. package/sources/components/SessionsList.tsx +471 -0
  754. package/sources/components/SessionsListWrapper.tsx +72 -0
  755. package/sources/components/SettingsView.tsx +470 -0
  756. package/sources/components/SettingsViewWrapper.tsx +21 -0
  757. package/sources/components/Shaker.tsx +42 -0
  758. package/sources/components/Shaker.web.tsx +46 -0
  759. package/sources/components/ShimmerView.tsx +106 -0
  760. package/sources/components/SidebarNavigator.tsx +218 -0
  761. package/sources/components/SidebarView.tsx +104 -0
  762. package/sources/components/SimpleSyntaxHighlighter.tsx +322 -0
  763. package/sources/components/StatusBarProvider.tsx +12 -0
  764. package/sources/components/StatusDot.tsx +49 -0
  765. package/sources/components/StyledText.tsx +35 -0
  766. package/sources/components/Switch.tsx +20 -0
  767. package/sources/components/TabBar.tsx +140 -0
  768. package/sources/components/ToolGroupView.tsx +101 -0
  769. package/sources/components/TransitionStack.tsx +14 -0
  770. package/sources/components/UpdateBanner.tsx +74 -0
  771. package/sources/components/UserCard.tsx +41 -0
  772. package/sources/components/UserSearchResult.tsx +129 -0
  773. package/sources/components/VoiceAssistantStatusBar.tsx +260 -0
  774. package/sources/components/VoiceBars.tsx +95 -0
  775. package/sources/components/autocomplete/applySuggestion.test.ts +194 -0
  776. package/sources/components/autocomplete/applySuggestion.ts +61 -0
  777. package/sources/components/autocomplete/findActiveWord.test.ts +365 -0
  778. package/sources/components/autocomplete/findActiveWord.ts +207 -0
  779. package/sources/components/autocomplete/suggestions.ts +79 -0
  780. package/sources/components/autocomplete/useActiveSuggestions.ts +130 -0
  781. package/sources/components/autocomplete/useActiveWord.ts +19 -0
  782. package/sources/components/diff/DiffView.tsx +188 -0
  783. package/sources/components/diff/PierreDiffView.tsx +253 -0
  784. package/sources/components/diff/calculateDiff.ts +317 -0
  785. package/sources/components/entityColor.ts +51 -0
  786. package/sources/components/haptics.ts +9 -0
  787. package/sources/components/haptics.web.ts +7 -0
  788. package/sources/components/layout.ts +44 -0
  789. package/sources/components/markdown/MarkdownView.tsx +670 -0
  790. package/sources/components/markdown/MermaidRenderer.tsx +233 -0
  791. package/sources/components/markdown/linkUtils.test.ts +17 -0
  792. package/sources/components/markdown/linkUtils.ts +5 -0
  793. package/sources/components/markdown/parseMarkdown.test.ts +64 -0
  794. package/sources/components/markdown/parseMarkdown.ts +46 -0
  795. package/sources/components/markdown/parseMarkdownBlock.test.ts +108 -0
  796. package/sources/components/markdown/parseMarkdownBlock.ts +200 -0
  797. package/sources/components/markdown/parseMarkdownSpans.ts +88 -0
  798. package/sources/components/modelModeOptions.test.ts +114 -0
  799. package/sources/components/modelModeOptions.ts +257 -0
  800. package/sources/components/navigation/Header.tsx +252 -0
  801. package/sources/components/parseLocalCommandMessage.spec.ts +96 -0
  802. package/sources/components/parseLocalCommandMessage.ts +97 -0
  803. package/sources/components/qr/QRCode.tsx +178 -0
  804. package/sources/components/qr/QRCode.web.tsx +229 -0
  805. package/sources/components/qr/index.ts +2 -0
  806. package/sources/components/qr/qrMatrix.ts +48 -0
  807. package/sources/components/tools/PermissionFooter.tsx +527 -0
  808. package/sources/components/tools/ToolDiffView.tsx +60 -0
  809. package/sources/components/tools/ToolError.tsx +49 -0
  810. package/sources/components/tools/ToolFullView.tsx +193 -0
  811. package/sources/components/tools/ToolHeader.tsx +93 -0
  812. package/sources/components/tools/ToolSectionView.tsx +42 -0
  813. package/sources/components/tools/ToolStatusIndicator.tsx +36 -0
  814. package/sources/components/tools/ToolView.tsx +340 -0
  815. package/sources/components/tools/knownTools.tsx +957 -0
  816. package/sources/components/tools/views/AskUserQuestionView.tsx +357 -0
  817. package/sources/components/tools/views/BashView.tsx +47 -0
  818. package/sources/components/tools/views/BashViewFull.tsx +81 -0
  819. package/sources/components/tools/views/CodexBashView.tsx +126 -0
  820. package/sources/components/tools/views/CodexDiffView.tsx +79 -0
  821. package/sources/components/tools/views/CodexPatchView.tsx +186 -0
  822. package/sources/components/tools/views/EditView.tsx +33 -0
  823. package/sources/components/tools/views/EditViewFull.tsx +38 -0
  824. package/sources/components/tools/views/ExitPlanToolView.tsx +21 -0
  825. package/sources/components/tools/views/FileView.tsx +118 -0
  826. package/sources/components/tools/views/GeminiEditView.tsx +75 -0
  827. package/sources/components/tools/views/GeminiExecuteView.tsx +92 -0
  828. package/sources/components/tools/views/MCPToolView.tsx +31 -0
  829. package/sources/components/tools/views/MultiEditView.tsx +41 -0
  830. package/sources/components/tools/views/MultiEditViewFull.tsx +84 -0
  831. package/sources/components/tools/views/TaskView.tsx +129 -0
  832. package/sources/components/tools/views/TodoView.tsx +90 -0
  833. package/sources/components/tools/views/WriteView.tsx +29 -0
  834. package/sources/components/tools/views/_all.tsx +88 -0
  835. package/sources/components/usage/UsageBar.tsx +80 -0
  836. package/sources/components/usage/UsageChart.tsx +164 -0
  837. package/sources/components/usage/UsagePanel.tsx +283 -0
  838. package/sources/components/web/FaviconPermissionIndicator.tsx +44 -0
  839. package/sources/config.ts +3 -0
  840. package/sources/constants/Languages.ts +116 -0
  841. package/sources/constants/Typography.ts +116 -0
  842. package/sources/dev/testRunner.ts +277 -0
  843. package/sources/docs/autocomplete-text-manipulation.md +224 -0
  844. package/sources/encryption/aes.appspec.ts +25 -0
  845. package/sources/encryption/aes.ts +21 -0
  846. package/sources/encryption/aes.web.test.ts +73 -0
  847. package/sources/encryption/aes.web.ts +79 -0
  848. package/sources/encryption/base64.appspec.ts +240 -0
  849. package/sources/encryption/base64.native.ts +12 -0
  850. package/sources/encryption/base64.ts +46 -0
  851. package/sources/encryption/blob.test.ts +120 -0
  852. package/sources/encryption/blob.ts +58 -0
  853. package/sources/encryption/deriveKey.appspec.ts +72 -0
  854. package/sources/encryption/deriveKey.ts +46 -0
  855. package/sources/encryption/hex.ts +17 -0
  856. package/sources/encryption/hmac_sha512.appspec.ts +40 -0
  857. package/sources/encryption/hmac_sha512.ts +42 -0
  858. package/sources/encryption/libsodium.lib.ts +2 -0
  859. package/sources/encryption/libsodium.lib.web.ts +2 -0
  860. package/sources/encryption/libsodium.ts +58 -0
  861. package/sources/encryption/text.test.ts +61 -0
  862. package/sources/encryption/text.ts +11 -0
  863. package/sources/hooks/useAsyncCommand.ts +25 -0
  864. package/sources/hooks/useAttachmentImage.ts +134 -0
  865. package/sources/hooks/useAutocomplete.ts +69 -0
  866. package/sources/hooks/useAutocompleteSession.ts +53 -0
  867. package/sources/hooks/useChangelog.ts +45 -0
  868. package/sources/hooks/useCheckCameraPermissions.ts +25 -0
  869. package/sources/hooks/useConnectAccount.ts +107 -0
  870. package/sources/hooks/useConnectTerminal.ts +112 -0
  871. package/sources/hooks/useDemoMessages.ts +48 -0
  872. package/sources/hooks/useDraft.ts +120 -0
  873. package/sources/hooks/useElapsedTime.ts +36 -0
  874. package/sources/hooks/useGetPath.ts +13 -0
  875. package/sources/hooks/useGitStatusFiles.ts +45 -0
  876. package/sources/hooks/useGlobalKeyboard.ts +29 -0
  877. package/sources/hooks/useGroupedMessages.ts +149 -0
  878. package/sources/hooks/useImagePicker.ts +135 -0
  879. package/sources/hooks/useInboxHasContent.ts +19 -0
  880. package/sources/hooks/useMultiClick.ts +56 -0
  881. package/sources/hooks/useNasTechAction.ts +45 -0
  882. package/sources/hooks/useNativeUpdate.ts +10 -0
  883. package/sources/hooks/useNavigateToSession.ts +20 -0
  884. package/sources/hooks/useNewSessionDraft.ts +70 -0
  885. package/sources/hooks/usePrefetchFileContents.ts +162 -0
  886. package/sources/hooks/useSearch.ts +120 -0
  887. package/sources/hooks/useSessionQuickActions.ts +325 -0
  888. package/sources/hooks/useTauriDrag.ts +76 -0
  889. package/sources/hooks/useTauriZoom.ts +67 -0
  890. package/sources/hooks/useUpdates.ts +84 -0
  891. package/sources/hooks/useVisibleSessionListViewData.ts +65 -0
  892. package/sources/hooks/useWorktreeCleanup.ts +69 -0
  893. package/sources/log.ts +108 -0
  894. package/sources/modal/ModalManager.ts +203 -0
  895. package/sources/modal/ModalProvider.tsx +103 -0
  896. package/sources/modal/components/BaseModal.tsx +122 -0
  897. package/sources/modal/components/CustomModal.tsx +42 -0
  898. package/sources/modal/components/WebAlertModal.tsx +139 -0
  899. package/sources/modal/components/WebPromptModal.tsx +185 -0
  900. package/sources/modal/index.ts +3 -0
  901. package/sources/modal/types.ts +79 -0
  902. package/sources/nastech-wire/index.ts +10 -0
  903. package/sources/nastech-wire/legacyProtocol.ts +27 -0
  904. package/sources/nastech-wire/messageMeta.ts +14 -0
  905. package/sources/nastech-wire/messages.ts +113 -0
  906. package/sources/nastech-wire/sessionProtocol.ts +134 -0
  907. package/sources/nastech-wire/voice.ts +34 -0
  908. package/sources/polyfills/screenOrientation.ts +33 -0
  909. package/sources/realtime/RealtimeProvider.tsx +20 -0
  910. package/sources/realtime/RealtimeProvider.web.tsx +15 -0
  911. package/sources/realtime/RealtimeSession.ts +200 -0
  912. package/sources/realtime/RealtimeVoiceSession.tsx +211 -0
  913. package/sources/realtime/RealtimeVoiceSession.web.tsx +209 -0
  914. package/sources/realtime/hooks/contextFormatters.ts +127 -0
  915. package/sources/realtime/hooks/voiceHooks.ts +232 -0
  916. package/sources/realtime/realtimeClientTools.ts +94 -0
  917. package/sources/realtime/types.ts +19 -0
  918. package/sources/realtime/voiceConfig.ts +31 -0
  919. package/sources/realtime/voiceExperiment.ts +91 -0
  920. package/sources/realtime/voiceSystemPrompt.ts +75 -0
  921. package/sources/scripts/compareTranslations.ts +217 -0
  922. package/sources/scripts/parseChangelog.ts +87 -0
  923. package/sources/sync/__testdata__/trace_0.json +3986 -0
  924. package/sources/sync/__testdata__/trace_1.json +1391 -0
  925. package/sources/sync/__testdata__/trace_2.json +182 -0
  926. package/sources/sync/agentDefaults.ts +108 -0
  927. package/sources/sync/apiArtifacts.ts +143 -0
  928. package/sources/sync/apiAttachments.ts +217 -0
  929. package/sources/sync/apiFeed.ts +60 -0
  930. package/sources/sync/apiFriends.ts +217 -0
  931. package/sources/sync/apiGithub.spec.ts +97 -0
  932. package/sources/sync/apiGithub.ts +103 -0
  933. package/sources/sync/apiKv.ts +270 -0
  934. package/sources/sync/apiPush.ts +83 -0
  935. package/sources/sync/apiServices.ts +64 -0
  936. package/sources/sync/apiSocket.ts +290 -0
  937. package/sources/sync/apiTypes.spec.ts +23 -0
  938. package/sources/sync/apiTypes.ts +213 -0
  939. package/sources/sync/apiUsage.ts +130 -0
  940. package/sources/sync/apiVoice.ts +57 -0
  941. package/sources/sync/appConfig.ts +95 -0
  942. package/sources/sync/artifactTypes.ts +85 -0
  943. package/sources/sync/attachmentTypes.ts +28 -0
  944. package/sources/sync/encryption/artifactEncryption.ts +83 -0
  945. package/sources/sync/encryption/encryption.ts +220 -0
  946. package/sources/sync/encryption/encryptionCache.ts +248 -0
  947. package/sources/sync/encryption/encryptor.appspec.ts +409 -0
  948. package/sources/sync/encryption/encryptor.ts +126 -0
  949. package/sources/sync/encryption/machineEncryption.ts +122 -0
  950. package/sources/sync/encryption/sessionEncryption.ts +207 -0
  951. package/sources/sync/feedTypes.ts +43 -0
  952. package/sources/sync/friendTypes.ts +92 -0
  953. package/sources/sync/git-parsers/LineParser.ts +62 -0
  954. package/sources/sync/git-parsers/parseBranch.ts +97 -0
  955. package/sources/sync/git-parsers/parseDiff.ts +180 -0
  956. package/sources/sync/git-parsers/parseStatus.ts +162 -0
  957. package/sources/sync/git-parsers/parseStatusV2.ts +307 -0
  958. package/sources/sync/gitStatusFiles.ts +185 -0
  959. package/sources/sync/gitStatusSync.ts +282 -0
  960. package/sources/sync/localSettings.ts +67 -0
  961. package/sources/sync/messageMeta.test.ts +87 -0
  962. package/sources/sync/messageMeta.ts +36 -0
  963. package/sources/sync/modeHacks.test.ts +29 -0
  964. package/sources/sync/modeHacks.ts +22 -0
  965. package/sources/sync/nastechApi.ts +124 -0
  966. package/sources/sync/ops.ts +776 -0
  967. package/sources/sync/persistence.ts +322 -0
  968. package/sources/sync/profile.ts +95 -0
  969. package/sources/sync/projectFiles.ts +54 -0
  970. package/sources/sync/prompt/systemPrompt.ts +20 -0
  971. package/sources/sync/purchases.ts +67 -0
  972. package/sources/sync/pushRegistration.ts +229 -0
  973. package/sources/sync/reducer/activityUpdateAccumulator.test.ts +492 -0
  974. package/sources/sync/reducer/activityUpdateAccumulator.ts +96 -0
  975. package/sources/sync/reducer/messageToEvent.ts +85 -0
  976. package/sources/sync/reducer/phase0-skipping.spec.ts +206 -0
  977. package/sources/sync/reducer/reducer.spec.ts +3169 -0
  978. package/sources/sync/reducer/reducer.ts +1214 -0
  979. package/sources/sync/reducer/reducerTracer.spec.ts +502 -0
  980. package/sources/sync/reducer/reducerTracer.ts +310 -0
  981. package/sources/sync/revenueCat/index.ts +21 -0
  982. package/sources/sync/revenueCat/revenueCat.ts +215 -0
  983. package/sources/sync/revenueCat/revenueCat.web.ts +238 -0
  984. package/sources/sync/revenueCat/types.ts +82 -0
  985. package/sources/sync/serverConfig.ts +72 -0
  986. package/sources/sync/settings.spec.ts +456 -0
  987. package/sources/sync/settings.ts +186 -0
  988. package/sources/sync/storage.ts +1653 -0
  989. package/sources/sync/storageTypes.spec.ts +24 -0
  990. package/sources/sync/storageTypes.ts +215 -0
  991. package/sources/sync/suggestionCommands.ts +149 -0
  992. package/sources/sync/suggestionFile.ts +206 -0
  993. package/sources/sync/sync.ts +2555 -0
  994. package/sources/sync/typesMessage.ts +69 -0
  995. package/sources/sync/typesMessageMeta.test.ts +14 -0
  996. package/sources/sync/typesMessageMeta.ts +17 -0
  997. package/sources/sync/typesRaw.spec.ts +2010 -0
  998. package/sources/sync/typesRaw.ts +1183 -0
  999. package/sources/sync/uploadFormFile.ts +29 -0
  1000. package/sources/sync/uploadFormFile.web.ts +14 -0
  1001. package/sources/sync/webTabTitle.ts +58 -0
  1002. package/sources/text/README.md +223 -0
  1003. package/sources/text/_all.ts +104 -0
  1004. package/sources/text/_default.ts +1015 -0
  1005. package/sources/text/index.ts +215 -0
  1006. package/sources/text/translations/ca.ts +993 -0
  1007. package/sources/text/translations/en.ts +1009 -0
  1008. package/sources/text/translations/es.ts +995 -0
  1009. package/sources/text/translations/it.ts +992 -0
  1010. package/sources/text/translations/ja.ts +993 -0
  1011. package/sources/text/translations/pl.ts +1024 -0
  1012. package/sources/text/translations/pt.ts +992 -0
  1013. package/sources/text/translations/ru.ts +1023 -0
  1014. package/sources/text/translations/zh-Hans.ts +992 -0
  1015. package/sources/text/translations/zh-Hant.ts +991 -0
  1016. package/sources/theme.css +72 -0
  1017. package/sources/theme.dark.json +31 -0
  1018. package/sources/theme.figma.json +457 -0
  1019. package/sources/theme.gen.ts +10 -0
  1020. package/sources/theme.light.json +31 -0
  1021. package/sources/theme.ts +452 -0
  1022. package/sources/track/index.ts +171 -0
  1023. package/sources/track/tracking.ts +12 -0
  1024. package/sources/track/useTrackScreens.ts +10 -0
  1025. package/sources/types/react-native-webrtc-web-shim.d.ts +6 -0
  1026. package/sources/unistyles.ts +97 -0
  1027. package/sources/utils/codexUnifiedDiff.spec.ts +43 -0
  1028. package/sources/utils/codexUnifiedDiff.ts +70 -0
  1029. package/sources/utils/consoleLogging.ts +145 -0
  1030. package/sources/utils/copySessionMetadataToClipboard.ts +53 -0
  1031. package/sources/utils/debounce.test.ts +646 -0
  1032. package/sources/utils/debounce.ts +122 -0
  1033. package/sources/utils/deviceCalculations.test.ts +318 -0
  1034. package/sources/utils/deviceCalculations.ts +87 -0
  1035. package/sources/utils/errors.ts +10 -0
  1036. package/sources/utils/formatPermissionParams.ts +23 -0
  1037. package/sources/utils/isTauri.ts +7 -0
  1038. package/sources/utils/loadSkia.ts +3 -0
  1039. package/sources/utils/loadSkia.web.ts +5 -0
  1040. package/sources/utils/lock.ts +40 -0
  1041. package/sources/utils/machineUtils.ts +6 -0
  1042. package/sources/utils/messageUtils.ts +254 -0
  1043. package/sources/utils/microphonePermissions.ts +109 -0
  1044. package/sources/utils/notificationRouting.test.ts +51 -0
  1045. package/sources/utils/notificationRouting.ts +81 -0
  1046. package/sources/utils/oauth.ts +143 -0
  1047. package/sources/utils/openExternalUrl.ts +19 -0
  1048. package/sources/utils/parseToken.ts +23 -0
  1049. package/sources/utils/pasteImages.web.ts +81 -0
  1050. package/sources/utils/pathUtils.spec.ts +226 -0
  1051. package/sources/utils/pathUtils.ts +75 -0
  1052. package/sources/utils/platform.ts +19 -0
  1053. package/sources/utils/readFileBytes.ts +11 -0
  1054. package/sources/utils/readFileBytes.web.ts +12 -0
  1055. package/sources/utils/requestReview.ts +135 -0
  1056. package/sources/utils/responsive.ts +87 -0
  1057. package/sources/utils/resumeCommand.test.ts +78 -0
  1058. package/sources/utils/resumeCommand.ts +70 -0
  1059. package/sources/utils/sessionFileLinks.test.ts +112 -0
  1060. package/sources/utils/sessionFileLinks.ts +388 -0
  1061. package/sources/utils/sessionUtils.ts +226 -0
  1062. package/sources/utils/stringUtils.ts +41 -0
  1063. package/sources/utils/sync.ts +164 -0
  1064. package/sources/utils/thumbhash.ts +17 -0
  1065. package/sources/utils/thumbhash.web.ts +85 -0
  1066. package/sources/utils/time.ts +41 -0
  1067. package/sources/utils/toSnakeCase.test.ts +182 -0
  1068. package/sources/utils/toSnakeCase.ts +40 -0
  1069. package/sources/utils/toolCommand.test.ts +23 -0
  1070. package/sources/utils/toolCommand.ts +35 -0
  1071. package/sources/utils/toolComparison.test.ts +101 -0
  1072. package/sources/utils/toolComparison.ts +73 -0
  1073. package/sources/utils/toolErrorParser.test.ts +126 -0
  1074. package/sources/utils/toolErrorParser.ts +102 -0
  1075. package/sources/utils/trimIdent.ts +27 -0
  1076. package/sources/utils/truncateForLogs.ts +37 -0
  1077. package/sources/utils/versionUtils.test.ts +58 -0
  1078. package/sources/utils/versionUtils.ts +69 -0
  1079. package/sources/utils/web/faviconGenerator.ts +39 -0
  1080. package/sources/utils/worktree.ts +192 -0
  1081. package/sources/wire/index.ts +97 -0
  1082. package/src-tauri/Cargo.lock +5978 -0
  1083. package/src-tauri/Cargo.toml +27 -0
  1084. package/src-tauri/build.rs +3 -0
  1085. package/src-tauri/capabilities/default.json +27 -0
  1086. package/src-tauri/deny.toml +53 -0
  1087. package/src-tauri/entitlements.plist +24 -0
  1088. package/src-tauri/icons/128x128.png +0 -0
  1089. package/src-tauri/icons/128x128@2x.png +0 -0
  1090. package/src-tauri/icons/32x32.png +0 -0
  1091. package/src-tauri/icons/64x64.png +0 -0
  1092. package/src-tauri/icons/Square107x107Logo.png +0 -0
  1093. package/src-tauri/icons/Square142x142Logo.png +0 -0
  1094. package/src-tauri/icons/Square150x150Logo.png +0 -0
  1095. package/src-tauri/icons/Square284x284Logo.png +0 -0
  1096. package/src-tauri/icons/Square30x30Logo.png +0 -0
  1097. package/src-tauri/icons/Square310x310Logo.png +0 -0
  1098. package/src-tauri/icons/Square44x44Logo.png +0 -0
  1099. package/src-tauri/icons/Square71x71Logo.png +0 -0
  1100. package/src-tauri/icons/Square89x89Logo.png +0 -0
  1101. package/src-tauri/icons/StoreLogo.png +0 -0
  1102. package/src-tauri/icons/icon.icns +0 -0
  1103. package/src-tauri/icons/icon.ico +0 -0
  1104. package/src-tauri/icons/icon.png +0 -0
  1105. package/src-tauri/src/lib.rs +18 -0
  1106. package/src-tauri/src/main.rs +6 -0
  1107. package/src-tauri/tauri.conf.json +51 -0
  1108. package/src-tauri/tauri.dev.conf.json +20 -0
  1109. package/src-tauri/tauri.preview.conf.json +20 -0
  1110. package/tsconfig.json +45 -0
  1111. package/vitest.config.ts +26 -0
@@ -0,0 +1,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
+ });