acrylblends 1.0.10 → 1.2.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/README.md +9 -0
- package/dist/index.d.ts +60 -2
- package/dist/index.js +1992 -33
- package/dist/index.js.map +1 -1
- package/docs/framework/agents/index.md +1 -1
- package/docs/framework/api/summary.md +1 -1
- package/docs/framework/getting-started/desktop.md +23 -4
- package/docs/framework/guides/create-acrylblends.md +1 -1
- package/docs/framework/guides/production.md +20 -0
- package/docs/framework/workshop/README.md +174 -285
- package/docs/framework/workshop/facilitator.md +81 -90
- package/legacy/README.md +9 -0
- package/legacy/agent/157ce3db1112/src/app.ts +98 -0
- package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
- package/legacy/agent/24c681a00fca/src/app.ts +160 -0
- package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
- package/legacy/agent/2e790da977b2/src/app.ts +98 -0
- package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
- package/legacy/agent/6307045545dc/src/app.ts +168 -0
- package/legacy/agent/6307045545dc/src/routes.ts +83 -0
- package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
- package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
- package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
- package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
- package/legacy/agent/74ece148269c/src/app.ts +168 -0
- package/legacy/agent/74ece148269c/src/routes.ts +93 -0
- package/legacy/agent/855359304707/src/app.ts +168 -0
- package/legacy/agent/855359304707/src/routes.ts +126 -0
- package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
- package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
- package/legacy/agent/c2f30958043b/src/app.ts +98 -0
- package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
- package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
- package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
- package/legacy/blank/aea589283445/src/routes.ts +16 -0
- package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
- package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
- package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
- package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
- package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
- package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
- package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
- package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
- package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
- package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
- package/legacy/layouts.json +524 -0
- package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
- package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
- package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
- package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
- package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
- package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
- package/legacy/notes/4edc36949c15/src/app.ts +107 -0
- package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
- package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
- package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
- package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
- package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
- package/legacy/todo/02e613b3625c/src/app.ts +71 -0
- package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
- package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
- package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
- package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
- package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
- package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
- package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
- package/legacy/todo/8e64211312f3/src/app.ts +71 -0
- package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
- package/legacy/todo/b79078a87565/src/app.ts +71 -0
- package/legacy/todo/b79078a87565/src/todo.ts +125 -0
- package/lib/{audit-Dw-siYTp.d.ts → audit-Bok_UNCn.d.ts} +41 -6
- package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
- package/lib/cordis/door.d.ts +44 -7
- package/lib/cordis/door.js +214 -60
- package/lib/cordis/inspect.js +1 -1
- package/lib/cordis/node-server.d.ts +101 -3
- package/lib/cordis/node-server.js +3 -3
- package/lib/cordis/router-http.d.ts +7 -1
- package/lib/cordis/router-http.js +44 -3
- package/lib/cordis/router.d.ts +1 -1
- package/lib/cordis/router.js +1 -1
- package/lib/cordis/state.d.ts +3 -2
- package/lib/cordis/state.js +371 -7
- package/lib/door/host.d.ts +2 -2
- package/lib/door/host.js +1 -1
- package/lib/door/page.d.ts +20 -1
- package/lib/door/page.js +75 -14
- package/lib/door/protocol.d.ts +1 -1
- package/lib/drivers.d.ts +10 -1
- package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
- package/lib/node-server.d.ts +7 -1
- package/lib/node-server.js +2 -2
- package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
- package/lib/{snapshot-text-BfEbEOjA.js → snapshot-text-CXTnjxGx.js} +121 -9
- package/lib/src-CtkIX_Gg.js +427 -0
- package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
- package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
- package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
- package/package.json +2 -1
- package/plugins/acryl-door/src/host/channel.ts +124 -9
- package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
- package/plugins/acryl-door/src/host/door-file.ts +7 -2
- package/plugins/acryl-door/src/host/door.ts +58 -23
- package/plugins/acryl-door/src/host/page-routes.ts +3 -1
- package/plugins/acryl-door/src/host/page-socket.ts +18 -8
- package/plugins/acryl-door/src/host/verbs.ts +3 -0
- package/plugins/acryl-door/src/index.ts +166 -29
- package/plugins/acryl-door/src/page/base.ts +32 -0
- package/plugins/acryl-door/src/page/channel.ts +11 -1
- package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
- package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
- package/plugins/acryl-door/src/page/driver/driver.ts +49 -7
- package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
- package/plugins/acryl-door/src/page/index.ts +46 -2
- package/plugins/acryl-door/src/page/indicator.ts +14 -6
- package/plugins/acryl-door/src/page/outline.ts +56 -0
- package/plugins/acryl-door/src/page/protocol.ts +4 -0
- package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
- package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
- package/plugins/acryl-node-server/src/client-modules.ts +7 -1
- package/plugins/acryl-node-server/src/index.ts +304 -16
- package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
- package/plugins/acryl-page/plugin.json +12 -0
- package/plugins/acryl-page/src/index.ts +163 -0
- package/plugins/acryl-router/src/service.ts +77 -14
- package/plugins/acryl-router-http/src/index.ts +15 -1
- package/plugins/acryl-router-http/src/serve.ts +31 -0
- package/plugins/acryl-state/plugin.json +4 -3
- package/plugins/acryl-state/src/client-source.ts +244 -0
- package/plugins/acryl-state/src/service.ts +33 -6
- package/plugins/acryl-state/src/socket.ts +96 -0
- package/plugins/acryl-web-server/plugin.json +14 -0
- package/plugins/acryl-web-server/src/index.ts +164 -0
- package/plugins/agent/agent.ts +38 -7
- package/plugins/agent/index.ts +1 -1
- package/plugins/agent/model.ts +10 -2
- package/plugins/agent/tools.ts +9 -3
- package/plugins/agent-panel/client/describe.ts +57 -0
- package/plugins/agent-panel/client/driver-pick.ts +22 -0
- package/plugins/agent-panel/client/frame.ts +51 -0
- package/plugins/agent-panel/client/main.ts +343 -81
- package/plugins/agent-panel/drivers.ts +7 -2
- package/plugins/agent-panel/headless.ts +5 -2
- package/plugins/agent-panel/index.ts +187 -61
- package/plugins/agent-panel/mount.ts +27 -0
- package/plugins/agent-panel/visitors.ts +231 -0
- package/plugins/desktop/frame.ts +231 -0
- package/plugins/desktop/launch.ts +44 -9
- package/plugins/desktop/main.ts +100 -32
- package/plugins/desktop/window.ts +21 -4
- package/plugins/driver-pty/client/terminal.ts +4 -1
- package/plugins/driver-pty/index.ts +38 -11
- package/plugins/driver-pty/spawn.ts +38 -0
- package/plugins/driver-pty/terminal.ts +2 -0
- package/plugins/extensions/index.ts +26 -1
- package/plugins/extensions/resolve.ts +13 -0
- package/plugins/feedback/client.d.ts +22 -0
- package/plugins/feedback/client.js +471 -0
- package/plugins/feedback/index.ts +143 -0
- package/plugins/feedback/plugin.json +11 -0
- package/plugins/feedback/store.ts +307 -0
- package/plugins/home/index.ts +122 -0
- package/plugins/home/plugin.json +11 -0
- package/plugins/instance/index.ts +12 -3
- package/plugins/manifest/app-plugins.ts +86 -0
- package/plugins/manifest/index.ts +12 -2
- package/plugins/manifest/snapshot.ts +275 -0
- package/plugins/openai/index.ts +64 -5
- package/plugins/openai/prompt.ts +23 -0
- package/shared/AGENTS.core.md +36 -16
- package/shared/skills/acryl-add-ui/SKILL.md +19 -0
- package/shared/skills/acryl-app/SKILL.md +13 -0
- package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
- package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
- package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
- package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
- package/shared/skills/cordis-plugin/SKILL.md +26 -0
- package/templates/agent/AGENTS.md +4 -2
- package/templates/agent/blend.yaml +4 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +1 -0
- package/templates/agent/src/app.ts +150 -33
- package/templates/agent/src/frame.ts +17 -0
- package/templates/agent/src/main.ts +5 -3
- package/templates/agent/src/routes.ts +73 -21
- package/templates/agent/template.json +2 -0
- package/templates/agent/test/app.test.ts +83 -5
- package/templates/blank/AGENTS.md +3 -2
- package/templates/blank/blend.yaml +11 -0
- package/templates/blank/gitignore +1 -0
- package/templates/blank/package.json +1 -0
- package/templates/blank/src/app.ts +100 -29
- package/templates/blank/src/door.ts +7 -0
- package/templates/blank/src/first-page.ts +5 -0
- package/templates/blank/src/frame.ts +17 -0
- package/templates/blank/src/routes.ts +65 -8
- package/templates/blank/template.json +14 -1
- package/templates/blank/test/app.test.ts +103 -16
- package/templates/chat/blend.yaml +2 -1
- package/templates/chat/gitignore +1 -0
- package/templates/chat/package.json +1 -0
- package/templates/chat/src/app.ts +72 -22
- package/templates/chat/src/chat-app.ts +11 -2
- package/templates/chat/src/client/chat.tsx +4 -2
- package/templates/chat/src/frame.ts +17 -0
- package/templates/chat/src/main.ts +1 -6
- package/templates/chat/src/page.tsx +6 -4
- package/templates/chat/src/routes.ts +14 -4
- package/templates/chat/src/runtime.ts +9 -3
- package/templates/chat/template.json +3 -1
- package/templates/chat/test/chat.test.ts +46 -1
- package/templates/chat/test/server.test.ts +17 -3
- package/templates/nodejs/blend.yaml +2 -1
- package/templates/nodejs/gitignore +1 -0
- package/templates/nodejs/package.json +1 -0
- package/templates/nodejs/src/app.ts +76 -21
- package/templates/nodejs/src/frame.ts +17 -0
- package/templates/nodejs/src/main.ts +1 -6
- package/templates/nodejs/src/page.tsx +3 -3
- package/templates/nodejs/src/routes.ts +32 -13
- package/templates/nodejs/template.json +3 -1
- package/templates/nodejs/test/app.test.ts +38 -3
- package/templates/nodejs/test/server.test.ts +17 -3
- package/templates/notes/blend.yaml +2 -1
- package/templates/notes/gitignore +1 -0
- package/templates/notes/package.json +1 -0
- package/templates/notes/src/app.ts +72 -22
- package/templates/notes/src/frame.ts +17 -0
- package/templates/notes/src/main.ts +1 -6
- package/templates/notes/src/notes-app.ts +24 -17
- package/templates/notes/src/routes.ts +11 -4
- package/templates/notes/src/runtime.ts +9 -3
- package/templates/notes/src/views.tsx +26 -17
- package/templates/notes/template.json +3 -1
- package/templates/notes/test/notes.test.ts +39 -1
- package/templates/notes/test/server.test.ts +17 -3
- package/templates/todo/blend.yaml +3 -0
- package/templates/todo/gitignore +1 -0
- package/templates/todo/package.json +1 -0
- package/templates/todo/src/app.ts +81 -27
- package/templates/todo/src/door.ts +7 -0
- package/templates/todo/src/frame.ts +17 -0
- package/templates/todo/src/page.ts +13 -6
- package/templates/todo/src/todo.ts +18 -6
- package/templates/todo/template.json +11 -1
- package/templates/todo/test/todo.test.ts +67 -9
- package/lib/src-LX0E4ec4.js +0 -117
- package/shared/SKILL.md +0 -14
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State over a WebSocket. A server-sent event stream holds one of the browser's six connections per host for as long as the page lives, and an app that follows a few keys in
|
|
3
|
+
* several windows uses them all; a WebSocket is not one of those six, and one socket carries every key of a page.
|
|
4
|
+
*
|
|
5
|
+
* `GET /api/state/ws` upgrades. Up, one JSON message per frame: `{ "t": "sub", "key": "<key>" }` and `{ "t": "unsub", "key": "<key>" }`. Down: `{ "key": "<key>", "value": <json> }` once on
|
|
6
|
+
* `sub` and after every change, `{ "key": "<key>", "error": "<why>" }` for a key that cannot be followed, and `{ "t": "ping" }` every {@link SOCKET_HEARTBEAT_MS} so a page can tell a quiet
|
|
7
|
+
* socket from a dead one. `GET /api/state/client.js` is the browser module that speaks this (`watch(key, callback)`), with the event stream as its fallback.
|
|
8
|
+
*
|
|
9
|
+
* A WebSocket has no CORS, so the upgrade is checked here ({@link refuseStranger}): a loopback peer on this port with a loopback `Host`, and an `Origin` that, when the request has one,
|
|
10
|
+
* is exactly this app's own origin. A program on this machine that sends no `Origin` (a script, a test) is let in, as it is for the JSON and stream routes; a web page of another
|
|
11
|
+
* origin, and a name that only resolves to this machine (DNS rebinding), are not.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Context, MiddlewareHandler } from 'acryl-router'
|
|
15
|
+
import type { UpgradeWebSocket } from 'acryl-router/helpers'
|
|
16
|
+
import { loopbackAuthority } from 'acryl-router/middleware'
|
|
17
|
+
import type { ReadonlyCell } from './cell.ts'
|
|
18
|
+
|
|
19
|
+
/** How often the server writes a `ping`; a page gives up on a socket that is silent for three of these. */
|
|
20
|
+
export const SOCKET_HEARTBEAT_MS = 15_000
|
|
21
|
+
|
|
22
|
+
/** How many keys one socket may follow; a page watches a handful. */
|
|
23
|
+
export const MAX_KEYS_PER_SOCKET = 256
|
|
24
|
+
|
|
25
|
+
/** What the socket needs of the state service. */
|
|
26
|
+
export interface Cells {
|
|
27
|
+
find(key: string): ReadonlyCell<unknown> | undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StateSocketOptions {
|
|
31
|
+
readonly upgrade: UpgradeWebSocket
|
|
32
|
+
readonly heartbeatMs?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A refusal (403) unless the upgrade comes from this machine to this app, and from its own page when it comes from a page at all. */
|
|
36
|
+
export function refuseStranger(c: Context): Response | undefined {
|
|
37
|
+
const authority = loopbackAuthority(c)
|
|
38
|
+
if (authority === undefined) return c.json({ error: 'request authority rejected' }, 403)
|
|
39
|
+
const origin = c.req.header('origin')
|
|
40
|
+
const site = c.req.header('sec-fetch-site')
|
|
41
|
+
if ((origin !== undefined && origin !== authority.origin) || (site !== undefined && site !== 'same-origin')) {
|
|
42
|
+
return c.json({ error: 'only this app\'s own page may follow its state' }, 403)
|
|
43
|
+
}
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The route handler that upgrades and follows keys of `cells` for as long as the socket is open. */
|
|
48
|
+
export function stateSocket(cells: Cells, options: StateSocketOptions): MiddlewareHandler {
|
|
49
|
+
const heartbeatMs = options.heartbeatMs ?? SOCKET_HEARTBEAT_MS
|
|
50
|
+
return options.upgrade(() => {
|
|
51
|
+
const following = new Map<string, () => void>()
|
|
52
|
+
let beat: NodeJS.Timeout | undefined
|
|
53
|
+
return {
|
|
54
|
+
onOpen(_event, ws) {
|
|
55
|
+
beat = setInterval(() => {
|
|
56
|
+
try { ws.send(JSON.stringify({ t: 'ping' })) } catch { /* the close event follows */ }
|
|
57
|
+
}, heartbeatMs)
|
|
58
|
+
beat.unref()
|
|
59
|
+
},
|
|
60
|
+
onMessage(event, ws) {
|
|
61
|
+
if (typeof event.data !== 'string') return
|
|
62
|
+
let message: { t?: unknown, key?: unknown }
|
|
63
|
+
try { message = JSON.parse(event.data) as typeof message } catch { return }
|
|
64
|
+
if (typeof message.key !== 'string') return
|
|
65
|
+
const key = message.key
|
|
66
|
+
const tell = (frame: object): void => {
|
|
67
|
+
try { ws.send(JSON.stringify(frame)) } catch { /* the close event follows */ }
|
|
68
|
+
}
|
|
69
|
+
const value = (current: unknown): void => {
|
|
70
|
+
let text: string
|
|
71
|
+
try { text = JSON.stringify({ key, value: current === undefined ? null : current }) } catch { tell({ key, error: 'the value cannot be sent as JSON' }); return }
|
|
72
|
+
try { ws.send(text) } catch { /* the close event follows */ }
|
|
73
|
+
}
|
|
74
|
+
if (message.t === 'unsub') {
|
|
75
|
+
following.get(key)?.()
|
|
76
|
+
following.delete(key)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
if (message.t !== 'sub') return
|
|
80
|
+
const cell = cells.find(key)
|
|
81
|
+
if (cell === undefined) { tell({ key, error: 'no such state' }); return }
|
|
82
|
+
const known = following.has(key)
|
|
83
|
+
if (!known && following.size >= MAX_KEYS_PER_SOCKET) { tell({ key, error: 'too many keys on one socket' }); return }
|
|
84
|
+
// A key asked for twice is answered again with its value, and followed once.
|
|
85
|
+
if (known) { value(cell.get()); return }
|
|
86
|
+
value(cell.get())
|
|
87
|
+
following.set(key, cell.subscribe(value))
|
|
88
|
+
},
|
|
89
|
+
onClose() {
|
|
90
|
+
if (beat !== undefined) clearInterval(beat)
|
|
91
|
+
for (const stop of following.values()) stop()
|
|
92
|
+
following.clear()
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}, { textOnly: true })
|
|
96
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "acryl-web-server",
|
|
3
|
+
"description": "The webServer port of a framework-hosted app: the node request/upgrade interface DSH's driver rows register on, backed by acryl-node-server's raw routes. Ours, so the chat driver's connection and gateway run on our host without DSH's web server.",
|
|
4
|
+
"requires": ["acryl-node-server"],
|
|
5
|
+
"inject": ["nodeServer"],
|
|
6
|
+
"provides": ["webServer"],
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@deepseek-ai/cordis": "4.0.4"
|
|
9
|
+
},
|
|
10
|
+
"mount": "mount it before the DSH driver rows that inject webServer (the chat connection and its gateway); the framework's page serving applies its index taps",
|
|
11
|
+
"entries": {
|
|
12
|
+
"": "src/index.ts"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `webServer` port for a framework-hosted app. DSH's driver rows (the chat's connection and its remote gateway) register node handlers with
|
|
3
|
+
* `webServer.register` and `webServer.registerUpgrade`; this plugin serves them on the framework's own node server, so those rows run unchanged
|
|
4
|
+
* while the HTTP host, the page and the client-module serving stay ours.
|
|
5
|
+
*
|
|
6
|
+
* It is the inverse of `acryl-router-http`: there the Host's web server serves a router prefix; here our node server presents the web-server
|
|
7
|
+
* interface a DSH row expects. The surface is deliberately small: `host` and `port` (what `acryl-web` reads), `register` (exact or prefix),
|
|
8
|
+
* `registerUpgrade` (one exact path), and the index taps the page serving applies. A row that needs more of DSH's interface gets the extra
|
|
9
|
+
* method here, named with the row that needs it - so this seam can shrink when a row goes away.
|
|
10
|
+
*/
|
|
11
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
12
|
+
import type { Duplex } from 'node:stream'
|
|
13
|
+
import { Context, Service } from '@deepseek-ai/cordis'
|
|
14
|
+
import type { NodeServer, RawRoute, RawUpgradeRoute } from 'acryl-node-server'
|
|
15
|
+
|
|
16
|
+
/** Stable Cordis plugin name, equal to the package name and the Loader row id. */
|
|
17
|
+
export const name = 'acryl-web-server'
|
|
18
|
+
|
|
19
|
+
/** Hard requirement: the node server whose raw routes carry these handlers. */
|
|
20
|
+
export const inject = ['nodeServer']
|
|
21
|
+
|
|
22
|
+
/** A node handler at a path `exact` (the pathname itself) or a `prefix` (the path and everything below it). */
|
|
23
|
+
export interface WebServerRoute {
|
|
24
|
+
readonly kind: 'prefix' | 'exact'
|
|
25
|
+
readonly path: string
|
|
26
|
+
readonly handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One exact-path upgrade handler; the socket belongs to the handler once its path matches. */
|
|
30
|
+
export interface WebServerUpgradeRoute {
|
|
31
|
+
readonly path: string
|
|
32
|
+
readonly handler: (req: IncomingMessage, socket: Duplex, head: Buffer) => void | Promise<void>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Transforms the served index document, in registration order. */
|
|
36
|
+
export type IndexTap = (html: string) => string
|
|
37
|
+
|
|
38
|
+
/** What a driver row (or `acryl-web`) reads from `webServer`. */
|
|
39
|
+
export interface WebServerService {
|
|
40
|
+
/** The address the node server bound. */
|
|
41
|
+
readonly host: string
|
|
42
|
+
readonly port: number
|
|
43
|
+
/** Serve `route` until the returned remover runs or the calling plugin unloads. A path already served throws. */
|
|
44
|
+
register(route: WebServerRoute): () => void
|
|
45
|
+
/** Claim `route.path` for an upgrade until the returned remover runs or the calling plugin unloads. */
|
|
46
|
+
registerUpgrade(route: WebServerUpgradeRoute): () => void
|
|
47
|
+
/** Transform the served index document while the caller lives; returns its remover. */
|
|
48
|
+
tapIndex(transform: IndexTap): () => void
|
|
49
|
+
/** The document with the structured injection rows (the client modules' boot wire) and then every live tap applied. */
|
|
50
|
+
index(html: string): string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
declare module '@deepseek-ai/cordis' {
|
|
54
|
+
interface Context {
|
|
55
|
+
webServer: WebServerService
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One structured index-injection row (DSH's contract): the kinds the client modules and a door script use. */
|
|
60
|
+
interface IndexRow {
|
|
61
|
+
readonly kind?: string
|
|
62
|
+
readonly placement?: 'head' | 'body'
|
|
63
|
+
readonly text?: string
|
|
64
|
+
readonly src?: string
|
|
65
|
+
readonly name?: string
|
|
66
|
+
readonly value?: unknown
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The boot-readiness tail the client shell waits for, as DSH renders it. */
|
|
70
|
+
const READY_MARKUP = '<script>(globalThis.__DSH_BOOT_READY__ ??= Promise.withResolvers()).resolve()</script>'
|
|
71
|
+
|
|
72
|
+
/** A JSON literal safe inside a `<script>`: a sequence like `</script>` must not end it. */
|
|
73
|
+
const scriptJson = (value: unknown): string => JSON.stringify(value).replaceAll('</', '<\\/')
|
|
74
|
+
|
|
75
|
+
/** Render one row; an unknown kind is skipped rather than emitted as broken markup. */
|
|
76
|
+
function rowMarkup(row: IndexRow): string {
|
|
77
|
+
if (row.kind === 'script') return `<script>${row.text ?? ''}</script>`
|
|
78
|
+
if (row.kind === 'script-src') return `<script type="module" crossorigin src="${row.src ?? ''}"></script>`
|
|
79
|
+
if (row.kind === 'script-preload') return `<link rel="modulepreload" crossorigin href="${row.src ?? ''}" />`
|
|
80
|
+
if (row.kind === 'global') return `<script>globalThis[${scriptJson(row.name ?? '')}] = ${scriptJson(row.value)}</script>`
|
|
81
|
+
return ''
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Insert each group after its opening tag, as DSH's served index does; a document without the tag gets the group first. */
|
|
85
|
+
export function renderIndexRows(html: string, rows: readonly IndexRow[]): string {
|
|
86
|
+
let head = ''
|
|
87
|
+
let body = ''
|
|
88
|
+
for (const row of rows) {
|
|
89
|
+
const markup = rowMarkup(row)
|
|
90
|
+
if (markup === '') continue
|
|
91
|
+
if (row.placement === 'body') body += markup
|
|
92
|
+
else head += markup
|
|
93
|
+
}
|
|
94
|
+
body += READY_MARKUP
|
|
95
|
+
let document = html
|
|
96
|
+
const headOpen = /<head(?:\s[^>]*)?>/iu.exec(document)
|
|
97
|
+
if (head !== '') {
|
|
98
|
+
const at = headOpen === null ? 0 : headOpen.index + headOpen[0].length
|
|
99
|
+
document = `${document.slice(0, at)}${head}${document.slice(at)}`
|
|
100
|
+
}
|
|
101
|
+
const bodyOpen = /<body(?:\s[^>]*)?>/iu.exec(document)
|
|
102
|
+
if (body !== '') {
|
|
103
|
+
const at = bodyOpen === null ? document.length : bodyOpen.index + bodyOpen[0].length
|
|
104
|
+
document = `${document.slice(0, at)}${body}${document.slice(at)}`
|
|
105
|
+
}
|
|
106
|
+
return document
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The port, backed by `nodeServer`'s raw routes; every registration belongs to the plugin that made it. */
|
|
110
|
+
export class WebServer extends Service implements WebServerService {
|
|
111
|
+
private readonly taps: IndexTap[] = []
|
|
112
|
+
|
|
113
|
+
constructor(ctx: Context) {
|
|
114
|
+
super(ctx, 'webServer')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get host(): string {
|
|
118
|
+
return this.ctx.nodeServer.hostname
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get port(): number {
|
|
122
|
+
return this.ctx.nodeServer.port
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
register(route: WebServerRoute): () => void {
|
|
126
|
+
const remove = this.ctx.nodeServer.registerRaw(route as RawRoute)
|
|
127
|
+
this.ctx.effect(() => () => { remove() }, `acryl-web-server: ${route.kind} ${route.path}`)
|
|
128
|
+
return remove
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
registerUpgrade(route: WebServerUpgradeRoute): () => void {
|
|
132
|
+
const remove = this.ctx.nodeServer.registerRawUpgrade(route as RawUpgradeRoute)
|
|
133
|
+
this.ctx.effect(() => () => { remove() }, `acryl-web-server: upgrade ${route.path}`)
|
|
134
|
+
return remove
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
tapIndex(transform: IndexTap): () => void {
|
|
138
|
+
this.taps.push(transform)
|
|
139
|
+
const remove = (): void => {
|
|
140
|
+
const at = this.taps.indexOf(transform)
|
|
141
|
+
if (at >= 0) this.taps.splice(at, 1)
|
|
142
|
+
}
|
|
143
|
+
this.ctx.effect(() => () => { remove() }, 'acryl-web-server: index tap')
|
|
144
|
+
return remove
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
index(html: string): string {
|
|
148
|
+
// The rows are DSH's index-injection contract (`webserver/index-inject`): the client-module row pushes its boot wire there. The rows render
|
|
149
|
+
// first, the string taps after, the same order DSH's own web server uses.
|
|
150
|
+
const rows: IndexRow[] = []
|
|
151
|
+
;(this.ctx as unknown as { emit(name: string, table: IndexRow[]): void }).emit('webserver/index-inject', rows)
|
|
152
|
+
let document = renderIndexRows(html, rows)
|
|
153
|
+
for (const tap of [...this.taps]) document = tap(document)
|
|
154
|
+
return document
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Provide `webServer` on this context; the node server must already be up (the plugin injects it). */
|
|
159
|
+
export function apply(ctx: Context): void {
|
|
160
|
+
new WebServer(ctx)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Re-exported for callers that shape their own routes: the raw types the port is built on. */
|
|
164
|
+
export type { NodeServer, RawRoute, RawUpgradeRoute }
|
package/plugins/agent/agent.ts
CHANGED
|
@@ -24,6 +24,8 @@ export type RunResult = (
|
|
|
24
24
|
export interface RunOptions {
|
|
25
25
|
/** Aborting it stops the run: the model call or tool in flight is dropped and the run ends `{ status: 'cancelled' }`. */
|
|
26
26
|
readonly signal?: AbortSignal
|
|
27
|
+
/** The door's id of the page the run was asked from: every tool call carries it, so the page tools drive that page and no other (a production assistant serves many visitors). */
|
|
28
|
+
readonly page?: string
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface Agent {
|
|
@@ -39,6 +41,16 @@ export interface Budget {
|
|
|
39
41
|
export interface AgentConfig {
|
|
40
42
|
/** Steps per run before it stops. */
|
|
41
43
|
steps?: number
|
|
44
|
+
/**
|
|
45
|
+
* The only tools a run may see and call; absent: every tool in the registry. A production assistant gets the page tools alone (`door_look`, `door_act`), so whatever
|
|
46
|
+
* a visitor asks, the agent can use the app and never change it. A call to any other name fails as an unknown tool would.
|
|
47
|
+
*/
|
|
48
|
+
tools?: readonly string[]
|
|
49
|
+
/**
|
|
50
|
+
* The tool whose result an answer must come right after. When the model answers after another tool call (it acted and did not look), the agent calls this tool
|
|
51
|
+
* itself, once, and asks the model again with what it shows. The production assistant sets `door_look`, so what it tells a visitor is what the page shows now.
|
|
52
|
+
*/
|
|
53
|
+
answerAfter?: string
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
/**
|
|
@@ -91,6 +103,8 @@ export const agent = {
|
|
|
91
103
|
inject: ['model', 'tools', 'memory'],
|
|
92
104
|
apply(ctx: Context, config?: AgentConfig) {
|
|
93
105
|
const limit = config?.steps ?? 6
|
|
106
|
+
const allowed = config?.tools === undefined ? undefined : new Set(config.tools)
|
|
107
|
+
const answerAfter = config?.answerAfter
|
|
94
108
|
let runs = 0
|
|
95
109
|
ctx.provide('agent', {
|
|
96
110
|
async run(question, options) {
|
|
@@ -104,13 +118,15 @@ export const agent = {
|
|
|
104
118
|
)
|
|
105
119
|
// Assigned inside the run's fiber; `as` keeps TypeScript from narrowing it to this first value.
|
|
106
120
|
let result = { status: 'budget_exceeded', steps: 0, trace } as RunResult
|
|
107
|
-
|
|
121
|
+
// Each run's `budget` is its own: runs may overlap (the production Assistant serves visitors at once), and a service name is shared unless isolated.
|
|
122
|
+
const task = ctx.isolate('budget').plugin({
|
|
108
123
|
name: `task-${runs}`,
|
|
109
124
|
inject: ['model', 'tools', 'memory'],
|
|
110
125
|
async apply(scope: Context) {
|
|
111
126
|
scope.provide('budget', { steps: 0, limit } satisfies Budget)
|
|
112
127
|
const budget = scope.budget
|
|
113
128
|
let toolResult: unknown
|
|
129
|
+
let looked = false
|
|
114
130
|
while (budget.steps < budget.limit) {
|
|
115
131
|
if (signal.aborted) {
|
|
116
132
|
result = { status: 'cancelled', steps: budget.steps, trace }
|
|
@@ -124,7 +140,7 @@ export const agent = {
|
|
|
124
140
|
step: budget.steps,
|
|
125
141
|
toolResult,
|
|
126
142
|
trace: [...trace],
|
|
127
|
-
tools: scope.tools.list(),
|
|
143
|
+
tools: scope.tools.list().filter((tool) => allowed?.has(tool.name) ?? true),
|
|
128
144
|
signal,
|
|
129
145
|
}),
|
|
130
146
|
signal,
|
|
@@ -136,16 +152,31 @@ export const agent = {
|
|
|
136
152
|
}
|
|
137
153
|
budget.steps += 1
|
|
138
154
|
if (step.tool === undefined) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
const last = trace.at(-1)
|
|
156
|
+
const unseen =
|
|
157
|
+
answerAfter !== undefined &&
|
|
158
|
+
last !== undefined &&
|
|
159
|
+
last.tool !== answerAfter &&
|
|
160
|
+
!looked
|
|
161
|
+
if (!unseen) {
|
|
162
|
+
scope.memory.remember(`${question} => ${step.text}`)
|
|
163
|
+
result = { status: 'completed', output: step.text, steps: budget.steps, trace }
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
step = { tool: answerAfter, input: {} }
|
|
167
|
+
looked = true
|
|
168
|
+
} else looked = false
|
|
143
169
|
const call = record
|
|
144
170
|
? recorder?.start(`tool:${step.tool}`, step.input, { parent: record.id })
|
|
145
171
|
: undefined
|
|
146
172
|
try {
|
|
173
|
+
if (allowed !== undefined && !allowed.has(step.tool))
|
|
174
|
+
throw new Error(`no tool named "${step.tool}" is allowed in this run`)
|
|
147
175
|
toolResult = await until(
|
|
148
|
-
scope.tools.call(step.tool, step.input, {
|
|
176
|
+
scope.tools.call(step.tool, step.input, {
|
|
177
|
+
signal,
|
|
178
|
+
...(options?.page === undefined ? {} : { page: options.page }),
|
|
179
|
+
}),
|
|
149
180
|
signal,
|
|
150
181
|
)
|
|
151
182
|
trace.push({
|
package/plugins/agent/index.ts
CHANGED
package/plugins/agent/model.ts
CHANGED
|
@@ -52,8 +52,16 @@ declare module '@deepseek-ai/cordis' {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
const builtIn: Rule[] = [
|
|
55
|
-
({ toolResult, step }) =>
|
|
56
|
-
step
|
|
55
|
+
({ toolResult, step }) => {
|
|
56
|
+
if (step === 0) return undefined
|
|
57
|
+
// A failed call comes back as `{ error }`: say what failed, not "[object Object]".
|
|
58
|
+
const failed = typeof toolResult === 'object' && toolResult !== null && 'error' in toolResult
|
|
59
|
+
return {
|
|
60
|
+
text: failed
|
|
61
|
+
? `That did not work: ${String(toolResult.error)}.`
|
|
62
|
+
: `The answer is ${String(toolResult)}.`,
|
|
63
|
+
}
|
|
64
|
+
},
|
|
57
65
|
({ question }) => {
|
|
58
66
|
const sum = /^add\s+(-?\d+)\s+(-?\d+)$/.exec(question.trim())
|
|
59
67
|
return sum ? { tool: 'add', input: [Number(sum[1]), Number(sum[2])] } : undefined
|
package/plugins/agent/tools.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis'
|
|
2
2
|
|
|
3
|
+
/** What a run passes with each call: its signal, and the door's id of the page it was asked from (the page tools drive only that page). */
|
|
4
|
+
export interface CallOptions {
|
|
5
|
+
readonly signal?: AbortSignal
|
|
6
|
+
readonly page?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
/** One tool: what it does (for the model) and how to run it. */
|
|
4
10
|
export interface Tool {
|
|
5
11
|
readonly description: string
|
|
6
12
|
/** The JSON Schema of the input (an object schema), sent to a real model so it knows which fields the tool takes. A tool without one takes any object. */
|
|
7
13
|
readonly parameters?: Readonly<Record<string, unknown>>
|
|
8
|
-
/** `options.signal` aborts when the run is cancelled; a tool that can stop its work listens to it. */
|
|
9
|
-
run(input: unknown, options?:
|
|
14
|
+
/** `options.signal` aborts when the run is cancelled; a tool that can stop its work listens to it. `options.page` is the page the run was asked from, when it was. */
|
|
15
|
+
run(input: unknown, options?: CallOptions): unknown | Promise<unknown>
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
/** The tool registry. A plugin registers its tools inside `ctx.effect`, so they leave with it. */
|
|
@@ -18,7 +24,7 @@ export interface Tools {
|
|
|
18
24
|
description: string
|
|
19
25
|
parameters?: Readonly<Record<string, unknown>>
|
|
20
26
|
}>
|
|
21
|
-
call(name: string, input: unknown, options?:
|
|
27
|
+
call(name: string, input: unknown, options?: CallOptions): Promise<unknown>
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
declare module '@deepseek-ai/cordis' {
|
|
@@ -42,3 +42,60 @@ export function describeTool(data: Record<string, unknown>): string | undefined
|
|
|
42
42
|
const failed = data.error ? ` failed: ${clip(String(data.error))}` : ''
|
|
43
43
|
return `${tool}${input}${failed}`
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/** What an act touched, from its result: the door's chat tool answers `done: link "My tasks" (detail); look again before the next act`, its verb `{ target }`. */
|
|
47
|
+
function targetOf(result: unknown): { role?: string; name?: string } {
|
|
48
|
+
if (typeof result === 'string') {
|
|
49
|
+
const match = /^done: (\S+) "(.*)"(?: \(.*\))?; look again/u.exec(result)
|
|
50
|
+
return match === null ? {} : { role: match[1], name: match[2] }
|
|
51
|
+
}
|
|
52
|
+
if (typeof result !== 'object' || result === null) return {}
|
|
53
|
+
const target = (result as { target?: unknown }).target
|
|
54
|
+
if (typeof target !== 'object' || target === null) return {}
|
|
55
|
+
const { role, name } = target as { role?: unknown; name?: unknown }
|
|
56
|
+
return {
|
|
57
|
+
...(typeof role === 'string' ? { role } : {}),
|
|
58
|
+
...(typeof name === 'string' ? { name } : {}),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The Assistant's step on the page, as the person reads it under the answer: what it did, named the way the page names it. `ui.open("My tasks")`,
|
|
64
|
+
* `ui.fill("Due", "This week")`, `ui.click("Save")`, `ui.press("Enter")`. A look changes nothing, so it is left out unless it failed. The control's name comes from the
|
|
65
|
+
* act's result (the page says what it acted on); the ref is the fallback.
|
|
66
|
+
*/
|
|
67
|
+
export function describePageStep(data: Record<string, unknown>): string | undefined {
|
|
68
|
+
const failed = data.error ? ' failed' : ''
|
|
69
|
+
if (data.tool === 'door_look') return failed === '' ? undefined : 'ui.look() failed'
|
|
70
|
+
if (data.tool !== 'door_act') return describeTool(data)
|
|
71
|
+
const input = (typeof data.input === 'object' && data.input !== null ? data.input : {}) as Record<
|
|
72
|
+
string,
|
|
73
|
+
unknown
|
|
74
|
+
>
|
|
75
|
+
const target = targetOf(data.result)
|
|
76
|
+
const quoted = (value: unknown): string => JSON.stringify(clip(String(value ?? '')))
|
|
77
|
+
const op = typeof input.op === 'string' ? input.op : 'act'
|
|
78
|
+
const named =
|
|
79
|
+
typeof target.name === 'string' && target.name !== ''
|
|
80
|
+
? quoted(target.name)
|
|
81
|
+
: typeof input.ref === 'string'
|
|
82
|
+
? input.ref
|
|
83
|
+
: undefined
|
|
84
|
+
const [verb, args]: [string, readonly (string | undefined)[]] =
|
|
85
|
+
op === 'navigate'
|
|
86
|
+
? ['open', [quoted(input.path)]]
|
|
87
|
+
: op === 'click' && target.role === 'link'
|
|
88
|
+
? ['open', [named]]
|
|
89
|
+
: op === 'type'
|
|
90
|
+
? ['fill', [named, quoted(input.text)]]
|
|
91
|
+
: op === 'select'
|
|
92
|
+
? ['fill', [named, quoted(input.option)]]
|
|
93
|
+
: op === 'press'
|
|
94
|
+
? ['press', [quoted(input.key)]]
|
|
95
|
+
: op === 'scroll'
|
|
96
|
+
? ['scroll', [input.direction === undefined ? undefined : quoted(input.direction)]]
|
|
97
|
+
: op === 'wait'
|
|
98
|
+
? ['wait', [quoted(input.text ?? input.name)]]
|
|
99
|
+
: [op, [named]]
|
|
100
|
+
return `ui.${verb}(${args.filter((arg) => arg !== undefined).join(', ')})${failed}`
|
|
101
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which driver pick a finished terminal open may apply to. `choose()` can be in flight when the person picks
|
|
3
|
+
* another driver (the panel also restores a remembered terminal at start), and only the latest pick may win: a
|
|
4
|
+
* stale terminal must not replace the form the person chose.
|
|
5
|
+
*/
|
|
6
|
+
export interface PickGeneration {
|
|
7
|
+
/** Start a pick and return the token its result is settled with. */
|
|
8
|
+
begin(): number
|
|
9
|
+
/** True only while `generation` is still the latest pick. */
|
|
10
|
+
isCurrent(generation: number): boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createPickGeneration(): PickGeneration {
|
|
14
|
+
let generation = 0
|
|
15
|
+
return {
|
|
16
|
+
begin: () => {
|
|
17
|
+
generation += 1
|
|
18
|
+
return generation
|
|
19
|
+
},
|
|
20
|
+
isCurrent: (candidate) => candidate === generation,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ACRYL frame's view as the app page sees it (ide/acryl-blends-frame/src/client/frame-view.ts): the frame's canvas hides the app's own build-time
|
|
3
|
+
* controls in Build and shows what production shows in Preview. The page is same-origin with the frame, so it reads the handle the frame publishes on
|
|
4
|
+
* its window through `window.parent`; a page opened directly has no such parent and keeps its own behavior.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const FRAME_VIEW_KEY: symbol = Symbol.for('acryl.frame.view')
|
|
8
|
+
export const FRAME_MODE_EVENT = 'acryl:frame-mode'
|
|
9
|
+
|
|
10
|
+
/** What the app page reads from `window.parent[FRAME_VIEW_KEY]`. */
|
|
11
|
+
export interface FrameViewHandle {
|
|
12
|
+
readonly mode: 'build' | 'preview'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The frame's view handle when this page runs in the frame's canvas.
|
|
17
|
+
* @param win - the page's window.
|
|
18
|
+
* @returns the handle, or undefined when the page is the top window or its parent is cross-origin.
|
|
19
|
+
*/
|
|
20
|
+
export function frameViewOf(win: Window = window): FrameViewHandle | undefined {
|
|
21
|
+
try {
|
|
22
|
+
if (win.parent === win) return undefined
|
|
23
|
+
return (win.parent as unknown as Record<symbol, FrameViewHandle | undefined>)[FRAME_VIEW_KEY]
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether the panel is the production Assistant: the Host itself is the Assistant, or the app's production environment names it (ACRYL_ASSISTANT=on)
|
|
31
|
+
* and the page runs in the frame, whose Preview shows the app as production would.
|
|
32
|
+
* @param session - the panel's session from its Host.
|
|
33
|
+
* @param frame - the frame's view handle, if any.
|
|
34
|
+
*/
|
|
35
|
+
export function panelVariant(
|
|
36
|
+
session: { readonly assistant?: boolean; readonly assistantInProduction?: boolean },
|
|
37
|
+
frame: FrameViewHandle | undefined,
|
|
38
|
+
): boolean {
|
|
39
|
+
if (session.assistant === true) return true
|
|
40
|
+
return frame !== undefined && session.assistantInProduction === true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether the panel shows at all: on a page opened directly, yes; in the frame, only in Preview, and only as the Assistant. Build is the builder's
|
|
45
|
+
* screen, where the app's own panel has no place over the canvas.
|
|
46
|
+
* @param frame - the frame's view handle, if any.
|
|
47
|
+
* @param assistant - whether this panel is the production Assistant (see {@link panelVariant}).
|
|
48
|
+
*/
|
|
49
|
+
export function panelVisible(frame: FrameViewHandle | undefined, assistant: boolean): boolean {
|
|
50
|
+
return frame === undefined || (frame.mode === 'preview' && assistant)
|
|
51
|
+
}
|