acrylblends 1.1.0 → 1.3.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 (263) hide show
  1. package/README.md +9 -0
  2. package/dist/index.d.ts +47 -2
  3. package/dist/index.js +2017 -32
  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 +5 -3
  8. package/docs/framework/workshop/README.md +139 -153
  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 +87 -9
  72. package/lib/cordis/door.js +324 -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/plugin.json +3 -0
  98. package/plugins/acryl-door/src/cli/acryl.ts +63 -6
  99. package/plugins/acryl-door/src/host/channel.ts +86 -9
  100. package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
  101. package/plugins/acryl-door/src/host/door-file.ts +7 -2
  102. package/plugins/acryl-door/src/host/door.ts +128 -24
  103. package/plugins/acryl-door/src/host/operator-routes.ts +21 -1
  104. package/plugins/acryl-door/src/host/page-routes.ts +3 -1
  105. package/plugins/acryl-door/src/host/page-socket.ts +18 -8
  106. package/plugins/acryl-door/src/host/verbs.ts +14 -1
  107. package/plugins/acryl-door/src/index.ts +168 -30
  108. package/plugins/acryl-door/src/page/base.ts +32 -0
  109. package/plugins/acryl-door/src/page/channel.ts +3 -1
  110. package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
  111. package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
  112. package/plugins/acryl-door/src/page/driver/driver.ts +32 -4
  113. package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
  114. package/plugins/acryl-door/src/page/index.ts +41 -2
  115. package/plugins/acryl-door/src/page/outline.ts +56 -0
  116. package/plugins/acryl-door/src/page/protocol.ts +4 -0
  117. package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
  118. package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
  119. package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
  120. package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
  121. package/plugins/acryl-node-server/src/client-modules.ts +7 -1
  122. package/plugins/acryl-node-server/src/index.ts +304 -16
  123. package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
  124. package/plugins/acryl-page/plugin.json +12 -0
  125. package/plugins/acryl-page/src/index.ts +163 -0
  126. package/plugins/acryl-router/src/service.ts +77 -14
  127. package/plugins/acryl-router-http/src/index.ts +15 -1
  128. package/plugins/acryl-router-http/src/serve.ts +31 -0
  129. package/plugins/acryl-web-server/plugin.json +14 -0
  130. package/plugins/acryl-web-server/src/index.ts +164 -0
  131. package/plugins/agent/agent.ts +38 -7
  132. package/plugins/agent/index.ts +1 -1
  133. package/plugins/agent/model.ts +10 -2
  134. package/plugins/agent/tools.ts +9 -3
  135. package/plugins/agent-panel/client/describe.ts +57 -0
  136. package/plugins/agent-panel/client/driver-pick.ts +22 -0
  137. package/plugins/agent-panel/client/frame.ts +51 -0
  138. package/plugins/agent-panel/client/main.ts +238 -26
  139. package/plugins/agent-panel/drivers.ts +7 -2
  140. package/plugins/agent-panel/headless.ts +5 -2
  141. package/plugins/agent-panel/index.ts +146 -57
  142. package/plugins/agent-panel/mount.ts +27 -0
  143. package/plugins/agent-panel/visitors.ts +231 -0
  144. package/plugins/builder/index.ts +13 -35
  145. package/plugins/builder/package.ts +186 -0
  146. package/plugins/builder/plugin.json +3 -1
  147. package/plugins/desktop/frame.ts +231 -0
  148. package/plugins/desktop/launch.ts +44 -9
  149. package/plugins/desktop/main.ts +100 -32
  150. package/plugins/desktop/window.ts +21 -4
  151. package/plugins/driver-pty/client/terminal.ts +4 -1
  152. package/plugins/driver-pty/index.ts +38 -11
  153. package/plugins/driver-pty/spawn.ts +38 -0
  154. package/plugins/driver-pty/terminal.ts +2 -0
  155. package/plugins/extensions/index.ts +47 -18
  156. package/plugins/extensions/plugin-store.ts +27 -0
  157. package/plugins/extensions/plugin.json +1 -1
  158. package/plugins/extensions/resolve.ts +13 -0
  159. package/plugins/extensions/stage.ts +15 -5
  160. package/plugins/feedback/client.d.ts +22 -0
  161. package/plugins/feedback/client.js +471 -0
  162. package/plugins/feedback/index.ts +143 -0
  163. package/plugins/feedback/plugin.json +11 -0
  164. package/plugins/feedback/store.ts +307 -0
  165. package/plugins/home/index.ts +122 -0
  166. package/plugins/home/plugin.json +11 -0
  167. package/plugins/instance/index.ts +6 -0
  168. package/plugins/manifest/app-plugins.ts +86 -0
  169. package/plugins/manifest/index.ts +12 -2
  170. package/plugins/manifest/plugin.json +1 -1
  171. package/plugins/manifest/snapshot.ts +275 -0
  172. package/plugins/openai/index.ts +64 -5
  173. package/plugins/openai/prompt.ts +23 -0
  174. package/plugins/plugin-share/README.md +51 -0
  175. package/plugins/plugin-share/index.ts +467 -0
  176. package/plugins/plugin-share/plugin.json +15 -0
  177. package/plugins/plugin-share/publish.ts +65 -0
  178. package/plugins/plugin-share/pull-check.ts +129 -0
  179. package/plugins/plugin-share/pulled-records.ts +70 -0
  180. package/shared/AGENTS.core.md +36 -16
  181. package/shared/skills/acryl-add-ui/SKILL.md +19 -0
  182. package/shared/skills/acryl-app/SKILL.md +13 -0
  183. package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
  184. package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
  185. package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
  186. package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
  187. package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
  188. package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
  189. package/shared/skills/cordis-plugin/SKILL.md +26 -0
  190. package/templates/agent/.gitattributes +2 -0
  191. package/templates/agent/AGENTS.md +1 -1
  192. package/templates/agent/blend.yaml +6 -0
  193. package/templates/agent/gitignore +1 -0
  194. package/templates/agent/package.json +1 -0
  195. package/templates/agent/src/app.ts +154 -33
  196. package/templates/agent/src/frame.ts +17 -0
  197. package/templates/agent/src/main.ts +5 -3
  198. package/templates/agent/src/routes.ts +38 -19
  199. package/templates/agent/template.json +3 -0
  200. package/templates/agent/test/app.test.ts +45 -3
  201. package/templates/blank/.gitattributes +2 -0
  202. package/templates/blank/AGENTS.md +3 -2
  203. package/templates/blank/blend.yaml +11 -0
  204. package/templates/blank/gitignore +1 -0
  205. package/templates/blank/package.json +1 -0
  206. package/templates/blank/src/app.ts +100 -29
  207. package/templates/blank/src/door.ts +7 -0
  208. package/templates/blank/src/first-page.ts +5 -0
  209. package/templates/blank/src/frame.ts +17 -0
  210. package/templates/blank/src/routes.ts +65 -8
  211. package/templates/blank/template.json +14 -1
  212. package/templates/blank/test/app.test.ts +103 -16
  213. package/templates/chat/.gitattributes +2 -0
  214. package/templates/chat/blend.yaml +2 -1
  215. package/templates/chat/gitignore +1 -0
  216. package/templates/chat/package.json +1 -0
  217. package/templates/chat/src/app.ts +72 -22
  218. package/templates/chat/src/chat-app.ts +11 -2
  219. package/templates/chat/src/client/chat.tsx +4 -2
  220. package/templates/chat/src/frame.ts +17 -0
  221. package/templates/chat/src/page.tsx +6 -4
  222. package/templates/chat/src/routes.ts +14 -4
  223. package/templates/chat/src/runtime.ts +9 -3
  224. package/templates/chat/template.json +3 -1
  225. package/templates/chat/test/chat.test.ts +46 -1
  226. package/templates/chat/test/server.test.ts +17 -3
  227. package/templates/nodejs/.gitattributes +2 -0
  228. package/templates/nodejs/blend.yaml +2 -1
  229. package/templates/nodejs/gitignore +1 -0
  230. package/templates/nodejs/package.json +1 -0
  231. package/templates/nodejs/src/app.ts +76 -21
  232. package/templates/nodejs/src/frame.ts +17 -0
  233. package/templates/nodejs/src/page.tsx +3 -3
  234. package/templates/nodejs/src/routes.ts +32 -13
  235. package/templates/nodejs/template.json +3 -1
  236. package/templates/nodejs/test/app.test.ts +38 -3
  237. package/templates/nodejs/test/server.test.ts +17 -3
  238. package/templates/notes/.gitattributes +2 -0
  239. package/templates/notes/blend.yaml +2 -1
  240. package/templates/notes/gitignore +1 -0
  241. package/templates/notes/package.json +1 -0
  242. package/templates/notes/src/app.ts +72 -22
  243. package/templates/notes/src/frame.ts +17 -0
  244. package/templates/notes/src/notes-app.ts +24 -17
  245. package/templates/notes/src/routes.ts +11 -4
  246. package/templates/notes/src/runtime.ts +9 -3
  247. package/templates/notes/src/views.tsx +26 -17
  248. package/templates/notes/template.json +3 -1
  249. package/templates/notes/test/notes.test.ts +39 -1
  250. package/templates/notes/test/server.test.ts +17 -3
  251. package/templates/todo/.gitattributes +2 -0
  252. package/templates/todo/blend.yaml +3 -0
  253. package/templates/todo/gitignore +1 -0
  254. package/templates/todo/package.json +1 -0
  255. package/templates/todo/src/app.ts +81 -27
  256. package/templates/todo/src/door.ts +7 -0
  257. package/templates/todo/src/frame.ts +17 -0
  258. package/templates/todo/src/page.ts +13 -6
  259. package/templates/todo/src/todo.ts +18 -6
  260. package/templates/todo/template.json +11 -1
  261. package/templates/todo/test/todo.test.ts +67 -9
  262. package/lib/src-LX0E4ec4.js +0 -117
  263. package/shared/SKILL.md +0 -14
