acrylblends 0.8.1 → 0.9.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.
Files changed (235) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +83 -7
  3. package/dist/index.js.map +1 -1
  4. package/lib/audit-BXnYsLge.d.ts +58 -0
  5. package/lib/cell-ChU41DZD.d.ts +32 -0
  6. package/lib/client-modules-CLvWOjbd.js +267 -0
  7. package/lib/connection-ZLEso7XL.d.ts +25 -0
  8. package/lib/conninfo.d.ts +1 -23
  9. package/lib/cordis/db-sqlite.d.ts +56 -0
  10. package/lib/cordis/db-sqlite.js +58 -0
  11. package/lib/cordis/db.d.ts +8 -0
  12. package/lib/cordis/db.js +1 -0
  13. package/lib/cordis/door.d.ts +159 -0
  14. package/lib/cordis/door.js +757 -0
  15. package/lib/cordis/inspect.d.ts +77 -0
  16. package/lib/cordis/inspect.js +149 -0
  17. package/lib/cordis/kv-db.d.ts +16 -0
  18. package/lib/cordis/kv-db.js +18 -0
  19. package/lib/cordis/kv-json.d.ts +16 -0
  20. package/lib/cordis/kv-json.js +44 -0
  21. package/lib/cordis/kv.d.ts +8 -0
  22. package/lib/cordis/kv.js +1 -0
  23. package/lib/cordis/node-server.d.ts +45 -0
  24. package/lib/cordis/node-server.js +93 -0
  25. package/lib/cordis/router-http.d.ts +56 -0
  26. package/lib/cordis/router-http.js +117 -0
  27. package/lib/cordis/router.d.ts +8 -0
  28. package/lib/cordis/router.js +7 -0
  29. package/lib/cordis/state/react.d.ts +6 -0
  30. package/lib/cordis/state/react.js +11 -0
  31. package/lib/cordis/state.d.ts +33 -0
  32. package/lib/cordis/state.js +163 -0
  33. package/lib/core-CaY8XDZ6.js +19 -0
  34. package/lib/database-DBHdwefi.d.ts +44 -0
  35. package/lib/db/conformance.d.ts +11 -0
  36. package/lib/db/conformance.js +267 -0
  37. package/lib/db/sqlite.d.ts +1 -1
  38. package/lib/db/sqlite.js +1 -196
  39. package/lib/db.d.ts +2 -1
  40. package/lib/{src-0hI7M7cN.js → deprecated-BP4fPt38.js} +0 -17
  41. package/lib/door/host.d.ts +24 -0
  42. package/lib/door/host.js +2 -0
  43. package/lib/door/page.d.ts +105 -0
  44. package/lib/door/page.js +1030 -0
  45. package/lib/door/protocol.d.ts +2 -0
  46. package/lib/door/protocol.js +197 -0
  47. package/lib/error-t4N2js2I.d.ts +8 -0
  48. package/lib/etag-DaKMOjty.js +135 -0
  49. package/lib/etag.js +1 -134
  50. package/lib/helpers-CvTauQT5.js +5 -0
  51. package/lib/http-exception-kpGJkmpi.d.ts +57 -0
  52. package/lib/http-exception.d.ts +1 -56
  53. package/lib/{index-BLTYsyc1.d.ts → index-BnGgRKTv.d.ts} +3 -39
  54. package/lib/index-BqVQkTnu.d.ts +98 -0
  55. package/lib/index-Dmst2QCQ.d.ts +44 -0
  56. package/lib/index.d.ts +1 -1
  57. package/lib/index.js +2 -1
  58. package/lib/jwk-LkzeoaU8.js +117 -0
  59. package/lib/jwk.js +1 -116
  60. package/lib/jwt-CskdhEW9.js +115 -0
  61. package/lib/jwt.js +2 -114
  62. package/lib/kv/conformance.d.ts +13 -0
  63. package/lib/kv/conformance.js +301 -0
  64. package/lib/kv/db.d.ts +2 -2
  65. package/lib/kv/json.d.ts +1 -1
  66. package/lib/kv-9f-AlRKN.d.ts +39 -0
  67. package/lib/kv.d.ts +2 -1
  68. package/lib/language-D9a4zqvn.js +198 -0
  69. package/lib/language.js +1 -197
  70. package/lib/middleware-CPQ8himN.js +12 -0
  71. package/lib/node-server/serve-static.d.ts +0 -1
  72. package/lib/node-server/serve-static.js +1 -1
  73. package/lib/node-server.d.ts +1 -7
  74. package/lib/node-server.js +3 -1586
  75. package/lib/open-CMhnJODn.js +197 -0
  76. package/lib/protocol-BizVt4J6.d.ts +144 -0
  77. package/lib/request-id-CKVvHOO1.js +34 -0
  78. package/lib/request-id.js +1 -33
  79. package/lib/secure-headers-mP3j6Hm6.js +163 -0
  80. package/lib/secure-headers.js +1 -162
  81. package/lib/{serve-static-B849HlX2.js → serve-static-3uo_9rjF.js} +1 -4
  82. package/lib/server-DvgMGtzo.js +1323 -0
  83. package/lib/snapshot-text-D3NspDyT.js +338 -0
  84. package/lib/src-CwABFr6r.js +328 -0
  85. package/lib/timeout.d.ts +1 -1
  86. package/lib/timing-Cky7C1Jf.js +183 -0
  87. package/lib/timing.js +1 -182
  88. package/package.json +89 -1
  89. package/plugins/acryl-db/plugin.json +6 -1
  90. package/plugins/acryl-db-sqlite/plugin.json +4 -0
  91. package/plugins/acryl-db-sqlite/src/facade/node.ts +1 -1
  92. package/plugins/acryl-db-sqlite/src/index.ts +2 -2
  93. package/plugins/acryl-db-sqlite/src/open.ts +2 -2
  94. package/plugins/acryl-door/plugin.json +4 -1
  95. package/plugins/acryl-door/src/host/http.ts +2 -2
  96. package/plugins/acryl-door/src/host/mcp-routes.ts +1 -1
  97. package/plugins/acryl-door/src/host/operator-routes.ts +1 -1
  98. package/plugins/acryl-door/src/host/page-routes.ts +3 -3
  99. package/plugins/acryl-door/src/index.ts +2 -2
  100. package/plugins/acryl-inspect/plugin.json +4 -1
  101. package/plugins/acryl-inspect/src/service.ts +2 -2
  102. package/plugins/acryl-kv/plugin.json +6 -1
  103. package/plugins/acryl-kv-db/plugin.json +4 -0
  104. package/plugins/acryl-kv-db/src/index.ts +1 -1
  105. package/plugins/acryl-kv-db/src/open.ts +3 -3
  106. package/plugins/acryl-kv-json/plugin.json +4 -0
  107. package/plugins/acryl-kv-json/src/index.ts +1 -1
  108. package/plugins/acryl-kv-json/src/open.ts +2 -2
  109. package/plugins/acryl-node-server/plugin.json +5 -1
  110. package/plugins/acryl-node-server/src/adapter/conninfo.ts +1 -1
  111. package/plugins/acryl-node-server/src/adapter/early-hints.ts +1 -1
  112. package/plugins/acryl-node-server/src/adapter/serve-static.ts +2 -2
  113. package/plugins/acryl-node-server/src/adapter/websocket.ts +2 -2
  114. package/plugins/acryl-node-server/src/client-modules.ts +2 -2
  115. package/plugins/acryl-node-server/src/index.ts +1 -1
  116. package/plugins/acryl-router/plugin.json +11 -1
  117. package/plugins/acryl-router/src/connection.d.ts +25 -0
  118. package/plugins/acryl-router/src/core/app-base.d.ts +215 -0
  119. package/plugins/acryl-router/src/core/app.d.ts +19 -0
  120. package/plugins/acryl-router/src/core/client/client.d.ts +4 -0
  121. package/plugins/acryl-router/src/core/client/fetch-result-please.d.ts +35 -0
  122. package/plugins/acryl-router/src/core/client/index.d.ts +7 -0
  123. package/plugins/acryl-router/src/core/client/types.d.ts +229 -0
  124. package/plugins/acryl-router/src/core/client/utils.d.ts +18 -0
  125. package/plugins/acryl-router/src/core/compose.d.ts +14 -0
  126. package/plugins/acryl-router/src/core/context.d.ts +443 -0
  127. package/plugins/acryl-router/src/core/helper/accepts/accepts.d.ts +30 -0
  128. package/plugins/acryl-router/src/core/helper/accepts/index.d.ts +5 -0
  129. package/plugins/acryl-router/src/core/helper/conninfo/index.d.ts +5 -0
  130. package/plugins/acryl-router/src/core/helper/conninfo/types.d.ts +36 -0
  131. package/plugins/acryl-router/src/core/helper/cookie/index.d.ts +25 -0
  132. package/plugins/acryl-router/src/core/helper/css/common.d.ts +59 -0
  133. package/plugins/acryl-router/src/core/helper/css/index.d.ts +80 -0
  134. package/plugins/acryl-router/src/core/helper/dev/index.d.ts +20 -0
  135. package/plugins/acryl-router/src/core/helper/factory/index.d.ts +82 -0
  136. package/plugins/acryl-router/src/core/helper/html/index.d.ts +8 -0
  137. package/plugins/acryl-router/src/core/helper/proxy/index.d.ts +73 -0
  138. package/plugins/acryl-router/src/core/helper/route/index.d.ts +75 -0
  139. package/plugins/acryl-router/src/core/helper/ssg/index.d.ts +7 -0
  140. package/plugins/acryl-router/src/core/helper/ssg/middleware.d.ts +44 -0
  141. package/plugins/acryl-router/src/core/helper/ssg/plugins.d.ts +27 -0
  142. package/plugins/acryl-router/src/core/helper/ssg/ssg.d.ts +91 -0
  143. package/plugins/acryl-router/src/core/helper/ssg/utils.d.ts +16 -0
  144. package/plugins/acryl-router/src/core/helper/streaming/index.d.ts +8 -0
  145. package/plugins/acryl-router/src/core/helper/streaming/sse.d.ts +13 -0
  146. package/plugins/acryl-router/src/core/helper/streaming/stream.d.ts +3 -0
  147. package/plugins/acryl-router/src/core/helper/streaming/text.d.ts +3 -0
  148. package/plugins/acryl-router/src/core/helper/streaming/utils.d.ts +1 -0
  149. package/plugins/acryl-router/src/core/helper/testing/index.d.ts +13 -0
  150. package/plugins/acryl-router/src/core/helper/websocket/index.d.ts +70 -0
  151. package/plugins/acryl-router/src/core/http-exception.d.ts +58 -0
  152. package/plugins/acryl-router/src/core/index.d.ts +37 -0
  153. package/plugins/acryl-router/src/core/jsx/base.d.ts +61 -0
  154. package/plugins/acryl-router/src/core/jsx/children.d.ts +9 -0
  155. package/plugins/acryl-router/src/core/jsx/components.d.ts +15 -0
  156. package/plugins/acryl-router/src/core/jsx/constants.d.ts +6 -0
  157. package/plugins/acryl-router/src/core/jsx/context.d.ts +51 -0
  158. package/plugins/acryl-router/src/core/jsx/dom/components.d.ts +10 -0
  159. package/plugins/acryl-router/src/core/jsx/dom/context.d.ts +3 -0
  160. package/plugins/acryl-router/src/core/jsx/dom/css.d.ts +83 -0
  161. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.d.ts +39 -0
  162. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.d.ts +16 -0
  163. package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.d.ts +8 -0
  164. package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.d.ts +7 -0
  165. package/plugins/acryl-router/src/core/jsx/dom/render.d.ts +73 -0
  166. package/plugins/acryl-router/src/core/jsx/dom/utils.d.ts +1 -0
  167. package/plugins/acryl-router/src/core/jsx/hooks/index.d.ts +47 -0
  168. package/plugins/acryl-router/src/core/jsx/index.d.ts +76 -0
  169. package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.d.ts +6 -0
  170. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.d.ts +13 -0
  171. package/plugins/acryl-router/src/core/jsx/intrinsic-elements.d.ts +735 -0
  172. package/plugins/acryl-router/src/core/jsx/streaming.d.ts +36 -0
  173. package/plugins/acryl-router/src/core/jsx/types.d.ts +36 -0
  174. package/plugins/acryl-router/src/core/jsx/utils.d.ts +4 -0
  175. package/plugins/acryl-router/src/core/request/constants.d.ts +1 -0
  176. package/plugins/acryl-router/src/core/request.d.ts +306 -0
  177. package/plugins/acryl-router/src/core/router/reg-exp-router/index.d.ts +6 -0
  178. package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.d.ts +7 -0
  179. package/plugins/acryl-router/src/core/router/reg-exp-router/node.d.ts +13 -0
  180. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.d.ts +17 -0
  181. package/plugins/acryl-router/src/core/router/reg-exp-router/router.d.ts +11 -0
  182. package/plugins/acryl-router/src/core/router/reg-exp-router/trie.d.ts +8 -0
  183. package/plugins/acryl-router/src/core/router/smart-router/index.d.ts +5 -0
  184. package/plugins/acryl-router/src/core/router/smart-router/router.d.ts +11 -0
  185. package/plugins/acryl-router/src/core/router/trie-router/index.d.ts +5 -0
  186. package/plugins/acryl-router/src/core/router/trie-router/node.d.ts +6 -0
  187. package/plugins/acryl-router/src/core/router/trie-router/router.d.ts +7 -0
  188. package/plugins/acryl-router/src/core/router/utils.d.ts +1 -0
  189. package/plugins/acryl-router/src/core/router.d.ts +97 -0
  190. package/plugins/acryl-router/src/core/types.d.ts +573 -0
  191. package/plugins/acryl-router/src/core/utils/accept.d.ts +6 -0
  192. package/plugins/acryl-router/src/core/utils/body.d.ts +79 -0
  193. package/plugins/acryl-router/src/core/utils/buffer.d.ts +17 -0
  194. package/plugins/acryl-router/src/core/utils/color.d.ts +21 -0
  195. package/plugins/acryl-router/src/core/utils/concurrent.d.ts +11 -0
  196. package/plugins/acryl-router/src/core/utils/constants.d.ts +4 -0
  197. package/plugins/acryl-router/src/core/utils/cookie.d.ts +41 -0
  198. package/plugins/acryl-router/src/core/utils/crypto.d.ts +15 -0
  199. package/plugins/acryl-router/src/core/utils/handler.d.ts +6 -0
  200. package/plugins/acryl-router/src/core/utils/headers.d.ts +8 -0
  201. package/plugins/acryl-router/src/core/utils/html.d.ts +44 -0
  202. package/plugins/acryl-router/src/core/utils/http-status.d.ts +32 -0
  203. package/plugins/acryl-router/src/core/utils/mime.d.ts +70 -0
  204. package/plugins/acryl-router/src/core/utils/stream.d.ts +31 -0
  205. package/plugins/acryl-router/src/core/utils/types.d.ts +128 -0
  206. package/plugins/acryl-router/src/core/utils/url.d.ts +44 -0
  207. package/plugins/acryl-router/src/core/validator/index.d.ts +7 -0
  208. package/plugins/acryl-router/src/core/validator/utils.d.ts +29 -0
  209. package/plugins/acryl-router/src/core/validator/validator.d.ts +16 -0
  210. package/plugins/acryl-router/src/helpers/index.d.ts +17 -0
  211. package/plugins/acryl-router/src/helpers/schema-validator.d.ts +54 -0
  212. package/plugins/acryl-router-http/plugin.json +4 -1
  213. package/plugins/acryl-router-http/src/index.ts +1 -1
  214. package/plugins/acryl-state/plugin.json +5 -1
  215. package/plugins/acryl-state/src/service.ts +3 -3
  216. package/plugins/tsconfig.json +32 -20
  217. package/docs/framework/UPSTREAM.md +0 -3
  218. package/docs/framework/upstream.json +0 -9
  219. package/lib/index-D4FAsBZu.d.ts +0 -85
  220. package/plugins/acryl-db/upstream.json +0 -18
  221. package/plugins/acryl-db-sqlite/upstream.json +0 -21
  222. package/plugins/acryl-door/upstream.json +0 -28
  223. package/plugins/acryl-inspect/upstream.json +0 -18
  224. package/plugins/acryl-kv/upstream.json +0 -18
  225. package/plugins/acryl-kv-db/upstream.json +0 -21
  226. package/plugins/acryl-kv-json/upstream.json +0 -22
  227. package/plugins/acryl-node-server/upstream.json +0 -21
  228. package/plugins/acryl-router/upstream.json +0 -20
  229. package/plugins/acryl-router-http/upstream.json +0 -22
  230. package/plugins/acryl-state/NOTICE +0 -30
  231. package/plugins/acryl-state/upstream.json +0 -22
  232. package/plugins/desktop/upstream.json +0 -12
  233. package/plugins/desktop-web/upstream.json +0 -12
  234. package/templates/todo/upstream.json +0 -12
  235. /package/lib/{index-CTy4N7ng.d.ts → deprecated-CTy4N7ng.d.ts} +0 -0
