@ng-forge/dynamic-forms 1.3.0-next.1 → 1.3.0-next.3
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/fesm2022/ng-forge-dynamic-forms-active-page-initialized.event-CiKN-mcc.mjs +12 -0
- package/fesm2022/ng-forge-dynamic-forms-active-page-initialized.event-CiKN-mcc.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-Dv9mPnJ3.mjs +770 -0
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-Dv9mPnJ3.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-array-item-registry.service-D-AxVLdk.mjs +83 -0
- package/fesm2022/ng-forge-dynamic-forms-array-item-registry.service-D-AxVLdk.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-EuYCbquo.mjs +1887 -0
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-EuYCbquo.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-web-mcp-2wQK_wPp.mjs +954 -0
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-web-mcp-2wQK_wPp.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-container-field-processors-C_JRvaVt.mjs +117 -0
- package/fesm2022/ng-forge-dynamic-forms-container-field-processors-C_JRvaVt.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-Bg7Win9n.mjs +228 -0
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-Bg7Win9n.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-R2yc-H0T.mjs +102 -0
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-R2yc-H0T.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-debounce-CG3uP5B-.mjs +4 -0
- package/fesm2022/{ng-forge-dynamic-forms-debounce-C9F4v1Ke.mjs.map → ng-forge-dynamic-forms-debounce-CG3uP5B-.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-default-value-D5M4MNE-.mjs +77 -0
- package/fesm2022/ng-forge-dynamic-forms-default-value-D5M4MNE-.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-dev-mode-4rbYBBo_.mjs +4 -0
- package/fesm2022/ng-forge-dynamic-forms-dev-mode-4rbYBBo_.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-df-field-outlet.directive-CQsDMV-s.mjs +644 -0
- package/fesm2022/ng-forge-dynamic-forms-df-field-outlet.directive-CQsDMV-s.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-field-errors-wrapper.component-DKgkpIpG.mjs +56 -0
- package/fesm2022/ng-forge-dynamic-forms-field-errors-wrapper.component-DKgkpIpG.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-form-schema.service-Dd-gHJ_7.mjs +38 -0
- package/fesm2022/{ng-forge-dynamic-forms-form-schema.service-Dx5Dlb15.mjs.map → ng-forge-dynamic-forms-form-schema.service-Dd-gHJ_7.mjs.map} +1 -1
- package/fesm2022/ng-forge-dynamic-forms-form-state-manager-DcOXRFRL.mjs +1522 -0
- package/fesm2022/ng-forge-dynamic-forms-form-state-manager-DcOXRFRL.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-BjZi0bhq.mjs +292 -0
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-BjZi0bhq.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs +1318 -843
- package/fesm2022/ng-forge-dynamic-forms-integration.mjs.map +1 -1
- package/fesm2022/ng-forge-dynamic-forms-internal.mjs +3326 -3641
- package/fesm2022/ng-forge-dynamic-forms-internal.mjs.map +1 -1
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-BRjQmWC9.mjs +295 -0
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-BRjQmWC9.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-resolve-field-ByGa5cpo.mjs +329 -0
- package/fesm2022/ng-forge-dynamic-forms-resolve-field-ByGa5cpo.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CuFycQb-.mjs +81 -0
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-CuFycQb-.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-schema-builder-C62afItJ.mjs +400 -0
- package/fesm2022/ng-forge-dynamic-forms-schema-builder-C62afItJ.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-schema-registry.service-axBV9FBH.mjs +45 -0
- package/fesm2022/ng-forge-dynamic-forms-schema-registry.service-axBV9FBH.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-schema.mjs +8 -14
- package/fesm2022/ng-forge-dynamic-forms-schema.mjs.map +1 -1
- package/fesm2022/ng-forge-dynamic-forms-testing.mjs +68 -73
- package/fesm2022/ng-forge-dynamic-forms-testing.mjs.map +1 -1
- package/fesm2022/ng-forge-dynamic-forms-text-field.component--MAh_h4Q.mjs +257 -0
- package/fesm2022/ng-forge-dynamic-forms-text-field.component--MAh_h4Q.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-validation-execution.token-BFyySKtp.mjs +8 -0
- package/fesm2022/ng-forge-dynamic-forms-validation-execution.token-BFyySKtp.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-with-logger-config-BAATqYY0.mjs +94 -0
- package/fesm2022/ng-forge-dynamic-forms-with-logger-config-BAATqYY0.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms-wrapper-chain-BnntfNU1.mjs +115 -0
- package/fesm2022/ng-forge-dynamic-forms-wrapper-chain-BnntfNU1.mjs.map +1 -0
- package/fesm2022/ng-forge-dynamic-forms.mjs +2136 -3
- package/fesm2022/ng-forge-dynamic-forms.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ng-forge-dynamic-forms-integration.d.ts +543 -415
- package/types/ng-forge-dynamic-forms-internal.d.ts +2319 -2436
- package/types/ng-forge-dynamic-forms-schema.d.ts +13 -16
- package/types/ng-forge-dynamic-forms-testing.d.ts +34 -37
- package/types/ng-forge-dynamic-forms.d.ts +735 -722
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-CSCKq_1K.mjs +0 -750
- package/fesm2022/ng-forge-dynamic-forms-array-field.component-CSCKq_1K.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-CTOTwzzw.mjs +0 -2394
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-CTOTwzzw.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-web-mcp-BtjqeJOj.mjs +0 -1054
- package/fesm2022/ng-forge-dynamic-forms-bootstrap-web-mcp-BtjqeJOj.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-Dc3ROhHL.mjs +0 -86
- package/fesm2022/ng-forge-dynamic-forms-container-field.component-Dc3ROhHL.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-BzgX8lAD.mjs +0 -33
- package/fesm2022/ng-forge-dynamic-forms-css-wrapper.component-BzgX8lAD.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-debounce-C9F4v1Ke.mjs +0 -17
- package/fesm2022/ng-forge-dynamic-forms-field-errors-wrapper.component-CIKbWqYA.mjs +0 -25
- package/fesm2022/ng-forge-dynamic-forms-field-errors-wrapper.component-CIKbWqYA.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-form-schema.service-Dx5Dlb15.mjs +0 -24
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-DAC3P9DC.mjs +0 -177
- package/fesm2022/ng-forge-dynamic-forms-group-field.component-DAC3P9DC.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CXQe_5td.mjs +0 -5133
- package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CXQe_5td.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-DPKJcwUO.mjs +0 -176
- package/fesm2022/ng-forge-dynamic-forms-page-field.component-DPKJcwUO.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-DMEeKZny.mjs +0 -19
- package/fesm2022/ng-forge-dynamic-forms-row-wrapper.component-DMEeKZny.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-schema-builder-BvDTzfdM.mjs +0 -570
- package/fesm2022/ng-forge-dynamic-forms-schema-builder-BvDTzfdM.mjs.map +0 -1
- package/fesm2022/ng-forge-dynamic-forms-text-field.component-BB-jutBq.mjs +0 -126
- package/fesm2022/ng-forge-dynamic-forms-text-field.component-BB-jutBq.mjs.map +0 -1
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
import { n as FormStateManager, s as FormSubmitEvent } from "./ng-forge-dynamic-forms-form-state-manager-DcOXRFRL.mjs";
|
|
2
|
+
import { n as getNormalizedArrayMetadata } from "./ng-forge-dynamic-forms-default-value-D5M4MNE-.mjs";
|
|
3
|
+
import { inject, untracked } from "@angular/core";
|
|
4
|
+
import { DynamicFormLogger, EventBus, getFieldValueHandling, interpolateParams } from "@ng-forge/dynamic-forms/internal";
|
|
5
|
+
function createPendingSubmission() {
|
|
6
|
+
let settled = false;
|
|
7
|
+
let accepted = false;
|
|
8
|
+
let resolve;
|
|
9
|
+
const outcome = new Promise((resolvePromise) => {
|
|
10
|
+
resolve = resolvePromise;
|
|
11
|
+
});
|
|
12
|
+
const settle = (result) => {
|
|
13
|
+
if (settled) return;
|
|
14
|
+
settled = true;
|
|
15
|
+
resolve(result);
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
reply: {
|
|
19
|
+
accept: () => {
|
|
20
|
+
accepted = true;
|
|
21
|
+
},
|
|
22
|
+
settle
|
|
23
|
+
},
|
|
24
|
+
accepted: () => accepted,
|
|
25
|
+
outcome,
|
|
26
|
+
cancel: () => settle({ status: "cancelled" })
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function buildToolSchema(plan) {
|
|
30
|
+
return {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: buildProperties(plan),
|
|
33
|
+
additionalProperties: false
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function buildProperties(nodes) {
|
|
37
|
+
const properties = {};
|
|
38
|
+
for (const node of nodes) {
|
|
39
|
+
if (!node.policy.writable) continue;
|
|
40
|
+
const schema = buildNodeSchema(node);
|
|
41
|
+
if (schema) properties[node.key] = schema;
|
|
42
|
+
}
|
|
43
|
+
return properties;
|
|
44
|
+
}
|
|
45
|
+
function buildNodeSchema(node) {
|
|
46
|
+
switch (node.kind) {
|
|
47
|
+
case "group": return annotate(node, {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: buildProperties(node.children),
|
|
50
|
+
additionalProperties: false
|
|
51
|
+
});
|
|
52
|
+
case "array": {
|
|
53
|
+
if (!node.item) return void 0;
|
|
54
|
+
const schema = {
|
|
55
|
+
type: "array",
|
|
56
|
+
items: buildItemSchema(node.item)
|
|
57
|
+
};
|
|
58
|
+
if (node.minItems !== void 0) schema.minItems = node.minItems;
|
|
59
|
+
if (node.maxItems !== void 0) schema.maxItems = node.maxItems;
|
|
60
|
+
return annotate(node, schema);
|
|
61
|
+
}
|
|
62
|
+
case "scalar": return annotate(node, buildScalarSchema(node));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function buildItemSchema(item) {
|
|
66
|
+
if (item.kind === "object") return {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: buildProperties(item.children),
|
|
69
|
+
additionalProperties: false
|
|
70
|
+
};
|
|
71
|
+
return buildScalarSchema(item.value);
|
|
72
|
+
}
|
|
73
|
+
function buildScalarSchema(plan) {
|
|
74
|
+
const base = plan.options ? optionSchema(plan) : { type: singleOrUnion(plan.types) };
|
|
75
|
+
if (plan.multiple) return {
|
|
76
|
+
type: "array",
|
|
77
|
+
items: base
|
|
78
|
+
};
|
|
79
|
+
const withConstraints = { ...base };
|
|
80
|
+
const { minLength, maxLength, minimum, maximum, pattern, format } = plan.constraints;
|
|
81
|
+
if (minLength !== void 0) withConstraints.minLength = minLength;
|
|
82
|
+
if (maxLength !== void 0) withConstraints.maxLength = maxLength;
|
|
83
|
+
if (minimum !== void 0) withConstraints.minimum = minimum;
|
|
84
|
+
if (maximum !== void 0) withConstraints.maximum = maximum;
|
|
85
|
+
if (pattern !== void 0) withConstraints.pattern = pattern;
|
|
86
|
+
if (format !== void 0) withConstraints.format = format;
|
|
87
|
+
return withConstraints;
|
|
88
|
+
}
|
|
89
|
+
function optionSchema(plan) {
|
|
90
|
+
const options = plan.options ?? [];
|
|
91
|
+
const values = options.map((option) => option.value);
|
|
92
|
+
const nullable = !plan.multiple && plan.types.includes("null");
|
|
93
|
+
const schema = {
|
|
94
|
+
type: singleOrUnion(plan.types),
|
|
95
|
+
enum: nullable ? [...values, null] : values
|
|
96
|
+
};
|
|
97
|
+
const branches = options.map((option) => option.title ? {
|
|
98
|
+
const: option.value,
|
|
99
|
+
title: option.title
|
|
100
|
+
} : { const: option.value });
|
|
101
|
+
if (nullable) branches.push({ const: null });
|
|
102
|
+
if (options.some((option) => option.title)) schema.anyOf = branches;
|
|
103
|
+
return schema;
|
|
104
|
+
}
|
|
105
|
+
function singleOrUnion(types) {
|
|
106
|
+
return types.length === 1 ? types[0] : types;
|
|
107
|
+
}
|
|
108
|
+
function annotate(node, schema) {
|
|
109
|
+
const annotated = { ...schema };
|
|
110
|
+
if (node.label) annotated.title = node.label;
|
|
111
|
+
if (node.description) annotated.description = node.description;
|
|
112
|
+
if (node.kind === "scalar" && node.default !== void 0) annotated.default = node.default;
|
|
113
|
+
return annotated;
|
|
114
|
+
}
|
|
115
|
+
function collectFieldReports(plan, tree) {
|
|
116
|
+
const walkResult = {
|
|
117
|
+
reports: [],
|
|
118
|
+
paths: /* @__PURE__ */ new Map(),
|
|
119
|
+
messages: /* @__PURE__ */ new Map(),
|
|
120
|
+
state: /* @__PURE__ */ new Map()
|
|
121
|
+
};
|
|
122
|
+
walk(plan, tree, walkResult);
|
|
123
|
+
return walkResult;
|
|
124
|
+
}
|
|
125
|
+
function walk(nodes, tree, out) {
|
|
126
|
+
const record = tree;
|
|
127
|
+
for (const node of nodes) {
|
|
128
|
+
const subtree = record[node.key];
|
|
129
|
+
if (typeof subtree !== "function") continue;
|
|
130
|
+
const state = subtree();
|
|
131
|
+
out.paths.set(subtree, node.path);
|
|
132
|
+
if (node.messages) out.messages.set(subtree, node.messages);
|
|
133
|
+
out.state.set(node.path, {
|
|
134
|
+
disabled: state.disabled(),
|
|
135
|
+
readonly: state.readonly(),
|
|
136
|
+
hidden: state.hidden()
|
|
137
|
+
});
|
|
138
|
+
out.reports.push({
|
|
139
|
+
path: node.path,
|
|
140
|
+
label: node.label,
|
|
141
|
+
applicable: !state.hidden() && !state.disabled(),
|
|
142
|
+
required: state.required(),
|
|
143
|
+
writable: node.policy.writable,
|
|
144
|
+
readable: node.policy.readable,
|
|
145
|
+
filled: hasValue(state.value()),
|
|
146
|
+
options: node.kind === "scalar" ? node.options : void 0
|
|
147
|
+
});
|
|
148
|
+
if (node.kind === "group") {
|
|
149
|
+
walk(node.children, subtree, out);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (node.kind === "array") walkArray(node, subtree, out);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function walkArray(node, subtree, out) {
|
|
156
|
+
if (node.item?.kind !== "object") return;
|
|
157
|
+
const value = subtree().value();
|
|
158
|
+
if (!Array.isArray(value)) return;
|
|
159
|
+
const indexed = subtree;
|
|
160
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
161
|
+
const itemTree = indexed[index];
|
|
162
|
+
if (typeof itemTree !== "function") continue;
|
|
163
|
+
walk(rebase(node.item.children, `${node.path}[${index}]`), itemTree, out);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function rebase(nodes, prefix) {
|
|
167
|
+
return nodes.map((node) => {
|
|
168
|
+
const path = `${prefix}.${node.key}`;
|
|
169
|
+
if (node.kind === "group") return {
|
|
170
|
+
...node,
|
|
171
|
+
path,
|
|
172
|
+
children: rebase(node.children, path)
|
|
173
|
+
};
|
|
174
|
+
if (node.kind === "array" && node.item?.kind === "object") return {
|
|
175
|
+
...node,
|
|
176
|
+
path,
|
|
177
|
+
item: {
|
|
178
|
+
kind: "object",
|
|
179
|
+
children: rebase(node.item.children, `${path}[]`)
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
...node,
|
|
184
|
+
path
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function hasValue(value) {
|
|
189
|
+
if (value === null || value === void 0 || value === "") return false;
|
|
190
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
191
|
+
if (typeof value === "number") return !Number.isNaN(value);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
function buildFieldPlan(fields, registry, warn) {
|
|
195
|
+
const nodes = [];
|
|
196
|
+
collectInto(nodes, fields, registry, "", warn);
|
|
197
|
+
return nodes;
|
|
198
|
+
}
|
|
199
|
+
function collectInto(out, fields, registry, prefix, warn) {
|
|
200
|
+
for (const field of fields) {
|
|
201
|
+
const valueHandling = getFieldValueHandling(field.type, registry);
|
|
202
|
+
if (valueHandling === "exclude") continue;
|
|
203
|
+
const candidate = field;
|
|
204
|
+
if (valueHandling === "flatten") {
|
|
205
|
+
const children = asFieldArray(candidate.fields);
|
|
206
|
+
if (children) collectInto(out, children, registry, prefix, warn);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (!candidate.key) continue;
|
|
210
|
+
const node = buildNode(candidate, registry, prefix, warn);
|
|
211
|
+
if (node) out.push(node);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function buildNode(field, registry, prefix, warn) {
|
|
215
|
+
const key = field.key;
|
|
216
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
217
|
+
const base = {
|
|
218
|
+
key,
|
|
219
|
+
path,
|
|
220
|
+
label: staticText(field.label),
|
|
221
|
+
description: staticText(field.placeholder) ?? staticText(field.props?.placeholder) ?? staticText(field.props?.hint),
|
|
222
|
+
policy: resolvePolicy(field),
|
|
223
|
+
messages: field.validationMessages
|
|
224
|
+
};
|
|
225
|
+
if (field.type === "group") {
|
|
226
|
+
const children = [];
|
|
227
|
+
collectInto(children, asFieldArray(field.fields) ?? [], registry, path, warn);
|
|
228
|
+
return {
|
|
229
|
+
...base,
|
|
230
|
+
kind: "group",
|
|
231
|
+
children
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (field.type === "array") return buildArrayNode(field, base, registry, warn);
|
|
235
|
+
return buildScalar(field, base, registry);
|
|
236
|
+
}
|
|
237
|
+
function buildArrayNode(field, base, registry, warn) {
|
|
238
|
+
const plan = {
|
|
239
|
+
...base,
|
|
240
|
+
kind: "array",
|
|
241
|
+
item: buildItemPlan(field, base.path, registry, warn)
|
|
242
|
+
};
|
|
243
|
+
if (typeof field.minLength === "number") plan.minItems = field.minLength;
|
|
244
|
+
if (typeof field.maxLength === "number") plan.maxItems = field.maxLength;
|
|
245
|
+
return plan;
|
|
246
|
+
}
|
|
247
|
+
function buildItemPlan(field, path, registry, warn) {
|
|
248
|
+
const template = getNormalizedArrayMetadata(field)?.template;
|
|
249
|
+
const source = template ?? firstDeclaredItem(field);
|
|
250
|
+
if (source === void 0) return void 0;
|
|
251
|
+
const itemPlan = itemPlanFrom(source, path, registry, warn);
|
|
252
|
+
if (!itemPlan) return void 0;
|
|
253
|
+
if (!template && !itemsAreHomogeneous(field, path, registry, itemPlan, warn)) {
|
|
254
|
+
warn?.(`WebMCP: array field "${field.key}" declares items with different shapes. Only homogeneous arrays can be described to an agent, so its items are left unspecified.`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
return itemPlan;
|
|
258
|
+
}
|
|
259
|
+
function itemPlanFrom(source, path, registry, warn) {
|
|
260
|
+
if (Array.isArray(source)) {
|
|
261
|
+
const children = [];
|
|
262
|
+
collectInto(children, source, registry, `${path}[]`, warn);
|
|
263
|
+
return {
|
|
264
|
+
kind: "object",
|
|
265
|
+
children
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const itemField = source;
|
|
269
|
+
if (!itemField?.type) return void 0;
|
|
270
|
+
const node = buildNode(itemField, registry, `${path}[]`, warn);
|
|
271
|
+
if (!node || node.kind !== "scalar") return void 0;
|
|
272
|
+
return {
|
|
273
|
+
kind: "value",
|
|
274
|
+
value: node
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function firstDeclaredItem(field) {
|
|
278
|
+
const items = Array.isArray(field.fields) ? field.fields : void 0;
|
|
279
|
+
return items?.length ? items[0] : void 0;
|
|
280
|
+
}
|
|
281
|
+
function itemsAreHomogeneous(field, path, registry, first, warn) {
|
|
282
|
+
return (Array.isArray(field.fields) ? field.fields : []).every((item) => {
|
|
283
|
+
const candidate = itemPlanFrom(item, path, registry, warn);
|
|
284
|
+
return candidate !== void 0 && sameShape(candidate, first);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function sameShape(a, b) {
|
|
288
|
+
if (a.kind !== b.kind) return false;
|
|
289
|
+
if (a.kind === "value" && b.kind === "value") return scalarShape(a.value) === scalarShape(b.value);
|
|
290
|
+
const aChildren = a.children;
|
|
291
|
+
const bChildren = b.children;
|
|
292
|
+
if (aChildren.length !== bChildren.length) return false;
|
|
293
|
+
return aChildren.every((child, index) => nodeShape(child) === nodeShape(bChildren[index]));
|
|
294
|
+
}
|
|
295
|
+
function nodeShape(node) {
|
|
296
|
+
switch (node.kind) {
|
|
297
|
+
case "scalar": return `${node.key}:${scalarShape(node)}`;
|
|
298
|
+
case "group": return `${node.key}:group(${node.children.map(nodeShape).join(",")})`;
|
|
299
|
+
case "array": return `${node.key}:array(${node.item ? itemShape(node.item) : "?"})`;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function itemShape(item) {
|
|
303
|
+
return item.kind === "value" ? scalarShape(item.value) : item.children.map(nodeShape).join(",");
|
|
304
|
+
}
|
|
305
|
+
function scalarShape(plan) {
|
|
306
|
+
const options = plan.options ? plan.options.map((option) => JSON.stringify(option.value)).join("|") : "";
|
|
307
|
+
return `${plan.types.join("/")}${plan.multiple ? "[]" : ""}${options ? `{${options}}` : ""}`;
|
|
308
|
+
}
|
|
309
|
+
function buildScalar(field, base, registry) {
|
|
310
|
+
const scope = resolveScope(field, registry);
|
|
311
|
+
const options = usableOptions(field);
|
|
312
|
+
const constraints = collectConstraints(field);
|
|
313
|
+
if (options && (scope === "single-select" || scope === "multi-select")) {
|
|
314
|
+
const types = [inferTypeFromValues(options.map((option) => option.value))];
|
|
315
|
+
const multiple = scope === "multi-select";
|
|
316
|
+
return {
|
|
317
|
+
...base,
|
|
318
|
+
kind: "scalar",
|
|
319
|
+
types: multiple || !field.nullable ? types : [...types, "null"],
|
|
320
|
+
options,
|
|
321
|
+
multiple,
|
|
322
|
+
default: jsonPrimitive(field.value),
|
|
323
|
+
constraints
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
if (scope === "date") return {
|
|
327
|
+
...base,
|
|
328
|
+
kind: "scalar",
|
|
329
|
+
types: withNull(["string"], field.nullable),
|
|
330
|
+
multiple: false,
|
|
331
|
+
default: jsonPrimitive(field.value),
|
|
332
|
+
constraints: {
|
|
333
|
+
...constraints,
|
|
334
|
+
format: constraints.format ?? "date"
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
return {
|
|
338
|
+
...base,
|
|
339
|
+
kind: "scalar",
|
|
340
|
+
types: withNull([scalarTypeFor(scope, field)], field.nullable),
|
|
341
|
+
multiple: false,
|
|
342
|
+
default: jsonPrimitive(field.value),
|
|
343
|
+
constraints
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function usableOptions(field) {
|
|
347
|
+
if (!field.options?.length) return void 0;
|
|
348
|
+
const usable = field.options.filter((option) => option.disabled !== true).map((option) => {
|
|
349
|
+
const title = staticText(option.label);
|
|
350
|
+
return title ? {
|
|
351
|
+
value: option.value,
|
|
352
|
+
title
|
|
353
|
+
} : { value: option.value };
|
|
354
|
+
});
|
|
355
|
+
return usable.length ? usable : void 0;
|
|
356
|
+
}
|
|
357
|
+
function scalarTypeFor(scope, field) {
|
|
358
|
+
switch (scope) {
|
|
359
|
+
case "boolean": return "boolean";
|
|
360
|
+
case "numeric": return "number";
|
|
361
|
+
case "text-input": return "string";
|
|
362
|
+
default: return inferTypeFromValues([field.value]);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function resolveScope(field, registry) {
|
|
366
|
+
const declared = registry.get(field.type)?.scope;
|
|
367
|
+
if (!declared) return void 0;
|
|
368
|
+
if (!Array.isArray(declared)) return declared;
|
|
369
|
+
if (declared.includes("numeric") && declared.includes("text-input")) return field.props?.type === "number" ? "numeric" : "text-input";
|
|
370
|
+
return declared[0];
|
|
371
|
+
}
|
|
372
|
+
function inferTypeFromValues(values) {
|
|
373
|
+
const known = values.filter((value) => value !== null && value !== void 0);
|
|
374
|
+
if (!known.length) return "string";
|
|
375
|
+
if (known.every((value) => typeof value === "number")) return "number";
|
|
376
|
+
if (known.every((value) => typeof value === "boolean")) return "boolean";
|
|
377
|
+
return "string";
|
|
378
|
+
}
|
|
379
|
+
function withNull(types, nullable) {
|
|
380
|
+
return nullable ? [...types, "null"] : types;
|
|
381
|
+
}
|
|
382
|
+
function collectConstraints(field) {
|
|
383
|
+
const constraints = {};
|
|
384
|
+
if (typeof field.minLength === "number" && field.type !== "array") constraints.minLength = field.minLength;
|
|
385
|
+
if (typeof field.maxLength === "number" && field.type !== "array") constraints.maxLength = field.maxLength;
|
|
386
|
+
if (typeof field.min === "number") constraints.minimum = field.min;
|
|
387
|
+
if (typeof field.max === "number") constraints.maximum = field.max;
|
|
388
|
+
if (field.pattern instanceof RegExp) constraints.pattern = field.pattern.source;
|
|
389
|
+
else if (typeof field.pattern === "string") constraints.pattern = field.pattern;
|
|
390
|
+
if (field.email) constraints.format = "email";
|
|
391
|
+
for (const validator of field.validators ?? []) {
|
|
392
|
+
if (!isStaticValidator(validator)) continue;
|
|
393
|
+
const value = validator.value;
|
|
394
|
+
switch (validator.type) {
|
|
395
|
+
case "minLength":
|
|
396
|
+
if (typeof value === "number") constraints.minLength = value;
|
|
397
|
+
break;
|
|
398
|
+
case "maxLength":
|
|
399
|
+
if (typeof value === "number") constraints.maxLength = value;
|
|
400
|
+
break;
|
|
401
|
+
case "min":
|
|
402
|
+
if (typeof value === "number") constraints.minimum = value;
|
|
403
|
+
break;
|
|
404
|
+
case "max":
|
|
405
|
+
if (typeof value === "number") constraints.maximum = value;
|
|
406
|
+
break;
|
|
407
|
+
case "pattern":
|
|
408
|
+
if (value instanceof RegExp) constraints.pattern = value.source;
|
|
409
|
+
else if (typeof value === "string") constraints.pattern = value;
|
|
410
|
+
break;
|
|
411
|
+
case "email": constraints.format = "email";
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return constraints;
|
|
415
|
+
}
|
|
416
|
+
function isStaticValidator(validator) {
|
|
417
|
+
return validator.when === void 0 && validator.expression === void 0;
|
|
418
|
+
}
|
|
419
|
+
function resolvePolicy(field) {
|
|
420
|
+
const declared = field.webMcp;
|
|
421
|
+
if (declared === false) return {
|
|
422
|
+
readable: false,
|
|
423
|
+
writable: false
|
|
424
|
+
};
|
|
425
|
+
const isHiddenType = field.type === "hidden";
|
|
426
|
+
const isSecret = field.props?.type === "password";
|
|
427
|
+
const isDerived = field.derivation !== void 0 || (field.logic ?? []).some((rule) => rule?.type === "derivation");
|
|
428
|
+
const isReadonly = field.readonly === true || (field.logic ?? []).some((rule) => rule?.type === "readonly");
|
|
429
|
+
return {
|
|
430
|
+
readable: declared?.readable ?? (!isHiddenType && !isSecret),
|
|
431
|
+
writable: declared?.writable ?? (!isHiddenType && !isDerived && !isReadonly)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function staticText(value) {
|
|
435
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
436
|
+
}
|
|
437
|
+
function jsonPrimitive(value) {
|
|
438
|
+
if (value === null) return null;
|
|
439
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
440
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
441
|
+
}
|
|
442
|
+
function asFieldArray(value) {
|
|
443
|
+
return Array.isArray(value) ? value : void 0;
|
|
444
|
+
}
|
|
445
|
+
function toErrorReports(errors, paths, messages, defaults) {
|
|
446
|
+
return errors.map((error) => {
|
|
447
|
+
const kind = String(error.kind);
|
|
448
|
+
const template = staticMessage(messages.get(error.fieldTree)?.[kind]) ?? staticMessage(defaults?.[kind]);
|
|
449
|
+
return {
|
|
450
|
+
path: paths.get(error.fieldTree) ?? "",
|
|
451
|
+
message: template ? interpolateParams(template, error) : error.message ?? kind
|
|
452
|
+
};
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
function staticMessage(message) {
|
|
456
|
+
return typeof message === "string" && message.length > 0 ? message : void 0;
|
|
457
|
+
}
|
|
458
|
+
function renderRejection(errors) {
|
|
459
|
+
return [
|
|
460
|
+
"Nothing was applied. The form is unchanged.",
|
|
461
|
+
"",
|
|
462
|
+
"Problems with the arguments:",
|
|
463
|
+
...errors.map((error) => `- ${error}`)
|
|
464
|
+
].join("\n");
|
|
465
|
+
}
|
|
466
|
+
function renderFormReport(report) {
|
|
467
|
+
const lines = [];
|
|
468
|
+
if (report.changed.length) {
|
|
469
|
+
lines.push(`Applied: ${report.changed.join(", ")}.`);
|
|
470
|
+
lines.push(report.scope === "all" ? "Current values:" : "Current values of the fields you set:");
|
|
471
|
+
lines.push(JSON.stringify(report.values, null, 2));
|
|
472
|
+
} else {
|
|
473
|
+
lines.push("No changes made.");
|
|
474
|
+
if (report.scope === "all") {
|
|
475
|
+
lines.push("Current values:");
|
|
476
|
+
lines.push(JSON.stringify(report.values, null, 2));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
appendFieldSections(lines, report.fields);
|
|
480
|
+
appendErrorSection(lines, report.errors, report.validationPending);
|
|
481
|
+
return lines.join("\n");
|
|
482
|
+
}
|
|
483
|
+
function appendFieldSections(lines, fields) {
|
|
484
|
+
const inapplicable = fields.filter((field) => !field.applicable);
|
|
485
|
+
if (inapplicable.length) {
|
|
486
|
+
lines.push("");
|
|
487
|
+
lines.push(`Not currently applicable (do not send these): ${paths(inapplicable)}`);
|
|
488
|
+
}
|
|
489
|
+
const required = fields.filter((field) => field.applicable && field.required);
|
|
490
|
+
if (required.length) {
|
|
491
|
+
lines.push("");
|
|
492
|
+
lines.push(`Required (given the current values): ${paths(required)}`);
|
|
493
|
+
}
|
|
494
|
+
const empty = fields.filter((field) => field.applicable && field.writable && !field.filled);
|
|
495
|
+
if (empty.length) {
|
|
496
|
+
lines.push("");
|
|
497
|
+
lines.push(`Still empty: ${paths(empty)}`);
|
|
498
|
+
}
|
|
499
|
+
const locked = fields.filter((field) => field.applicable && !field.writable);
|
|
500
|
+
if (locked.length) {
|
|
501
|
+
lines.push("");
|
|
502
|
+
lines.push(`Cannot be set by an agent: ${paths(locked)}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function appendErrorSection(lines, errors, validationPending) {
|
|
506
|
+
if (errors.length) {
|
|
507
|
+
lines.push("");
|
|
508
|
+
lines.push("Validation errors:");
|
|
509
|
+
for (const error of errors) lines.push(`- ${error.path ? `${error.path}: ` : ""}${error.message}`);
|
|
510
|
+
}
|
|
511
|
+
if (validationPending) {
|
|
512
|
+
lines.push("");
|
|
513
|
+
lines.push("Validation has not finished — some checks are still running, so this list may be incomplete. Call again to re-read it.");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (!errors.length) {
|
|
517
|
+
lines.push("");
|
|
518
|
+
lines.push("No validation errors.");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function paths(fields) {
|
|
522
|
+
return fields.map((field) => field.path).join(", ");
|
|
523
|
+
}
|
|
524
|
+
function renderSubmitResult(outcome, report) {
|
|
525
|
+
switch (outcome.status) {
|
|
526
|
+
case "success": return "Form submitted successfully.";
|
|
527
|
+
case "dispatched": return "Form submitted. The page handled the submission itself, so there is no result to report back.";
|
|
528
|
+
case "server-errors": return renderFailure("Submitted, but it came back with errors.", report, "Correct these and call again.");
|
|
529
|
+
case "action-failed": return renderFailure(`Not submitted: the submission failed. ${describeError(outcome.error)}`, report, "The values you sent are still in the form. This may be worth retrying.");
|
|
530
|
+
case "validation-failed": return renderFailure("Not submitted: validation failed.", report, changedNote(report));
|
|
531
|
+
case "pending-validation": return renderFailure("Not submitted: validation had not finished when the form was asked to submit.", report, "The values you sent are still in the form. Call again in a moment.");
|
|
532
|
+
case "busy": return "Not submitted: this form is already submitting. Wait for that to finish before trying again.";
|
|
533
|
+
case "cancelled": return "Not submitted: the form went away before the submission finished.";
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function renderFailure(headline, report, closing) {
|
|
537
|
+
const lines = [headline];
|
|
538
|
+
for (const error of report.errors) lines.push(`- ${error.path ? `${error.path}: ` : ""}${error.message}`);
|
|
539
|
+
lines.push("");
|
|
540
|
+
lines.push(closing);
|
|
541
|
+
return lines.join("\n");
|
|
542
|
+
}
|
|
543
|
+
function changedNote(report) {
|
|
544
|
+
return report.changed.length ? "The values you sent were applied to the form and are still there. Send only the corrected fields and call again." : "The form was not modified. Send the corrected fields and call again.";
|
|
545
|
+
}
|
|
546
|
+
function describeError(error) {
|
|
547
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
548
|
+
return message ? `Reason: ${message.slice(0, 200)}` : "No reason was given.";
|
|
549
|
+
}
|
|
550
|
+
const NAME_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
551
|
+
const NAME_BUDGET = 30;
|
|
552
|
+
function findModelContext() {
|
|
553
|
+
const hosts = globalThis;
|
|
554
|
+
const fromDocument = hosts.document?.["modelContext"];
|
|
555
|
+
if (isModelContext(fromDocument)) return fromDocument;
|
|
556
|
+
const fromNavigator = hosts.navigator?.["modelContext"];
|
|
557
|
+
if (isModelContext(fromNavigator)) return fromNavigator;
|
|
558
|
+
}
|
|
559
|
+
function isModelContext(candidate) {
|
|
560
|
+
return typeof candidate?.registerTool === "function";
|
|
561
|
+
}
|
|
562
|
+
function validateToolName(name) {
|
|
563
|
+
if (!NAME_PATTERN.test(name)) return `Tool name "${name}" is not usable: names must be 1-128 characters of A-Z, a-z, 0-9, "_", "-" or ".".`;
|
|
564
|
+
}
|
|
565
|
+
function isOverNameBudget(name) {
|
|
566
|
+
return name.length > NAME_BUDGET;
|
|
567
|
+
}
|
|
568
|
+
async function registerTool(context, descriptor, signal) {
|
|
569
|
+
const invalid = validateToolName(descriptor.name);
|
|
570
|
+
if (invalid) return {
|
|
571
|
+
ok: false,
|
|
572
|
+
reason: invalid
|
|
573
|
+
};
|
|
574
|
+
if (signal.aborted) return {
|
|
575
|
+
ok: false,
|
|
576
|
+
reason: "Registration was superseded before it started."
|
|
577
|
+
};
|
|
578
|
+
try {
|
|
579
|
+
await context.registerTool(descriptor, { signal });
|
|
580
|
+
return { ok: true };
|
|
581
|
+
} catch (error) {
|
|
582
|
+
return {
|
|
583
|
+
ok: false,
|
|
584
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const SKIP = Symbol("skip");
|
|
589
|
+
function parseAgentInput(plan, input, liveBlock, current) {
|
|
590
|
+
if (input === void 0 || input === null) return {
|
|
591
|
+
ok: true,
|
|
592
|
+
patch: {},
|
|
593
|
+
paths: []
|
|
594
|
+
};
|
|
595
|
+
if (!isPlainObject$1(input)) return {
|
|
596
|
+
ok: false,
|
|
597
|
+
errors: ["Arguments must be an object of field names to values."]
|
|
598
|
+
};
|
|
599
|
+
const errors = [];
|
|
600
|
+
const paths = [];
|
|
601
|
+
const patch = parseLevel(plan, input, "", errors, paths, liveBlock, isPlainObject$1(current) ? current : void 0);
|
|
602
|
+
return errors.length ? {
|
|
603
|
+
ok: false,
|
|
604
|
+
errors
|
|
605
|
+
} : {
|
|
606
|
+
ok: true,
|
|
607
|
+
patch,
|
|
608
|
+
paths
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function parseLevel(nodes, input, prefix, errors, paths, liveBlock, current) {
|
|
612
|
+
const byKey = new Map(nodes.map((node) => [node.key, node]));
|
|
613
|
+
const patch = {};
|
|
614
|
+
for (const [key, value] of Object.entries(input)) {
|
|
615
|
+
const label = prefix ? `${prefix}.${key}` : key;
|
|
616
|
+
const node = byKey.get(key);
|
|
617
|
+
if (!node) {
|
|
618
|
+
const writable = nodes.filter((candidate) => candidate.policy.writable).map((candidate) => candidate.key);
|
|
619
|
+
errors.push(`Unknown field "${label}". This form accepts: ${writable.join(", ") || "(no writable fields)"}.`);
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (!node.policy.writable) {
|
|
623
|
+
errors.push(`Field "${label}" cannot be set by an agent.`);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
const blocked = liveBlock?.(node.path);
|
|
627
|
+
if (blocked) {
|
|
628
|
+
errors.push(`Field "${label}" cannot be set right now: ${blocked}.`);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
const parsed = parseNode(node, value, label, errors, paths, liveBlock, current?.[key]);
|
|
632
|
+
if (parsed !== SKIP) {
|
|
633
|
+
patch[key] = parsed;
|
|
634
|
+
if (node.kind !== "group") paths.push(node.path);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return patch;
|
|
638
|
+
}
|
|
639
|
+
function parseNode(node, value, label, errors, paths, liveBlock, current) {
|
|
640
|
+
switch (node.kind) {
|
|
641
|
+
case "scalar": return parseScalar(node, value, label, errors);
|
|
642
|
+
case "group": {
|
|
643
|
+
if (!isPlainObject$1(value)) {
|
|
644
|
+
errors.push(`Field "${label}" is a group and expects an object.`);
|
|
645
|
+
return SKIP;
|
|
646
|
+
}
|
|
647
|
+
const before = errors.length;
|
|
648
|
+
const nested = parseLevel(node.children, value, label, errors, paths, liveBlock, isPlainObject$1(current) ? current : void 0);
|
|
649
|
+
return errors.length === before ? nested : SKIP;
|
|
650
|
+
}
|
|
651
|
+
case "array": {
|
|
652
|
+
if (!Array.isArray(value)) {
|
|
653
|
+
errors.push(`Field "${label}" is a list and expects an array.`);
|
|
654
|
+
return SKIP;
|
|
655
|
+
}
|
|
656
|
+
if (!node.item) {
|
|
657
|
+
errors.push(`Field "${label}" has an item shape that cannot be described to an agent, so it cannot be set.`);
|
|
658
|
+
return SKIP;
|
|
659
|
+
}
|
|
660
|
+
const protectedKeys = protectedValuesInUse(node.item, current);
|
|
661
|
+
if (protectedKeys.length) {
|
|
662
|
+
errors.push(`Field "${label}" cannot be replaced: its items carry ${protectedKeys.join(", ")}, which an agent cannot set, and a list is replaced whole rather than merged item by item. Sending it would discard those values.`);
|
|
663
|
+
return SKIP;
|
|
664
|
+
}
|
|
665
|
+
const before = errors.length;
|
|
666
|
+
const itemPaths = [];
|
|
667
|
+
const items = value.map((item, index) => parseItem(node.item, item, `${label}[${index}]`, errors, itemPaths, liveBlock));
|
|
668
|
+
return errors.length === before ? items : SKIP;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function parseItem(item, value, label, errors, paths, liveBlock) {
|
|
673
|
+
if (item.kind === "value") return parseScalar(item.value, value, label, errors);
|
|
674
|
+
if (!isPlainObject$1(value)) {
|
|
675
|
+
errors.push(`Item "${label}" expects an object.`);
|
|
676
|
+
return SKIP;
|
|
677
|
+
}
|
|
678
|
+
return parseLevel(item.children, value, label, errors, paths, liveBlock, void 0);
|
|
679
|
+
}
|
|
680
|
+
function parseScalar(plan, value, label, errors) {
|
|
681
|
+
if (plan.multiple) {
|
|
682
|
+
if (!Array.isArray(value)) {
|
|
683
|
+
errors.push(`Field "${label}" accepts multiple values and expects an array.`);
|
|
684
|
+
return SKIP;
|
|
685
|
+
}
|
|
686
|
+
const invalid = value.filter((entry) => !isAllowedOption(plan, entry));
|
|
687
|
+
if (invalid.length) {
|
|
688
|
+
errors.push(`Field "${label}" got ${describe(invalid[0])}, which is not one of: ${optionList(plan)}.`);
|
|
689
|
+
return SKIP;
|
|
690
|
+
}
|
|
691
|
+
return value;
|
|
692
|
+
}
|
|
693
|
+
if (plan.options) {
|
|
694
|
+
if (!isAllowedOption(plan, value) && !(value === null && plan.types.includes("null"))) {
|
|
695
|
+
errors.push(`Field "${label}" got ${describe(value)}, which is not one of: ${optionList(plan)}.`);
|
|
696
|
+
return SKIP;
|
|
697
|
+
}
|
|
698
|
+
return value;
|
|
699
|
+
}
|
|
700
|
+
if (!plan.types.some((type) => matchesType(type, value))) {
|
|
701
|
+
errors.push(`Field "${label}" expects ${plan.types.join(" or ")} but got ${describe(value)}.`);
|
|
702
|
+
return SKIP;
|
|
703
|
+
}
|
|
704
|
+
return value;
|
|
705
|
+
}
|
|
706
|
+
function isAllowedOption(plan, value) {
|
|
707
|
+
return (plan.options ?? []).some((option) => Object.is(option.value, value));
|
|
708
|
+
}
|
|
709
|
+
function optionList(plan) {
|
|
710
|
+
return (plan.options ?? []).map((option) => JSON.stringify(option.value)).join(", ");
|
|
711
|
+
}
|
|
712
|
+
function matchesType(type, value) {
|
|
713
|
+
switch (type) {
|
|
714
|
+
case "string": return typeof value === "string";
|
|
715
|
+
case "number": return typeof value === "number" && Number.isFinite(value);
|
|
716
|
+
case "integer": return typeof value === "number" && Number.isInteger(value);
|
|
717
|
+
case "boolean": return typeof value === "boolean";
|
|
718
|
+
case "null": return value === null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function describe(value) {
|
|
722
|
+
if (value === null) return "null";
|
|
723
|
+
if (value === void 0) return "undefined";
|
|
724
|
+
if (Array.isArray(value)) return "an array";
|
|
725
|
+
if (typeof value === "object") return "an object";
|
|
726
|
+
if (typeof value === "string") return value.length > 40 ? "a long string" : JSON.stringify(value);
|
|
727
|
+
return JSON.stringify(value);
|
|
728
|
+
}
|
|
729
|
+
function protectedValuesInUse(item, current) {
|
|
730
|
+
if (item.kind !== "object" || !Array.isArray(current) || !current.length) return [];
|
|
731
|
+
const names = /* @__PURE__ */ new Set();
|
|
732
|
+
for (const existing of current) collectProtected(item.children, existing, "", names);
|
|
733
|
+
return [...names];
|
|
734
|
+
}
|
|
735
|
+
function collectProtected(nodes, value, prefix, into) {
|
|
736
|
+
if (!isPlainObject$1(value)) return;
|
|
737
|
+
for (const node of nodes) {
|
|
738
|
+
const name = prefix ? `${prefix}.${node.key}` : node.key;
|
|
739
|
+
const held = value[node.key];
|
|
740
|
+
if (node.kind === "group") {
|
|
741
|
+
collectProtected(node.children, held, name, into);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (!node.policy.writable && holdsValue(held)) {
|
|
745
|
+
into.add(`"${name}"`);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (node.kind === "array" && node.item && Array.isArray(held)) collectProtectedInItems(node.item, held, `${name}[]`, into);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function collectProtectedInItems(item, items, prefix, into) {
|
|
752
|
+
for (const entry of items) {
|
|
753
|
+
if (item.kind === "value") {
|
|
754
|
+
if (!item.value.policy.writable && holdsValue(entry)) into.add(`"${prefix}"`);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
collectProtected(item.children, entry, prefix, into);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function holdsValue(value) {
|
|
761
|
+
if (value === void 0 || value === null || value === "") return false;
|
|
762
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
function isPlainObject$1(value) {
|
|
766
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
767
|
+
}
|
|
768
|
+
function mergePatch(current, patch, plan) {
|
|
769
|
+
const byKey = new Map(plan.map((node) => [node.key, node]));
|
|
770
|
+
const next = { ...current };
|
|
771
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
772
|
+
const node = byKey.get(key);
|
|
773
|
+
if (node?.kind === "group" && isPlainObject(value)) {
|
|
774
|
+
next[key] = mergePatch(isPlainObject(current[key]) ? current[key] : {}, value, node.children);
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
next[key] = value;
|
|
778
|
+
}
|
|
779
|
+
return next;
|
|
780
|
+
}
|
|
781
|
+
const REDACTED = "[not readable by agents]";
|
|
782
|
+
function redactValues(value, plan) {
|
|
783
|
+
if (!isPlainObject(value)) return value;
|
|
784
|
+
const out = {};
|
|
785
|
+
for (const node of plan) {
|
|
786
|
+
if (!(node.key in value)) continue;
|
|
787
|
+
if (!node.policy.readable) {
|
|
788
|
+
out[node.key] = REDACTED;
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
out[node.key] = redactNode(node, value[node.key]);
|
|
792
|
+
}
|
|
793
|
+
return out;
|
|
794
|
+
}
|
|
795
|
+
function redactNode(node, value) {
|
|
796
|
+
switch (node.kind) {
|
|
797
|
+
case "group": return redactValues(value, node.children);
|
|
798
|
+
case "array":
|
|
799
|
+
if (!Array.isArray(value) || node.item?.kind !== "object") return value;
|
|
800
|
+
return value.map((item) => redactValues(item, node.item.children));
|
|
801
|
+
case "scalar": return value;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function pickPaths(value, paths) {
|
|
805
|
+
if (!isPlainObject(value)) return {};
|
|
806
|
+
const out = {};
|
|
807
|
+
for (const path of paths) {
|
|
808
|
+
const [head, ...rest] = path.split(".");
|
|
809
|
+
if (!(head in value)) continue;
|
|
810
|
+
if (!rest.length) {
|
|
811
|
+
out[head] = value[head];
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const nested = pickPaths(value[head], [rest.join(".")]);
|
|
815
|
+
out[head] = isPlainObject(out[head]) ? {
|
|
816
|
+
...out[head],
|
|
817
|
+
...nested
|
|
818
|
+
} : nested;
|
|
819
|
+
}
|
|
820
|
+
return out;
|
|
821
|
+
}
|
|
822
|
+
function isPlainObject(value) {
|
|
823
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
824
|
+
}
|
|
825
|
+
const NOT_READY = "The form is not ready yet. Try again shortly.";
|
|
826
|
+
const CANCELLED = "The call was cancelled before it ran, so the form is unchanged and nothing was submitted.";
|
|
827
|
+
const SETTLE_TIMEOUT_MS = 5e3;
|
|
828
|
+
const SETTLE_POLL_MS = 10;
|
|
829
|
+
async function bootstrapWebMcp(options, signal) {
|
|
830
|
+
const stateManager = inject(FormStateManager);
|
|
831
|
+
const eventBus = inject(EventBus);
|
|
832
|
+
const logger = inject(DynamicFormLogger);
|
|
833
|
+
const context = findModelContext();
|
|
834
|
+
if (!context) {
|
|
835
|
+
logger.debug("No WebMCP model context on this page; the form is not exposed to agents.");
|
|
836
|
+
return "unsupported";
|
|
837
|
+
}
|
|
838
|
+
const defaultMessages = untracked(() => stateManager.activeConfig()?.defaultValidationMessages);
|
|
839
|
+
const setup = untracked(() => stateManager.formSetup());
|
|
840
|
+
const plan = buildFieldPlan(setup?.schemaFields ?? [], setup?.registry ?? /* @__PURE__ */ new Map(), (message) => logger.warn(message));
|
|
841
|
+
const inputSchema = buildToolSchema(plan);
|
|
842
|
+
const scope = options.readback === "all" ? "all" : "changed";
|
|
843
|
+
const annotations = { untrustedContentHint: true };
|
|
844
|
+
const descriptors = [{
|
|
845
|
+
name: `fill_${options.name}`,
|
|
846
|
+
description: `Fill the "${options.name}" form: ${options.description} Accepts any subset of fields and leaves the rest untouched; a nested group is merged key by key, a list is replaced whole. Does not submit. Returns the values it set, which fields currently apply, which are still empty, and any validation errors. Call with no fields to see which apply, which are required and which are still empty, without changing anything.`,
|
|
847
|
+
inputSchema,
|
|
848
|
+
annotations,
|
|
849
|
+
execute: (args, execution) => fill(args, invocationSignal(execution))
|
|
850
|
+
}];
|
|
851
|
+
if (options.allowSubmit) {
|
|
852
|
+
const reportsOutcome = untracked(() => stateManager.activeConfig()?.submission?.action) !== void 0;
|
|
853
|
+
descriptors.push({
|
|
854
|
+
name: `submit_${options.name}`,
|
|
855
|
+
description: `Submit the "${options.name}" form: ${options.description} Applies any fields given, then submits and waits for the result. If validation fails nothing is submitted and the errors are returned; the values still remain in the form for correction. ` + (reportsOutcome ? `Reports whether the submission succeeded, was rejected by the server, or failed.` : `This page handles submission itself, so a successful call confirms the form was submitted but cannot report what came of it.`),
|
|
856
|
+
inputSchema,
|
|
857
|
+
annotations: {
|
|
858
|
+
...annotations,
|
|
859
|
+
consequentialHint: true
|
|
860
|
+
},
|
|
861
|
+
execute: (args, execution) => submitForm(args, invocationSignal(execution))
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
const failures = [];
|
|
865
|
+
for (const descriptor of descriptors) {
|
|
866
|
+
if (isOverNameBudget(descriptor.name)) logger.warn(`WebMCP tool name "${descriptor.name}" is longer than the ~30 characters agents can comfortably scan. A shorter \`webMcp.name\` reads better in a tool list.`);
|
|
867
|
+
const result = await registerTool(context, descriptor, signal);
|
|
868
|
+
if (!result.ok) failures.push(`${descriptor.name}: ${result.reason}`);
|
|
869
|
+
}
|
|
870
|
+
if (signal.aborted) return "idle";
|
|
871
|
+
if (failures.length) {
|
|
872
|
+
logger.error(`WebMCP tool registration failed. ${failures.join(" ")}`);
|
|
873
|
+
return "failed";
|
|
874
|
+
}
|
|
875
|
+
return "active";
|
|
876
|
+
async function fill(args, invocation) {
|
|
877
|
+
const tree = untracked(() => stateManager.form());
|
|
878
|
+
if (!tree) return NOT_READY;
|
|
879
|
+
const applied = await apply(args, tree, invocation);
|
|
880
|
+
if ("rejection" in applied) return applied.rejection;
|
|
881
|
+
return renderFormReport(applied.report);
|
|
882
|
+
}
|
|
883
|
+
async function submitForm(args, invocation) {
|
|
884
|
+
const tree = untracked(() => stateManager.form());
|
|
885
|
+
if (!tree) return NOT_READY;
|
|
886
|
+
const applied = await apply(args, tree, invocation);
|
|
887
|
+
if ("rejection" in applied) return applied.rejection;
|
|
888
|
+
const report = applied.report;
|
|
889
|
+
if (report.errors.length || report.validationPending) return renderSubmitResult({ status: report.validationPending ? "pending-validation" : "validation-failed" }, report);
|
|
890
|
+
return renderSubmitResult(await dispatchSubmit(invocation), reportNow(tree, report.changed));
|
|
891
|
+
}
|
|
892
|
+
function reportNow(tree, changed) {
|
|
893
|
+
return buildReport(tree, untracked(() => collectFieldReports(plan, tree)), changed, untracked(() => tree().pending()));
|
|
894
|
+
}
|
|
895
|
+
async function dispatchSubmit(invocation) {
|
|
896
|
+
const pending = createPendingSubmission();
|
|
897
|
+
if (signal.aborted || invocation?.aborted) return { status: "cancelled" };
|
|
898
|
+
const cancel = () => pending.cancel();
|
|
899
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
900
|
+
invocation?.addEventListener("abort", cancel, { once: true });
|
|
901
|
+
try {
|
|
902
|
+
eventBus.dispatch(new FormSubmitEvent(pending.reply));
|
|
903
|
+
if (!pending.accepted()) return { status: "busy" };
|
|
904
|
+
return await pending.outcome;
|
|
905
|
+
} finally {
|
|
906
|
+
signal.removeEventListener("abort", cancel);
|
|
907
|
+
invocation?.removeEventListener("abort", cancel);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
async function apply(args, tree, invocation) {
|
|
911
|
+
if (signal.aborted || invocation?.aborted) return { rejection: CANCELLED };
|
|
912
|
+
const before = untracked(() => collectFieldReports(plan, tree));
|
|
913
|
+
const parsed = parseAgentInput(plan, args, (path) => liveBlock(before, path), untracked(() => stateManager.formValue()));
|
|
914
|
+
if (!parsed.ok) return { rejection: renderRejection(parsed.errors) };
|
|
915
|
+
const changed = parsed.paths;
|
|
916
|
+
if (changed.length) stateManager.entity.update((current) => mergePatch(current ?? {}, parsed.patch, plan));
|
|
917
|
+
const validationPending = changed.length ? await settle(tree, invocation) : untracked(() => tree().pending());
|
|
918
|
+
return { report: buildReport(tree, untracked(() => collectFieldReports(plan, tree)), changed, validationPending) };
|
|
919
|
+
}
|
|
920
|
+
function buildReport(tree, walk, changed, validationPending) {
|
|
921
|
+
const readable = redactValues(untracked(() => stateManager.formValue()), plan);
|
|
922
|
+
return {
|
|
923
|
+
values: scope === "all" ? readable : pickPaths(readable, changed),
|
|
924
|
+
fields: walk.reports,
|
|
925
|
+
errors: toErrorReports(untracked(() => tree().errorSummary()), walk.paths, walk.messages, defaultMessages),
|
|
926
|
+
changed,
|
|
927
|
+
validationPending,
|
|
928
|
+
scope
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function liveBlock(walk, path) {
|
|
932
|
+
const state = walk.state.get(path);
|
|
933
|
+
if (state?.disabled) return "the form has disabled it";
|
|
934
|
+
if (state?.readonly) return "the form has made it read-only";
|
|
935
|
+
}
|
|
936
|
+
async function settle(tree, invocation) {
|
|
937
|
+
const deadline = Date.now() + SETTLE_TIMEOUT_MS;
|
|
938
|
+
await nextTask();
|
|
939
|
+
while (untracked(() => tree().pending())) {
|
|
940
|
+
if (Date.now() >= deadline || signal.aborted || invocation?.aborted) return true;
|
|
941
|
+
await nextTask(SETTLE_POLL_MS);
|
|
942
|
+
}
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function invocationSignal(execution) {
|
|
947
|
+
return execution?.signal instanceof AbortSignal ? execution.signal : void 0;
|
|
948
|
+
}
|
|
949
|
+
function nextTask(delayMs = 0) {
|
|
950
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
951
|
+
}
|
|
952
|
+
export { bootstrapWebMcp };
|
|
953
|
+
|
|
954
|
+
//# sourceMappingURL=ng-forge-dynamic-forms-bootstrap-web-mcp-2wQK_wPp.mjs.map
|