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,120 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
const STEP = 16
|
|
6
|
+
|
|
7
|
+
const clamp = ({
|
|
8
|
+
width,
|
|
9
|
+
min,
|
|
10
|
+
max,
|
|
11
|
+
}: {
|
|
12
|
+
width: number
|
|
13
|
+
min: number
|
|
14
|
+
max: number
|
|
15
|
+
}): number => Math.round(Math.max(min, Math.min(Math.max(min, max), width)))
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The right edge of the column it sits in, which resizes that column: drag
|
|
19
|
+
* it, or focus it and use the arrow keys.
|
|
20
|
+
*/
|
|
21
|
+
export function Splitter({
|
|
22
|
+
label,
|
|
23
|
+
min,
|
|
24
|
+
max,
|
|
25
|
+
onResize,
|
|
26
|
+
}: {
|
|
27
|
+
label: string
|
|
28
|
+
min: number
|
|
29
|
+
/** The widest the column can be right now. */
|
|
30
|
+
max: () => number
|
|
31
|
+
/** Each new width while dragging, then the settled one, with `done`. */
|
|
32
|
+
onResize: (resize: { width: number; done: boolean }) => void
|
|
33
|
+
}): ReactNode {
|
|
34
|
+
const [range, setRange] = useState<{ now: number; max: number } | null>(null)
|
|
35
|
+
const [dragging, setDragging] = useState(false)
|
|
36
|
+
const drag = useRef<{
|
|
37
|
+
startX: number
|
|
38
|
+
start: number
|
|
39
|
+
max: number
|
|
40
|
+
latest: number
|
|
41
|
+
} | null>(null)
|
|
42
|
+
const measure = (handle: HTMLElement): number =>
|
|
43
|
+
handle.parentElement?.getBoundingClientRect().width ?? min
|
|
44
|
+
const settle = (next: number): void => {
|
|
45
|
+
setRange({ now: next, max: Math.round(max()) })
|
|
46
|
+
onResize({ width: next, done: true })
|
|
47
|
+
}
|
|
48
|
+
// Capture ends on its own when the pointer lifts or is cancelled.
|
|
49
|
+
const end = (): void => {
|
|
50
|
+
if (!drag.current) return
|
|
51
|
+
const { latest } = drag.current
|
|
52
|
+
drag.current = null
|
|
53
|
+
setDragging(false)
|
|
54
|
+
settle(latest)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
role="separator"
|
|
60
|
+
aria-orientation="vertical"
|
|
61
|
+
aria-label={label}
|
|
62
|
+
aria-valuemin={min}
|
|
63
|
+
aria-valuenow={range?.now}
|
|
64
|
+
aria-valuemax={range?.max}
|
|
65
|
+
tabIndex={0}
|
|
66
|
+
className="a0-splitter"
|
|
67
|
+
// CSS reads this to keep the cursor and pointer events off iframes,
|
|
68
|
+
// which would otherwise swallow the drag.
|
|
69
|
+
data-dragging={dragging || undefined}
|
|
70
|
+
onFocus={(event) =>
|
|
71
|
+
setRange({
|
|
72
|
+
now: Math.round(measure(event.currentTarget)),
|
|
73
|
+
max: Math.round(max()),
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
onKeyDown={(event) => {
|
|
77
|
+
const step = event.shiftKey ? STEP * 4 : STEP
|
|
78
|
+
const delta =
|
|
79
|
+
event.key === 'ArrowLeft'
|
|
80
|
+
? -step
|
|
81
|
+
: event.key === 'ArrowRight'
|
|
82
|
+
? step
|
|
83
|
+
: 0
|
|
84
|
+
if (!delta) return
|
|
85
|
+
event.preventDefault()
|
|
86
|
+
settle(
|
|
87
|
+
clamp({
|
|
88
|
+
width: measure(event.currentTarget) + delta,
|
|
89
|
+
min,
|
|
90
|
+
max: max(),
|
|
91
|
+
}),
|
|
92
|
+
)
|
|
93
|
+
}}
|
|
94
|
+
onPointerDown={(event) => {
|
|
95
|
+
if (event.button !== 0) return
|
|
96
|
+
event.preventDefault()
|
|
97
|
+
event.currentTarget.setPointerCapture(event.pointerId)
|
|
98
|
+
const start = Math.round(measure(event.currentTarget))
|
|
99
|
+
drag.current = {
|
|
100
|
+
startX: event.clientX,
|
|
101
|
+
start,
|
|
102
|
+
max: max(),
|
|
103
|
+
latest: start,
|
|
104
|
+
}
|
|
105
|
+
setDragging(true)
|
|
106
|
+
}}
|
|
107
|
+
onPointerMove={(event) => {
|
|
108
|
+
const current = drag.current
|
|
109
|
+
if (!current) return
|
|
110
|
+
current.latest = clamp({
|
|
111
|
+
width: current.start + event.clientX - current.startX,
|
|
112
|
+
min,
|
|
113
|
+
max: current.max,
|
|
114
|
+
})
|
|
115
|
+
onResize({ width: current.latest, done: false })
|
|
116
|
+
}}
|
|
117
|
+
onLostPointerCapture={end}
|
|
118
|
+
/>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -4,20 +4,21 @@ import type {
|
|
|
4
4
|
KeyboardEvent as ReactKeyboardEvent,
|
|
5
5
|
ReactNode,
|
|
6
6
|
} from 'react'
|
|
7
|
-
import { useEffect, useRef, useState } from 'react'
|
|
7
|
+
import { useEffect, useId, useRef, useState } from 'react'
|
|
8
8
|
import type { UIMessage } from 'ai'
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
type PendingApproval,
|
|
13
|
-
} from 'experimental-a2/ai'
|
|
9
|
+
import { isCompactionPart } from 'experimental-a2/ai'
|
|
10
|
+
import { useAgent, type AgentIndicator } from 'experimental-a2/ai/react'
|
|
11
|
+
import type { InitialSnapshot } from 'experimental-a2'
|
|
14
12
|
import { ActivityFeed } from '@/app/components/activity-feed'
|
|
15
13
|
import { ConnectionPill } from '@/app/components/connection-pill'
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
14
|
+
import { agentClient } from '../session'
|
|
15
|
+
import type { AgentState } from '../model'
|
|
16
|
+
import { ModelInput } from '../model-input'
|
|
18
17
|
import { CompactionEvent } from './compaction-event'
|
|
19
18
|
import { CompactionPanel } from './compaction-panel'
|
|
20
19
|
import { AgentQueue } from './agent-queue'
|
|
20
|
+
import { CommandReceipt } from './command-receipt'
|
|
21
|
+
import { traceUrl } from '../trace'
|
|
21
22
|
|
|
22
23
|
type MessagePart = UIMessage['parts'][number]
|
|
23
24
|
type ToolPartView = {
|
|
@@ -110,34 +111,34 @@ function ToolCard({ part }: { part: ToolPartView }): ReactNode {
|
|
|
110
111
|
|
|
111
112
|
function MessagePartView({ part }: { part: MessagePart }): ReactNode {
|
|
112
113
|
if (part.type === 'text') return part.text ? <p>{part.text}</p> : null
|
|
114
|
+
if (isCompactionPart(part)) return <CompactionEvent data={part.data} />
|
|
113
115
|
const tool = toolPartView(part)
|
|
114
116
|
if (tool) return <ToolCard part={tool} />
|
|
115
117
|
return null
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
function Thinking({ starting = false }: { starting?: boolean }): ReactNode {
|
|
120
|
+
function Thinking({
|
|
121
|
+
indicator,
|
|
122
|
+
}: {
|
|
123
|
+
indicator: Exclude<AgentIndicator, null>
|
|
124
|
+
}): ReactNode {
|
|
126
125
|
return (
|
|
127
126
|
<p className="agent-thinking" role="status">
|
|
128
127
|
<span aria-hidden="true" />
|
|
129
|
-
{starting ? 'Starting…' : 'Thinking…'}
|
|
128
|
+
{indicator === 'starting' ? 'Starting…' : 'Thinking…'}
|
|
130
129
|
</p>
|
|
131
130
|
)
|
|
132
131
|
}
|
|
133
132
|
|
|
134
133
|
function AgentMessage({
|
|
135
134
|
message,
|
|
136
|
-
|
|
135
|
+
indicator,
|
|
137
136
|
}: {
|
|
138
137
|
message: UIMessage
|
|
139
|
-
|
|
138
|
+
indicator: AgentIndicator
|
|
140
139
|
}): ReactNode {
|
|
140
|
+
const traceHref =
|
|
141
|
+
message.role === 'assistant' ? traceUrl(message.metadata) : undefined
|
|
141
142
|
const role =
|
|
142
143
|
message.role === 'user'
|
|
143
144
|
? message.id.startsWith('reminder:')
|
|
@@ -152,7 +153,17 @@ function AgentMessage({
|
|
|
152
153
|
{message.parts.map((part, partIndex) => (
|
|
153
154
|
<MessagePartView key={`${message.id}:${partIndex}`} part={part} />
|
|
154
155
|
))}
|
|
155
|
-
{
|
|
156
|
+
{indicator ? <Thinking indicator={indicator} /> : null}
|
|
157
|
+
{traceHref ? (
|
|
158
|
+
<a
|
|
159
|
+
className="agent-trace"
|
|
160
|
+
href={traceHref}
|
|
161
|
+
target="_blank"
|
|
162
|
+
rel="noreferrer"
|
|
163
|
+
>
|
|
164
|
+
View trace ↗
|
|
165
|
+
</a>
|
|
166
|
+
) : null}
|
|
156
167
|
</div>
|
|
157
168
|
</div>
|
|
158
169
|
)
|
|
@@ -170,27 +181,41 @@ function submitOnEnter(event: ReactKeyboardEvent<HTMLTextAreaElement>): void {
|
|
|
170
181
|
event.currentTarget.form?.requestSubmit()
|
|
171
182
|
}
|
|
172
183
|
|
|
173
|
-
export function AgentClient({
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
export function AgentClient({
|
|
185
|
+
agentId,
|
|
186
|
+
initial,
|
|
187
|
+
}: {
|
|
188
|
+
agentId: string
|
|
189
|
+
initial: InitialSnapshot<AgentState>
|
|
190
|
+
}): ReactNode {
|
|
191
|
+
const agent = useAgent({ client: agentClient, id: agentId, initial })
|
|
192
|
+
const { state, messages, indicator } = agent
|
|
193
|
+
const promptId = useId()
|
|
176
194
|
const promptRef = useRef<HTMLTextAreaElement>(null)
|
|
177
195
|
const [prompt, setPrompt] = useState('')
|
|
178
196
|
const [error, setError] = useState<string | null>(null)
|
|
179
197
|
const [responding, setResponding] = useState(false)
|
|
180
|
-
|
|
181
|
-
const generating = state.status === 'generating'
|
|
182
|
-
const compacting = state.compaction?.status === 'running'
|
|
198
|
+
const [stopCommandIds, setStopCommandIds] = useState<string[]>([])
|
|
183
199
|
const canSend = state.status !== 'closed'
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
200
|
+
// The indicator joins the response it belongs to, or opens a row for it.
|
|
201
|
+
const thinking =
|
|
202
|
+
indicator === 'starting' || indicator === 'thinking' ? indicator : null
|
|
203
|
+
const last = messages.at(-1)
|
|
204
|
+
const joinsLast = thinking !== null && last?.role === 'assistant'
|
|
189
205
|
|
|
190
206
|
useEffect(() => {
|
|
191
207
|
if (canSend) promptRef.current?.focus({ preventScroll: true })
|
|
192
208
|
}, [canSend])
|
|
193
209
|
|
|
210
|
+
const run = async (action: () => Promise<unknown>): Promise<void> => {
|
|
211
|
+
setError(null)
|
|
212
|
+
try {
|
|
213
|
+
await action()
|
|
214
|
+
} catch (err) {
|
|
215
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
194
219
|
const send = async (event?: FormEvent, steer = false): Promise<void> => {
|
|
195
220
|
event?.preventDefault()
|
|
196
221
|
const submittedPrompt = prompt
|
|
@@ -200,77 +225,22 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
200
225
|
promptRef.current?.focus({ preventScroll: true })
|
|
201
226
|
setError(null)
|
|
202
227
|
try {
|
|
203
|
-
|
|
204
|
-
id: crypto.randomUUID(),
|
|
205
|
-
role: 'user',
|
|
206
|
-
parts: [{ type: 'text', text }],
|
|
207
|
-
}
|
|
208
|
-
await push(
|
|
209
|
-
...(steer && state.active
|
|
210
|
-
? inputs.steer({
|
|
211
|
-
turnId: state.active.turnId,
|
|
212
|
-
message,
|
|
213
|
-
})
|
|
214
|
-
: inputs.message(message)),
|
|
215
|
-
)
|
|
228
|
+
await (steer ? agent.steer(text) : agent.sendMessage(text))
|
|
216
229
|
} catch (err) {
|
|
217
230
|
setPrompt((current) => (current === '' ? submittedPrompt : current))
|
|
218
231
|
setError(err instanceof Error ? err.message : String(err))
|
|
219
232
|
}
|
|
220
233
|
}
|
|
221
234
|
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}: {
|
|
226
|
-
approval: PendingApproval
|
|
227
|
-
approved: boolean
|
|
228
|
-
}): Promise<void> => {
|
|
229
|
-
setResponding(true)
|
|
230
|
-
setError(null)
|
|
231
|
-
try {
|
|
232
|
-
await push(
|
|
233
|
-
...inputs.approval({
|
|
234
|
-
requestId: approval.requestId,
|
|
235
|
-
messageId: approval.messageId,
|
|
236
|
-
generationId: approval.generationId,
|
|
237
|
-
approvalId: approval.approvalId,
|
|
238
|
-
approved,
|
|
239
|
-
...(!approved ? { reason: 'operator denied the restart' } : {}),
|
|
240
|
-
}),
|
|
241
|
-
)
|
|
242
|
-
} catch (err) {
|
|
243
|
-
setError(err instanceof Error ? err.message : String(err))
|
|
244
|
-
} finally {
|
|
245
|
-
setResponding(false)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const command = async (
|
|
250
|
-
commands: {
|
|
251
|
-
type: 'ai.control.requested'
|
|
252
|
-
payload: ControlCommand
|
|
253
|
-
id?: string
|
|
254
|
-
}[],
|
|
255
|
-
): Promise<void> => {
|
|
256
|
-
setError(null)
|
|
257
|
-
try {
|
|
258
|
-
await push(...commands)
|
|
259
|
-
} catch (err) {
|
|
260
|
-
setError(err instanceof Error ? err.message : String(err))
|
|
261
|
-
}
|
|
235
|
+
const stop = async (): Promise<void> => {
|
|
236
|
+
const stops = await agent.stop()
|
|
237
|
+
setStopCommandIds((ids) => [...ids, ...stops.map((event) => event.id)])
|
|
262
238
|
}
|
|
263
239
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
await push(
|
|
269
|
-
...inputs.stop({ turnId: state.active.turnId, lastSeenIndex: index }),
|
|
270
|
-
)
|
|
271
|
-
} catch (err) {
|
|
272
|
-
setError(err instanceof Error ? err.message : String(err))
|
|
273
|
-
}
|
|
240
|
+
const answer = async (action: () => Promise<unknown>): Promise<void> => {
|
|
241
|
+
setResponding(true)
|
|
242
|
+
await run(action)
|
|
243
|
+
setResponding(false)
|
|
274
244
|
}
|
|
275
245
|
|
|
276
246
|
return (
|
|
@@ -279,62 +249,44 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
279
249
|
<h1>
|
|
280
250
|
Agent <code>{agentId}</code>
|
|
281
251
|
</h1>
|
|
282
|
-
<ConnectionPill connection={connection} />
|
|
252
|
+
<ConnectionPill connection={agent.connection} />
|
|
283
253
|
<span
|
|
284
|
-
className={`badge${generating ? ' live' : ''}${
|
|
285
|
-
|
|
286
|
-
}${
|
|
254
|
+
className={`badge${agent.status === 'generating' ? ' live' : ''}${
|
|
255
|
+
agent.status === 'waiting' ? ' waiting' : ''
|
|
256
|
+
}${agent.status === 'failed' ? ' cut' : ''}`}
|
|
287
257
|
>
|
|
288
|
-
{
|
|
258
|
+
{indicator === 'starting'
|
|
289
259
|
? 'starting…'
|
|
290
|
-
: compacting
|
|
260
|
+
: indicator === 'compacting'
|
|
291
261
|
? 'compacting…'
|
|
292
|
-
: STATUS_LABEL[
|
|
262
|
+
: STATUS_LABEL[agent.status]}
|
|
293
263
|
</span>
|
|
294
264
|
</header>
|
|
295
265
|
|
|
296
|
-
<CompactionPanel
|
|
266
|
+
<CompactionPanel key={agentId} session={agent} />
|
|
297
267
|
|
|
298
268
|
<section className="agent-conversation" aria-label="Conversation">
|
|
299
269
|
<div className="agent-transcript" aria-live="polite">
|
|
300
|
-
{
|
|
270
|
+
{messages.length === 0 && thinking === null ? (
|
|
301
271
|
<p className="agent-empty">
|
|
302
272
|
Ask the agent to investigate an incident, run a Bash command, or
|
|
303
273
|
set a reminder for itself. Its tools and decisions will appear
|
|
304
274
|
here as they enter the log.
|
|
305
275
|
</p>
|
|
306
276
|
) : (
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
thinking={
|
|
315
|
-
generating &&
|
|
316
|
-
!state.starting &&
|
|
317
|
-
!compacting &&
|
|
318
|
-
entry.lastSegment &&
|
|
319
|
-
entry.message.id ===
|
|
320
|
-
state.activeGeneration?.responseMessageId &&
|
|
321
|
-
!hasVisibleParts(entry.message)
|
|
322
|
-
}
|
|
323
|
-
/>
|
|
324
|
-
),
|
|
325
|
-
)
|
|
277
|
+
messages.map((message) => (
|
|
278
|
+
<AgentMessage
|
|
279
|
+
key={message.id}
|
|
280
|
+
message={message}
|
|
281
|
+
indicator={joinsLast && message === last ? thinking : null}
|
|
282
|
+
/>
|
|
283
|
+
))
|
|
326
284
|
)}
|
|
327
|
-
{
|
|
328
|
-
(generating &&
|
|
329
|
-
!compacting &&
|
|
330
|
-
state.messages.at(-1)?.role === 'user' &&
|
|
331
|
-
!state.messages.some(
|
|
332
|
-
(message) => message.id === responseMessageId,
|
|
333
|
-
)) ? (
|
|
285
|
+
{thinking !== null && !joinsLast ? (
|
|
334
286
|
<div className="agent-message assistant">
|
|
335
287
|
<span className="agent-role">Agent</span>
|
|
336
288
|
<div>
|
|
337
|
-
<Thinking
|
|
289
|
+
<Thinking indicator={thinking} />
|
|
338
290
|
</div>
|
|
339
291
|
</div>
|
|
340
292
|
) : null}
|
|
@@ -354,9 +306,7 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
354
306
|
<button
|
|
355
307
|
type="button"
|
|
356
308
|
disabled={responding}
|
|
357
|
-
onClick={() =>
|
|
358
|
-
void answerApproval({ approval, approved: true })
|
|
359
|
-
}
|
|
309
|
+
onClick={() => void answer(() => agent.approve(approval))}
|
|
360
310
|
>
|
|
361
311
|
Approve restart
|
|
362
312
|
</button>
|
|
@@ -365,7 +315,11 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
365
315
|
className="secondary"
|
|
366
316
|
disabled={responding}
|
|
367
317
|
onClick={() =>
|
|
368
|
-
void
|
|
318
|
+
void answer(() =>
|
|
319
|
+
agent.deny(approval, {
|
|
320
|
+
reason: 'operator denied the restart',
|
|
321
|
+
}),
|
|
322
|
+
)
|
|
369
323
|
}
|
|
370
324
|
>
|
|
371
325
|
Deny
|
|
@@ -381,45 +335,52 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
381
335
|
) : null}
|
|
382
336
|
|
|
383
337
|
<AgentQueue
|
|
384
|
-
items={inbox.items}
|
|
385
|
-
paused={inbox.paused}
|
|
386
|
-
remove={
|
|
387
|
-
|
|
388
|
-
}}
|
|
389
|
-
sendNow={async (inputId) => {
|
|
390
|
-
await command(
|
|
391
|
-
inputs.queue.sendNow({
|
|
392
|
-
inputId,
|
|
393
|
-
turnId: state.active?.turnId ?? null,
|
|
394
|
-
}),
|
|
395
|
-
)
|
|
396
|
-
}}
|
|
338
|
+
items={state.inbox.items}
|
|
339
|
+
paused={state.inbox.paused}
|
|
340
|
+
remove={(inputId) => run(() => agent.queue.remove(inputId))}
|
|
341
|
+
sendNow={(inputId) => run(() => agent.queue.sendNow(inputId))}
|
|
397
342
|
edit={async (message) => {
|
|
398
|
-
await
|
|
343
|
+
await agent.queue.edit(message)
|
|
399
344
|
}}
|
|
400
345
|
move={(inputId, beforeId) =>
|
|
401
|
-
void
|
|
346
|
+
void run(() => agent.queue.move({ inputId, beforeId }))
|
|
402
347
|
}
|
|
403
348
|
/>
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
</p>
|
|
409
|
-
) : null}
|
|
349
|
+
<CommandReceipt
|
|
350
|
+
receipt={state.receipt}
|
|
351
|
+
stopCommandIds={stopCommandIds}
|
|
352
|
+
/>
|
|
410
353
|
|
|
411
354
|
<form className="agent-form" onSubmit={(event) => void send(event)}>
|
|
412
|
-
<
|
|
413
|
-
Message
|
|
414
|
-
<
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
355
|
+
<div className="agent-message-field">
|
|
356
|
+
<label htmlFor={promptId}>Message</label>
|
|
357
|
+
<div className="agent-composer">
|
|
358
|
+
<textarea
|
|
359
|
+
id={promptId}
|
|
360
|
+
ref={promptRef}
|
|
361
|
+
rows={3}
|
|
362
|
+
placeholder="Message the agent…"
|
|
363
|
+
value={prompt}
|
|
364
|
+
onChange={(event) => setPrompt(event.target.value)}
|
|
365
|
+
onKeyDown={submitOnEnter}
|
|
366
|
+
disabled={!canSend}
|
|
367
|
+
/>
|
|
368
|
+
<div className="agent-composer-toolbar">
|
|
369
|
+
<ModelInput
|
|
370
|
+
value={state.modelChoice}
|
|
371
|
+
onChange={(next) =>
|
|
372
|
+
void run(() =>
|
|
373
|
+
agent.push({
|
|
374
|
+
type: 'model.choice.updated',
|
|
375
|
+
payload: next,
|
|
376
|
+
}),
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
disabled={!canSend}
|
|
380
|
+
/>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
423
384
|
<div className="agent-form-actions">
|
|
424
385
|
<button type="submit" disabled={!canSend || !prompt.trim()}>
|
|
425
386
|
Send to agent
|
|
@@ -428,12 +389,13 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
428
389
|
type="button"
|
|
429
390
|
className="danger"
|
|
430
391
|
disabled={!state.active}
|
|
431
|
-
onClick={() => void stop
|
|
392
|
+
onClick={() => void run(stop)}
|
|
432
393
|
>
|
|
433
394
|
Stop
|
|
434
395
|
</button>
|
|
435
396
|
<button
|
|
436
397
|
type="button"
|
|
398
|
+
className="secondary"
|
|
437
399
|
disabled={!canSend || !prompt.trim()}
|
|
438
400
|
onClick={() => void send(undefined, true)}
|
|
439
401
|
>
|
|
@@ -442,7 +404,8 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
442
404
|
{state.inbox.paused ? (
|
|
443
405
|
<button
|
|
444
406
|
type="button"
|
|
445
|
-
|
|
407
|
+
className="secondary"
|
|
408
|
+
onClick={() => void run(agent.resume)}
|
|
446
409
|
>
|
|
447
410
|
Resume
|
|
448
411
|
</button>
|
|
@@ -450,15 +413,17 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
450
413
|
<>
|
|
451
414
|
<button
|
|
452
415
|
type="button"
|
|
416
|
+
className="secondary"
|
|
453
417
|
onClick={() =>
|
|
454
|
-
void
|
|
418
|
+
void run(() => agent.pause({ when: 'after-turn' }))
|
|
455
419
|
}
|
|
456
420
|
>
|
|
457
421
|
Pause after turn
|
|
458
422
|
</button>
|
|
459
423
|
<button
|
|
460
424
|
type="button"
|
|
461
|
-
|
|
425
|
+
className="secondary"
|
|
426
|
+
onClick={() => void run(() => agent.pause({ when: 'now' }))}
|
|
462
427
|
>
|
|
463
428
|
Pause now
|
|
464
429
|
</button>
|
|
@@ -469,12 +434,17 @@ export function AgentClient({ agentId }: { agentId: string }): ReactNode {
|
|
|
469
434
|
</section>
|
|
470
435
|
{error ? <p className="error">{error}</p> : null}
|
|
471
436
|
<p className="hint">
|
|
472
|
-
The browser only appends inputs. <code>
|
|
473
|
-
|
|
474
|
-
|
|
437
|
+
The browser only appends inputs. <code>agent()</code> records the
|
|
438
|
+
generation, progress snapshots, tool activity, and pause; approving or
|
|
439
|
+
delivering a reminder wakes the same durable session.
|
|
475
440
|
</p>
|
|
476
441
|
|
|
477
|
-
<ActivityFeed
|
|
442
|
+
<ActivityFeed
|
|
443
|
+
events={agent.events}
|
|
444
|
+
index={agent.index}
|
|
445
|
+
history={agent.history}
|
|
446
|
+
loadHistory={agent.loadHistory}
|
|
447
|
+
/>
|
|
478
448
|
</article>
|
|
479
449
|
)
|
|
480
450
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { cleanup, render, screen } from '@testing-library/react'
|
|
3
|
+
import { afterEach, expect, it } from 'vitest'
|
|
4
|
+
import { CommandReceipt } from './command-receipt'
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup)
|
|
7
|
+
|
|
8
|
+
it('reports a Stop that arrived after its turn finished as a status', () => {
|
|
9
|
+
render(
|
|
10
|
+
<CommandReceipt
|
|
11
|
+
receipt={{
|
|
12
|
+
commandId: 'stop-1',
|
|
13
|
+
outcome: 'rejected',
|
|
14
|
+
reason: 'stale-turn',
|
|
15
|
+
}}
|
|
16
|
+
stopCommandIds={['stop-1']}
|
|
17
|
+
/>,
|
|
18
|
+
)
|
|
19
|
+
expect(screen.getByRole('status').textContent).toBe(
|
|
20
|
+
'That turn had already finished.',
|
|
21
|
+
)
|
|
22
|
+
expect(screen.queryByRole('alert')).toBeNull()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('keeps other rejections as alerts', () => {
|
|
26
|
+
render(
|
|
27
|
+
<CommandReceipt
|
|
28
|
+
receipt={{
|
|
29
|
+
commandId: 'send-now-1',
|
|
30
|
+
outcome: 'rejected',
|
|
31
|
+
reason: 'stale-turn',
|
|
32
|
+
}}
|
|
33
|
+
stopCommandIds={['stop-1']}
|
|
34
|
+
/>,
|
|
35
|
+
)
|
|
36
|
+
expect(screen.getByRole('alert').textContent).toContain(
|
|
37
|
+
'Command rejected: stale-turn.',
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('renders nothing for applied commands', () => {
|
|
42
|
+
const { container } = render(
|
|
43
|
+
<CommandReceipt
|
|
44
|
+
receipt={{ commandId: 'stop-1', outcome: 'applied' }}
|
|
45
|
+
stopCommandIds={['stop-1']}
|
|
46
|
+
/>,
|
|
47
|
+
)
|
|
48
|
+
expect(container.textContent).toBe('')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('reports an earlier Stop after a later Stop was issued', () => {
|
|
52
|
+
render(
|
|
53
|
+
<CommandReceipt
|
|
54
|
+
receipt={{
|
|
55
|
+
commandId: 'stop-1',
|
|
56
|
+
outcome: 'rejected',
|
|
57
|
+
reason: 'stale-turn',
|
|
58
|
+
}}
|
|
59
|
+
stopCommandIds={['stop-1', 'stop-2']}
|
|
60
|
+
/>,
|
|
61
|
+
)
|
|
62
|
+
expect(screen.getByRole('status').textContent).toBe(
|
|
63
|
+
'That turn had already finished.',
|
|
64
|
+
)
|
|
65
|
+
})
|