clay-server 4.7.1-beta.2 → 4.7.1-beta.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/lib/project-elicitation-response.js +42 -0
- package/lib/project-sessions.js +2 -14
- package/lib/public/css/elicitation-form.css +33 -0
- package/lib/public/modules/app-messages.js +5 -1
- package/lib/public/modules/elicitation-form.js +260 -0
- package/lib/public/modules/elicitation-schema.js +139 -0
- package/lib/public/modules/tools.js +14 -179
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +7 -4
- package/lib/sdk-worker.js +1 -1
- package/lib/ws-schema.js +1 -0
- package/lib/yoke/acp-agent-profiles.js +1 -1
- package/lib/yoke/acp-event-normalizer.js +17 -3
- package/lib/yoke/acp-query-handle.js +9 -9
- package/lib/yoke/acp-user-input.js +70 -0
- package/lib/yoke/adapters/acp.js +22 -5
- package/lib/yoke/adapters/antigravity.js +55 -6
- package/lib/yoke/adapters/claude-worker.js +1 -1
- package/lib/yoke/adapters/codex.js +5 -2
- package/lib/yoke/adapters/kiro.js +16 -17
- package/lib/yoke/claude-user-input.js +2 -4
- package/lib/yoke/codex-user-input.js +7 -4
- package/lib/yoke/elicitation-schema.js +204 -0
- package/lib/yoke/interface.js +12 -7
- package/lib/yoke/user-input.js +34 -25
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Browser elicitation_response handling: action mapping and server-side form
|
|
2
|
+
// validation before the provider callback settles.
|
|
3
|
+
|
|
4
|
+
var elicitationSchema = require("./yoke/elicitation-schema");
|
|
5
|
+
|
|
6
|
+
// "reject" is the legacy browser spelling of an explicit refusal.
|
|
7
|
+
function normalizeAction(action) {
|
|
8
|
+
if (action === "accept") return "accept";
|
|
9
|
+
if (action === "decline" || action === "reject") return "decline";
|
|
10
|
+
return "cancel";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Returns true when handled. Invalid accepted forms stay pending so the user
|
|
14
|
+
// can correct them; only the submitting socket receives the error.
|
|
15
|
+
function handleElicitationResponse(session, ws, msg, deps) {
|
|
16
|
+
var pending = session.pendingElicitations && session.pendingElicitations[msg.requestId];
|
|
17
|
+
if (!pending) {
|
|
18
|
+
deps.sendTo(ws, { type: "elicitation_error", requestId: msg.requestId, error: "This request is no longer active.", expired: true });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
var action = normalizeAction(msg.action);
|
|
22
|
+
var result = { action: action };
|
|
23
|
+
if (action === "accept") {
|
|
24
|
+
var request = pending.request || {};
|
|
25
|
+
var content = msg.content && typeof msg.content === "object" && !Array.isArray(msg.content) ? msg.content : {};
|
|
26
|
+
if (request.mode !== "url") {
|
|
27
|
+
try {
|
|
28
|
+
content = elicitationSchema.validateContent(content, request.requestedSchema);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
deps.sendTo(ws, { type: "elicitation_error", requestId: msg.requestId, error: error.message });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
result.content = content;
|
|
35
|
+
}
|
|
36
|
+
delete session.pendingElicitations[msg.requestId];
|
|
37
|
+
pending.resolve(result);
|
|
38
|
+
deps.sendAndRecord(session, { type: "elicitation_resolved", requestId: msg.requestId, action: action });
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { handleElicitationResponse: handleElicitationResponse, normalizeAction: normalizeAction };
|
package/lib/project-sessions.js
CHANGED
|
@@ -8,6 +8,7 @@ var yoke = require("./yoke");
|
|
|
8
8
|
var sessionVisibility = require("./session-visibility");
|
|
9
9
|
var contextUsageModule = require("./context-usage");
|
|
10
10
|
var sessionDeleteModule = require("./project-session-delete");
|
|
11
|
+
var elicitationResponse = require("./project-elicitation-response");
|
|
11
12
|
|
|
12
13
|
function vendorSupportsTui(vendor) {
|
|
13
14
|
var info = yoke.getVendorInfo(vendor);
|
|
@@ -1805,20 +1806,7 @@ function attachSessions(ctx) {
|
|
|
1805
1806
|
if (msg.type === "elicitation_response") {
|
|
1806
1807
|
var session = getSessionForWs(ws);
|
|
1807
1808
|
if (!session) return true;
|
|
1808
|
-
|
|
1809
|
-
if (!pending) return true;
|
|
1810
|
-
delete session.pendingElicitations[msg.requestId];
|
|
1811
|
-
if (msg.action === "accept") {
|
|
1812
|
-
pending.resolve({ action: "accept", content: msg.content || {} });
|
|
1813
|
-
} else {
|
|
1814
|
-
pending.resolve({ action: "reject" });
|
|
1815
|
-
}
|
|
1816
|
-
sm.sendAndRecord(session, {
|
|
1817
|
-
type: "elicitation_resolved",
|
|
1818
|
-
requestId: msg.requestId,
|
|
1819
|
-
action: msg.action,
|
|
1820
|
-
});
|
|
1821
|
-
return true;
|
|
1809
|
+
return elicitationResponse.handleElicitationResponse(session, ws, msg, { sendTo: sendTo, sendAndRecord: sm.sendAndRecord });
|
|
1822
1810
|
}
|
|
1823
1811
|
|
|
1824
1812
|
// --- Host user dialog response (SDK request_user_dialog) ---
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* Elicitation request form (lib/public/modules/elicitation-form.js) */
|
|
2
|
+
|
|
3
|
+
.elicitation-form { margin-top: 8px; display: flex; flex-direction: column; gap: 10px; }
|
|
4
|
+
.elicitation-field { display: flex; flex-direction: column; gap: 3px; }
|
|
5
|
+
.elicitation-label { font-size: 12px; font-weight: 500; color: var(--text-secondary); }
|
|
6
|
+
.elicitation-help { font-size: 11px; color: var(--text-dimmer); overflow-wrap: anywhere; }
|
|
7
|
+
|
|
8
|
+
.elicitation-input {
|
|
9
|
+
padding: 4px 8px;
|
|
10
|
+
border-radius: 4px;
|
|
11
|
+
border: 1px solid var(--border);
|
|
12
|
+
background: var(--input-bg);
|
|
13
|
+
color: var(--text-primary, var(--text));
|
|
14
|
+
font-size: 13px;
|
|
15
|
+
font-family: inherit;
|
|
16
|
+
}
|
|
17
|
+
.elicitation-checkbox { align-self: flex-start; }
|
|
18
|
+
|
|
19
|
+
.elicitation-choice-group { display: flex; flex-wrap: wrap; gap: 6px 14px; }
|
|
20
|
+
.elicitation-choice { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; color: var(--text-secondary); cursor: pointer; }
|
|
21
|
+
|
|
22
|
+
.elicitation-unsupported { font-size: 12px; color: var(--text-dimmer); font-style: italic; }
|
|
23
|
+
.elicitation-url-info { margin-top: 8px; font-size: 12px; color: var(--text-muted); overflow-wrap: anywhere; }
|
|
24
|
+
|
|
25
|
+
.elicitation-field-error,
|
|
26
|
+
.elicitation-form-error { font-size: 12px; color: var(--error); }
|
|
27
|
+
.elicitation-field-error:empty,
|
|
28
|
+
.elicitation-form-error:empty { display: none; }
|
|
29
|
+
.elicitation-form-error { margin-top: 8px; }
|
|
30
|
+
.elicitation-field.invalid .elicitation-input,
|
|
31
|
+
.elicitation-field.invalid .elicitation-choice-group { border-color: var(--error); outline: 1px solid var(--error-15); }
|
|
32
|
+
|
|
33
|
+
.elicitation-container.submitting .permission-actions button { opacity: 0.6; cursor: progress; }
|
|
@@ -34,7 +34,7 @@ import { handleSessionActionMessage } from './session-actions.js';
|
|
|
34
34
|
import { renderWorkerProposal, updateWorkerProposal } from './worker-proposal.js';
|
|
35
35
|
import { renderDriverContinuation, updateDriverContinuation, renderDriverContinuationContext } from './driver-continuation.js';
|
|
36
36
|
import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled, sendShellResultToAgent, handleAcceptedOrdinaryMessage, handleScheduleMessageResult } from './input.js';
|
|
37
|
-
import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
|
|
37
|
+
import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, markElicitationError, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
|
|
38
38
|
import { isInterimToolResult } from './tool-result-lifecycle.js';
|
|
39
39
|
import { finishThinkingTurn, resumeThinkingAfterReplay } from './thinking-lifecycle.js';
|
|
40
40
|
import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
|
|
@@ -1257,6 +1257,10 @@ export function processMessage(msg) {
|
|
|
1257
1257
|
stopUrgentBlink();
|
|
1258
1258
|
break;
|
|
1259
1259
|
|
|
1260
|
+
case "elicitation_error":
|
|
1261
|
+
markElicitationError(msg.requestId, msg.error, msg.expired === true);
|
|
1262
|
+
break;
|
|
1263
|
+
|
|
1260
1264
|
case "user_dialog_request":
|
|
1261
1265
|
renderUserDialogRequest(msg);
|
|
1262
1266
|
startUrgentBlink();
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Elicitation request card: renders a restricted-schema form, keeps omitted
|
|
2
|
+
// optional values distinct from explicit false/0/"", validates before sending,
|
|
3
|
+
// and stays editable until the server confirms the response.
|
|
4
|
+
import { escapeHtml } from './utils.js';
|
|
5
|
+
import { iconHtml } from './icons.js';
|
|
6
|
+
import { getWs } from './ws-ref.js';
|
|
7
|
+
import { fieldInfo, patternIsBounded, validateForm } from './elicitation-schema.js';
|
|
8
|
+
|
|
9
|
+
var UNSET = "__clay_unset__";
|
|
10
|
+
var FORMAT_INPUT_TYPES = { email: "email", uri: "url", date: "date" };
|
|
11
|
+
|
|
12
|
+
function el(tag, className, text) {
|
|
13
|
+
var node = document.createElement(tag);
|
|
14
|
+
if (className) node.className = className;
|
|
15
|
+
if (text != null) node.textContent = text;
|
|
16
|
+
return node;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hasDefault(prop) {
|
|
20
|
+
return Object.prototype.hasOwnProperty.call(prop, "default");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function choiceSelect(name, choices, prop, required, labels) {
|
|
24
|
+
var select = el("select", "elicitation-input");
|
|
25
|
+
var defaultIndex = -1;
|
|
26
|
+
for (var i = 0; i < choices.length; i++) {
|
|
27
|
+
if (hasDefault(prop) && choices[i].value === prop.default) defaultIndex = i;
|
|
28
|
+
}
|
|
29
|
+
if (!required || defaultIndex === -1) {
|
|
30
|
+
var unset = el("option", null, required ? labels.required : labels.optional);
|
|
31
|
+
unset.value = UNSET;
|
|
32
|
+
select.appendChild(unset);
|
|
33
|
+
}
|
|
34
|
+
choices.forEach(function (choice, index) {
|
|
35
|
+
var option = el("option", null, choice.label);
|
|
36
|
+
option.value = String(index);
|
|
37
|
+
if (choice.description) option.title = choice.description;
|
|
38
|
+
select.appendChild(option);
|
|
39
|
+
});
|
|
40
|
+
select.value = defaultIndex === -1 ? UNSET : String(defaultIndex);
|
|
41
|
+
select.name = name;
|
|
42
|
+
return select;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Each builder returns { node, read } where read() yields { present, value }
|
|
46
|
+
// or { present: true, raw } for unparsable text.
|
|
47
|
+
function buildControl(name, prop, info, required) {
|
|
48
|
+
if (info.kind === "boolean") {
|
|
49
|
+
if (required) {
|
|
50
|
+
var box = el("input", "elicitation-checkbox");
|
|
51
|
+
box.type = "checkbox";
|
|
52
|
+
box.name = name;
|
|
53
|
+
box.checked = prop.default === true;
|
|
54
|
+
return { node: box, read: function () { return { present: true, value: box.checked }; } };
|
|
55
|
+
}
|
|
56
|
+
var boolSelect = choiceSelect(name, [{ value: true, label: "Yes" }, { value: false, label: "No" }], prop, false, { optional: "Not set" });
|
|
57
|
+
return { node: boolSelect, read: function () {
|
|
58
|
+
return boolSelect.value === UNSET ? { present: false } : { present: true, value: boolSelect.value === "0" };
|
|
59
|
+
} };
|
|
60
|
+
}
|
|
61
|
+
if (info.kind === "array") {
|
|
62
|
+
var group = el("div", "elicitation-choice-group");
|
|
63
|
+
group.setAttribute("role", "group");
|
|
64
|
+
var defaults = Array.isArray(prop.default) ? prop.default : [];
|
|
65
|
+
var boxes = info.choices.map(function (choice, index) {
|
|
66
|
+
var label = el("label", "elicitation-choice");
|
|
67
|
+
var input = el("input");
|
|
68
|
+
input.type = "checkbox";
|
|
69
|
+
input.name = name;
|
|
70
|
+
input.value = String(index);
|
|
71
|
+
input.checked = defaults.indexOf(choice.value) !== -1;
|
|
72
|
+
label.appendChild(input);
|
|
73
|
+
label.appendChild(el("span", null, choice.label));
|
|
74
|
+
if (choice.description) label.title = choice.description;
|
|
75
|
+
group.appendChild(label);
|
|
76
|
+
return input;
|
|
77
|
+
});
|
|
78
|
+
return { node: group, read: function () {
|
|
79
|
+
var values = [];
|
|
80
|
+
boxes.forEach(function (input, index) { if (input.checked) values.push(info.choices[index].value); });
|
|
81
|
+
return values.length || required ? { present: true, value: values } : { present: false };
|
|
82
|
+
} };
|
|
83
|
+
}
|
|
84
|
+
if (info.choices) {
|
|
85
|
+
var select = choiceSelect(name, info.choices, prop, required, { optional: "Not set", required: "Select…" });
|
|
86
|
+
return { node: select, read: function () {
|
|
87
|
+
return select.value === UNSET ? { present: false } : { present: true, value: info.choices[Number(select.value)].value };
|
|
88
|
+
} };
|
|
89
|
+
}
|
|
90
|
+
if (info.kind === "unsupported") {
|
|
91
|
+
return { node: el("div", "elicitation-unsupported", "This field type is not supported here."), read: function () { return { present: false }; } };
|
|
92
|
+
}
|
|
93
|
+
var input = el("input", "elicitation-input");
|
|
94
|
+
input.name = name;
|
|
95
|
+
if (info.kind === "number" || info.kind === "integer") {
|
|
96
|
+
input.type = "number";
|
|
97
|
+
input.step = info.kind === "integer" ? "1" : "any";
|
|
98
|
+
if (typeof prop.minimum === "number") input.min = String(prop.minimum);
|
|
99
|
+
if (typeof prop.maximum === "number") input.max = String(prop.maximum);
|
|
100
|
+
if (typeof prop.default === "number") input.value = String(prop.default);
|
|
101
|
+
return { node: input, read: function () {
|
|
102
|
+
if (input.value === "") return input.validity && input.validity.badInput ? { present: true, raw: true } : { present: false };
|
|
103
|
+
return { present: true, value: Number(input.value) };
|
|
104
|
+
} };
|
|
105
|
+
}
|
|
106
|
+
input.type = FORMAT_INPUT_TYPES[prop.format] || "text";
|
|
107
|
+
if (typeof prop.default === "string") input.value = prop.default;
|
|
108
|
+
if (prop.description) input.placeholder = prop.description;
|
|
109
|
+
return { node: input, read: function () {
|
|
110
|
+
if (input.value === "" && !required) return { present: false };
|
|
111
|
+
return { present: true, value: input.value };
|
|
112
|
+
} };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildField(name, prop, required) {
|
|
116
|
+
var info = fieldInfo(prop);
|
|
117
|
+
var wrapper = el("div", "elicitation-field");
|
|
118
|
+
wrapper.dataset.propName = name;
|
|
119
|
+
var label = el("label", "elicitation-label", (prop.title || name) + (required ? " *" : ""));
|
|
120
|
+
wrapper.appendChild(label);
|
|
121
|
+
var control = buildControl(name, prop, info, required);
|
|
122
|
+
wrapper.appendChild(control.node);
|
|
123
|
+
if (prop.description && prop.title) wrapper.appendChild(el("div", "elicitation-help", prop.description));
|
|
124
|
+
if (typeof prop.pattern === "string" && !patternIsBounded(prop.pattern)) {
|
|
125
|
+
wrapper.appendChild(el("div", "elicitation-help", "Expected pattern (checked by the agent): " + prop.pattern));
|
|
126
|
+
}
|
|
127
|
+
var error = el("div", "elicitation-field-error");
|
|
128
|
+
error.setAttribute("role", "alert");
|
|
129
|
+
wrapper.appendChild(error);
|
|
130
|
+
return { name: name, wrapper: wrapper, control: control, error: error };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function collect(fields, schema) {
|
|
134
|
+
var content = {};
|
|
135
|
+
var errors = {};
|
|
136
|
+
fields.forEach(function (field) {
|
|
137
|
+
var read = field.control.read();
|
|
138
|
+
if (read.raw) errors[field.name] = "Enter a number.";
|
|
139
|
+
else if (read.present) content[field.name] = read.value;
|
|
140
|
+
});
|
|
141
|
+
var validation = validateForm(content, schema);
|
|
142
|
+
Object.keys(validation.errors).forEach(function (name) { if (!errors[name]) errors[name] = validation.errors[name]; });
|
|
143
|
+
return { content: content, errors: errors };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function setBusy(container, busy) {
|
|
147
|
+
container.classList.toggle("submitting", busy);
|
|
148
|
+
container.querySelectorAll(".permission-actions button").forEach(function (button) { button.disabled = busy; });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function showFormError(container, message) {
|
|
152
|
+
var summary = container.querySelector(".elicitation-form-error");
|
|
153
|
+
if (summary) summary.textContent = message || "";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function send(container, action, content, onSent) {
|
|
157
|
+
var ws = getWs();
|
|
158
|
+
if (!ws || ws.readyState !== 1) {
|
|
159
|
+
showFormError(container, "Not connected. Try again when the connection is restored.");
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
var msg = { type: "elicitation_response", requestId: container.dataset.requestId, action: action };
|
|
163
|
+
if (action === "accept") msg.content = content;
|
|
164
|
+
ws.send(JSON.stringify(msg));
|
|
165
|
+
setBusy(container, true);
|
|
166
|
+
showFormError(container, "");
|
|
167
|
+
if (onSent) onSent();
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function actionButton(label, className, handler) {
|
|
172
|
+
var button = el("button", "permission-btn " + className, label);
|
|
173
|
+
button.type = "button";
|
|
174
|
+
button.addEventListener("click", handler);
|
|
175
|
+
return button;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function buildElicitationCard(msg, onSent) {
|
|
179
|
+
var container = el("div", "permission-container elicitation-container");
|
|
180
|
+
container.dataset.requestId = msg.requestId;
|
|
181
|
+
var header = el("div", "permission-header");
|
|
182
|
+
header.innerHTML = '<span class="permission-icon">' + iconHtml("key") + '</span>' +
|
|
183
|
+
'<span class="permission-title">' + escapeHtml(msg.serverName || "MCP Server") + ' requests input</span>';
|
|
184
|
+
var body = el("div", "permission-body");
|
|
185
|
+
if (msg.message) body.appendChild(el("div", "permission-reason", msg.message));
|
|
186
|
+
var isUrl = msg.mode === "url" && msg.url;
|
|
187
|
+
var schema = msg.requestedSchema || null;
|
|
188
|
+
var fields = [];
|
|
189
|
+
if (isUrl) {
|
|
190
|
+
body.appendChild(el("div", "elicitation-url-info", "Opens: " + msg.url));
|
|
191
|
+
} else if (schema && schema.properties) {
|
|
192
|
+
var form = el("div", "elicitation-form");
|
|
193
|
+
var required = Array.isArray(schema.required) ? schema.required : [];
|
|
194
|
+
Object.keys(schema.properties).forEach(function (name) {
|
|
195
|
+
var field = buildField(name, schema.properties[name] || {}, required.indexOf(name) !== -1);
|
|
196
|
+
fields.push(field);
|
|
197
|
+
form.appendChild(field.wrapper);
|
|
198
|
+
});
|
|
199
|
+
body.appendChild(form);
|
|
200
|
+
}
|
|
201
|
+
var summary = el("div", "elicitation-form-error");
|
|
202
|
+
summary.setAttribute("role", "alert");
|
|
203
|
+
body.appendChild(summary);
|
|
204
|
+
|
|
205
|
+
var actions = el("div", "permission-actions");
|
|
206
|
+
actions.appendChild(actionButton(isUrl ? "Open & Approve" : "Submit", "permission-allow elicitation-submit", function () {
|
|
207
|
+
if (isUrl) {
|
|
208
|
+
if (send(container, "accept", {}, onSent)) window.open(msg.url, "_blank");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
var result = collect(fields, schema);
|
|
212
|
+
var invalid = false;
|
|
213
|
+
fields.forEach(function (field) {
|
|
214
|
+
var message = result.errors[field.name] || "";
|
|
215
|
+
field.error.textContent = message;
|
|
216
|
+
field.wrapper.classList.toggle("invalid", !!message);
|
|
217
|
+
if (message) invalid = true;
|
|
218
|
+
});
|
|
219
|
+
if (invalid) {
|
|
220
|
+
showFormError(container, "Fix the highlighted fields before submitting.");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
send(container, "accept", result.content, onSent);
|
|
224
|
+
}));
|
|
225
|
+
actions.appendChild(actionButton("Decline", "permission-deny elicitation-decline", function () { send(container, "decline", null, onSent); }));
|
|
226
|
+
actions.appendChild(actionButton("Cancel", "permission-allow-session elicitation-cancel", function () { send(container, "cancel", null, onSent); }));
|
|
227
|
+
container.appendChild(header);
|
|
228
|
+
container.appendChild(body);
|
|
229
|
+
container.appendChild(actions);
|
|
230
|
+
return container;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
var RESOLVED = {
|
|
234
|
+
accept: { label: "Submitted", className: "resolved-allowed" },
|
|
235
|
+
decline: { label: "Declined", className: "resolved-denied" },
|
|
236
|
+
reject: { label: "Declined", className: "resolved-denied" },
|
|
237
|
+
cancel: { label: "Cancelled", className: "resolved-cancelled" },
|
|
238
|
+
expired: { label: "No longer active", className: "resolved-cancelled" },
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export function markElicitationCard(container, action) {
|
|
242
|
+
if (!container || container.classList.contains("resolved")) return;
|
|
243
|
+
var state = RESOLVED[action] || RESOLVED.cancel;
|
|
244
|
+
container.classList.remove("submitting");
|
|
245
|
+
container.classList.add("resolved", state.className);
|
|
246
|
+
container.querySelectorAll("input, select, button").forEach(function (control) { control.disabled = true; });
|
|
247
|
+
var actions = container.querySelector(".permission-actions");
|
|
248
|
+
if (actions) actions.innerHTML = '<span class="permission-decision-label">' + state.label + '</span>';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Server-side rejection keeps the form editable; an expired request resolves.
|
|
252
|
+
export function showElicitationError(container, message, expired) {
|
|
253
|
+
if (!container || container.classList.contains("resolved")) return;
|
|
254
|
+
if (expired) {
|
|
255
|
+
markElicitationCard(container, "expired");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
setBusy(container, false);
|
|
259
|
+
showFormError(container, message || "The response was not accepted.");
|
|
260
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Restricted elicitation schema handling for the browser form.
|
|
2
|
+
// Mirrors lib/yoke/elicitation-schema.js; keep both in sync
|
|
3
|
+
// (test/elicitation-schema-parity.test.js runs one fixture table on both).
|
|
4
|
+
|
|
5
|
+
var PATTERN_MAX_LENGTH = 200;
|
|
6
|
+
var PATTERN_MAX_VARIABLE_QUANTIFIERS = 2;
|
|
7
|
+
var PATTERN_MAX_INPUT_LENGTH = 256;
|
|
8
|
+
|
|
9
|
+
function hasOwn(object, key) {
|
|
10
|
+
return !!object && Object.prototype.hasOwnProperty.call(object, key);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function constChoices(list) {
|
|
14
|
+
if (!Array.isArray(list)) return null;
|
|
15
|
+
var values = [];
|
|
16
|
+
for (var i = 0; i < list.length; i++) {
|
|
17
|
+
if (!hasOwn(list[i], "const")) return null;
|
|
18
|
+
values.push({ value: list[i].const, label: String(list[i].title || list[i].const), description: list[i].description || "" });
|
|
19
|
+
}
|
|
20
|
+
return values.length ? values : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function enumChoices(list) {
|
|
24
|
+
if (!Array.isArray(list) || !list.length) return null;
|
|
25
|
+
return list.map(function (value) { return { value: value, label: String(value), description: "" }; });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function fieldInfo(property) {
|
|
29
|
+
property = property || {};
|
|
30
|
+
var type = property.type || (Array.isArray(property.enum) || Array.isArray(property.oneOf) ? "string" : null);
|
|
31
|
+
if (type === "array") {
|
|
32
|
+
var items = property.items || {};
|
|
33
|
+
var itemType = items.type || "string";
|
|
34
|
+
var itemChoices = itemType === "string" ? (enumChoices(items.enum) || constChoices(items.anyOf)) : null;
|
|
35
|
+
return itemChoices ? { kind: "array", multi: true, choices: itemChoices } : { kind: "unsupported", multi: false, choices: null };
|
|
36
|
+
}
|
|
37
|
+
if (type === "string") return { kind: "string", multi: false, choices: enumChoices(property.enum) || constChoices(property.oneOf) };
|
|
38
|
+
if (type === "number" || type === "integer" || type === "boolean") return { kind: type, multi: false, choices: null };
|
|
39
|
+
return { kind: "unsupported", multi: false, choices: null };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Agent-supplied patterns are evaluated only inside a subset whose backtracking
|
|
43
|
+
// cost is bounded; anything else stays a visible, unenforced hint.
|
|
44
|
+
export function patternIsBounded(pattern) {
|
|
45
|
+
if (typeof pattern !== "string" || !pattern || pattern.length > PATTERN_MAX_LENGTH) return false;
|
|
46
|
+
var variable = 0;
|
|
47
|
+
var inClass = false;
|
|
48
|
+
for (var i = 0; i < pattern.length; i++) {
|
|
49
|
+
var ch = pattern[i];
|
|
50
|
+
if (ch === "\\") {
|
|
51
|
+
var next = pattern[i + 1];
|
|
52
|
+
if (next === undefined || /[1-9k]/.test(next)) return false;
|
|
53
|
+
i++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (inClass) { if (ch === "]") inClass = false; continue; }
|
|
57
|
+
if (ch === "[") { inClass = true; continue; }
|
|
58
|
+
if (ch === "(" && pattern[i + 1] === "?") {
|
|
59
|
+
if (pattern[i + 2] !== ":") return false;
|
|
60
|
+
i += 2;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (ch === "*" || ch === "+" || ch === "?" || ch === "{") {
|
|
64
|
+
if (pattern[i - 1] === ")") return false;
|
|
65
|
+
if (ch === "{") {
|
|
66
|
+
var close = pattern.indexOf("}", i);
|
|
67
|
+
if (close === -1) return false;
|
|
68
|
+
var body = pattern.slice(i + 1, close);
|
|
69
|
+
if (!/^\d+(,\d*)?$/.test(body)) return false;
|
|
70
|
+
if (body.indexOf(",") !== -1) variable++;
|
|
71
|
+
i = close;
|
|
72
|
+
} else {
|
|
73
|
+
variable++;
|
|
74
|
+
}
|
|
75
|
+
if (pattern[i + 1] === "?") i++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (inClass || variable > PATTERN_MAX_VARIABLE_QUANTIFIERS) return false;
|
|
79
|
+
try { new RegExp(pattern, "u"); } catch (e) { return false; }
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function patternMatches(pattern, value) {
|
|
84
|
+
if (!patternIsBounded(pattern) || value.length > PATTERN_MAX_INPUT_LENGTH) return null;
|
|
85
|
+
return new RegExp(pattern, "u").test(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function inChoices(choices, value) {
|
|
89
|
+
return choices.some(function (choice) { return choice.value === value; });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Returns an error message or null. `value` must already be typed.
|
|
93
|
+
export function valueError(value, property) {
|
|
94
|
+
property = property || {};
|
|
95
|
+
var info = fieldInfo(property);
|
|
96
|
+
if (info.kind === "unsupported") return null;
|
|
97
|
+
if (info.kind === "boolean") return typeof value === "boolean" ? null : "Choose yes or no.";
|
|
98
|
+
if (info.kind === "number" || info.kind === "integer") {
|
|
99
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "Enter a number.";
|
|
100
|
+
if (info.kind === "integer" && !Number.isInteger(value)) return "Enter a whole number.";
|
|
101
|
+
if (typeof property.minimum === "number" && value < property.minimum) return "Must be at least " + property.minimum + ".";
|
|
102
|
+
if (typeof property.maximum === "number" && value > property.maximum) return "Must be at most " + property.maximum + ".";
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if (info.kind === "array") {
|
|
106
|
+
if (!Array.isArray(value)) return "Choose from the listed options.";
|
|
107
|
+
for (var i = 0; i < value.length; i++) {
|
|
108
|
+
if (!inChoices(info.choices, value[i])) return "Choose only listed options.";
|
|
109
|
+
if (value.indexOf(value[i]) !== i) return "Choose each option once.";
|
|
110
|
+
}
|
|
111
|
+
if (typeof property.minItems === "number" && value.length < property.minItems) return "Choose at least " + property.minItems + ".";
|
|
112
|
+
if (typeof property.maxItems === "number" && value.length > property.maxItems) return "Choose at most " + property.maxItems + ".";
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (typeof value !== "string") return "Enter text.";
|
|
116
|
+
if (info.choices) return inChoices(info.choices, value) ? null : "Choose one of the listed options.";
|
|
117
|
+
var length = Array.from(value).length;
|
|
118
|
+
if (typeof property.minLength === "number" && length < property.minLength) return "Must be at least " + property.minLength + " characters.";
|
|
119
|
+
if (typeof property.maxLength === "number" && length > property.maxLength) return "Must be at most " + property.maxLength + " characters.";
|
|
120
|
+
if (patternMatches(property.pattern, value) === false) return "Does not match the required format.";
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Required means presence only. Returns { content, errors } where errors maps
|
|
125
|
+
// property name to message; content holds only present properties.
|
|
126
|
+
export function validateForm(content, schema) {
|
|
127
|
+
var properties = schema && schema.properties && typeof schema.properties === "object" ? schema.properties : {};
|
|
128
|
+
var required = schema && Array.isArray(schema.required) ? schema.required : [];
|
|
129
|
+
var errors = {};
|
|
130
|
+
Object.keys(properties).forEach(function (id) {
|
|
131
|
+
if (!hasOwn(content, id)) {
|
|
132
|
+
if (required.indexOf(id) !== -1) errors[id] = fieldInfo(properties[id]).kind === "unsupported" ? "This field type is not supported here." : "A value is required.";
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
var error = valueError(content[id], properties[id]);
|
|
136
|
+
if (error) errors[id] = error;
|
|
137
|
+
});
|
|
138
|
+
return { content: content, errors: errors };
|
|
139
|
+
}
|