@particle-academy/agent-integrations 0.51.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 +78 -0
- package/README.md +100 -0
- package/dist/bridges/forms.d.cts +60 -8
- package/dist/bridges/forms.d.ts +60 -8
- package/dist/bridges-forms.cjs +293 -51
- package/dist/bridges-forms.cjs.map +1 -1
- package/dist/bridges-forms.js +2 -1
- package/dist/bridges-screens.cjs +4 -2
- package/dist/bridges-screens.cjs.map +1 -1
- package/dist/bridges-screens.js +2 -1
- package/dist/{chunk-CP3EHLZJ.js → chunk-7HHZBPDE.js} +3 -25
- package/dist/chunk-7HHZBPDE.js.map +1 -0
- package/dist/chunk-AET2R7DJ.js +391 -0
- package/dist/chunk-AET2R7DJ.js.map +1 -0
- package/dist/chunk-DPBAEWB5.js +28 -0
- package/dist/chunk-DPBAEWB5.js.map +1 -0
- package/dist/index.cjs +304 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/docs/multi-form-hosts.md +163 -0
- package/package.json +1 -1
- package/dist/chunk-CP3EHLZJ.js.map +0 -1
- package/dist/chunk-DNF3QVTW.js +0 -172
- package/dist/chunk-DNF3QVTW.js.map +0 -1
package/dist/bridges-forms.cjs
CHANGED
|
@@ -126,28 +126,248 @@ function serialize(entry) {
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// src/bridges/host-commit.ts
|
|
130
|
+
function hostErrorMessage(e, fallback) {
|
|
131
|
+
let msg;
|
|
132
|
+
if (e instanceof Error) msg = e.message;
|
|
133
|
+
else if (typeof e === "string") msg = e;
|
|
134
|
+
else if (e && typeof e === "object" && typeof e.message === "string") {
|
|
135
|
+
msg = e.message;
|
|
136
|
+
}
|
|
137
|
+
const firstFrame = msg ? msg.search(/\n\s+at\s/) : -1;
|
|
138
|
+
if (msg && firstFrame >= 0) msg = msg.slice(0, firstFrame);
|
|
139
|
+
msg = msg?.trim();
|
|
140
|
+
return msg ? msg : fallback;
|
|
141
|
+
}
|
|
142
|
+
async function commit(run, fallback) {
|
|
143
|
+
try {
|
|
144
|
+
const r = await run();
|
|
145
|
+
if (r && typeof r === "object" && r.ok === false) {
|
|
146
|
+
return { ok: false, error: hostErrorMessage(r.error, fallback) };
|
|
147
|
+
}
|
|
148
|
+
return { ok: true };
|
|
149
|
+
} catch (e) {
|
|
150
|
+
return { ok: false, error: hostErrorMessage(e, fallback) };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
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
|
+
|
|
129
218
|
// src/bridges/forms.ts
|
|
130
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
|
+
}
|
|
131
319
|
function registerFormBridge(host, options) {
|
|
132
320
|
const { adapter } = options;
|
|
133
321
|
const agent = { ...DEFAULT_AGENT, ...options.agent ?? {} };
|
|
134
322
|
const pendingMode = options.pendingMode ?? true;
|
|
135
|
-
const
|
|
136
|
-
|
|
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}`;
|
|
137
330
|
ensureUndoToolsRegistered(host, { defaultAgentId: agent.id });
|
|
138
|
-
const
|
|
331
|
+
const handlers = {};
|
|
332
|
+
let disposed = false;
|
|
333
|
+
const onDispose = /* @__PURE__ */ new Set();
|
|
334
|
+
const CLOSED = /* @__PURE__ */ Symbol("form-closed");
|
|
335
|
+
const untilClosed = (p) => {
|
|
336
|
+
if (disposed) {
|
|
337
|
+
p.catch(() => {
|
|
338
|
+
});
|
|
339
|
+
return Promise.resolve(CLOSED);
|
|
340
|
+
}
|
|
341
|
+
return new Promise((resolve2, reject) => {
|
|
342
|
+
const close = () => resolve2(CLOSED);
|
|
343
|
+
onDispose.add(close);
|
|
344
|
+
p.then(
|
|
345
|
+
(v) => {
|
|
346
|
+
onDispose.delete(close);
|
|
347
|
+
resolve2(v);
|
|
348
|
+
},
|
|
349
|
+
(e) => {
|
|
350
|
+
onDispose.delete(close);
|
|
351
|
+
reject(e);
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
const closedUnknown = (what) => errorResult(
|
|
357
|
+
`Form "${formId}" was closed before the host confirmed ${what}, so whether it was applied is unknown. Do not assume it was. If the form is open again, read it back (form_get_values) before retrying.`
|
|
358
|
+
);
|
|
139
359
|
const target = (args) => ({
|
|
140
360
|
kind: "form",
|
|
141
361
|
screenId: adapter.screenId,
|
|
142
362
|
elementId: typeof args?.field === "string" ? `${formId}:${args.field}` : formId,
|
|
143
363
|
label: typeof args?.field === "string" ? `${adapter.title ?? formId} \u2192 ${args.field}` : adapter.title ?? formId
|
|
144
364
|
});
|
|
145
|
-
const reg = (name,
|
|
365
|
+
const reg = (name, handler, isMutation) => {
|
|
146
366
|
const wrapped = async (args) => {
|
|
147
367
|
try {
|
|
148
368
|
return await handler(args);
|
|
149
369
|
} catch (e) {
|
|
150
|
-
return errorResult(e
|
|
370
|
+
return errorResult(hostErrorMessage(e, `${name} failed.`));
|
|
151
371
|
}
|
|
152
372
|
};
|
|
153
373
|
const final = isMutation ? wrapToolWithActivity(wrapped, {
|
|
@@ -157,23 +377,10 @@ function registerFormBridge(host, options) {
|
|
|
157
377
|
screenId: adapter.screenId,
|
|
158
378
|
resolveTarget: ({ args }) => target(args)
|
|
159
379
|
}) : wrapped;
|
|
160
|
-
|
|
161
|
-
host.registerTool(
|
|
162
|
-
{
|
|
163
|
-
name,
|
|
164
|
-
description,
|
|
165
|
-
inputSchema: { type: "object", properties, required, additionalProperties: false }
|
|
166
|
-
},
|
|
167
|
-
final,
|
|
168
|
-
{ owner: bridgeId }
|
|
169
|
-
)
|
|
170
|
-
);
|
|
380
|
+
handlers[name] = final;
|
|
171
381
|
};
|
|
172
382
|
reg(
|
|
173
383
|
"form_describe",
|
|
174
|
-
`Describe the form "${formId}" \u2014 fields, types, options, required flags. Call this first to know what's fillable.`,
|
|
175
|
-
{},
|
|
176
|
-
[],
|
|
177
384
|
() => {
|
|
178
385
|
const fields = adapter.getFields();
|
|
179
386
|
const text = fields.map((f) => `${f.name}${f.required ? "*" : ""} (${f.type})${f.label ? ` \u2014 ${f.label}` : ""}`).join("\n");
|
|
@@ -183,9 +390,6 @@ function registerFormBridge(host, options) {
|
|
|
183
390
|
);
|
|
184
391
|
reg(
|
|
185
392
|
"form_get_value",
|
|
186
|
-
"Read a single field's current value.",
|
|
187
|
-
{ field: { type: "string" } },
|
|
188
|
-
["field"],
|
|
189
393
|
(args) => {
|
|
190
394
|
const name = String(args.field ?? "");
|
|
191
395
|
if (!adapter.getFields().find((f) => f.name === name)) {
|
|
@@ -198,9 +402,6 @@ function registerFormBridge(host, options) {
|
|
|
198
402
|
);
|
|
199
403
|
reg(
|
|
200
404
|
"form_get_values",
|
|
201
|
-
"Read every field's current value as a JSON object.",
|
|
202
|
-
{},
|
|
203
|
-
[],
|
|
204
405
|
() => {
|
|
205
406
|
const values = adapter.getValues();
|
|
206
407
|
return textResult(JSON.stringify(values, null, 2), values);
|
|
@@ -209,36 +410,67 @@ function registerFormBridge(host, options) {
|
|
|
209
410
|
);
|
|
210
411
|
reg(
|
|
211
412
|
"form_set_value",
|
|
212
|
-
|
|
213
|
-
{
|
|
214
|
-
field: { type: "string" },
|
|
215
|
-
value: { description: "Value to set. Type depends on the field's `type`." }
|
|
216
|
-
},
|
|
217
|
-
["field", "value"],
|
|
218
|
-
(args) => {
|
|
413
|
+
async (args) => {
|
|
219
414
|
const name = String(args.field ?? "");
|
|
220
415
|
const fieldDef = adapter.getFields().find((f) => f.name === name);
|
|
221
416
|
if (!fieldDef) return errorResult(`Unknown field: ${name}`);
|
|
222
|
-
|
|
417
|
+
const done = await untilClosed(
|
|
418
|
+
commit(() => adapter.setValue(name, args.value), `Host refused the write to "${name}".`)
|
|
419
|
+
);
|
|
420
|
+
if (done === CLOSED) return closedUnknown(`the write to "${name}"`);
|
|
421
|
+
if (!done.ok) return errorResult(done.error);
|
|
223
422
|
return textResult(`${name} \u2190 ${JSON.stringify(args.value)}`, { field: name, value: args.value });
|
|
224
423
|
},
|
|
225
424
|
true
|
|
226
425
|
);
|
|
227
426
|
reg(
|
|
228
427
|
"form_set_values",
|
|
229
|
-
|
|
230
|
-
{ values: { type: "object" } },
|
|
231
|
-
["values"],
|
|
232
|
-
(args) => {
|
|
428
|
+
async (args) => {
|
|
233
429
|
const values = args.values && typeof args.values === "object" ? args.values : {};
|
|
234
430
|
const fields = adapter.getFields();
|
|
235
431
|
const known = new Set(fields.map((f) => f.name));
|
|
236
432
|
const unknownKeys = Object.keys(values).filter((k) => !known.has(k));
|
|
237
433
|
if (unknownKeys.length) return errorResult(`Unknown fields: ${unknownKeys.join(", ")}`);
|
|
238
|
-
|
|
239
|
-
|
|
434
|
+
const setValues = adapter.setValues;
|
|
435
|
+
if (setValues) {
|
|
436
|
+
const done = await untilClosed(commit(() => setValues(values), "Host refused the write."));
|
|
437
|
+
if (done === CLOSED) return closedUnknown("the write");
|
|
438
|
+
if (!done.ok) return errorResult(done.error);
|
|
240
439
|
} else {
|
|
241
|
-
|
|
440
|
+
const keys = Object.keys(values);
|
|
441
|
+
const committed = [];
|
|
442
|
+
for (const [i, k] of keys.entries()) {
|
|
443
|
+
const done = await untilClosed(
|
|
444
|
+
commit(() => adapter.setValue(k, values[k]), `Host refused the write to "${k}".`)
|
|
445
|
+
);
|
|
446
|
+
if (done === CLOSED) {
|
|
447
|
+
return {
|
|
448
|
+
...closedUnknown(`the write to "${k}"`),
|
|
449
|
+
structuredContent: { committed, pending: k, notAttempted: keys.slice(i + 1) }
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
if (!done.ok) {
|
|
453
|
+
const state = { committed, failed: k, notAttempted: keys.slice(i + 1) };
|
|
454
|
+
if (committed.length) {
|
|
455
|
+
fancyAutoCommon.emitActivity({
|
|
456
|
+
agentId: agent.id,
|
|
457
|
+
agentName: agent.name,
|
|
458
|
+
agentColor: agent.color,
|
|
459
|
+
target: target({}),
|
|
460
|
+
action: "form_set_values",
|
|
461
|
+
timestamp: Date.now(),
|
|
462
|
+
meta: state
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
...errorResult(
|
|
467
|
+
`"${k}" was refused: ${done.error} ` + (committed.length ? `Already committed and NOT rolled back: ${committed.join(", ")}. ` : "Nothing was committed. ") + (state.notAttempted.length ? `Not attempted: ${state.notAttempted.join(", ")}.` : "")
|
|
468
|
+
),
|
|
469
|
+
structuredContent: state
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
committed.push(k);
|
|
473
|
+
}
|
|
242
474
|
}
|
|
243
475
|
return textResult(`Set ${Object.keys(values).length} field(s)`, { values });
|
|
244
476
|
},
|
|
@@ -246,9 +478,6 @@ function registerFormBridge(host, options) {
|
|
|
246
478
|
);
|
|
247
479
|
reg(
|
|
248
480
|
"form_focus",
|
|
249
|
-
"Move browser focus to a field (host-implemented). Useful before streaming text into it.",
|
|
250
|
-
{ field: { type: "string" } },
|
|
251
|
-
["field"],
|
|
252
481
|
(args) => {
|
|
253
482
|
const name = String(args.field ?? "");
|
|
254
483
|
if (!adapter.focus) return errorResult("Host did not provide a focus implementation.");
|
|
@@ -262,31 +491,44 @@ function registerFormBridge(host, options) {
|
|
|
262
491
|
);
|
|
263
492
|
reg(
|
|
264
493
|
"form_submit",
|
|
265
|
-
"Submit the form. Host returns ok + values (or an error). In pendingMode this is staged for the human to confirm first.",
|
|
266
|
-
{},
|
|
267
|
-
[],
|
|
268
494
|
async () => {
|
|
269
495
|
if (!adapter.submit) return errorResult("Host did not provide a submit implementation.");
|
|
270
496
|
if (pendingMode && adapter.confirm) {
|
|
271
|
-
const
|
|
497
|
+
const confirm = adapter.confirm;
|
|
498
|
+
const ok = await untilClosed(Promise.resolve().then(() => confirm({
|
|
272
499
|
action: "submit",
|
|
273
500
|
form: formId,
|
|
274
501
|
title: adapter.title,
|
|
275
502
|
values: adapter.getValues()
|
|
276
|
-
});
|
|
503
|
+
})));
|
|
504
|
+
if (ok === CLOSED) {
|
|
505
|
+
return errorResult(`Form "${formId}" was closed before the submit was confirmed. Nothing was submitted.`);
|
|
506
|
+
}
|
|
277
507
|
if (!ok) return errorResult("Declined by user");
|
|
278
508
|
}
|
|
279
|
-
const
|
|
280
|
-
|
|
509
|
+
const submit = adapter.submit;
|
|
510
|
+
const result = await untilClosed(Promise.resolve().then(() => submit()));
|
|
511
|
+
if (result === CLOSED) return closedUnknown("the submit");
|
|
512
|
+
if (!result.ok) return errorResult(hostErrorMessage(result.error, "Submit failed"));
|
|
281
513
|
return textResult("Submitted", { values: result.values });
|
|
282
514
|
},
|
|
283
515
|
true
|
|
284
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
|
+
);
|
|
285
523
|
return {
|
|
286
524
|
id: bridgeId,
|
|
287
525
|
title: adapter.title ?? formId,
|
|
288
526
|
dispose: () => {
|
|
289
|
-
|
|
527
|
+
if (disposed) return;
|
|
528
|
+
disposed = true;
|
|
529
|
+
membership.leave();
|
|
530
|
+
for (const close of [...onDispose]) close();
|
|
531
|
+
onDispose.clear();
|
|
290
532
|
}
|
|
291
533
|
};
|
|
292
534
|
}
|