experimental-a2 0.16.0 → 0.17.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 (334) hide show
  1. package/CHANGELOG.md +360 -0
  2. package/dist/{actor-ohPC-81x.d.ts → actor-DBbe5xeE.d.ts} +21 -11
  3. package/dist/actor-DBbe5xeE.d.ts.map +1 -0
  4. package/dist/actor-client.d.ts +2 -5
  5. package/dist/actor-client.d.ts.map +1 -1
  6. package/dist/actor-client.js +13 -4
  7. package/dist/actor-client.js.map +1 -1
  8. package/dist/actor-react.d.ts +5 -3
  9. package/dist/actor-react.d.ts.map +1 -1
  10. package/dist/actor-react.js +20 -10
  11. package/dist/actor-react.js.map +1 -1
  12. package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
  13. package/dist/actor-shared-t7w8xIKP.js.map +1 -0
  14. package/dist/actor.d.ts +1 -1
  15. package/dist/actor.js +42 -23
  16. package/dist/actor.js.map +1 -1
  17. package/dist/{ai-CrLYwNEx.js → ai-UrWfKeuv.js} +966 -541
  18. package/dist/ai-UrWfKeuv.js.map +1 -0
  19. package/dist/ai-client.d.ts +27 -0
  20. package/dist/ai-client.d.ts.map +1 -0
  21. package/dist/ai-client.js +29 -0
  22. package/dist/ai-client.js.map +1 -0
  23. package/dist/ai-react.d.ts +96 -0
  24. package/dist/ai-react.d.ts.map +1 -0
  25. package/dist/ai-react.js +142 -0
  26. package/dist/ai-react.js.map +1 -0
  27. package/dist/ai-server.browser.js +2 -2
  28. package/dist/ai-server.browser.js.map +1 -1
  29. package/dist/ai-server.d.ts +80 -36
  30. package/dist/ai-server.d.ts.map +1 -1
  31. package/dist/ai-server.js +466 -265
  32. package/dist/ai-server.js.map +1 -1
  33. package/dist/{ai-HJ9fHfYI.d.ts → ai-vpGtcaCB.d.ts} +115 -101
  34. package/dist/ai-vpGtcaCB.d.ts.map +1 -0
  35. package/dist/ai.d.ts +2 -2
  36. package/dist/ai.js +2 -2
  37. package/dist/{client-BYzHjkwU.js → client-COaiaDub.js} +447 -80
  38. package/dist/client-COaiaDub.js.map +1 -0
  39. package/dist/{client-CzyacQpJ.d.ts → client-qRnRUzBS.d.ts} +14 -13
  40. package/dist/client-qRnRUzBS.d.ts.map +1 -0
  41. package/dist/client.d.ts +4 -2
  42. package/dist/client.js +1 -1
  43. package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
  44. package/dist/contract-BYKASlCV.js.map +1 -0
  45. package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
  46. package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
  47. package/dist/index.d.ts +6 -5
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -3
  50. package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-BrLso5x_.d.ts} +7 -3
  51. package/dist/initial-snapshot-BrLso5x_.d.ts.map +1 -0
  52. package/dist/{internal-Dq2qYxou.js → internal-CbVvEwad.js} +23 -2
  53. package/dist/internal-CbVvEwad.js.map +1 -0
  54. package/dist/otel.d.ts +1 -1
  55. package/dist/react-BbJ-g6Uu.d.ts +86 -0
  56. package/dist/react-BbJ-g6Uu.d.ts.map +1 -0
  57. package/dist/react.d.ts +2 -122
  58. package/dist/react.js +107 -100
  59. package/dist/react.js.map +1 -1
  60. package/dist/reducer-BAXc8JyF.js +87 -0
  61. package/dist/reducer-BAXc8JyF.js.map +1 -0
  62. package/dist/{reducer-otzHjuJj.d.ts → reducer-BYu6DT7h.d.ts} +45 -29
  63. package/dist/reducer-BYu6DT7h.d.ts.map +1 -0
  64. package/dist/reducer-runtime-DQwz_J59.js +28 -0
  65. package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +3 -3
  67. package/dist/scheduler-qstash.d.ts.map +1 -1
  68. package/dist/scheduler-qstash.js +33 -10
  69. package/dist/scheduler-qstash.js.map +1 -1
  70. package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
  71. package/dist/scheduler-task-X1SToDjP.js.map +1 -0
  72. package/dist/scheduler-vercel.d.ts +4 -9
  73. package/dist/scheduler-vercel.d.ts.map +1 -1
  74. package/dist/scheduler-vercel.js +25 -44
  75. package/dist/scheduler-vercel.js.map +1 -1
  76. package/dist/{server-DbD7IJVK.d.ts → server-CGM8WzRG.d.ts} +87 -26
  77. package/dist/server-CGM8WzRG.d.ts.map +1 -0
  78. package/dist/{server-BD5ckxZb.js → server-Rj64fNvX.js} +854 -202
  79. package/dist/server-Rj64fNvX.js.map +1 -0
  80. package/dist/server.d.ts +4 -4
  81. package/dist/server.js +2 -2
  82. package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
  83. package/dist/session-clear-DGI4jTQ5.js.map +1 -0
  84. package/dist/session-clear-Dshpuhff.d.ts +12 -0
  85. package/dist/session-clear-Dshpuhff.d.ts.map +1 -0
  86. package/dist/{store-DyZM6fS5.d.ts → store-Gpkq7WBo.d.ts} +44 -3
  87. package/dist/store-Gpkq7WBo.d.ts.map +1 -0
  88. package/dist/store-N8PXxDAS.js.map +1 -1
  89. package/dist/store-memory.d.ts +1 -1
  90. package/dist/store-memory.d.ts.map +1 -1
  91. package/dist/store-memory.js +182 -69
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-CmxUbV93.js → store-polling-4_y8lETm.js} +21 -4
  94. package/dist/store-polling-4_y8lETm.js.map +1 -0
  95. package/dist/store-postgres.d.ts +1 -1
  96. package/dist/store-postgres.d.ts.map +1 -1
  97. package/dist/store-postgres.js +399 -108
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-Q1C-k2dd.js} +3 -3
  100. package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-Q1C-k2dd.js.map} +1 -1
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.d.ts.map +1 -1
  103. package/dist/store-redis-http.js +293 -11
  104. package/dist/store-redis-http.js.map +1 -1
  105. package/dist/store-redis-notify-tqWNnMRu.js +2374 -0
  106. package/dist/store-redis-notify-tqWNnMRu.js.map +1 -0
  107. package/dist/store-redis.d.ts +1 -1
  108. package/dist/store-redis.d.ts.map +1 -1
  109. package/dist/store-redis.js +11 -11
  110. package/dist/store-redis.js.map +1 -1
  111. package/dist/store-sqlite.d.ts +1 -1
  112. package/dist/store-sqlite.d.ts.map +1 -1
  113. package/dist/store-sqlite.js +166 -32
  114. package/dist/store-sqlite.js.map +1 -1
  115. package/dist/stream-buffer-Cx7AAPGq.js +23 -0
  116. package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
  117. package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-cZH-cibj.d.ts} +2 -2
  118. package/dist/telemetry-cZH-cibj.d.ts.map +1 -0
  119. package/dist/{wire-BO5wWCb1.js → wire-DjfHOvdW.js} +132 -10
  120. package/dist/wire-DjfHOvdW.js.map +1 -0
  121. package/docs/01-quickstart.mdx +2 -2
  122. package/docs/actors/01-introduction.mdx +4 -0
  123. package/docs/actors/04-routes.mdx +14 -1
  124. package/docs/concepts/01-contracts.mdx +95 -0
  125. package/docs/concepts/04-state.mdx +3 -3
  126. package/docs/guides/01-timers.mdx +22 -14
  127. package/docs/guides/02-cancellation.mdx +1 -1
  128. package/docs/guides/03-react.mdx +268 -328
  129. package/docs/guides/04-local-first.mdx +32 -73
  130. package/docs/guides/05-production.mdx +40 -27
  131. package/docs/guides/06-ai-agents.mdx +680 -580
  132. package/docs/guides/09-presence.mdx +3 -7
  133. package/docs/guides/10-transports.mdx +5 -5
  134. package/docs/index.mdx +10 -18
  135. package/docs/reference/01-api.mdx +662 -195
  136. package/docs/reference/02-errors.mdx +2 -1
  137. package/examples/playground/DEPLOY.md +52 -6
  138. package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
  139. package/examples/playground/app/a0/[appId]/page.tsx +48 -0
  140. package/examples/playground/app/a0/a0.css +818 -0
  141. package/examples/playground/app/a0/app-list.tsx +211 -0
  142. package/examples/playground/app/a0/app-view.tsx +55 -0
  143. package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
  144. package/examples/playground/app/a0/apps/[appId]/route.ts +23 -0
  145. package/examples/playground/app/a0/apps.test.ts +116 -0
  146. package/examples/playground/app/a0/apps.ts +87 -0
  147. package/examples/playground/app/a0/chat.tsx +364 -0
  148. package/examples/playground/app/a0/columns.tsx +109 -0
  149. package/examples/playground/app/a0/context-menu.tsx +116 -0
  150. package/examples/playground/app/a0/cookies.ts +46 -0
  151. package/examples/playground/app/a0/crash-on-request.ts +72 -0
  152. package/examples/playground/app/a0/crashes/route.ts +3 -0
  153. package/examples/playground/app/a0/crashes.test.ts +33 -0
  154. package/examples/playground/app/a0/crashes.ts +28 -0
  155. package/examples/playground/app/a0/devtools.tsx +164 -0
  156. package/examples/playground/app/a0/events/route.ts +6 -0
  157. package/examples/playground/app/a0/flags.ts +30 -0
  158. package/examples/playground/app/a0/layout.tsx +38 -0
  159. package/examples/playground/app/a0/list/route.ts +10 -0
  160. package/examples/playground/app/a0/model.test.ts +56 -0
  161. package/examples/playground/app/a0/model.ts +58 -0
  162. package/examples/playground/app/a0/page.tsx +14 -0
  163. package/examples/playground/app/a0/preview-pane.tsx +145 -0
  164. package/examples/playground/app/a0/preview.test.ts +53 -0
  165. package/examples/playground/app/a0/preview.ts +26 -0
  166. package/examples/playground/app/a0/sandbox-log.ts +69 -0
  167. package/examples/playground/app/a0/sandbox.ts +191 -0
  168. package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
  169. package/examples/playground/app/a0/scheduler/route.ts +7 -0
  170. package/examples/playground/app/a0/server.ts +158 -0
  171. package/examples/playground/app/a0/session.ts +47 -0
  172. package/examples/playground/app/a0/shell.tsx +150 -0
  173. package/examples/playground/app/a0/short/events/route.ts +9 -0
  174. package/examples/playground/app/a0/splitter.tsx +120 -0
  175. package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
  176. package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
  177. package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
  178. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
  179. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
  180. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
  181. package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
  182. package/examples/playground/app/agent/events/route.ts +5 -2
  183. package/examples/playground/app/agent/ingress.test.ts +98 -0
  184. package/examples/playground/app/agent/model-input.test.tsx +58 -0
  185. package/examples/playground/app/agent/model-input.tsx +115 -0
  186. package/examples/playground/app/agent/model.test.ts +73 -0
  187. package/examples/playground/app/agent/model.ts +58 -26
  188. package/examples/playground/app/agent/new-agent-session.tsx +37 -19
  189. package/examples/playground/app/agent/page.tsx +4 -4
  190. package/examples/playground/app/agent/server.ts +15 -14
  191. package/examples/playground/app/agent/session.ts +5 -6
  192. package/examples/playground/app/agent/telemetry.test.ts +168 -0
  193. package/examples/playground/app/agent/telemetry.ts +30 -0
  194. package/examples/playground/app/agent/trace.test.ts +23 -0
  195. package/examples/playground/app/agent/trace.ts +18 -0
  196. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +11 -11
  197. package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
  198. package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
  199. package/examples/playground/app/canvas/model.ts +53 -53
  200. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
  201. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
  202. package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
  203. package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
  204. package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
  205. package/examples/playground/app/chat/[chatId]/session.ts +2 -19
  206. package/examples/playground/app/chat/agent/route.ts +9 -0
  207. package/examples/playground/app/chat/model.test.ts +46 -119
  208. package/examples/playground/app/chat/model.ts +82 -128
  209. package/examples/playground/app/chat/scheduler/route.ts +2 -2
  210. package/examples/playground/app/chat/server.test.ts +108 -0
  211. package/examples/playground/app/chat/server.ts +69 -73
  212. package/examples/playground/app/components/activity-feed.tsx +30 -1
  213. package/examples/playground/app/components/activity-history.test.tsx +35 -0
  214. package/examples/playground/app/components/demo-icons.tsx +89 -0
  215. package/examples/playground/app/components/demos.tsx +33 -0
  216. package/examples/playground/app/components/fast-link.test.tsx +58 -0
  217. package/examples/playground/app/components/fast-link.tsx +55 -0
  218. package/examples/playground/app/components/site-header.tsx +96 -0
  219. package/examples/playground/app/counter/counter-client.tsx +3 -3
  220. package/examples/playground/app/counter/model.test.ts +3 -3
  221. package/examples/playground/app/counter/model.ts +11 -21
  222. package/examples/playground/app/counter/page.tsx +2 -15
  223. package/examples/playground/app/counter/session.ts +2 -2
  224. package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
  225. package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
  226. package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
  227. package/examples/playground/app/documents/[documentId]/session.ts +2 -2
  228. package/examples/playground/app/documents/model.ts +27 -27
  229. package/examples/playground/app/globals.css +420 -207
  230. package/examples/playground/app/initial-state.test.tsx +54 -0
  231. package/examples/playground/app/layout.tsx +7 -23
  232. package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
  233. package/examples/playground/app/orders/[orderId]/session.ts +2 -2
  234. package/examples/playground/app/orders/model.ts +49 -49
  235. package/examples/playground/app/page.tsx +13 -102
  236. package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
  237. package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
  238. package/examples/playground/app/recovery/model.ts +30 -30
  239. package/examples/playground/instrumentation.ts +5 -0
  240. package/examples/playground/package.json +10 -2
  241. package/examples/playground/scripts/a0-template.ts +77 -0
  242. package/examples/playground/vercel.json +18 -0
  243. package/package.json +3 -1
  244. package/src/actor-client.ts +16 -4
  245. package/src/actor-react.ts +25 -11
  246. package/src/actor-shared.ts +21 -6
  247. package/src/actor.ts +76 -31
  248. package/src/ai-agent-actions.ts +219 -0
  249. package/src/ai-agent.ts +2343 -0
  250. package/src/ai-app-state.ts +91 -0
  251. package/src/ai-client-state.ts +39 -2
  252. package/src/ai-client.ts +62 -0
  253. package/src/ai-context-schema.ts +25 -5
  254. package/src/ai-context.ts +90 -40
  255. package/src/ai-control-server.ts +94 -55
  256. package/src/ai-control.ts +2 -0
  257. package/src/ai-coordinator.ts +23 -22
  258. package/src/ai-message-base.ts +209 -0
  259. package/src/ai-message-projection.ts +382 -58
  260. package/src/ai-progress-batches.ts +26 -0
  261. package/src/ai-projector.ts +95 -14
  262. package/src/ai-react.ts +146 -0
  263. package/src/ai-server.browser.ts +2 -2
  264. package/src/ai-server.ts +20 -1951
  265. package/src/ai-stored-state.ts +48 -163
  266. package/src/ai.ts +343 -276
  267. package/src/bootstrap-stream.ts +72 -0
  268. package/src/bounded-cache.ts +55 -0
  269. package/src/client-render-session.ts +49 -0
  270. package/src/client.ts +688 -151
  271. package/src/contract.ts +92 -42
  272. package/src/deterministic-id.ts +37 -0
  273. package/src/equal-json.ts +32 -0
  274. package/src/errors.ts +13 -1
  275. package/src/index.ts +5 -1
  276. package/src/initial-snapshot.ts +39 -0
  277. package/src/internal.ts +45 -0
  278. package/src/postgres-notification-scope.ts +14 -3
  279. package/src/react.ts +171 -206
  280. package/src/redis-http-reply.ts +256 -0
  281. package/src/redis-http-stream.ts +73 -0
  282. package/src/redis-http-subscriptions.ts +13 -5
  283. package/src/reducer-runtime.ts +52 -0
  284. package/src/reducer.ts +92 -10
  285. package/src/scheduler-qstash.ts +49 -20
  286. package/src/scheduler-task.ts +34 -2
  287. package/src/scheduler-vercel.ts +32 -84
  288. package/src/server-fetch.ts +139 -35
  289. package/src/server.ts +957 -237
  290. package/src/session-clear.ts +23 -0
  291. package/src/session-socket.ts +154 -49
  292. package/src/sse.ts +71 -4
  293. package/src/store-memory.ts +220 -78
  294. package/src/store-polling.ts +18 -4
  295. package/src/store-postgres.ts +461 -114
  296. package/src/store-redis-core.ts +1260 -434
  297. package/src/store-redis-http.ts +18 -2
  298. package/src/store-redis-notify.ts +260 -85
  299. package/src/store-redis.ts +19 -9
  300. package/src/store-sqlite.ts +259 -45
  301. package/src/store.ts +46 -1
  302. package/src/stream-buffer.ts +43 -0
  303. package/src/telemetry.ts +13 -1
  304. package/src/wire.ts +241 -13
  305. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  306. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  307. package/dist/ai-CrLYwNEx.js.map +0 -1
  308. package/dist/ai-HJ9fHfYI.d.ts.map +0 -1
  309. package/dist/client-BYzHjkwU.js.map +0 -1
  310. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  311. package/dist/contract-CKRg_E4q.js.map +0 -1
  312. package/dist/errors-DCk6ch5n.js.map +0 -1
  313. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  314. package/dist/internal-Dq2qYxou.js.map +0 -1
  315. package/dist/react.d.ts.map +0 -1
  316. package/dist/reducer-DEMjEY_O.js +0 -29
  317. package/dist/reducer-DEMjEY_O.js.map +0 -1
  318. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  319. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  320. package/dist/server-BD5ckxZb.js.map +0 -1
  321. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  322. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  323. package/dist/store-polling-CmxUbV93.js.map +0 -1
  324. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  325. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  326. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  327. package/dist/validate-XKT4FSNn.js +0 -30
  328. package/dist/validate-XKT4FSNn.js.map +0 -1
  329. package/dist/wire-BO5wWCb1.js.map +0 -1
  330. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  331. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  332. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  333. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  334. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
