@particle-academy/agent-integrations 0.52.0 → 0.53.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 +38 -0
- package/README.md +45 -3
- package/dist/bridges/forms.d.cts +13 -6
- package/dist/bridges/forms.d.ts +13 -6
- package/dist/bridges-forms.cjs +184 -43
- package/dist/bridges-forms.cjs.map +1 -1
- package/dist/bridges-forms.js +1 -1
- package/dist/{chunk-AL77RO25.js → chunk-AET2R7DJ.js} +186 -45
- package/dist/chunk-AET2R7DJ.js.map +1 -0
- package/dist/index.cjs +192 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/docs/multi-form-hosts.md +70 -4
- package/package.json +1 -1
- package/dist/chunk-AL77RO25.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.53.0] - 2026-10-30
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Several forms on one host.** A second `registerFormBridge` on the same host
|
|
19
|
+
now JOINS the existing form tools instead of throwing. Every form tool takes an
|
|
20
|
+
optional `form` argument (the form's `adapter.id`). It may be omitted while
|
|
21
|
+
exactly one form is mounted. With two or more, a call without it is refused
|
|
22
|
+
with the mounted ids listed, and an id that is not mounted is always refused.
|
|
23
|
+
A new `form_list` tool returns `{ forms: [{ id, title, screenId, fields }] }`.
|
|
24
|
+
Each registration's `dispose()` removes only that form and settles only its
|
|
25
|
+
pending writes, with the 0.52.0 "outcome unknown" error. The tools are
|
|
26
|
+
withdrawn when the last form on the host is disposed. `pendingMode`, the
|
|
27
|
+
confirm gate, `agent` and the activity target stay per form. This is option A
|
|
28
|
+
of `docs/multi-form-hosts.md`, which now records what was built.
|
|
29
|
+
**Nothing to do for a single-form host:** every existing call shape works
|
|
30
|
+
unchanged. Hosts that gave each form its own MCP host to work around the
|
|
31
|
+
0.51.0 refusal can now mount them all on one host. Several `<BridgedForm>`s
|
|
32
|
+
may share one `server`.
|
|
33
|
+
- Internal: `src/bridges/multi-instance.ts` (`joinInstanceRouter`), the shared
|
|
34
|
+
per-host router behind this. Only the form bridge uses it so far. The design
|
|
35
|
+
note says what adopting it would take for charts, grids and the other
|
|
36
|
+
bridges keyed by an adapter id.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- The form tools are now registered once per host under the single owner
|
|
41
|
+
`"form-bridge"`, not `form:<adapter.id>`. An unrelated registrant claiming a
|
|
42
|
+
form tool name is still refused, and `Bridge.id` is still `form:<adapter.id>`.
|
|
43
|
+
Their descriptions no longer name a form, because they serve every mounted
|
|
44
|
+
form. `form_describe` still returns the form's `id` and `title`.
|
|
45
|
+
- `registerFormBridge` throws if `adapter.id` is missing or empty, since it is
|
|
46
|
+
how agents pick the form. `adapter.id` was already a required `string` in the
|
|
47
|
+
type, so only code that bypassed the type is affected.
|
|
48
|
+
- Registering a form whose `adapter.id` is already mounted replaces it, as a
|
|
49
|
+
remount did before. The earlier registration's `dispose()` leaves the newer
|
|
50
|
+
one in place.
|
|
51
|
+
|
|
14
52
|
## [0.52.0] - 2026-10-30
|
|
15
53
|
|
|
16
54
|
### Added
|
package/README.md
CHANGED
|
@@ -193,9 +193,51 @@ argument: your write is the authority, so check your own version or ETag there
|
|
|
193
193
|
and refuse with `{ ok: false, error: "Draft changed since you read it; re-read
|
|
194
194
|
and retry", code: "stale" }`. The agent gets that sentence as the tool error.
|
|
195
195
|
|
|
196
|
-
The form bridge pushes **no undo entries
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
The form bridge pushes **no undo entries**.
|
|
197
|
+
|
|
198
|
+
### Several forms on one host
|
|
199
|
+
|
|
200
|
+
Since 0.53.0, call `registerFormBridge` once per form on the **same** host. The
|
|
201
|
+
first form registers the form tools; later forms join them, so the tool list
|
|
202
|
+
stays the same eight tools however many forms are mounted:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { registerFormBridge } from "@particle-academy/agent-integrations/bridges/forms";
|
|
206
|
+
|
|
207
|
+
const billing = registerFormBridge(host, { adapter: billingAdapter }); // adapter.id === "billing"
|
|
208
|
+
const shipping = registerFormBridge(host, {
|
|
209
|
+
adapter: shippingAdapter, // adapter.id === "shipping"
|
|
210
|
+
pendingMode: false, // per form: shipping submits without a human confirm
|
|
211
|
+
agent: { id: "checkout-agent", name: "Checkout" },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// An agent then picks the form with the `form` argument:
|
|
215
|
+
await host.callTool("form_list", {}); // [{ id: "billing", … }, { id: "shipping", … }]
|
|
216
|
+
await host.callTool("form_set_value", { form: "shipping", field: "street", value: "2 Ship Rd" });
|
|
217
|
+
await host.callTool("form_set_value", { field: "street", value: "?" });
|
|
218
|
+
// → error: 2 forms are mounted, so pass "form" with one of: "billing", "shipping". form_list describes them.
|
|
219
|
+
|
|
220
|
+
shipping.dispose(); // removes only shipping; billing keeps working, and `form` is optional again
|
|
221
|
+
billing.dispose(); // the last form: every form tool is withdrawn
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
- **`form`** is accepted by every form tool. With exactly one form mounted it is
|
|
225
|
+
optional, so a single-form host and its agent prompts work exactly as before.
|
|
226
|
+
With two or more, a call without it is refused with the mounted ids listed. An
|
|
227
|
+
id that is not mounted is always refused, never routed to another form.
|
|
228
|
+
- **`form_list`** returns `{ forms: [{ id, title, screenId, fields }] }`.
|
|
229
|
+
- Each form is identified by `adapter.id`, which must be a non-empty string.
|
|
230
|
+
Registering an id that is already mounted replaces it (a remount); the earlier
|
|
231
|
+
registration's `dispose()` then leaves the newer one in place.
|
|
232
|
+
- `pendingMode`, `adapter.confirm`, `agent` and the presence target stay **per
|
|
233
|
+
form**; activity names `<formId>:<field>` on the form's own `screenId`.
|
|
234
|
+
- `dispose()` settles only that form's pending writes, with the "outcome
|
|
235
|
+
unknown" error described above.
|
|
236
|
+
- The tools are held by one registrant (`"form-bridge"`), so an unrelated
|
|
237
|
+
registrant claiming a form tool name is still refused.
|
|
238
|
+
|
|
239
|
+
`<BridgedForm>` needs no change: several of them may now share one `server`.
|
|
240
|
+
Background and the alternatives considered:
|
|
199
241
|
[`docs/multi-form-hosts.md`](./docs/multi-form-hosts.md).
|
|
200
242
|
|
|
201
243
|
`registerScreenDocBridge` edits the nodes inside one screen and exports its
|
package/dist/bridges/forms.d.cts
CHANGED
|
@@ -129,13 +129,20 @@ type FormBridgeOptions = {
|
|
|
129
129
|
pendingMode?: boolean;
|
|
130
130
|
};
|
|
131
131
|
/**
|
|
132
|
-
* registerFormBridge — wires schema-driven MCP access to a
|
|
132
|
+
* registerFormBridge — wires schema-driven MCP access to a form.
|
|
133
133
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
134
|
+
* Several forms may share one host. The first form on a host registers the
|
|
135
|
+
* form tools; later ones JOIN them (since 0.53.0; before, a second form threw).
|
|
136
|
+
* Each tool takes an optional `form` argument naming the form by `adapter.id`:
|
|
137
|
+
* optional while exactly one form is mounted, so a single-form host behaves as
|
|
138
|
+
* it always did, and required once two or more are. `form_list` lists what is
|
|
139
|
+
* mounted. The returned `dispose()` removes only this form; the tools are
|
|
140
|
+
* withdrawn when the last form on the host is disposed. A second registration
|
|
141
|
+
* with an id that is already mounted replaces it (a remount); the earlier
|
|
142
|
+
* registration's `dispose()` then leaves the newer one in place.
|
|
143
|
+
*
|
|
144
|
+
* `pendingMode`, the confirm gate, the agent identity and the activity target
|
|
145
|
+
* stay per form. See `docs/multi-form-hosts.md`.
|
|
139
146
|
*/
|
|
140
147
|
declare function registerFormBridge(host: ToolHost, options: FormBridgeOptions): Bridge;
|
|
141
148
|
|
package/dist/bridges/forms.d.ts
CHANGED
|
@@ -129,13 +129,20 @@ type FormBridgeOptions = {
|
|
|
129
129
|
pendingMode?: boolean;
|
|
130
130
|
};
|
|
131
131
|
/**
|
|
132
|
-
* registerFormBridge — wires schema-driven MCP access to a
|
|
132
|
+
* registerFormBridge — wires schema-driven MCP access to a form.
|
|
133
133
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
134
|
+
* Several forms may share one host. The first form on a host registers the
|
|
135
|
+
* form tools; later ones JOIN them (since 0.53.0; before, a second form threw).
|
|
136
|
+
* Each tool takes an optional `form` argument naming the form by `adapter.id`:
|
|
137
|
+
* optional while exactly one form is mounted, so a single-form host behaves as
|
|
138
|
+
* it always did, and required once two or more are. `form_list` lists what is
|
|
139
|
+
* mounted. The returned `dispose()` removes only this form; the tools are
|
|
140
|
+
* withdrawn when the last form on the host is disposed. A second registration
|
|
141
|
+
* with an id that is already mounted replaces it (a remount); the earlier
|
|
142
|
+
* registration's `dispose()` then leaves the newer one in place.
|
|
143
|
+
*
|
|
144
|
+
* `pendingMode`, the confirm gate, the agent identity and the activity target
|
|
145
|
+
* stay per form. See `docs/multi-form-hosts.md`.
|
|
139
146
|
*/
|
|
140
147
|
declare function registerFormBridge(host: ToolHost, options: FormBridgeOptions): Bridge;
|
|
141
148
|
|
package/dist/bridges-forms.cjs
CHANGED
|
@@ -151,16 +151,184 @@ async function commit(run, fallback) {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// src/bridges/multi-instance.ts
|
|
155
|
+
var routers = /* @__PURE__ */ new WeakMap();
|
|
156
|
+
var quoteAll = (ids) => ids.map((id) => `"${id}"`).join(", ");
|
|
157
|
+
function joinInstanceRouter(host, options, id, instance) {
|
|
158
|
+
if (typeof id !== "string" || id.trim() === "") {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`A ${options.family} on a shared host needs a non-empty string id (got ${JSON.stringify(id)}); agents select it by that id through the "${options.argument}" argument.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
let byFamily = routers.get(host);
|
|
164
|
+
if (!byFamily) {
|
|
165
|
+
byFamily = /* @__PURE__ */ new Map();
|
|
166
|
+
routers.set(host, byFamily);
|
|
167
|
+
}
|
|
168
|
+
let state = byFamily.get(options.family);
|
|
169
|
+
if (!state) {
|
|
170
|
+
const instances = /* @__PURE__ */ new Map();
|
|
171
|
+
const router = {
|
|
172
|
+
ids: () => [...instances.keys()],
|
|
173
|
+
entries: () => [...instances.entries()],
|
|
174
|
+
resolve: (requested) => resolve(options, instances, requested)
|
|
175
|
+
};
|
|
176
|
+
const uninstall = options.install(router);
|
|
177
|
+
state = { instances, router, uninstall };
|
|
178
|
+
byFamily.set(options.family, state);
|
|
179
|
+
}
|
|
180
|
+
const current = state;
|
|
181
|
+
current.instances.set(id, instance);
|
|
182
|
+
let left = false;
|
|
183
|
+
const leave = () => {
|
|
184
|
+
if (left) return;
|
|
185
|
+
left = true;
|
|
186
|
+
if (current.instances.get(id) !== instance) return;
|
|
187
|
+
current.instances.delete(id);
|
|
188
|
+
if (current.instances.size === 0 && byFamily.get(options.family) === current) {
|
|
189
|
+
byFamily.delete(options.family);
|
|
190
|
+
current.uninstall();
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
return { router: current.router, leave };
|
|
194
|
+
}
|
|
195
|
+
function resolve(options, instances, requested) {
|
|
196
|
+
const ids = [...instances.keys()];
|
|
197
|
+
const { family, argument, listTool } = options;
|
|
198
|
+
if (requested === void 0 || requested === null || requested === "") {
|
|
199
|
+
if (ids.length === 1) return { ok: true, id: ids[0], instance: instances.get(ids[0]) };
|
|
200
|
+
if (ids.length === 0) return { ok: false, error: `No ${family} is mounted.` };
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
error: `${ids.length} ${family}s are mounted, so pass "${argument}" with one of: ${quoteAll(ids)}. ${listTool} describes them.`
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (typeof requested !== "string") {
|
|
207
|
+
return { ok: false, error: `"${argument}" must be a ${family} id string. Mounted: ${quoteAll(ids)}.` };
|
|
208
|
+
}
|
|
209
|
+
if (!instances.has(requested)) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
error: `No ${family} "${requested}" is mounted. ` + (ids.length ? `Mounted: ${quoteAll(ids)}. ` : "") + `${listTool} lists what is open.`
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return { ok: true, id: requested, instance: instances.get(requested) };
|
|
216
|
+
}
|
|
217
|
+
|
|
154
218
|
// src/bridges/forms.ts
|
|
155
219
|
var DEFAULT_AGENT = { id: "agent", name: "Agent", color: "#a855f7" };
|
|
220
|
+
var FORM_TOOLS_OWNER = "form-bridge";
|
|
221
|
+
var FORM_ARG = {
|
|
222
|
+
type: "string",
|
|
223
|
+
description: "Id of the form to act on (see form_list). Optional while exactly one form is mounted; required when several are."
|
|
224
|
+
};
|
|
225
|
+
var FORM_TOOL_DEFS = [
|
|
226
|
+
{
|
|
227
|
+
name: "form_describe",
|
|
228
|
+
description: "Describe a form \u2014 fields, types, options, required flags. Call this first to know what's fillable.",
|
|
229
|
+
properties: {},
|
|
230
|
+
required: []
|
|
231
|
+
},
|
|
232
|
+
{ name: "form_get_value", description: "Read a single field's current value.", properties: { field: { type: "string" } }, required: ["field"] },
|
|
233
|
+
{ name: "form_get_values", description: "Read every field's current value as a JSON object.", properties: {}, required: [] },
|
|
234
|
+
{
|
|
235
|
+
name: "form_set_value",
|
|
236
|
+
description: "Set one field's value. The host's controlled state updates and the human sees the field change.",
|
|
237
|
+
properties: {
|
|
238
|
+
field: { type: "string" },
|
|
239
|
+
value: { description: "Value to set. Type depends on the field's `type`." }
|
|
240
|
+
},
|
|
241
|
+
required: ["field", "value"]
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: "form_set_values",
|
|
245
|
+
description: "Set multiple fields atomically. Pass a `values` object keyed by field name.",
|
|
246
|
+
properties: { values: { type: "object" } },
|
|
247
|
+
required: ["values"]
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: "form_focus",
|
|
251
|
+
description: "Move browser focus to a field (host-implemented). Useful before streaming text into it.",
|
|
252
|
+
properties: { field: { type: "string" } },
|
|
253
|
+
required: ["field"]
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "form_submit",
|
|
257
|
+
description: "Submit the form. Host returns ok + values (or an error). In pendingMode this is staged for the human to confirm first.",
|
|
258
|
+
properties: {},
|
|
259
|
+
required: []
|
|
260
|
+
}
|
|
261
|
+
];
|
|
262
|
+
function installFormTools(host, router) {
|
|
263
|
+
const disposers = [];
|
|
264
|
+
try {
|
|
265
|
+
for (const def of FORM_TOOL_DEFS) {
|
|
266
|
+
disposers.push(
|
|
267
|
+
host.registerTool(
|
|
268
|
+
{
|
|
269
|
+
name: def.name,
|
|
270
|
+
description: def.description,
|
|
271
|
+
inputSchema: {
|
|
272
|
+
type: "object",
|
|
273
|
+
properties: { form: FORM_ARG, ...def.properties },
|
|
274
|
+
required: def.required,
|
|
275
|
+
additionalProperties: false
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
async (args) => {
|
|
279
|
+
const target = router.resolve(args?.form);
|
|
280
|
+
if (!target.ok) return errorResult(target.error);
|
|
281
|
+
return target.instance.handlers[def.name](args);
|
|
282
|
+
},
|
|
283
|
+
{ owner: FORM_TOOLS_OWNER }
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
disposers.push(
|
|
288
|
+
host.registerTool(
|
|
289
|
+
{
|
|
290
|
+
name: "form_list",
|
|
291
|
+
description: "List the forms mounted on this host \u2014 id, title, screen and fields. Pass an id as `form` to the other form tools when more than one is mounted.",
|
|
292
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
293
|
+
},
|
|
294
|
+
() => {
|
|
295
|
+
try {
|
|
296
|
+
const forms = router.entries().map(([id, { adapter }]) => ({
|
|
297
|
+
id,
|
|
298
|
+
title: adapter.title,
|
|
299
|
+
screenId: adapter.screenId,
|
|
300
|
+
fields: adapter.getFields()
|
|
301
|
+
}));
|
|
302
|
+
const text = forms.map((f) => `${f.id}${f.title ? ` \u2014 ${f.title}` : ""}: ${f.fields.map((x) => x.name).join(", ") || "(no fields)"}`).join("\n");
|
|
303
|
+
return textResult(text || "(no forms)", { forms });
|
|
304
|
+
} catch (e) {
|
|
305
|
+
return errorResult(hostErrorMessage(e, "form_list failed."));
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
{ owner: FORM_TOOLS_OWNER }
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
} catch (e) {
|
|
312
|
+
for (const d of disposers) d();
|
|
313
|
+
throw e;
|
|
314
|
+
}
|
|
315
|
+
return () => {
|
|
316
|
+
for (const d of disposers) d();
|
|
317
|
+
};
|
|
318
|
+
}
|
|
156
319
|
function registerFormBridge(host, options) {
|
|
157
320
|
const { adapter } = options;
|
|
158
321
|
const agent = { ...DEFAULT_AGENT, ...options.agent ?? {} };
|
|
159
322
|
const pendingMode = options.pendingMode ?? true;
|
|
160
|
-
const
|
|
161
|
-
|
|
323
|
+
const formId = adapter?.id == null ? "" : String(adapter.id);
|
|
324
|
+
if (formId.trim() === "") {
|
|
325
|
+
throw new Error(
|
|
326
|
+
"registerFormBridge: adapter.id must be a non-empty string. It is how agents pick this form (the `form` argument) when several forms share a host."
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const bridgeId = `form:${formId}`;
|
|
162
330
|
ensureUndoToolsRegistered(host, { defaultAgentId: agent.id });
|
|
163
|
-
const
|
|
331
|
+
const handlers = {};
|
|
164
332
|
let disposed = false;
|
|
165
333
|
const onDispose = /* @__PURE__ */ new Set();
|
|
166
334
|
const CLOSED = /* @__PURE__ */ Symbol("form-closed");
|
|
@@ -170,13 +338,13 @@ function registerFormBridge(host, options) {
|
|
|
170
338
|
});
|
|
171
339
|
return Promise.resolve(CLOSED);
|
|
172
340
|
}
|
|
173
|
-
return new Promise((
|
|
174
|
-
const close = () =>
|
|
341
|
+
return new Promise((resolve2, reject) => {
|
|
342
|
+
const close = () => resolve2(CLOSED);
|
|
175
343
|
onDispose.add(close);
|
|
176
344
|
p.then(
|
|
177
345
|
(v) => {
|
|
178
346
|
onDispose.delete(close);
|
|
179
|
-
|
|
347
|
+
resolve2(v);
|
|
180
348
|
},
|
|
181
349
|
(e) => {
|
|
182
350
|
onDispose.delete(close);
|
|
@@ -194,7 +362,7 @@ function registerFormBridge(host, options) {
|
|
|
194
362
|
elementId: typeof args?.field === "string" ? `${formId}:${args.field}` : formId,
|
|
195
363
|
label: typeof args?.field === "string" ? `${adapter.title ?? formId} \u2192 ${args.field}` : adapter.title ?? formId
|
|
196
364
|
});
|
|
197
|
-
const reg = (name,
|
|
365
|
+
const reg = (name, handler, isMutation) => {
|
|
198
366
|
const wrapped = async (args) => {
|
|
199
367
|
try {
|
|
200
368
|
return await handler(args);
|
|
@@ -209,23 +377,10 @@ function registerFormBridge(host, options) {
|
|
|
209
377
|
screenId: adapter.screenId,
|
|
210
378
|
resolveTarget: ({ args }) => target(args)
|
|
211
379
|
}) : wrapped;
|
|
212
|
-
|
|
213
|
-
host.registerTool(
|
|
214
|
-
{
|
|
215
|
-
name,
|
|
216
|
-
description,
|
|
217
|
-
inputSchema: { type: "object", properties, required, additionalProperties: false }
|
|
218
|
-
},
|
|
219
|
-
final,
|
|
220
|
-
{ owner: bridgeId }
|
|
221
|
-
)
|
|
222
|
-
);
|
|
380
|
+
handlers[name] = final;
|
|
223
381
|
};
|
|
224
382
|
reg(
|
|
225
383
|
"form_describe",
|
|
226
|
-
`Describe the form "${formId}" \u2014 fields, types, options, required flags. Call this first to know what's fillable.`,
|
|
227
|
-
{},
|
|
228
|
-
[],
|
|
229
384
|
() => {
|
|
230
385
|
const fields = adapter.getFields();
|
|
231
386
|
const text = fields.map((f) => `${f.name}${f.required ? "*" : ""} (${f.type})${f.label ? ` \u2014 ${f.label}` : ""}`).join("\n");
|
|
@@ -235,9 +390,6 @@ function registerFormBridge(host, options) {
|
|
|
235
390
|
);
|
|
236
391
|
reg(
|
|
237
392
|
"form_get_value",
|
|
238
|
-
"Read a single field's current value.",
|
|
239
|
-
{ field: { type: "string" } },
|
|
240
|
-
["field"],
|
|
241
393
|
(args) => {
|
|
242
394
|
const name = String(args.field ?? "");
|
|
243
395
|
if (!adapter.getFields().find((f) => f.name === name)) {
|
|
@@ -250,9 +402,6 @@ function registerFormBridge(host, options) {
|
|
|
250
402
|
);
|
|
251
403
|
reg(
|
|
252
404
|
"form_get_values",
|
|
253
|
-
"Read every field's current value as a JSON object.",
|
|
254
|
-
{},
|
|
255
|
-
[],
|
|
256
405
|
() => {
|
|
257
406
|
const values = adapter.getValues();
|
|
258
407
|
return textResult(JSON.stringify(values, null, 2), values);
|
|
@@ -261,12 +410,6 @@ function registerFormBridge(host, options) {
|
|
|
261
410
|
);
|
|
262
411
|
reg(
|
|
263
412
|
"form_set_value",
|
|
264
|
-
"Set one field's value. The host's controlled state updates and the human sees the field change.",
|
|
265
|
-
{
|
|
266
|
-
field: { type: "string" },
|
|
267
|
-
value: { description: "Value to set. Type depends on the field's `type`." }
|
|
268
|
-
},
|
|
269
|
-
["field", "value"],
|
|
270
413
|
async (args) => {
|
|
271
414
|
const name = String(args.field ?? "");
|
|
272
415
|
const fieldDef = adapter.getFields().find((f) => f.name === name);
|
|
@@ -282,9 +425,6 @@ function registerFormBridge(host, options) {
|
|
|
282
425
|
);
|
|
283
426
|
reg(
|
|
284
427
|
"form_set_values",
|
|
285
|
-
"Set multiple fields atomically. Pass a `values` object keyed by field name.",
|
|
286
|
-
{ values: { type: "object" } },
|
|
287
|
-
["values"],
|
|
288
428
|
async (args) => {
|
|
289
429
|
const values = args.values && typeof args.values === "object" ? args.values : {};
|
|
290
430
|
const fields = adapter.getFields();
|
|
@@ -338,9 +478,6 @@ function registerFormBridge(host, options) {
|
|
|
338
478
|
);
|
|
339
479
|
reg(
|
|
340
480
|
"form_focus",
|
|
341
|
-
"Move browser focus to a field (host-implemented). Useful before streaming text into it.",
|
|
342
|
-
{ field: { type: "string" } },
|
|
343
|
-
["field"],
|
|
344
481
|
(args) => {
|
|
345
482
|
const name = String(args.field ?? "");
|
|
346
483
|
if (!adapter.focus) return errorResult("Host did not provide a focus implementation.");
|
|
@@ -354,9 +491,6 @@ function registerFormBridge(host, options) {
|
|
|
354
491
|
);
|
|
355
492
|
reg(
|
|
356
493
|
"form_submit",
|
|
357
|
-
"Submit the form. Host returns ok + values (or an error). In pendingMode this is staged for the human to confirm first.",
|
|
358
|
-
{},
|
|
359
|
-
[],
|
|
360
494
|
async () => {
|
|
361
495
|
if (!adapter.submit) return errorResult("Host did not provide a submit implementation.");
|
|
362
496
|
if (pendingMode && adapter.confirm) {
|
|
@@ -380,12 +514,19 @@ function registerFormBridge(host, options) {
|
|
|
380
514
|
},
|
|
381
515
|
true
|
|
382
516
|
);
|
|
517
|
+
const membership = joinInstanceRouter(
|
|
518
|
+
host,
|
|
519
|
+
{ family: "form", argument: "form", listTool: "form_list", install: (router) => installFormTools(host, router) },
|
|
520
|
+
formId,
|
|
521
|
+
{ adapter, handlers }
|
|
522
|
+
);
|
|
383
523
|
return {
|
|
384
524
|
id: bridgeId,
|
|
385
525
|
title: adapter.title ?? formId,
|
|
386
526
|
dispose: () => {
|
|
527
|
+
if (disposed) return;
|
|
387
528
|
disposed = true;
|
|
388
|
-
|
|
529
|
+
membership.leave();
|
|
389
530
|
for (const close of [...onDispose]) close();
|
|
390
531
|
onDispose.clear();
|
|
391
532
|
}
|