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
package/lib/door/page.js
CHANGED
|
@@ -84,7 +84,7 @@ var PageChannel = class {
|
|
|
84
84
|
}
|
|
85
85
|
connect() {
|
|
86
86
|
if (this.disposed || this.source !== void 0) return;
|
|
87
|
-
const query = `window=${encodeURIComponent(this.options.windowId)}&focused=${this.options.focused() ? "1" : "0"}${this.options.shellWindow === void 0 ? "" : `&shell=${String(this.options.shellWindow)}`}`;
|
|
87
|
+
const query = `window=${encodeURIComponent(this.options.windowId)}&focused=${this.options.focused() ? "1" : "0"}${this.options.shellWindow === void 0 ? "" : `&shell=${String(this.options.shellWindow)}`}${this.options.tab === void 0 ? "" : `&tab=${encodeURIComponent(this.options.tab)}`}`;
|
|
88
88
|
const viaSocket = this.socketAllowed;
|
|
89
89
|
this.socketReady = false;
|
|
90
90
|
const source = viaSocket ? (this.options.createSocket ?? ((url) => socketSource(new WebSocket(url))))(`${socketBase(this.options.base)}/channel/ws?${query}`) : (this.options.createSource ?? ((url) => new EventSource(url)))(`${this.options.base}/channel?${query}`);
|
|
@@ -99,6 +99,10 @@ var PageChannel = class {
|
|
|
99
99
|
heard();
|
|
100
100
|
});
|
|
101
101
|
source.addEventListener("ping", heard);
|
|
102
|
+
source.addEventListener("run", (event) => {
|
|
103
|
+
heard();
|
|
104
|
+
this.options.onRun?.(event.data === "1");
|
|
105
|
+
});
|
|
102
106
|
source.addEventListener("call", (event) => {
|
|
103
107
|
heard();
|
|
104
108
|
if (typeof event.data === "string") this.answer(event.data);
|
|
@@ -170,6 +174,7 @@ var PageChannel = class {
|
|
|
170
174
|
if (source !== void 0) {
|
|
171
175
|
source.onerror = null;
|
|
172
176
|
source.close();
|
|
177
|
+
this.options.onRun?.(false);
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
answer(text) {
|
|
@@ -492,6 +497,15 @@ function valueOf(el, role) {
|
|
|
492
497
|
else if (el.hasAttribute("contenteditable")) value = el.textContent ?? void 0;
|
|
493
498
|
return value === void 0 || value === "" ? void 0 : clip(value, 100);
|
|
494
499
|
}
|
|
500
|
+
/** Choices of a select listed at most; a longer list ends with how many more there are. */
|
|
501
|
+
const MAX_OPTIONS = 20;
|
|
502
|
+
/** The choices of a native select (what `act select` takes), or undefined when it is not one or is sensitive. */
|
|
503
|
+
function optionsOf(el) {
|
|
504
|
+
if (!(el instanceof HTMLSelectElement) || isSensitive(el)) return void 0;
|
|
505
|
+
const labels = [...el.options].filter((option) => !option.hidden).map((option) => clip((option.textContent ?? "").trim(), 60)).filter((label) => label !== "");
|
|
506
|
+
if (labels.length <= MAX_OPTIONS) return labels;
|
|
507
|
+
return [...labels.slice(0, MAX_OPTIONS), `(${String(labels.length - MAX_OPTIONS)} more)`];
|
|
508
|
+
}
|
|
495
509
|
/** Heading level, when the element is a heading. */
|
|
496
510
|
function levelOf(el) {
|
|
497
511
|
const explicit = Number(el.getAttribute("aria-level"));
|
|
@@ -578,7 +592,7 @@ var SnapshotBuilder = class {
|
|
|
578
592
|
const root = this.root();
|
|
579
593
|
const view = root.ownerDocument.defaultView;
|
|
580
594
|
let offset = options.cursor ?? 0;
|
|
581
|
-
if (
|
|
595
|
+
if (offset === 0) {
|
|
582
596
|
this.refs.begin();
|
|
583
597
|
const all = collect(root);
|
|
584
598
|
this.candidates = [...all.filter((c) => inViewport(c.element, view)), ...all.filter((c) => !inViewport(c.element, view))];
|
|
@@ -590,6 +604,7 @@ var SnapshotBuilder = class {
|
|
|
590
604
|
if (isProtected(candidate.element)) states.push("protected");
|
|
591
605
|
const value = valueOf(candidate.element, candidate.role);
|
|
592
606
|
const level = candidate.role === "heading" ? levelOf(candidate.element) : void 0;
|
|
607
|
+
const options = optionsOf(candidate.element);
|
|
593
608
|
return {
|
|
594
609
|
ref: this.refs.issue(candidate.element, candidate.role, candidate.name),
|
|
595
610
|
role: candidate.role,
|
|
@@ -597,7 +612,8 @@ var SnapshotBuilder = class {
|
|
|
597
612
|
depth: candidate.depth,
|
|
598
613
|
states,
|
|
599
614
|
...value === void 0 ? {} : { value },
|
|
600
|
-
...level === void 0 ? {} : { level }
|
|
615
|
+
...level === void 0 ? {} : { level },
|
|
616
|
+
...options === void 0 ? {} : { options }
|
|
601
617
|
};
|
|
602
618
|
});
|
|
603
619
|
const end = offset + page.length;
|
|
@@ -640,6 +656,7 @@ function prepare(env, ref) {
|
|
|
640
656
|
if (isProtected(element)) throw new UiControlError("protected", `"${resolved.name}" belongs to approval, permissions or Agent Control itself; only the user operates it`);
|
|
641
657
|
if (effectivelyHidden(element)) throw new UiControlError("not-actionable", `"${resolved.name}" is not visible`);
|
|
642
658
|
if (element.disabled === true || element.getAttribute("aria-disabled") === "true") throw new UiControlError("not-actionable", `"${resolved.name}" is disabled`);
|
|
659
|
+
env.touched?.(element);
|
|
643
660
|
return resolved;
|
|
644
661
|
}
|
|
645
662
|
function mouse(element, type) {
|
|
@@ -781,6 +798,7 @@ function scroll(env, request) {
|
|
|
781
798
|
const dy = request.direction === "up" ? -amount : request.direction === "down" ? amount : 0;
|
|
782
799
|
if (request.ref !== void 0) {
|
|
783
800
|
const { element, role, name } = env.refs.resolve(request.ref);
|
|
801
|
+
env.touched?.(element);
|
|
784
802
|
element.scrollBy?.({
|
|
785
803
|
left: dx,
|
|
786
804
|
top: dy
|
|
@@ -909,6 +927,8 @@ var RefTable = class {
|
|
|
909
927
|
/** After the user touches the page, agent actions wait this long, so the user always wins. */
|
|
910
928
|
const YIELD_MS = 400;
|
|
911
929
|
const MAX_YIELD_MS = 2e3;
|
|
930
|
+
/** How long after an act a navigation it started is still noticed (a link's navigation starts a task or two after the click). */
|
|
931
|
+
const LEAVE_WATCH_MS = 50;
|
|
912
932
|
function realSleep(ms, signal) {
|
|
913
933
|
return new Promise((resolve, reject) => {
|
|
914
934
|
if (signal.aborted) {
|
|
@@ -938,9 +958,11 @@ var UiDriver = class {
|
|
|
938
958
|
doing = "";
|
|
939
959
|
current = {
|
|
940
960
|
busy: false,
|
|
961
|
+
running: false,
|
|
941
962
|
doing: "",
|
|
942
963
|
killed: false
|
|
943
964
|
};
|
|
965
|
+
runningFlag = false;
|
|
944
966
|
options;
|
|
945
967
|
constructor(options) {
|
|
946
968
|
this.options = options;
|
|
@@ -948,7 +970,8 @@ var UiDriver = class {
|
|
|
948
970
|
this.env = {
|
|
949
971
|
refs: this.refs,
|
|
950
972
|
document: options.document,
|
|
951
|
-
sleep: options.sleep ?? realSleep
|
|
973
|
+
sleep: options.sleep ?? realSleep,
|
|
974
|
+
...options.touched === void 0 ? {} : { touched: options.touched }
|
|
952
975
|
};
|
|
953
976
|
}
|
|
954
977
|
/** The current activity; the same object until something changes, as a store snapshot must be. */
|
|
@@ -969,6 +992,16 @@ var UiDriver = class {
|
|
|
969
992
|
this.snapshots.clear();
|
|
970
993
|
this.emit();
|
|
971
994
|
}
|
|
995
|
+
/**
|
|
996
|
+
* A run asked for from this page's panel starts (`true`) or ends (`false`). The indicator stays up for the whole run, not only while a call is in flight. A new run is a new
|
|
997
|
+
* question from the person, so it also allows the agent to drive again after an earlier Stop.
|
|
998
|
+
*/
|
|
999
|
+
setRunning(running) {
|
|
1000
|
+
if (running === this.runningFlag) return;
|
|
1001
|
+
this.runningFlag = running;
|
|
1002
|
+
if (running) this.killedFlag = false;
|
|
1003
|
+
this.emit();
|
|
1004
|
+
}
|
|
972
1005
|
/** The user allowed the agent to drive again. */
|
|
973
1006
|
resume() {
|
|
974
1007
|
this.killedFlag = false;
|
|
@@ -1017,10 +1050,10 @@ var UiDriver = class {
|
|
|
1017
1050
|
...request.cursor === void 0 ? {} : { cursor: request.cursor },
|
|
1018
1051
|
...request.maxNodes === void 0 ? {} : { maxNodes: request.maxNodes }
|
|
1019
1052
|
});
|
|
1020
|
-
case "click": return click(this.env, request);
|
|
1021
|
-
case "type": return typeText(this.env, request);
|
|
1053
|
+
case "click": return await this.watchLeaving(() => click(this.env, request), signal);
|
|
1054
|
+
case "type": return await this.watchLeaving(() => typeText(this.env, request), signal);
|
|
1022
1055
|
case "select": return selectOption(this.env, request);
|
|
1023
|
-
case "press": return pressKey(this.env, request);
|
|
1056
|
+
case "press": return await this.watchLeaving(() => pressKey(this.env, request), signal);
|
|
1024
1057
|
case "scroll": return scroll(this.env, request);
|
|
1025
1058
|
case "wait": return wait(this.env, request, signal);
|
|
1026
1059
|
case "reload": return this.leave({ reload: true }, "reloading");
|
|
@@ -1028,6 +1061,29 @@ var UiDriver = class {
|
|
|
1028
1061
|
}
|
|
1029
1062
|
}
|
|
1030
1063
|
/**
|
|
1064
|
+
* Run an act that may start a navigation (a click on a link, Enter in a form) and say so: the browser asks `beforeunload` when a navigation starts, during the act or a
|
|
1065
|
+
* moment after it. The answer is sent before the page goes; the Host then waits for the next page instead of reading this one again.
|
|
1066
|
+
*/
|
|
1067
|
+
async watchLeaving(act, signal) {
|
|
1068
|
+
const view = this.options.document().defaultView;
|
|
1069
|
+
let leaving = false;
|
|
1070
|
+
const onLeave = () => {
|
|
1071
|
+
leaving = true;
|
|
1072
|
+
};
|
|
1073
|
+
view?.addEventListener("beforeunload", onLeave);
|
|
1074
|
+
try {
|
|
1075
|
+
const result = act();
|
|
1076
|
+
if (!leaving) await this.env.sleep(LEAVE_WATCH_MS, signal);
|
|
1077
|
+
return leaving ? {
|
|
1078
|
+
...result,
|
|
1079
|
+
leaving: true,
|
|
1080
|
+
detail: `${result.detail === void 0 ? "" : `${result.detail}; `}the page is going to another address`
|
|
1081
|
+
} : result;
|
|
1082
|
+
} finally {
|
|
1083
|
+
view?.removeEventListener("beforeunload", onLeave);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1031
1087
|
* The page is about to be replaced, so the answer is given first and the move happens a moment later (the answer is a POST that the page's own unload would cut off).
|
|
1032
1088
|
* The Host does not wait for this answer to be the last word: it waits for the page to come back.
|
|
1033
1089
|
*/
|
|
@@ -1058,10 +1114,11 @@ var UiDriver = class {
|
|
|
1058
1114
|
emit() {
|
|
1059
1115
|
const next = {
|
|
1060
1116
|
busy: this.busyCount > 0,
|
|
1117
|
+
running: this.runningFlag,
|
|
1061
1118
|
doing: this.doing,
|
|
1062
1119
|
killed: this.killedFlag
|
|
1063
1120
|
};
|
|
1064
|
-
if (next.busy === this.current.busy && next.doing === this.current.doing && next.killed === this.current.killed) return;
|
|
1121
|
+
if (next.busy === this.current.busy && next.running === this.current.running && next.doing === this.current.doing && next.killed === this.current.killed) return;
|
|
1065
1122
|
this.current = next;
|
|
1066
1123
|
const activity = next;
|
|
1067
1124
|
for (const listener of [...this.listeners]) listener(activity);
|
|
@@ -1111,6 +1168,8 @@ const LABELS = {
|
|
|
1111
1168
|
scroll: "scrolling",
|
|
1112
1169
|
wait: "waiting"
|
|
1113
1170
|
};
|
|
1171
|
+
/** Dispatched on the page's window by the Stop button: the agent panel in the same page cancels its run. */
|
|
1172
|
+
const STOP_RUN_EVENT = "acryl:stop-run";
|
|
1114
1173
|
const STYLE_ID = "acryl-door-indicator-style";
|
|
1115
1174
|
const STYLES = `
|
|
1116
1175
|
.acrylDoorBar { position: fixed; z-index: 2147483647; left: 50%; bottom: 16px; transform: translateX(-50%); display: flex; align-items: center; gap: 10px;
|
|
@@ -1133,12 +1192,16 @@ function mountIndicator(driver, document) {
|
|
|
1133
1192
|
bar.setAttribute("data-acryl-no-agent", "");
|
|
1134
1193
|
bar.setAttribute("role", "status");
|
|
1135
1194
|
bar.setAttribute("aria-live", "polite");
|
|
1195
|
+
const stopAgent = () => {
|
|
1196
|
+
driver.kill();
|
|
1197
|
+
document.defaultView?.dispatchEvent(new CustomEvent(STOP_RUN_EVENT));
|
|
1198
|
+
};
|
|
1136
1199
|
const onKey = (event) => {
|
|
1137
|
-
if (event.key === "Escape" && event.isTrusted && driver.activity.busy
|
|
1200
|
+
if (event.key === "Escape" && event.isTrusted && (driver.activity.busy || driver.activity.running)) stopAgent();
|
|
1138
1201
|
};
|
|
1139
1202
|
document.addEventListener("keydown", onKey, true);
|
|
1140
1203
|
const render = (activity) => {
|
|
1141
|
-
if (!activity.busy && !activity.killed) {
|
|
1204
|
+
if (!activity.busy && !activity.running && !activity.killed) {
|
|
1142
1205
|
bar.remove();
|
|
1143
1206
|
return;
|
|
1144
1207
|
}
|
|
@@ -1158,11 +1221,9 @@ function mountIndicator(driver, document) {
|
|
|
1158
1221
|
const dot = document.createElement("span");
|
|
1159
1222
|
dot.className = "acrylDoorDot";
|
|
1160
1223
|
dot.setAttribute("aria-hidden", "true");
|
|
1161
|
-
text.textContent = `Agent is driving: ${LABELS[activity.doing] ?? activity.doing}`;
|
|
1224
|
+
text.textContent = `Agent is driving: ${activity.busy ? LABELS[activity.doing] ?? activity.doing : "working on your request"}`;
|
|
1162
1225
|
button.textContent = "Stop";
|
|
1163
|
-
button.addEventListener("click",
|
|
1164
|
-
driver.kill();
|
|
1165
|
-
});
|
|
1226
|
+
button.addEventListener("click", stopAgent);
|
|
1166
1227
|
bar.replaceChildren(dot, text, button);
|
|
1167
1228
|
}
|
|
1168
1229
|
if (!bar.isConnected) document.body.append(bar);
|
package/lib/door/protocol.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as parseUiRequest, S as parseChannelToPage, _ as UiRequest, a as DEFAULT_SNAPSHOT_NODES, b as WINDOW_ID, c as MAX_WAIT_MS, d as UI_OPS, f as UiActionResult, g as UiOp, h as UiNode, i as ChannelToPage, l as MUTATING_OPS, m as UiErrorCode, n as AuditEntry, o as MAX_SNAPSHOT_NODES, p as UiControlError, r as ChannelToHost, s as MAX_TEXT_LENGTH, t as APP_PATH, u as UI_ERROR_CODES, v as UiResult, x as parseChannelToHost, y as UiSnapshot } from "../protocol-
|
|
1
|
+
import { C as parseUiRequest, S as parseChannelToPage, _ as UiRequest, a as DEFAULT_SNAPSHOT_NODES, b as WINDOW_ID, c as MAX_WAIT_MS, d as UI_OPS, f as UiActionResult, g as UiOp, h as UiNode, i as ChannelToPage, l as MUTATING_OPS, m as UiErrorCode, n as AuditEntry, o as MAX_SNAPSHOT_NODES, p as UiControlError, r as ChannelToHost, s as MAX_TEXT_LENGTH, t as APP_PATH, u as UI_ERROR_CODES, v as UiResult, x as parseChannelToHost, y as UiSnapshot } from "../protocol-BeIYCgTo.js";
|
|
2
2
|
export { APP_PATH, AuditEntry, ChannelToHost, ChannelToPage, DEFAULT_SNAPSHOT_NODES, MAX_SNAPSHOT_NODES, MAX_TEXT_LENGTH, MAX_WAIT_MS, MUTATING_OPS, UI_ERROR_CODES, UI_OPS, UiActionResult, UiControlError, UiErrorCode, UiNode, UiOp, UiRequest, UiResult, UiSnapshot, WINDOW_ID, parseChannelToHost, parseChannelToPage, parseUiRequest };
|
package/lib/drivers.d.ts
CHANGED
|
@@ -26,10 +26,11 @@ type ChatEvent = {
|
|
|
26
26
|
interface ChatDriver {
|
|
27
27
|
readonly kind: 'chat';
|
|
28
28
|
readonly name: string;
|
|
29
|
-
/** Runs one message. Aborting `signal` stops the run. */
|
|
29
|
+
/** Runs one message. Aborting `signal` stops the run. `page` is the door's id of the page whose panel asked, when it sent one: a driver that uses the page drives that one. */
|
|
30
30
|
start(options: {
|
|
31
31
|
message: string;
|
|
32
32
|
signal: AbortSignal;
|
|
33
|
+
page?: string;
|
|
33
34
|
}): AsyncIterable<ChatEvent>;
|
|
34
35
|
}
|
|
35
36
|
/** A live pseudo-terminal session. */
|
|
@@ -45,6 +46,14 @@ interface TerminalSession {
|
|
|
45
46
|
interface TerminalDriver {
|
|
46
47
|
readonly kind: 'terminal';
|
|
47
48
|
readonly name: string;
|
|
49
|
+
/**
|
|
50
|
+
* Where the panel loads the terminal view from: the browser module that draws it, and the URL prefix the view's endpoints live under. The driver
|
|
51
|
+
* plugin serves both, so the view follows wherever the app is mounted (the root of its own server, or the prefix the ACRYL frame serves it at).
|
|
52
|
+
*/
|
|
53
|
+
readonly view: {
|
|
54
|
+
readonly module: string;
|
|
55
|
+
readonly prefix: string;
|
|
56
|
+
};
|
|
48
57
|
start(options: {
|
|
49
58
|
cwd: string;
|
|
50
59
|
env: NodeJS.ProcessEnv;
|
|
@@ -88,7 +88,17 @@ declare class Router extends Service {
|
|
|
88
88
|
* mount's code a request is in. The core's `app.route` runs first so a sub-app's own `onError` still applies to its routes.
|
|
89
89
|
*/
|
|
90
90
|
private snapshot;
|
|
91
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* The router's own disposal: let the flights already in flight finish within `disposeGraceMs` - a save being written when the app stops must not be cut
|
|
93
|
+
* by the router beneath it - then abort what is left and settle it for a moment, so disposal does not wait a second full grace on code that ignores
|
|
94
|
+
* its abort signal.
|
|
95
|
+
*/
|
|
96
|
+
private drain;
|
|
97
|
+
/** The one draining rule: let `flights` finish inside the grace, then abort and settle what is left. */
|
|
98
|
+
private drainFlights;
|
|
99
|
+
/** Resolve true when every flight settled inside `ms`, false at the deadline. */
|
|
100
|
+
private settledWithin;
|
|
101
|
+
/** Abort `flights`, cut their bodies short and wait for them to settle, at most `settleMs`. */
|
|
92
102
|
private end;
|
|
93
103
|
/** Turn errors no mounted app handled into responses. One handler per router; the calling plugin owns it. */
|
|
94
104
|
onError(handler: ErrorHandler): () => void;
|
package/lib/node-server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as Acryl } from "./app-D8riatvv.js";
|
|
2
|
-
import { a as Owner, c as UpgradeHubOptions, d as upgradeWebSocket, f as WebSocketData, i as testWebSocket, l as UpgradeRouter, m as WebSocketServerLike, n as TestWebSocketOptions, o as UpgradeEmitter, p as WebSocketLike, r as TestWebSocketResult, s as UpgradeHub, t as TestSocket, u as RequestError } from "./test-websocket-
|
|
2
|
+
import { a as Owner, c as UpgradeHubOptions, d as upgradeWebSocket, f as WebSocketData, i as testWebSocket, l as UpgradeRouter, m as WebSocketServerLike, n as TestWebSocketOptions, o as UpgradeEmitter, p as WebSocketLike, r as TestWebSocketResult, s as UpgradeHub, t as TestSocket, u as RequestError } from "./test-websocket-GUTAkFQT.js";
|
|
3
3
|
import { n as serveStatic, t as ServeStaticOptions } from "./serve-static-B7sqnftW.js";
|
|
4
4
|
import { t as getConnInfo } from "./conninfo-BCROmdnf.js";
|
|
5
5
|
import { AddressInfo } from "node:net";
|
|
@@ -90,6 +90,12 @@ declare const getRequestListener: (fetchCallback: FetchCallback, options?: {
|
|
|
90
90
|
interface ClientModulesOptions {
|
|
91
91
|
/** The folder of client modules, absolute or relative to the working directory. */
|
|
92
92
|
readonly root: string;
|
|
93
|
+
/**
|
|
94
|
+
* 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
|
|
95
|
+
* Host whose router is a different copy of this package the request's match result is not visible here, so `basePath(c)` throws; the
|
|
96
|
+
* explicit prefix is then the only reliable way to know where the mount starts. Omitted, the router's `basePath(c)` is used.
|
|
97
|
+
*/
|
|
98
|
+
readonly mount?: string | undefined;
|
|
93
99
|
/** The folder {@link buildClientModules} wrote; with `NODE_ENV=production` it is served instead of compiling, which needs no `typescript`. */
|
|
94
100
|
readonly compiled?: string | undefined;
|
|
95
101
|
/** At most this many bytes of compiled modules are kept in memory (least recently served first out); 16 MiB when not given. */
|
package/lib/node-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as serve,
|
|
1
|
+
import { a as serve, d as RequestError, i as createAdaptorServer, n as UpgradeEmitter, o as upgradeWebSocket, r as UpgradeHub, s as getRequestListener, t as testWebSocket } from "./test-websocket-CQOSvBth.js";
|
|
2
2
|
import { t as serveStatic } from "./serve-static-C1Nkj_xB.js";
|
|
3
3
|
import { t as getConnInfo } from "./conninfo-CWJwieh4.js";
|
|
4
|
-
import { n as clientModules, t as buildClientModules } from "./client-modules-
|
|
4
|
+
import { n as clientModules, t as buildClientModules } from "./client-modules-Bkdr83jO.js";
|
|
5
5
|
export { RequestError, UpgradeEmitter, UpgradeHub, buildClientModules, clientModules, createAdaptorServer, getConnInfo, getRequestListener, serve, serveStatic, testWebSocket, upgradeWebSocket };
|
|
@@ -36,6 +36,8 @@ interface UiNode {
|
|
|
36
36
|
readonly value?: string;
|
|
37
37
|
/** Heading level, when it is a heading. */
|
|
38
38
|
readonly level?: number;
|
|
39
|
+
/** The choices of a select, as `act select` takes them, when it is not sensitive. */
|
|
40
|
+
readonly options?: readonly string[];
|
|
39
41
|
}
|
|
40
42
|
interface UiSnapshot {
|
|
41
43
|
readonly generation: number;
|
|
@@ -93,6 +95,8 @@ interface UiActionResult {
|
|
|
93
95
|
readonly name: string;
|
|
94
96
|
};
|
|
95
97
|
readonly detail?: string;
|
|
98
|
+
/** The act started a navigation (a link, a form that submits): this page is going away, and the Host waits for the next one. */
|
|
99
|
+
readonly leaving?: true;
|
|
96
100
|
}
|
|
97
101
|
type UiResult = UiSnapshot | UiActionResult;
|
|
98
102
|
/**
|
|
@@ -5,7 +5,7 @@ import { r as streamSSE } from "./streaming-BlDaph8U.js";
|
|
|
5
5
|
import { i as loopbackOnly, r as loopbackAuthority } from "./loopback-only-BbasoF7R.js";
|
|
6
6
|
import "./helpers-BEjcdbgw.js";
|
|
7
7
|
import { d as parseChannelToHost, l as UiControlError, u as WINDOW_ID } from "./protocol-CQ_if7Dg.js";
|
|
8
|
-
import "./src-
|
|
8
|
+
import "./src-iBJ2ysV9.js";
|
|
9
9
|
import "./middleware-BfG_fp8P.js";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
11
|
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, statSync, writeSync } from "node:fs";
|
|
@@ -29,17 +29,22 @@ const DEFAULT_CALL_TIMEOUT_MS = 3e4;
|
|
|
29
29
|
/** How long a page has to say it heard a read-only call before the call goes to the next page. */
|
|
30
30
|
const DEFAULT_ACK_MS = 3e3;
|
|
31
31
|
/**
|
|
32
|
-
* The pages and who drives which. One page answers a call, chosen the same way every time: the page
|
|
33
|
-
* one in front (it reports `focused`), else the most recently active; a page that did not acknowledge a call in time goes to the end of the line. An explicit desktop `window` picks its page.
|
|
32
|
+
* The pages and who drives which. One page answers a call, chosen the same way every time: the page whose panel asked for the run that is making the call (`driveFor`), else the page the
|
|
33
|
+
* Host just sent somewhere (it comes back as a new page, `prefer`), else the one in front (it reports `focused`), else the most recently active; a page that did not acknowledge a call in time goes to the end of the line. An explicit desktop `window` picks its page.
|
|
34
|
+
* A call that names its `page` (the page a run was asked from) goes to that page alone, followed into its tab when it reloads or moves, and to no other.
|
|
34
35
|
*/
|
|
35
36
|
var UiChannel = class {
|
|
36
37
|
ackMs;
|
|
37
38
|
attachWaitMs;
|
|
39
|
+
requirePage;
|
|
38
40
|
/** The page the Host sent to a new place and that came back: it keeps being driven until the person focuses another page. */
|
|
39
41
|
preferred;
|
|
42
|
+
/** The pages whose panels have a run going, the latest last: a call with no `window` goes to the latest of them that is still connected. */
|
|
43
|
+
askers = [];
|
|
40
44
|
constructor(options = {}) {
|
|
41
45
|
this.ackMs = options.ackMs ?? DEFAULT_ACK_MS;
|
|
42
46
|
this.attachWaitMs = options.attachWaitMs ?? 0;
|
|
47
|
+
this.requirePage = options.requirePage === true;
|
|
43
48
|
}
|
|
44
49
|
connections = /* @__PURE__ */ new Map();
|
|
45
50
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -52,18 +57,22 @@ var UiChannel = class {
|
|
|
52
57
|
* Register a page's stream. A stream already open for the same window is closed first, and its calls settle as `disconnected`.
|
|
53
58
|
* @returns the function to call when this stream ends.
|
|
54
59
|
*/
|
|
55
|
-
attach(windowId, focused, stream, shellWindow) {
|
|
60
|
+
attach(windowId, focused, stream, shellWindow, tab) {
|
|
56
61
|
const previous = this.connections.get(windowId);
|
|
57
62
|
if (previous !== void 0) this.drop(previous, true);
|
|
58
63
|
const connection = {
|
|
59
64
|
stream,
|
|
60
65
|
windowId,
|
|
61
66
|
shellWindow,
|
|
67
|
+
tab,
|
|
62
68
|
focused,
|
|
63
69
|
lastActive: ++this.clock,
|
|
64
70
|
suspect: previous?.suspect === true
|
|
65
71
|
};
|
|
66
72
|
this.connections.set(windowId, connection);
|
|
73
|
+
for (const asker of this.askers) if (asker.windowId === windowId) asker.tab ??= tab;
|
|
74
|
+
else if (tab !== void 0 && asker.tab === tab) asker.windowId = windowId;
|
|
75
|
+
if (this.askers.some((asker) => asker.windowId === windowId)) this.tell(connection, "1");
|
|
67
76
|
for (const waiter of [...this.attachWaiters]) waiter(windowId);
|
|
68
77
|
return () => {
|
|
69
78
|
if (this.connections.get(windowId) === connection) this.drop(connection, false);
|
|
@@ -104,12 +113,83 @@ var UiChannel = class {
|
|
|
104
113
|
...shellWindow === void 0 ? {} : { shellWindow }
|
|
105
114
|
}));
|
|
106
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* A run asked for from this page's panel is going: its calls (which name no window) are for this page, whatever else is open and whichever was driven last, until the run ends.
|
|
118
|
+
* An explicit desktop `window` still picks its own page.
|
|
119
|
+
* @returns the function to call when the run ends.
|
|
120
|
+
*/
|
|
121
|
+
driveFor(windowId) {
|
|
122
|
+
const asker = {
|
|
123
|
+
origin: windowId,
|
|
124
|
+
windowId,
|
|
125
|
+
tab: this.connections.get(windowId)?.tab
|
|
126
|
+
};
|
|
127
|
+
this.askers.push(asker);
|
|
128
|
+
this.tellPage(windowId, "1");
|
|
129
|
+
return () => {
|
|
130
|
+
const at = this.askers.indexOf(asker);
|
|
131
|
+
if (at === -1) return;
|
|
132
|
+
this.askers.splice(at, 1);
|
|
133
|
+
if (!this.askers.some((other) => other.windowId === asker.windowId)) this.tellPage(asker.windowId, "0");
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Where the page a run was asked from is now: the same window, or the page that replaced it in the same tab. */
|
|
137
|
+
whereIs(page) {
|
|
138
|
+
const asker = [...this.askers].reverse().find((entry) => entry.origin === page);
|
|
139
|
+
return asker === void 0 ? {
|
|
140
|
+
windowId: page,
|
|
141
|
+
tab: this.connections.get(page)?.tab
|
|
142
|
+
} : {
|
|
143
|
+
windowId: asker.windowId,
|
|
144
|
+
tab: asker.tab
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* A promise for the run's page coming back after a reload or a move: the next page that connects in its tab (or with its window id). Ask for it BEFORE the call that makes the page
|
|
149
|
+
* leave. Pages of other tabs, another visitor's among them, are never taken for it.
|
|
150
|
+
* @param signal - stop waiting (the act did not move the page after all); the promise then answers `undefined`.
|
|
151
|
+
* @returns the window id of the page that came back, or `undefined` when it did not within `timeoutMs`.
|
|
152
|
+
*/
|
|
153
|
+
expectPageBack(page, timeoutMs, signal) {
|
|
154
|
+
const { windowId, tab } = this.whereIs(page);
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
const done = (value) => {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
this.attachWaiters.delete(waiter);
|
|
159
|
+
signal?.removeEventListener("abort", stop);
|
|
160
|
+
resolve(value);
|
|
161
|
+
};
|
|
162
|
+
const stop = () => {
|
|
163
|
+
done(void 0);
|
|
164
|
+
};
|
|
165
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
166
|
+
const waiter = (attached) => {
|
|
167
|
+
const connection = this.connections.get(attached);
|
|
168
|
+
if (attached === windowId || tab !== void 0 && connection?.tab === tab) done(attached);
|
|
169
|
+
};
|
|
170
|
+
const timer = setTimeout(() => {
|
|
171
|
+
done(void 0);
|
|
172
|
+
}, timeoutMs);
|
|
173
|
+
this.attachWaiters.add(waiter);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
tellPage(windowId, running) {
|
|
177
|
+
const connection = this.connections.get(windowId);
|
|
178
|
+
if (connection !== void 0) this.tell(connection, running);
|
|
179
|
+
}
|
|
180
|
+
tell(connection, running) {
|
|
181
|
+
try {
|
|
182
|
+
connection.stream.notify?.("run", running);
|
|
183
|
+
} catch {}
|
|
184
|
+
}
|
|
107
185
|
/** Drive this page from now on (it is the one the Host just sent to a new place), until the person focuses another. */
|
|
108
186
|
prefer(windowId) {
|
|
109
187
|
const connection = this.connections.get(windowId);
|
|
110
188
|
if (connection === void 0) return;
|
|
111
189
|
this.preferred = windowId;
|
|
112
190
|
connection.lastActive = ++this.clock;
|
|
191
|
+
for (const asker of this.askers) if (!this.connections.has(asker.windowId) && (asker.tab === void 0 || asker.tab === connection.tab)) asker.windowId = windowId;
|
|
192
|
+
if (this.askers.some((asker) => asker.windowId === windowId)) this.tellPage(windowId, "1");
|
|
113
193
|
}
|
|
114
194
|
/**
|
|
115
195
|
* Ask the current page to do something. A read-only call (`look`, `wait`) that the page does not acknowledge in time goes to the next page, and the silent one is last in line
|
|
@@ -119,6 +199,8 @@ var UiChannel = class {
|
|
|
119
199
|
async call(request, options = {}, retried = false) {
|
|
120
200
|
if (this.closed) throw new UiControlError("unloaded", "the door was unloaded");
|
|
121
201
|
const readOnlyCall = request.op === "snapshot" || request.op === "wait";
|
|
202
|
+
if (options.page !== void 0) return await this.callPage(request, options.page, options, readOnlyCall);
|
|
203
|
+
if (this.requirePage) throw new UiControlError("no-window", "this door drives only the page whose visitor asked, and the call names no page");
|
|
122
204
|
if (readOnlyCall && this.attachWaitMs > 0 && this.connections.size === 0) await this.expectNewPage(this.attachWaitMs);
|
|
123
205
|
const ordered = this.ordered();
|
|
124
206
|
const candidates = options.window === void 0 ? ordered : ordered.filter((connection) => connection.shellWindow === options.window);
|
|
@@ -144,6 +226,25 @@ var UiChannel = class {
|
|
|
144
226
|
}
|
|
145
227
|
throw new UiControlError("timeout", `no page of the app answered within ${String(Math.round(this.ackMs / 1e3))} s: the app's pages are open but none responded. Likely causes: a tab in the background, a page that is still loading after a navigation (look again), a page busy running a long script, or a page that holds the browser's six connections with event streams of its own (a page should keep to two). Bring the app's tab to the front, or navigate it, then look again`);
|
|
146
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* A call for one page (the page a run was asked from, followed into its tab): it goes to that page alone and never fails over to another one. A page that is between two
|
|
231
|
+
* documents (it reloads, or moves to another path) is waited for, up to the attach wait.
|
|
232
|
+
*/
|
|
233
|
+
async callPage(request, page, options, readOnly) {
|
|
234
|
+
let target = this.connections.get(this.whereIs(page).windowId);
|
|
235
|
+
if (target === void 0 && this.attachWaitMs > 0) {
|
|
236
|
+
const back = await this.expectPageBack(page, this.attachWaitMs);
|
|
237
|
+
if (back !== void 0) target = this.connections.get(back);
|
|
238
|
+
}
|
|
239
|
+
if (target === void 0) throw new UiControlError("no-window", "the page this run is for is not open any more");
|
|
240
|
+
if (options.signal?.aborted === true) throw new UiControlError("aborted", "the call was cancelled");
|
|
241
|
+
try {
|
|
242
|
+
return await this.callOne(target, request, options, readOnly);
|
|
243
|
+
} catch (cause) {
|
|
244
|
+
if (cause instanceof Silent) throw new UiControlError("timeout", "the page this run is for did not answer: is its tab in the background or still loading? Look again");
|
|
245
|
+
throw cause;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
147
248
|
callOne(target, request, options, failOver) {
|
|
148
249
|
const signal = options.signal;
|
|
149
250
|
const id = this.nextId;
|
|
@@ -209,16 +310,22 @@ var UiChannel = class {
|
|
|
209
310
|
}
|
|
210
311
|
/**
|
|
211
312
|
* A promise for the window id of the next page that connects and was not connected when this was asked (the page that came back after a reload or a move).
|
|
313
|
+
* @param signal - stop waiting; the promise then answers `undefined`.
|
|
212
314
|
* @returns that id, or `undefined` when no new page connected within `timeoutMs`.
|
|
213
315
|
*/
|
|
214
|
-
expectNewPage(timeoutMs) {
|
|
316
|
+
expectNewPage(timeoutMs, signal) {
|
|
215
317
|
const known = new Set(this.connections.keys());
|
|
216
318
|
return new Promise((resolve) => {
|
|
217
319
|
const done = (value) => {
|
|
218
320
|
clearTimeout(timer);
|
|
219
321
|
this.attachWaiters.delete(waiter);
|
|
322
|
+
signal?.removeEventListener("abort", stop);
|
|
220
323
|
resolve(value);
|
|
221
324
|
};
|
|
325
|
+
const stop = () => {
|
|
326
|
+
done(void 0);
|
|
327
|
+
};
|
|
328
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
222
329
|
const waiter = (windowId) => {
|
|
223
330
|
if (!known.has(windowId)) done(windowId);
|
|
224
331
|
};
|
|
@@ -258,9 +365,10 @@ var UiChannel = class {
|
|
|
258
365
|
for (const connection of [...this.connections.values()]) connection.stream.close();
|
|
259
366
|
this.connections.clear();
|
|
260
367
|
}
|
|
261
|
-
/** The order pages are driven in: the one the Host sent somewhere, suspect ones last, the one in front, then the most recently active. */
|
|
368
|
+
/** The order pages are driven in: the one whose panel asked, the one the Host sent somewhere, suspect ones last, the one in front, then the most recently active. */
|
|
262
369
|
ordered() {
|
|
263
|
-
const
|
|
370
|
+
const asker = [...this.askers].reverse().find((entry) => this.connections.has(entry.windowId))?.windowId;
|
|
371
|
+
const rank = (connection) => (connection.suspect ? 0 : 4) + (connection.windowId === this.preferred ? 2 : 0) + (connection.focused ? 1 : 0) + (connection.windowId === asker && !connection.suspect ? 8 : 0);
|
|
264
372
|
return [...this.connections.values()].sort((a, b) => rank(b) - rank(a) || b.lastActive - a.lastActive);
|
|
265
373
|
}
|
|
266
374
|
/** Forget a connection; its calls in flight settle as `disconnected` at once rather than waiting for their timeout. */
|
|
@@ -423,6 +531,8 @@ function pageChannelRoutes(channel, options) {
|
|
|
423
531
|
const focused = c.req.query("focused") === "1";
|
|
424
532
|
const shellQuery = Number(c.req.query("shell"));
|
|
425
533
|
const shellWindow = Number.isSafeInteger(shellQuery) && shellQuery > 0 ? shellQuery : void 0;
|
|
534
|
+
const tabQuery = c.req.query("tab");
|
|
535
|
+
const tab = tabQuery !== void 0 && WINDOW_ID.test(tabQuery) ? tabQuery : void 0;
|
|
426
536
|
const signal = c.req.raw.signal;
|
|
427
537
|
return streamSSE(c, async (stream) => {
|
|
428
538
|
let end = () => {};
|
|
@@ -441,8 +551,9 @@ function pageChannelRoutes(channel, options) {
|
|
|
441
551
|
send: (data) => {
|
|
442
552
|
write("call", data);
|
|
443
553
|
},
|
|
554
|
+
notify: write,
|
|
444
555
|
close: end
|
|
445
|
-
}, shellWindow);
|
|
556
|
+
}, shellWindow, tab);
|
|
446
557
|
const beat = setInterval(() => {
|
|
447
558
|
write("ping", "");
|
|
448
559
|
}, heartbeatMs);
|
|
@@ -476,7 +587,8 @@ function renderSnapshot(snapshot, more = (cursor) => `call ui_snapshot with curs
|
|
|
476
587
|
const level = node.level === void 0 ? "" : ` level=${String(node.level)}`;
|
|
477
588
|
const states = node.states.length === 0 ? "" : ` [${node.states.join(", ")}]`;
|
|
478
589
|
const value = node.value === void 0 ? "" : ` value=${JSON.stringify(node.value)}`;
|
|
479
|
-
|
|
590
|
+
const options = node.options === void 0 ? "" : ` options=${JSON.stringify(node.options)}`;
|
|
591
|
+
return `${" ".repeat(Math.min(node.depth, 8))}- ${node.role}${node.name === "" ? "" : ` ${JSON.stringify(node.name)}`}${level}${states}${value}${options} [ref=${node.ref}]`;
|
|
480
592
|
});
|
|
481
593
|
const rest = snapshot.nextCursor === void 0 ? "" : `\n(${String(snapshot.total - snapshot.nextCursor)} more elements; ${more(snapshot.nextCursor)})`;
|
|
482
594
|
return `Page: ${snapshot.title || "(untitled)"}\nSnapshot ${String(snapshot.generation)}, ${String(snapshot.nodes.length)} of ${String(snapshot.total)} elements:\n${lines.join("\n")}${rest}`;
|