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,211 @@
1
+ 'use client'
2
+ import type { ReactNode } from 'react'
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import { useSearchParams } from 'next/navigation'
5
+ import { Asterisk, Plus } from 'lucide-react'
6
+ import { FastLink } from '../components/fast-link'
7
+ import { type App, timeAgo } from './apps'
8
+ import { ContextMenu, type MenuItem } from './context-menu'
9
+ import { queryString } from './flags'
10
+
11
+ /** The clock the list's times read from: the server's at first, then ticking. */
12
+ function useNow(renderedAt: number): number {
13
+ const [now, setNow] = useState(renderedAt)
14
+ useEffect(() => {
15
+ const timer = setInterval(() => setNow(Date.now()), 30_000)
16
+ return () => clearInterval(timer)
17
+ }, [])
18
+ return now
19
+ }
20
+
21
+ export function AppList({
22
+ apps,
23
+ currentId,
24
+ renderedAt,
25
+ newAppId,
26
+ onRename,
27
+ onDelete,
28
+ }: {
29
+ /** Most recently prompted first. */
30
+ apps: App[]
31
+ currentId: string | null
32
+ renderedAt: number
33
+ newAppId: string
34
+ onRename: (app: { id: string; name: string }) => void
35
+ onDelete: (app: App) => void
36
+ }): ReactNode {
37
+ const now = useNow(renderedAt)
38
+ // Links keep the dev tools' state from app to app.
39
+ const params = useSearchParams()
40
+ const query = queryString(new URLSearchParams(params))
41
+ // Open dev tools also read each page's log, so they don't prefetch every
42
+ // app's in full.
43
+ const prefetch = params.has('devtools') ? null : true
44
+ return (
45
+ <>
46
+ <div className="a0-label">
47
+ <span>Apps</span>
48
+ <span className="a0-rule" />
49
+ <FastLink
50
+ href={`/a0/${newAppId}${query}`}
51
+ prefetch={prefetch}
52
+ className="a0-icon-button"
53
+ aria-label="New app"
54
+ title="New app"
55
+ >
56
+ <Plus size={14} />
57
+ </FastLink>
58
+ </div>
59
+ <nav aria-label="Apps">
60
+ {apps.map((app) => (
61
+ <AppItem
62
+ key={app.id}
63
+ app={app}
64
+ when={timeAgo({ at: app.promptedAt, now })}
65
+ current={app.id === currentId}
66
+ href={`/a0/${app.id}${query}`}
67
+ prefetch={prefetch}
68
+ onRename={(name) => onRename({ id: app.id, name })}
69
+ onDelete={() => onDelete(app)}
70
+ />
71
+ ))}
72
+ </nav>
73
+ </>
74
+ )
75
+ }
76
+
77
+ const under = (row: HTMLElement): { x: number; y: number } => {
78
+ const box = row.getBoundingClientRect()
79
+ return { x: box.left + 16, y: box.bottom }
80
+ }
81
+
82
+ /**
83
+ * Clicking the current app renames it in place. Right-clicking any app, or
84
+ * its context-menu key, opens its menu.
85
+ */
86
+ function AppItem({
87
+ app,
88
+ when,
89
+ current,
90
+ href,
91
+ prefetch,
92
+ onRename,
93
+ onDelete,
94
+ }: {
95
+ app: App
96
+ when: string
97
+ current: boolean
98
+ href: string
99
+ prefetch: boolean | null
100
+ onRename: (name: string) => void
101
+ onDelete: () => void
102
+ }): ReactNode {
103
+ const [editing, setEditing] = useState(false)
104
+ const [menu, setMenu] = useState<{ x: number; y: number } | null>(null)
105
+ const row = useRef<HTMLDivElement>(null)
106
+ const cancelled = useRef(false)
107
+ const rename = (): void => {
108
+ cancelled.current = false
109
+ setEditing(true)
110
+ }
111
+ const link = (): string => new URL(href, location.origin).href
112
+ const items: MenuItem[] = [
113
+ { label: 'Open in new tab', onSelect: () => window.open(link(), '_blank') },
114
+ {
115
+ label: 'Copy link',
116
+ onSelect: () => void navigator.clipboard.writeText(link()),
117
+ },
118
+ { label: 'Rename', onSelect: rename },
119
+ { label: 'Delete', onSelect: onDelete, danger: true },
120
+ ]
121
+ const icon = <Asterisk size={14} aria-hidden />
122
+ const lines = (
123
+ <span className="a0-app-lines">
124
+ <span className="a0-app-name">{app.name}</span>
125
+ <span className="a0-app-when">{when}</span>
126
+ </span>
127
+ )
128
+
129
+ if (editing)
130
+ return (
131
+ <div className="a0-app" data-current>
132
+ {icon}
133
+ <input
134
+ aria-label={`Rename ${app.name}`}
135
+ defaultValue={app.name}
136
+ maxLength={80}
137
+ autoFocus
138
+ onFocus={(event) => event.currentTarget.select()}
139
+ onBlur={(event) => {
140
+ setEditing(false)
141
+ const next = event.currentTarget.value.trim()
142
+ if (!cancelled.current && next && next !== app.name) onRename(next)
143
+ }}
144
+ onKeyDown={(event) => {
145
+ if (event.key === 'Enter') event.currentTarget.blur()
146
+ if (event.key === 'Escape') {
147
+ cancelled.current = true
148
+ event.currentTarget.blur()
149
+ }
150
+ }}
151
+ />
152
+ </div>
153
+ )
154
+
155
+ return (
156
+ <div
157
+ ref={row}
158
+ className="a0-app"
159
+ data-current={current || undefined}
160
+ data-menu={menu ? '' : undefined}
161
+ onContextMenu={(event) => {
162
+ event.preventDefault()
163
+ // The keyboard's menu key has no pointer, so it opens under the row.
164
+ setMenu(
165
+ event.clientX || event.clientY
166
+ ? { x: event.clientX, y: event.clientY }
167
+ : under(event.currentTarget),
168
+ )
169
+ }}
170
+ // Not every browser turns these keys into a contextmenu event.
171
+ onKeyDown={(event) => {
172
+ if (
173
+ event.key !== 'ContextMenu' &&
174
+ !(event.key === 'F10' && event.shiftKey)
175
+ )
176
+ return
177
+ event.preventDefault()
178
+ setMenu(under(event.currentTarget))
179
+ }}
180
+ >
181
+ {current ? (
182
+ <button
183
+ type="button"
184
+ className="a0-app-open"
185
+ aria-current="page"
186
+ title="Rename"
187
+ onClick={rename}
188
+ >
189
+ {icon}
190
+ {lines}
191
+ </button>
192
+ ) : (
193
+ <FastLink className="a0-app-open" href={href} prefetch={prefetch}>
194
+ {icon}
195
+ {lines}
196
+ </FastLink>
197
+ )}
198
+ {menu && (
199
+ <ContextMenu
200
+ at={menu}
201
+ items={items}
202
+ onClose={({ returnFocus }) => {
203
+ setMenu(null)
204
+ if (returnFocus)
205
+ row.current?.querySelector<HTMLElement>('.a0-app-open')?.focus()
206
+ }}
207
+ />
208
+ )}
209
+ </div>
210
+ )
211
+ }
@@ -0,0 +1,55 @@
1
+ 'use client'
2
+ import type { ReactNode } from 'react'
3
+ import { useEffect } from 'react'
4
+ import { useSearchParams } from 'next/navigation'
5
+ import { titleFromPrompt } from './apps'
6
+ import { Chat } from './chat'
7
+ import { ColumnEdge } from './columns'
8
+ import { checkApp, PreviewPane } from './preview-pane'
9
+ import { useSession } from './session'
10
+ import { useApps } from './shell'
11
+
12
+ /** One app: its chat with the agent, and the app it builds. */
13
+ export function AppView({
14
+ appId,
15
+ devtools,
16
+ }: {
17
+ appId: string
18
+ /** Shown while the query has `devtools`. */
19
+ devtools: ReactNode
20
+ }): ReactNode {
21
+ const { listed, prompted, opened } = useApps()
22
+ const { state } = useSession()
23
+ const showDevtools = useSearchParams().has('devtools')
24
+ const app = listed(appId)
25
+ const prompt = state.messages
26
+ .find((message) => message.role === 'user')
27
+ ?.parts.flatMap((part) => (part.type === 'text' ? [part.text] : []))
28
+ .join(' ')
29
+ const name = app?.name ?? (prompt ? titleFromPrompt(prompt) : 'New app')
30
+ // A new app's sandbox opens on sight, so it's ready by the first prompt;
31
+ // if none comes, its idle check deletes it.
32
+ const newApp = state.messages.length === 0
33
+ useEffect(() => {
34
+ if (!newApp) return
35
+ opened(appId)
36
+ checkApp({ appId, newApp }).catch(() => {})
37
+ }, [appId])
38
+
39
+ return (
40
+ <>
41
+ <section aria-label="Chat" className="a0-chat">
42
+ <h1 className="a0-title">{name}</h1>
43
+ <Chat
44
+ onPrompt={(text) =>
45
+ prompted({ id: appId, name: titleFromPrompt(prompt ?? text) })
46
+ }
47
+ autoFocus={!app}
48
+ />
49
+ <ColumnEdge column="chat" />
50
+ </section>
51
+ <PreviewPane appId={appId} />
52
+ {showDevtools && devtools}
53
+ </>
54
+ )
55
+ }
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod'
2
+ import { isAppId } from '../../../apps'
3
+ import { ensureApp } from '../../../sandbox'
4
+ import { checkWhenIdle } from '../../../sandbox-log'
5
+
6
+ const body = z.object({ newApp: z.boolean().default(false) })
7
+
8
+ /**
9
+ * Makes sure the app is up, and says whether it answers. The browser calls
10
+ * it while the app is on screen, which keeps its sandbox running. A new
11
+ * app's sandbox also gets its idle check; the check itself decides, so a
12
+ * wrong `newApp` costs nothing.
13
+ */
14
+ export async function POST(
15
+ request: Request,
16
+ context: { params: Promise<{ appId: string }> },
17
+ ): Promise<Response> {
18
+ const { appId } = await context.params
19
+ if (!isAppId(appId))
20
+ return Response.json({ error: 'Unknown app.' }, { status: 404 })
21
+ const { newApp } = body
22
+ .catch({ newApp: false })
23
+ .parse(await request.json().catch(() => ({})))
24
+ const { up, createdAt } = await ensureApp({ appId })
25
+ if (newApp) await checkWhenIdle({ appId, createdAt })
26
+ return Response.json({ up }, { headers: { 'cache-control': 'no-store' } })
27
+ }
@@ -0,0 +1,23 @@
1
+ import { inputs } from 'experimental-a2/ai'
2
+ import { isAppId } from '../../apps'
3
+ import { deleteAppSandbox } from '../../sandbox'
4
+ import { agentServer } from '../../server'
5
+
6
+ /**
7
+ * Stops the agent's current turn, so no later tool call brings the sandbox
8
+ * back, then deletes it. The conversation stays in the A2 log.
9
+ */
10
+ export async function DELETE(
11
+ _request: Request,
12
+ context: { params: Promise<{ appId: string }> },
13
+ ): Promise<Response> {
14
+ const { appId } = await context.params
15
+ if (!isAppId(appId))
16
+ return Response.json({ error: 'Unknown app.' }, { status: 404 })
17
+ const session = agentServer.session(appId)
18
+ const { state } = await session.state()
19
+ if (state.active)
20
+ await session.append(...inputs.stop({ turnId: state.active.turnId }))
21
+ await deleteAppSandbox({ appId })
22
+ return Response.json({ ok: true })
23
+ }
@@ -0,0 +1,116 @@
1
+ import type { ContractEvent } from 'experimental-a2'
2
+ import { expect, it } from 'vitest'
3
+ import {
4
+ type App,
5
+ appList,
6
+ byLatestPrompt,
7
+ isAppId,
8
+ timeAgo,
9
+ titleFromPrompt,
10
+ } from './apps'
11
+ import { parseWidths, serializeWidths } from './cookies'
12
+
13
+ type ListEvents = typeof appList.events
14
+
15
+ const first = '6f1c1c3e-2f7a-4f0e-9d4e-5b8a9f1d2c3b'
16
+ const second = '0d9a8b7c-6e5f-4a3b-8c2d-1e0f9a8b7c6d'
17
+
18
+ const fold = (
19
+ ...events: [type: keyof ListEvents & string, payload: object, at: number][]
20
+ ): App[] =>
21
+ events.reduce(
22
+ (apps, [type, payload, at], index) =>
23
+ appList.reducer.fold(apps, {
24
+ id: `event-${index}`,
25
+ type,
26
+ payload,
27
+ index: index + 1,
28
+ sessionId: 'list-1',
29
+ createdAt: new Date(at),
30
+ } as ContractEvent<ListEvents>),
31
+ appList.reducer.initialState,
32
+ )
33
+
34
+ it('lists apps with the server time of their latest prompt', () => {
35
+ expect(
36
+ fold(
37
+ ['app.created', { appId: first, name: 'todo app' }, 1_000],
38
+ ['app.created', { appId: second, name: 'counter' }, 2_000],
39
+ ['app.prompted', { appId: first }, 3_000],
40
+ ),
41
+ ).toEqual([
42
+ { id: first, name: 'todo app', promptedAt: 3_000 },
43
+ { id: second, name: 'counter', promptedAt: 2_000 },
44
+ ])
45
+ })
46
+
47
+ it('renames and deletes apps, and lists each one once', () => {
48
+ expect(
49
+ fold(
50
+ ['app.created', { appId: first, name: 'todo app' }, 1_000],
51
+ ['app.created', { appId: first, name: 'again' }, 2_000],
52
+ ['app.renamed', { appId: first, name: 'Tasks' }, 3_000],
53
+ ['app.created', { appId: second, name: 'counter' }, 4_000],
54
+ ['app.deleted', { appId: second }, 5_000],
55
+ ),
56
+ ).toEqual([{ id: first, name: 'Tasks', promptedAt: 1_000 }])
57
+ })
58
+
59
+ it('sorts by the latest prompt first', () => {
60
+ const apps = [
61
+ { id: first, name: 'older', promptedAt: 1_000 },
62
+ { id: second, name: 'newer', promptedAt: 2_000 },
63
+ ]
64
+ expect(byLatestPrompt(apps).map((app) => app.name)).toEqual([
65
+ 'newer',
66
+ 'older',
67
+ ])
68
+ })
69
+
70
+ it('rejects names and ids the list would not show', () => {
71
+ const created = appList.events['app.created']
72
+ const valid = (payload: object): boolean =>
73
+ !('issues' in created['~standard'].validate(payload))
74
+ expect(valid({ appId: first, name: 'todo app' })).toBe(true)
75
+ expect(valid({ appId: first, name: ' ' })).toBe(false)
76
+ expect(valid({ appId: first, name: 'x'.repeat(81) })).toBe(false)
77
+ expect(valid({ appId: 'not-a-uuid', name: 'todo app' })).toBe(false)
78
+ expect(isAppId(first)).toBe(true)
79
+ expect(isAppId('not-a-uuid')).toBe(false)
80
+ })
81
+
82
+ it('names an app after the first line of its prompt, cut to fit', () => {
83
+ expect(titleFromPrompt(' make a todo app\nwith tags ')).toBe(
84
+ 'make a todo app',
85
+ )
86
+ const long = titleFromPrompt(
87
+ 'build a landing page for a coffee shop called Ember with a menu',
88
+ )
89
+ expect(long).toBe('build a landing page for a coffee shop…')
90
+ expect(long.length).toBeLessThanOrEqual(40)
91
+ })
92
+
93
+ it('says how long ago in words', () => {
94
+ const now = Date.UTC(2026, 8, 26, 12)
95
+ const ago = (ms: number): string => timeAgo({ at: now - ms, now })
96
+ expect(ago(-5_000)).toBe('just now')
97
+ expect(ago(30_000)).toBe('just now')
98
+ expect(ago(60_000)).toBe('1 minute ago')
99
+ expect(ago(5 * 60_000)).toBe('5 minutes ago')
100
+ expect(ago(3 * 60 * 60_000)).toBe('3 hours ago')
101
+ expect(ago(30 * 60 * 60_000)).toBe('yesterday')
102
+ expect(ago(10 * 24 * 60 * 60_000)).toBe('last week')
103
+ expect(ago(400 * 24 * 60 * 60_000)).toBe('last year')
104
+ })
105
+
106
+ it('reads the widths someone dragged to, and ignores bad ones', () => {
107
+ expect(parseWidths(undefined)).toEqual({ apps: 216, chat: null })
108
+ expect(parseWidths('240,520')).toEqual({ apps: 240, chat: 520 })
109
+ expect(parseWidths('240')).toEqual({ apps: 240, chat: null })
110
+ expect(parseWidths('9999,10')).toEqual({ apps: 216, chat: null })
111
+ expect(parseWidths('abc,def')).toEqual({ apps: 216, chat: null })
112
+ expect(parseWidths(serializeWidths({ apps: 200, chat: 480 }))).toEqual({
113
+ apps: 200,
114
+ chat: 480,
115
+ })
116
+ })
@@ -0,0 +1,87 @@
1
+ import { contract } from 'experimental-a2'
2
+ import { z } from 'zod'
3
+
4
+ export const isAppId = (value: string): boolean =>
5
+ z.uuid().safeParse(value).success
6
+
7
+ /**
8
+ * An app is one conversation with the coding agent and one sandbox. It's
9
+ * listed with the time of its latest prompt, as the server recorded it.
10
+ */
11
+ export type App = { id: string; name: string; promptedAt: number }
12
+
13
+ const appId = z.uuid()
14
+ const name = z.string().trim().min(1).max(80)
15
+
16
+ /**
17
+ * One browser's apps. Each app is a session of its own; this one lists the
18
+ * apps this browser has prompted, since knowing an app's id is what lets you
19
+ * open it.
20
+ */
21
+ export const appList = contract({
22
+ name: 'a0-apps',
23
+ events: {
24
+ 'app.created': z.object({ appId, name }),
25
+ 'app.prompted': z.object({ appId }),
26
+ 'app.renamed': z.object({ appId, name }),
27
+ 'app.deleted': z.object({ appId }),
28
+ },
29
+ reducer: {
30
+ name: 'a0-apps.v1',
31
+ initialState: [] as App[],
32
+ fold: (apps, event): App[] => {
33
+ const { appId: id } = event.payload
34
+ const promptedAt = event.createdAt.getTime()
35
+ switch (event.type) {
36
+ case 'app.created':
37
+ return apps.some((app) => app.id === id)
38
+ ? apps
39
+ : [...apps, { id, name: event.payload.name, promptedAt }]
40
+ case 'app.prompted':
41
+ return apps.map((app) =>
42
+ app.id === id ? { ...app, promptedAt } : app,
43
+ )
44
+ case 'app.renamed': {
45
+ const { name: renamed } = event.payload
46
+ return apps.map((app) =>
47
+ app.id === id ? { ...app, name: renamed } : app,
48
+ )
49
+ }
50
+ case 'app.deleted':
51
+ return apps.filter((app) => app.id !== id)
52
+ }
53
+ },
54
+ },
55
+ })
56
+
57
+ export const byLatestPrompt = (apps: readonly App[]): App[] =>
58
+ apps.toSorted((a, b) => b.promptedAt - a.promptedAt)
59
+
60
+ const maxTitle = 40
61
+
62
+ /** An app's name from its prompt: the first line, cut to fit. */
63
+ export function titleFromPrompt(prompt: string): string {
64
+ const line = (prompt.trim().split('\n')[0] ?? '').replace(/\s+/g, ' ')
65
+ return line.length > maxTitle
66
+ ? `${line.slice(0, maxTitle - 1).trimEnd()}…`
67
+ : line
68
+ }
69
+
70
+ const relative = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
71
+ const units = [
72
+ ['year', 365 * 24 * 60 * 60_000],
73
+ ['month', 30 * 24 * 60 * 60_000],
74
+ ['week', 7 * 24 * 60 * 60_000],
75
+ ['day', 24 * 60 * 60_000],
76
+ ['hour', 60 * 60_000],
77
+ ['minute', 60_000],
78
+ ] as const
79
+
80
+ /** How long ago, in words: "just now", "5 minutes ago", "yesterday". */
81
+ export function timeAgo({ at, now }: { at: number; now: number }): string {
82
+ const elapsed = now - at
83
+ for (const [unit, size] of units)
84
+ if (elapsed >= size)
85
+ return relative.format(-Math.floor(elapsed / size), unit)
86
+ return 'just now'
87
+ }