@@ -0,0 +1,364 @@
1
+ 'use client'
2
+ import type { ReactNode } from 'react'
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import { Asterisk, LoaderCircle, Zap } from 'lucide-react'
5
+ import { inputs } from 'experimental-a2/ai'
6
+ import {
7
+ type AppMessage,
8
+ type ModelChoice,
9
+ reasoningSchema,
10
+ toolLabels,
11
+ writingLabels,
12
+ } from './model'
13
+ import { useSession } from './session'
14
+
15
+ type Part = AppMessage['parts'][number]
16
+
17
+ const suggestions = [
18
+ 'A pomodoro timer with start, pause and reset',
19
+ 'A landing page for a coffee shop called Ember',
20
+ 'A tip calculator that splits the bill',
21
+ ]
22
+
23
+ /** The coding agent's conversation: its words, and one line per tool step. */
24
+ export function Chat({
25
+ onPrompt,
26
+ autoFocus,
27
+ }: {
28
+ onPrompt: (prompt: string) => void
29
+ autoFocus: boolean
30
+ }): ReactNode {
31
+ const { state, push } = useSession()
32
+ const [error, setError] = useState<string | null>(null)
33
+ // A change here applies when the next message goes out with it, so typing a
34
+ // model id doesn't record a choice per keystroke. Until then, the composer
35
+ // shows the app's choice, which every client sees.
36
+ const [chosen, setChosen] = useState<ModelChoice | null>(null)
37
+ const settings = chosen ?? state.modelChoice
38
+
39
+ const last = state.messages.at(-1)
40
+ // A send is optimistic, and so is the turn it starts, so this holds from
41
+ // the moment it's pushed rather than once the server accepts it.
42
+ const working =
43
+ state.starting || state.status === 'generating' || state.active !== null
44
+ const waiting =
45
+ working &&
46
+ (last?.role !== 'assistant' || !last.parts.some((part) => isVisible(part)))
47
+
48
+ const send = async (text: string): Promise<void> => {
49
+ setError(null)
50
+ onPrompt(text)
51
+ try {
52
+ await push(
53
+ ...(chosen === null
54
+ ? []
55
+ : [
56
+ {
57
+ type: 'model.chosen' as const,
58
+ payload: { ...chosen, model: chosen.model.trim() },
59
+ },
60
+ ]),
61
+ ...inputs.message({
62
+ id: crypto.randomUUID(),
63
+ role: 'user',
64
+ parts: [{ type: 'text', text }],
65
+ }),
66
+ )
67
+ setChosen(null)
68
+ } catch (cause) {
69
+ setError(cause instanceof Error ? cause.message : String(cause))
70
+ throw cause
71
+ }
72
+ }
73
+ const stop = (): void => {
74
+ if (!state.active) return
75
+ push(...inputs.stop({ turnId: state.active.turnId })).catch((cause) =>
76
+ setError(cause instanceof Error ? cause.message : String(cause)),
77
+ )
78
+ }
79
+
80
+ return (
81
+ <>
82
+ {/* Reversed, so it opens at the newest message and stays there. */}
83
+ <div className="a0-transcript-scroll">
84
+ <ol className="a0-transcript" aria-label="Conversation">
85
+ {!state.messages.length && (
86
+ <li className="a0-suggestions">
87
+ {suggestions.map((suggestion) => (
88
+ <button
89
+ key={suggestion}
90
+ type="button"
91
+ className="a0-suggestion"
92
+ disabled={!settings.model.trim()}
93
+ onClick={() => void send(suggestion).catch(() => {})}
94
+ >
95
+ <Asterisk size={12} aria-hidden />
96
+ {suggestion}
97
+ </button>
98
+ ))}
99
+ </li>
100
+ )}
101
+ {state.messages.flatMap((message) =>
102
+ message.parts.map((part, index) => (
103
+ <MessagePart
104
+ key={`${message.id}:${index}`}
105
+ message={message}
106
+ part={part}
107
+ />
108
+ )),
109
+ )}
110
+ {waiting && (
111
+ <li className="a0-step">
112
+ <LoaderCircle size={12} className="a0-spin" aria-hidden />
113
+ Thinking
114
+ </li>
115
+ )}
116
+ {state.error && (
117
+ <li role="alert" className="a0-error">
118
+ {state.error}
119
+ </li>
120
+ )}
121
+ </ol>
122
+ </div>
123
+ {error && (
124
+ <p role="alert" className="a0-error a0-send-error">
125
+ {error}
126
+ </p>
127
+ )}
128
+ <Composer
129
+ onSend={send}
130
+ working={working}
131
+ // A turn can be stopped once it's accepted and has an id.
132
+ onStop={state.active ? stop : null}
133
+ queued={state.inbox.items.length}
134
+ settings={settings}
135
+ onSettings={setChosen}
136
+ autoFocus={autoFocus}
137
+ />
138
+ </>
139
+ )
140
+ }
141
+
142
+ const isVisible = (part: Part): boolean =>
143
+ ((part.type === 'text' || part.type === 'reasoning') && !!part.text.trim()) ||
144
+ part.type.startsWith('tool-')
145
+
146
+ function MessagePart({
147
+ message,
148
+ part,
149
+ }: {
150
+ message: AppMessage
151
+ part: Part
152
+ }): ReactNode {
153
+ if (part.type === 'text' && part.text.trim())
154
+ return (
155
+ <li className="a0-text" data-role={message.role}>
156
+ {part.text}
157
+ </li>
158
+ )
159
+ if (part.type === 'reasoning' && part.text.trim()) {
160
+ const thinking = part.state === 'streaming'
161
+ return (
162
+ <li className="a0-reasoning">
163
+ {/* Open while the model thinks, folded once it's done. */}
164
+ <details open={thinking}>
165
+ <summary>
166
+ {thinking ? (
167
+ <LoaderCircle size={12} className="a0-spin" aria-hidden />
168
+ ) : (
169
+ <Asterisk size={12} aria-hidden />
170
+ )}
171
+ {thinking ? 'Thinking' : 'Thought'}
172
+ </summary>
173
+ <p>{part.text}</p>
174
+ </details>
175
+ </li>
176
+ )
177
+ }
178
+ if (!part.type.startsWith('tool-') || !('state' in part)) return null
179
+ const name = part.type.slice('tool-'.length)
180
+ const done = part.state === 'output-available'
181
+ const failed = part.state === 'output-error'
182
+ const detail = (part as { input?: { command?: unknown } }).input?.command
183
+ const took = (part as { output?: { durationMs?: unknown } }).output
184
+ ?.durationMs
185
+ const label =
186
+ (part.state === 'input-streaming' && writingLabels[name]) ||
187
+ toolLabels[name] ||
188
+ name
189
+ return (
190
+ <li className="a0-step" data-tool={name} data-failed={failed || undefined}>
191
+ {done || failed ? (
192
+ <Asterisk size={12} className="a0-step-done" aria-hidden />
193
+ ) : (
194
+ <LoaderCircle size={12} className="a0-spin" aria-hidden />
195
+ )}
196
+ <span className="a0-step-label">{label}</span>
197
+ {typeof detail === 'string' && <code>{detail}</code>}
198
+ {done && typeof took === 'number' && (
199
+ <span className="a0-step-took">{duration(took)}</span>
200
+ )}
201
+ </li>
202
+ )
203
+ }
204
+
205
+ const duration = (ms: number): string =>
206
+ ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`
207
+
208
+ const reasoningLabel = (level: ModelChoice['reasoning']): string =>
209
+ level === 'provider-default'
210
+ ? 'default'
211
+ : level === 'xhigh'
212
+ ? 'extra high'
213
+ : level
214
+
215
+ /** The message box: a dotted frame, then the model settings it sends with. */
216
+ function Composer({
217
+ onSend,
218
+ working,
219
+ onStop,
220
+ queued,
221
+ settings,
222
+ onSettings,
223
+ autoFocus,
224
+ }: {
225
+ onSend: (text: string) => Promise<void>
226
+ working: boolean
227
+ onStop: (() => void) | null
228
+ queued: number
229
+ settings: ModelChoice
230
+ onSettings: (settings: ModelChoice) => void
231
+ autoFocus: boolean
232
+ }): ReactNode {
233
+ const [text, setText] = useState('')
234
+ const input = useRef<HTMLTextAreaElement>(null)
235
+ // Typing anywhere else starts a message. Focus moves on keydown, so the
236
+ // key itself lands in the box.
237
+ useEffect(() => {
238
+ const type = (event: KeyboardEvent): void => {
239
+ const { target } = event
240
+ if (
241
+ event.defaultPrevented ||
242
+ event.isComposing ||
243
+ event.metaKey ||
244
+ event.ctrlKey ||
245
+ event.altKey ||
246
+ event.key.length !== 1 ||
247
+ (event.key === ' ' && target !== document.body) ||
248
+ (target instanceof HTMLElement &&
249
+ (target.isContentEditable ||
250
+ target.closest('input, textarea, select')))
251
+ )
252
+ return
253
+ input.current?.focus()
254
+ }
255
+ document.addEventListener('keydown', type)
256
+ return () => document.removeEventListener('keydown', type)
257
+ }, [])
258
+ // Grows with its text, up to a point, then scrolls.
259
+ useEffect(() => {
260
+ const textarea = input.current
261
+ if (!textarea) return
262
+ textarea.style.height = 'auto'
263
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 208)}px`
264
+ }, [text])
265
+ const ready = !!text.trim() && !!settings.model.trim()
266
+ return (
267
+ <form
268
+ className="a0-composer"
269
+ onSubmit={async (event) => {
270
+ event.preventDefault()
271
+ const message = text.trim()
272
+ if (!ready) return
273
+ setText('')
274
+ await onSend(message).catch(() =>
275
+ setText((current) => current || message),
276
+ )
277
+ }}
278
+ >
279
+ <textarea
280
+ ref={input}
281
+ aria-label="Message"
282
+ value={text}
283
+ rows={2}
284
+ autoFocus={autoFocus}
285
+ onChange={(event) => setText(event.target.value)}
286
+ onKeyDown={(event) => {
287
+ if (
288
+ event.key === 'Enter' &&
289
+ !event.shiftKey &&
290
+ !event.nativeEvent.isComposing
291
+ ) {
292
+ event.preventDefault()
293
+ event.currentTarget.form?.requestSubmit()
294
+ }
295
+ }}
296
+ placeholder="Ask the agent to build something"
297
+ />
298
+ <div className="a0-composer-bar">
299
+ <div className="a0-composer-model">
300
+ <input
301
+ aria-label="Model"
302
+ title="The Gateway model the next request runs with"
303
+ value={settings.model}
304
+ size={Math.max(settings.model.length, 12)}
305
+ onChange={(event) =>
306
+ onSettings({ ...settings, model: event.target.value })
307
+ }
308
+ autoComplete="off"
309
+ autoCorrect="off"
310
+ autoCapitalize="none"
311
+ spellCheck={false}
312
+ />
313
+ <select
314
+ aria-label="Reasoning"
315
+ title="Reasoning effort"
316
+ value={settings.reasoning}
317
+ onChange={(event) =>
318
+ onSettings({
319
+ ...settings,
320
+ reasoning: reasoningSchema.parse(event.target.value),
321
+ })
322
+ }
323
+ >
324
+ {reasoningSchema.options.map((level) => (
325
+ <option key={level} value={level}>
326
+ {reasoningLabel(level)}
327
+ </option>
328
+ ))}
329
+ </select>
330
+ <button
331
+ type="button"
332
+ className="a0-fast"
333
+ aria-label="Fast"
334
+ aria-pressed={settings.fast}
335
+ title="Fast: the faster tier when the model has one. May cost more."
336
+ onClick={() => onSettings({ ...settings, fast: !settings.fast })}
337
+ >
338
+ <Zap
339
+ size={13}
340
+ fill={settings.fast ? 'currentColor' : 'none'}
341
+ aria-hidden
342
+ />
343
+ </button>
344
+ </div>
345
+ <div className="a0-composer-actions">
346
+ {!!queued && <span className="a0-queued">{queued} queued</span>}
347
+ {working && (
348
+ <button
349
+ type="button"
350
+ className="a0-ghost"
351
+ disabled={!onStop}
352
+ onClick={onStop ?? undefined}
353
+ >
354
+ Stop
355
+ </button>
356
+ )}
357
+ <button type="submit" disabled={!ready}>
358
+ Send
359
+ </button>
360
+ </div>
361
+ </div>
362
+ </form>
363
+ )
364
+ }
@@ -0,0 +1,109 @@
1
+ 'use client'
2
+ import type { CSSProperties, ReactNode } from 'react'
3
+ import { createContext, useContext, useRef, useState } from 'react'
4
+ import {
5
+ appsWidth,
6
+ chatMin,
7
+ previewMin,
8
+ serializeWidths,
9
+ type Widths,
10
+ widthsCookie,
11
+ writeCookie,
12
+ } from './cookies'
13
+ import { Splitter } from './splitter'
14
+
15
+ type Column = keyof Widths
16
+
17
+ type Resize = (column: Column, width: number, done: boolean) => void
18
+
19
+ const edges: Record<
20
+ Column,
21
+ { label: string; min: number; max: (widths: Widths) => number }
22
+ > = {
23
+ apps: {
24
+ label: 'Resize the apps list',
25
+ min: appsWidth.min,
26
+ max: () => appsWidth.max,
27
+ },
28
+ chat: {
29
+ label: 'Resize the chat',
30
+ min: chatMin,
31
+ max: ({ apps }) => window.innerWidth - apps - previewMin,
32
+ },
33
+ }
34
+
35
+ const ColumnsContext = createContext<{
36
+ widths: Widths
37
+ resize: Resize
38
+ } | null>(null)
39
+
40
+ // A saved width still leaves the preview its minimum in a smaller window.
41
+ const chatTrack = (width: number): string =>
42
+ `min(${width}px, 100% - var(--a0-apps-width) - ${previewMin}px)`
43
+
44
+ const track = (column: Column, width: number): string =>
45
+ column === 'chat' ? chatTrack(width) : `${width}px`
46
+
47
+ /**
48
+ * A0's columns, apps, chat and preview, with the widths someone dragged the
49
+ * first two to, kept in a cookie so the server renders them. `panel` keeps
50
+ * a row under the chat and preview for the dev tools, even while the page
51
+ * beside it loads.
52
+ */
53
+ export function Columns({
54
+ saved,
55
+ panel,
56
+ children,
57
+ }: {
58
+ saved: Widths
59
+ panel: boolean
60
+ children: ReactNode
61
+ }): ReactNode {
62
+ const grid = useRef<HTMLDivElement>(null)
63
+ const [widths, setWidths] = useState(saved)
64
+ const resize: Resize = (column, width, done) => {
65
+ // Dragging sets the width directly, without rendering the page.
66
+ grid.current?.style.setProperty(
67
+ `--a0-${column}-width`,
68
+ track(column, width),
69
+ )
70
+ if (!done) return
71
+ const next = { ...widths, [column]: width }
72
+ setWidths(next)
73
+ writeCookie({ name: widthsCookie, value: serializeWidths(next) })
74
+ }
75
+ return (
76
+ <ColumnsContext value={{ widths, resize }}>
77
+ <div
78
+ ref={grid}
79
+ className="a0-page"
80
+ data-panel={panel || undefined}
81
+ style={
82
+ {
83
+ '--a0-apps-width': track('apps', widths.apps),
84
+ ...(widths.chat === null
85
+ ? {}
86
+ : { '--a0-chat-width': track('chat', widths.chat) }),
87
+ } as CSSProperties
88
+ }
89
+ >
90
+ {children}
91
+ </div>
92
+ </ColumnsContext>
93
+ )
94
+ }
95
+
96
+ /** The right edge of the column it sits in, which resizes that column. */
97
+ export function ColumnEdge({ column }: { column: Column }): ReactNode {
98
+ const columns = useContext(ColumnsContext)
99
+ if (!columns) throw new Error('ColumnEdge needs Columns around it.')
100
+ const { label, min, max } = edges[column]
101
+ return (
102
+ <Splitter
103
+ label={label}
104
+ min={min}
105
+ max={() => max(columns.widths)}
106
+ onResize={({ width, done }) => columns.resize(column, width, done)}
107
+ />
108
+ )
109
+ }
@@ -0,0 +1,116 @@
1
+ 'use client'
2
+ import type { KeyboardEvent, ReactNode } from 'react'
3
+ import {
4
+ useEffect,
5
+ useEffectEvent,
6
+ useLayoutEffect,
7
+ useRef,
8
+ useState,
9
+ } from 'react'
10
+
11
+ export type MenuItem = {
12
+ label: string
13
+ onSelect: () => void
14
+ danger?: boolean
15
+ }
16
+
17
+ // Keeps the menu this far inside the window's edges.
18
+ const MARGIN = 8
19
+
20
+ /**
21
+ * A menu at a point, as a right-click opens one. It takes focus, moves with
22
+ * the arrow keys, and closes on Escape, a press outside, or a pick; Escape
23
+ * and a pick hand focus back through `onClose`.
24
+ */
25
+ export function ContextMenu({
26
+ at,
27
+ items,
28
+ onClose,
29
+ }: {
30
+ at: { x: number; y: number }
31
+ items: MenuItem[]
32
+ onClose: (closed: { returnFocus: boolean }) => void
33
+ }): ReactNode {
34
+ const menu = useRef<HTMLDivElement>(null)
35
+ const [position, setPosition] = useState(at)
36
+
37
+ // Measured once it's drawn, so it can stay inside the window.
38
+ useLayoutEffect(() => {
39
+ const element = menu.current
40
+ if (!element) return
41
+ const { width, height } = element.getBoundingClientRect()
42
+ setPosition({
43
+ x: Math.max(MARGIN, Math.min(at.x, innerWidth - width - MARGIN)),
44
+ y: Math.max(MARGIN, Math.min(at.y, innerHeight - height - MARGIN)),
45
+ })
46
+ element.querySelector<HTMLElement>('[role="menuitem"]')?.focus()
47
+ }, [at])
48
+
49
+ const dismiss = useEffectEvent((returnFocus: boolean) =>
50
+ onClose({ returnFocus }),
51
+ )
52
+ useEffect(() => {
53
+ const outside = (event: PointerEvent): void => {
54
+ if (!menu.current?.contains(event.target as Node)) dismiss(false)
55
+ }
56
+ const away = (): void => dismiss(false)
57
+ document.addEventListener('pointerdown', outside, true)
58
+ window.addEventListener('blur', away)
59
+ window.addEventListener('resize', away)
60
+ return () => {
61
+ document.removeEventListener('pointerdown', outside, true)
62
+ window.removeEventListener('blur', away)
63
+ window.removeEventListener('resize', away)
64
+ }
65
+ }, [])
66
+
67
+ const move = (event: KeyboardEvent<HTMLDivElement>): void => {
68
+ const buttons = [
69
+ ...event.currentTarget.querySelectorAll<HTMLElement>('[role="menuitem"]'),
70
+ ]
71
+ const focused = buttons.indexOf(document.activeElement as HTMLElement)
72
+ const next =
73
+ event.key === 'ArrowDown'
74
+ ? (focused + 1) % buttons.length
75
+ : event.key === 'ArrowUp'
76
+ ? (focused - 1 + buttons.length) % buttons.length
77
+ : event.key === 'Home'
78
+ ? 0
79
+ : event.key === 'End'
80
+ ? buttons.length - 1
81
+ : null
82
+ if (event.key === 'Escape' || event.key === 'Tab') {
83
+ event.preventDefault()
84
+ onClose({ returnFocus: true })
85
+ } else if (next !== null) {
86
+ event.preventDefault()
87
+ buttons[next]?.focus()
88
+ }
89
+ }
90
+
91
+ return (
92
+ <div
93
+ ref={menu}
94
+ role="menu"
95
+ className="a0-menu"
96
+ style={{ left: position.x, top: position.y }}
97
+ onKeyDown={move}
98
+ onContextMenu={(event) => event.preventDefault()}
99
+ >
100
+ {items.map((item) => (
101
+ <button
102
+ key={item.label}
103
+ type="button"
104
+ role="menuitem"
105
+ data-danger={item.danger || undefined}
106
+ onClick={() => {
107
+ onClose({ returnFocus: true })
108
+ item.onSelect()
109
+ }}
110
+ >
111
+ {item.label}
112
+ </button>
113
+ ))}
114
+ </div>
115
+ )
116
+ }
@@ -0,0 +1,46 @@
1
+ /** The id of this browser's app list session. */
2
+ export const listCookie = 'a0_list'
3
+
4
+ /** The sidebar and chat widths someone dragged them to, in pixels. */
5
+ export const widthsCookie = 'a0_widths'
6
+
7
+ export type Widths = { apps: number; chat: number | null }
8
+
9
+ export const defaultWidths: Widths = { apps: 216, chat: null }
10
+
11
+ export const appsWidth = { min: 160, max: 400 }
12
+ export const chatMin = 320
13
+ export const previewMin = 320
14
+
15
+ export function parseWidths(value: string | undefined): Widths {
16
+ const [apps, chat] = (value ?? '').split(',').map(Number)
17
+ return {
18
+ apps:
19
+ apps && apps >= appsWidth.min && apps <= appsWidth.max
20
+ ? apps
21
+ : defaultWidths.apps,
22
+ chat: chat && chat >= chatMin ? chat : null,
23
+ }
24
+ }
25
+
26
+ export const serializeWidths = ({ apps, chat }: Widths): string =>
27
+ chat === null ? String(apps) : `${apps},${chat}`
28
+
29
+ /** Browser-only. */
30
+ export function readCookie(name: string): string | undefined {
31
+ return document.cookie
32
+ .split('; ')
33
+ .find((entry) => entry.startsWith(`${name}=`))
34
+ ?.slice(name.length + 1)
35
+ }
36
+
37
+ /** Browser-only. */
38
+ export function writeCookie({
39
+ name,
40
+ value,
41
+ }: {
42
+ name: string
43
+ value: string
44
+ }): void {
45
+ document.cookie = `${name}=${value}; path=/; max-age=31536000; samesite=lax`
46
+ }
@@ -0,0 +1,72 @@
1
+ import 'server-only'
2
+ import type { AIState } from 'experimental-a2/ai'
3
+ import { type AppMessage, appAgent } from './model'
4
+ import type { agentServer, crashServer } from './server'
5
+
6
+ type AppSession = ReturnType<typeof agentServer.session>
7
+ type CrashLog = ReturnType<typeof crashServer.session>
8
+
9
+ const working = (state: AIState<AppMessage>): boolean =>
10
+ state.status === 'generating' ||
11
+ state.status === 'waiting' ||
12
+ !!state.pendingQueueCommands?.length
13
+
14
+ /** Resolves once the app's agent stops working. */
15
+ async function workDone(app: AppSession): Promise<void> {
16
+ const { state: initial, index } = await app.state()
17
+ let state = initial
18
+ if (!working(state)) return
19
+ for await (const event of app.stream({ startAfter: index })) {
20
+ // A cleared app has nothing left to work on.
21
+ if (event.type === 'a2.cleared') return
22
+ state = appAgent.reducer.fold(state, event)
23
+ if (!working(state)) return
24
+ }
25
+ }
26
+
27
+ async function listen({
28
+ app,
29
+ log,
30
+ }: {
31
+ app: AppSession
32
+ log: CrashLog
33
+ }): Promise<void> {
34
+ const crash = async (): Promise<never> => {
35
+ // Recorded first, so the resumed work doesn't crash again.
36
+ await log.append({ type: 'crashed', payload: {} })
37
+ process.exit(1)
38
+ }
39
+ const { state: pending, index } = await log.state()
40
+ if (pending) return crash()
41
+ const requests = log.stream({ startAfter: index })[Symbol.asyncIterator]()
42
+ // When the work ends, closing the stream ends the loop below.
43
+ const close = (): void => void requests.return?.()
44
+ void workDone(app).then(close, close)
45
+ for await (const event of { [Symbol.asyncIterator]: () => requests })
46
+ if (event.type === 'crash.requested') return crash()
47
+ }
48
+
49
+ // One listener per app in this process, while its agent works here.
50
+ const listening = new Map<string, Promise<void>>()
51
+
52
+ /**
53
+ * The dev tools' crash: while the app's agent works in this process, a
54
+ * request in its crash log ends the process at once, mid-stream or
55
+ * mid-command, and the scheduler resumes the turn in a fresh invocation.
56
+ * Each step calls this, so work resumed elsewhere gets a listener too.
57
+ */
58
+ export function crashOnRequest({
59
+ appId,
60
+ app,
61
+ log,
62
+ }: {
63
+ appId: string
64
+ app: AppSession
65
+ log: CrashLog
66
+ }): Promise<void> {
67
+ const current = listening.get(appId)
68
+ if (current) return current
69
+ const started = listen({ app, log }).finally(() => listening.delete(appId))
70
+ listening.set(appId, started)
71
+ return started
72
+ }