acrylblends 1.1.0 → 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.
Files changed (241) hide show
  1. package/README.md +9 -0
  2. package/dist/index.d.ts +45 -1
  3. package/dist/index.js +1980 -27
  4. package/dist/index.js.map +1 -1
  5. package/docs/framework/agents/index.md +1 -1
  6. package/docs/framework/getting-started/desktop.md +23 -4
  7. package/docs/framework/guides/create-acrylblends.md +1 -1
  8. package/docs/framework/workshop/README.md +138 -152
  9. package/docs/framework/workshop/facilitator.md +69 -101
  10. package/legacy/README.md +9 -0
  11. package/legacy/agent/157ce3db1112/src/app.ts +98 -0
  12. package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
  13. package/legacy/agent/24c681a00fca/src/app.ts +160 -0
  14. package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
  15. package/legacy/agent/2e790da977b2/src/app.ts +98 -0
  16. package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
  17. package/legacy/agent/6307045545dc/src/app.ts +168 -0
  18. package/legacy/agent/6307045545dc/src/routes.ts +83 -0
  19. package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
  20. package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
  21. package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
  22. package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
  23. package/legacy/agent/74ece148269c/src/app.ts +168 -0
  24. package/legacy/agent/74ece148269c/src/routes.ts +93 -0
  25. package/legacy/agent/855359304707/src/app.ts +168 -0
  26. package/legacy/agent/855359304707/src/routes.ts +126 -0
  27. package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
  28. package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
  29. package/legacy/agent/c2f30958043b/src/app.ts +98 -0
  30. package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
  31. package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
  32. package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
  33. package/legacy/blank/aea589283445/src/routes.ts +16 -0
  34. package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
  35. package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
  36. package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
  37. package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
  38. package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
  39. package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
  40. package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
  41. package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
  42. package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
  43. package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
  44. package/legacy/layouts.json +524 -0
  45. package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
  46. package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
  47. package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
  48. package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
  49. package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
  50. package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
  51. package/legacy/notes/4edc36949c15/src/app.ts +107 -0
  52. package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
  53. package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
  54. package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
  55. package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
  56. package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
  57. package/legacy/todo/02e613b3625c/src/app.ts +71 -0
  58. package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
  59. package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
  60. package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
  61. package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
  62. package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
  63. package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
  64. package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
  65. package/legacy/todo/8e64211312f3/src/app.ts +71 -0
  66. package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
  67. package/legacy/todo/b79078a87565/src/app.ts +71 -0
  68. package/legacy/todo/b79078a87565/src/todo.ts +125 -0
  69. package/lib/{audit-BFpULBCj.d.ts → audit-Bok_UNCn.d.ts} +26 -3
  70. package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
  71. package/lib/cordis/door.d.ts +42 -7
  72. package/lib/cordis/door.js +204 -60
  73. package/lib/cordis/inspect.js +1 -1
  74. package/lib/cordis/node-server.d.ts +101 -3
  75. package/lib/cordis/node-server.js +3 -3
  76. package/lib/cordis/router-http.d.ts +7 -1
  77. package/lib/cordis/router-http.js +44 -3
  78. package/lib/cordis/router.d.ts +1 -1
  79. package/lib/cordis/router.js +1 -1
  80. package/lib/cordis/state.js +1 -1
  81. package/lib/door/host.d.ts +2 -2
  82. package/lib/door/host.js +1 -1
  83. package/lib/door/page.d.ts +10 -1
  84. package/lib/door/page.js +46 -7
  85. package/lib/door/protocol.d.ts +1 -1
  86. package/lib/drivers.d.ts +10 -1
  87. package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
  88. package/lib/node-server.d.ts +7 -1
  89. package/lib/node-server.js +2 -2
  90. package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
  91. package/lib/{snapshot-text-BwM6T2nG.js → snapshot-text-CXTnjxGx.js} +87 -7
  92. package/lib/src-CtkIX_Gg.js +427 -0
  93. package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
  94. package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
  95. package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
  96. package/package.json +2 -1
  97. package/plugins/acryl-door/src/host/channel.ts +86 -9
  98. package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
  99. package/plugins/acryl-door/src/host/door-file.ts +7 -2
  100. package/plugins/acryl-door/src/host/door.ts +53 -23
  101. package/plugins/acryl-door/src/host/page-routes.ts +3 -1
  102. package/plugins/acryl-door/src/host/page-socket.ts +18 -8
  103. package/plugins/acryl-door/src/host/verbs.ts +3 -0
  104. package/plugins/acryl-door/src/index.ts +166 -29
  105. package/plugins/acryl-door/src/page/base.ts +32 -0
  106. package/plugins/acryl-door/src/page/channel.ts +3 -1
  107. package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
  108. package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
  109. package/plugins/acryl-door/src/page/driver/driver.ts +32 -4
  110. package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
  111. package/plugins/acryl-door/src/page/index.ts +41 -2
  112. package/plugins/acryl-door/src/page/outline.ts +56 -0
  113. package/plugins/acryl-door/src/page/protocol.ts +4 -0
  114. package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
  115. package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
  116. package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
  117. package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
  118. package/plugins/acryl-node-server/src/client-modules.ts +7 -1
  119. package/plugins/acryl-node-server/src/index.ts +304 -16
  120. package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
  121. package/plugins/acryl-page/plugin.json +12 -0
  122. package/plugins/acryl-page/src/index.ts +163 -0
  123. package/plugins/acryl-router/src/service.ts +77 -14
  124. package/plugins/acryl-router-http/src/index.ts +15 -1
  125. package/plugins/acryl-router-http/src/serve.ts +31 -0
  126. package/plugins/acryl-web-server/plugin.json +14 -0
  127. package/plugins/acryl-web-server/src/index.ts +164 -0
  128. package/plugins/agent/agent.ts +38 -7
  129. package/plugins/agent/index.ts +1 -1
  130. package/plugins/agent/model.ts +10 -2
  131. package/plugins/agent/tools.ts +9 -3
  132. package/plugins/agent-panel/client/describe.ts +57 -0
  133. package/plugins/agent-panel/client/driver-pick.ts +22 -0
  134. package/plugins/agent-panel/client/frame.ts +51 -0
  135. package/plugins/agent-panel/client/main.ts +238 -26
  136. package/plugins/agent-panel/drivers.ts +7 -2
  137. package/plugins/agent-panel/headless.ts +5 -2
  138. package/plugins/agent-panel/index.ts +146 -57
  139. package/plugins/agent-panel/mount.ts +27 -0
  140. package/plugins/agent-panel/visitors.ts +231 -0
  141. package/plugins/desktop/frame.ts +231 -0
  142. package/plugins/desktop/launch.ts +44 -9
  143. package/plugins/desktop/main.ts +100 -32
  144. package/plugins/desktop/window.ts +21 -4
  145. package/plugins/driver-pty/client/terminal.ts +4 -1
  146. package/plugins/driver-pty/index.ts +38 -11
  147. package/plugins/driver-pty/spawn.ts +38 -0
  148. package/plugins/driver-pty/terminal.ts +2 -0
  149. package/plugins/extensions/index.ts +26 -1
  150. package/plugins/extensions/resolve.ts +13 -0
  151. package/plugins/feedback/client.d.ts +22 -0
  152. package/plugins/feedback/client.js +471 -0
  153. package/plugins/feedback/index.ts +143 -0
  154. package/plugins/feedback/plugin.json +11 -0
  155. package/plugins/feedback/store.ts +307 -0
  156. package/plugins/home/index.ts +122 -0
  157. package/plugins/home/plugin.json +11 -0
  158. package/plugins/instance/index.ts +6 -0
  159. package/plugins/manifest/app-plugins.ts +86 -0
  160. package/plugins/manifest/index.ts +12 -2
  161. package/plugins/manifest/snapshot.ts +275 -0
  162. package/plugins/openai/index.ts +64 -5
  163. package/plugins/openai/prompt.ts +23 -0
  164. package/shared/AGENTS.core.md +36 -16
  165. package/shared/skills/acryl-add-ui/SKILL.md +19 -0
  166. package/shared/skills/acryl-app/SKILL.md +13 -0
  167. package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
  168. package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
  169. package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
  170. package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
  171. package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
  172. package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
  173. package/shared/skills/cordis-plugin/SKILL.md +26 -0
  174. package/templates/agent/AGENTS.md +1 -1
  175. package/templates/agent/blend.yaml +4 -0
  176. package/templates/agent/gitignore +1 -0
  177. package/templates/agent/package.json +1 -0
  178. package/templates/agent/src/app.ts +150 -33
  179. package/templates/agent/src/frame.ts +17 -0
  180. package/templates/agent/src/main.ts +5 -3
  181. package/templates/agent/src/routes.ts +38 -19
  182. package/templates/agent/template.json +2 -0
  183. package/templates/agent/test/app.test.ts +45 -3
  184. package/templates/blank/AGENTS.md +3 -2
  185. package/templates/blank/blend.yaml +11 -0
  186. package/templates/blank/gitignore +1 -0
  187. package/templates/blank/package.json +1 -0
  188. package/templates/blank/src/app.ts +100 -29
  189. package/templates/blank/src/door.ts +7 -0
  190. package/templates/blank/src/first-page.ts +5 -0
  191. package/templates/blank/src/frame.ts +17 -0
  192. package/templates/blank/src/routes.ts +65 -8
  193. package/templates/blank/template.json +14 -1
  194. package/templates/blank/test/app.test.ts +103 -16
  195. package/templates/chat/blend.yaml +2 -1
  196. package/templates/chat/gitignore +1 -0
  197. package/templates/chat/package.json +1 -0
  198. package/templates/chat/src/app.ts +72 -22
  199. package/templates/chat/src/chat-app.ts +11 -2
  200. package/templates/chat/src/client/chat.tsx +4 -2
  201. package/templates/chat/src/frame.ts +17 -0
  202. package/templates/chat/src/page.tsx +6 -4
  203. package/templates/chat/src/routes.ts +14 -4
  204. package/templates/chat/src/runtime.ts +9 -3
  205. package/templates/chat/template.json +3 -1
  206. package/templates/chat/test/chat.test.ts +46 -1
  207. package/templates/chat/test/server.test.ts +17 -3
  208. package/templates/nodejs/blend.yaml +2 -1
  209. package/templates/nodejs/gitignore +1 -0
  210. package/templates/nodejs/package.json +1 -0
  211. package/templates/nodejs/src/app.ts +76 -21
  212. package/templates/nodejs/src/frame.ts +17 -0
  213. package/templates/nodejs/src/page.tsx +3 -3
  214. package/templates/nodejs/src/routes.ts +32 -13
  215. package/templates/nodejs/template.json +3 -1
  216. package/templates/nodejs/test/app.test.ts +38 -3
  217. package/templates/nodejs/test/server.test.ts +17 -3
  218. package/templates/notes/blend.yaml +2 -1
  219. package/templates/notes/gitignore +1 -0
  220. package/templates/notes/package.json +1 -0
  221. package/templates/notes/src/app.ts +72 -22
  222. package/templates/notes/src/frame.ts +17 -0
  223. package/templates/notes/src/notes-app.ts +24 -17
  224. package/templates/notes/src/routes.ts +11 -4
  225. package/templates/notes/src/runtime.ts +9 -3
  226. package/templates/notes/src/views.tsx +26 -17
  227. package/templates/notes/template.json +3 -1
  228. package/templates/notes/test/notes.test.ts +39 -1
  229. package/templates/notes/test/server.test.ts +17 -3
  230. package/templates/todo/blend.yaml +3 -0
  231. package/templates/todo/gitignore +1 -0
  232. package/templates/todo/package.json +1 -0
  233. package/templates/todo/src/app.ts +81 -27
  234. package/templates/todo/src/door.ts +7 -0
  235. package/templates/todo/src/frame.ts +17 -0
  236. package/templates/todo/src/page.ts +13 -6
  237. package/templates/todo/src/todo.ts +18 -6
  238. package/templates/todo/template.json +11 -1
  239. package/templates/todo/test/todo.test.ts +67 -9
  240. package/lib/src-LX0E4ec4.js +0 -117
  241. package/shared/SKILL.md +0 -14
