@jini-ai/ui 0.3.11 → 0.3.12
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.
|
@@ -13,6 +13,16 @@ export interface CheckboxProps {
|
|
|
13
13
|
*/
|
|
14
14
|
readonly required?: boolean;
|
|
15
15
|
readonly disabled?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* One extra `data-*` attribute on the `<input>`, escaped like every other value. For a caller
|
|
18
|
+
* that needs to look the box up by something other than its DOM `name` — e.g. the confirmation
|
|
19
|
+
* surface's per-choice ids, which are caller data and may contain characters `fieldElementId`'s
|
|
20
|
+
* `name` validation disallows.
|
|
21
|
+
*/
|
|
22
|
+
readonly dataAttribute?: {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly value: string;
|
|
25
|
+
};
|
|
16
26
|
}
|
|
17
27
|
/**
|
|
18
28
|
* Renders one boolean field.
|
|
@@ -16,10 +16,14 @@ import { fieldDescribedBy, fieldElementId, renderFieldLabel } from './document.j
|
|
|
16
16
|
*/
|
|
17
17
|
export function renderCheckbox(props) {
|
|
18
18
|
const id = fieldElementId(props.name);
|
|
19
|
+
const dataAttribute = props.dataAttribute === undefined
|
|
20
|
+
? ''
|
|
21
|
+
: ` ${escapeHtml(props.dataAttribute.name)}="${escapeHtml(props.dataAttribute.value)}"`;
|
|
19
22
|
const attributes = ` id="${escapeHtml(id)}" name="${escapeHtml(props.name)}"` +
|
|
20
23
|
(props.value === true ? ' checked' : '') +
|
|
21
24
|
(props.required === true ? ' required' : '') +
|
|
22
25
|
(props.disabled === true ? ' disabled' : '') +
|
|
26
|
+
dataAttribute +
|
|
23
27
|
fieldDescribedBy(props);
|
|
24
28
|
return `<div class="mcpui-field mcpui-field-inline">\n<input class="mcpui-checkbox" type="checkbox"${attributes}>\n<span>${renderFieldLabel(props)}</span>\n</div>`;
|
|
25
29
|
}
|
|
@@ -38,11 +38,37 @@ export interface ConfirmationToolAction {
|
|
|
38
38
|
/** Passed verbatim as the tool's `arguments`. The only safe home for a confirmation token. */
|
|
39
39
|
readonly params: Readonly<Record<string, unknown>>;
|
|
40
40
|
}
|
|
41
|
+
/** One row of an optional per-item checkbox list — see {@link ConfirmationSurfaceSpec.choices}. */
|
|
42
|
+
export interface ConfirmationChoice {
|
|
43
|
+
/**
|
|
44
|
+
* The value reported back when this box is ticked. Caller data, not a DOM name: it may contain
|
|
45
|
+
* any character, because it never becomes an HTML `name`/`id` (those are `choice-<index>` in the
|
|
46
|
+
* DOM; see `renderChoices`) — only a `data-mcpui-choice` attribute value, which any string escapes
|
|
47
|
+
* into safely.
|
|
48
|
+
*/
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly label: string;
|
|
51
|
+
readonly hint?: string;
|
|
52
|
+
}
|
|
41
53
|
export interface ConfirmationSurfaceSpec {
|
|
42
54
|
readonly title: string;
|
|
43
55
|
readonly description?: string;
|
|
44
56
|
/** The facts being agreed about. A dialog that says "delete this?" without naming *this* is not consent. */
|
|
45
57
|
readonly details?: readonly SurfaceDetail[];
|
|
58
|
+
/**
|
|
59
|
+
* Optional per-row checkboxes, rendered unchecked between the details and the warning. Ticking one
|
|
60
|
+
* never confirms anything by itself — the same property the module doc opens with, extended to a
|
|
61
|
+
* second input: a tick reaches a tool call ONLY by riding along on confirm's own trusted,
|
|
62
|
+
* past-dwell click, merged into {@link ConfirmationToolAction.params} under {@link choicesParam}
|
|
63
|
+
* at the moment of that click and never before. With no `choices`, the rendered document and the
|
|
64
|
+
* params confirm posts are byte-identical to a spec written before this field existed.
|
|
65
|
+
*/
|
|
66
|
+
readonly choices?: readonly ConfirmationChoice[];
|
|
67
|
+
/**
|
|
68
|
+
* The params key the ticked ids are posted under, merged into (never replacing) `confirm.params`.
|
|
69
|
+
* Defaults to `"overwrite"`.
|
|
70
|
+
*/
|
|
71
|
+
readonly choicesParam?: string;
|
|
46
72
|
/** A callout above the buttons — the consequence that is not obvious from the details alone. */
|
|
47
73
|
readonly warning?: string;
|
|
48
74
|
/** Styles the affirmative button as destructive. Affects presentation only; it changes no behavior. */
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { createUIResource } from '../resource.js';
|
|
31
31
|
import { escapeHtml, escapeJsValue } from '../escape.js';
|
|
32
32
|
import { DEFAULT_SURFACE_STATUS_TEXT, SURFACE_SCRIPT_PRELUDE, renderActions, renderDetailList, renderStatusRegion, renderSurfaceDocument, renderSurfaceHeader, } from './document.js';
|
|
33
|
+
import { renderCheckbox } from './checkbox.js';
|
|
33
34
|
/**
|
|
34
35
|
* How long the dialog must have been visible before a confirm click counts, in ms. A click sooner
|
|
35
36
|
* than a person could have read the dialog is ignored — the same dwell browsers put on permission
|
|
@@ -42,6 +43,38 @@ const DEFAULT_APP = { appName: 'jini-mcp-ui-confirmation', appVersion: '1' };
|
|
|
42
43
|
function isToolAction(action) {
|
|
43
44
|
return action !== undefined && 'toolName' in action;
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Renders the optional choice checkboxes, one unchecked box per {@link ConfirmationChoice}.
|
|
48
|
+
*
|
|
49
|
+
* Named `choice-<index>` in the DOM — never by `choice.id`, which is caller data and may contain
|
|
50
|
+
* characters `fieldElementId`'s `name` validation rejects — with the real id carried on the
|
|
51
|
+
* `data-mcpui-choice` attribute instead, which the script reads back on change.
|
|
52
|
+
*
|
|
53
|
+
* @returns `''` for an empty list, matching every other optional fragment in this document.
|
|
54
|
+
*/
|
|
55
|
+
function renderChoices(choices) {
|
|
56
|
+
if (choices.length === 0)
|
|
57
|
+
return '';
|
|
58
|
+
// Ticked state is keyed by id, so two boxes sharing one would tick and report as one.
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
for (const choice of choices) {
|
|
61
|
+
if (seen.has(choice.id)) {
|
|
62
|
+
throw new Error(`Confirmation choices must have unique ids; ${JSON.stringify(choice.id)} appears more than once.`);
|
|
63
|
+
}
|
|
64
|
+
seen.add(choice.id);
|
|
65
|
+
}
|
|
66
|
+
const boxes = choices
|
|
67
|
+
.map((choice, index) => renderCheckbox({
|
|
68
|
+
name: `choice-${index}`,
|
|
69
|
+
label: choice.label,
|
|
70
|
+
...(choice.hint === undefined ? {} : { hint: choice.hint }),
|
|
71
|
+
dataAttribute: { name: 'data-mcpui-choice', value: choice.id },
|
|
72
|
+
}))
|
|
73
|
+
.join('\n');
|
|
74
|
+
// Reuses form.ts's `.mcpui-fields` spacing rather than inventing a second stylesheet rule for the
|
|
75
|
+
// same "stack of controls" layout.
|
|
76
|
+
return `<fieldset class="mcpui-fields">\n${boxes}\n</fieldset>`;
|
|
77
|
+
}
|
|
45
78
|
/**
|
|
46
79
|
* Renders the confirmation dialog as a complete, self-contained HTML document.
|
|
47
80
|
*
|
|
@@ -67,11 +100,18 @@ function confirmationActions(spec) {
|
|
|
67
100
|
}
|
|
68
101
|
export function renderConfirmationDocument(spec) {
|
|
69
102
|
const text = { ...DEFAULT_SURFACE_STATUS_TEXT, ...spec.text };
|
|
103
|
+
const choicesParam = spec.choicesParam ?? 'overwrite';
|
|
104
|
+
// The ticked ids are merged into confirm.params at click time; a key already there (the token,
|
|
105
|
+
// say) would be silently replaced by an array.
|
|
106
|
+
if ((spec.choices ?? []).length > 0 && Object.prototype.hasOwnProperty.call(spec.confirm.params, choicesParam)) {
|
|
107
|
+
throw new Error(`choicesParam ${JSON.stringify(choicesParam)} collides with a key already in confirm.params.`);
|
|
108
|
+
}
|
|
70
109
|
const actions = confirmationActions(spec);
|
|
71
110
|
const warning = spec.warning === undefined ? '' : `<p class="mcpui-warning">${escapeHtml(spec.warning)}</p>`;
|
|
72
111
|
const bodyHtml = [
|
|
73
112
|
renderSurfaceHeader(spec.description === undefined ? { title: spec.title } : { title: spec.title, description: spec.description }),
|
|
74
113
|
renderDetailList(spec.details ?? []),
|
|
114
|
+
renderChoices(spec.choices ?? []),
|
|
75
115
|
warning,
|
|
76
116
|
// Confirm starts disabled; the script enables it once the dwell has run out.
|
|
77
117
|
renderActions(actions.map((action) => (action.id === 'confirm' ? { ...action, disabled: true } : action))),
|
|
@@ -96,6 +136,7 @@ ${SURFACE_SCRIPT_PRELUDE}
|
|
|
96
136
|
var PLAN = ${escapeJsValue(plan)};
|
|
97
137
|
var TEXT = ${escapeJsValue(text)};
|
|
98
138
|
var DWELL_MS = ${CONFIRM_DWELL_MS};
|
|
139
|
+
var CHOICES_PARAM = ${escapeJsValue(choicesParam)};
|
|
99
140
|
|
|
100
141
|
// Monotonic where available: a wall clock set backwards would otherwise stretch the dwell.
|
|
101
142
|
function now() {
|
|
@@ -141,6 +182,47 @@ ${SURFACE_SCRIPT_PRELUDE}
|
|
|
141
182
|
actionButtons[i].addEventListener("click", onClick);
|
|
142
183
|
}
|
|
143
184
|
|
|
185
|
+
// Ticked state, tracked ourselves rather than trusted from each input's own \`.checked\` at
|
|
186
|
+
// confirm-time -- the same "only a browser-attributed action counts" rule \`onClick\` applies to
|
|
187
|
+
// the buttons, extended to the one other interactive element this document can contain. Keyed by
|
|
188
|
+
// \`data-mcpui-choice\` (the caller's id), not DOM order, so a checkbox reordering upstream could
|
|
189
|
+
// never silently swap which id a tick reports.
|
|
190
|
+
var choiceInputs = Array.prototype.slice.call(document.querySelectorAll("[data-mcpui-choice]"));
|
|
191
|
+
var checkedIds = {};
|
|
192
|
+
for (var c = 0; c < choiceInputs.length; c++) {
|
|
193
|
+
choiceInputs[c].addEventListener("change", onChoiceChange);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function onChoiceChange(event) {
|
|
197
|
+
var id = event.currentTarget.getAttribute("data-mcpui-choice");
|
|
198
|
+
if (event.isTrusted !== true) {
|
|
199
|
+
// Put the box back to what we last recorded, so a script-driven toggle from anywhere in this
|
|
200
|
+
// document can never leave the visible box and the ids confirm would send disagreeing.
|
|
201
|
+
event.currentTarget.checked = checkedIds[id] === true;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (event.currentTarget.checked) checkedIds[id] = true;
|
|
205
|
+
else delete checkedIds[id];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Frozen with the buttons while a call is in flight and for good once one settles the dialog, so
|
|
209
|
+
// the ticks on screen are always the ones that were sent.
|
|
210
|
+
function setChoicesDisabled(disabled) {
|
|
211
|
+
for (var k = 0; k < choiceInputs.length; k++) choiceInputs[k].disabled = disabled;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// DOM order, not \`for...in\` over checkedIds: object key order for arbitrary caller-supplied
|
|
215
|
+
// strings is not something to depend on, even where every engine we run on happens to preserve it.
|
|
216
|
+
function checkedChoiceIds() {
|
|
217
|
+
var ids = [];
|
|
218
|
+
for (var j = 0; j < choiceInputs.length; j++) {
|
|
219
|
+
var choiceId = choiceInputs[j].getAttribute("data-mcpui-choice");
|
|
220
|
+
// Both: a trusted tick recorded, and the box still showing it. Never send what isn't on screen.
|
|
221
|
+
if (checkedIds[choiceId] === true && choiceInputs[j].checked) ids.push(choiceId);
|
|
222
|
+
}
|
|
223
|
+
return ids;
|
|
224
|
+
}
|
|
225
|
+
|
|
144
226
|
function onClick(event) {
|
|
145
227
|
// Only a click the browser attributes to the user counts. element.click() and dispatchEvent
|
|
146
228
|
// from any script in this document produce isTrusted === false.
|
|
@@ -151,15 +233,26 @@ ${SURFACE_SCRIPT_PRELUDE}
|
|
|
151
233
|
// Cancel is exempt: backing out early is never the harm this guards against.
|
|
152
234
|
if (action === "confirm" && !dwellDone()) return;
|
|
153
235
|
locked = true;
|
|
236
|
+
setChoicesDisabled(true);
|
|
154
237
|
if (step === null) {
|
|
155
238
|
setBusy(true);
|
|
156
239
|
setStatus(TEXT.dismissed, "dismissed");
|
|
157
240
|
api.requestTeardown();
|
|
158
241
|
return;
|
|
159
242
|
}
|
|
243
|
+
// Choices ride only on confirm, and only when this document has any -- with none, \`params\` stays
|
|
244
|
+
// the exact \`step.params\` reference PLAN was built from, byte-identical to before this existed.
|
|
245
|
+
var params = step.params;
|
|
246
|
+
if (action === "confirm" && choiceInputs.length > 0) {
|
|
247
|
+
var merged = {};
|
|
248
|
+
var key;
|
|
249
|
+
for (key in step.params) if (Object.prototype.hasOwnProperty.call(step.params, key)) merged[key] = step.params[key];
|
|
250
|
+
merged[CHOICES_PARAM] = checkedChoiceIds();
|
|
251
|
+
params = merged;
|
|
252
|
+
}
|
|
160
253
|
setBusy(true);
|
|
161
254
|
setStatus(TEXT.working, "pending");
|
|
162
|
-
api.callTool(step.toolName,
|
|
255
|
+
api.callTool(step.toolName, params).then(function () {
|
|
163
256
|
setStatus(TEXT.done, "done");
|
|
164
257
|
api.requestTeardown();
|
|
165
258
|
}, function (error) {
|
|
@@ -167,6 +260,7 @@ ${SURFACE_SCRIPT_PRELUDE}
|
|
|
167
260
|
// or cancel), unless the Host says this dialog is no longer pending -- see reportCallFailure.
|
|
168
261
|
if (!reportCallFailure(error)) return;
|
|
169
262
|
locked = false;
|
|
263
|
+
setChoicesDisabled(false);
|
|
170
264
|
// reportCallFailure re-enabled every button; confirm still waits out an unfinished dwell.
|
|
171
265
|
syncConfirm();
|
|
172
266
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Generic, product-neutral React UI primitives and feature domains. Framework-free types, decision rules, catalogs, and host port contracts live behind ./core (no React, no DOM). The Excalidraw and Lexical editors live behind ./sketch-editor and ./lexical-rich-text-editor; the GrapesJS-backed inline HTML editor lives behind ./html-editor.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -107,8 +107,8 @@
|
|
|
107
107
|
"shiki": "^1.24.4",
|
|
108
108
|
"tailwind-merge": "^2.6.0",
|
|
109
109
|
"zod": "^3.25.76",
|
|
110
|
-
"@jini-ai/agentic": "0.3.6",
|
|
111
110
|
"@jini-ai/platform": "0.3.0",
|
|
111
|
+
"@jini-ai/agentic": "0.3.6",
|
|
112
112
|
"@jini-ai/protocol": "0.3.0"
|
|
113
113
|
},
|
|
114
114
|
"devDependencies": {
|