@@ -1,6 +1,9 @@
1
- import { c as UpgradeHubOptions, i as testWebSocket, l as UpgradeRouter, n as TestWebSocketOptions, o as UpgradeEmitter, p as WebSocketLike, r as TestWebSocketResult, s as UpgradeHub, t as TestSocket, u as RequestError } from "../test-websocket-DyeS50Tr.js";
1
+ import { t as ConnectionInfo } from "../connection-ZLEso7XL.js";
2
+ import { c as UpgradeHubOptions, i as testWebSocket, l as UpgradeRouter, n as TestWebSocketOptions, o as UpgradeEmitter, p as WebSocketLike, r as TestWebSocketResult, s as UpgradeHub, t as TestSocket, u as RequestError } from "../test-websocket-GUTAkFQT.js";
2
3
  import { n as UpgradeWebSocket } from "../index-C-SklY6u.js";
3
4
  import { n as serveStatic, t as ServeStaticOptions } from "../serve-static-B7sqnftW.js";
5
+ import { IncomingMessage, ServerResponse } from "node:http";
6
+ import { Duplex } from "node:stream";
4
7
  import { Context, Service } from "@deepseek-ai/cordis";
5
8
  import z from "@deepseek-ai/schemastery";
6
9
 
@@ -8,6 +11,24 @@ import z from "@deepseek-ai/schemastery";
8
11
  /** Stable Cordis plugin name, equal to the package name and the Loader row id. */
9
12
  declare const name = "acryl-node-server";
10
13
  declare const inject: string[];
14
+ /** A raw node handler: the request and response are the platform's, and the adapter writes nothing for them. */
15
+ interface RawRoute {
16
+ readonly kind: 'prefix' | 'exact';
17
+ readonly path: string;
18
+ readonly handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
19
+ }
20
+ /** A raw upgrade handler: the socket is the handler's once the path matches; the router never sees the upgrade. */
21
+ interface RawUpgradeRoute {
22
+ readonly path: string;
23
+ readonly handler: (req: IncomingMessage, socket: Duplex, head: Buffer) => void | Promise<void>;
24
+ }
25
+ /**
26
+ * Answer a request before raw routes and the router, or pass it on with `undefined`. The callback runs synchronously, so a request no gate answers still
27
+ * reaches a raw route's sentinel synchronously; a gate that needs the router (a host keeping an app's URLs in scope under a prefix, the ACRYL frame's canvas)
28
+ * returns its promise, and the adapter handles that like the router's own answer. `connection` carries the socket facts, which a router mount needs to check
29
+ * the peer and the real port.
30
+ */
31
+ type RequestGate = (request: Request, connection: ConnectionInfo) => Response | Promise<Response> | undefined;
11
32
  interface Config {
12
33
  hostname: string;
13
34
  port: number;
@@ -22,28 +43,105 @@ declare module '@deepseek-ai/cordis' {
22
43
  nodeServer: NodeServer;
23
44
  }
24
45
  }
