sygnal 7.0.0 → 7.2.0-preview.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 (343) hide show
  1. package/CHANGELOG.md +632 -0
  2. package/dist/agent-ui.cjs.js +592 -0
  3. package/dist/agent-ui.cjs.js.map +1 -0
  4. package/dist/agent-ui.esm.js +582 -0
  5. package/dist/agent-ui.esm.js.map +1 -0
  6. package/dist/api.cjs.js +63 -0
  7. package/dist/api.cjs.js.map +1 -0
  8. package/dist/api.esm.js +2 -0
  9. package/dist/api.esm.js.map +1 -0
  10. package/dist/astro/index.cjs.js +968 -4
  11. package/dist/astro/index.cjs.js.map +1 -1
  12. package/dist/astro/index.mjs +968 -4
  13. package/dist/astro/index.mjs.map +1 -1
  14. package/dist/astro/server.cjs.js +73 -45
  15. package/dist/astro/server.cjs.js.map +1 -1
  16. package/dist/astro/server.mjs +73 -45
  17. package/dist/astro/server.mjs.map +1 -1
  18. package/dist/canvas.cjs.js +2919 -0
  19. package/dist/canvas.cjs.js.map +1 -0
  20. package/dist/canvas.esm.js +2866 -0
  21. package/dist/canvas.esm.js.map +1 -0
  22. package/dist/devtools.cjs.js +38 -6
  23. package/dist/devtools.cjs.js.map +1 -1
  24. package/dist/devtools.esm.js +38 -6
  25. package/dist/devtools.esm.js.map +1 -1
  26. package/dist/diagnostics.cjs.js +224 -20
  27. package/dist/diagnostics.cjs.js.map +1 -1
  28. package/dist/diagnostics.esm.js +224 -20
  29. package/dist/diagnostics.esm.js.map +1 -1
  30. package/dist/guide/accessibility.md +1 -1
  31. package/dist/guide/ai-compose.md +150 -0
  32. package/dist/guide/behaviors.md +2 -1
  33. package/dist/guide/collaborative-state.md +265 -0
  34. package/dist/guide/element-commands.md +36 -2
  35. package/dist/guide/forms-reference.md +5 -5
  36. package/dist/guide/forms.md +3 -2
  37. package/dist/guide/http.md +28 -1
  38. package/dist/guide/islands.md +240 -0
  39. package/dist/guide/recipes/code-editor.md +1 -1
  40. package/dist/guide/renderers/agent-ui.md +138 -0
  41. package/dist/guide/renderers/custom-targets.md +126 -0
  42. package/dist/guide/renderers/overview.md +120 -0
  43. package/dist/guide/renderers/react-native.md +128 -0
  44. package/dist/guide/renderers/terminal.md +306 -0
  45. package/dist/guide/renderers/three.md +332 -0
  46. package/dist/guide/renderers/write-once.md +137 -0
  47. package/dist/guide/resources.md +2 -0
  48. package/dist/guide/sandboxed-components.md +223 -0
  49. package/dist/guide/timers.md +32 -5
  50. package/dist/guide/typed-apis.md +442 -0
  51. package/dist/guide/ui/accordion.md +11 -0
  52. package/dist/guide/ui/combobox.md +112 -8
  53. package/dist/guide/ui/dialog.md +96 -20
  54. package/dist/guide/ui/menu.md +57 -5
  55. package/dist/guide/ui/overview.md +8 -4
  56. package/dist/guide/ui/popover.md +69 -9
  57. package/dist/guide/ui/select.md +54 -4
  58. package/dist/guide/ui/tabs.md +1 -1
  59. package/dist/guide/ui/tooltip.md +7 -5
  60. package/dist/guide/virtual-collections.md +3 -0
  61. package/dist/guide/widgets.md +90 -1
  62. package/dist/host.cjs.js +2181 -0
  63. package/dist/host.cjs.js.map +1 -0
  64. package/dist/host.esm.js +2146 -0
  65. package/dist/host.esm.js.map +1 -0
  66. package/dist/index.cjs.js +3948 -586
  67. package/dist/index.cjs.js.map +1 -1
  68. package/dist/index.d.ts +584 -44
  69. package/dist/index.esm.js +3918 -586
  70. package/dist/index.esm.js.map +1 -1
  71. package/dist/primitives.cjs.js +143 -0
  72. package/dist/primitives.cjs.js.map +1 -0
  73. package/dist/primitives.esm.js +133 -0
  74. package/dist/primitives.esm.js.map +1 -0
  75. package/dist/react-native.cjs.js +248 -0
  76. package/dist/react-native.cjs.js.map +1 -0
  77. package/dist/react-native.esm.js +244 -0
  78. package/dist/react-native.esm.js.map +1 -0
  79. package/dist/replay.cjs.js +39 -0
  80. package/dist/replay.cjs.js.map +1 -0
  81. package/dist/replay.esm.js +2 -0
  82. package/dist/replay.esm.js.map +1 -0
  83. package/dist/sandbox-build.cjs.js +140 -0
  84. package/dist/sandbox-build.cjs.js.map +1 -0
  85. package/dist/sandbox-build.esm.js +137 -0
  86. package/dist/sandbox-build.esm.js.map +1 -0
  87. package/dist/sandbox-kernel.esm.js +212 -0
  88. package/dist/sandbox-kernel.esm.js.map +1 -0
  89. package/dist/sandbox-worker.cjs.js +857 -0
  90. package/dist/sandbox-worker.cjs.js.map +1 -0
  91. package/dist/sandbox-worker.esm.js +854 -0
  92. package/dist/sandbox-worker.esm.js.map +1 -0
  93. package/dist/sandbox.cjs.js +1425 -0
  94. package/dist/sandbox.cjs.js.map +1 -0
  95. package/dist/sandbox.esm.js +1406 -0
  96. package/dist/sandbox.esm.js.map +1 -0
  97. package/dist/sygnal.min.js +1 -1
  98. package/dist/sygnal.min.js.map +1 -1
  99. package/dist/terminal.cjs.js +1314 -0
  100. package/dist/terminal.cjs.js.map +1 -0
  101. package/dist/terminal.esm.js +1304 -0
  102. package/dist/terminal.esm.js.map +1 -0
  103. package/dist/three.cjs.js +1111 -0
  104. package/dist/three.cjs.js.map +1 -0
  105. package/dist/three.esm.js +1079 -0
  106. package/dist/three.esm.js.map +1 -0
  107. package/dist/ui-combobox.cjs.js +256 -18
  108. package/dist/ui-combobox.cjs.js.map +1 -1
  109. package/dist/ui-combobox.esm.js +256 -18
  110. package/dist/ui-combobox.esm.js.map +1 -1
  111. package/dist/ui-menu.cjs.js +150 -16
  112. package/dist/ui-menu.cjs.js.map +1 -1
  113. package/dist/ui-menu.esm.js +151 -17
  114. package/dist/ui-menu.esm.js.map +1 -1
  115. package/dist/ui-select.cjs.js +95 -9
  116. package/dist/ui-select.cjs.js.map +1 -1
  117. package/dist/ui-select.esm.js +95 -9
  118. package/dist/ui-select.esm.js.map +1 -1
  119. package/dist/ui.cjs.js +679 -173
  120. package/dist/ui.cjs.js.map +1 -1
  121. package/dist/ui.esm.js +679 -175
  122. package/dist/ui.esm.js.map +1 -1
  123. package/dist/vike/config/package.json +1 -1
  124. package/dist/vite/plugin.cjs.js +968 -4
  125. package/dist/vite/plugin.cjs.js.map +1 -1
  126. package/dist/vite/plugin.mjs +968 -4
  127. package/dist/vite/plugin.mjs.map +1 -1
  128. package/dist/worker-page.cjs.js +669 -0
  129. package/dist/worker-page.cjs.js.map +1 -0
  130. package/dist/worker-page.esm.js +665 -0
  131. package/dist/worker-page.esm.js.map +1 -0
  132. package/dist/worker.cjs.js +377 -0
  133. package/dist/worker.cjs.js.map +1 -0
  134. package/dist/worker.esm.js +374 -0
  135. package/dist/worker.esm.js.map +1 -0
  136. package/dist/yjs-testing.cjs.js +98 -0
  137. package/dist/yjs-testing.cjs.js.map +1 -0
  138. package/dist/yjs-testing.esm.js +77 -0
  139. package/dist/yjs-testing.esm.js.map +1 -0
  140. package/dist/yjs.cjs.js +923 -0
  141. package/dist/yjs.cjs.js.map +1 -0
  142. package/dist/yjs.esm.js +900 -0
  143. package/dist/yjs.esm.js.map +1 -0
  144. package/dist/zag.cjs.js +11 -1
  145. package/dist/zag.cjs.js.map +1 -1
  146. package/dist/zag.esm.js +11 -1
  147. package/dist/zag.esm.js.map +1 -1
  148. package/llms/ai.md +65 -0
  149. package/llms/collaboration.md +5 -0
  150. package/llms/graphics.md +10 -0
  151. package/llms/renderers.md +7 -0
  152. package/llms/replay.md +5 -0
  153. package/llms/sandbox.md +5 -0
  154. package/llms/typed-apis.md +5 -0
  155. package/llms/ui.md +10 -0
  156. package/llms-full.txt +444 -0
  157. package/llms.txt +37 -35
  158. package/package.json +139 -3
  159. package/src/agent-ui/mcp.ts +152 -0
  160. package/src/agent-ui/snapshot.ts +212 -0
  161. package/src/agent-ui.d.ts +106 -0
  162. package/src/agent-ui.ts +249 -0
  163. package/src/ai.d.ts +95 -2
  164. package/src/api.d.ts +303 -0
  165. package/src/api.ts +9 -0
  166. package/src/canvas/Canvas.ts +585 -0
  167. package/src/canvas/backend.ts +627 -0
  168. package/src/canvas/geometry.ts +187 -0
  169. package/src/canvas/gesture.ts +23 -0
  170. package/src/canvas/image.ts +197 -0
  171. package/src/canvas/kit.ts +130 -0
  172. package/src/canvas/paint.ts +260 -0
  173. package/src/canvas/pointer.ts +25 -0
  174. package/src/canvas/snap.ts +68 -0
  175. package/src/canvas/svg.ts +99 -0
  176. package/src/canvas.d.ts +373 -0
  177. package/src/canvas.ts +46 -0
  178. package/src/core/actions.ts +14 -3
  179. package/src/core/cell.ts +12 -4
  180. package/src/core/debug.ts +8 -3
  181. package/src/core/hooks.ts +16 -2
  182. package/src/core/hosts/collection.ts +1 -1
  183. package/src/core/hosts/switchable.ts +26 -8
  184. package/src/core/instance.ts +98 -45
  185. package/src/core/markers/portal.ts +4 -0
  186. package/src/core/registry.ts +4 -0
  187. package/src/core/runtime.ts +34 -25
  188. package/src/core/statics.ts +5 -5
  189. package/src/cycle/dom/EventDelegator.ts +37 -33
  190. package/src/cycle/dom/MainDOMSource.ts +16 -14
  191. package/src/cycle/dom/enrichEventStream.ts +13 -9
  192. package/src/cycle/dom/fromEvent.ts +6 -10
  193. package/src/cycle/dom/index.ts +11 -7
  194. package/src/cycle/dom/isolate.ts +13 -6
  195. package/src/cycle/dom/makeDOMDriver.ts +6 -2
  196. package/src/cycle/dom/utils.ts +9 -2
  197. package/src/cycle/run/adapt.ts +4 -12
  198. package/src/devtools.d.ts +9 -0
  199. package/src/devtools.ts +2 -1
  200. package/src/extra/ai/compose/catalog.ts +2 -1
  201. package/src/extra/ai/compose/decisions.ts +1 -2
  202. package/src/extra/ai/compose/generated.ts +234 -0
  203. package/src/extra/ai/compose/index.ts +6 -2
  204. package/src/extra/ai/compose/layout.ts +3 -1
  205. package/src/extra/ai/compose/llm.ts +7 -3
  206. package/src/extra/ai/compose/types.ts +2 -2
  207. package/src/extra/ai/transports/shared.ts +3 -1
  208. package/src/extra/api/contract.ts +485 -0
  209. package/src/extra/api/index.ts +26 -0
  210. package/src/extra/behaviors.ts +3 -3
  211. package/src/extra/controlled.ts +44 -0
  212. package/src/extra/copyAsTest.ts +22 -0
  213. package/src/extra/devtools.ts +5 -3
  214. package/src/extra/devtoolsActions.ts +8 -0
  215. package/src/extra/devtoolsNext.ts +1 -1
  216. package/src/extra/diagnostics/checks/actionLog.ts +8 -3
  217. package/src/extra/diagnostics/checks/behaviors.ts +25 -0
  218. package/src/extra/diagnostics/checks/elementCommands.ts +17 -5
  219. package/src/extra/diagnostics/checks/index.ts +1 -1
  220. package/src/extra/diagnostics/checks/next.ts +78 -4
  221. package/src/extra/diagnostics/checks/props.ts +6 -1
  222. package/src/extra/diagnostics/checks/replies.ts +11 -7
  223. package/src/extra/diagnostics/checks/timers.ts +8 -2
  224. package/src/extra/diagnostics/checks/virtual.ts +11 -0
  225. package/src/extra/diagnostics/codes.ts +51 -1
  226. package/src/extra/elementCommands.ts +20 -4
  227. package/src/extra/fetchDriver.ts +4 -1
  228. package/src/extra/fetchEventSource.ts +6 -2
  229. package/src/extra/field.ts +29 -0
  230. package/src/extra/form.ts +42 -16
  231. package/src/extra/frame.ts +54 -0
  232. package/src/extra/keymap.ts +41 -0
  233. package/src/extra/pager.ts +2 -0
  234. package/src/extra/persist.ts +2 -1
  235. package/src/extra/pointer.ts +262 -0
  236. package/src/extra/processForm.ts +34 -1
  237. package/src/extra/replay/address.ts +49 -0
  238. package/src/extra/replay/entropy.ts +99 -0
  239. package/src/extra/replay/format.ts +263 -0
  240. package/src/extra/replay/index.ts +8 -0
  241. package/src/extra/replay/inert.ts +22 -0
  242. package/src/extra/replay/recorder.ts +247 -0
  243. package/src/extra/replay/replayer.ts +234 -0
  244. package/src/extra/replay/report.ts +126 -0
  245. package/src/extra/replay/schema.ts +52 -0
  246. package/src/extra/replay/version.ts +3 -0
  247. package/src/extra/router.ts +7 -3
  248. package/src/extra/selection.ts +2 -0
  249. package/src/extra/socketDriver.ts +51 -6
  250. package/src/extra/ssr.ts +128 -42
  251. package/src/extra/testing.ts +295 -39
  252. package/src/extra/timers.ts +96 -7
  253. package/src/extra/undo.ts +2 -0
  254. package/src/extra/virtual.ts +25 -2
  255. package/src/extra/widget.ts +106 -17
  256. package/src/host/actions.ts +383 -0
  257. package/src/host/delegation.ts +101 -0
  258. package/src/host/dom.ts +553 -0
  259. package/src/host/driver.ts +185 -0
  260. package/src/host/selector.ts +119 -0
  261. package/src/host/style.ts +51 -0
  262. package/src/host.d.ts +298 -0
  263. package/src/host.ts +46 -0
  264. package/src/index.d.ts +302 -38
  265. package/src/index.ts +15 -3
  266. package/src/pragma/blocks.ts +211 -0
  267. package/src/pragma/index.ts +44 -10
  268. package/src/primitives.d.ts +54 -0
  269. package/src/primitives.ts +136 -0
  270. package/src/react-native.d.ts +57 -0
  271. package/src/react-native.ts +285 -0
  272. package/src/react-peers.d.ts +21 -3
  273. package/src/replay.d.ts +185 -0
  274. package/src/replay.ts +6 -0
  275. package/src/sandbox/build.ts +119 -0
  276. package/src/sandbox/create.ts +187 -0
  277. package/src/sandbox/engine.ts +267 -0
  278. package/src/sandbox/generated.ts +195 -0
  279. package/src/sandbox/kernel.ts +180 -0
  280. package/src/sandbox/mirror.ts +269 -0
  281. package/src/sandbox/protocol.ts +128 -0
  282. package/src/sandbox/quickjs.ts +74 -0
  283. package/src/sandbox/ssr.ts +41 -0
  284. package/src/sandbox/widget.ts +60 -0
  285. package/src/sandbox/worker-entry.ts +59 -0
  286. package/src/sandbox/worker.ts +137 -0
  287. package/src/sandbox-build.d.ts +12 -0
  288. package/src/sandbox-build.ts +6 -0
  289. package/src/sandbox.d.ts +299 -0
  290. package/src/sandbox.ts +25 -0
  291. package/src/shared.ts +12 -0
  292. package/src/terminal/paint.ts +712 -0
  293. package/src/terminal.d.ts +167 -0
  294. package/src/terminal.ts +530 -0
  295. package/src/three/assets.ts +170 -0
  296. package/src/three/host.ts +36 -0
  297. package/src/three/scene.ts +217 -0
  298. package/src/three/target.ts +252 -0
  299. package/src/three.d.ts +221 -0
  300. package/src/three.ts +30 -0
  301. package/src/ui/accordion.ts +21 -7
  302. package/src/ui/dialog.ts +192 -90
  303. package/src/ui/disclosure.ts +2 -0
  304. package/src/ui/dom.ts +30 -0
  305. package/src/ui/layers.ts +132 -0
  306. package/src/ui/popover.ts +120 -21
  307. package/src/ui/shared.ts +98 -12
  308. package/src/ui/stack.ts +18 -0
  309. package/src/ui/tabs.ts +2 -0
  310. package/src/ui/toaster.ts +16 -15
  311. package/src/ui/tooltip.ts +24 -8
  312. package/src/ui/tooltips.ts +26 -14
  313. package/src/ui/zag/combobox.ts +180 -13
  314. package/src/ui/zag/menu.ts +97 -17
  315. package/src/ui/zag/select.ts +15 -4
  316. package/src/ui/zag/shared.ts +60 -8
  317. package/src/ui-combobox.d.ts +16 -1
  318. package/src/ui-menu.d.ts +22 -7
  319. package/src/ui-zag-types.d.ts +13 -5
  320. package/src/ui.d.ts +62 -7
  321. package/src/ui.ts +2 -2
  322. package/src/vite/api.ts +186 -0
  323. package/src/vite/compile.ts +280 -0
  324. package/src/vite/plugin.d.ts +34 -0
  325. package/src/vite/plugin.ts +78 -4
  326. package/src/vite/sandbox.ts +16 -0
  327. package/src/worker/page.ts +396 -0
  328. package/src/worker/protocol.ts +48 -0
  329. package/src/worker/target.ts +282 -0
  330. package/src/worker-page.d.ts +42 -0
  331. package/src/worker-page.ts +12 -0
  332. package/src/worker.d.ts +65 -0
  333. package/src/worker.ts +16 -0
  334. package/src/yjs/driver.ts +253 -0
  335. package/src/yjs/mapping.ts +389 -0
  336. package/src/yjs/shared.ts +138 -0
  337. package/src/yjs/testing.ts +91 -0
  338. package/src/yjs-testing.d.ts +34 -0
  339. package/src/yjs-testing.ts +7 -0
  340. package/src/yjs.d.ts +109 -0
  341. package/src/yjs.ts +18 -0
  342. package/src/zag.d.ts +2 -0
  343. package/src/zag.ts +8 -1
