experimental-a2 0.20.0 → 0.22.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 +55 -0
- package/README.md +1 -1
- package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
- package/dist/actor-CqibLgjQ.d.ts.map +1 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +2 -2
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +5 -5
- package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
- package/dist/actor-shared-DFET5q16.js.map +1 -0
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +54 -43
- package/dist/actor.js.map +1 -1
- package/dist/{ai-Cz3hqVhk.d.ts → ai-DAu77CDr.d.ts} +8 -8
- package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
- package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
- package/dist/ai-EEfKyc-z.js.map +1 -0
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +4 -2
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +214 -67
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
- package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
- package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
- package/dist/ai-harness-dQeJaR1t.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +3 -3
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +14 -17
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +86 -73
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BhHLoNKF.js → client-BQS4i_V9.js} +29 -6
- package/dist/client-BQS4i_V9.js.map +1 -0
- package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
- package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
- package/dist/contract-CWukt01d.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
- package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
- package/dist/{internal-CscSY7K_.js → internal-CvsrprnU.js} +40 -16
- package/dist/internal-CvsrprnU.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/otel.js +1 -1
- package/dist/otel.js.map +1 -1
- package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
- package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
- package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
- package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
- package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
- package/dist/runtime-consumer-CIeL33wj.js +12 -0
- package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +3 -3
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
- package/dist/server-DtFrWxi3.js.map +1 -0
- package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
- package/dist/server-qgez0Xny.d.ts.map +1 -0
- package/dist/server.browser.js +1 -1
- package/dist/server.browser.js.map +1 -1
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
- package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
- package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
- package/dist/session-clear-wHd5GCpZ.js.map +1 -0
- package/dist/store-D0MhBvg6.d.ts +532 -0
- package/dist/store-D0MhBvg6.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-codec-DwrWlgQx.js +65 -0
- package/dist/store-codec-DwrWlgQx.js.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +406 -225
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
- package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +1598 -587
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
- package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +4 -4
- package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
- package/dist/store-redis-notify-CsJw_hfD.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 +4 -5
- 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 +527 -299
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-DrTyiIuo.js +57 -0
- package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
- package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
- package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
- package/dist/{wire-BKxH1vbP.js → wire-CV-Z7wz-.js} +6 -5
- package/dist/wire-CV-Z7wz-.js.map +1 -0
- package/docs/01-quickstart.mdx +21 -15
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +13 -2
- package/docs/actors/02-concurrency.mdx +1 -1
- package/docs/agents/01-introduction.mdx +1 -1
- package/docs/agents/02-tools.mdx +12 -11
- package/docs/agents/03-conversations.mdx +36 -25
- package/docs/agents/04-harnesses.mdx +14 -7
- package/docs/client/03-presence.mdx +18 -14
- package/docs/contract/01-events.mdx +2 -2
- package/docs/contract/02-state.mdx +18 -14
- package/docs/contract/03-sessions.mdx +32 -25
- package/docs/index.mdx +29 -24
- package/docs/reference/01-core.mdx +7 -6
- package/docs/reference/02-server.mdx +250 -191
- package/docs/reference/04-ai.mdx +33 -23
- package/docs/reference/05-telemetry.mdx +27 -13
- package/docs/reference/06-errors.mdx +25 -19
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +8 -8
- package/docs/schedulers/02-vercel-queues.mdx +2 -2
- package/docs/schedulers/03-qstash.mdx +4 -4
- package/docs/server/01-consumers.mdx +418 -0
- package/docs/server/02-timers.mdx +31 -21
- package/docs/server/03-cancellation.mdx +52 -44
- package/docs/server/04-routes.mdx +4 -1
- package/docs/server/05-sql-projections.mdx +391 -0
- package/docs/server/06-guarantees.mdx +61 -51
- package/docs/stores/01-overview.mdx +3 -3
- package/docs/stores/02-redis.mdx +24 -10
- package/docs/stores/03-redis-http.mdx +8 -6
- package/docs/stores/04-postgres.mdx +65 -14
- package/docs/stores/05-sqlite.mdx +5 -3
- package/docs/upgrades/01-overview.mdx +31 -0
- package/docs/upgrades/78-0-22.mdx +115 -0
- package/docs/upgrades/79-0-21.mdx +165 -0
- package/docs/upgrades/80-0-20.mdx +43 -0
- package/docs/upgrades/81-0-19.mdx +33 -0
- package/docs/upgrades/82-0-18.mdx +33 -0
- package/docs/upgrades/83-0-17.mdx +69 -0
- package/docs/upgrades/meta.ts +3 -0
- package/examples/playground/DEPLOY.md +39 -7
- package/examples/playground/app/a0/chat.test.tsx +2 -2
- package/examples/playground/app/a0/sandbox-log.ts +17 -15
- package/examples/playground/app/a0/sandbox.ts +58 -13
- package/examples/playground/app/a0/server.ts +4 -4
- package/examples/playground/app/a2slack/a2slack.css +1067 -0
- package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
- package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
- package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
- package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
- package/examples/playground/app/a2slack/consumers/search.ts +37 -0
- package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
- package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
- package/examples/playground/app/a2slack/db/a2.ts +209 -0
- package/examples/playground/app/a2slack/db/index.ts +73 -0
- package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
- package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
- package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
- package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
- package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -0
- package/examples/playground/app/a2slack/db/queries.ts +263 -0
- package/examples/playground/app/a2slack/db/schema.ts +145 -0
- package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
- package/examples/playground/app/a2slack/drills/harness.ts +244 -0
- package/examples/playground/app/a2slack/events/route.ts +36 -0
- package/examples/playground/app/a2slack/identity.ts +44 -0
- package/examples/playground/app/a2slack/inbox/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/page.tsx +101 -0
- package/examples/playground/app/a2slack/lag.ts +73 -0
- package/examples/playground/app/a2slack/layout.tsx +43 -0
- package/examples/playground/app/a2slack/messages/route.ts +17 -0
- package/examples/playground/app/a2slack/model.test.ts +208 -0
- package/examples/playground/app/a2slack/model.ts +369 -0
- package/examples/playground/app/a2slack/models.test.ts +72 -0
- package/examples/playground/app/a2slack/page.tsx +10 -0
- package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
- package/examples/playground/app/a2slack/search/route.ts +40 -0
- package/examples/playground/app/a2slack/search-index.ts +108 -0
- package/examples/playground/app/a2slack/server.test.ts +315 -0
- package/examples/playground/app/a2slack/server.ts +55 -0
- package/examples/playground/app/a2slack/sessions.ts +50 -0
- package/examples/playground/app/a2slack/thread/route.ts +8 -0
- package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
- package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
- package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
- package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
- package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
- package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
- package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
- package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
- package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
- package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
- package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
- package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
- package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
- package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
- package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
- package/examples/playground/app/agent/server.ts +3 -3
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
- package/examples/playground/app/chat/server.ts +45 -35
- package/examples/playground/app/components/demo-icons.tsx +7 -0
- package/examples/playground/app/components/demos.tsx +7 -0
- package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
- package/examples/playground/app/orders/page.tsx +1 -1
- package/examples/playground/app/orders/server.ts +49 -30
- package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
- package/examples/playground/app/recovery/page.tsx +4 -4
- package/examples/playground/app/recovery/server.ts +23 -32
- package/examples/playground/drizzle.config.ts +14 -0
- package/examples/playground/lib/store.ts +1 -1
- package/examples/playground/next.config.ts +6 -1
- package/examples/playground/package.json +12 -1
- package/examples/playground/scripts/migrate.mjs +31 -0
- package/examples/playground/vercel.json +9 -0
- package/examples/playground/vitest.drills.config.ts +32 -0
- package/package.json +5 -2
- package/sql/postgres/drizzle-schema.ts +209 -0
- package/sql/postgres/schema.sql +227 -67
- package/src/actor-shared.ts +10 -2
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +162 -111
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +324 -82
- package/src/ai-harness.ts +17 -2
- package/src/ai-pi.ts +2 -2
- package/src/ai-server.ts +2 -2
- package/src/ai.ts +7 -7
- package/src/client.ts +19 -2
- package/src/contract.ts +1 -1
- package/src/deterministic-id.ts +27 -4
- package/src/errors.ts +8 -5
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +57 -16
- package/src/otel.ts +1 -1
- package/src/postgres-schema.ts +98 -57
- package/src/presence-recovery.ts +18 -11
- package/src/presence-tombstones.ts +52 -0
- package/src/push-envelope.ts +20 -0
- package/src/runtime-consumer.ts +10 -0
- package/src/scheduler-task.ts +2 -2
- package/src/scheduler-vercel.ts +1 -1
- package/src/server.browser.ts +1 -1
- package/src/server.ts +819 -218
- package/src/session-socket.ts +1 -1
- package/src/store-codec.ts +66 -0
- package/src/store-memory.ts +604 -286
- package/src/store-postgres.ts +2006 -857
- package/src/store-presence-polling.ts +59 -34
- package/src/store-redis-core.ts +1961 -1333
- package/src/store-redis-notify.ts +76 -26
- package/src/store-redis.ts +2 -3
- package/src/store-sqlite.ts +915 -516
- package/src/store.ts +241 -102
- package/src/telemetry.ts +2 -1
- package/src/wire.ts +1 -0
- package/dist/actor-C7b-1arn.d.ts.map +0 -1
- package/dist/actor-shared-t7w8xIKP.js.map +0 -1
- package/dist/ai-harness-Clbg5ei7.js.map +0 -1
- package/dist/ai-mMiWul1T.js.map +0 -1
- package/dist/client-BhHLoNKF.js.map +0 -1
- package/dist/contract-BYKASlCV.js.map +0 -1
- package/dist/idempotent-replay-JdgMQRRm.js +0 -21
- package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
- package/dist/internal-CscSY7K_.js.map +0 -1
- package/dist/server-DYAATFuT.d.ts.map +0 -1
- package/dist/server-qyb5aJJG.js.map +0 -1
- package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
- package/dist/session-clear-DGI4jTQ5.js.map +0 -1
- package/dist/store-CyLUy86v.d.ts +0 -404
- package/dist/store-CyLUy86v.d.ts.map +0 -1
- package/dist/store-codec-DTG0Ftek.js +0 -10
- package/dist/store-codec-DTG0Ftek.js.map +0 -1
- package/dist/store-presence-polling-B1ixeqIS.js +0 -94
- package/dist/store-presence-polling-B1ixeqIS.js.map +0 -1
- package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
- package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
- package/dist/stream-buffer-Cx7AAPGq.js +0 -23
- package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
- package/dist/wire-BKxH1vbP.js.map +0 -1
- package/docs/server/01-handlers.mdx +0 -227
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { usePathname, useRouter } from 'next/navigation'
|
|
5
|
+
import { SiteNav } from '@/app/components/site-header'
|
|
6
|
+
import { channelIdSchema } from '@/app/a2slack/model'
|
|
7
|
+
import { useIdentity } from '../identity'
|
|
8
|
+
import { useWorkspace } from '../sessions'
|
|
9
|
+
import { useInboxPanel } from './InboxContext'
|
|
10
|
+
import { useSearch } from './SearchContext'
|
|
11
|
+
|
|
12
|
+
const MIN_WIDTH = 180
|
|
13
|
+
const MAX_WIDTH = 400
|
|
14
|
+
const WIDTH_KEY = 'a2slack-sidebar-width'
|
|
15
|
+
|
|
16
|
+
function slugify(raw: string): string {
|
|
17
|
+
return raw
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.trim()
|
|
20
|
+
.replace(/\s+/g, '-')
|
|
21
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
22
|
+
.replace(/^-+/, '')
|
|
23
|
+
.slice(0, 32)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type SidebarSession = ReturnType<typeof useWorkspace> & {
|
|
27
|
+
state: NonNullable<ReturnType<typeof useWorkspace>['state']>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function Sidebar(): ReactNode {
|
|
31
|
+
const session = useWorkspace()
|
|
32
|
+
if (session.state === undefined) {
|
|
33
|
+
const error =
|
|
34
|
+
'error' in session.connection ? session.connection.error : null
|
|
35
|
+
return (
|
|
36
|
+
<p className="a2slack-loading" role="status">
|
|
37
|
+
{error?.message ?? 'Loading…'}
|
|
38
|
+
</p>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
return <SidebarView session={{ ...session, state: session.state }} />
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function SidebarView({
|
|
45
|
+
session,
|
|
46
|
+
}: Record<never, never> & { session: SidebarSession }): ReactNode {
|
|
47
|
+
const identity = useIdentity()
|
|
48
|
+
const search = useSearch()
|
|
49
|
+
const inbox = useInboxPanel()
|
|
50
|
+
const { state, push } = session
|
|
51
|
+
const pathname = usePathname()
|
|
52
|
+
const router = useRouter()
|
|
53
|
+
const [draft, setDraft] = useState('')
|
|
54
|
+
const [width, setWidth] = useState(224)
|
|
55
|
+
const [resizing, setResizing] = useState(false)
|
|
56
|
+
const startRef = useRef({ x: 0, width: 224 })
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const stored = Number(localStorage.getItem(WIDTH_KEY))
|
|
60
|
+
if (stored >= MIN_WIDTH && stored <= MAX_WIDTH) setWidth(stored)
|
|
61
|
+
}, [])
|
|
62
|
+
|
|
63
|
+
const create = (): void => {
|
|
64
|
+
const id = slugify(draft)
|
|
65
|
+
if (!identity || !channelIdSchema.safeParse(id).success) return
|
|
66
|
+
if (!state.channels.includes(id)) {
|
|
67
|
+
push({ type: 'channel.created', payload: { id, by: identity } }).catch(
|
|
68
|
+
() => {},
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
setDraft('')
|
|
72
|
+
search.clear()
|
|
73
|
+
inbox.setOpen(false)
|
|
74
|
+
router.push(`/a2slack/c/${id}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<nav className="a2slack-sidebar" style={{ width }}>
|
|
79
|
+
<div className="site-header a2slack-head">
|
|
80
|
+
<SiteNav />
|
|
81
|
+
</div>
|
|
82
|
+
<form
|
|
83
|
+
className="a2slack-form a2slack-search"
|
|
84
|
+
role="search"
|
|
85
|
+
onSubmit={(e) => {
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
search.submit()
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<input
|
|
91
|
+
ref={search.input}
|
|
92
|
+
className="a2slack-input"
|
|
93
|
+
aria-label="search messages"
|
|
94
|
+
enterKeyHint="search"
|
|
95
|
+
placeholder="search…"
|
|
96
|
+
value={search.query}
|
|
97
|
+
onChange={(e) => {
|
|
98
|
+
search.setQuery(e.target.value)
|
|
99
|
+
inbox.setOpen(false)
|
|
100
|
+
}}
|
|
101
|
+
onKeyDown={(e) => {
|
|
102
|
+
if (e.key !== 'Escape') return
|
|
103
|
+
e.preventDefault()
|
|
104
|
+
search.clear()
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
</form>
|
|
108
|
+
<div className="a2slack-list">
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
className="a2slack-channel a2slack-inbox-row"
|
|
112
|
+
aria-current={inbox.open ? 'page' : undefined}
|
|
113
|
+
aria-label={`inbox, ${inbox.unread} unread`}
|
|
114
|
+
onClick={() => {
|
|
115
|
+
search.clear()
|
|
116
|
+
inbox.setOpen(!inbox.open)
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
<span>inbox</span>
|
|
120
|
+
{inbox.unread > 0 && (
|
|
121
|
+
<span className="a2slack-unread">{inbox.unread}</span>
|
|
122
|
+
)}
|
|
123
|
+
</button>
|
|
124
|
+
<span className="a2slack-label a2slack-label-spaced">channels</span>
|
|
125
|
+
{state.channels.map((id) => (
|
|
126
|
+
<Link
|
|
127
|
+
key={id}
|
|
128
|
+
href={`/a2slack/c/${id}`}
|
|
129
|
+
className="a2slack-channel"
|
|
130
|
+
aria-current={
|
|
131
|
+
!inbox.open && pathname === `/a2slack/c/${id}`
|
|
132
|
+
? 'page'
|
|
133
|
+
: undefined
|
|
134
|
+
}
|
|
135
|
+
onClick={() => {
|
|
136
|
+
search.clear()
|
|
137
|
+
inbox.setOpen(false)
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
#{id}
|
|
141
|
+
</Link>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
<form
|
|
145
|
+
className="a2slack-form"
|
|
146
|
+
onSubmit={(e) => {
|
|
147
|
+
e.preventDefault()
|
|
148
|
+
create()
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<input
|
|
152
|
+
className="a2slack-input"
|
|
153
|
+
placeholder="+ new channel"
|
|
154
|
+
value={draft}
|
|
155
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
156
|
+
/>
|
|
157
|
+
</form>
|
|
158
|
+
{identity && (
|
|
159
|
+
<div className="a2slack-you">
|
|
160
|
+
<span>you: {identity.name}</span>
|
|
161
|
+
<Link className="a2slack-lag-link" href="/a2slack/lag">
|
|
162
|
+
lag
|
|
163
|
+
</Link>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
<div
|
|
167
|
+
className="a2slack-handle"
|
|
168
|
+
onPointerDown={(e) => {
|
|
169
|
+
e.preventDefault()
|
|
170
|
+
startRef.current = { x: e.clientX, width }
|
|
171
|
+
setResizing(true)
|
|
172
|
+
e.currentTarget.setPointerCapture(e.pointerId)
|
|
173
|
+
}}
|
|
174
|
+
onPointerMove={(e) => {
|
|
175
|
+
if (!resizing) return
|
|
176
|
+
const next = Math.min(
|
|
177
|
+
MAX_WIDTH,
|
|
178
|
+
Math.max(
|
|
179
|
+
MIN_WIDTH,
|
|
180
|
+
startRef.current.width + e.clientX - startRef.current.x,
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
setWidth(next)
|
|
184
|
+
}}
|
|
185
|
+
onPointerUp={(e) => {
|
|
186
|
+
if (!resizing) return
|
|
187
|
+
setResizing(false)
|
|
188
|
+
e.currentTarget.releasePointerCapture(e.pointerId)
|
|
189
|
+
localStorage.setItem(WIDTH_KEY, String(width))
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
<span
|
|
193
|
+
className="a2slack-handle-line"
|
|
194
|
+
data-active={resizing || undefined}
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
</nav>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
3
|
+
import { X } from 'lucide-react'
|
|
4
|
+
import {
|
|
5
|
+
liveThread,
|
|
6
|
+
type ChatMessage,
|
|
7
|
+
type ThreadPage,
|
|
8
|
+
} from '@/app/a2slack/model'
|
|
9
|
+
import { useWorkspace } from '../sessions'
|
|
10
|
+
import { Composer } from './Composer'
|
|
11
|
+
import { useInboxPanel } from './InboxContext'
|
|
12
|
+
import { MessageRow } from './MessageRow'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One thread, read from `/a2slack/thread` when it opens and kept live from
|
|
16
|
+
* the workspace's events. Until the read answers it shows the root as the
|
|
17
|
+
* feed has it, if the feed has it.
|
|
18
|
+
*/
|
|
19
|
+
export function ThreadPanel({
|
|
20
|
+
channel,
|
|
21
|
+
rootId,
|
|
22
|
+
known,
|
|
23
|
+
onClose,
|
|
24
|
+
}: {
|
|
25
|
+
channel: string
|
|
26
|
+
rootId: string
|
|
27
|
+
/** The root, live, when it's on the channel's newest page. */
|
|
28
|
+
known: ChatMessage | null
|
|
29
|
+
onClose: () => void
|
|
30
|
+
}): ReactNode {
|
|
31
|
+
const { events } = useWorkspace()
|
|
32
|
+
const inbox = useInboxPanel()
|
|
33
|
+
const [page, setPage] = useState<ThreadPage | null>(null)
|
|
34
|
+
const [failed, setFailed] = useState(false)
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const controller = new AbortController()
|
|
38
|
+
const read = async (): Promise<void> => {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(
|
|
41
|
+
`/a2slack/thread?root=${encodeURIComponent(rootId)}`,
|
|
42
|
+
{ signal: controller.signal },
|
|
43
|
+
)
|
|
44
|
+
if (!response.ok) throw new Error(`thread: ${response.status}`)
|
|
45
|
+
setPage((await response.json()) as ThreadPage)
|
|
46
|
+
} catch {
|
|
47
|
+
if (!controller.signal.aborted) setFailed(true)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
void read()
|
|
51
|
+
return () => controller.abort()
|
|
52
|
+
}, [rootId])
|
|
53
|
+
|
|
54
|
+
const thread = useMemo(
|
|
55
|
+
() => (page ? liveThread(page, events, rootId) : null),
|
|
56
|
+
[page, events, rootId],
|
|
57
|
+
)
|
|
58
|
+
const root = thread?.root ?? known
|
|
59
|
+
const replies = thread?.replies ?? []
|
|
60
|
+
|
|
61
|
+
// Reading the thread reads its notifications, including any that arrive
|
|
62
|
+
// while it's open.
|
|
63
|
+
const unread =
|
|
64
|
+
inbox.items.find((item) => item.threadId === rootId)?.unread ?? 0
|
|
65
|
+
const { markRead } = inbox
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (unread > 0) markRead(rootId)
|
|
68
|
+
}, [unread, rootId, markRead])
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<aside className="a2slack-thread">
|
|
72
|
+
<header className="a2slack-thread-header">
|
|
73
|
+
<span className="a2slack-thread-title">thread</span>
|
|
74
|
+
<button className="a2slack-close" onClick={onClose} title="close">
|
|
75
|
+
<X size={15} strokeWidth={1.75} />
|
|
76
|
+
</button>
|
|
77
|
+
</header>
|
|
78
|
+
<div className="a2slack-thread-scroll">
|
|
79
|
+
{root === null ? (
|
|
80
|
+
<div className="a2slack-divider">
|
|
81
|
+
<span>
|
|
82
|
+
{failed
|
|
83
|
+
? "this thread didn't load"
|
|
84
|
+
: page
|
|
85
|
+
? 'this message is gone'
|
|
86
|
+
: 'loading…'}
|
|
87
|
+
</span>
|
|
88
|
+
<span className="a2slack-divider-line" />
|
|
89
|
+
</div>
|
|
90
|
+
) : (
|
|
91
|
+
<>
|
|
92
|
+
<MessageRow message={root} grouped={false} inThread />
|
|
93
|
+
<div className="a2slack-divider">
|
|
94
|
+
<span>
|
|
95
|
+
{root.replyCount === 0
|
|
96
|
+
? 'no replies yet'
|
|
97
|
+
: `${root.replyCount} ${root.replyCount === 1 ? 'reply' : 'replies'}`}
|
|
98
|
+
{thread === null &&
|
|
99
|
+
root.replyCount > 0 &&
|
|
100
|
+
(failed ? " · these didn't load" : ' · loading…')}
|
|
101
|
+
</span>
|
|
102
|
+
<span className="a2slack-divider-line" />
|
|
103
|
+
</div>
|
|
104
|
+
{replies.map((reply, i) => {
|
|
105
|
+
const prev = replies[i - 1]
|
|
106
|
+
const grouped =
|
|
107
|
+
prev !== undefined &&
|
|
108
|
+
!prev.deleted &&
|
|
109
|
+
prev.author.id === reply.author.id &&
|
|
110
|
+
Date.parse(reply.at) - Date.parse(prev.at) < 5 * 60 * 1000
|
|
111
|
+
return (
|
|
112
|
+
<MessageRow
|
|
113
|
+
key={reply.id}
|
|
114
|
+
message={reply}
|
|
115
|
+
grouped={grouped}
|
|
116
|
+
inThread
|
|
117
|
+
/>
|
|
118
|
+
)
|
|
119
|
+
})}
|
|
120
|
+
</>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
{root !== null && (
|
|
124
|
+
<Composer
|
|
125
|
+
channel={channel}
|
|
126
|
+
threadId={root.id}
|
|
127
|
+
placeholder="reply… or ask @bot"
|
|
128
|
+
autoFocus
|
|
129
|
+
compact
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
</aside>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
3
|
+
import { liveInbox, type InboxItem, type InboxPage } from '@/app/a2slack/model'
|
|
4
|
+
import { useWorkspace } from '../sessions'
|
|
5
|
+
|
|
6
|
+
export type Inbox = {
|
|
7
|
+
/** One per thread with news for `me`, newest first. */
|
|
8
|
+
items: InboxItem[]
|
|
9
|
+
/** Unread notifications across every thread. */
|
|
10
|
+
unread: number
|
|
11
|
+
/** The page hasn't answered yet. */
|
|
12
|
+
loading: boolean
|
|
13
|
+
error: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `me`'s inbox: the page `/a2slack/inbox` read, kept live from the
|
|
18
|
+
* workspace's events after it. A new `me` reads a new page.
|
|
19
|
+
*/
|
|
20
|
+
export function useInbox(me: string | null): Inbox {
|
|
21
|
+
const { events } = useWorkspace()
|
|
22
|
+
const [read, setRead] = useState<{
|
|
23
|
+
me: string
|
|
24
|
+
page: InboxPage | null
|
|
25
|
+
error: string | null
|
|
26
|
+
} | null>(null)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (me === null) return
|
|
30
|
+
const controller = new AbortController()
|
|
31
|
+
const load = async (): Promise<void> => {
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(
|
|
34
|
+
`/a2slack/inbox?user=${encodeURIComponent(me)}`,
|
|
35
|
+
{ signal: controller.signal },
|
|
36
|
+
)
|
|
37
|
+
const body = (await response.json().catch(() => ({}))) as
|
|
38
|
+
InboxPage | { error?: string }
|
|
39
|
+
if (response.ok && 'items' in body) {
|
|
40
|
+
setRead({ me, page: body, error: null })
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
const error = 'error' in body ? body.error : undefined
|
|
44
|
+
setRead({
|
|
45
|
+
me,
|
|
46
|
+
page: null,
|
|
47
|
+
error: error ?? `the inbox failed (${response.status})`,
|
|
48
|
+
})
|
|
49
|
+
} catch {
|
|
50
|
+
if (controller.signal.aborted) return
|
|
51
|
+
setRead({ me, page: null, error: "the inbox didn't answer" })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
void load()
|
|
55
|
+
return () => controller.abort()
|
|
56
|
+
}, [me])
|
|
57
|
+
|
|
58
|
+
const current = me !== null && read?.me === me ? read : null
|
|
59
|
+
const items = useMemo(
|
|
60
|
+
() =>
|
|
61
|
+
current?.page && me !== null ? liveInbox(current.page, events, me) : [],
|
|
62
|
+
[current, events, me],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
items,
|
|
67
|
+
unread: items.reduce((sum, item) => sum + item.unread, 0),
|
|
68
|
+
loading: me !== null && current === null,
|
|
69
|
+
error: current?.error ?? null,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -3,7 +3,7 @@ import { Buffer } from 'node:buffer'
|
|
|
3
3
|
import { createHash } from 'node:crypto'
|
|
4
4
|
import { Sandbox } from '@vercel/sandbox'
|
|
5
5
|
import { tool } from 'ai'
|
|
6
|
-
import {
|
|
6
|
+
import { consumerContext, inputs } from 'experimental-a2/ai'
|
|
7
7
|
import { agent } from 'experimental-a2/ai/server'
|
|
8
8
|
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
9
9
|
import { otel } from 'experimental-a2/otel'
|
|
@@ -45,7 +45,7 @@ const tools = {
|
|
|
45
45
|
command: z.string().min(1).describe('The Bash command to execute.'),
|
|
46
46
|
}),
|
|
47
47
|
execute: async ({ command }) => {
|
|
48
|
-
const context =
|
|
48
|
+
const context = consumerContext(demoAgent)
|
|
49
49
|
const sandbox = await Sandbox.getOrCreate({
|
|
50
50
|
name: sandboxName(context.session.id),
|
|
51
51
|
persistent: true,
|
|
@@ -109,7 +109,7 @@ const tools = {
|
|
|
109
109
|
reminder: z.string().min(1).max(500),
|
|
110
110
|
}),
|
|
111
111
|
execute: async ({ delaySeconds, reminder }, { toolCallId }) => {
|
|
112
|
-
const context =
|
|
112
|
+
const context = consumerContext(demoAgent)
|
|
113
113
|
await context.session.schedule(
|
|
114
114
|
`reminder:${toolCallId}`,
|
|
115
115
|
{ delay: `${delaySeconds}s` },
|
|
@@ -488,7 +488,7 @@ export function ChatClient({
|
|
|
488
488
|
inputRef.current?.focus({ preventScroll: true }),
|
|
489
489
|
)
|
|
490
490
|
try {
|
|
491
|
-
// The browser authors only the room fact. A serialized server
|
|
491
|
+
// The browser authors only the room fact. A serialized server consumer
|
|
492
492
|
// derives AI metadata and generation policy from its durable log index.
|
|
493
493
|
await push({
|
|
494
494
|
type: 'message.sent',
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import 'server-only'
|
|
2
2
|
import { tool } from 'ai'
|
|
3
3
|
import { idempotentId } from 'experimental-a2'
|
|
4
|
-
import {
|
|
4
|
+
import { consumerContext, inputs } from 'experimental-a2/ai'
|
|
5
5
|
import { agent } from 'experimental-a2/ai/server'
|
|
6
6
|
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
consumer,
|
|
9
|
+
createServer,
|
|
10
|
+
type A2Operation,
|
|
11
|
+
} from 'experimental-a2/server'
|
|
8
12
|
import { store } from '@/lib/store'
|
|
9
13
|
import {
|
|
10
14
|
chat,
|
|
@@ -22,7 +26,7 @@ const tools = {
|
|
|
22
26
|
'Post a visible message to the multiplayer chat. Call it once for each chat bubble you want to send; this tool is the only way to speak in the room.',
|
|
23
27
|
inputSchema: chatPostInputSchema,
|
|
24
28
|
execute: async ({ message }, { toolCallId }) => {
|
|
25
|
-
const context =
|
|
29
|
+
const context = consumerContext(chatAgent)
|
|
26
30
|
context.signal.throwIfAborted()
|
|
27
31
|
const { generationId, messageId } = context.event.payload.call
|
|
28
32
|
// The agent's session shares the room's id. The explicit id makes a
|
|
@@ -114,42 +118,48 @@ Use as many post calls as the moment naturally calls for. Several short messages
|
|
|
114
118
|
},
|
|
115
119
|
})
|
|
116
120
|
|
|
117
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Each room message goes on to the agent's session, marked for whether it
|
|
123
|
+
* should answer. One lane keeps the agent's messages in the room's order.
|
|
124
|
+
*/
|
|
125
|
+
const forwardToAgent = consumer({
|
|
126
|
+
contract: chat,
|
|
127
|
+
name: 'forward-to-agent',
|
|
128
|
+
types: ['message.sent'],
|
|
129
|
+
lane: 'a2.chat.ingress',
|
|
130
|
+
}).run(async (context) => {
|
|
131
|
+
const ambient = (await context.session.state()).state.ambient.enabled
|
|
132
|
+
const { author, text } = context.event.payload
|
|
133
|
+
const mode: ChatParticipantMode = ambient
|
|
134
|
+
? 'ambient'
|
|
135
|
+
: mentionsAgent(text)
|
|
136
|
+
? 'mention'
|
|
137
|
+
: 'passive'
|
|
138
|
+
const message: ChatAgentMessage = {
|
|
139
|
+
// Derived from the room event's id, so a retried forward converges on
|
|
140
|
+
// the same message in the agent's session.
|
|
141
|
+
id: await idempotentId('chat.message', context.event.id),
|
|
142
|
+
role: 'user',
|
|
143
|
+
metadata: {
|
|
144
|
+
kind: 'participant',
|
|
145
|
+
author,
|
|
146
|
+
text,
|
|
147
|
+
sentAt: context.event.createdAt.toISOString(),
|
|
148
|
+
mode,
|
|
149
|
+
},
|
|
150
|
+
parts: [{ type: 'text', text: `${author}: ${text}` }],
|
|
151
|
+
}
|
|
152
|
+
await chatAgentServer
|
|
153
|
+
.session(context.session.id)
|
|
154
|
+
.append(...inputs.message(message, { generate: mode !== 'passive' }))
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
/** The room. Its messages reach the agent through `forwardToAgent`. */
|
|
118
158
|
export const chatServer = createServer({
|
|
119
159
|
...(store ? { store } : {}),
|
|
120
160
|
...(chatScheduler ? { scheduler: chatScheduler } : {}),
|
|
121
161
|
contract: chat,
|
|
122
|
-
|
|
123
|
-
'message.sent': {
|
|
124
|
-
lane: 'a2.chat.ingress',
|
|
125
|
-
handler: async (context) => {
|
|
126
|
-
const ambient = (await context.session.state()).state.ambient.enabled
|
|
127
|
-
const { author, text } = context.event.payload
|
|
128
|
-
const mode: ChatParticipantMode = ambient
|
|
129
|
-
? 'ambient'
|
|
130
|
-
: mentionsAgent(text)
|
|
131
|
-
? 'mention'
|
|
132
|
-
: 'passive'
|
|
133
|
-
const message: ChatAgentMessage = {
|
|
134
|
-
// Event ids are unique across a store's sessions, so the forward
|
|
135
|
-
// derives its own; a retried forward converges on the same id.
|
|
136
|
-
id: await idempotentId('chat.message', context.event.id),
|
|
137
|
-
role: 'user',
|
|
138
|
-
metadata: {
|
|
139
|
-
kind: 'participant',
|
|
140
|
-
author,
|
|
141
|
-
text,
|
|
142
|
-
sentAt: context.event.createdAt.toISOString(),
|
|
143
|
-
mode,
|
|
144
|
-
},
|
|
145
|
-
parts: [{ type: 'text', text: `${author}: ${text}` }],
|
|
146
|
-
}
|
|
147
|
-
await chatAgentServer
|
|
148
|
-
.session(context.session.id)
|
|
149
|
-
.append(...inputs.message(message, { generate: mode !== 'passive' }))
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
},
|
|
162
|
+
consumers: [forwardToAgent],
|
|
153
163
|
})
|
|
154
164
|
|
|
155
165
|
/** Room pushes: people's messages, typing, and ambient mode. Posts are the agent's. */
|
|
@@ -40,6 +40,13 @@ export const ChatIcon = (): ReactNode => (
|
|
|
40
40
|
</Glyph>
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
+
export const A2slackIcon = (): ReactNode => (
|
|
44
|
+
<Glyph>
|
|
45
|
+
<path d="M3 4h18v13H10l-4 3.5V17H3z" />
|
|
46
|
+
<path className="accent" d="M10.5 7l-1 7M14.5 7l-1 7M8 9h8M7.5 12h8" />
|
|
47
|
+
</Glyph>
|
|
48
|
+
)
|
|
49
|
+
|
|
43
50
|
export const AgentIcon = (): ReactNode => (
|
|
44
51
|
<Glyph>
|
|
45
52
|
<rect x="3" y="3" width="18" height="18" />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
A0Icon,
|
|
4
|
+
A2slackIcon,
|
|
4
5
|
AgentIcon,
|
|
5
6
|
CanvasIcon,
|
|
6
7
|
ChatIcon,
|
|
@@ -24,6 +25,12 @@ export const demos: Demo[] = [
|
|
|
24
25
|
{ href: '/a0', name: 'A0', icon: <A0Icon />, fullWindow: true },
|
|
25
26
|
{ href: '/orders', name: 'Orders', icon: <OrdersIcon /> },
|
|
26
27
|
{ href: '/chat', name: 'Chat', icon: <ChatIcon /> },
|
|
28
|
+
{
|
|
29
|
+
href: '/a2slack',
|
|
30
|
+
name: 'a2slack',
|
|
31
|
+
icon: <A2slackIcon />,
|
|
32
|
+
fullWindow: true,
|
|
33
|
+
},
|
|
27
34
|
{ href: '/agent', name: 'Agent', icon: <AgentIcon /> },
|
|
28
35
|
{ href: '/canvas', name: 'Canvas', icon: <CanvasIcon /> },
|
|
29
36
|
{ href: '/documents', name: 'Documents', icon: <DocumentsIcon /> },
|
|
@@ -118,7 +118,7 @@ export function OrderClient({ sessionId }: { sessionId: string }): ReactNode {
|
|
|
118
118
|
<p className="hint">
|
|
119
119
|
The button pushes <code>shop.started</code> optimistically — the
|
|
120
120
|
badge flips before the server acks. Everything after that is
|
|
121
|
-
|
|
121
|
+
consumers appending the next fact: courier assigned, picked up,
|
|
122
122
|
delivered — watch it arrive over the stream, hands off.
|
|
123
123
|
</p>
|
|
124
124
|
</>
|
|
@@ -129,7 +129,7 @@ export function OrderClient({ sessionId }: { sessionId: string }): ReactNode {
|
|
|
129
129
|
</p>
|
|
130
130
|
) : (
|
|
131
131
|
<p className="hint">
|
|
132
|
-
Hands off from here — each status lands on its own as
|
|
132
|
+
Hands off from here — each status lands on its own as consumers append
|
|
133
133
|
the next fact.
|
|
134
134
|
</p>
|
|
135
135
|
)}
|
|
@@ -8,7 +8,7 @@ export default function OrdersPage(): ReactNode {
|
|
|
8
8
|
<p className="lede">
|
|
9
9
|
Creating an order appends a single <code>created</code> event.
|
|
10
10
|
Everything after that — the shop notification, the courier, the delivery
|
|
11
|
-
itself — is
|
|
11
|
+
itself — is consumers appending the next fact to the same log, all the
|
|
12
12
|
way to <code>delivered</code>.
|
|
13
13
|
</p>
|
|
14
14
|
<NewOrderForm />
|