46
+ /**
47
+ * 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
48
+ * restart must not be cut; a stream that never ends must not hold the stop open.
49
+ */
50
+ declare const STOP_GRACE_MS = 2000;
25
51
  declare class NodeServer extends Service {
26
52
  readonly hostname: string;
27
53
  readonly config: Config;
28
54
  private readonly server;
55
+ private readonly router;
29
56
  private hub;
30
57
  private bound;
58
+ /** The one stop this server runs: a second call (the disposer after an explicit stop) waits on the same promise. */
59
+ private stopping;
60
+ /** Requests whose response has not finished yet: the stop waits for them before their sockets are cut. */
61
+ private active;
62
+ /** Upgraded sockets this server handed out (WebSockets): neither close() nor closeAllConnections() reaches them, so the stop tracks them itself. */
63
+ private readonly upgraded;
64
+ /** Raw routes registered by the host's compatibility layer (see {@link registerRaw}). */
65
+ private readonly rawExact;
66
+ private readonly rawPrefix;
67
+ private readonly rawUpgrades;
68
+ /** Gates registered by a host that maps an app's URLs (see {@link NodeServer.gate}); they run in the order they were added. */
69
+ private readonly gates;
70
+ /** One raw dispatch per upgrade request: a second listener (or a re-added one) must never emit the same request again. */
71
+ private readonly rawUpgradeSeen;
31
72
  constructor(ctx: Context, config: Config);
73
+ /**
74
+ * 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
75
+ * 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
76
+ * disposer calls it.
77
+ *
78
+ * 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
79
+ * and ends their bodies by design, concurrently with every other disposer (Cordis runs them together). A launcher that disposes the context directly
80
+ * therefore cuts the request this drains; a launcher that stops the server first lets it answer, then disposes.
81
+ */
82
+ stop(): Promise<void>;
83
+ private closeGracefully;
84
+ /**
85
+ * 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
86
+ * keep-alive sockets again: the single sweep `close()` does misses one that becomes idle after its request finished, and such a socket would otherwise
87
+ * hold the stop to its deadline.
88
+ */
89
+ private settleStop;
90
+ /**
91
+ * Serve a raw node handler at `path`, beside the router: `exact` matches the pathname only, `prefix` the path and everything below it. The
92
+ * handler gets the platform's `IncomingMessage`/`ServerResponse` and owns the response; the router is not run for it. A path already served
93
+ * throws. The returned remover restores the path (the server itself goes on serving the rest).
94
+ */
95
+ registerRaw(route: RawRoute): () => void;
96
+ /**
97
+ * 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
98
+ * 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.
99
+ */
100
+ gate(handler: RequestGate): () => void;
101
+ /**
102
+ * 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
103
+ * socket facts. The raw routes do not run for that request.
104
+ */
105
+ throughRouter(request: Request, connection?: ConnectionInfo): Promise<Response>;
106
+ /**
107
+ * 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
108
+ * WebSocket handling never sees a claimed upgrade. A path already claimed throws; the remover releases the claim. The transport's one raw
109
+ * listener exists for the server's whole lifetime and consults this registry, so claims coming and going never add or remove a listener.
110
+ */
111
+ registerRawUpgrade(route: RawUpgradeRoute): () => void;
112
+ /** The raw handler for a request: an exact path first, then the longest matching prefix. */
113
+ private rawFor;
32
114
  /**
33
115
  * `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
34
116
  * 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
35
117
  * 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.
36
118
  */
37
119
  upgrades(): UpgradeWebSocket<WebSocketLike>;
120
+ /** How many `upgrade` listeners the transport holds (raw plus router setups); a test asserts it does not grow across claim changes. */
121
+ get upgradeListenerCount(): number;
38
122
  /** How many WebSocket connections are open now. */
39
123
  get sockets(): number;
40
124
  /** The port in use, 0 before the server listens. */
41
125
  get port(): number;
42
126
  /** The address clients use. */
43
127
  get url(): string;
44
- /** Bind, scanning past taken ports. */
128
+ /** Bind, scanning past taken ports and past the ports a browser refuses. */
45
129
  listen(): Promise<void>;
46
130
  }
131
+ /**
132
+ * The ports every Fetch client refuses to connect to: the Fetch standard's bad ports (https://fetch.spec.whatwg.org/#port-blocking). Chromium
133
+ * 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
134
+ * 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
135
+ * 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).
136
+ */
137
+ declare const RESTRICTED_PORTS: ReadonlySet<number>;
138
+ /** Whether a browser refuses to open this port. */
139
+ declare function isRestrictedPort(port: number): boolean;
140
+ /**
141
+ * 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
142
+ * system chooses it, and the ports it hands out are above the restricted range.
143
+ */
144
+ declare function portCandidates(port: number, scan: number): number[];
47
145
  /**
48
146
  * Whether a failed bind means "this port, try the next one". A port another socket holds answers `EADDRINUSE` everywhere. On Windows a port
49
147
  * inside a range the system reserves (Hyper-V, WSL and Docker exclude ranges, `netsh interface ipv4 show excludedportrange`) answers
@@ -54,4 +152,4 @@ declare function isPortUnavailable(error: unknown, platform?: NodeJS.Platform):
54
152
  /** Loads once the server listens, so a plugin that injects `nodeServer` can rely on `url`. A failure to bind fails the plugin. */
55
153
  declare function apply(ctx: Context, config?: Partial<Config>): Promise<void>;
56
154
  //#endregion
