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.
- package/CHANGELOG.md +632 -0
- package/dist/agent-ui.cjs.js +592 -0
- package/dist/agent-ui.cjs.js.map +1 -0
- package/dist/agent-ui.esm.js +582 -0
- package/dist/agent-ui.esm.js.map +1 -0
- package/dist/api.cjs.js +63 -0
- package/dist/api.cjs.js.map +1 -0
- package/dist/api.esm.js +2 -0
- package/dist/api.esm.js.map +1 -0
- package/dist/astro/index.cjs.js +968 -4
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +968 -4
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +73 -45
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +73 -45
- package/dist/astro/server.mjs.map +1 -1
- package/dist/canvas.cjs.js +2919 -0
- package/dist/canvas.cjs.js.map +1 -0
- package/dist/canvas.esm.js +2866 -0
- package/dist/canvas.esm.js.map +1 -0
- package/dist/devtools.cjs.js +38 -6
- package/dist/devtools.cjs.js.map +1 -1
- package/dist/devtools.esm.js +38 -6
- package/dist/devtools.esm.js.map +1 -1
- package/dist/diagnostics.cjs.js +224 -20
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +224 -20
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/accessibility.md +1 -1
- package/dist/guide/ai-compose.md +150 -0
- package/dist/guide/behaviors.md +2 -1
- package/dist/guide/collaborative-state.md +265 -0
- package/dist/guide/element-commands.md +36 -2
- package/dist/guide/forms-reference.md +5 -5
- package/dist/guide/forms.md +3 -2
- package/dist/guide/http.md +28 -1
- package/dist/guide/islands.md +240 -0
- package/dist/guide/recipes/code-editor.md +1 -1
- package/dist/guide/renderers/agent-ui.md +138 -0
- package/dist/guide/renderers/custom-targets.md +126 -0
- package/dist/guide/renderers/overview.md +120 -0
- package/dist/guide/renderers/react-native.md +128 -0
- package/dist/guide/renderers/terminal.md +306 -0
- package/dist/guide/renderers/three.md +332 -0
- package/dist/guide/renderers/write-once.md +137 -0
- package/dist/guide/resources.md +2 -0
- package/dist/guide/sandboxed-components.md +223 -0
- package/dist/guide/timers.md +32 -5
- package/dist/guide/typed-apis.md +442 -0
- package/dist/guide/ui/accordion.md +11 -0
- package/dist/guide/ui/combobox.md +112 -8
- package/dist/guide/ui/dialog.md +96 -20
- package/dist/guide/ui/menu.md +57 -5
- package/dist/guide/ui/overview.md +8 -4
- package/dist/guide/ui/popover.md +69 -9
- package/dist/guide/ui/select.md +54 -4
- package/dist/guide/ui/tabs.md +1 -1
- package/dist/guide/ui/tooltip.md +7 -5
- package/dist/guide/virtual-collections.md +3 -0
- package/dist/guide/widgets.md +90 -1
- package/dist/host.cjs.js +2181 -0
- package/dist/host.cjs.js.map +1 -0
- package/dist/host.esm.js +2146 -0
- package/dist/host.esm.js.map +1 -0
- package/dist/index.cjs.js +3948 -586
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +584 -44
- package/dist/index.esm.js +3918 -586
- package/dist/index.esm.js.map +1 -1
- package/dist/primitives.cjs.js +143 -0
- package/dist/primitives.cjs.js.map +1 -0
- package/dist/primitives.esm.js +133 -0
- package/dist/primitives.esm.js.map +1 -0
- package/dist/react-native.cjs.js +248 -0
- package/dist/react-native.cjs.js.map +1 -0
- package/dist/react-native.esm.js +244 -0
- package/dist/react-native.esm.js.map +1 -0
- package/dist/replay.cjs.js +39 -0
- package/dist/replay.cjs.js.map +1 -0
- package/dist/replay.esm.js +2 -0
- package/dist/replay.esm.js.map +1 -0
- package/dist/sandbox-build.cjs.js +140 -0
- package/dist/sandbox-build.cjs.js.map +1 -0
- package/dist/sandbox-build.esm.js +137 -0
- package/dist/sandbox-build.esm.js.map +1 -0
- package/dist/sandbox-kernel.esm.js +212 -0
- package/dist/sandbox-kernel.esm.js.map +1 -0
- package/dist/sandbox-worker.cjs.js +857 -0
- package/dist/sandbox-worker.cjs.js.map +1 -0
- package/dist/sandbox-worker.esm.js +854 -0
- package/dist/sandbox-worker.esm.js.map +1 -0
- package/dist/sandbox.cjs.js +1425 -0
- package/dist/sandbox.cjs.js.map +1 -0
- package/dist/sandbox.esm.js +1406 -0
- package/dist/sandbox.esm.js.map +1 -0
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/terminal.cjs.js +1314 -0
- package/dist/terminal.cjs.js.map +1 -0
- package/dist/terminal.esm.js +1304 -0
- package/dist/terminal.esm.js.map +1 -0
- package/dist/three.cjs.js +1111 -0
- package/dist/three.cjs.js.map +1 -0
- package/dist/three.esm.js +1079 -0
- package/dist/three.esm.js.map +1 -0
- package/dist/ui-combobox.cjs.js +256 -18
- package/dist/ui-combobox.cjs.js.map +1 -1
- package/dist/ui-combobox.esm.js +256 -18
- package/dist/ui-combobox.esm.js.map +1 -1
- package/dist/ui-menu.cjs.js +150 -16
- package/dist/ui-menu.cjs.js.map +1 -1
- package/dist/ui-menu.esm.js +151 -17
- package/dist/ui-menu.esm.js.map +1 -1
- package/dist/ui-select.cjs.js +95 -9
- package/dist/ui-select.cjs.js.map +1 -1
- package/dist/ui-select.esm.js +95 -9
- package/dist/ui-select.esm.js.map +1 -1
- package/dist/ui.cjs.js +679 -173
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.esm.js +679 -175
- package/dist/ui.esm.js.map +1 -1
- package/dist/vike/config/package.json +1 -1
- package/dist/vite/plugin.cjs.js +968 -4
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +968 -4
- package/dist/vite/plugin.mjs.map +1 -1
- package/dist/worker-page.cjs.js +669 -0
- package/dist/worker-page.cjs.js.map +1 -0
- package/dist/worker-page.esm.js +665 -0
- package/dist/worker-page.esm.js.map +1 -0
- package/dist/worker.cjs.js +377 -0
- package/dist/worker.cjs.js.map +1 -0
- package/dist/worker.esm.js +374 -0
- package/dist/worker.esm.js.map +1 -0
- package/dist/yjs-testing.cjs.js +98 -0
- package/dist/yjs-testing.cjs.js.map +1 -0
- package/dist/yjs-testing.esm.js +77 -0
- package/dist/yjs-testing.esm.js.map +1 -0
- package/dist/yjs.cjs.js +923 -0
- package/dist/yjs.cjs.js.map +1 -0
- package/dist/yjs.esm.js +900 -0
- package/dist/yjs.esm.js.map +1 -0
- package/dist/zag.cjs.js +11 -1
- package/dist/zag.cjs.js.map +1 -1
- package/dist/zag.esm.js +11 -1
- package/dist/zag.esm.js.map +1 -1
- package/llms/ai.md +65 -0
- package/llms/collaboration.md +5 -0
- package/llms/graphics.md +10 -0
- package/llms/renderers.md +7 -0
- package/llms/replay.md +5 -0
- package/llms/sandbox.md +5 -0
- package/llms/typed-apis.md +5 -0
- package/llms/ui.md +10 -0
- package/llms-full.txt +444 -0
- package/llms.txt +37 -35
- package/package.json +139 -3
- package/src/agent-ui/mcp.ts +152 -0
- package/src/agent-ui/snapshot.ts +212 -0
- package/src/agent-ui.d.ts +106 -0
- package/src/agent-ui.ts +249 -0
- package/src/ai.d.ts +95 -2
- package/src/api.d.ts +303 -0
- package/src/api.ts +9 -0
- package/src/canvas/Canvas.ts +585 -0
- package/src/canvas/backend.ts +627 -0
- package/src/canvas/geometry.ts +187 -0
- package/src/canvas/gesture.ts +23 -0
- package/src/canvas/image.ts +197 -0
- package/src/canvas/kit.ts +130 -0
- package/src/canvas/paint.ts +260 -0
- package/src/canvas/pointer.ts +25 -0
- package/src/canvas/snap.ts +68 -0
- package/src/canvas/svg.ts +99 -0
- package/src/canvas.d.ts +373 -0
- package/src/canvas.ts +46 -0
- package/src/core/actions.ts +14 -3
- package/src/core/cell.ts +12 -4
- package/src/core/debug.ts +8 -3
- package/src/core/hooks.ts +16 -2
- package/src/core/hosts/collection.ts +1 -1
- package/src/core/hosts/switchable.ts +26 -8
- package/src/core/instance.ts +98 -45
- package/src/core/markers/portal.ts +4 -0
- package/src/core/registry.ts +4 -0
- package/src/core/runtime.ts +34 -25
- package/src/core/statics.ts +5 -5
- package/src/cycle/dom/EventDelegator.ts +37 -33
- package/src/cycle/dom/MainDOMSource.ts +16 -14
- package/src/cycle/dom/enrichEventStream.ts +13 -9
- package/src/cycle/dom/fromEvent.ts +6 -10
- package/src/cycle/dom/index.ts +11 -7
- package/src/cycle/dom/isolate.ts +13 -6
- package/src/cycle/dom/makeDOMDriver.ts +6 -2
- package/src/cycle/dom/utils.ts +9 -2
- package/src/cycle/run/adapt.ts +4 -12
- package/src/devtools.d.ts +9 -0
- package/src/devtools.ts +2 -1
- package/src/extra/ai/compose/catalog.ts +2 -1
- package/src/extra/ai/compose/decisions.ts +1 -2
- package/src/extra/ai/compose/generated.ts +234 -0
- package/src/extra/ai/compose/index.ts +6 -2
- package/src/extra/ai/compose/layout.ts +3 -1
- package/src/extra/ai/compose/llm.ts +7 -3
- package/src/extra/ai/compose/types.ts +2 -2
- package/src/extra/ai/transports/shared.ts +3 -1
- package/src/extra/api/contract.ts +485 -0
- package/src/extra/api/index.ts +26 -0
- package/src/extra/behaviors.ts +3 -3
- package/src/extra/controlled.ts +44 -0
- package/src/extra/copyAsTest.ts +22 -0
- package/src/extra/devtools.ts +5 -3
- package/src/extra/devtoolsActions.ts +8 -0
- package/src/extra/devtoolsNext.ts +1 -1
- package/src/extra/diagnostics/checks/actionLog.ts +8 -3
- package/src/extra/diagnostics/checks/behaviors.ts +25 -0
- package/src/extra/diagnostics/checks/elementCommands.ts +17 -5
- package/src/extra/diagnostics/checks/index.ts +1 -1
- package/src/extra/diagnostics/checks/next.ts +78 -4
- package/src/extra/diagnostics/checks/props.ts +6 -1
- package/src/extra/diagnostics/checks/replies.ts +11 -7
- package/src/extra/diagnostics/checks/timers.ts +8 -2
- package/src/extra/diagnostics/checks/virtual.ts +11 -0
- package/src/extra/diagnostics/codes.ts +51 -1
- package/src/extra/elementCommands.ts +20 -4
- package/src/extra/fetchDriver.ts +4 -1
- package/src/extra/fetchEventSource.ts +6 -2
- package/src/extra/field.ts +29 -0
- package/src/extra/form.ts +42 -16
- package/src/extra/frame.ts +54 -0
- package/src/extra/keymap.ts +41 -0
- package/src/extra/pager.ts +2 -0
- package/src/extra/persist.ts +2 -1
- package/src/extra/pointer.ts +262 -0
- package/src/extra/processForm.ts +34 -1
- package/src/extra/replay/address.ts +49 -0
- package/src/extra/replay/entropy.ts +99 -0
- package/src/extra/replay/format.ts +263 -0
- package/src/extra/replay/index.ts +8 -0
- package/src/extra/replay/inert.ts +22 -0
- package/src/extra/replay/recorder.ts +247 -0
- package/src/extra/replay/replayer.ts +234 -0
- package/src/extra/replay/report.ts +126 -0
- package/src/extra/replay/schema.ts +52 -0
- package/src/extra/replay/version.ts +3 -0
- package/src/extra/router.ts +7 -3
- package/src/extra/selection.ts +2 -0
- package/src/extra/socketDriver.ts +51 -6
- package/src/extra/ssr.ts +128 -42
- package/src/extra/testing.ts +295 -39
- package/src/extra/timers.ts +96 -7
- package/src/extra/undo.ts +2 -0
- package/src/extra/virtual.ts +25 -2
- package/src/extra/widget.ts +106 -17
- package/src/host/actions.ts +383 -0
- package/src/host/delegation.ts +101 -0
- package/src/host/dom.ts +553 -0
- package/src/host/driver.ts +185 -0
- package/src/host/selector.ts +119 -0
- package/src/host/style.ts +51 -0
- package/src/host.d.ts +298 -0
- package/src/host.ts +46 -0
- package/src/index.d.ts +302 -38
- package/src/index.ts +15 -3
- package/src/pragma/blocks.ts +211 -0
- package/src/pragma/index.ts +44 -10
- package/src/primitives.d.ts +54 -0
- package/src/primitives.ts +136 -0
- package/src/react-native.d.ts +57 -0
- package/src/react-native.ts +285 -0
- package/src/react-peers.d.ts +21 -3
- package/src/replay.d.ts +185 -0
- package/src/replay.ts +6 -0
- package/src/sandbox/build.ts +119 -0
- package/src/sandbox/create.ts +187 -0
- package/src/sandbox/engine.ts +267 -0
- package/src/sandbox/generated.ts +195 -0
- package/src/sandbox/kernel.ts +180 -0
- package/src/sandbox/mirror.ts +269 -0
- package/src/sandbox/protocol.ts +128 -0
- package/src/sandbox/quickjs.ts +74 -0
- package/src/sandbox/ssr.ts +41 -0
- package/src/sandbox/widget.ts +60 -0
- package/src/sandbox/worker-entry.ts +59 -0
- package/src/sandbox/worker.ts +137 -0
- package/src/sandbox-build.d.ts +12 -0
- package/src/sandbox-build.ts +6 -0
- package/src/sandbox.d.ts +299 -0
- package/src/sandbox.ts +25 -0
- package/src/shared.ts +12 -0
- package/src/terminal/paint.ts +712 -0
- package/src/terminal.d.ts +167 -0
- package/src/terminal.ts +530 -0
- package/src/three/assets.ts +170 -0
- package/src/three/host.ts +36 -0
- package/src/three/scene.ts +217 -0
- package/src/three/target.ts +252 -0
- package/src/three.d.ts +221 -0
- package/src/three.ts +30 -0
- package/src/ui/accordion.ts +21 -7
- package/src/ui/dialog.ts +192 -90
- package/src/ui/disclosure.ts +2 -0
- package/src/ui/dom.ts +30 -0
- package/src/ui/layers.ts +132 -0
- package/src/ui/popover.ts +120 -21
- package/src/ui/shared.ts +98 -12
- package/src/ui/stack.ts +18 -0
- package/src/ui/tabs.ts +2 -0
- package/src/ui/toaster.ts +16 -15
- package/src/ui/tooltip.ts +24 -8
- package/src/ui/tooltips.ts +26 -14
- package/src/ui/zag/combobox.ts +180 -13
- package/src/ui/zag/menu.ts +97 -17
- package/src/ui/zag/select.ts +15 -4
- package/src/ui/zag/shared.ts +60 -8
- package/src/ui-combobox.d.ts +16 -1
- package/src/ui-menu.d.ts +22 -7
- package/src/ui-zag-types.d.ts +13 -5
- package/src/ui.d.ts +62 -7
- package/src/ui.ts +2 -2
- package/src/vite/api.ts +186 -0
- package/src/vite/compile.ts +280 -0
- package/src/vite/plugin.d.ts +34 -0
- package/src/vite/plugin.ts +78 -4
- package/src/vite/sandbox.ts +16 -0
- package/src/worker/page.ts +396 -0
- package/src/worker/protocol.ts +48 -0
- package/src/worker/target.ts +282 -0
- package/src/worker-page.d.ts +42 -0
- package/src/worker-page.ts +12 -0
- package/src/worker.d.ts +65 -0
- package/src/worker.ts +16 -0
- package/src/yjs/driver.ts +253 -0
- package/src/yjs/mapping.ts +389 -0
- package/src/yjs/shared.ts +138 -0
- package/src/yjs/testing.ts +91 -0
- package/src/yjs-testing.d.ts +34 -0
- package/src/yjs-testing.ts +7 -0
- package/src/yjs.d.ts +109 -0
- package/src/yjs.ts +18 -0
- package/src/zag.d.ts +2 -0
- package/src/zag.ts +8 -1
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { run } from 'sygnal';
|
|
2
|
+
import { isCheckable, isTextField, labelTarget, computeStyle, isInteractive, nodeId, optionValue, HostDocument, makeHostDriver, activate, chooseOption, pressKey, typeText, fill, doubleClick } from 'sygnal/host';
|
|
3
|
+
|
|
4
|
+
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)$/;
|
|
5
|
+
const NATIVE = /^(input|textarea|select|button|a|summary|option)$/;
|
|
6
|
+
const LANDMARKS = { nav: 'navigation', main: 'main', aside: 'complementary', search: 'search', dialog: 'dialog' };
|
|
7
|
+
function roleOf(el) {
|
|
8
|
+
const r = el.getAttribute('role');
|
|
9
|
+
if (r)
|
|
10
|
+
return r.split(/\s+/)[0];
|
|
11
|
+
const t = el.localName;
|
|
12
|
+
// a target's own field tags (registerFieldTags)
|
|
13
|
+
if (t != 'input' && t != 'textarea' && isCheckable(el))
|
|
14
|
+
return 'checkbox';
|
|
15
|
+
if (t != 'input' && t != 'textarea' && isTextField(el))
|
|
16
|
+
return 'textbox';
|
|
17
|
+
if (t == 'a')
|
|
18
|
+
return el.hasAttribute('href') ? 'link' : null;
|
|
19
|
+
if (t == 'button' || t == 'summary')
|
|
20
|
+
return 'button';
|
|
21
|
+
if (t == 'input') {
|
|
22
|
+
const ty = el.type || 'text';
|
|
23
|
+
if (/^(button|submit|reset|image)$/.test(ty))
|
|
24
|
+
return 'button';
|
|
25
|
+
if (ty == 'checkbox' || ty == 'radio')
|
|
26
|
+
return ty;
|
|
27
|
+
if (ty == 'range')
|
|
28
|
+
return 'slider';
|
|
29
|
+
if (ty == 'number')
|
|
30
|
+
return 'spinbutton';
|
|
31
|
+
if (ty == 'search')
|
|
32
|
+
return 'searchbox';
|
|
33
|
+
if (ty == 'hidden' || ty == 'file')
|
|
34
|
+
return null;
|
|
35
|
+
return 'textbox';
|
|
36
|
+
}
|
|
37
|
+
if (t == 'textarea')
|
|
38
|
+
return 'textbox';
|
|
39
|
+
if (t == 'select')
|
|
40
|
+
return el.multiple ? 'listbox' : 'combobox';
|
|
41
|
+
if (t == 'option')
|
|
42
|
+
return 'option';
|
|
43
|
+
if (/^h[1-6]$/.test(t))
|
|
44
|
+
return 'heading';
|
|
45
|
+
if (t == 'ul' || t == 'ol' || t == 'menu')
|
|
46
|
+
return 'list';
|
|
47
|
+
if (t == 'li')
|
|
48
|
+
return 'listitem';
|
|
49
|
+
if (t == 'img')
|
|
50
|
+
return el.getAttribute('alt') === '' ? null : 'img';
|
|
51
|
+
if (t == 'table')
|
|
52
|
+
return 'table';
|
|
53
|
+
if (t == 'tr')
|
|
54
|
+
return 'row';
|
|
55
|
+
if (t == 'td')
|
|
56
|
+
return 'cell';
|
|
57
|
+
if (t == 'th')
|
|
58
|
+
return 'columnheader';
|
|
59
|
+
if (t == 'progress')
|
|
60
|
+
return 'progressbar';
|
|
61
|
+
if (t == 'fieldset' || t == 'details')
|
|
62
|
+
return 'group';
|
|
63
|
+
if (t == 'form')
|
|
64
|
+
return el.getAttribute('aria-label') || el.getAttribute('name') ? 'form' : null;
|
|
65
|
+
if (t == 'section')
|
|
66
|
+
return el.getAttribute('aria-label') ? 'region' : null;
|
|
67
|
+
return LANDMARKS[t] || null;
|
|
68
|
+
}
|
|
69
|
+
const squash = (s) => s.replace(/\s+/g, ' ').trim();
|
|
70
|
+
/** visible text, without hidden subtrees */
|
|
71
|
+
function textOf(n, rules, skip) {
|
|
72
|
+
if (n.nodeType === 3)
|
|
73
|
+
return n.data;
|
|
74
|
+
const el = n;
|
|
75
|
+
if (el === skip || computeStyle(el, rules).display == 'none' || el.getAttribute('aria-hidden') == 'true')
|
|
76
|
+
return '';
|
|
77
|
+
if (el.localName == 'input' || el.localName == 'select' || el.localName == 'textarea')
|
|
78
|
+
return '';
|
|
79
|
+
return el.childNodes.map(c => textOf(c, rules, skip)).join(BLOCK.test(el.localName) ? ' ' : '');
|
|
80
|
+
}
|
|
81
|
+
/** the accessible name: aria-label, aria-labelledby, a label, contents, alt / value / placeholder / title */
|
|
82
|
+
function nameOf(el, rules = []) {
|
|
83
|
+
const a = el.getAttribute('aria-label');
|
|
84
|
+
if (a)
|
|
85
|
+
return squash(a);
|
|
86
|
+
const by = el.getAttribute('aria-labelledby');
|
|
87
|
+
if (by)
|
|
88
|
+
return squash(by.split(/\s+/).map(id => { const t = el.ownerDocument.getElementById(id); return t ? textOf(t, rules) : ''; }).join(' '));
|
|
89
|
+
const t = el.localName;
|
|
90
|
+
if (t == 'input' || t == 'textarea' || t == 'select') {
|
|
91
|
+
const id = el.id;
|
|
92
|
+
const lab = (id && el.ownerDocument.querySelector(`label[for="${id}"]`)) || el.closest('label');
|
|
93
|
+
// (a label names its control even when it's visually hidden, as the browser's accessible name)
|
|
94
|
+
const l = lab ? squash(textOf(lab, [], el)) : '';
|
|
95
|
+
if (l)
|
|
96
|
+
return l;
|
|
97
|
+
if (t == 'input' && /^(button|submit|reset)$/.test(el.type))
|
|
98
|
+
return el.getAttribute('value') || (el.type == 'reset' ? 'Reset' : 'Submit');
|
|
99
|
+
return squash(el.placeholder || el.title || '');
|
|
100
|
+
}
|
|
101
|
+
if (t == 'img')
|
|
102
|
+
return squash(el.getAttribute('alt') || el.title || '');
|
|
103
|
+
return squash(textOf(el, rules)) || squash(el.title || '');
|
|
104
|
+
}
|
|
105
|
+
const CONTENT_NAMED = /^(button|link|heading|checkbox|radio|switch|textbox|searchbox|combobox|listbox|slider|spinbutton|option|img|columnheader|cell|tab|menuitem|treeitem|tooltip|progressbar)$/;
|
|
106
|
+
const labelOnly = (el, rules) => {
|
|
107
|
+
const a = el.getAttribute('aria-label');
|
|
108
|
+
if (a)
|
|
109
|
+
return squash(a);
|
|
110
|
+
const by = el.getAttribute('aria-labelledby');
|
|
111
|
+
if (by)
|
|
112
|
+
return squash(by.split(/\s+/).map(id => { const t = el.ownerDocument.getElementById(id); return t ? textOf(t, rules) : ''; }).join(' '));
|
|
113
|
+
return squash(el.title || '');
|
|
114
|
+
};
|
|
115
|
+
/** the host tree as agent nodes (generic containers flattened, their text merged into text nodes) */
|
|
116
|
+
function snapshotTree(doc, options = {}) {
|
|
117
|
+
const rules = options.rules || [];
|
|
118
|
+
// labels that name a control are read as that control's name, not again as text
|
|
119
|
+
const naming = new Set();
|
|
120
|
+
for (const l of doc.body.querySelectorAll('label')) {
|
|
121
|
+
const t = labelTarget(l);
|
|
122
|
+
if (t && t.localName != 'button')
|
|
123
|
+
naming.add(l);
|
|
124
|
+
}
|
|
125
|
+
const kids = (el) => {
|
|
126
|
+
const out = [];
|
|
127
|
+
let text = '';
|
|
128
|
+
const flushText = () => { const t = squash(text); if (t)
|
|
129
|
+
out.push({ role: 'text', text: t }); text = ''; };
|
|
130
|
+
// a flex container's children are separate items (a column's lines, a row's gap): their text doesn't run together
|
|
131
|
+
const visit = (n, inFlex = false) => {
|
|
132
|
+
if (n.nodeType === 3) {
|
|
133
|
+
text += n.data;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const c = n;
|
|
137
|
+
const st = computeStyle(c, rules);
|
|
138
|
+
if (st.display == 'none' || c.getAttribute('aria-hidden') == 'true')
|
|
139
|
+
return;
|
|
140
|
+
const node = nodeFor(c);
|
|
141
|
+
if (node) {
|
|
142
|
+
flushText();
|
|
143
|
+
out.push(node);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (naming.has(c)) {
|
|
147
|
+
// the label's own text names its control; any other content stays
|
|
148
|
+
for (const x of c.childNodes)
|
|
149
|
+
if (x.nodeType === 1)
|
|
150
|
+
visit(x);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const block = inFlex || BLOCK.test(c.localName);
|
|
154
|
+
const flex = /flex$/.test(st.display || '');
|
|
155
|
+
if (block)
|
|
156
|
+
flushText();
|
|
157
|
+
for (const x of c.childNodes)
|
|
158
|
+
visit(x, flex);
|
|
159
|
+
if (block)
|
|
160
|
+
flushText();
|
|
161
|
+
};
|
|
162
|
+
for (const c of el.childNodes)
|
|
163
|
+
visit(c, /flex$/.test(computeStyle(el, rules).display || ''));
|
|
164
|
+
flushText();
|
|
165
|
+
return out;
|
|
166
|
+
};
|
|
167
|
+
const nodeFor = (el) => {
|
|
168
|
+
const role = roleOf(el);
|
|
169
|
+
const interactive = isInteractive(el, rules);
|
|
170
|
+
const listened = NATIVE.test(el.localName) ? [] : el.ownerDocument.listenedTypes(el).filter(t => !/^(focus|blur|focusin|focusout)$/.test(t));
|
|
171
|
+
if (!role && !(interactive && listened.length))
|
|
172
|
+
return null;
|
|
173
|
+
const node = { role: role || 'generic' };
|
|
174
|
+
// a role-less element the app listens to is one control (its contents its name), unless
|
|
175
|
+
// interactive elements are inside it: a scene-graph group with a click intent (canvas-three F1)
|
|
176
|
+
// keeps its children
|
|
177
|
+
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));
|
|
178
|
+
// a name from contents only for the roles that take one (ARIA "name from content"); a
|
|
179
|
+
// landmark, list or group (or such a container) is named by aria-label / aria-labelledby / title only
|
|
180
|
+
const name = !role && leaf || CONTENT_NAMED.test(node.role) ? nameOf(el, rules) : labelOnly(el, rules);
|
|
181
|
+
if (name)
|
|
182
|
+
node.name = name;
|
|
183
|
+
if (interactive)
|
|
184
|
+
node.ref = 'e' + nodeId(el);
|
|
185
|
+
if (listened.length)
|
|
186
|
+
node.on = listened;
|
|
187
|
+
const states = [];
|
|
188
|
+
if (/^(checkbox|radio|switch)$/.test(node.role)) {
|
|
189
|
+
if (isCheckable(el) ? el.checked : el.getAttribute('aria-checked') == 'true')
|
|
190
|
+
states.push('checked');
|
|
191
|
+
}
|
|
192
|
+
if (el.disabled || el.getAttribute('aria-disabled') == 'true')
|
|
193
|
+
states.push('disabled');
|
|
194
|
+
for (const s of ['expanded', 'pressed', 'selected']) {
|
|
195
|
+
const v = el.getAttribute('aria-' + s);
|
|
196
|
+
if (v)
|
|
197
|
+
states.push(v == 'true' ? s : `${s}=${v}`);
|
|
198
|
+
}
|
|
199
|
+
if (node.role == 'option' && el.selected)
|
|
200
|
+
states.push('selected');
|
|
201
|
+
if (el.required)
|
|
202
|
+
states.push('required');
|
|
203
|
+
if (node.role == 'heading')
|
|
204
|
+
states.push('level=' + (+(el.getAttribute('aria-level') || el.localName.slice(1)) || 2));
|
|
205
|
+
if (el.ownerDocument.activeElement === el)
|
|
206
|
+
states.push('focused');
|
|
207
|
+
if (states.length)
|
|
208
|
+
node.states = states;
|
|
209
|
+
if (isTextField(el))
|
|
210
|
+
node.value = el.type == 'password' ? '•'.repeat([...el.value].length) : el.value;
|
|
211
|
+
if (el.localName == 'select') {
|
|
212
|
+
const o = el.options.find(x => optionValue(x) == el.value);
|
|
213
|
+
if (o)
|
|
214
|
+
node.value = squash(o.textContent);
|
|
215
|
+
node.children = el.options.map(x => ({ role: 'option', name: squash(x.textContent), ...(x === o ? { states: ['selected'] } : {}) }));
|
|
216
|
+
return node;
|
|
217
|
+
}
|
|
218
|
+
if (!leaf) {
|
|
219
|
+
const c = kids(el);
|
|
220
|
+
if (c.length)
|
|
221
|
+
node.children = c;
|
|
222
|
+
}
|
|
223
|
+
return node;
|
|
224
|
+
};
|
|
225
|
+
return kids(doc.body);
|
|
226
|
+
}
|
|
227
|
+
const q = (s) => JSON.stringify(s);
|
|
228
|
+
/** agent nodes as the YAML-like text */
|
|
229
|
+
function formatSnapshot(nodes, indent = '') {
|
|
230
|
+
const lines = [];
|
|
231
|
+
for (const n of nodes) {
|
|
232
|
+
if (n.role == 'text') {
|
|
233
|
+
lines.push(`${indent}- text: ${n.text}`);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
let line = `${indent}- ${n.role}`;
|
|
237
|
+
if (n.name)
|
|
238
|
+
line += ' ' + q(n.name);
|
|
239
|
+
for (const s of n.states || [])
|
|
240
|
+
line += ` [${s}]`;
|
|
241
|
+
if (n.ref)
|
|
242
|
+
line += ` [ref=${n.ref}]`;
|
|
243
|
+
if (n.on?.length)
|
|
244
|
+
line += ` [on=${n.on.join(',')}]`;
|
|
245
|
+
if (n.value !== undefined && n.value !== '')
|
|
246
|
+
line += ': ' + q(n.value);
|
|
247
|
+
if (n.children?.length)
|
|
248
|
+
lines.push(line + ':', formatSnapshot(n.children, indent + ' '));
|
|
249
|
+
else
|
|
250
|
+
lines.push(line);
|
|
251
|
+
}
|
|
252
|
+
return lines.join('\n');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* sygnal/agent-ui: a minimal Model Context Protocol server (tools only), as sygnal-check's
|
|
257
|
+
* (sygnal-check/src/mcp.js) and sygnal/vite's dev endpoint (src/vite/mcp.ts): hand-rolled, no
|
|
258
|
+
* dependency, both protocol eras answered per request.
|
|
259
|
+
*
|
|
260
|
+
* - legacy, the `initialize` revisions (2024-11-05 … 2025-11-25): initialize, ping, tools/list,
|
|
261
|
+
* tools/call; JSON-RPC batches answered. tools.listChanged is true: the tool set can change
|
|
262
|
+
* when extra tools (sygnal/ai agentTools) do, and notifications/tools/list_changed is sent.
|
|
263
|
+
* - modern, the stateless 2026-07-28 revision (`_meta['io.modelcontextprotocol/protocolVersion']`
|
|
264
|
+
* with client capabilities): server/discover, tools/list, tools/call; results carry
|
|
265
|
+
* `resultType: 'complete'` and the server info in `_meta`.
|
|
266
|
+
* Transport: newline-delimited JSON over any stream pair with `on('data')` and `write()` (Node's
|
|
267
|
+
* stdin / stdout); messages are handled one at a time, in order.
|
|
268
|
+
*/
|
|
269
|
+
const MODERN_VERSIONS = ['2026-07-28'];
|
|
270
|
+
const LEGACY_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
|
|
271
|
+
const SUPPORTED = [...MODERN_VERSIONS, ...LEGACY_VERSIONS];
|
|
272
|
+
const META = 'io.modelcontextprotocol/';
|
|
273
|
+
const isObj = (v) => !!v && typeof v == 'object' && !Array.isArray(v);
|
|
274
|
+
function createMcpHandler(spec) {
|
|
275
|
+
const serverInfo = { name: spec.name, version: spec.version };
|
|
276
|
+
const result = (id, value) => ({ jsonrpc: '2.0', id, result: value });
|
|
277
|
+
const error = (id, code, message, data) => ({ jsonrpc: '2.0', id, error: { code, message, ...(data !== undefined ? { data } : {}) } });
|
|
278
|
+
async function route(id, method, params, modern) {
|
|
279
|
+
if (modern && (method == 'initialize' || method == 'ping'))
|
|
280
|
+
return error(id, -32601, `Method not found: ${method} is not in 2026-07-28`);
|
|
281
|
+
switch (method) {
|
|
282
|
+
case 'initialize':
|
|
283
|
+
return result(id, {
|
|
284
|
+
protocolVersion: LEGACY_VERSIONS.includes(params.protocolVersion) ? params.protocolVersion : LEGACY_VERSIONS[0],
|
|
285
|
+
capabilities: { tools: { listChanged: true } },
|
|
286
|
+
serverInfo,
|
|
287
|
+
...(spec.instructions ? { instructions: spec.instructions } : {}),
|
|
288
|
+
});
|
|
289
|
+
case 'server/discover':
|
|
290
|
+
if (!modern)
|
|
291
|
+
return error(id, -32602, 'Invalid params: server/discover is a 2026-07-28 request');
|
|
292
|
+
return result(id, { supportedVersions: SUPPORTED, capabilities: { tools: {} }, ...(spec.instructions ? { instructions: spec.instructions } : {}) });
|
|
293
|
+
case 'ping':
|
|
294
|
+
return result(id, {});
|
|
295
|
+
case 'tools/list':
|
|
296
|
+
return result(id, { tools: spec.tools() });
|
|
297
|
+
case 'tools/call': {
|
|
298
|
+
const name = params.name;
|
|
299
|
+
if (!spec.tools().some(t => t.name == name))
|
|
300
|
+
return error(id, -32602, `Unknown tool: ${name}`);
|
|
301
|
+
try {
|
|
302
|
+
const r = await spec.call(name, isObj(params.arguments) ? params.arguments : {});
|
|
303
|
+
return result(id, { content: [{ type: 'text', text: r.text }], ...(r.value ? { structuredContent: r.value } : {}), isError: !!r.isError });
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
return result(id, { content: [{ type: 'text', text: `${name} failed: ${e?.message || e}` }], isError: true });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
default:
|
|
310
|
+
return error(id, -32601, `Method not found: ${method}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/** one message → its response (null for a notification); never throws */
|
|
314
|
+
async function handle(msg, inBatch = false) {
|
|
315
|
+
try {
|
|
316
|
+
if (!isObj(msg) || msg.jsonrpc !== '2.0' || typeof msg.method != 'string') {
|
|
317
|
+
return isObj(msg) && 'id' in msg && !('result' in msg || 'error' in msg) ? error(msg.id ?? null, -32600, 'Invalid Request') : null;
|
|
318
|
+
}
|
|
319
|
+
if (!('id' in msg) || msg.id === null)
|
|
320
|
+
return null;
|
|
321
|
+
const { id, method } = msg;
|
|
322
|
+
const params = isObj(msg.params) ? msg.params : {};
|
|
323
|
+
const meta = isObj(params._meta) ? params._meta : {};
|
|
324
|
+
let modern = false;
|
|
325
|
+
if (META + 'protocolVersion' in meta) {
|
|
326
|
+
const v = meta[META + 'protocolVersion'];
|
|
327
|
+
if (!SUPPORTED.includes(v))
|
|
328
|
+
return error(id, -32022, 'Unsupported protocol version', { supported: SUPPORTED, requested: v });
|
|
329
|
+
if (MODERN_VERSIONS.includes(v)) {
|
|
330
|
+
if (!isObj(meta[META + 'clientCapabilities']))
|
|
331
|
+
return error(id, -32602, `Invalid params: _meta["${META}clientCapabilities"] is required`);
|
|
332
|
+
if (inBatch)
|
|
333
|
+
return error(id, -32600, 'Invalid Request: 2026-07-28 messages are sent one per line, not in a batch');
|
|
334
|
+
modern = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const out = await route(id, method, params, modern);
|
|
338
|
+
if (modern && isObj(out.result))
|
|
339
|
+
out.result = { resultType: 'complete', ...out.result, _meta: { ...(out.result._meta || {}), [META + 'serverInfo']: serverInfo } };
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
catch (e) {
|
|
343
|
+
return error(isObj(msg) ? msg.id ?? null : null, -32603, `Internal error: ${e?.message || e}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return { handle };
|
|
347
|
+
}
|
|
348
|
+
/** serve over a newline-delimited JSON stream pair (default: process.stdin / process.stdout) */
|
|
349
|
+
function serveMcpStream(spec, input, output) {
|
|
350
|
+
const proc = globalThis.process;
|
|
351
|
+
input || (input = proc?.stdin);
|
|
352
|
+
output || (output = proc?.stdout);
|
|
353
|
+
const { handle } = createMcpHandler(spec);
|
|
354
|
+
const send = (m) => { if (m && !(Array.isArray(m) && !m.length))
|
|
355
|
+
output.write(JSON.stringify(m) + '\n'); };
|
|
356
|
+
let buf = '', queue = Promise.resolve(), end;
|
|
357
|
+
const done = new Promise(r => end = r);
|
|
358
|
+
const line = (l) => {
|
|
359
|
+
if (!l.trim())
|
|
360
|
+
return;
|
|
361
|
+
queue = queue.then(async () => {
|
|
362
|
+
let msg;
|
|
363
|
+
try {
|
|
364
|
+
msg = JSON.parse(l);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
368
|
+
}
|
|
369
|
+
if (Array.isArray(msg))
|
|
370
|
+
send((await Promise.all(msg.map(m => handle(m, true)))).filter(Boolean));
|
|
371
|
+
else
|
|
372
|
+
send(await handle(msg));
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
const onData = (chunk) => {
|
|
376
|
+
buf += String(chunk);
|
|
377
|
+
let i;
|
|
378
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
379
|
+
line(buf.slice(0, i));
|
|
380
|
+
buf = buf.slice(i + 1);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
const onEnd = () => { if (buf)
|
|
384
|
+
line(buf); buf = ''; queue.then(end); };
|
|
385
|
+
input.setEncoding?.('utf8');
|
|
386
|
+
input.on('data', onData);
|
|
387
|
+
input.on('end', onEnd);
|
|
388
|
+
input.resume?.();
|
|
389
|
+
return {
|
|
390
|
+
listChanged: () => send({ jsonrpc: '2.0', method: 'notifications/tools/list_changed' }),
|
|
391
|
+
stop: () => { input.off?.('data', onData); input.off?.('end', onEnd); end(); },
|
|
392
|
+
done,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/*
|
|
397
|
+
* 'sygnal/agent-ui': a Sygnal app rendered for an LLM agent. The app renders into a sygnal/host
|
|
398
|
+
* tree; this target's "screen" is an accessibility snapshot (roles, names, states, values, refs:
|
|
399
|
+
* the format browser agents read), and its "input" is tool calls: ui_click, ui_type, ui_press,
|
|
400
|
+
* ui_select, ui_check, which become the same DOM events a user's input makes. The app's views,
|
|
401
|
+
* intents and models don't change: an agent operates the app the way a person does, without a
|
|
402
|
+
* browser, deterministically, and in a test.
|
|
403
|
+
*
|
|
404
|
+
* const {app, ui} = runAgentUI(App)
|
|
405
|
+
* ui.snapshot() // what the agent sees
|
|
406
|
+
* await ui.call('ui_click', { ref: 'e12' }) // → { ok: true, snapshot }
|
|
407
|
+
* serveMcp(ui, { name: 'my-app' }) // the same tools over MCP (stdio)
|
|
408
|
+
*
|
|
409
|
+
* With sygnal/ai: `agentTools(app)` (the semantic tools of the app's `agent` declarations) can be
|
|
410
|
+
* served alongside (`serveMcp(ui, { tools: agentTools(app) })`); `ui.chatTools()` gives the tools
|
|
411
|
+
* in the chat driver's format. Separate entry: './index' and './host' become the external
|
|
412
|
+
* 'sygnal' and 'sygnal/host'.
|
|
413
|
+
*/
|
|
414
|
+
const REF = { type: 'string', description: 'The element\'s ref from the snapshot, like "e12"' };
|
|
415
|
+
const TOOLS = [
|
|
416
|
+
{ name: 'ui_snapshot', description: 'Read the app\'s current screen: an accessibility snapshot with a ref on each element you can act on. [on=…] lists events the app handles on that element.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, annotations: { readOnlyHint: true } },
|
|
417
|
+
{ name: 'ui_click', description: 'Click an element (a button, link, checkbox, or an element with [on=click]). double: true for a double click ([on=dblclick]).', inputSchema: { type: 'object', properties: { ref: REF, double: { type: 'boolean', description: 'A double click' } }, required: ['ref'], additionalProperties: false }, annotations: {} },
|
|
418
|
+
{ name: 'ui_type', description: 'Type into a text field. Replaces its value unless append is true; submit: true presses Enter afterwards (submits its form).', inputSchema: { type: 'object', properties: { ref: REF, text: { type: 'string' }, append: { type: 'boolean' }, submit: { type: 'boolean' } }, required: ['ref', 'text'], additionalProperties: false }, annotations: {} },
|
|
419
|
+
{ name: 'ui_press', description: 'Press a key ("Enter", "Escape", "Tab", "ArrowDown", "a", modifiers as "Control+s") on an element, or on the focused element / the whole app without a ref.', inputSchema: { type: 'object', properties: { key: { type: 'string' }, ref: REF }, required: ['key'], additionalProperties: false }, annotations: {} },
|
|
420
|
+
{ name: 'ui_select', description: 'Choose an option of a combobox (<select>) by its label or value.', inputSchema: { type: 'object', properties: { ref: REF, option: { type: 'string' } }, required: ['ref', 'option'], additionalProperties: false }, annotations: {} },
|
|
421
|
+
{ name: 'ui_check', description: 'Set a checkbox or radio to checked (true) or unchecked (false); does nothing when it already is.', inputSchema: { type: 'object', properties: { ref: REF, checked: { type: 'boolean' } }, required: ['ref', 'checked'], additionalProperties: false }, annotations: {} },
|
|
422
|
+
];
|
|
423
|
+
function createAgentUI(options = {}) {
|
|
424
|
+
const doc = options.document || new HostDocument();
|
|
425
|
+
const rules = options.styles || [];
|
|
426
|
+
const ua = { onNavigate: options.onNavigate };
|
|
427
|
+
const quiet = options.quietMs ?? 20, maxWait = options.maxWaitMs ?? 2000;
|
|
428
|
+
const listeners = new Set();
|
|
429
|
+
let changes = 0, runtime = null;
|
|
430
|
+
doc.onChange = () => { changes++; for (const l of [...listeners])
|
|
431
|
+
l(); };
|
|
432
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
433
|
+
const find = (ref) => {
|
|
434
|
+
const id = typeof ref == 'string' && /^e(\d+)$/.exec(ref.trim());
|
|
435
|
+
if (!id)
|
|
436
|
+
return null;
|
|
437
|
+
return [doc.body, ...doc.body.querySelectorAll('*')].find(el => nodeId(el) == +id[1]) || null;
|
|
438
|
+
};
|
|
439
|
+
const target = (ref) => {
|
|
440
|
+
const el = find(ref);
|
|
441
|
+
if (!el)
|
|
442
|
+
throw new Error(`no element with ref ${JSON.stringify(ref)}: take a new ui_snapshot (refs of elements that left the screen are gone)`);
|
|
443
|
+
if (!el.isConnected)
|
|
444
|
+
throw new Error(`ref ${ref} is no longer on the screen`);
|
|
445
|
+
if (!isInteractive(el, rules) && !/^(input|textarea|select)$/.test(el.localName) && !isTextField(el) && !isCheckable(el))
|
|
446
|
+
throw new Error(`ref ${ref} is not interactive (no ref in the snapshot)`);
|
|
447
|
+
if (el.disabled)
|
|
448
|
+
throw new Error(`ref ${ref} is disabled`);
|
|
449
|
+
return el;
|
|
450
|
+
};
|
|
451
|
+
const parseKey = (k) => {
|
|
452
|
+
const parts = k.split('+').filter(Boolean);
|
|
453
|
+
const key = parts.pop() || k;
|
|
454
|
+
const m = new Set(parts.map(p => p.toLowerCase()));
|
|
455
|
+
return { key: key == 'Space' ? ' ' : key, init: { ctrlKey: m.has('control') || m.has('ctrl'), shiftKey: m.has('shift'), altKey: m.has('alt'), metaKey: m.has('meta') || m.has('cmd') } };
|
|
456
|
+
};
|
|
457
|
+
const ui = {
|
|
458
|
+
doc,
|
|
459
|
+
driver: makeHostDriver(doc),
|
|
460
|
+
tree: () => snapshotTree(doc, { rules }),
|
|
461
|
+
snapshot: () => formatSnapshot(ui.tree()) || '(the screen is empty)',
|
|
462
|
+
element: find,
|
|
463
|
+
tools: () => TOOLS.map(t => ({ ...t })),
|
|
464
|
+
chatTools: () => Object.fromEntries(TOOLS.map(t => [t.name, { description: t.description, inputSchema: t.inputSchema }])),
|
|
465
|
+
attach(app) { runtime = app?.__runtime || app || null; },
|
|
466
|
+
subscribe(l) { listeners.add(l); return () => { listeners.delete(l); }; },
|
|
467
|
+
async settle() {
|
|
468
|
+
const end = Date.now() + maxWait;
|
|
469
|
+
for (;;) {
|
|
470
|
+
const n = changes;
|
|
471
|
+
if (runtime?.flushed)
|
|
472
|
+
await runtime.flushed();
|
|
473
|
+
await sleep(quiet);
|
|
474
|
+
if (changes === n || Date.now() > end)
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
async call(name, args = {}) {
|
|
479
|
+
try {
|
|
480
|
+
switch (name) {
|
|
481
|
+
case 'ui_snapshot': break;
|
|
482
|
+
case 'ui_click': {
|
|
483
|
+
const el = target(args.ref);
|
|
484
|
+
args.double ? doubleClick(el, ua) : (el.focus(), activate(el, ua));
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
case 'ui_type': {
|
|
488
|
+
const el = target(args.ref);
|
|
489
|
+
if (typeof args.text != 'string')
|
|
490
|
+
throw new Error('text must be a string');
|
|
491
|
+
if (!isTextField(el))
|
|
492
|
+
throw new Error(`ref ${args.ref} is not a text field`);
|
|
493
|
+
args.append ? typeText(el, args.text, ua) : fill(el, args.text);
|
|
494
|
+
if (args.submit)
|
|
495
|
+
pressKey(el, 'Enter', {}, ua);
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
case 'ui_press': {
|
|
499
|
+
if (typeof args.key != 'string' || !args.key)
|
|
500
|
+
throw new Error('key must be a key name, like "Enter"');
|
|
501
|
+
const el = args.ref != null ? target(args.ref) : doc.activeElement || doc.body;
|
|
502
|
+
if (args.ref != null)
|
|
503
|
+
el.focus();
|
|
504
|
+
const { key, init } = parseKey(args.key);
|
|
505
|
+
pressKey(el, key, init, ua);
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
case 'ui_select': {
|
|
509
|
+
const el = target(args.ref);
|
|
510
|
+
if (el.localName != 'select')
|
|
511
|
+
throw new Error(`ref ${args.ref} is not a combobox`);
|
|
512
|
+
el.focus();
|
|
513
|
+
if (!chooseOption(el, String(args.option)))
|
|
514
|
+
throw new Error(`no option ${JSON.stringify(args.option)}; options: ${el.options.map(o => JSON.stringify(o.textContent.trim())).join(', ')}`);
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
case 'ui_check': {
|
|
518
|
+
const el = target(args.ref);
|
|
519
|
+
if (!isCheckable(el))
|
|
520
|
+
throw new Error(`ref ${args.ref} is not a checkbox, switch or radio`);
|
|
521
|
+
if (el.checked !== !!args.checked) {
|
|
522
|
+
el.focus();
|
|
523
|
+
activate(el, ua);
|
|
524
|
+
}
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
default:
|
|
528
|
+
throw new Error(`unknown tool ${name}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
catch (e) {
|
|
532
|
+
await ui.settle();
|
|
533
|
+
return { ok: false, error: e?.message || String(e), snapshot: ui.snapshot() };
|
|
534
|
+
}
|
|
535
|
+
await ui.settle();
|
|
536
|
+
return { ok: true, snapshot: ui.snapshot() };
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
return ui;
|
|
540
|
+
}
|
|
541
|
+
/** run an app for an agent: `{ app, ui }`, the ui attached to the app's flushes */
|
|
542
|
+
function runAgentUI(App, options = {}) {
|
|
543
|
+
const ui = createAgentUI(options);
|
|
544
|
+
const app = run(App, { ...options.drivers, DOM: ui.driver }, options.runOptions);
|
|
545
|
+
ui.attach(app);
|
|
546
|
+
return { app, ui };
|
|
547
|
+
}
|
|
548
|
+
const INSTRUCTIONS = 'This server is an app\'s user interface. Call ui_snapshot to see the screen, then act on elements by their ref (ui_click, ui_type, ui_press, ui_select, ui_check); every action answers with the new snapshot. Refs change when elements are re-created: use the latest snapshot.';
|
|
549
|
+
/** the agent UI's tools (and any extra tools) as an MCP server on a stream pair (default stdio) */
|
|
550
|
+
function serveMcp(ui, options = {}) {
|
|
551
|
+
const extra = options.tools;
|
|
552
|
+
const server = serveMcpStream({
|
|
553
|
+
name: options.name || 'sygnal-agent-ui',
|
|
554
|
+
version: options.version || '0.0.0',
|
|
555
|
+
instructions: options.instructions ?? INSTRUCTIONS,
|
|
556
|
+
tools: () => [...ui.tools(), ...(extra ? extra.list().map(t => ({ name: t.name, description: t.description, inputSchema: t.inputSchema, annotations: t.annotations })) : [])],
|
|
557
|
+
async call(name, args) {
|
|
558
|
+
if (name.startsWith('ui_')) {
|
|
559
|
+
const r = await ui.call(name, args);
|
|
560
|
+
return r.ok ? { text: r.snapshot, value: r } : { text: `Error: ${r.error}\n\n${r.snapshot ?? ''}`, value: r, isError: true };
|
|
561
|
+
}
|
|
562
|
+
const r = await extra.call(name, args);
|
|
563
|
+
await ui.settle();
|
|
564
|
+
return { text: JSON.stringify(r), value: r && typeof r == 'object' ? r : { result: r }, isError: r?.ok === false };
|
|
565
|
+
},
|
|
566
|
+
}, options.input, options.output);
|
|
567
|
+
// only a change of the tool set is announced (agentTools' listener also fires for context changes)
|
|
568
|
+
let names = extra ? extra.list().map(t => t.name).join() : '';
|
|
569
|
+
const off = extra?.subscribe?.(() => {
|
|
570
|
+
const now = extra.list().map(t => t.name).join();
|
|
571
|
+
if (now !== names) {
|
|
572
|
+
names = now;
|
|
573
|
+
server.listChanged();
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
const stop = server.stop;
|
|
577
|
+
server.stop = () => { off?.(); stop(); };
|
|
578
|
+
return server;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export { createAgentUI, createMcpHandler, formatSnapshot, nameOf, roleOf, runAgentUI, serveMcp, serveMcpStream, snapshotTree };
|
|
582
|
+
//# sourceMappingURL=agent-ui.esm.js.map
|