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,89 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ /** A 24px line icon: ink strokes, with its `accent` parts in orange. */
4
+ function Glyph({ children }: { children: ReactNode }): ReactNode {
5
+ return (
6
+ <svg
7
+ viewBox="0 0 24 24"
8
+ fill="none"
9
+ stroke="currentColor"
10
+ strokeWidth={1.5}
11
+ strokeLinecap="square"
12
+ aria-hidden
13
+ >
14
+ {children}
15
+ </svg>
16
+ )
17
+ }
18
+
19
+ export const A0Icon = (): ReactNode => (
20
+ <Glyph>
21
+ <rect x="3" y="4" width="18" height="16" />
22
+ <path d="M3 8h18" />
23
+ <path className="accent" d="M7 12l2.5 2L7 16M12 16h5" />
24
+ </Glyph>
25
+ )
26
+
27
+ export const OrdersIcon = (): ReactNode => (
28
+ <Glyph>
29
+ <path d="M3 4h18v5H3zM5 9v11h14V9" />
30
+ <path className="accent" d="M10 13h4" />
31
+ </Glyph>
32
+ )
33
+
34
+ export const ChatIcon = (): ReactNode => (
35
+ <Glyph>
36
+ <path d="M3 4h18v13H10l-4 3.5V17H3z" />
37
+ <rect className="fill" x="7" y="10" width="2" height="2" />
38
+ <rect className="fill" x="11" y="10" width="2" height="2" />
39
+ <rect className="accent-fill" x="15" y="10" width="2" height="2" />
40
+ </Glyph>
41
+ )
42
+
43
+ export const AgentIcon = (): ReactNode => (
44
+ <Glyph>
45
+ <rect x="3" y="3" width="18" height="18" />
46
+ <path className="accent" d="M12 7v10M7.7 9.5l8.6 5M7.7 14.5l8.6-5" />
47
+ </Glyph>
48
+ )
49
+
50
+ export const CanvasIcon = (): ReactNode => (
51
+ <Glyph>
52
+ <rect x="3" y="3" width="10" height="10" />
53
+ <circle cx="13" cy="13" r="5" />
54
+ <path className="accent-fill" d="M13 13l8.5 3.4-3.6 1.3-1.3 3.6z" />
55
+ </Glyph>
56
+ )
57
+
58
+ export const DocumentsIcon = (): ReactNode => (
59
+ <Glyph>
60
+ <path d="M5 3h10l4 4v14H5z" />
61
+ <path d="M8 10h8M8 14h4M8 18h7" />
62
+ <path className="accent" d="M14 12v4" />
63
+ </Glyph>
64
+ )
65
+
66
+ export const RecoveryIcon = (): ReactNode => (
67
+ <Glyph>
68
+ <path d="M20.5 12a8.5 8.5 0 1 1-2.5-6l2.5 2.5" />
69
+ <path d="M20.5 3.5v5h-5" />
70
+ <rect className="accent-fill" x="10" y="10" width="4" height="4" />
71
+ </Glyph>
72
+ )
73
+
74
+ export const VaultIcon = (): ReactNode => (
75
+ <Glyph>
76
+ <rect x="3" y="4" width="18" height="15" />
77
+ <circle cx="11" cy="11.5" r="3.5" />
78
+ <path className="accent" d="M11 11.5l2-2" />
79
+ <path d="M17.5 9.5v4M6 19v2M18 19v2" />
80
+ </Glyph>
81
+ )
82
+
83
+ export const CounterIcon = (): ReactNode => (
84
+ <Glyph>
85
+ <rect x="3" y="6" width="18" height="12" />
86
+ <path className="accent" d="M6.5 12h5M9 9.5v5" />
87
+ <path d="M16.5 9v6M15 10.5L16.5 9" />
88
+ </Glyph>
89
+ )
@@ -0,0 +1,33 @@
1
+ import type { ReactNode } from 'react'
2
+ import {
3
+ A0Icon,
4
+ AgentIcon,
5
+ CanvasIcon,
6
+ ChatIcon,
7
+ CounterIcon,
8
+ DocumentsIcon,
9
+ OrdersIcon,
10
+ RecoveryIcon,
11
+ VaultIcon,
12
+ } from './demo-icons'
13
+
14
+ export type Demo = {
15
+ href: string
16
+ name: string
17
+ icon: ReactNode
18
+ /** The demo fills the window and places the site nav itself. */
19
+ fullWindow?: true
20
+ }
21
+
22
+ /** Every demo, in the order the header menu and the home page list them. */
23
+ export const demos: Demo[] = [
24
+ { href: '/a0', name: 'A0', icon: <A0Icon />, fullWindow: true },
25
+ { href: '/orders', name: 'Orders', icon: <OrdersIcon /> },
26
+ { href: '/chat', name: 'Chat', icon: <ChatIcon /> },
27
+ { href: '/agent', name: 'Agent', icon: <AgentIcon /> },
28
+ { href: '/canvas', name: 'Canvas', icon: <CanvasIcon /> },
29
+ { href: '/documents', name: 'Documents', icon: <DocumentsIcon /> },
30
+ { href: '/recovery', name: 'Recovery', icon: <RecoveryIcon /> },
31
+ { href: '/vault', name: 'Vault', icon: <VaultIcon /> },
32
+ { href: '/counter', name: 'Counter', icon: <CounterIcon /> },
33
+ ]
@@ -0,0 +1,58 @@
1
+ // @vitest-environment jsdom
2
+ import {
3
+ cleanup,
4
+ createEvent,
5
+ fireEvent,
6
+ render,
7
+ screen,
8
+ } from '@testing-library/react'
9
+ import { afterEach, beforeEach, expect, it, vi } from 'vitest'
10
+ import { FastLink } from './fast-link'
11
+
12
+ const push = vi.fn<(href: string) => void>()
13
+ vi.mock('next/navigation', () => ({ useRouter: () => ({ push }) }))
14
+
15
+ // Whether each click reached the browser's default; then stop jsdom's
16
+ // unimplemented navigation.
17
+ let navigatedByClick: boolean[] = []
18
+ const record = (event: MouseEvent): void => {
19
+ navigatedByClick.push(!event.defaultPrevented)
20
+ event.preventDefault()
21
+ }
22
+ beforeEach(() => {
23
+ push.mockReset()
24
+ navigatedByClick = []
25
+ document.addEventListener('click', record)
26
+ })
27
+ afterEach(() => {
28
+ document.removeEventListener('click', record)
29
+ cleanup()
30
+ })
31
+
32
+ const link = () => screen.getByRole('link', { name: 'Apps' })
33
+ const press = (): void => {
34
+ const event = createEvent.pointerDown(link())
35
+ // jsdom has no PointerEvent, so the plain event gets its fields here.
36
+ Object.defineProperties(event, {
37
+ pointerType: { value: 'mouse' },
38
+ button: { value: 0 },
39
+ })
40
+ fireEvent(link(), event)
41
+ }
42
+
43
+ it('navigates on the press and swallows only that click', () => {
44
+ render(<FastLink href="/a0">Apps</FastLink>)
45
+ press()
46
+ expect(push).toHaveBeenCalledWith('/a0')
47
+ fireEvent.click(link(), { detail: 1 })
48
+ fireEvent.click(link(), { detail: 1 })
49
+ expect(navigatedByClick).toEqual([false, true])
50
+ })
51
+
52
+ it('lets a keyboard activation through after a press that never clicked', () => {
53
+ render(<FastLink href="/a0">Apps</FastLink>)
54
+ press()
55
+ // The pointer left before release, so no click followed the press.
56
+ fireEvent.click(link(), { detail: 0 })
57
+ expect(navigatedByClick).toEqual([true])
58
+ })
@@ -0,0 +1,55 @@
1
+ 'use client'
2
+ import type { ComponentProps, ReactNode } from 'react'
3
+ import { useRef } from 'react'
4
+ import Link from 'next/link'
5
+ import { useRouter } from 'next/navigation'
6
+
7
+ /**
8
+ * A Link that navigates on mouse down instead of on click, which saves the
9
+ * press-to-release time. Anything else keeps the browser's behavior: modified
10
+ * presses (open in a new tab or window), other buttons, and touch, where a
11
+ * press can still turn into a scroll.
12
+ */
13
+ export function FastLink({
14
+ href,
15
+ onPointerDown,
16
+ onClick,
17
+ ...props
18
+ }: Omit<ComponentProps<typeof Link>, 'href'> & { href: string }): ReactNode {
19
+ const router = useRouter()
20
+ const navigated = useRef(false)
21
+ return (
22
+ <Link
23
+ href={href}
24
+ {...props}
25
+ onPointerDown={(event) => {
26
+ onPointerDown?.(event)
27
+ navigated.current = false
28
+ if (
29
+ event.defaultPrevented ||
30
+ event.pointerType !== 'mouse' ||
31
+ event.button !== 0 ||
32
+ event.metaKey ||
33
+ event.ctrlKey ||
34
+ event.shiftKey ||
35
+ event.altKey ||
36
+ props.target
37
+ )
38
+ return
39
+ navigated.current = true
40
+ router.push(href)
41
+ }}
42
+ onClick={(event) => {
43
+ onClick?.(event)
44
+ const pressed = navigated.current
45
+ navigated.current = false
46
+ // The press already navigated, so its click doesn't navigate again. A
47
+ // keyboard activation (detail 0) is never that click, even when an
48
+ // earlier press never got one.
49
+ if (pressed && event.detail !== 0) {
50
+ event.preventDefault()
51
+ }
52
+ }}
53
+ />
54
+ )
55
+ }
@@ -0,0 +1,96 @@
1
+ 'use client'
2
+ import type { FocusEvent, ReactNode } from 'react'
3
+ import { useEffect, useRef } from 'react'
4
+ import { FastLink } from './fast-link'
5
+ import { usePathname } from 'next/navigation'
6
+ import { Asterisk, ChevronDown } from 'lucide-react'
7
+ import { type Demo, demos } from './demos'
8
+
9
+ const close = (details: HTMLDetailsElement | null): void =>
10
+ details?.removeAttribute('open')
11
+
12
+ function useCurrentDemo(): { pathname: string; current: Demo | undefined } {
13
+ const pathname = usePathname()
14
+ const current = demos.find(
15
+ (demo) => pathname === demo.href || pathname.startsWith(`${demo.href}/`),
16
+ )
17
+ return { pathname, current }
18
+ }
19
+
20
+ /** The site header, unless the demo lays out the whole window itself. */
21
+ export function SiteHeader(): ReactNode {
22
+ const { current } = useCurrentDemo()
23
+ if (current?.fullWindow) return null
24
+ return (
25
+ <header className="site-header">
26
+ <SiteNav />
27
+ </header>
28
+ )
29
+ }
30
+
31
+ /** The wordmark, then a menu that switches between the demos. */
32
+ export function SiteNav(): ReactNode {
33
+ const { pathname, current } = useCurrentDemo()
34
+ const menu = useRef<HTMLDetailsElement>(null)
35
+
36
+ useEffect(() => {
37
+ const outside = (event: PointerEvent): void => {
38
+ if (!menu.current?.contains(event.target as Node)) close(menu.current)
39
+ }
40
+ const escape = (event: KeyboardEvent): void => {
41
+ const details = menu.current
42
+ if (event.key !== 'Escape' || !details?.open) return
43
+ const hadFocus = details.contains(document.activeElement)
44
+ close(details)
45
+ if (hadFocus) details.querySelector('summary')?.focus()
46
+ }
47
+ document.addEventListener('pointerdown', outside)
48
+ document.addEventListener('keydown', escape)
49
+ return () => {
50
+ document.removeEventListener('pointerdown', outside)
51
+ document.removeEventListener('keydown', escape)
52
+ }
53
+ }, [])
54
+
55
+ const leave = (event: FocusEvent<HTMLDetailsElement>): void => {
56
+ if (!event.currentTarget.contains(event.relatedTarget)) {
57
+ close(event.currentTarget)
58
+ }
59
+ }
60
+
61
+ return (
62
+ <>
63
+ <FastLink href="/" className="brand">
64
+ <Asterisk size={18} strokeWidth={2.5} aria-hidden />
65
+ <span>A2</span>
66
+ </FastLink>
67
+ <details ref={menu} className="demo-menu" onBlur={leave}>
68
+ <summary>
69
+ <span aria-hidden>/</span>
70
+ <span className="sr-only">Demo:</span>
71
+ {current?.name ?? 'Playground'}
72
+ <ChevronDown size={13} aria-hidden />
73
+ </summary>
74
+ <nav aria-label="Demos">
75
+ {demos.map((demo) => (
76
+ <FastLink
77
+ key={demo.href}
78
+ href={demo.href}
79
+ aria-current={
80
+ demo !== current
81
+ ? undefined
82
+ : pathname === demo.href
83
+ ? 'page'
84
+ : 'true'
85
+ }
86
+ onClick={() => close(menu.current)}
87
+ >
88
+ {demo.name}
89
+ {demo === current ? <Asterisk size={12} aria-hidden /> : null}
90
+ </FastLink>
91
+ ))}
92
+ </nav>
93
+ </details>
94
+ </>
95
+ )
96
+ }
@@ -34,9 +34,9 @@ export function CounterClient(): ReactNode {
34
34
  <span className="badge live">shared</span>
35
35
  </header>
36
36
  <p className="lede">
37
- Every click appends one fact to a durable log. This page folds those
38
- facts into a value on the server for the first paint, then keeps folding
39
- the live stream in your browser.
37
+ Every click appends one fact to a durable log. The subscription starts
38
+ with the current value from the server, then keeps folding live events
39
+ in your browser.
40
40
  </p>
41
41
 
42
42
  <section className="counter-panel" aria-label="Shared counter controls">
@@ -1,6 +1,6 @@
1
1
  import type { ContractEvent } from 'experimental-a2'
2
2
  import { describe, expect, it } from 'vitest'
3
- import { counter, counterReducer } from './model'
3
+ import { counter } from './model'
4
4
 
5
5
  type CounterEvents = typeof counter.events
6
6
  type CounterEvent<
@@ -27,8 +27,8 @@ describe('counter reducer', () => {
27
27
  event(2, 'incremented'),
28
28
  event(3, 'decremented'),
29
29
  ].reduce(
30
- (current, nextEvent) => counterReducer.fold(current, nextEvent),
31
- counterReducer.initialState,
30
+ (current, nextEvent) => counter.reducer.fold(current, nextEvent),
31
+ counter.reducer.initialState,
32
32
  )
33
33
 
34
34
  expect(state).toEqual({ value: 1 })
@@ -1,31 +1,21 @@
1
- import * as a2 from 'experimental-a2'
1
+ import { contract } from 'experimental-a2'
2
2
  import { z } from 'zod'
3
3
 
4
- export const counter = a2.contract({
4
+ const counterState = z.object({ value: z.number().int() })
5
+ export type CounterState = z.output<typeof counterState>
6
+
7
+ export const counter = contract({
5
8
  name: 'counter',
6
9
  events: {
7
10
  incremented: z.object({}),
8
11
  decremented: z.object({}),
9
12
  },
10
- })
11
-
12
- const counterState = z.object({ value: z.number().int() })
13
-
14
- export type CounterState = z.output<typeof counterState>
15
-
16
- export const counterReducer = counter
17
- .reducer({
13
+ reducer: {
18
14
  name: 'counter-value',
19
15
  initialState: { value: 0 },
20
16
  stateSchema: counterState,
21
- })
22
- .fold((state, event) => {
23
- switch (event.type) {
24
- case 'incremented':
25
- return { value: state.value + 1 }
26
- case 'decremented':
27
- return { value: state.value - 1 }
28
- default:
29
- return state
30
- }
31
- })
17
+ fold: (state, event) => ({
18
+ value: state.value + (event.type === 'incremented' ? 1 : -1),
19
+ }),
20
+ },
21
+ })
@@ -1,23 +1,10 @@
1
1
  import type { ReactNode } from 'react'
2
- import { counterReducer } from './model'
3
- import { counterServer } from './server'
4
2
  import { CounterClient } from './counter-client'
5
3
  import { SessionProvider } from './session'
6
4
 
7
- export const dynamic = 'force-dynamic'
8
-
9
- const COUNTER_SESSION_ID = 'shared'
10
-
11
- export default async function CounterPage(): Promise<ReactNode> {
12
- const session = counterServer.session(COUNTER_SESSION_ID)
13
- const { state, index } = await session.state(counterReducer)
14
-
5
+ export default function CounterPage(): ReactNode {
15
6
  return (
16
- <SessionProvider
17
- sessionId={COUNTER_SESSION_ID}
18
- initialState={state}
19
- initialIndex={index}
20
- >
7
+ <SessionProvider sessionId="shared">
21
8
  <CounterClient />
22
9
  </SessionProvider>
23
10
  )
@@ -1,10 +1,10 @@
1
1
  'use client'
2
2
  import { createClient } from 'experimental-a2/client'
3
3
  import { createReact } from 'experimental-a2/react'
4
- import { counterReducer } from './model'
4
+ import { counter } from './model'
5
5
 
6
6
  export const counterClient = createClient({
7
- reducer: counterReducer,
7
+ contract: counter,
8
8
  api: '/counter/events',
9
9
  })
10
10
 
@@ -23,10 +23,14 @@ export function CodeMirrorEditor({
23
23
  const host = useRef<HTMLDivElement>(null)
24
24
  const view = useRef<EditorView | null>(null)
25
25
  const [editable] = useState(() => new Compartment())
26
+ const [theme] = useState(() => new Compartment())
26
27
 
27
28
  useEffect(() => {
28
29
  if (!host.current) return
29
30
 
31
+ // CodeMirror's own defaults (cursor, search panel, tooltips) follow the
32
+ // system theme along with the page.
33
+ const dark = window.matchMedia('(prefers-color-scheme: dark)')
30
34
  const body = doc.getText('body')
31
35
  const editor = new EditorView({
32
36
  parent: host.current,
@@ -34,6 +38,7 @@ export function CodeMirrorEditor({
34
38
  doc: body.toString(),
35
39
  extensions: [
36
40
  basicSetup,
41
+ theme.of(EditorView.darkTheme.of(dark.matches)),
37
42
  EditorView.lineWrapping,
38
43
  EditorView.contentAttributes.of({
39
44
  'aria-label': 'Document body',
@@ -60,12 +65,18 @@ export function CodeMirrorEditor({
60
65
  }),
61
66
  })
62
67
  view.current = editor
68
+ const followTheme = (): void =>
69
+ editor.dispatch({
70
+ effects: theme.reconfigure(EditorView.darkTheme.of(dark.matches)),
71
+ })
72
+ dark.addEventListener('change', followTheme)
63
73
 
64
74
  return () => {
75
+ dark.removeEventListener('change', followTheme)
65
76
  editor.destroy()
66
77
  view.current = null
67
78
  }
68
- }, [doc, awareness, editable])
79
+ }, [doc, awareness, editable, theme])
69
80
 
70
81
  useEffect(() => {
71
82
  view.current?.dispatch({
@@ -1,4 +1,5 @@
1
1
  'use client'
2
+ import type { StateSnapshot } from 'experimental-a2'
2
3
  import type { CSSProperties, ChangeEvent, ReactNode } from 'react'
3
4
  import {
4
5
  useCallback,
@@ -25,11 +26,11 @@ const ADJECTIVES = ['Bright', 'Calm', 'Curious', 'Kind', 'Swift'] as const
25
26
  const ANIMALS = ['Fox', 'Heron', 'Otter', 'Panda', 'Wren'] as const
26
27
  const COLORS = ['teal', 'amber', 'blue', 'violet', 'rose'] as const
27
28
  const COLOR_HEX: Record<DocumentActor['color'], string> = {
28
- teal: '#16836a',
29
- amber: '#bd7917',
30
- blue: '#3874c8',
31
- violet: '#7956b6',
32
- rose: '#bd5269',
29
+ teal: '#3ccf9f',
30
+ amber: '#f2b54b',
31
+ blue: '#6ea8fe',
32
+ violet: '#a98bff',
33
+ rose: '#ff7a93',
33
34
  }
34
35
 
35
36
  type SyncState = 'saved' | 'saving' | 'error'
@@ -154,12 +155,10 @@ function useYText(doc: Y.Doc, name: string): string {
154
155
 
155
156
  export function DocumentSession({
156
157
  documentId,
157
- initialState,
158
- initialIndex,
158
+ initial,
159
159
  }: {
160
160
  documentId: string
161
- initialState: DocumentState
162
- initialIndex: number
161
+ initial: StateSnapshot<DocumentState>
163
162
  }): ReactNode {
164
163
  // Minted before the provider connects, so the session carries its
165
164
  // participant id from the first frame. The rendered UI keeps its
@@ -172,8 +171,8 @@ export function DocumentSession({
172
171
  <SessionProvider
173
172
  sessionId={documentId}
174
173
  api="/documents/events"
175
- initialState={initialState}
176
- initialIndex={initialIndex}
174
+ initial={initial}
175
+
177
176
  {...(identity ? { participant: identity.id } : {})}
178
177
  >
179
178
  <DocumentClient documentId={documentId} />
@@ -16,8 +16,7 @@ export default async function DocumentPage({
16
16
  return (
17
17
  <DocumentSession
18
18
  documentId={documentId}
19
- initialState={state}
20
- initialIndex={index}
19
+ initial={{ state: state, index: index }}
21
20
  />
22
21
  )
23
22
  }
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
  import { createReact } from 'experimental-a2/react'
3
- import { documentsReducer } from '../model'
3
+ import { documents } from '../model'
4
4
 
5
5
  export const { SessionProvider, useSession } = createReact({
6
- reducer: documentsReducer,
6
+ contract: documents,
7
7
  })
@@ -11,6 +11,13 @@ export const documentActorSchema = z.object({
11
11
 
12
12
  export type DocumentActor = z.output<typeof documentActorSchema>
13
13
 
14
+ const documentState = z.object({
15
+ updates: z.array(z.string()),
16
+ contributors: z.array(documentActorSchema),
17
+ })
18
+
19
+ export type DocumentState = z.output<typeof documentState>
20
+
14
21
  export const documents = a2.contract({
15
22
  name: 'documents',
16
23
  events: {
@@ -22,34 +29,27 @@ export const documents = a2.contract({
22
29
  presence: {
23
30
  awareness: z.string().min(1),
24
31
  },
25
- })
26
32
 
27
- const documentState = z.object({
28
- updates: z.array(z.string()),
29
- contributors: z.array(documentActorSchema),
30
- })
31
-
32
- export type DocumentState = z.output<typeof documentState>
33
-
34
- export const documentsReducer = documents
35
- .reducer({
33
+ reducer: {
36
34
  name: 'document-updates',
37
35
  initialState: { updates: [], contributors: [] },
38
36
  stateSchema: documentState,
39
- })
40
- .fold((state, event) => {
41
- switch (event.type) {
42
- case 'document.updated':
43
- return {
44
- updates: [...state.updates, event.payload.update],
45
- contributors: [
46
- ...state.contributors.filter(
47
- (actor) => actor.id !== event.payload.actor.id,
48
- ),
49
- event.payload.actor,
50
- ],
51
- }
52
- default:
53
- return state
54
- }
55
- })
37
+ fold: (state, event) => {
38
+ switch (event.type) {
39
+ case 'document.updated':
40
+ return {
41
+ updates: [...state.updates, event.payload.update],
42
+ contributors: [
43
+ ...state.contributors.filter(
44
+ (actor) => actor.id !== event.payload.actor.id,
45
+ ),
46
+ event.payload.actor,
47
+ ],
48
+ }
49
+ default:
50
+ return state
51
+ }
52
+ },
53
+ },
54
+ })
55
+ export const documentsReducer = documents.reducer