57
- export { Config, NodeServer, RequestError, type ServeStaticOptions, type TestSocket, type TestWebSocketOptions, type TestWebSocketResult, UpgradeEmitter, UpgradeHub, type UpgradeHubOptions, type UpgradeRouter, apply, inject, isPortUnavailable, name, serveStatic, testWebSocket };
155
+ export { Config, NodeServer, RESTRICTED_PORTS, RawRoute, RawUpgradeRoute, RequestError, RequestGate, STOP_GRACE_MS, type ServeStaticOptions, type TestSocket, type TestWebSocketOptions, type TestWebSocketResult, UpgradeEmitter, UpgradeHub, type UpgradeHubOptions, type UpgradeRouter, apply, inject, isPortUnavailable, isRestrictedPort, name, portCandidates, serveStatic, testWebSocket };
@@ -1,4 +1,4 @@
1
- import { l as RequestError, n as UpgradeEmitter, r as UpgradeHub, t as testWebSocket } from "../test-websocket-BLeEvybj.js";
1
+ import { d as RequestError, n as UpgradeEmitter, r as UpgradeHub, t as testWebSocket } from "../test-websocket-CQOSvBth.js";
2
2
  import { t as serveStatic } from "../serve-static-C1Nkj_xB.js";
3
- import { a as isPortUnavailable, i as inject, n as NodeServer, o as name, r as apply, t as Config } from "../src-LX0E4ec4.js";
4
- export { Config, NodeServer, RequestError, UpgradeEmitter, UpgradeHub, apply, inject, isPortUnavailable, name, serveStatic, testWebSocket };
3
+ import { a as apply, c as isRestrictedPort, i as STOP_GRACE_MS, l as name, n as NodeServer, o as inject, r as RESTRICTED_PORTS, s as isPortUnavailable, t as Config, u as portCandidates } from "../src-CtkIX_Gg.js";
4
+ export { Config, NodeServer, RESTRICTED_PORTS, RequestError, STOP_GRACE_MS, UpgradeEmitter, UpgradeHub, apply, inject, isPortUnavailable, isRestrictedPort, name, portCandidates, serveStatic, testWebSocket };
@@ -1,5 +1,5 @@
1
1
  import { n as UpgradeWebSocket } from "../index-C-SklY6u.js";
2
- import { i as RouterHttpService } from "../index-DMOtA-NJ.js";
2
+ import { i as RouterHttpService } from "../index-DGq7H0lb.js";
3
3
  import { IncomingMessage, ServerResponse } from "node:http";
4
4
  import { Duplex } from "node:stream";
5
5
  import { Context, Service } from "@deepseek-ai/cordis";
