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.
- package/CHANGELOG.md +360 -0
- package/dist/{actor-ohPC-81x.d.ts → actor-DBbe5xeE.d.ts} +21 -11
- package/dist/actor-DBbe5xeE.d.ts.map +1 -0
- package/dist/actor-client.d.ts +2 -5
- package/dist/actor-client.d.ts.map +1 -1
- package/dist/actor-client.js +13 -4
- package/dist/actor-client.js.map +1 -1
- package/dist/actor-react.d.ts +5 -3
- package/dist/actor-react.d.ts.map +1 -1
- package/dist/actor-react.js +20 -10
- package/dist/actor-react.js.map +1 -1
- package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
- package/dist/actor-shared-t7w8xIKP.js.map +1 -0
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +42 -23
- package/dist/actor.js.map +1 -1
- package/dist/{ai-CrLYwNEx.js → ai-UrWfKeuv.js} +966 -541
- package/dist/ai-UrWfKeuv.js.map +1 -0
- package/dist/ai-client.d.ts +27 -0
- package/dist/ai-client.d.ts.map +1 -0
- package/dist/ai-client.js +29 -0
- package/dist/ai-client.js.map +1 -0
- package/dist/ai-react.d.ts +96 -0
- package/dist/ai-react.d.ts.map +1 -0
- package/dist/ai-react.js +142 -0
- package/dist/ai-react.js.map +1 -0
- package/dist/ai-server.browser.js +2 -2
- package/dist/ai-server.browser.js.map +1 -1
- package/dist/ai-server.d.ts +80 -36
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +466 -265
- package/dist/ai-server.js.map +1 -1
- package/dist/{ai-HJ9fHfYI.d.ts → ai-vpGtcaCB.d.ts} +115 -101
- package/dist/ai-vpGtcaCB.d.ts.map +1 -0
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BYzHjkwU.js → client-COaiaDub.js} +447 -80
- package/dist/client-COaiaDub.js.map +1 -0
- package/dist/{client-CzyacQpJ.d.ts → client-qRnRUzBS.d.ts} +14 -13
- package/dist/client-qRnRUzBS.d.ts.map +1 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.js +1 -1
- package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
- package/dist/contract-BYKASlCV.js.map +1 -0
- package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
- package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-BrLso5x_.d.ts} +7 -3
- package/dist/initial-snapshot-BrLso5x_.d.ts.map +1 -0
- package/dist/{internal-Dq2qYxou.js → internal-CbVvEwad.js} +23 -2
- package/dist/internal-CbVvEwad.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/react-BbJ-g6Uu.d.ts +86 -0
- package/dist/react-BbJ-g6Uu.d.ts.map +1 -0
- package/dist/react.d.ts +2 -122
- package/dist/react.js +107 -100
- package/dist/react.js.map +1 -1
- package/dist/reducer-BAXc8JyF.js +87 -0
- package/dist/reducer-BAXc8JyF.js.map +1 -0
- package/dist/{reducer-otzHjuJj.d.ts → reducer-BYu6DT7h.d.ts} +45 -29
- package/dist/reducer-BYu6DT7h.d.ts.map +1 -0
- package/dist/reducer-runtime-DQwz_J59.js +28 -0
- package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +33 -10
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -0
- package/dist/scheduler-vercel.d.ts +4 -9
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +25 -44
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-DbD7IJVK.d.ts → server-CGM8WzRG.d.ts} +87 -26
- package/dist/server-CGM8WzRG.d.ts.map +1 -0
- package/dist/{server-BD5ckxZb.js → server-Rj64fNvX.js} +854 -202
- package/dist/server-Rj64fNvX.js.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
- package/dist/session-clear-DGI4jTQ5.js.map +1 -0
- package/dist/session-clear-Dshpuhff.d.ts +12 -0
- package/dist/session-clear-Dshpuhff.d.ts.map +1 -0
- package/dist/{store-DyZM6fS5.d.ts → store-Gpkq7WBo.d.ts} +44 -3
- package/dist/store-Gpkq7WBo.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +182 -69
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-CmxUbV93.js → store-polling-4_y8lETm.js} +21 -4
- package/dist/store-polling-4_y8lETm.js.map +1 -0
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +399 -108
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-Q1C-k2dd.js} +3 -3
- package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-Q1C-k2dd.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.d.ts.map +1 -1
- package/dist/store-redis-http.js +293 -11
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis-notify-tqWNnMRu.js +2374 -0
- package/dist/store-redis-notify-tqWNnMRu.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +11 -11
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +166 -32
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-Cx7AAPGq.js +23 -0
- package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
- package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-cZH-cibj.d.ts} +2 -2
- package/dist/telemetry-cZH-cibj.d.ts.map +1 -0
- package/dist/{wire-BO5wWCb1.js → wire-DjfHOvdW.js} +132 -10
- package/dist/wire-DjfHOvdW.js.map +1 -0
- package/docs/01-quickstart.mdx +2 -2
- package/docs/actors/01-introduction.mdx +4 -0
- package/docs/actors/04-routes.mdx +14 -1
- package/docs/concepts/01-contracts.mdx +95 -0
- package/docs/concepts/04-state.mdx +3 -3
- package/docs/guides/01-timers.mdx +22 -14
- package/docs/guides/02-cancellation.mdx +1 -1
- package/docs/guides/03-react.mdx +268 -328
- package/docs/guides/04-local-first.mdx +32 -73
- package/docs/guides/05-production.mdx +40 -27
- package/docs/guides/06-ai-agents.mdx +680 -580
- package/docs/guides/09-presence.mdx +3 -7
- package/docs/guides/10-transports.mdx +5 -5
- package/docs/index.mdx +10 -18
- package/docs/reference/01-api.mdx +662 -195
- package/docs/reference/02-errors.mdx +2 -1
- package/examples/playground/DEPLOY.md +52 -6
- package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
- package/examples/playground/app/a0/[appId]/page.tsx +48 -0
- package/examples/playground/app/a0/a0.css +818 -0
- package/examples/playground/app/a0/app-list.tsx +211 -0
- package/examples/playground/app/a0/app-view.tsx +55 -0
- package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
- package/examples/playground/app/a0/apps/[appId]/route.ts +23 -0
- package/examples/playground/app/a0/apps.test.ts +116 -0
- package/examples/playground/app/a0/apps.ts +87 -0
- package/examples/playground/app/a0/chat.tsx +364 -0
- package/examples/playground/app/a0/columns.tsx +109 -0
- package/examples/playground/app/a0/context-menu.tsx +116 -0
- package/examples/playground/app/a0/cookies.ts +46 -0
- package/examples/playground/app/a0/crash-on-request.ts +72 -0
- package/examples/playground/app/a0/crashes/route.ts +3 -0
- package/examples/playground/app/a0/crashes.test.ts +33 -0
- package/examples/playground/app/a0/crashes.ts +28 -0
- package/examples/playground/app/a0/devtools.tsx +164 -0
- package/examples/playground/app/a0/events/route.ts +6 -0
- package/examples/playground/app/a0/flags.ts +30 -0
- package/examples/playground/app/a0/layout.tsx +38 -0
- package/examples/playground/app/a0/list/route.ts +10 -0
- package/examples/playground/app/a0/model.test.ts +56 -0
- package/examples/playground/app/a0/model.ts +58 -0
- package/examples/playground/app/a0/page.tsx +14 -0
- package/examples/playground/app/a0/preview-pane.tsx +145 -0
- package/examples/playground/app/a0/preview.test.ts +53 -0
- package/examples/playground/app/a0/preview.ts +26 -0
- package/examples/playground/app/a0/sandbox-log.ts +69 -0
- package/examples/playground/app/a0/sandbox.ts +191 -0
- package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
- package/examples/playground/app/a0/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/server.ts +158 -0
- package/examples/playground/app/a0/session.ts +47 -0
- package/examples/playground/app/a0/shell.tsx +150 -0
- package/examples/playground/app/a0/short/events/route.ts +9 -0
- package/examples/playground/app/a0/splitter.tsx +120 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
- package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
- package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
- package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
- package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
- package/examples/playground/app/agent/events/route.ts +5 -2
- package/examples/playground/app/agent/ingress.test.ts +98 -0
- package/examples/playground/app/agent/model-input.test.tsx +58 -0
- package/examples/playground/app/agent/model-input.tsx +115 -0
- package/examples/playground/app/agent/model.test.ts +73 -0
- package/examples/playground/app/agent/model.ts +58 -26
- package/examples/playground/app/agent/new-agent-session.tsx +37 -19
- package/examples/playground/app/agent/page.tsx +4 -4
- package/examples/playground/app/agent/server.ts +15 -14
- package/examples/playground/app/agent/session.ts +5 -6
- package/examples/playground/app/agent/telemetry.test.ts +168 -0
- package/examples/playground/app/agent/telemetry.ts +30 -0
- package/examples/playground/app/agent/trace.test.ts +23 -0
- package/examples/playground/app/agent/trace.ts +18 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +11 -11
- package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
- package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
- package/examples/playground/app/canvas/model.ts +53 -53
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
- package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
- package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
- package/examples/playground/app/chat/[chatId]/session.ts +2 -19
- package/examples/playground/app/chat/agent/route.ts +9 -0
- package/examples/playground/app/chat/model.test.ts +46 -119
- package/examples/playground/app/chat/model.ts +82 -128
- package/examples/playground/app/chat/scheduler/route.ts +2 -2
- package/examples/playground/app/chat/server.test.ts +108 -0
- package/examples/playground/app/chat/server.ts +69 -73
- package/examples/playground/app/components/activity-feed.tsx +30 -1
- package/examples/playground/app/components/activity-history.test.tsx +35 -0
- package/examples/playground/app/components/demo-icons.tsx +89 -0
- package/examples/playground/app/components/demos.tsx +33 -0
- package/examples/playground/app/components/fast-link.test.tsx +58 -0
- package/examples/playground/app/components/fast-link.tsx +55 -0
- package/examples/playground/app/components/site-header.tsx +96 -0
- package/examples/playground/app/counter/counter-client.tsx +3 -3
- package/examples/playground/app/counter/model.test.ts +3 -3
- package/examples/playground/app/counter/model.ts +11 -21
- package/examples/playground/app/counter/page.tsx +2 -15
- package/examples/playground/app/counter/session.ts +2 -2
- package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
- package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
- package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
- package/examples/playground/app/documents/[documentId]/session.ts +2 -2
- package/examples/playground/app/documents/model.ts +27 -27
- package/examples/playground/app/globals.css +420 -207
- package/examples/playground/app/initial-state.test.tsx +54 -0
- package/examples/playground/app/layout.tsx +7 -23
- package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
- package/examples/playground/app/orders/[orderId]/session.ts +2 -2
- package/examples/playground/app/orders/model.ts +49 -49
- package/examples/playground/app/page.tsx +13 -102
- package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
- package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
- package/examples/playground/app/recovery/model.ts +30 -30
- package/examples/playground/instrumentation.ts +5 -0
- package/examples/playground/package.json +10 -2
- package/examples/playground/scripts/a0-template.ts +77 -0
- package/examples/playground/vercel.json +18 -0
- package/package.json +3 -1
- package/src/actor-client.ts +16 -4
- package/src/actor-react.ts +25 -11
- package/src/actor-shared.ts +21 -6
- package/src/actor.ts +76 -31
- package/src/ai-agent-actions.ts +219 -0
- package/src/ai-agent.ts +2343 -0
- package/src/ai-app-state.ts +91 -0
- package/src/ai-client-state.ts +39 -2
- package/src/ai-client.ts +62 -0
- package/src/ai-context-schema.ts +25 -5
- package/src/ai-context.ts +90 -40
- package/src/ai-control-server.ts +94 -55
- package/src/ai-control.ts +2 -0
- package/src/ai-coordinator.ts +23 -22
- package/src/ai-message-base.ts +209 -0
- package/src/ai-message-projection.ts +382 -58
- package/src/ai-progress-batches.ts +26 -0
- package/src/ai-projector.ts +95 -14
- package/src/ai-react.ts +146 -0
- package/src/ai-server.browser.ts +2 -2
- package/src/ai-server.ts +20 -1951
- package/src/ai-stored-state.ts +48 -163
- package/src/ai.ts +343 -276
- package/src/bootstrap-stream.ts +72 -0
- package/src/bounded-cache.ts +55 -0
- package/src/client-render-session.ts +49 -0
- package/src/client.ts +688 -151
- package/src/contract.ts +92 -42
- package/src/deterministic-id.ts +37 -0
- package/src/equal-json.ts +32 -0
- package/src/errors.ts +13 -1
- package/src/index.ts +5 -1
- package/src/initial-snapshot.ts +39 -0
- package/src/internal.ts +45 -0
- package/src/postgres-notification-scope.ts +14 -3
- package/src/react.ts +171 -206
- package/src/redis-http-reply.ts +256 -0
- package/src/redis-http-stream.ts +73 -0
- package/src/redis-http-subscriptions.ts +13 -5
- package/src/reducer-runtime.ts +52 -0
- package/src/reducer.ts +92 -10
- package/src/scheduler-qstash.ts +49 -20
- package/src/scheduler-task.ts +34 -2
- package/src/scheduler-vercel.ts +32 -84
- package/src/server-fetch.ts +139 -35
- package/src/server.ts +957 -237
- package/src/session-clear.ts +23 -0
- package/src/session-socket.ts +154 -49
- package/src/sse.ts +71 -4
- package/src/store-memory.ts +220 -78
- package/src/store-polling.ts +18 -4
- package/src/store-postgres.ts +461 -114
- package/src/store-redis-core.ts +1260 -434
- package/src/store-redis-http.ts +18 -2
- package/src/store-redis-notify.ts +260 -85
- package/src/store-redis.ts +19 -9
- package/src/store-sqlite.ts +259 -45
- package/src/store.ts +46 -1
- package/src/stream-buffer.ts +43 -0
- package/src/telemetry.ts +13 -1
- package/src/wire.ts +241 -13
- package/dist/actor-ohPC-81x.d.ts.map +0 -1
- package/dist/actor-shared-B5tJfzt-.js.map +0 -1
- package/dist/ai-CrLYwNEx.js.map +0 -1
- package/dist/ai-HJ9fHfYI.d.ts.map +0 -1
- package/dist/client-BYzHjkwU.js.map +0 -1
- package/dist/client-CzyacQpJ.d.ts.map +0 -1
- package/dist/contract-CKRg_E4q.js.map +0 -1
- package/dist/errors-DCk6ch5n.js.map +0 -1
- package/dist/errors-DvhSXnxk.d.ts.map +0 -1
- package/dist/internal-Dq2qYxou.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/reducer-DEMjEY_O.js +0 -29
- package/dist/reducer-DEMjEY_O.js.map +0 -1
- package/dist/reducer-otzHjuJj.d.ts.map +0 -1
- package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
- package/dist/server-BD5ckxZb.js.map +0 -1
- package/dist/server-DbD7IJVK.d.ts.map +0 -1
- package/dist/store-DyZM6fS5.d.ts.map +0 -1
- package/dist/store-polling-CmxUbV93.js.map +0 -1
- package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
- package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
- package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
- package/dist/validate-XKT4FSNn.js +0 -30
- package/dist/validate-XKT4FSNn.js.map +0 -1
- package/dist/wire-BO5wWCb1.js.map +0 -1
- package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
- package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
- package/examples/playground/app/agent/compaction-settings.ts +0 -49
- package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
- 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
|
+
}
|