@@ -0,0 +1,1030 @@
1
+ import { MAX_SNAPSHOT_NODES, MAX_WAIT_MS, MUTATING_OPS, UiControlError, parseChannelToPage } from "./protocol.js";
2
+ //#region ../../plugins/acryl-door/src/page/channel.ts
3
+ /**
4
+ * The page's end of the door's channel.
5
+ *
6
+ * It keeps an event stream open to the Host (`GET <base>/channel`), runs each call on the driver, and posts the answer back. The Host
7
+ * writes a `ping` every few seconds; a stream silent for {@link SILENCE_MS} is dropped and opened again, so a page whose connection died
8
+ * without an error (a sleeping laptop, a proxy) resumes on its own. A page that reconnects keeps its window id, and the Host settles a
9
+ * call in flight on the old stream as `disconnected`.
10
+ */
11
+ /** The Host's heartbeat is 15 s; three missed beats and the stream counts as dead. */
12
+ const SILENCE_MS = 45e3;
13
+ const CLOSED = 2;
14
+ const BACKOFF_START_MS = 500;
15
+ const BACKOFF_MAX_MS = 1e4;
16
+ var PageChannel = class {
17
+ source;
18
+ reconnectTimer;
19
+ watchdog;
20
+ backoff = BACKOFF_START_MS;
21
+ lastHeard = 0;
22
+ disposed = false;
23
+ now;
24
+ driver;
25
+ options;
26
+ constructor(driver, options) {
27
+ this.driver = driver;
28
+ this.options = options;
29
+ this.now = options.now ?? (() => Date.now());
30
+ }
31
+ connect() {
32
+ if (this.disposed || this.source !== void 0) return;
33
+ const create = this.options.createSource ?? ((url) => new EventSource(url));
34
+ const query = `window=${encodeURIComponent(this.options.windowId)}&focused=${this.options.focused() ? "1" : "0"}`;
35
+ const source = create(`${this.options.base}/channel?${query}`);
36
+ this.source = source;
37
+ this.lastHeard = this.now();
38
+ const heard = () => {
39
+ this.lastHeard = this.now();
40
+ this.backoff = BACKOFF_START_MS;
41
+ };
42
+ source.addEventListener("ready", heard);
43
+ source.addEventListener("ping", heard);
44
+ source.addEventListener("call", (event) => {
45
+ heard();
46
+ if (typeof event.data === "string") this.answer(event.data);
47
+ });
48
+ source.onerror = () => {
49
+ if (source.readyState === CLOSED) this.restart(source);
50
+ };
51
+ const silence = this.options.silenceMs ?? 45e3;
52
+ this.watchdog = setInterval(() => {
53
+ if (this.now() - this.lastHeard > silence) this.restart(source);
54
+ }, Math.max(50, Math.floor(silence / 3)));
55
+ }
56
+ /** Tell the Host whether this window is the one in front. */
57
+ reportFocus(focused) {
58
+ this.send({
59
+ t: "focus",
60
+ focused
61
+ });
62
+ }
63
+ dispose() {
64
+ this.disposed = true;
65
+ if (this.reconnectTimer !== void 0) clearTimeout(this.reconnectTimer);
66
+ this.stop();
67
+ }
68
+ /** Drop the stream and open a new one after the backoff. */
69
+ restart(source) {
70
+ if (this.source !== source) return;
71
+ this.stop();
72
+ if (this.disposed) return;
73
+ this.reconnectTimer = setTimeout(() => {
74
+ this.reconnectTimer = void 0;
75
+ this.connect();
76
+ }, this.backoff);
77
+ this.backoff = Math.min(this.backoff * 2, BACKOFF_MAX_MS);
78
+ }
79
+ stop() {
80
+ if (this.watchdog !== void 0) clearInterval(this.watchdog);
81
+ this.watchdog = void 0;
82
+ const source = this.source;
83
+ this.source = void 0;
84
+ if (source !== void 0) {
85
+ source.onerror = null;
86
+ source.close();
87
+ }
88
+ }
89
+ answer(text) {
90
+ let call;
91
+ try {
92
+ call = parseChannelToPage(text);
93
+ } catch {
94
+ return;
95
+ }
96
+ this.driver.handle(call.request).then((value) => {
97
+ this.send({
98
+ t: "result",
99
+ id: call.id,
100
+ value
101
+ });
102
+ }, (cause) => {
103
+ const failure = cause instanceof UiControlError ? cause : new UiControlError("not-actionable", cause instanceof Error ? cause.message : "the action failed");
104
+ this.send({
105
+ t: "error",
106
+ id: call.id,
107
+ code: failure.code,
108
+ message: failure.message
109
+ });
110
+ });
111
+ }
112
+ send(message) {
113
+ (this.options.post ?? ((url, body) => fetch(url, {
114
+ method: "POST",
115
+ headers: { "content-type": "application/json" },
116
+ body
117
+ })))(`${this.options.base}/channel/answer`, JSON.stringify({
118
+ window: this.options.windowId,
119
+ message
120
+ })).catch(() => {});
121
+ }
122
+ };
123
+ //#endregion
124
+ //#region ../../plugins/acryl-door/src/page/driver/sensitivity.ts
125
+ /**
126
+ * What the agent may never see or type into, decided by rule and not by trusting the agent:
127
+ * secrets and payment fields (sensitive), and controls that govern the agent itself (protected).
128
+ */
129
+ /** Field names and labels that mean a secret or a payment detail. */
130
+ const SENSITIVE_WORDS = /pass(?:word|code|phrase)|secret|token|api[-_ ]?key|credential|private[-_ ]?key|\bpin\b|card|cvv|cvc|iban|routing|account[-_ ]?number|ssn|social[-_ ]?security|one[-_ ]?time|otp/i;
131
+ /** `autocomplete` values that browsers treat as credentials or payment data. */
132
+ const SENSITIVE_AUTOCOMPLETE = /^(?:cc-|current-password|new-password|one-time-code)/;
133
+ const label = (el, attr) => el.getAttribute(attr) ?? "";
134
+ /** True for a form field (or anything marked) that holds a secret or payment detail. */
135
+ function isSensitive(el) {
136
+ if (el.closest("[data-acryl-sensitive]") !== null) return true;
137
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) return false;
138
+ if (el instanceof HTMLInputElement && el.type === "password") return true;
139
+ if (label(el, "autocomplete").trim().toLowerCase().split(/\s+/).some((token) => SENSITIVE_AUTOCOMPLETE.test(token))) return true;
140
+ return SENSITIVE_WORDS.test([
141
+ el.name,
142
+ el.id,
143
+ label(el, "placeholder"),
144
+ label(el, "aria-label")
145
+ ].join(" "));
146
+ }
147
+ /** Areas whose controls the agent may look at but never operate. */
148
+ const PROTECTED_REGION = /approval|permission|policy|agent control|ui control|acryl-agent-control|acryl-door|kill switch/i;
149
+ /** The text an ancestor uses to name itself, for region rules. */
150
+ function ancestorName(el) {
151
+ const own = label(el, "aria-label") || label(el, "data-region") || "";
152
+ if (own !== "") return own;
153
+ const labelled = label(el, "aria-labelledby");
154
+ if (labelled !== "") return labelled.split(/\s+/).map((id) => el.ownerDocument.getElementById(id)?.textContent ?? "").join(" ");
155
+ return "";
156
+ }
157
+ /**
158
+ * True for a control the agent must not operate: inside `[data-acryl-no-agent]`, inside a region whose
159
+ * name says approval, permission, policy or Agent Control, or itself named after switching Agent Control off.
160
+ */
161
+ function isProtected(el) {
162
+ if (el.closest("[data-acryl-no-agent]") !== null) return true;
163
+ let node = el;
164
+ for (let depth = 0; node !== null && depth < 12; depth += 1) {
165
+ if (PROTECTED_REGION.test(ancestorName(node))) return true;
166
+ node = node.parentElement;
167
+ }
168
+ return false;
169
+ }
170
+ //#endregion
171
+ //#region ../../plugins/acryl-door/src/page/driver/accessibility.ts
172
+ /** Roles, accessible names and states of DOM elements, in the small subset a snapshot needs. */
173
+ const INPUT_ROLES = {
174
+ checkbox: "checkbox",
175
+ radio: "radio",
176
+ button: "button",
177
+ submit: "button",
178
+ reset: "button",
179
+ image: "button",
180
+ range: "slider",
181
+ number: "spinbutton",
182
+ search: "searchbox",
183
+ text: "textbox",
184
+ email: "textbox",
185
+ url: "textbox",
186
+ tel: "textbox",
187
+ password: "textbox"
188
+ };
189
+ const LANDMARKS = {
190
+ nav: "navigation",
191
+ main: "main",
192
+ aside: "complementary",
193
+ dialog: "dialog",
194
+ form: "form"
195
+ };
196
+ /** ARIA roles an element may declare that the driver treats as interactive or structural. */
197
+ const EXPLICIT_ROLES = /* @__PURE__ */ new Set([
198
+ "button",
199
+ "link",
200
+ "tab",
201
+ "tabpanel",
202
+ "menu",
203
+ "menuitem",
204
+ "menuitemcheckbox",
205
+ "menuitemradio",
206
+ "checkbox",
207
+ "radio",
208
+ "switch",
209
+ "textbox",
210
+ "searchbox",
211
+ "combobox",
212
+ "listbox",
213
+ "option",
214
+ "slider",
215
+ "spinbutton",
216
+ "dialog",
217
+ "alertdialog",
218
+ "alert",
219
+ "status",
220
+ "navigation",
221
+ "main",
222
+ "heading",
223
+ "tree",
224
+ "treeitem",
225
+ "row",
226
+ "gridcell",
227
+ "tablist",
228
+ "toolbar",
229
+ "region",
230
+ "form",
231
+ "img"
232
+ ]);
233
+ /** @returns the element's role, or null when it is not something a snapshot lists. */
234
+ function roleOf(el) {
235
+ const explicit = el.getAttribute("role")?.trim().toLowerCase().split(/\s+/)[0];
236
+ if (explicit !== void 0 && explicit !== "" && EXPLICIT_ROLES.has(explicit)) return explicit;
237
+ const tag = el.tagName.toLowerCase();
238
+ if (tag === "a") return el.hasAttribute("href") ? "link" : null;
239
+ if (tag === "button" || tag === "summary") return "button";
240
+ if (tag === "select") return el.multiple ? "listbox" : "combobox";
241
+ if (tag === "textarea") return "textbox";
242
+ if (/^h[1-6]$/.test(tag)) return "heading";
243
+ if (tag === "img") return el.getAttribute("alt")?.trim() ? "img" : null;
244
+ if (tag === "input") {
245
+ const type = (el.getAttribute("type") ?? "text").toLowerCase();
246
+ if (type === "hidden") return null;
247
+ return INPUT_ROLES[type] ?? "textbox";
248
+ }
249
+ if (el.hasAttribute("contenteditable") && el.getAttribute("contenteditable") !== "false") return "textbox";
250
+ if (tag === "form") return el.hasAttribute("aria-label") || el.hasAttribute("aria-labelledby") ? "form" : null;
251
+ return LANDMARKS[tag] ?? null;
252
+ }
253
+ /** Roles whose accessible name is the text they contain, so that text is not listed again on its own. */
254
+ const NAMED_BY_CONTENT = /* @__PURE__ */ new Set([
255
+ "button",
256
+ "link",
257
+ "tab",
258
+ "menuitem",
259
+ "menuitemcheckbox",
260
+ "menuitemradio",
261
+ "option",
262
+ "heading",
263
+ "switch",
264
+ "treeitem",
265
+ "checkbox",
266
+ "radio",
267
+ "alert",
268
+ "status"
269
+ ]);
270
+ /** Roles that show their content as a value (a field, a native select), so the text inside them is not listed on its own either. */
271
+ const SHOWN_AS_VALUE = /* @__PURE__ */ new Set([
272
+ "textbox",
273
+ "searchbox",
274
+ "combobox",
275
+ "spinbutton"
276
+ ]);
277
+ const collapse = (value) => value.replace(/\s+/g, " ").trim();
278
+ const clip = (value, max) => value.length > max ? `${value.slice(0, max - 1)}…` : value;
279
+ /** The text a reader would hear: like `textContent`, but leaving out what is `aria-hidden` or `hidden` (a check mark, an icon glyph). */
280
+ function readableText(root) {
281
+ const parts = [];
282
+ const walk = (node) => {
283
+ if (node.nodeType === Node.TEXT_NODE) parts.push(node.textContent ?? "");
284
+ else if (!(node instanceof Element && (node.getAttribute("aria-hidden") === "true" || node.hasAttribute("hidden")))) node.childNodes.forEach(walk);
285
+ };
286
+ root.childNodes.forEach(walk);
287
+ return parts.join("");
288
+ }
289
+ const textOf = (el, max) => clip(collapse(readableText(el)), max);
290
+ /** The role of a run of visible text that is not already a control's name or a field's value: what an agent reads a value from. */
291
+ const TEXT_ROLE = "text";
292
+ /** One run of text as a snapshot lists it: collapsed and clipped, empty when there is nothing to read. */
293
+ const textRun = (raw) => clip(collapse(raw), 200);
294
+ /** The runs of text directly inside an element (between its child elements), as a snapshot lists them. */
295
+ function textRunsOf(el) {
296
+ const runs = [];
297
+ let run = "";
298
+ for (const child of Array.from(el.childNodes)) if (child.nodeType === Node.TEXT_NODE) run += child.textContent ?? "";
299
+ else if (child instanceof Element) {
300
+ runs.push(textRun(run));
301
+ run = "";
302
+ }
303
+ runs.push(textRun(run));
304
+ return runs.filter((text) => text !== "");
305
+ }
306
+ /** ARIA-style accessible name, kept short. */
307
+ function nameOf(el, role) {
308
+ const aria = el.getAttribute("aria-label")?.trim();
309
+ if (aria !== void 0 && aria !== "") return clip(collapse(aria), 120);
310
+ const by = el.getAttribute("aria-labelledby");
311
+ if (by !== null && by.trim() !== "") {
312
+ const joined = collapse(by.split(/\s+/).map((id) => el.ownerDocument.getElementById(id)?.textContent ?? "").join(" "));
313
+ if (joined !== "") return clip(joined, 120);
314
+ }
315
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
316
+ const id = el.id;
317
+ const explicit = id === "" ? null : el.ownerDocument.querySelector(`label[for="${CSS.escape(id)}"]`);
318
+ const wrapping = el.closest("label");
319
+ const label = explicit ?? wrapping;
320
+ const labelText = label === null ? "" : collapse(readableText(label));
321
+ if (labelText !== "") return clip(labelText, 120);
322
+ if (el instanceof HTMLInputElement && (el.type === "button" || el.type === "submit" || el.type === "reset") && el.value !== "") return clip(el.value, 120);
323
+ const placeholder = el.getAttribute("placeholder")?.trim();
324
+ if (placeholder !== void 0 && placeholder !== "") return clip(placeholder, 120);
325
+ }
326
+ if (role === "img") return clip(collapse(el.getAttribute("alt") ?? ""), 120);
327
+ if (NAMED_BY_CONTENT.has(role)) {
328
+ const text = textOf(el, 120);
329
+ if (text !== "") return text;
330
+ }
331
+ if (role === "alert" || role === "status" || role === "dialog" || role === "alertdialog") {
332
+ const text = textOf(el, 160);
333
+ if (text !== "") return text;
334
+ }
335
+ return clip(collapse(el.getAttribute("title") ?? ""), 120);
336
+ }
337
+ /** The state words a snapshot shows for an element. */
338
+ function statesOf(el, role) {
339
+ const states = [];
340
+ const aria = (name) => el.getAttribute(name);
341
+ if (el.disabled === true || aria("aria-disabled") === "true") states.push("disabled");
342
+ if (el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")) {
343
+ if (el.checked) states.push("checked");
344
+ } else if (aria("aria-checked") === "true") states.push("checked");
345
+ if (aria("aria-expanded") === "true") states.push("expanded");
346
+ if (aria("aria-expanded") === "false") states.push("collapsed");
347
+ if (aria("aria-selected") === "true") states.push("selected");
348
+ if (aria("aria-pressed") === "true") states.push("pressed");
349
+ if (aria("aria-current") !== null && aria("aria-current") !== "false") states.push("current");
350
+ if (aria("aria-invalid") === "true") states.push("invalid");
351
+ if (el.required === true || aria("aria-required") === "true") states.push("required");
352
+ if (el.readOnly === true || aria("aria-readonly") === "true") states.push("readonly");
353
+ if (el.ownerDocument.activeElement === el) states.push("focused");
354
+ return states;
355
+ }
356
+ /** The visible value of an editable field, or undefined when it is empty, not editable, or sensitive. */
357
+ function valueOf(el, role) {
358
+ if (role !== "textbox" && role !== "searchbox" && role !== "combobox" && role !== "spinbutton") return void 0;
359
+ if (isSensitive(el)) return void 0;
360
+ let value;
361
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) value = el.value;
362
+ else if (el instanceof HTMLSelectElement) value = el.selectedOptions[0]?.textContent ?? void 0;
363
+ else if (el.hasAttribute("contenteditable")) value = el.textContent ?? void 0;
364
+ return value === void 0 || value === "" ? void 0 : clip(value, 100);
365
+ }
366
+ /** Heading level, when the element is a heading. */
367
+ function levelOf(el) {
368
+ const explicit = Number(el.getAttribute("aria-level"));
369
+ if (Number.isInteger(explicit) && explicit >= 1 && explicit <= 6) return explicit;
370
+ const match = /^h([1-6])$/.exec(el.tagName.toLowerCase());
371
+ return match === null ? void 0 : Number(match[1]);
372
+ }
373
+ /** Hidden from everyone: the driver skips it and everything inside it. */
374
+ function isHidden(el) {
375
+ if (el.hasAttribute("hidden") || el.getAttribute("aria-hidden") === "true") return true;
376
+ if (el instanceof HTMLInputElement && el.type === "hidden") return true;
377
+ const view = el.ownerDocument.defaultView;
378
+ if (view === null) return false;
379
+ const style = view.getComputedStyle(el);
380
+ return style.display === "none" || style.visibility === "hidden";
381
+ }
382
+ /** The most elements one snapshot considers, however large the page. */
383
+ const MAX_CANDIDATES = 5e3;
384
+ /** True when the element (with layout) is at least partly inside the window; unknown layout counts as inside. */
385
+ function inViewport(element, view) {
386
+ if (view === null) return true;
387
+ const rect = element.getBoundingClientRect();
388
+ if (rect.width === 0 && rect.height === 0) return true;
389
+ return rect.bottom > 0 && rect.right > 0 && rect.top < view.innerHeight && rect.left < view.innerWidth;
390
+ }
391
+ /**
392
+ * Every element with a role, and every run of visible text, in reading order. Text that is already a name (inside a button, a link, a
393
+ * heading, a label) is not listed again; a run is the text between two child elements, so `<p>Left: <b>24:59</b></p>` reads as
394
+ * `Left:` and `24:59`.
395
+ */
396
+ function collect(root) {
397
+ const found = [];
398
+ const walk = (element, depth, named) => {
399
+ if (found.length >= MAX_CANDIDATES) return;
400
+ if (element.hasAttribute("data-acryl-agent-control")) return;
401
+ const tag = element.tagName.toLowerCase();
402
+ if (tag === "script" || tag === "style" || tag === "template" || tag === "noscript") return;
403
+ if (isHidden(element)) return;
404
+ if (isSensitive(element)) return;
405
+ const role = roleOf(element);
406
+ let childDepth = depth;
407
+ if (role !== null) {
408
+ found.push({
409
+ element,
410
+ role,
411
+ name: nameOf(element, role),
412
+ depth
413
+ });
414
+ childDepth = depth + 1;
415
+ }
416
+ const inName = named || tag === "label" || role !== null && (NAMED_BY_CONTENT.has(role) || SHOWN_AS_VALUE.has(role));
417
+ let run = "";
418
+ const flush = () => {
419
+ const text = textRun(run);
420
+ run = "";
421
+ if (text !== "" && !inName && found.length < MAX_CANDIDATES) found.push({
422
+ element,
423
+ role: TEXT_ROLE,
424
+ name: text,
425
+ depth: childDepth
426
+ });
427
+ };
428
+ for (const child of Array.from(element.childNodes)) if (child.nodeType === Node.TEXT_NODE) run += child.textContent ?? "";
429
+ else if (child instanceof Element) {
430
+ flush();
431
+ walk(child, childDepth, inName);
432
+ }
433
+ flush();
434
+ };
435
+ walk(root, 0, false);
436
+ return found;
437
+ }
438
+ /** Snapshots the page and remembers the full candidate list of the current generation, for paging. */
439
+ var SnapshotBuilder = class {
440
+ candidates = [];
441
+ refs;
442
+ root;
443
+ constructor(refs, root) {
444
+ this.refs = refs;
445
+ this.root = root;
446
+ }
447
+ snapshot(options = {}) {
448
+ const max = Math.min(options.maxNodes ?? 300, MAX_SNAPSHOT_NODES);
449
+ const root = this.root();
450
+ const view = root.ownerDocument.defaultView;
451
+ let offset = options.cursor ?? 0;
452
+ if (options.cursor === void 0) {
453
+ this.refs.begin();
454
+ const all = collect(root);
455
+ this.candidates = [...all.filter((c) => inViewport(c.element, view)), ...all.filter((c) => !inViewport(c.element, view))];
456
+ offset = 0;
457
+ } else if (this.candidates.length === 0 || offset > this.candidates.length) throw new UiControlError("stale-ref", "that page of the snapshot is gone; take a new snapshot");
458
+ const page = this.candidates.slice(offset, offset + max);
459
+ const nodes = page.map((candidate) => {
460
+ const states = statesOf(candidate.element, candidate.role);
461
+ if (isProtected(candidate.element)) states.push("protected");
462
+ const value = valueOf(candidate.element, candidate.role);
463
+ const level = candidate.role === "heading" ? levelOf(candidate.element) : void 0;
464
+ return {
465
+ ref: this.refs.issue(candidate.element, candidate.role, candidate.name),
466
+ role: candidate.role,
467
+ name: candidate.name,
468
+ depth: candidate.depth,
469
+ states,
470
+ ...value === void 0 ? {} : { value },
471
+ ...level === void 0 ? {} : { level }
472
+ };
473
+ });
474
+ const end = offset + page.length;
475
+ return {
476
+ generation: this.refs.generation,
477
+ title: root.ownerDocument.title,
478
+ nodes,
479
+ total: this.candidates.length,
480
+ ...end < this.candidates.length ? { nextCursor: end } : {}
481
+ };
482
+ }
483
+ /** Drop the remembered page list (with the refs). */
484
+ clear() {
485
+ this.candidates = [];
486
+ }
487
+ };
488
+ //#endregion
489
+ //#region ../../plugins/acryl-door/src/page/driver/actions.ts
490
+ /** The actions the driver performs on the page, each guarded by the same rules. */
491
+ const EDITABLE_ROLES = /* @__PURE__ */ new Set([
492
+ "textbox",
493
+ "searchbox",
494
+ "spinbutton",
495
+ "combobox"
496
+ ]);
497
+ const POLL_MS = 50;
498
+ const target = (role, name) => ({
499
+ role,
500
+ name
501
+ });
502
+ function effectivelyHidden(element) {
503
+ for (let node = element; node !== null; node = node.parentElement) if (isHidden(node)) return true;
504
+ return false;
505
+ }
506
+ /** Everything an action checks before touching an element. */
507
+ function prepare(env, ref) {
508
+ const resolved = env.refs.resolve(ref);
509
+ const { element } = resolved;
510
+ if (isSensitive(element)) throw new UiControlError("sensitive", "that is a password, token or payment field; the agent does not read or type into it");
511
+ if (isProtected(element)) throw new UiControlError("protected", `"${resolved.name}" belongs to approval, permissions or Agent Control itself; only the user operates it`);
512
+ if (effectivelyHidden(element)) throw new UiControlError("not-actionable", `"${resolved.name}" is not visible`);
513
+ if (element.disabled === true || element.getAttribute("aria-disabled") === "true") throw new UiControlError("not-actionable", `"${resolved.name}" is disabled`);
514
+ return resolved;
515
+ }
516
+ function mouse(element, type) {
517
+ element.dispatchEvent(new MouseEvent(type, {
518
+ bubbles: true,
519
+ cancelable: true
520
+ }));
521
+ }
522
+ /** xterm.js reads neither the value of its input textarea nor a bare synthetic key: text arrives as a paste, and key handling reads the legacy `keyCode`. */
523
+ function isTerminalInput(element) {
524
+ return element.classList.contains("xterm-helper-textarea");
525
+ }
526
+ function pasteInto(element, text) {
527
+ const clipboardData = new DataTransfer();
528
+ clipboardData.setData("text/plain", text);
529
+ element.dispatchEvent(new ClipboardEvent("paste", {
530
+ clipboardData,
531
+ bubbles: true,
532
+ cancelable: true
533
+ }));
534
+ }
535
+ const LEGACY_KEY_CODES = {
536
+ Backspace: 8,
537
+ Tab: 9,
538
+ Enter: 13,
539
+ Escape: 27,
540
+ " ": 32,
541
+ PageUp: 33,
542
+ PageDown: 34,
543
+ End: 35,
544
+ Home: 36,
545
+ ArrowLeft: 37,
546
+ ArrowUp: 38,
547
+ ArrowRight: 39,
548
+ ArrowDown: 40,
549
+ Delete: 46
550
+ };
551
+ function setNativeValue(element, value) {
552
+ const proto = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
553
+ const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
554
+ if (setter === void 0) element.value = value;
555
+ else setter.call(element, value);
556
+ }
557
+ function click(env, request) {
558
+ const { element, role, name } = prepare(env, request.ref);
559
+ element.scrollIntoView?.({ block: "nearest" });
560
+ element.focus?.();
561
+ mouse(element, "mousedown");
562
+ mouse(element, "mouseup");
563
+ element.click();
564
+ return {
565
+ ok: true,
566
+ target: target(role, name)
567
+ };
568
+ }
569
+ function typeText(env, request) {
570
+ const { element, role, name } = prepare(env, request.ref);
571
+ if (!EDITABLE_ROLES.has(role) || role === "combobox" && element instanceof HTMLSelectElement) throw new UiControlError("not-actionable", `"${name}" is a ${role}, not a text field; use select or click`);
572
+ if (element.readOnly === true || element.getAttribute("aria-readonly") === "true") throw new UiControlError("not-actionable", `"${name}" is read-only`);
573
+ const clear = request.clear ?? true;
574
+ element.focus?.();
575
+ if (isTerminalInput(element)) pasteInto(element, request.text);
576
+ else if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
577
+ setNativeValue(element, clear ? request.text : element.value + request.text);
578
+ element.dispatchEvent(new Event("input", { bubbles: true }));
579
+ element.dispatchEvent(new Event("change", { bubbles: true }));
580
+ } else {
581
+ element.textContent = clear ? request.text : (element.textContent ?? "") + request.text;
582
+ element.dispatchEvent(new Event("input", { bubbles: true }));
583
+ }
584
+ if (request.submit === true) pressKey(env, {
585
+ op: "press",
586
+ key: "Enter",
587
+ ref: request.ref
588
+ });
589
+ return {
590
+ ok: true,
591
+ target: target(role, name),
592
+ detail: `typed ${String(request.text.length)} characters`
593
+ };
594
+ }
595
+ function selectOption(env, request) {
596
+ const { element, role, name } = prepare(env, request.ref);
597
+ if (!(element instanceof HTMLSelectElement)) throw new UiControlError("not-actionable", `"${name}" is a ${role}, not a select; click it and pick the option`);
598
+ const wanted = request.option.trim().toLowerCase();
599
+ const options = Array.from(element.options);
600
+ const match = options.find((option) => option.value.toLowerCase() === wanted) ?? options.find((option) => (option.textContent ?? "").trim().toLowerCase() === wanted);
601
+ if (match === void 0) throw new UiControlError("not-found", `"${name}" has no option "${request.option}"`);
602
+ if (match.disabled) throw new UiControlError("not-actionable", `the option "${request.option}" is disabled`);
603
+ element.value = match.value;
604
+ element.dispatchEvent(new Event("input", { bubbles: true }));
605
+ element.dispatchEvent(new Event("change", { bubbles: true }));
606
+ return {
607
+ ok: true,
608
+ target: target(role, name),
609
+ detail: `selected ${match.textContent?.trim() ?? match.value}`
610
+ };
611
+ }
612
+ function pressKey(env, request) {
613
+ const doc = env.document();
614
+ let element;
615
+ let role;
616
+ let name;
617
+ if (request.ref !== void 0) ({element, role, name} = prepare(env, request.ref));
618
+ else {
619
+ element = doc.activeElement ?? doc.body;
620
+ if (isSensitive(element)) throw new UiControlError("sensitive", "the focused field is a password, token or payment field");
621
+ if (isProtected(element)) throw new UiControlError("protected", "the focused control belongs to approval, permissions or Agent Control itself");
622
+ role = roleOf(element) ?? "page";
623
+ name = roleOf(element) === null ? "" : nameOf(element, role);
624
+ }
625
+ const key = request.key === "Space" ? " " : request.key;
626
+ const keyCode = LEGACY_KEY_CODES[key];
627
+ const init = {
628
+ key,
629
+ bubbles: true,
630
+ cancelable: true,
631
+ ...keyCode === void 0 ? {} : {
632
+ keyCode,
633
+ which: keyCode
634
+ }
635
+ };
636
+ element.focus?.();
637
+ if (element.dispatchEvent(new KeyboardEvent("keydown", init))) {
638
+ if ((key === "Enter" || key === " ") && (role === "button" || role === "link")) element.click();
639
+ else if (key === "Enter" && element instanceof HTMLInputElement && element.form !== null) element.form.requestSubmit?.();
640
+ }
641
+ element.dispatchEvent(new KeyboardEvent("keyup", init));
642
+ return {
643
+ ok: true,
644
+ target: target(role, name),
645
+ detail: `pressed ${request.key}`
646
+ };
647
+ }
648
+ function scroll(env, request) {
649
+ const doc = env.document();
650
+ const amount = request.amount ?? 400;
651
+ const dx = request.direction === "left" ? -amount : request.direction === "right" ? amount : 0;
652
+ const dy = request.direction === "up" ? -amount : request.direction === "down" ? amount : 0;
653
+ if (request.ref !== void 0) {
654
+ const { element, role, name } = env.refs.resolve(request.ref);
655
+ element.scrollBy?.({
656
+ left: dx,
657
+ top: dy
658
+ });
659
+ if (typeof element.scrollBy !== "function") {
660
+ element.scrollTop += dy;
661
+ element.scrollLeft += dx;
662
+ }
663
+ return {
664
+ ok: true,
665
+ target: target(role, name),
666
+ detail: `scrolled ${request.direction}`
667
+ };
668
+ }
669
+ doc.defaultView?.scrollBy?.({
670
+ left: dx,
671
+ top: dy
672
+ });
673
+ return {
674
+ ok: true,
675
+ detail: `scrolled the page ${request.direction}`
676
+ };
677
+ }
678
+ /** True when something visible on the page matches `request`. */
679
+ function present(env, request) {
680
+ const doc = env.document();
681
+ const walk = (element) => {
682
+ if (element.hasAttribute("data-acryl-agent-control") || isHidden(element) || isSensitive(element)) return false;
683
+ const role = roleOf(element);
684
+ if (request.role !== void 0 || request.name !== void 0) {
685
+ if (role !== null && (request.role === void 0 || role === request.role.toLowerCase()) && (request.name === void 0 || nameOf(element, role).toLowerCase().includes(request.name.toLowerCase()))) return true;
686
+ }
687
+ if (request.text !== void 0 && element.children.length === 0 && (element.textContent ?? "").toLowerCase().includes(request.text.toLowerCase())) return true;
688
+ return Array.from(element.children).some(walk);
689
+ };
690
+ return walk(doc.body);
691
+ }
692
+ async function wait(env, request, signal) {
693
+ const budget = Math.min(request.timeoutMs ?? 3e3, MAX_WAIT_MS);
694
+ let waited = 0;
695
+ const wantGone = request.gone === true;
696
+ for (;;) {
697
+ if (present(env, request) !== wantGone) return {
698
+ ok: true,
699
+ detail: wantGone ? "it is gone" : "it is there"
700
+ };
701
+ if (waited >= budget) throw new UiControlError("not-found", wantGone ? "it did not go away in time" : "it did not appear in time");
702
+ await env.sleep(POLL_MS, signal);
703
+ waited += POLL_MS;
704
+ }
705
+ }
706
+ //#endregion
707
+ //#region ../../plugins/acryl-door/src/page/driver/ref-table.ts
708
+ /**
709
+ * Refs: short-lived handles an agent uses to name an element.
710
+ *
711
+ * A ref is `<generation>.<n>`. Each snapshot starts a new generation and every older ref becomes stale, so a
712
+ * ref can never quietly point at a different element after the page changed. Even within a generation, a
713
+ * ref is honoured only while its element is still in the document and still has the role and name it had when
714
+ * the ref was issued; a re-rendered or replaced control is a typed `stale-ref`, never a click somewhere else.
715
+ */
716
+ var RefTable = class {
717
+ generationNumber = 0;
718
+ entries = /* @__PURE__ */ new Map();
719
+ nextIndex = 1;
720
+ get generation() {
721
+ return this.generationNumber;
722
+ }
723
+ /** Start a new generation: every earlier ref is now stale. */
724
+ begin() {
725
+ this.generationNumber += 1;
726
+ this.entries = /* @__PURE__ */ new Map();
727
+ this.nextIndex = 1;
728
+ return this.generationNumber;
729
+ }
730
+ /** Forget everything (the driver is going away or was stopped). */
731
+ clear() {
732
+ this.begin();
733
+ }
734
+ issue(element, role, name) {
735
+ const index = this.nextIndex;
736
+ this.nextIndex += 1;
737
+ this.entries.set(index, {
738
+ element: new WeakRef(element),
739
+ role,
740
+ name
741
+ });
742
+ return `${String(this.generationNumber)}.${String(index)}`;
743
+ }
744
+ /** @throws UiControlError `unknown-ref` or `stale-ref`. */
745
+ resolve(ref) {
746
+ const [generationText, indexText] = ref.split(".");
747
+ const generation = Number(generationText);
748
+ const index = Number(indexText);
749
+ if (!Number.isInteger(generation) || !Number.isInteger(index)) throw new UiControlError("unknown-ref", `${ref} is not a ref`);
750
+ if (generation !== this.generationNumber) throw new UiControlError("stale-ref", `${ref} is from an earlier snapshot (the latest is ${String(this.generationNumber)}); take a new snapshot`);
751
+ const entry = this.entries.get(index);
752
+ if (entry === void 0) throw new UiControlError("unknown-ref", `${ref} was not in the snapshot`);
753
+ const element = entry.element.deref();
754
+ if (element === void 0 || !element.isConnected) throw new UiControlError("stale-ref", `${ref} is no longer on the page; take a new snapshot`);
755
+ if (entry.role === "text") {
756
+ if (!textRunsOf(element).includes(entry.name)) throw new UiControlError("stale-ref", `${ref} changed since the snapshot ("${entry.name}" is no longer there); take a new snapshot`);
757
+ return {
758
+ element,
759
+ role: entry.role,
760
+ name: entry.name
761
+ };
762
+ }
763
+ const role = roleOf(element);
764
+ if (role !== entry.role || nameOf(element, role) !== entry.name) throw new UiControlError("stale-ref", `${ref} changed since the snapshot ("${entry.name}" is now something else); take a new snapshot`);
765
+ return {
766
+ element,
767
+ role: entry.role,
768
+ name: entry.name
769
+ };
770
+ }
771
+ };
772
+ //#endregion
773
+ //#region ../../plugins/acryl-door/src/page/driver/driver.ts
774
+ /**
775
+ * The in-page driver: takes one {@link UiRequest} and does it, under the rules.
776
+ *
777
+ * It owns the ref table, the kill switch, the yield-to-the-user rule and the "driving" activity the indicator
778
+ * shows. It touches only the document it is given, never another window and never the OS.
779
+ */
780
+ /** After the user touches the page, agent actions wait this long, so the user always wins. */
781
+ const YIELD_MS = 400;
782
+ const MAX_YIELD_MS = 2e3;
783
+ function realSleep(ms, signal) {
784
+ return new Promise((resolve, reject) => {
785
+ if (signal.aborted) {
786
+ reject(signal.reason);
787
+ return;
788
+ }
789
+ const timer = setTimeout(() => {
790
+ signal.removeEventListener("abort", onAbort);
791
+ resolve();
792
+ }, ms);
793
+ const onAbort = () => {
794
+ clearTimeout(timer);
795
+ reject(signal.reason);
796
+ };
797
+ signal.addEventListener("abort", onAbort, { once: true });
798
+ });
799
+ }
800
+ var UiDriver = class {
801
+ refs = new RefTable();
802
+ snapshots;
803
+ listeners = /* @__PURE__ */ new Set();
804
+ env;
805
+ stopAll = /* @__PURE__ */ new Set();
806
+ killedFlag = false;
807
+ disposed = false;
808
+ busyCount = 0;
809
+ doing = "";
810
+ current = {
811
+ busy: false,
812
+ doing: "",
813
+ killed: false
814
+ };
815
+ options;
816
+ constructor(options) {
817
+ this.options = options;
818
+ this.snapshots = new SnapshotBuilder(this.refs, () => options.document().body);
819
+ this.env = {
820
+ refs: this.refs,
821
+ document: options.document,
822
+ sleep: options.sleep ?? realSleep
823
+ };
824
+ }
825
+ /** The current activity; the same object until something changes, as a store snapshot must be. */
826
+ get activity() {
827
+ return this.current;
828
+ }
829
+ subscribe(listener) {
830
+ this.listeners.add(listener);
831
+ return () => {
832
+ this.listeners.delete(listener);
833
+ };
834
+ }
835
+ /** The user pressed the kill switch: pending calls settle as `killed` and new ones are refused. */
836
+ kill() {
837
+ this.killedFlag = true;
838
+ for (const controller of [...this.stopAll]) controller.abort(new UiControlError("killed", "the user stopped the agent"));
839
+ this.refs.clear();
840
+ this.snapshots.clear();
841
+ this.emit();
842
+ }
843
+ /** The user allowed the agent to drive again. */
844
+ resume() {
845
+ this.killedFlag = false;
846
+ this.emit();
847
+ }
848
+ /** The driver goes away: pending calls settle as `unloaded`; refs are gone. */
849
+ dispose() {
850
+ this.disposed = true;
851
+ for (const controller of [...this.stopAll]) controller.abort(new UiControlError("unloaded", "the driver was unloaded"));
852
+ this.refs.clear();
853
+ this.snapshots.clear();
854
+ this.listeners.clear();
855
+ }
856
+ /** @param signal - the Host cancelled the call. @throws UiControlError. */
857
+ async handle(request, signal) {
858
+ if (this.disposed) throw new UiControlError("unloaded", "the driver was unloaded");
859
+ if (this.killedFlag) throw new UiControlError("killed", "the user stopped the agent; it cannot drive until they allow it again");
860
+ const controller = new AbortController();
861
+ this.stopAll.add(controller);
862
+ const onCancel = () => {
863
+ controller.abort(new UiControlError("aborted", "the call was cancelled"));
864
+ };
865
+ signal?.addEventListener("abort", onCancel, { once: true });
866
+ this.busyCount += 1;
867
+ this.doing = request.op;
868
+ this.emit();
869
+ try {
870
+ if (MUTATING_OPS.includes(request.op)) await this.yieldToUser(controller.signal);
871
+ if (controller.signal.aborted) throw controller.signal.reason;
872
+ return await this.run(request, controller.signal);
873
+ } catch (cause) {
874
+ if (cause instanceof UiControlError) throw cause;
875
+ if (controller.signal.aborted && controller.signal.reason instanceof UiControlError) throw controller.signal.reason;
876
+ throw new UiControlError("not-actionable", cause instanceof Error ? cause.message : "the action failed");
877
+ } finally {
878
+ signal?.removeEventListener("abort", onCancel);
879
+ this.stopAll.delete(controller);
880
+ this.busyCount -= 1;
881
+ if (this.busyCount === 0) this.doing = "";
882
+ this.emit();
883
+ }
884
+ }
885
+ async run(request, signal) {
886
+ switch (request.op) {
887
+ case "snapshot": return this.snapshots.snapshot({
888
+ ...request.cursor === void 0 ? {} : { cursor: request.cursor },
889
+ ...request.maxNodes === void 0 ? {} : { maxNodes: request.maxNodes }
890
+ });
891
+ case "click": return click(this.env, request);
892
+ case "type": return typeText(this.env, request);
893
+ case "select": return selectOption(this.env, request);
894
+ case "press": return pressKey(this.env, request);
895
+ case "scroll": return scroll(this.env, request);
896
+ case "wait": return wait(this.env, request, signal);
897
+ }
898
+ }
899
+ async yieldToUser(signal) {
900
+ const since = this.options.msSinceUserInput;
901
+ if (since === void 0) return;
902
+ let waited = 0;
903
+ while (since() < YIELD_MS && waited < MAX_YIELD_MS) {
904
+ await this.env.sleep(100, signal);
905
+ waited += 100;
906
+ }
907
+ }
908
+ emit() {
909
+ const next = {
910
+ busy: this.busyCount > 0,
911
+ doing: this.doing,
912
+ killed: this.killedFlag
913
+ };
914
+ if (next.busy === this.current.busy && next.doing === this.current.doing && next.killed === this.current.killed) return;
915
+ this.current = next;
916
+ const activity = next;
917
+ for (const listener of [...this.listeners]) listener(activity);
918
+ }
919
+ };
920
+ //#endregion
921
+ //#region ../../plugins/acryl-door/src/page/user-input.ts
922
+ /** Tracks when the user last used the page, so the agent yields to them. */
923
+ /** Events only a real person produces: a script's `dispatchEvent` is never `isTrusted`. */
924
+ const USER_EVENTS = [
925
+ "pointerdown",
926
+ "mousedown",
927
+ "keydown",
928
+ "wheel",
929
+ "touchstart"
930
+ ];
931
+ var UserInputClock = class {
932
+ last = Number.NEGATIVE_INFINITY;
933
+ now;
934
+ constructor(now = () => Date.now()) {
935
+ this.now = now;
936
+ }
937
+ /** Milliseconds since the user last touched the page; Infinity when they have not. */
938
+ msSince = () => this.last === Number.NEGATIVE_INFINITY ? Number.POSITIVE_INFINITY : this.now() - this.last;
939
+ /** Start listening. @returns disposer. */
940
+ attach(target) {
941
+ const handler = (event) => {
942
+ if (event.isTrusted) this.last = this.now();
943
+ };
944
+ for (const type of USER_EVENTS) target.addEventListener(type, handler, {
945
+ capture: true,
946
+ passive: true
947
+ });
948
+ return () => {
949
+ for (const type of USER_EVENTS) target.removeEventListener(type, handler, { capture: true });
950
+ };
951
+ }
952
+ };
953
+ //#endregion
954
+ //#region ../../plugins/acryl-door/src/page/indicator.ts
955
+ const LABELS = {
956
+ snapshot: "looking at the page",
957
+ click: "clicking",
958
+ type: "typing",
959
+ select: "choosing an option",
960
+ press: "pressing a key",
961
+ scroll: "scrolling",
962
+ wait: "waiting"
963
+ };
964
+ const STYLE_ID = "acryl-door-indicator-style";
965
+ const STYLES = `
966
+ .acrylDoorBar { position: fixed; z-index: 2147483647; left: 50%; bottom: 16px; transform: translateX(-50%); display: flex; align-items: center; gap: 10px;
967
+ padding: 8px 12px; border-radius: 8px; font: 13px/1.4 system-ui, sans-serif; color: #fff; background: #1f2937; box-shadow: 0 4px 16px rgba(0,0,0,.25); }
968
+ .acrylDoorBar[data-killed] { background: #7f1d1d; }
969
+ .acrylDoorBar button { font: inherit; color: inherit; background: rgba(255,255,255,.15); border: 1px solid rgba(255,255,255,.35); border-radius: 6px; padding: 2px 10px; cursor: pointer; }
970
+ .acrylDoorDot { width: 8px; height: 8px; border-radius: 50%; background: #34d399; }
971
+ `;
972
+ /** Show the bar for `driver` in `document`. @returns the disposer, which removes the bar, its style and its listeners. */
973
+ function mountIndicator(driver, document) {
974
+ if (document.getElementById(STYLE_ID) === null) {
975
+ const style = document.createElement("style");
976
+ style.id = STYLE_ID;
977
+ style.textContent = STYLES;
978
+ document.head.append(style);
979
+ }
980
+ const bar = document.createElement("div");
981
+ bar.className = "acrylDoorBar";
982
+ bar.setAttribute("data-acryl-agent-control", "");
983
+ bar.setAttribute("data-acryl-no-agent", "");
984
+ bar.setAttribute("role", "status");
985
+ bar.setAttribute("aria-live", "polite");
986
+ const onKey = (event) => {
987
+ if (event.key === "Escape" && event.isTrusted && driver.activity.busy) driver.kill();
988
+ };
989
+ document.addEventListener("keydown", onKey, true);
990
+ const render = (activity) => {
991
+ if (!activity.busy && !activity.killed) {
992
+ bar.remove();
993
+ return;
994
+ }
995
+ const button = document.createElement("button");
996
+ button.type = "button";
997
+ const text = document.createElement("span");
998
+ if (activity.killed) {
999
+ bar.setAttribute("data-killed", "");
1000
+ text.textContent = "Agent stopped. It cannot use the page until you allow it.";
1001
+ button.textContent = "Allow again";
1002
+ button.addEventListener("click", () => {
1003
+ driver.resume();
1004
+ });
1005
+ bar.replaceChildren(text, button);
1006
+ } else {
1007
+ bar.removeAttribute("data-killed");
1008
+ const dot = document.createElement("span");
1009
+ dot.className = "acrylDoorDot";
1010
+ dot.setAttribute("aria-hidden", "true");
1011
+ text.textContent = `Agent is driving: ${LABELS[activity.doing] ?? activity.doing}`;
1012
+ button.textContent = "Stop";
1013
+ button.addEventListener("click", () => {
1014
+ driver.kill();
1015
+ });
1016
+ bar.replaceChildren(dot, text, button);
1017
+ }
1018
+ if (!bar.isConnected) document.body.append(bar);
1019
+ };
1020
+ const unsubscribe = driver.subscribe(render);
1021
+ render(driver.activity);
1022
+ return () => {
1023
+ unsubscribe();
1024
+ document.removeEventListener("keydown", onKey, true);
1025
+ bar.remove();
1026
+ document.getElementById(STYLE_ID)?.remove();
1027
+ };
1028
+ }
1029
+ //#endregion
1030
+ export { PageChannel, SILENCE_MS, UiDriver, UserInputClock, mountIndicator };