@@ -0,0 +1,152 @@
1
+ /**
2
+ * sygnal/agent-ui: a minimal Model Context Protocol server (tools only), as sygnal-check's
3
+ * (sygnal-check/src/mcp.js) and sygnal/vite's dev endpoint (src/vite/mcp.ts): hand-rolled, no
4
+ * dependency, both protocol eras answered per request.
5
+ *
6
+ * - legacy, the `initialize` revisions (2024-11-05 … 2025-11-25): initialize, ping, tools/list,
7
+ * tools/call; JSON-RPC batches answered. tools.listChanged is true: the tool set can change
8
+ * when extra tools (sygnal/ai agentTools) do, and notifications/tools/list_changed is sent.
9
+ * - modern, the stateless 2026-07-28 revision (`_meta['io.modelcontextprotocol/protocolVersion']`
10
+ * with client capabilities): server/discover, tools/list, tools/call; results carry
11
+ * `resultType: 'complete'` and the server info in `_meta`.
12
+ * Transport: newline-delimited JSON over any stream pair with `on('data')` and `write()` (Node's
13
+ * stdin / stdout); messages are handled one at a time, in order.
14
+ */
15
+ export const MODERN_VERSIONS = ['2026-07-28']
16
+ export const LEGACY_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05']
17
+ const SUPPORTED = [...MODERN_VERSIONS, ...LEGACY_VERSIONS]
18
+ const META = 'io.modelcontextprotocol/'
19
+
20
+ export interface McpTool {
21
+ name: string
22
+ description: string
23
+ inputSchema: Record<string, any>
24
+ annotations?: Record<string, any>
25
+ }
26
+ export interface McpToolResult {
27
+ /** the text the model reads */
28
+ text: string
29
+ /** structured content (an object) */
30
+ value?: Record<string, any>
31
+ isError?: boolean
32
+ }
33
+ export interface McpServerSpec {
34
+ name: string
35
+ version: string
36
+ instructions?: string
37
+ tools(): McpTool[]
38
+ call(name: string, args: Record<string, any>): Promise<McpToolResult>
39
+ }
40
+
41
+ const isObj = (v: any): v is Record<string, any> => !!v && typeof v == 'object' && !Array.isArray(v)
42
+
43
+ export function createMcpHandler(spec: McpServerSpec) {
44
+ const serverInfo = {name: spec.name, version: spec.version}
45
+ const result = (id: any, value: any) => ({jsonrpc: '2.0', id, result: value})
46
+ const error = (id: any, code: number, message: string, data?: any) => ({jsonrpc: '2.0', id, error: {code, message, ...(data !== undefined ? {data} : {})}})
47
+
48
+ async function route(id: any, method: string, params: Record<string, any>, modern: boolean): Promise<any> {
49
+ if (modern && (method == 'initialize' || method == 'ping')) return error(id, -32601, `Method not found: ${method} is not in 2026-07-28`)
50
+ switch (method) {
51
+ case 'initialize':
52
+ return result(id, {
53
+ protocolVersion: LEGACY_VERSIONS.includes(params.protocolVersion) ? params.protocolVersion : LEGACY_VERSIONS[0],
54
+ capabilities: {tools: {listChanged: true}},
55
+ serverInfo,
56
+ ...(spec.instructions ? {instructions: spec.instructions} : {}),
57
+ })
58
+ case 'server/discover':
59
+ if (!modern) return error(id, -32602, 'Invalid params: server/discover is a 2026-07-28 request')
60
+ return result(id, {supportedVersions: SUPPORTED, capabilities: {tools: {}}, ...(spec.instructions ? {instructions: spec.instructions} : {})})
61
+ case 'ping':
62
+ return result(id, {})
63
+ case 'tools/list':
64
+ return result(id, {tools: spec.tools()})
65
+ case 'tools/call': {
66
+ const name = params.name
67
+ if (!spec.tools().some(t => t.name == name)) return error(id, -32602, `Unknown tool: ${name}`)
68
+ try {
69
+ const r = await spec.call(name, isObj(params.arguments) ? params.arguments : {})
70
+ return result(id, {content: [{type: 'text', text: r.text}], ...(r.value ? {structuredContent: r.value} : {}), isError: !!r.isError})
71
+ } catch (e: any) {
72
+ return result(id, {content: [{type: 'text', text: `${name} failed: ${e?.message || e}`}], isError: true})
73
+ }
74
+ }
75
+ default:
76
+ return error(id, -32601, `Method not found: ${method}`)
77
+ }
78
+ }
79
+
80
+ /** one message → its response (null for a notification); never throws */
81
+ async function handle(msg: any, inBatch = false): Promise<any> {
82
+ try {
83
+ if (!isObj(msg) || msg.jsonrpc !== '2.0' || typeof msg.method != 'string') {
84
+ return isObj(msg) && 'id' in msg && !('result' in msg || 'error' in msg) ? error(msg.id ?? null, -32600, 'Invalid Request') : null
85
+ }
86
+ if (!('id' in msg) || msg.id === null) return null
87
+ const {id, method} = msg
88
+ const params = isObj(msg.params) ? msg.params : {}
89
+ const meta = isObj(params._meta) ? params._meta : {}
90
+ let modern = false
91
+ if (META + 'protocolVersion' in meta) {
92
+ const v = meta[META + 'protocolVersion']
93
+ if (!SUPPORTED.includes(v)) return error(id, -32022, 'Unsupported protocol version', {supported: SUPPORTED, requested: v})
94
+ if (MODERN_VERSIONS.includes(v)) {
95
+ if (!isObj(meta[META + 'clientCapabilities'])) return error(id, -32602, `Invalid params: _meta["${META}clientCapabilities"] is required`)
96
+ if (inBatch) return error(id, -32600, 'Invalid Request: 2026-07-28 messages are sent one per line, not in a batch')
97
+ modern = true
98
+ }
99
+ }
100
+ const out = await route(id, method, params, modern)
101
+ if (modern && isObj(out.result)) out.result = {resultType: 'complete', ...out.result, _meta: {...(out.result._meta || {}), [META + 'serverInfo']: serverInfo}}
102
+ return out
103
+ } catch (e: any) {
104
+ return error(isObj(msg) ? msg.id ?? null : null, -32603, `Internal error: ${e?.message || e}`)
105
+ }
106
+ }
107
+ return {handle}
108
+ }
109
+
110
+ export interface McpStdio {
111
+ /** notifications/tools/list_changed to the client */
112
+ listChanged(): void
113
+ /** stop reading */
114
+ stop(): void
115
+ /** resolves when the input ends */
116
+ done: Promise<void>
117
+ }
118
+
119
+ /** serve over a newline-delimited JSON stream pair (default: process.stdin / process.stdout) */
120
+ export function serveMcpStream(spec: McpServerSpec, input?: any, output?: any): McpStdio {
121
+ const proc: any = (globalThis as any).process
122
+ input ||= proc?.stdin
123
+ output ||= proc?.stdout
124
+ const {handle} = createMcpHandler(spec)
125
+ const send = (m: any) => { if (m && !(Array.isArray(m) && !m.length)) output.write(JSON.stringify(m) + '\n') }
126
+ let buf = '', queue = Promise.resolve(), end!: () => void
127
+ const done = new Promise<void>(r => end = r)
128
+ const line = (l: string) => {
129
+ if (!l.trim()) return
130
+ queue = queue.then(async () => {
131
+ let msg: any
132
+ try { msg = JSON.parse(l) } catch { return send({jsonrpc: '2.0', id: null, error: {code: -32700, message: 'Parse error'}}) }
133
+ if (Array.isArray(msg)) send((await Promise.all(msg.map(m => handle(m, true)))).filter(Boolean))
134
+ else send(await handle(msg))
135
+ })
136
+ }
137
+ const onData = (chunk: any) => {
138
+ buf += String(chunk)
139
+ let i
140
+ while ((i = buf.indexOf('\n')) >= 0) { line(buf.slice(0, i)); buf = buf.slice(i + 1) }
141
+ }
142
+ const onEnd = () => { if (buf) line(buf); buf = ''; queue.then(end) }
143
+ input.setEncoding?.('utf8')
144
+ input.on('data', onData)
145
+ input.on('end', onEnd)
146
+ input.resume?.()
147
+ return {
148
+ listChanged: () => send({jsonrpc: '2.0', method: 'notifications/tools/list_changed'}),
149
+ stop: () => { input.off?.('data', onData); input.off?.('end', onEnd); end() },
150
+ done,
151
+ }
152
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * sygnal/agent-ui: the accessibility snapshot. The host tree as an LLM agent reads a page: roles,
3
+ * accessible names, states and values, with a ref on each element it can act on. The format is
4
+ * the YAML-like aria snapshot browser agents already know (Playwright's, Claude in Chrome's
5
+ * read_page):
6
+ *
7
+ * - heading "todos" [level=1]
8
+ * - textbox "What needs to be done?" [ref=e4]
9
+ * - list:
10
+ * - listitem:
11
+ * - checkbox "Toggle" [checked] [ref=e9]
12
+ * - generic "Write the spike" [ref=e10] [on=dblclick]
13
+ * - button "Delete" [ref=e11]
14
+ * - text: 1 item left
15
+ *
16
+ * What a browser can't give an agent: `on=…` lists the events the app's intents listen for on an
17
+ * element that isn't a native control (Sygnal's intents are selectors, so the renderer knows
18
+ * exactly which elements are interactive).
19
+ */
20
+ import type {HostDocument, HostElement, HostNode, HostStyleRule} from '../host'
21
+ import {computeStyle, isInteractive, isTextField, isCheckable, labelTarget, nodeId, optionValue} from '../host'
22
+
23
+ export interface AgentNode {
24
+ role: string
25
+ name?: string
26
+ /** `e<id>`: the element tools act on */
27
+ ref?: string
28
+ value?: string
29
+ /** checked, disabled, expanded, pressed, selected, focused, required, level=n */
30
+ states?: string[]
31
+ /** events the app listens for on a non-native element */
32
+ on?: string[]
33
+ /** a text node's text */
34
+ text?: string
35
+ children?: AgentNode[]
36
+ }
37
+
38
+ const BLOCK = /^(root|div|section|header|footer|main|nav|article|aside|ul|ol|li|form|fieldset|h[1-6]|p|pre|table|tr|td|th|details|summary|dialog|blockquote|figure|figcaption|dl|dt|dd|hr|br|menu|search|label)$/
39
+ const NATIVE = /^(input|textarea|select|button|a|summary|option)$/
40
+ const LANDMARKS: Record<string, string> = {nav: 'navigation', main: 'main', aside: 'complementary', search: 'search', dialog: 'dialog'}
41
+
42
+ export function roleOf(el: HostElement): string | null {
43
+ const r = el.getAttribute('role')
44
+ if (r) return r.split(/\s+/)[0]
45
+ const t = el.localName
46
+ // a target's own field tags (registerFieldTags)
47
+ if (t != 'input' && t != 'textarea' && isCheckable(el)) return 'checkbox'
48
+ if (t != 'input' && t != 'textarea' && isTextField(el)) return 'textbox'
49
+ if (t == 'a') return el.hasAttribute('href') ? 'link' : null
50
+ if (t == 'button' || t == 'summary') return 'button'
51
+ if (t == 'input') {
52
+ const ty = el.type || 'text'
53
+ if (/^(button|submit|reset|image)$/.test(ty)) return 'button'
54
+ if (ty == 'checkbox' || ty == 'radio') return ty
55
+ if (ty == 'range') return 'slider'
56
+ if (ty == 'number') return 'spinbutton'
57
+ if (ty == 'search') return 'searchbox'
58
+ if (ty == 'hidden' || ty == 'file') return null
59
+ return 'textbox'
60
+ }
61
+ if (t == 'textarea') return 'textbox'
62
+ if (t == 'select') return el.multiple ? 'listbox' : 'combobox'
63
+ if (t == 'option') return 'option'
64
+ if (/^h[1-6]$/.test(t)) return 'heading'
65
+ if (t == 'ul' || t == 'ol' || t == 'menu') return 'list'
66
+ if (t == 'li') return 'listitem'
67
+ if (t == 'img') return el.getAttribute('alt') === '' ? null : 'img'
68
+ if (t == 'table') return 'table'
69
+ if (t == 'tr') return 'row'
70
+ if (t == 'td') return 'cell'
71
+ if (t == 'th') return 'columnheader'
72
+ if (t == 'progress') return 'progressbar'
73
+ if (t == 'fieldset' || t == 'details') return 'group'
74
+ if (t == 'form') return el.getAttribute('aria-label') || el.getAttribute('name') ? 'form' : null
75
+ if (t == 'section') return el.getAttribute('aria-label') ? 'region' : null
76
+ return LANDMARKS[t] || null
77
+ }
78
+
79
+ const squash = (s: string) => s.replace(/\s+/g, ' ').trim()
80
+ /** visible text, without hidden subtrees */
81
+ function textOf(n: HostNode, rules: readonly HostStyleRule[], skip?: HostElement): string {
82
+ if (n.nodeType === 3) return (n as any).data
83
+ const el = n as HostElement
84
+ if (el === skip || computeStyle(el, rules).display == 'none' || el.getAttribute('aria-hidden') == 'true') return ''
85
+ if (el.localName == 'input' || el.localName == 'select' || el.localName == 'textarea') return ''
86
+ return el.childNodes.map(c => textOf(c, rules, skip)).join(BLOCK.test(el.localName) ? ' ' : '')
87
+ }
88
+
89
+ /** the accessible name: aria-label, aria-labelledby, a label, contents, alt / value / placeholder / title */
90
+ export function nameOf(el: HostElement, rules: readonly HostStyleRule[] = []): string {
91
+ const a = el.getAttribute('aria-label')
92
+ if (a) return squash(a)
93
+ const by = el.getAttribute('aria-labelledby')
94
+ if (by) return squash(by.split(/\s+/).map(id => { const t = el.ownerDocument.getElementById(id); return t ? textOf(t, rules) : '' }).join(' '))
95
+ const t = el.localName
96
+ if (t == 'input' || t == 'textarea' || t == 'select') {
97
+ const id = el.id
98
+ const lab = (id && el.ownerDocument.querySelector(`label[for="${id}"]`)) || el.closest('label')
99
+ // (a label names its control even when it's visually hidden, as the browser's accessible name)
100
+ const l = lab ? squash(textOf(lab, [], el)) : ''
101
+ if (l) return l
102
+ if (t == 'input' && /^(button|submit|reset)$/.test(el.type)) return el.getAttribute('value') || (el.type == 'reset' ? 'Reset' : 'Submit')
103
+ return squash(el.placeholder || el.title || '')
104
+ }
105
+ if (t == 'img') return squash(el.getAttribute('alt') || el.title || '')
106
+ return squash(textOf(el, rules)) || squash(el.title || '')
107
+ }
108
+
109
+ const CONTENT_NAMED = /^(button|link|heading|checkbox|radio|switch|textbox|searchbox|combobox|listbox|slider|spinbutton|option|img|columnheader|cell|tab|menuitem|treeitem|tooltip|progressbar)$/
110
+ const labelOnly = (el: HostElement, rules: readonly HostStyleRule[]) => {
111
+ const a = el.getAttribute('aria-label')
112
+ if (a) return squash(a)
113
+ const by = el.getAttribute('aria-labelledby')
114
+ if (by) return squash(by.split(/\s+/).map(id => { const t = el.ownerDocument.getElementById(id); return t ? textOf(t, rules) : '' }).join(' '))
115
+ return squash(el.title || '')
116
+ }
117
+
118
+ export interface SnapshotOptions {
119
+ rules?: readonly HostStyleRule[]
120
+ }
121
+
122
+ /** the host tree as agent nodes (generic containers flattened, their text merged into text nodes) */
123
+ export function snapshotTree(doc: HostDocument, options: SnapshotOptions = {}): AgentNode[] {
124
+ const rules = options.rules || []
125
+ // labels that name a control are read as that control's name, not again as text
126
+ const naming = new Set<HostElement>()
127
+ for (const l of doc.body.querySelectorAll('label')) {
128
+ const t = labelTarget(l)
129
+ if (t && t.localName != 'button') naming.add(l)
130
+ }
131
+ const kids = (el: HostElement): AgentNode[] => {
132
+ const out: AgentNode[] = []
133
+ let text = ''
134
+ const flushText = () => { const t = squash(text); if (t) out.push({role: 'text', text: t}); text = '' }
135
+ // a flex container's children are separate items (a column's lines, a row's gap): their text doesn't run together
136
+ const visit = (n: HostNode, inFlex = false) => {
137
+ if (n.nodeType === 3) { text += (n as any).data; return }
138
+ const c = n as HostElement
139
+ const st = computeStyle(c, rules)
140
+ if (st.display == 'none' || c.getAttribute('aria-hidden') == 'true') return
141
+ const node = nodeFor(c)
142
+ if (node) { flushText(); out.push(node); return }
143
+ if (naming.has(c)) {
144
+ // the label's own text names its control; any other content stays
145
+ for (const x of c.childNodes) if (x.nodeType === 1) visit(x)
146
+ return
147
+ }
148
+ const block = inFlex || BLOCK.test(c.localName)
149
+ const flex = /flex$/.test(st.display || '')
150
+ if (block) flushText()
151
+ for (const x of c.childNodes) visit(x, flex)
152
+ if (block) flushText()
153
+ }
154
+ for (const c of el.childNodes) visit(c, /flex$/.test(computeStyle(el, rules).display || ''))
155
+ flushText()
156
+ return out
157
+ }
158
+ const nodeFor = (el: HostElement): AgentNode | null => {
159
+ const role = roleOf(el)
160
+ const interactive = isInteractive(el, rules)
161
+ const listened = NATIVE.test(el.localName) ? [] : el.ownerDocument.listenedTypes(el).filter(t => !/^(focus|blur|focusin|focusout)$/.test(t))
162
+ if (!role && !(interactive && listened.length)) return null
163
+ const node: AgentNode = {role: role || 'generic'}
164
+ // a role-less element the app listens to is one control (its contents its name), unless
165
+ // interactive elements are inside it: a scene-graph group with a click intent (canvas-three F1)
166
+ // keeps its children
167
+ const leaf = /^(textbox|searchbox|checkbox|radio|switch|slider|spinbutton|combobox|listbox|button|link|img|heading|option|progressbar)$/.test(node.role) || !role && !el.querySelectorAll('*').some(d => isInteractive(d, rules))
168
+ // a name from contents only for the roles that take one (ARIA "name from content"); a
169
+ // landmark, list or group (or such a container) is named by aria-label / aria-labelledby / title only
170
+ const name = !role && leaf || CONTENT_NAMED.test(node.role) ? nameOf(el, rules) : labelOnly(el, rules)
171
+ if (name) node.name = name
172
+ if (interactive) node.ref = 'e' + nodeId(el)
173
+ if (listened.length) node.on = listened
174
+ const states: string[] = []
175
+ if (/^(checkbox|radio|switch)$/.test(node.role)) { if (isCheckable(el) ? el.checked : el.getAttribute('aria-checked') == 'true') states.push('checked') }
176
+ if (el.disabled || el.getAttribute('aria-disabled') == 'true') states.push('disabled')
177
+ for (const s of ['expanded', 'pressed', 'selected']) { const v = el.getAttribute('aria-' + s); if (v) states.push(v == 'true' ? s : `${s}=${v}`) }
178
+ if (node.role == 'option' && el.selected) states.push('selected')
179
+ if (el.required) states.push('required')
180
+ if (node.role == 'heading') states.push('level=' + (+(el.getAttribute('aria-level') || el.localName.slice(1)) || 2))
181
+ if (el.ownerDocument.activeElement === el) states.push('focused')
182
+ if (states.length) node.states = states
183
+ if (isTextField(el)) node.value = el.type == 'password' ? '•'.repeat([...el.value].length) : el.value
184
+ if (el.localName == 'select') {
185
+ const o = el.options.find(x => optionValue(x) == el.value)
186
+ if (o) node.value = squash(o.textContent)
187
+ node.children = el.options.map(x => ({role: 'option', name: squash(x.textContent), ...(x === o ? {states: ['selected']} : {})}))
188
+ return node
189
+ }
190
+ if (!leaf) { const c = kids(el); if (c.length) node.children = c }
191
+ return node
192
+ }
193
+ return kids(doc.body)
194
+ }
195
+
196
+ const q = (s: string) => JSON.stringify(s)
197
+ /** agent nodes as the YAML-like text */
198
+ export function formatSnapshot(nodes: AgentNode[], indent = ''): string {
199
+ const lines: string[] = []
200
+ for (const n of nodes) {
201
+ if (n.role == 'text') { lines.push(`${indent}- text: ${n.text}`); continue }
202
+ let line = `${indent}- ${n.role}`
203
+ if (n.name) line += ' ' + q(n.name)
204
+ for (const s of n.states || []) line += ` [${s}]`
205
+ if (n.ref) line += ` [ref=${n.ref}]`
206
+ if (n.on?.length) line += ` [on=${n.on.join(',')}]`
207
+ if (n.value !== undefined && n.value !== '') line += ': ' + q(n.value)
208
+ if (n.children?.length) lines.push(line + ':', formatSnapshot(n.children, indent + ' '))
209
+ else lines.push(line)
210
+ }
211
+ return lines.join('\n')
212
+ }
@@ -0,0 +1,106 @@
1
+ // 'sygnal/agent-ui': a Sygnal app rendered for an LLM agent, on 'sygnal/host'. Guide: https://sygnal.js.org/renderers/agent-ui
2
+ import type { HostDocument, HostElement, HostSource, HostStyleRule } from 'sygnal/host'
3
+
4
+ /** A snapshot node */
5
+ export interface AgentNode {
6
+ role: string
7
+ name?: string
8
+ /** `e<id>`: what tools act on */
9
+ ref?: string
10
+ value?: string
11
+ /** checked, disabled, expanded, pressed, selected, focused, required, level=n */
12
+ states?: string[]
13
+ /** events the app's intents listen for on a non-native element */
14
+ on?: string[]
15
+ text?: string
16
+ children?: AgentNode[]
17
+ }
18
+ export function snapshotTree(doc: HostDocument, options?: { rules?: readonly HostStyleRule[] }): AgentNode[]
19
+ export function formatSnapshot(nodes: AgentNode[], indent?: string): string
20
+ export function roleOf(el: HostElement): string | null
21
+ export function nameOf(el: HostElement, rules?: readonly HostStyleRule[]): string
22
+
23
+ export interface AgentUIOptions {
24
+ /** what is shown: `['.todo .edit', { display: 'none' }]` */
25
+ styles?: readonly HostStyleRule[]
26
+ /** a link's default action (a router driver's go) */
27
+ onNavigate?: (href: string, el: HostElement) => void
28
+ /** quiet time before a tool call answers (ms, default 20) */
29
+ quietMs?: number
30
+ /** longest wait for the app to settle (ms, default 2000) */
31
+ maxWaitMs?: number
32
+ document?: HostDocument
33
+ }
34
+
35
+ export interface McpTool {
36
+ name: string
37
+ description: string
38
+ inputSchema: Record<string, any>
39
+ annotations?: Record<string, any>
40
+ }
41
+ export interface AgentUITool extends McpTool {
42
+ annotations: { readOnlyHint?: boolean, destructiveHint?: boolean }
43
+ }
44
+ export type AgentUIResult =
45
+ | { ok: true, snapshot: string }
46
+ | { ok: false, error: string, snapshot?: string }
47
+
48
+ export interface AgentUI {
49
+ readonly doc: HostDocument
50
+ /** run(App, { DOM: ui.driver }) */
51
+ readonly driver: (vnode$: any, name?: string) => HostSource
52
+ snapshot(): string
53
+ tree(): AgentNode[]
54
+ element(ref: string): HostElement | null
55
+ /** ui_snapshot, ui_click, ui_type, ui_press, ui_select, ui_check */
56
+ tools(): AgentUITool[]
57
+ /** the tools as ChatRequest.tools (sygnal/ai) */
58
+ chatTools(): Record<string, { description: string, inputSchema: Record<string, any> }>
59
+ /** run a tool; answers after the app settles, with the new snapshot */
60
+ call(name: string, args?: Record<string, any>): Promise<AgentUIResult>
61
+ /** run()'s result, to wait on its flushes (runAgentUI does this) */
62
+ attach(app: any): void
63
+ settle(): Promise<void>
64
+ subscribe(listener: () => void): () => void
65
+ }
66
+ export function createAgentUI(options?: AgentUIOptions): AgentUI
67
+
68
+ export interface RunAgentUIOptions extends AgentUIOptions {
69
+ drivers?: Record<string, any>
70
+ runOptions?: Record<string, any>
71
+ }
72
+ export function runAgentUI(App: any, options?: RunAgentUIOptions): { app: any, ui: AgentUI }
73
+
74
+ /** a tool set served alongside: sygnal/ai's agentTools(app), or your own */
75
+ export interface ExtraTools {
76
+ list(): { name: string, description: string, inputSchema: Record<string, any>, annotations?: Record<string, any> }[]
77
+ call(name: string, args?: unknown): Promise<unknown>
78
+ subscribe?(listener: (change: any) => void): () => void
79
+ }
80
+ export interface ServeMcpOptions {
81
+ name?: string
82
+ version?: string
83
+ instructions?: string
84
+ tools?: ExtraTools
85
+ /** default process.stdin / process.stdout */
86
+ input?: any
87
+ output?: any
88
+ }
89
+ export interface McpStdio {
90
+ listChanged(): void
91
+ stop(): void
92
+ done: Promise<void>
93
+ }
94
+ /** the UI tools (and extra tools) as an MCP server over newline-delimited JSON (default stdio) */
95
+ export function serveMcp(ui: AgentUI, options?: ServeMcpOptions): McpStdio
96
+
97
+ export interface McpToolResult { text: string, value?: Record<string, any>, isError?: boolean }
98
+ export interface McpServerSpec {
99
+ name: string
100
+ version: string
101
+ instructions?: string
102
+ tools(): McpTool[]
103
+ call(name: string, args: Record<string, any>): Promise<McpToolResult>
104
+ }
105
+ export function createMcpHandler(spec: McpServerSpec): { handle(message: unknown, inBatch?: boolean): Promise<unknown> }
106
+ export function serveMcpStream(spec: McpServerSpec, input?: any, output?: any): McpStdio