@@ -52,6 +52,10 @@ declare class RouterHttp extends Service implements RouterHttpService {
52
52
  private readonly router;
53
53
  private readonly web;
54
54
  private hub;
55
+ /** One upgrade helper per calling context: a fresh wrapper per call made every service event look like an owner change. */
56
+ private readonly upgradeHelpers;
57
+ /** A stable identity for this service instance's hub: a caller that must recognize the same owner across calls keys on this, not on a helper. */
58
+ private readonly hubIdentity;
55
59
  /** The adapter's own context: the hub lives as long as the adapter, whichever plugin asked for it first (`this.ctx` is the caller's). */
56
60
  private readonly home;
57
61
  private readonly upgradesIn;
@@ -69,6 +73,8 @@ declare class RouterHttp extends Service implements RouterHttpService {
69
73
  * `upgradeWebSocket` for the calling plugin: the sockets its routes accept are closed with 1001 when it unloads. Needs the `ws` package.
70
74
  * The route must be on a path the plugin mounted with `upgrade: [path]`.
71
75
  */
76
+ /** Stable for this service's lifetime; a re-provided adapter has a new one, which is how a caller sees a new owner. */
77
+ hubToken(): object;
72
78
  upgrades(): UpgradeWebSocket;
73
79
  /** How many WebSocket connections are open now. */
74
80
  get sockets(): number;
@@ -1,5 +1,5 @@
1
- import { n as UpgradeEmitter, r as UpgradeHub } from "../test-websocket-BLeEvybj.js";
2
- import "../src-LX0E4ec4.js";
1
+ import { n as UpgradeEmitter, r as UpgradeHub } from "../test-websocket-CQOSvBth.js";
2
+ import "../src-CtkIX_Gg.js";
3
3
  import { Readable } from "node:stream";
4
4
  import { Service } from "@deepseek-ai/cordis";
5
5
  import { pipeline } from "node:stream/promises";
@@ -59,6 +59,33 @@ async function sendResponse(res, response, method) {
59
59
  }
60
60
  //#endregion
61
61
  //#region ../../plugins/acryl-router-http/src/serve.ts
62
+ /** How long a refusal waits for a request body the handler never read. */
63
+ const REQUEST_BODY_SETTLE_MS = 1e3;
64
+ /**
65
+ * Read and discard a request body the handler left unread (a guard's 403, a 415, a body over its limit), so the client can finish sending and
66
+ * read the answer. Ending a response while request data is still arriving makes Node close the socket with that data pending, and the peer sees a
67
+ * reset instead of the answer: reliably on Windows (the workspace git worktree test's read ECONNRESET) and on other systems under load. Bounded:
68
+ * a client that stops sending waits at most `ms`, then loses the socket as before.
69
+ */
70
+ function settleRequestBody(req, ms = REQUEST_BODY_SETTLE_MS) {
71
+ if (req.readableEnded || req.destroyed) return Promise.resolve();
72
+ return new Promise((resolve) => {
73
+ let timer;
74
+ const done = () => {
75
+ if (timer !== void 0) clearTimeout(timer);
76
+ req.off("end", done);
77
+ req.off("close", done);
78
+ req.off("error", done);
79
+ resolve();
80
+ };
81
+ timer = setTimeout(done, ms);
82
+ timer.unref?.();
83
+ req.once("end", done);
84
+ req.once("close", done);
85
+ req.once("error", done);
86
+ req.resume();
87
+ });
88
+ }
62
89
  /**
63
90
  * Serve one node request through the router and write the result. The request's abort signal fires when the client
64
91
  * disconnects before the response finished; the socket's peer address and local port travel as connection info, never in the request.
@@ -78,6 +105,7 @@ async function serve(router, req, res) {
78
105
  } catch {
79
106
  response = new Response("Internal Server Error", { status: 500 });
80
107
  }
108
+ if (method !== "GET" && method !== "HEAD") await settleRequestBody(req);
81
109
  await sendResponse(res, response, method);
82
110
  }
83
111
  //#endregion
@@ -91,6 +119,10 @@ var RouterHttp = class extends Service {
91
119
  router;
92
120
  web;
93
121
  hub;
122
+ /** One upgrade helper per calling context: a fresh wrapper per call made every service event look like an owner change. */
123
+ upgradeHelpers = /* @__PURE__ */ new WeakMap();
124
+ /** A stable identity for this service instance's hub: a caller that must recognize the same owner across calls keys on this, not on a helper. */
125
+ hubIdentity = {};
94
126
  /** The adapter's own context: the hub lives as long as the adapter, whichever plugin asked for it first (`this.ctx` is the caller's). */
95
127
  home;
96
128
  upgradesIn = new UpgradeEmitter();
@@ -141,8 +173,17 @@ var RouterHttp = class extends Service {
141
173
  * `upgradeWebSocket` for the calling plugin: the sockets its routes accept are closed with 1001 when it unloads. Needs the `ws` package.
142
174
  * The route must be on a path the plugin mounted with `upgrade: [path]`.
143
175
  */
176
+ /** Stable for this service's lifetime; a re-provided adapter has a new one, which is how a caller sees a new owner. */
177
+ hubToken() {
178
+ return this.hubIdentity;
179
+ }
144
180
  upgrades() {
145
- return this.ensureHub().upgradeWebSocketFor(this.ctx);
181
+ const owner = this.ctx;
182
+ const cached = this.upgradeHelpers.get(owner);
183
+ if (cached !== void 0) return cached;
184
+ const created = this.ensureHub().upgradeWebSocketFor(owner);
185
+ this.upgradeHelpers.set(owner, created);
186
+ return created;
146
187
  }
147
188
  /** How many WebSocket connections are open now. */
148
189
  get sockets() {
@@ -2,6 +2,6 @@ import { B as NotFoundHandler, L as MiddlewareHandler, O as Handler, Q as Valida
2
2
  import { c as StatusCode, n as ContentfulStatusCode } from "../http-status-DaAwGAFL.js";
3
3
  import { a as getConnInfo, i as connectionOf, n as RouterBindings, r as RouterEnv, t as ConnectionInfo } from "../connection-ZLEso7XL.js";
4
4
  import { t as Acryl } from "../app-D8riatvv.js";
5
- import { a as TrailingSlash, i as RouterHttpService, n as MountableApp, o as apply, r as Router, s as name, t as Config } from "../index-DMOtA-NJ.js";
5
+ import { a as TrailingSlash, i as RouterHttpService, n as MountableApp, o as apply, r as Router, s as name, t as Config } from "../index-DGq7H0lb.js";
6
6
  import { t as HTTPException } from "../http-exception-kpGJkmpi.js";
7
7
  export { Acryl, type AcrylRequest, Config, type ConnectionInfo, type ContentfulStatusCode, Context, type ContextRenderer, type ContextVariableMap, type Env, type ErrorHandler, type ExecutionContext, HTTPException, type Handler, type Input, type MiddlewareHandler, type MountableApp, type Next, type NotFoundHandler, type NotFoundResponse, Router, type RouterBindings, type RouterEnv, type RouterHttpService, type Schema, type StatusCode, type ToSchema, type TrailingSlash, type TypedResponse, type ValidationTargets, apply, connectionOf, getConnInfo, name };
@@ -2,5 +2,5 @@ import { t as HTTPException } from "../http-exception-MjWkT89V.js";
2
2
  import { t as Context } from "../context-CkOh7Ygv.js";
3
3
  import { t as Acryl } from "../app-CQcGWjrk.js";
4
4
  import { n as getConnInfo, t as connectionOf } from "../connection-C7xgP1hP.js";
5
- import { i as name, n as Router, r as apply, t as Config } from "../src-BTBm4OPD.js";
5
+ import { i as name, n as Router, r as apply, t as Config } from "../src-iBJ2ysV9.js";
6
6
  export { Acryl, Config, Context, HTTPException, Router, apply, connectionOf, getConnInfo, name };
@@ -3,7 +3,7 @@ import { t as methodNotAllowed } from "../method-not-allowed-TO2_hnfl.js";
3
3
  import { r as streamSSE } from "../streaming-BlDaph8U.js";
4
4
  import { i as loopbackOnly, r as loopbackAuthority } from "../loopback-only-BbasoF7R.js";
5
5
  import "../helpers-BEjcdbgw.js";
6
- import "../src-BTBm4OPD.js";
6
+ import "../src-iBJ2ysV9.js";
7
7
  import "../middleware-BfG_fp8P.js";
8
8
  import { Service } from "@deepseek-ai/cordis";
9
9
  //#region ../../plugins/acryl-state/src/cell.ts
@@ -1,6 +1,6 @@
1
1
  import { t as Acryl } from "../app-D8riatvv.js";
2
- import { n as AuditEntry, y as UiSnapshot } from "../protocol-a2aqrp2X.js";
3
- import { i as UiChannel, n as auditPath, r as PageStream, t as AuditLog } from "../audit-BFpULBCj.js";
2
+ import { n as AuditEntry, y as UiSnapshot } from "../protocol-BeIYCgTo.js";
3
+ import { i as UiChannel, n as auditPath, r as PageStream, t as AuditLog } from "../audit-Bok_UNCn.js";
4
4
 
5
5
  //#region ../../plugins/acryl-door/src/host/page-routes.d.ts
6
6
  /** How often the Host writes a `ping`; the page gives up on a stream that is silent for three of these. */
package/lib/door/host.js CHANGED
@@ -1,2 +1,2 @@
1
- import { d as AuditLog, f as auditPath, n as HEARTBEAT_MS, p as UiChannel, r as pageChannelRoutes, t as renderSnapshot } from "../snapshot-text-BwM6T2nG.js";
1
+ import { d as AuditLog, f as auditPath, n as HEARTBEAT_MS, p as UiChannel, r as pageChannelRoutes, t as renderSnapshot } from "../snapshot-text-CXTnjxGx.js";
2
2
  export { AuditLog, HEARTBEAT_MS, UiChannel, auditPath, pageChannelRoutes, renderSnapshot };
@@ -1,4 +1,4 @@
1
- import { _ as UiRequest, v as UiResult } from "../protocol-a2aqrp2X.js";
1
+ import { _ as UiRequest, v as UiResult } from "../protocol-BeIYCgTo.js";
2
2
 
3
3
  //#region ../../plugins/acryl-door/src/page/driver/driver.d.ts
4
4
  interface DriverActivity {
@@ -16,6 +16,8 @@ interface UiDriverOptions {
16
16
  /** Milliseconds since the user last used the page (mouse, keyboard); Infinity when never. */
17
17
  readonly msSinceUserInput?: () => number;
18
18
  readonly sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
19
+ /** Called with the element an act is about to use, for the page to show it (the door's outline on what the agent touches). */
20
+ readonly touched?: (element: Element) => void;
19
21
  /** Leave the page: reload it, or go to a path of the app. Default: the document's own `location`, a moment after the answer is sent. */
20
22
  readonly leave?: (to: {
21
23
  readonly reload: true;
@@ -54,6 +56,11 @@ declare class UiDriver {
54
56
  /** @param signal - the Host cancelled the call. @throws UiControlError. */
55
57
  handle(request: UiRequest, signal?: AbortSignal): Promise<UiResult>;
56
58
  private run;
59
+ /**
60
+ * Run an act that may start a navigation (a click on a link, Enter in a form) and say so: the browser asks `beforeunload` when a navigation starts, during the act or a
61
+ * moment after it. The answer is sent before the page goes; the Host then waits for the next page instead of reading this one again.
62
+ */
63
+ private watchLeaving;
57
64
  /**
58
65
  * The page is about to be replaced, so the answer is given first and the move happens a moment later (the answer is a POST that the page's own unload would cut off).
59
66
  * The Host does not wait for this answer to be the last word: it waits for the page to come back.
@@ -81,6 +88,8 @@ interface ChannelSource {
81
88
  declare const SILENCE_MS = 45000;
82
89
  interface PageChannelOptions {
83
90
  readonly windowId: string;
91
+ /** The browser tab the page is in (kept across the tab's reloads and moves), so the Host follows a run's page into the next document of its tab. */
92
+ readonly tab?: string;
84
93
  /** The desktop window this page is in (`AcrylWindow/<n>` in the user agent, set by the Electron shell), so the Host can drive it by the number `window list` prints. */
85
94
  readonly shellWindow?: number;
86
95
  readonly focused: () => boolean;
package/lib/door/page.js CHANGED
@@ -84,7 +84,7 @@ var PageChannel = class {
84
84
  }
85
85
  connect() {
86
86
  if (this.disposed || this.source !== void 0) return;
87
- const query = `window=${encodeURIComponent(this.options.windowId)}&focused=${this.options.focused() ? "1" : "0"}${this.options.shellWindow === void 0 ? "" : `&shell=${String(this.options.shellWindow)}`}`;
87
+ const query = `window=${encodeURIComponent(this.options.windowId)}&focused=${this.options.focused() ? "1" : "0"}${this.options.shellWindow === void 0 ? "" : `&shell=${String(this.options.shellWindow)}`}${this.options.tab === void 0 ? "" : `&tab=${encodeURIComponent(this.options.tab)}`}`;
88
88
  const viaSocket = this.socketAllowed;
89
89
  this.socketReady = false;
90
90
  const source = viaSocket ? (this.options.createSocket ?? ((url) => socketSource(new WebSocket(url))))(`${socketBase(this.options.base)}/channel/ws?${query}`) : (this.options.createSource ?? ((url) => new EventSource(url)))(`${this.options.base}/channel?${query}`);
@@ -497,6 +497,15 @@ function valueOf(el, role) {
497
497
  else if (el.hasAttribute("contenteditable")) value = el.textContent ?? void 0;
498
498
  return value === void 0 || value === "" ? void 0 : clip(value, 100);
499
499
  }
500
+ /** Choices of a select listed at most; a longer list ends with how many more there are. */
501
+ const MAX_OPTIONS = 20;
502
+ /** The choices of a native select (what `act select` takes), or undefined when it is not one or is sensitive. */
503
+ function optionsOf(el) {
504
+ if (!(el instanceof HTMLSelectElement) || isSensitive(el)) return void 0;
505
+ const labels = [...el.options].filter((option) => !option.hidden).map((option) => clip((option.textContent ?? "").trim(), 60)).filter((label) => label !== "");
506
+ if (labels.length <= MAX_OPTIONS) return labels;
507
+ return [...labels.slice(0, MAX_OPTIONS), `(${String(labels.length - MAX_OPTIONS)} more)`];
508
+ }
500
509
  /** Heading level, when the element is a heading. */
501
510
  function levelOf(el) {
502
511
  const explicit = Number(el.getAttribute("aria-level"));
@@ -583,7 +592,7 @@ var SnapshotBuilder = class {
583
592
  const root = this.root();
584
593
  const view = root.ownerDocument.defaultView;
585
594
  let offset = options.cursor ?? 0;
586
- if (options.cursor === void 0) {
595
+ if (offset === 0) {
587
596
  this.refs.begin();
588
597
  const all = collect(root);
589
598
  this.candidates = [...all.filter((c) => inViewport(c.element, view)), ...all.filter((c) => !inViewport(c.element, view))];
@@ -595,6 +604,7 @@ var SnapshotBuilder = class {
595
604
  if (isProtected(candidate.element)) states.push("protected");
596
605
  const value = valueOf(candidate.element, candidate.role);
597
606
  const level = candidate.role === "heading" ? levelOf(candidate.element) : void 0;
607
+ const options = optionsOf(candidate.element);
598
608
  return {
599
609
  ref: this.refs.issue(candidate.element, candidate.role, candidate.name),
600
610
  role: candidate.role,
@@ -602,7 +612,8 @@ var SnapshotBuilder = class {
602
612
  depth: candidate.depth,
603
613
  states,
604
614
  ...value === void 0 ? {} : { value },
605
- ...level === void 0 ? {} : { level }
615
+ ...level === void 0 ? {} : { level },
616
+ ...options === void 0 ? {} : { options }
606
617
  };
607
618
  });
608
619
  const end = offset + page.length;
@@ -645,6 +656,7 @@ function prepare(env, ref) {
645
656
  if (isProtected(element)) throw new UiControlError("protected", `"${resolved.name}" belongs to approval, permissions or Agent Control itself; only the user operates it`);
646
657
  if (effectivelyHidden(element)) throw new UiControlError("not-actionable", `"${resolved.name}" is not visible`);
647
658
  if (element.disabled === true || element.getAttribute("aria-disabled") === "true") throw new UiControlError("not-actionable", `"${resolved.name}" is disabled`);
659
+ env.touched?.(element);
648
660
  return resolved;
649
661
  }
650
662
  function mouse(element, type) {
@@ -786,6 +798,7 @@ function scroll(env, request) {
786
798
  const dy = request.direction === "up" ? -amount : request.direction === "down" ? amount : 0;
787
799
  if (request.ref !== void 0) {
788
800
  const { element, role, name } = env.refs.resolve(request.ref);
801
+ env.touched?.(element);
789
802
  element.scrollBy?.({
790
803
  left: dx,
791
804
  top: dy
@@ -914,6 +927,8 @@ var RefTable = class {
914
927
  /** After the user touches the page, agent actions wait this long, so the user always wins. */
915
928
  const YIELD_MS = 400;
916
929
  const MAX_YIELD_MS = 2e3;
930
+ /** How long after an act a navigation it started is still noticed (a link's navigation starts a task or two after the click). */
931
+ const LEAVE_WATCH_MS = 50;
917
932
  function realSleep(ms, signal) {
918
933
  return new Promise((resolve, reject) => {
919
934
  if (signal.aborted) {
@@ -955,7 +970,8 @@ var UiDriver = class {
955
970
  this.env = {
956
971
  refs: this.refs,
957
972
  document: options.document,
958
- sleep: options.sleep ?? realSleep
973
+ sleep: options.sleep ?? realSleep,
974
+ ...options.touched === void 0 ? {} : { touched: options.touched }
959
975
  };
960
976
  }
961
977
  /** The current activity; the same object until something changes, as a store snapshot must be. */
@@ -1034,10 +1050,10 @@ var UiDriver = class {
1034
1050
  ...request.cursor === void 0 ? {} : { cursor: request.cursor },
1035
1051
  ...request.maxNodes === void 0 ? {} : { maxNodes: request.maxNodes }
1036
1052
  });
1037
- case "click": return click(this.env, request);
1038
- case "type": return typeText(this.env, request);
1053
+ case "click": return await this.watchLeaving(() => click(this.env, request), signal);
1054
+ case "type": return await this.watchLeaving(() => typeText(this.env, request), signal);
1039
1055
  case "select": return selectOption(this.env, request);
1040
- case "press": return pressKey(this.env, request);
1056
+ case "press": return await this.watchLeaving(() => pressKey(this.env, request), signal);
1041
1057
  case "scroll": return scroll(this.env, request);
1042
1058
  case "wait": return wait(this.env, request, signal);
1043
1059
  case "reload": return this.leave({ reload: true }, "reloading");
@@ -1045,6 +1061,29 @@ var UiDriver = class {
1045
1061
  }
1046
1062
  }
1047
1063
  /**
1064
+ * Run an act that may start a navigation (a click on a link, Enter in a form) and say so: the browser asks `beforeunload` when a navigation starts, during the act or a
1065
+ * moment after it. The answer is sent before the page goes; the Host then waits for the next page instead of reading this one again.
1066
+ */
1067
+ async watchLeaving(act, signal) {
1068
+ const view = this.options.document().defaultView;
1069
+ let leaving = false;
1070
+ const onLeave = () => {
1071
+ leaving = true;
1072
+ };
1073
+ view?.addEventListener("beforeunload", onLeave);
1074
+ try {
1075
+ const result = act();
1076
+ if (!leaving) await this.env.sleep(LEAVE_WATCH_MS, signal);
1077
+ return leaving ? {
1078
+ ...result,
1079
+ leaving: true,
1080
+ detail: `${result.detail === void 0 ? "" : `${result.detail}; `}the page is going to another address`
1081
+ } : result;
1082
+ } finally {
1083
+ view?.removeEventListener("beforeunload", onLeave);
1084
+ }
1085
+ }
1086
+ /**
1048
1087
  * The page is about to be replaced, so the answer is given first and the move happens a moment later (the answer is a POST that the page's own unload would cut off).
1049
1088
  * The Host does not wait for this answer to be the last word: it waits for the page to come back.
1050
1089
  */
@@ -1,2 +1,2 @@
1
- import { C as parseUiRequest, S as parseChannelToPage, _ as UiRequest, a as DEFAULT_SNAPSHOT_NODES, b as WINDOW_ID, c as MAX_WAIT_MS, d as UI_OPS, f as UiActionResult, g as UiOp, h as UiNode, i as ChannelToPage, l as MUTATING_OPS, m as UiErrorCode, n as AuditEntry, o as MAX_SNAPSHOT_NODES, p as UiControlError, r as ChannelToHost, s as MAX_TEXT_LENGTH, t as APP_PATH, u as UI_ERROR_CODES, v as UiResult, x as parseChannelToHost, y as UiSnapshot } from "../protocol-a2aqrp2X.js";
1
+ import { C as parseUiRequest, S as parseChannelToPage, _ as UiRequest, a as DEFAULT_SNAPSHOT_NODES, b as WINDOW_ID, c as MAX_WAIT_MS, d as UI_OPS, f as UiActionResult, g as UiOp, h as UiNode, i as ChannelToPage, l as MUTATING_OPS, m as UiErrorCode, n as AuditEntry, o as MAX_SNAPSHOT_NODES, p as UiControlError, r as ChannelToHost, s as MAX_TEXT_LENGTH, t as APP_PATH, u as UI_ERROR_CODES, v as UiResult, x as parseChannelToHost, y as UiSnapshot } from "../protocol-BeIYCgTo.js";
2
2
  export { APP_PATH, AuditEntry, ChannelToHost, ChannelToPage, DEFAULT_SNAPSHOT_NODES, MAX_SNAPSHOT_NODES, MAX_TEXT_LENGTH, MAX_WAIT_MS, MUTATING_OPS, UI_ERROR_CODES, UI_OPS, UiActionResult, UiControlError, UiErrorCode, UiNode, UiOp, UiRequest, UiResult, UiSnapshot, WINDOW_ID, parseChannelToHost, parseChannelToPage, parseUiRequest };
package/lib/drivers.d.ts CHANGED
@@ -26,10 +26,11 @@ type ChatEvent = {
26
26
  interface ChatDriver {
27
27
  readonly kind: 'chat';
28
28
  readonly name: string;
29
- /** Runs one message. Aborting `signal` stops the run. */
29
+ /** Runs one message. Aborting `signal` stops the run. `page` is the door's id of the page whose panel asked, when it sent one: a driver that uses the page drives that one. */
30
30
  start(options: {
31
31
  message: string;
32
32
  signal: AbortSignal;
33
+ page?: string;
33
34
  }): AsyncIterable<ChatEvent>;
34
35
  }
35
36
  /** A live pseudo-terminal session. */
@@ -45,6 +46,14 @@ interface TerminalSession {
45
46
  interface TerminalDriver {
46
47
  readonly kind: 'terminal';
47
48
  readonly name: string;
49
+ /**
50
+ * Where the panel loads the terminal view from: the browser module that draws it, and the URL prefix the view's endpoints live under. The driver
51
+ * plugin serves both, so the view follows wherever the app is mounted (the root of its own server, or the prefix the ACRYL frame serves it at).
52
+ */
53
+ readonly view: {
54
+ readonly module: string;
55
+ readonly prefix: string;
56
+ };
48
57
  start(options: {
49
58
  cwd: string;
50
59
  env: NodeJS.ProcessEnv;
@@ -88,7 +88,17 @@ declare class Router extends Service {
88
88
  * mount's code a request is in. The core's `app.route` runs first so a sub-app's own `onError` still applies to its routes.
89
89
  */
90
90
  private snapshot;
91
- /** Abort `flights`, cut their bodies short and wait for them to settle, at most `disposeGraceMs`. */
91
+ /**
92
+ * The router's own disposal: let the flights already in flight finish within `disposeGraceMs` - a save being written when the app stops must not be cut
93
+ * by the router beneath it - then abort what is left and settle it for a moment, so disposal does not wait a second full grace on code that ignores
94
+ * its abort signal.
95
+ */
96
+ private drain;
97
+ /** The one draining rule: let `flights` finish inside the grace, then abort and settle what is left. */
98
+ private drainFlights;
99
+ /** Resolve true when every flight settled inside `ms`, false at the deadline. */
100
+ private settledWithin;
101
+ /** Abort `flights`, cut their bodies short and wait for them to settle, at most `settleMs`. */
92
102
  private end;
93
103
  /** Turn errors no mounted app handled into responses. One handler per router; the calling plugin owns it. */
94
104
  onError(handler: ErrorHandler): () => void;
@@ -1,5 +1,5 @@
1
1
  import { t as Acryl } from "./app-D8riatvv.js";
2
- import { a as Owner, c as UpgradeHubOptions, d as upgradeWebSocket, f as WebSocketData, i as testWebSocket, l as UpgradeRouter, m as WebSocketServerLike, n as TestWebSocketOptions, o as UpgradeEmitter, p as WebSocketLike, r as TestWebSocketResult, s as UpgradeHub, t as TestSocket, u as RequestError } from "./test-websocket-DyeS50Tr.js";
2
+ import { a as Owner, c as UpgradeHubOptions, d as upgradeWebSocket, f as WebSocketData, i as testWebSocket, l as UpgradeRouter, m as WebSocketServerLike, n as TestWebSocketOptions, o as UpgradeEmitter, p as WebSocketLike, r as TestWebSocketResult, s as UpgradeHub, t as TestSocket, u as RequestError } from "./test-websocket-GUTAkFQT.js";
3
3
  import { n as serveStatic, t as ServeStaticOptions } from "./serve-static-B7sqnftW.js";
4
4
  import { t as getConnInfo } from "./conninfo-BCROmdnf.js";
5
5
  import { AddressInfo } from "node:net";
@@ -90,6 +90,12 @@ declare const getRequestListener: (fetchCallback: FetchCallback, options?: {
90
90
  interface ClientModulesOptions {
91
91
  /** The folder of client modules, absolute or relative to the working directory. */
92
92
  readonly root: string;
93
+ /**
94
+ * The URL prefix the app is mounted at, when the caller knows it. A host can compose app plugins under its own router, and under a
95
+ * Host whose router is a different copy of this package the request's match result is not visible here, so `basePath(c)` throws; the
96
+ * explicit prefix is then the only reliable way to know where the mount starts. Omitted, the router's `basePath(c)` is used.
97
+ */
98
+ readonly mount?: string | undefined;
93
99
  /** The folder {@link buildClientModules} wrote; with `NODE_ENV=production` it is served instead of compiling, which needs no `typescript`. */
94
100
  readonly compiled?: string | undefined;
95
101
  /** At most this many bytes of compiled modules are kept in memory (least recently served first out); 16 MiB when not given. */
@@ -1,5 +1,5 @@
1
- import { a as serve, i as createAdaptorServer, l as RequestError, n as UpgradeEmitter, o as upgradeWebSocket, r as UpgradeHub, s as getRequestListener, t as testWebSocket } from "./test-websocket-BLeEvybj.js";
1
+ import { a as serve, d as RequestError, i as createAdaptorServer, n as UpgradeEmitter, o as upgradeWebSocket, r as UpgradeHub, s as getRequestListener, t as testWebSocket } from "./test-websocket-CQOSvBth.js";
2
2
  import { t as serveStatic } from "./serve-static-C1Nkj_xB.js";
3
3
  import { t as getConnInfo } from "./conninfo-CWJwieh4.js";
4
- import { n as clientModules, t as buildClientModules } from "./client-modules-drj4mjhj.js";
4
+ import { n as clientModules, t as buildClientModules } from "./client-modules-Bkdr83jO.js";
5
5
  export { RequestError, UpgradeEmitter, UpgradeHub, buildClientModules, clientModules, createAdaptorServer, getConnInfo, getRequestListener, serve, serveStatic, testWebSocket, upgradeWebSocket };
@@ -36,6 +36,8 @@ interface UiNode {
36
36
  readonly value?: string;
37
37
  /** Heading level, when it is a heading. */
38
38
  readonly level?: number;
39
+ /** The choices of a select, as `act select` takes them, when it is not sensitive. */
40
+ readonly options?: readonly string[];
39
41
  }
40
42
  interface UiSnapshot {
41
43
  readonly generation: number;
@@ -93,6 +95,8 @@ interface UiActionResult {
93
95
  readonly name: string;
94
96
  };
95
97
  readonly detail?: string;
98
+ /** The act started a navigation (a link, a form that submits): this page is going away, and the Host waits for the next one. */
99
+ readonly leaving?: true;
96
100
  }
97
101
  type UiResult = UiSnapshot | UiActionResult;
98
102
  /**