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
|
@@ -4,20 +4,62 @@
|
|
|
4
4
|
* the kill switch. It touches only this document, never another window and never the OS.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { appBaseOf, underAppBase } from './base.ts'
|
|
7
8
|
import { PageChannel } from './channel.ts'
|
|
8
9
|
import { UiDriver } from './driver/driver.ts'
|
|
9
10
|
import { mountIndicator } from './indicator.ts'
|
|
11
|
+
import { mountOutlines } from './outline.ts'
|
|
10
12
|
import { UserInputClock } from './user-input.ts'
|
|
11
13
|
|
|
12
14
|
export const DOOR_BASE = '/api/acryl-door'
|
|
13
15
|
|
|
16
|
+
/** The key on the page's `window` under which the door publishes `{ windowId }`: the agent panel in the same page sends it with a question, and the door drives that page for the run. */
|
|
17
|
+
export const PAGE_KEY = Symbol.for('acryl.door.page')
|
|
18
|
+
|
|
19
|
+
/** Where the page keeps its tab's id: per tab, kept across reloads and moves within the app, never shared with another tab. */
|
|
20
|
+
const TAB_KEY = 'acryl-door-tab'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* This tab's id: the page that replaces this one in the same tab (a reload, a move to another path) says the same, so the Host keeps driving that tab for a run asked from it.
|
|
24
|
+
* `undefined` when the browser keeps no session storage.
|
|
25
|
+
*/
|
|
26
|
+
function tabOf(view: Window): string | undefined {
|
|
27
|
+
try {
|
|
28
|
+
const kept = view.sessionStorage.getItem(TAB_KEY)
|
|
29
|
+
if (kept !== null && /^[A-Za-z0-9-]{8,80}$/u.test(kept)) return kept
|
|
30
|
+
const made = typeof view.crypto?.randomUUID === 'function' ? view.crypto.randomUUID() : undefined
|
|
31
|
+
if (made === undefined) return undefined
|
|
32
|
+
view.sessionStorage.setItem(TAB_KEY, made)
|
|
33
|
+
return made
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
/** Start the door on `document`. @returns the disposer. */
|
|
15
40
|
export function startDoor(document: Document, base: string = DOOR_BASE): () => void {
|
|
16
41
|
const view = document.defaultView
|
|
17
42
|
if (view === null) throw new Error('acryl-door: the document has no window')
|
|
18
43
|
const clock = new UserInputClock()
|
|
19
|
-
const
|
|
44
|
+
const appBase = appBaseOf(document)
|
|
45
|
+
// The ring on whatever the agent touches, drawn in this page (the frame's canvas and production's assistant both get it).
|
|
46
|
+
const outlines = mountOutlines(document)
|
|
47
|
+
const driver = new UiDriver({
|
|
48
|
+
document: () => document,
|
|
49
|
+
msSinceUserInput: clock.msSince,
|
|
50
|
+
touched: (element) => { outlines.touched(element) },
|
|
51
|
+
// A host that serves the app under a prefix (the ACRYL frame's canvas) marks its pages: an app path navigates under that prefix, so the same
|
|
52
|
+
// `act navigate --path /board` reaches the app's /board in the frame and on the app's own server.
|
|
53
|
+
...(appBase === '' ? {} : {
|
|
54
|
+
leave: (to: { readonly reload: true } | { readonly path: string }): void => {
|
|
55
|
+
if ('reload' in to) view.location.reload()
|
|
56
|
+
else view.location.assign(underAppBase(appBase, to.path))
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
})
|
|
20
60
|
const windowId = typeof view.crypto?.randomUUID === 'function' ? view.crypto.randomUUID() : `w-${String(Date.now())}-${String(Math.random()).slice(2, 8)}`
|
|
61
|
+
Object.defineProperty(view, PAGE_KEY, { value: Object.freeze({ windowId }), configurable: true })
|
|
62
|
+
const tab = tabOf(view)
|
|
21
63
|
const focused = (): boolean => document.visibilityState === 'visible' && document.hasFocus()
|
|
22
64
|
const detachInput = clock.attach(document)
|
|
23
65
|
const removeIndicator = mountIndicator(driver, document)
|
|
@@ -30,7 +72,7 @@ export function startDoor(document: Document, base: string = DOOR_BASE): () => v
|
|
|
30
72
|
return 'auto'
|
|
31
73
|
}
|
|
32
74
|
})()
|
|
33
|
-
const channel = new PageChannel(driver, { windowId, focused, base, transport, ...(Number.isSafeInteger(shellWindow) && shellWindow > 0 ? { shellWindow } : {}) })
|
|
75
|
+
const channel = new PageChannel(driver, { windowId, ...(tab === undefined ? {} : { tab }), focused, base, transport, onRun: running => { driver.setRunning(running) }, ...(Number.isSafeInteger(shellWindow) && shellWindow > 0 ? { shellWindow } : {}) })
|
|
34
76
|
const reportFocus = (): void => { channel.reportFocus(focused()) }
|
|
35
77
|
view.addEventListener('focus', reportFocus)
|
|
36
78
|
view.addEventListener('blur', reportFocus)
|
|
@@ -47,8 +89,10 @@ export function startDoor(document: Document, base: string = DOOR_BASE): () => v
|
|
|
47
89
|
view.removeEventListener('pagehide', leave)
|
|
48
90
|
view.removeEventListener('pageshow', back)
|
|
49
91
|
document.removeEventListener('visibilitychange', reportFocus)
|
|
92
|
+
Reflect.deleteProperty(view, PAGE_KEY)
|
|
50
93
|
channel.dispose()
|
|
51
94
|
removeIndicator()
|
|
95
|
+
outlines.dispose()
|
|
52
96
|
detachInput()
|
|
53
97
|
// Pending calls settle as `unloaded` and every ref goes stale.
|
|
54
98
|
driver.dispose()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* "The agent is driving" bar with its kill switch, in plain DOM (the React one in `acryl-agent-control` shows the same states). Shown
|
|
3
|
-
* a call runs and after the person stopped the agent.
|
|
2
|
+
* "The agent is driving" bar with its kill switch, in plain DOM (the React one in `acryl-agent-control` shows the same states). Shown for the whole of a run asked for from
|
|
3
|
+
* this page's panel (between the calls too), while a call runs, and after the person stopped the agent. Stop kills the page's control and asks the panel to stop the run (the
|
|
4
|
+
* `acryl:stop-run` event on the window, which the panel listens for). It is marked as driver UI (so snapshots skip it) and as off limits to the agent (so
|
|
4
5
|
* it can never press its own Stop or Allow buttons). Escape stops the agent too, while it is working.
|
|
5
6
|
*/
|
|
6
7
|
|
|
@@ -8,6 +9,9 @@ import type { DriverActivity, UiDriver } from './driver/driver.ts'
|
|
|
8
9
|
|
|
9
10
|
const LABELS: Record<string, string> = { snapshot: 'looking at the page', click: 'clicking', type: 'typing', select: 'choosing an option', press: 'pressing a key', scroll: 'scrolling', wait: 'waiting' }
|
|
10
11
|
|
|
12
|
+
/** Dispatched on the page's window by the Stop button: the agent panel in the same page cancels its run. */
|
|
13
|
+
export const STOP_RUN_EVENT = 'acryl:stop-run'
|
|
14
|
+
|
|
11
15
|
const STYLE_ID = 'acryl-door-indicator-style'
|
|
12
16
|
const STYLES = `
|
|
13
17
|
.acrylDoorBar { position: fixed; z-index: 2147483647; left: 50%; bottom: 16px; transform: translateX(-50%); display: flex; align-items: center; gap: 10px;
|
|
@@ -32,13 +36,17 @@ export function mountIndicator(driver: UiDriver, document: Document): () => void
|
|
|
32
36
|
bar.setAttribute('role', 'status')
|
|
33
37
|
bar.setAttribute('aria-live', 'polite')
|
|
34
38
|
|
|
39
|
+
const stopAgent = (): void => {
|
|
40
|
+
driver.kill()
|
|
41
|
+
document.defaultView?.dispatchEvent(new CustomEvent(STOP_RUN_EVENT))
|
|
42
|
+
}
|
|
35
43
|
const onKey = (event: KeyboardEvent): void => {
|
|
36
|
-
if (event.key === 'Escape' && event.isTrusted && driver.activity.busy
|
|
44
|
+
if (event.key === 'Escape' && event.isTrusted && (driver.activity.busy || driver.activity.running)) stopAgent()
|
|
37
45
|
}
|
|
38
46
|
document.addEventListener('keydown', onKey, true)
|
|
39
47
|
|
|
40
48
|
const render = (activity: DriverActivity): void => {
|
|
41
|
-
if (!activity.busy && !activity.killed) { bar.remove(); return }
|
|
49
|
+
if (!activity.busy && !activity.running && !activity.killed) { bar.remove(); return }
|
|
42
50
|
const button = document.createElement('button')
|
|
43
51
|
button.type = 'button'
|
|
44
52
|
const text = document.createElement('span')
|
|
@@ -53,9 +61,9 @@ export function mountIndicator(driver: UiDriver, document: Document): () => void
|
|
|
53
61
|
const dot = document.createElement('span')
|
|
54
62
|
dot.className = 'acrylDoorDot'
|
|
55
63
|
dot.setAttribute('aria-hidden', 'true')
|
|
56
|
-
text.textContent = `Agent is driving: ${LABELS[activity.doing] ?? activity.doing}`
|
|
64
|
+
text.textContent = `Agent is driving: ${activity.busy ? (LABELS[activity.doing] ?? activity.doing) : 'working on your request'}`
|
|
57
65
|
button.textContent = 'Stop'
|
|
58
|
-
button.addEventListener('click',
|
|
66
|
+
button.addEventListener('click', stopAgent)
|
|
59
67
|
bar.replaceChildren(dot, text, button)
|
|
60
68
|
}
|
|
61
69
|
if (!bar.isConnected) document.body.append(bar)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A green ring on the element the agent just used, for a moment (docs/design/blends-frame-mockup.html outlines the "My tasks" the assistant opened). It is
|
|
3
|
+
* drawn here, in the page itself, so the person sees what the agent touched in the frame's canvas and in production's assistant alike, and so it goes on its
|
|
4
|
+
* own: nothing outside the page has to draw it, position it, or take it away.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const STYLE_ID = 'acryl-door-outline-style'
|
|
8
|
+
/** The class the ring is drawn with: the page's element keeps its own styles, and an outline follows the element's shape. */
|
|
9
|
+
const CLASS = 'acrylDoorOutline'
|
|
10
|
+
/** How long the ring stays after the agent's last act; each act replaces it, so one run leaves one ring, a moment after its last step. */
|
|
11
|
+
export const OUTLINE_MS = 2000
|
|
12
|
+
|
|
13
|
+
const STYLES = `.${CLASS} { outline: 2px solid #3fb67b !important; outline-offset: 1px; }`
|
|
14
|
+
|
|
15
|
+
export interface Outlines {
|
|
16
|
+
/** Ring `element` as what the agent just used; the previous ring goes. */
|
|
17
|
+
touched(element: Element): void
|
|
18
|
+
/** Take the ring off and remove the style: nothing of this remains in the page. */
|
|
19
|
+
dispose(): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Draw the ring in `document`. @returns the control the driver calls, and the disposer that leaves the page as it was. */
|
|
23
|
+
export function mountOutlines(document: Document): Outlines {
|
|
24
|
+
if (document.getElementById(STYLE_ID) === null) {
|
|
25
|
+
const style = document.createElement('style')
|
|
26
|
+
style.id = STYLE_ID
|
|
27
|
+
style.textContent = STYLES
|
|
28
|
+
document.head.append(style)
|
|
29
|
+
}
|
|
30
|
+
let ringed: Element | undefined
|
|
31
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
32
|
+
const clear = (): void => {
|
|
33
|
+
if (timer !== undefined) {
|
|
34
|
+
clearTimeout(timer)
|
|
35
|
+
timer = undefined
|
|
36
|
+
}
|
|
37
|
+
ringed?.classList.remove(CLASS)
|
|
38
|
+
ringed = undefined
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
touched(element: Element): void {
|
|
42
|
+
// The same control again in one run keeps its ring where it is; the person sees one ring, not a flicker.
|
|
43
|
+
if (element !== ringed) {
|
|
44
|
+
ringed?.classList.remove(CLASS)
|
|
45
|
+
ringed = element
|
|
46
|
+
element.classList.add(CLASS)
|
|
47
|
+
}
|
|
48
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
49
|
+
timer = setTimeout(clear, OUTLINE_MS)
|
|
50
|
+
},
|
|
51
|
+
dispose(): void {
|
|
52
|
+
clear()
|
|
53
|
+
document.getElementById(STYLE_ID)?.remove()
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -60,6 +60,8 @@ export interface UiNode {
|
|
|
60
60
|
readonly value?: string
|
|
61
61
|
/** Heading level, when it is a heading. */
|
|
62
62
|
readonly level?: number
|
|
63
|
+
/** The choices of a select, as `act select` takes them, when it is not sensitive. */
|
|
64
|
+
readonly options?: readonly string[]
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export interface UiSnapshot {
|
|
@@ -88,6 +90,8 @@ export interface UiActionResult {
|
|
|
88
90
|
readonly ok: true
|
|
89
91
|
readonly target?: { readonly role: string; readonly name: string }
|
|
90
92
|
readonly detail?: string
|
|
93
|
+
/** The act started a navigation (a link, a form that submits): this page is going away, and the Host waits for the next one. */
|
|
94
|
+
readonly leaving?: true
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
export type UiResult = UiSnapshot | UiActionResult
|
|
@@ -11,7 +11,8 @@ export function renderSnapshot(snapshot: UiSnapshot, more: (cursor: number) => s
|
|
|
11
11
|
const level = node.level === undefined ? '' : ` level=${String(node.level)}`
|
|
12
12
|
const states = node.states.length === 0 ? '' : ` [${node.states.join(', ')}]`
|
|
13
13
|
const value = node.value === undefined ? '' : ` value=${JSON.stringify(node.value)}`
|
|
14
|
-
|
|
14
|
+
const options = node.options === undefined ? '' : ` options=${JSON.stringify(node.options)}`
|
|
15
|
+
return `${' '.repeat(Math.min(node.depth, 8))}- ${node.role}${node.name === '' ? '' : ` ${JSON.stringify(node.name)}`}${level}${states}${value}${options} [ref=${node.ref}]`
|
|
15
16
|
})
|
|
16
17
|
const rest = snapshot.nextCursor === undefined ? '' : `\n(${String(snapshot.total - snapshot.nextCursor)} more elements; ${more(snapshot.nextCursor)})`
|
|
17
18
|
return `Page: ${snapshot.title || '(untitled)'}\nSnapshot ${String(snapshot.generation)}, ${String(snapshot.nodes.length)} of ${String(snapshot.total)} elements:\n${lines.join('\n')}${rest}`
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from './request.ts'
|
|
15
15
|
import { defaultContentType, cacheKey, Response as LightweightResponse } from './response.ts'
|
|
16
16
|
import type { InternalCache } from './response.ts'
|
|
17
|
+
import { RAW_RESPONSE_HANDLED } from './raw.ts'
|
|
17
18
|
import type { CustomErrorHandler, FetchCallback, HttpBindings } from './types.ts'
|
|
18
19
|
import {
|
|
19
20
|
readWithoutBlocking,
|
|
@@ -500,6 +501,9 @@ export const getRequestListener = (
|
|
|
500
501
|
res = fetchCallback(req, { incoming, outgoing } as HttpBindings) as
|
|
501
502
|
| Response
|
|
502
503
|
| Promise<Response>
|
|
504
|
+
| typeof RAW_RESPONSE_HANDLED
|
|
505
|
+
// A raw handler (not a `Response`) owns the node response: it has written (or is writing) it, so the adapter writes nothing.
|
|
506
|
+
if (res === RAW_RESPONSE_HANDLED) return
|
|
503
507
|
if (!isPromise(res) && isImmediateCacheableResponse(res)) {
|
|
504
508
|
// Synchronous cacheable response — no close listener needed.
|
|
505
509
|
// No I/O events can fire between fetchCallback returning and responseViaCache
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw HTTP handlers, outside the router: a host that must serve another server's node `(req, res)` handlers (a DSH compatibility port, the
|
|
3
|
+
* desktop shell's own endpoints) registers by path instead of building an `Acryl`. The adapter hands a matched request to the handler with the
|
|
4
|
+
* real `IncomingMessage`/`ServerResponse` and writes nothing itself; `RAW_RESPONSE_HANDLED` is how the fetch callback tells it so.
|
|
5
|
+
*
|
|
6
|
+
* Upgrades work the same way at the socket level: a registered path is claimed before the router's WebSocket handling, and the adapter's
|
|
7
|
+
* WebSocket setup leaves a request marked with `RAW_UPGRADE_HANDLED` alone.
|
|
8
|
+
*/
|
|
9
|
+
import type { IncomingMessage } from 'node:http'
|
|
10
|
+
|
|
11
|
+
/** Returned by a raw-aware fetch callback: the node response is already owned, do not write. */
|
|
12
|
+
export const RAW_RESPONSE_HANDLED: unique symbol = Symbol.for('acryl-node-server.raw-response')
|
|
13
|
+
|
|
14
|
+
/** Set on an upgrade request a raw handler took over; the adapter's WebSocket setup skips it. */
|
|
15
|
+
export const RAW_UPGRADE_HANDLED: unique symbol = Symbol.for('acryl-node-server.raw-upgrade')
|
|
16
|
+
|
|
17
|
+
/** An upgrade request a raw handler took over. */
|
|
18
|
+
export type RawUpgradeRequest = IncomingMessage & { [RAW_UPGRADE_HANDLED]?: true }
|
|
@@ -3,6 +3,7 @@ import { defineWebSocketHelper } from 'acryl-router/helpers'
|
|
|
3
3
|
import type { IncomingMessage } from 'node:http'
|
|
4
4
|
import { STATUS_CODES } from 'node:http'
|
|
5
5
|
import type { Duplex } from 'node:stream'
|
|
6
|
+
import { RAW_UPGRADE_HANDLED, type RawUpgradeRequest } from './raw.ts'
|
|
6
7
|
import type { FetchCallback } from './types.ts'
|
|
7
8
|
import type { WebSocketData, WebSocketLike, WebSocketServerLike } from './websocket-types.ts'
|
|
8
9
|
|
|
@@ -144,6 +145,8 @@ const appendResponseHeaders = (headers: string[], responseHeaders?: Headers) =>
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
const rejectUpgradeRequest = (socket: Duplex, status: number, responseHeaders?: Headers) => {
|
|
148
|
+
// The socket already has its answer (a provider transition can route one upgrade to two handlers for a moment): never write a second one.
|
|
149
|
+
if (socket.destroyed || socket.writableEnded) return
|
|
147
150
|
const responseLines = ['Connection: close', 'Content-Length: 0']
|
|
148
151
|
appendResponseHeaders(responseLines, responseHeaders)
|
|
149
152
|
|
|
@@ -181,8 +184,13 @@ export const setupWebSocket = (options: {
|
|
|
181
184
|
server: UpgradeSource
|
|
182
185
|
fetchCallback: FetchCallback
|
|
183
186
|
wss: WebSocketServerLike
|
|
187
|
+
/**
|
|
188
|
+
* Leave a request alone when a raw upgrade handler claimed it. Set by a transport that shares its upgrade source with raw claims (the node
|
|
189
|
+
* server's HTTP server); a hub fed by its own emitter (the Web adapter) must still answer its own upgrades, which arrive through a raw claim.
|
|
190
|
+
*/
|
|
191
|
+
skipWhenRawUpgrade?: boolean
|
|
184
192
|
}): void => {
|
|
185
|
-
const { server, fetchCallback, wss } = options
|
|
193
|
+
const { server, fetchCallback, wss, skipWhenRawUpgrade = false } = options
|
|
186
194
|
|
|
187
195
|
const waiterMap = new Map<
|
|
188
196
|
IncomingMessage,
|
|
@@ -216,9 +224,11 @@ export const setupWebSocket = (options: {
|
|
|
216
224
|
}
|
|
217
225
|
|
|
218
226
|
server.on('upgrade', async (request, socket: Duplex, head) => {
|
|
227
|
+
// A raw upgrade handler claimed this path before the WebSocket setup ran: the socket is (or will be) its own.
|
|
228
|
+
if (skipWhenRawUpgrade && (request as RawUpgradeRequest)[RAW_UPGRADE_HANDLED] === true) return
|
|
219
229
|
if (request.headers.upgrade?.toLowerCase() !== 'websocket') {
|
|
220
|
-
//
|
|
221
|
-
|
|
230
|
+
// An upgrade to another protocol is answered, not left hanging (the transport's raw listener flags the ones it already answered).
|
|
231
|
+
rejectUpgradeRequest(socket, 400)
|
|
222
232
|
return
|
|
223
233
|
}
|
|
224
234
|
|
|
@@ -241,9 +251,7 @@ export const setupWebSocket = (options: {
|
|
|
241
251
|
responseHeaders = response.headers
|
|
242
252
|
}
|
|
243
253
|
} catch {
|
|
244
|
-
|
|
245
|
-
rejectUpgradeRequest(socket, 500)
|
|
246
|
-
}
|
|
254
|
+
rejectUpgradeRequest(socket, 500)
|
|
247
255
|
return
|
|
248
256
|
}
|
|
249
257
|
|
|
@@ -251,9 +259,7 @@ export const setupWebSocket = (options: {
|
|
|
251
259
|
|
|
252
260
|
if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) {
|
|
253
261
|
rejectWaiter(request)
|
|
254
|
-
|
|
255
|
-
rejectUpgradeRequest(socket, status, responseHeaders)
|
|
256
|
-
}
|
|
262
|
+
rejectUpgradeRequest(socket, status, responseHeaders)
|
|
257
263
|
return
|
|
258
264
|
}
|
|
259
265
|
|
|
@@ -38,6 +38,12 @@ const DEFAULT_CACHE_BYTES = 16 * 1024 * 1024
|
|
|
38
38
|
export interface ClientModulesOptions {
|
|
39
39
|
/** The folder of client modules, absolute or relative to the working directory. */
|
|
40
40
|
readonly root: string
|
|
41
|
+
/**
|
|
42
|
+
* The URL prefix the app is mounted at, when the caller knows it. A host can compose app plugins under its own router, and under a
|
|
43
|
+
* Host whose router is a different copy of this package the request's match result is not visible here, so `basePath(c)` throws; the
|
|
44
|
+
* explicit prefix is then the only reliable way to know where the mount starts. Omitted, the router's `basePath(c)` is used.
|
|
45
|
+
*/
|
|
46
|
+
readonly mount?: string | undefined
|
|
41
47
|
/** The folder {@link buildClientModules} wrote; with `NODE_ENV=production` it is served instead of compiling, which needs no `typescript`. */
|
|
42
48
|
readonly compiled?: string | undefined
|
|
43
49
|
/** At most this many bytes of compiled modules are kept in memory (least recently served first out); 16 MiB when not given. */
|
|
@@ -204,7 +210,7 @@ export function clientModules(options: ClientModulesOptions): Acryl {
|
|
|
204
210
|
|
|
205
211
|
const app = new Acryl()
|
|
206
212
|
app.get('/*', (c) => {
|
|
207
|
-
const requestPath = c.req.path.slice(basePath(c).length)
|
|
213
|
+
const requestPath = c.req.path.slice(options.mount === undefined ? basePath(c).length : options.mount.length)
|
|
208
214
|
const runtime = /^\/@acrylblends\/([a-z-]+\.js)$/u.exec(requestPath)?.[1]
|
|
209
215
|
const send = (code: string, etag: string): Response => {
|
|
210
216
|
if (c.req.header('if-none-match') === etag) return c.body(null, 304, { etag, 'cache-control': 'no-cache' })
|