@@ -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-BTBm4OPD.js";
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";
@@ -31,10 +31,12 @@ const DEFAULT_ACK_MS = 3e3;
31
31
  /**
32
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
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;
40
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. */
@@ -42,6 +44,7 @@ var UiChannel = class {
42
44
  constructor(options = {}) {
43
45
  this.ackMs = options.ackMs ?? DEFAULT_ACK_MS;
44
46
  this.attachWaitMs = options.attachWaitMs ?? 0;
47
+ this.requirePage = options.requirePage === true;
45
48
  }
46
49
  connections = /* @__PURE__ */ new Map();
47
50
  pending = /* @__PURE__ */ new Map();
@@ -54,18 +57,21 @@ var UiChannel = class {
54
57
  * Register a page's stream. A stream already open for the same window is closed first, and its calls settle as `disconnected`.
55
58
  * @returns the function to call when this stream ends.
56
59
  */
57
- attach(windowId, focused, stream, shellWindow) {
60
+ attach(windowId, focused, stream, shellWindow, tab) {
58
61
  const previous = this.connections.get(windowId);
59
62
  if (previous !== void 0) this.drop(previous, true);
60
63
  const connection = {
61
64
  stream,
62
65
  windowId,
63
66
  shellWindow,
67
+ tab,
64
68
  focused,
65
69
  lastActive: ++this.clock,
66
70
  suspect: previous?.suspect === true
67
71
  };
68
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;
69
75
  if (this.askers.some((asker) => asker.windowId === windowId)) this.tell(connection, "1");
70
76
  for (const waiter of [...this.attachWaiters]) waiter(windowId);
71
77
  return () => {
@@ -113,7 +119,11 @@ var UiChannel = class {
113
119
  * @returns the function to call when the run ends.
114
120
  */
115
121
  driveFor(windowId) {
116
- const asker = { windowId };
122
+ const asker = {
123
+ origin: windowId,
124
+ windowId,
125
+ tab: this.connections.get(windowId)?.tab
126
+ };
117
127
  this.askers.push(asker);
118
128
  this.tellPage(windowId, "1");
119
129
  return () => {
@@ -123,6 +133,46 @@ var UiChannel = class {
123
133
  if (!this.askers.some((other) => other.windowId === asker.windowId)) this.tellPage(asker.windowId, "0");
124
134
  };
125
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
+ }
126
176
  tellPage(windowId, running) {
127
177
  const connection = this.connections.get(windowId);
128
178
  if (connection !== void 0) this.tell(connection, running);
@@ -138,7 +188,7 @@ var UiChannel = class {
138
188
  if (connection === void 0) return;
139
189
  this.preferred = windowId;
140
190
  connection.lastActive = ++this.clock;
141
- for (const asker of this.askers) if (!this.connections.has(asker.windowId)) asker.windowId = windowId;
191
+ for (const asker of this.askers) if (!this.connections.has(asker.windowId) && (asker.tab === void 0 || asker.tab === connection.tab)) asker.windowId = windowId;
142
192
  if (this.askers.some((asker) => asker.windowId === windowId)) this.tellPage(windowId, "1");
143
193
  }
144
194
  /**
@@ -149,6 +199,8 @@ var UiChannel = class {
149
199
  async call(request, options = {}, retried = false) {
150
200
  if (this.closed) throw new UiControlError("unloaded", "the door was unloaded");
151
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");
152
204
  if (readOnlyCall && this.attachWaitMs > 0 && this.connections.size === 0) await this.expectNewPage(this.attachWaitMs);
153
205
  const ordered = this.ordered();
154
206
  const candidates = options.window === void 0 ? ordered : ordered.filter((connection) => connection.shellWindow === options.window);
@@ -174,6 +226,25 @@ var UiChannel = class {
174
226
  }
175
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`);
176
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
+ }
177
248
  callOne(target, request, options, failOver) {
178
249
  const signal = options.signal;
179
250
  const id = this.nextId;
@@ -239,16 +310,22 @@ var UiChannel = class {
239
310
  }
240
311
  /**
241
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`.
242
314
  * @returns that id, or `undefined` when no new page connected within `timeoutMs`.
243
315
  */
244
- expectNewPage(timeoutMs) {
316
+ expectNewPage(timeoutMs, signal) {
245
317
  const known = new Set(this.connections.keys());
246
318
  return new Promise((resolve) => {
247
319
  const done = (value) => {
248
320
  clearTimeout(timer);
249
321
  this.attachWaiters.delete(waiter);
322
+ signal?.removeEventListener("abort", stop);
250
323
  resolve(value);
251
324
  };
325
+ const stop = () => {
326
+ done(void 0);
327
+ };
328
+ signal?.addEventListener("abort", stop, { once: true });
252
329
  const waiter = (windowId) => {
253
330
  if (!known.has(windowId)) done(windowId);
254
331
  };
@@ -454,6 +531,8 @@ function pageChannelRoutes(channel, options) {
454
531
  const focused = c.req.query("focused") === "1";
455
532
  const shellQuery = Number(c.req.query("shell"));
456
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;
457
536
  const signal = c.req.raw.signal;
458
537
  return streamSSE(c, async (stream) => {
459
538
  let end = () => {};
@@ -474,7 +553,7 @@ function pageChannelRoutes(channel, options) {
474
553
  },
475
554
  notify: write,
476
555
  close: end
477
- }, shellWindow);
556
+ }, shellWindow, tab);
478
557
  const beat = setInterval(() => {
479
558
  write("ping", "");
480
559
  }, heartbeatMs);
@@ -508,7 +587,8 @@ function renderSnapshot(snapshot, more = (cursor) => `call ui_snapshot with curs
508
587
  const level = node.level === void 0 ? "" : ` level=${String(node.level)}`;
509
588
  const states = node.states.length === 0 ? "" : ` [${node.states.join(", ")}]`;
510
589
  const value = node.value === void 0 ? "" : ` value=${JSON.stringify(node.value)}`;
511
- return `${" ".repeat(Math.min(node.depth, 8))}- ${node.role}${node.name === "" ? "" : ` ${JSON.stringify(node.name)}`}${level}${states}${value} [ref=${node.ref}]`;
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}]`;
512
592
  });
513
593
  const rest = snapshot.nextCursor === void 0 ? "" : `\n(${String(snapshot.total - snapshot.nextCursor)} more elements; ${more(snapshot.nextCursor)})`;
514
594
  return `Page: ${snapshot.title || "(untitled)"}\nSnapshot ${String(snapshot.generation)}, ${String(snapshot.nodes.length)} of ${String(snapshot.total)} elements:\n${lines.join("\n")}${rest}`;
@@ -0,0 +1,427 @@
1
+ import { c as RAW_RESPONSE_HANDLED, i as createAdaptorServer, l as RAW_UPGRADE_HANDLED, r as UpgradeHub, u as Request } from "./test-websocket-CQOSvBth.js";
2
+ import "./serve-static-C1Nkj_xB.js";
3
+ import { Service } from "@deepseek-ai/cordis";
4
+ import z from "@deepseek-ai/schemastery";
5
+ //#region ../../plugins/acryl-node-server/src/index.ts
6
+ /** Stable Cordis plugin name, equal to the package name and the Loader row id. */
7
+ const name = "acryl-node-server";
8
+ const inject = ["router"];
9
+ /** The pathname of a request target, for raw matching: a malformed one matches nothing (`'/'` matches only itself). */
10
+ function pathnameOf(target) {
11
+ if (target === void 0) return "/";
12
+ try {
13
+ return new URL(target, "http://node-server.invalid").pathname;
14
+ } catch {
15
+ return "/";
16
+ }
17
+ }
18
+ /** Every field has a default, so a caller passes only what it changes: `ctx.plugin(plugin, { ... })` takes the partial shape. */
19
+ const Config = z.object({
20
+ hostname: z.string().default("127.0.0.1").description("Address to bind. Loopback by default; binding elsewhere exposes the app to the network."),
21
+ port: z.number().min(0).max(65535).default(0).description("Port to listen on; 0 lets the system choose. A taken port, or one a browser refuses, is followed by the next usable one."),
22
+ scan: z.number().min(0).default(20).description("How many further ports to try when the configured one is taken or a browser refuses it."),
23
+ websocket: z.object({
24
+ maxPayload: z.number().min(1).default(1024 * 1024).description("The largest WebSocket frame accepted, in bytes; a bigger one closes the socket with 1009."),
25
+ heartbeat: z.number().min(0).default(3e4).description("Milliseconds between pings; a peer that did not answer the last one is terminated. 0 turns it off."),
26
+ grace: z.number().min(0).default(2e3).description("Milliseconds a closing socket gets before it is terminated when its plugin unloads.")
27
+ }).default({})
28
+ });
29
+ /**
30
+ * Milliseconds a request already in flight, or an upgraded socket's own owner, may finish once the server stops taking new work. A save during a
31
+ * restart must not be cut; a stream that never ends must not hold the stop open.
32
+ */
33
+ const STOP_GRACE_MS = 2e3;
34
+ var NodeServer = class extends Service {
35
+ hostname;
36
+ config;
37
+ server;
38
+ router;
39
+ hub;
40
+ bound = 0;
41
+ /** The one stop this server runs: a second call (the disposer after an explicit stop) waits on the same promise. */
42
+ stopping;
43
+ /** Requests whose response has not finished yet: the stop waits for them before their sockets are cut. */
44
+ active = 0;
45
+ /** Upgraded sockets this server handed out (WebSockets): neither close() nor closeAllConnections() reaches them, so the stop tracks them itself. */
46
+ upgraded = /* @__PURE__ */ new Set();
47
+ /** Raw routes registered by the host's compatibility layer (see {@link registerRaw}). */
48
+ rawExact = /* @__PURE__ */ new Map();
49
+ rawPrefix = /* @__PURE__ */ new Map();
50
+ rawUpgrades = /* @__PURE__ */ new Map();
51
+ /** Gates registered by a host that maps an app's URLs (see {@link NodeServer.gate}); they run in the order they were added. */
52
+ gates = [];
53
+ /** One raw dispatch per upgrade request: a second listener (or a re-added one) must never emit the same request again. */
54
+ rawUpgradeSeen = /* @__PURE__ */ new WeakSet();
55
+ constructor(ctx, config) {
56
+ super(ctx, "nodeServer");
57
+ this.config = config;
58
+ this.hostname = config.hostname;
59
+ const router = ctx.router;
60
+ this.router = router;
61
+ this.server = createAdaptorServer({
62
+ fetch: (request, bindings) => {
63
+ const { incoming, outgoing } = bindings;
64
+ this.active += 1;
65
+ let finished = false;
66
+ const settle = () => {
67
+ if (finished) return;
68
+ finished = true;
69
+ this.active -= 1;
70
+ };
71
+ outgoing.once("finish", settle);
72
+ outgoing.once("close", settle);
73
+ if (incoming.headers.upgrade !== void 0) {
74
+ const websocket = incoming.headers.upgrade.toLowerCase() === "websocket";
75
+ return new Response("", {
76
+ status: websocket ? 503 : 400,
77
+ headers: { connection: "close" }
78
+ });
79
+ }
80
+ if (this.gates.length > 0) {
81
+ const requestLike = request;
82
+ const connection = {
83
+ remoteAddress: incoming.socket.remoteAddress,
84
+ localPort: incoming.socket.localPort
85
+ };
86
+ for (const gate of this.gates) {
87
+ const answer = gate(requestLike, connection);
88
+ if (answer !== void 0) return answer;
89
+ }
90
+ }
91
+ const raw = this.rawFor(incoming);
92
+ if (raw !== void 0) {
93
+ (async () => {
94
+ try {
95
+ await raw(incoming, outgoing);
96
+ } catch {
97
+ if (!outgoing.headersSent) {
98
+ outgoing.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
99
+ outgoing.end("internal error");
100
+ } else outgoing.destroy();
101
+ }
102
+ })();
103
+ return RAW_RESPONSE_HANDLED;
104
+ }
105
+ return router.fetch(new Request(request), {
106
+ remoteAddress: incoming.socket.remoteAddress,
107
+ localPort: incoming.socket.localPort
108
+ });
109
+ },
110
+ hostname: config.hostname,
111
+ overrideGlobalObjects: false
112
+ });
113
+ this.server.on("upgrade", (request, socket, head) => {
114
+ if (this.rawUpgradeSeen.has(request)) return;
115
+ this.upgraded.add(socket);
116
+ socket.once("close", () => {
117
+ this.upgraded.delete(socket);
118
+ });
119
+ socket.on("error", () => {
120
+ socket.destroy();
121
+ });
122
+ const websocket = request.headers.upgrade?.toLowerCase() === "websocket";
123
+ const handler = this.rawUpgrades.get(pathnameOf(request.url));
124
+ if (handler === void 0) {
125
+ if (!websocket || this.hub !== void 0) return;
126
+ if (socket.destroyed || socket.writableEnded) return;
127
+ request[RAW_UPGRADE_HANDLED] = true;
128
+ socket.end("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
129
+ return;
130
+ }
131
+ if (!websocket) {
132
+ if (socket.destroyed || socket.writableEnded) return;
133
+ request[RAW_UPGRADE_HANDLED] = true;
134
+ socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
135
+ return;
136
+ }
137
+ this.rawUpgradeSeen.add(request);
138
+ request[RAW_UPGRADE_HANDLED] = true;
139
+ Promise.resolve(handler(request, socket, head)).catch(() => socket.destroy());
140
+ });
141
+ ctx.effect(() => () => this.stop(), "acryl-node-server: close listener and open connections");
142
+ }
143
+ /**
144
+ * Stop serving, gracefully, and resolve once the port is free: no new connections; requests already in flight may finish (a stream that never ends is
145
+ * cut) within {@link STOP_GRACE_MS}; each upgraded socket's owner closes it with a close frame first, and what is left is destroyed. Idempotent - the
146
+ * disposer calls it.
147
+ *
148
+ * Calling this BEFORE the rest of the app is disposed is what makes the grace count: the router has its own disposal, and it aborts in-flight requests
149
+ * and ends their bodies by design, concurrently with every other disposer (Cordis runs them together). A launcher that disposes the context directly
150
+ * therefore cuts the request this drains; a launcher that stops the server first lets it answer, then disposes.
151
+ */
152
+ stop() {
153
+ this.stopping ??= this.closeGracefully();
154
+ return this.stopping;
155
+ }
156
+ async closeGracefully() {
157
+ const upgrades = this.hub?.close();
158
+ if (!this.server.listening) {
159
+ await upgrades;
160
+ return;
161
+ }
162
+ const stopped = new Promise((resolve) => {
163
+ this.server.close(() => {
164
+ resolve();
165
+ });
166
+ });
167
+ this.server.closeIdleConnections();
168
+ const settled = await this.settleStop(STOP_GRACE_MS);
169
+ this.server.closeIdleConnections();
170
+ if (!(settled ? await Promise.race([stopped.then(() => true), new Promise((resolve) => {
171
+ setTimeout(() => {
172
+ resolve(false);
173
+ }, 300);
174
+ })]) : false)) {
175
+ this.server.closeAllConnections();
176
+ for (const socket of this.upgraded) socket.destroy();
177
+ this.upgraded.clear();
178
+ }
179
+ await Promise.all([upgrades, stopped]);
180
+ }
181
+ /**
182
+ * Wait, bounded, until no request is in flight and no upgraded socket is left; true when both reached zero inside the bound. Every pass sweeps idle
183
+ * keep-alive sockets again: the single sweep `close()` does misses one that becomes idle after its request finished, and such a socket would otherwise
184
+ * hold the stop to its deadline.
185
+ */
186
+ async settleStop(ms) {
187
+ const deadline = Date.now() + ms;
188
+ while (Date.now() < deadline) {
189
+ if (this.active === 0 && this.upgraded.size === 0) return true;
190
+ this.server.closeIdleConnections();
191
+ await new Promise((resolve) => setTimeout(resolve, 25));
192
+ }
193
+ return this.active === 0 && this.upgraded.size === 0;
194
+ }
195
+ /**
196
+ * Serve a raw node handler at `path`, beside the router: `exact` matches the pathname only, `prefix` the path and everything below it. The
197
+ * handler gets the platform's `IncomingMessage`/`ServerResponse` and owns the response; the router is not run for it. A path already served
198
+ * throws. The returned remover restores the path (the server itself goes on serving the rest).
199
+ */
200
+ registerRaw(route) {
201
+ const table = route.kind === "exact" ? this.rawExact : this.rawPrefix;
202
+ if (table.has(route.path)) throw new Error(`acryl-node-server: ${route.path} is already served`);
203
+ table.set(route.path, route.handler);
204
+ return () => {
205
+ if (table.get(route.path) === route.handler) table.delete(route.path);
206
+ };
207
+ }
208
+ /**
209
+ * Answer matching requests before raw routes and the router, so a host can keep an app's URLs in scope under a prefix (the frame's canvas). A gate runs
210
+ * synchronously and can answer with a promise (see {@link RequestGate}); gates run in the order they were added, and the returned remover takes this one back.
211
+ */
212
+ gate(handler) {
213
+ this.gates.push(handler);
214
+ return () => {
215
+ const at = this.gates.indexOf(handler);
216
+ if (at !== -1) this.gates.splice(at, 1);
217
+ };
218
+ }
219
+ /**
220
+ * A gate's asynchronous answer: run the request it was given through the router, in the adapter's request form (a gate never makes one itself), with the
221
+ * socket facts. The raw routes do not run for that request.
222
+ */
223
+ throughRouter(request, connection = {}) {
224
+ return this.router.fetch(new Request(request), connection);
225
+ }
226
+ /**
227
+ * Claim an upgrade path for a raw node handler: the handler receives `(req, socket, head)` and from then on owns the socket. The router's
228
+ * WebSocket handling never sees a claimed upgrade. A path already claimed throws; the remover releases the claim. The transport's one raw
229
+ * listener exists for the server's whole lifetime and consults this registry, so claims coming and going never add or remove a listener.
230
+ */
231
+ registerRawUpgrade(route) {
232
+ if (this.rawUpgrades.has(route.path)) throw new Error(`acryl-node-server: the upgrade path ${route.path} is already claimed`);
233
+ this.rawUpgrades.set(route.path, route.handler);
234
+ return () => {
235
+ if (this.rawUpgrades.get(route.path) !== route.handler) return;
236
+ this.rawUpgrades.delete(route.path);
237
+ };
238
+ }
239
+ /** The raw handler for a request: an exact path first, then the longest matching prefix. */
240
+ rawFor(incoming) {
241
+ const path = pathnameOf(incoming.url);
242
+ const exact = this.rawExact.get(path);
243
+ if (exact !== void 0) return exact;
244
+ let best;
245
+ for (const [prefix, handler] of this.rawPrefix) if ((path === prefix || path.startsWith(`${prefix}/`)) && (best === void 0 || prefix.length > best.length)) best = {
246
+ length: prefix.length,
247
+ handler
248
+ };
249
+ return best?.handler;
250
+ }
251
+ /**
252
+ * `upgradeWebSocket` for the plugin that asks (call it once, in `apply`): `app.get('/ws', upgrade((c) => ({ onMessage(event, ws) { ... } })))`. Checks go in ordinary middleware on the
253
+ * route, and a handler that returns a `Response` refuses the upgrade with that status before any handshake. Every socket it accepts belongs to the calling plugin: when the plugin
254
+ * unloads they are closed with 1001 within `websocket.grace` and the handler sees `close` once. Pass `{ textOnly: true }` to drop binary frames. Needs the `ws` package.
255
+ */
256
+ upgrades() {
257
+ if (!this.hub) {
258
+ this.hub = new UpgradeHub(this.config.websocket);
259
+ this.hub.attach(this.server, this.ctx.router, { skipWhenRawUpgrade: true });
260
+ }
261
+ return this.hub.upgradeWebSocketFor(this.ctx);
262
+ }
263
+ /** How many `upgrade` listeners the transport holds (raw plus router setups); a test asserts it does not grow across claim changes. */
264
+ get upgradeListenerCount() {
265
+ return this.server.listenerCount("upgrade");
266
+ }
267
+ /** How many WebSocket connections are open now. */
268
+ get sockets() {
269
+ return this.hub?.size ?? 0;
270
+ }
271
+ /** The port in use, 0 before the server listens. */
272
+ get port() {
273
+ return this.bound;
274
+ }
275
+ /** The address clients use. */
276
+ get url() {
277
+ return `http://${this.hostname.includes(":") ? `[${this.hostname}]` : this.hostname}:${String(this.bound)}`;
278
+ }
279
+ /** Bind, scanning past taken ports and past the ports a browser refuses. */
280
+ async listen() {
281
+ const { port, scan, hostname } = this.config;
282
+ const last = port === 0 ? 0 : Math.min(65535, port + scan);
283
+ for (const candidate of portCandidates(port, scan)) try {
284
+ await new Promise((resolve, reject) => {
285
+ const onError = (error) => {
286
+ reject(error);
287
+ };
288
+ this.server.once("error", onError);
289
+ this.server.listen(candidate, hostname, () => {
290
+ this.server.off("error", onError);
291
+ resolve();
292
+ });
293
+ });
294
+ this.bound = this.server.address().port;
295
+ return;
296
+ } catch (error) {
297
+ if (!isPortUnavailable(error)) throw error;
298
+ }
299
+ throw new Error(`acryl-node-server: no usable port from ${String(port)} to ${String(last)} on ${hostname}: each is taken, or one a browser refuses`);
300
+ }
301
+ };
302
+ /**
303
+ * The ports every Fetch client refuses to connect to: the Fetch standard's bad ports (https://fetch.spec.whatwg.org/#port-blocking). Chromium
304
+ * refuses a page on one (ERR_UNSAFE_PORT) and Node's fetch answers "bad port"; the list below is the one Node 24 reports, probed port by port
305
+ * on 2026-11-03 (82 ports, 1 to 10080). An app's stable port is hashed into 3100..3999, and 3659 is on the list: without this rule one app in
306
+ * 900 would come up on an address no browser may open, which the frame gate caught that day (verify-frame-token was refused at 3659).
307
+ */
308
+ const RESTRICTED_PORTS = /* @__PURE__ */ new Set([
309
+ 1,
310
+ 7,
311
+ 9,
312
+ 11,
313
+ 13,
314
+ 15,
315
+ 17,
316
+ 19,
317
+ 20,
318
+ 21,
319
+ 22,
320
+ 23,
321
+ 25,
322
+ 37,
323
+ 42,
324
+ 43,
325
+ 53,
326
+ 69,
327
+ 77,
328
+ 79,
329
+ 87,
330
+ 95,
331
+ 101,
332
+ 102,
333
+ 103,
334
+ 104,
335
+ 109,
336
+ 110,
337
+ 111,
338
+ 113,
339
+ 115,
340
+ 117,
341
+ 119,
342
+ 123,
343
+ 135,
344
+ 137,
345
+ 139,
346
+ 143,
347
+ 161,
348
+ 179,
349
+ 389,
350
+ 427,
351
+ 465,
352
+ 512,
353
+ 513,
354
+ 514,
355
+ 515,
356
+ 526,
357
+ 530,
358
+ 531,
359
+ 532,
360
+ 540,
361
+ 548,
362
+ 554,
363
+ 556,
364
+ 563,
365
+ 587,
366
+ 601,
367
+ 636,
368
+ 989,
369
+ 990,
370
+ 993,
371
+ 995,
372
+ 1719,
373
+ 1720,
374
+ 1723,
375
+ 2049,
376
+ 3659,
377
+ 4045,
378
+ 4190,
379
+ 5060,
380
+ 5061,
381
+ 6e3,
382
+ 6566,
383
+ 6665,
384
+ 6666,
385
+ 6667,
386
+ 6668,
387
+ 6669,
388
+ 6679,
389
+ 6697,
390
+ 10080
391
+ ]);
392
+ /** Whether a browser refuses to open this port. */
393
+ function isRestrictedPort(port) {
394
+ return RESTRICTED_PORTS.has(port);
395
+ }
396
+ /**
397
+ * The ports `listen` may try, in order: `port` with at most `scan` further ones, without the ports a browser refuses. Port 0 stays alone: the
398
+ * system chooses it, and the ports it hands out are above the restricted range.
399
+ */
400
+ function portCandidates(port, scan) {
401
+ const last = port === 0 ? 0 : Math.min(65535, port + scan);
402
+ const candidates = [];
403
+ for (let candidate = port; candidate <= last; candidate += 1) if (!isRestrictedPort(candidate)) candidates.push(candidate);
404
+ return candidates;
405
+ }
406
+ /**
407
+ * Whether a failed bind means "this port, try the next one". A port another socket holds answers `EADDRINUSE` everywhere. On Windows a port
408
+ * inside a range the system reserves (Hyper-V, WSL and Docker exclude ranges, `netsh interface ipv4 show excludedportrange`) answers
409
+ * `EACCES`, which is a property of that port too; elsewhere `EACCES` means the process may not bind low ports at all, so moving on would
410
+ * only hide the reason.
411
+ */
412
+ function isPortUnavailable(error, platform = process.platform) {
413
+ const code = error?.code;
414
+ return code === "EADDRINUSE" || platform === "win32" && code === "EACCES";
415
+ }
416
+ /** Loads once the server listens, so a plugin that injects `nodeServer` can rely on `url`. A failure to bind fails the plugin. */
417
+ async function apply(ctx, config = {}) {
418
+ await new NodeServer(ctx, {
419
+ hostname: "127.0.0.1",
420
+ port: 0,
421
+ scan: 20,
422
+ websocket: {},
423
+ ...config
424
+ }).listen();
425
+ }
426
+ //#endregion
427
+ export { apply as a, isRestrictedPort as c, STOP_GRACE_MS as i, name as l, NodeServer as n, inject as o, RESTRICTED_PORTS as r, isPortUnavailable as s, Config as t, portCandidates as u };