clay-server 4.7.1-beta.2 → 4.7.1-beta.4

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.
@@ -260,7 +260,7 @@ function attachConnection(ctx) {
260
260
  }
261
261
  var _vendorCaps = (sm.capabilitiesByVendor && sm.capabilitiesByVendor[active.vendor || sm.defaultVendor || "claude"]) || {};
262
262
  var activePermissionState = permissionModes.clientState(active, sm.currentPermissionMode, sm.defaultVendor);
263
- sendTo(ws, { type: "session_switched", id: active.localId, sessionOriginId: active.sessionOriginId || null, cliSessionId: active.cliSessionId || null, loop: active.loop || null, vendor: active.vendor || null, model: active.model || null, effort: active.effort || initialEffort || null, hasHistory: (active.history && active.history.length > 0), capabilities: _vendorCaps, mode: active.mode || "gui", terminalId: typeof active.terminalId === "number" ? active.terminalId : null, runtimeMode: active.runtimeMode || null, runtimeTerminalId: typeof active.runtimeTerminalId === "number" ? active.runtimeTerminalId : null, tuiSuspended: !!active.tuiSuspended, permissionMode: activePermissionState.requestedPermissionMode, effectivePermissionMode: activePermissionState.effectivePermissionMode, permissionCapabilities: activePermissionState.permissionCapabilities, mcpPermissionModeOverrides: activePermissionState.mcpPermissionModeOverrides });
263
+ sendTo(ws, { type: "session_switched", id: active.localId, sessionOriginId: active.sessionOriginId || null, cliSessionId: active.cliSessionId || null, loop: active.loop || null, vendor: active.vendor || null, model: active.model || null, effort: active.effort || initialEffort || null, hasHistory: (active.history && active.history.length > 0), capabilities: _vendorCaps, isProcessing: !!active.isProcessing, mode: active.mode || "gui", terminalId: typeof active.terminalId === "number" ? active.terminalId : null, runtimeMode: active.runtimeMode || null, runtimeTerminalId: typeof active.runtimeTerminalId === "number" ? active.runtimeTerminalId : null, tuiSuspended: !!active.tuiSuspended, permissionMode: activePermissionState.requestedPermissionMode, effectivePermissionMode: activePermissionState.effectivePermissionMode, permissionCapabilities: activePermissionState.permissionCapabilities, mcpPermissionModeOverrides: activePermissionState.mcpPermissionModeOverrides });
264
264
  if (ctx.autonomousRun) sendTo(ws, { type: "autonomous_run_state", sessionId: active.localId, run: ctx.autonomousRun.project(active.autonomousRun) });
265
265
  // Send per-session context sources
266
266
  var sessionSources = loadContextSources(slug, active.localId);
@@ -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 };
@@ -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);
@@ -1759,7 +1760,7 @@ function attachSessions(ctx) {
1759
1760
  // Send feedback as next user message if there's text
1760
1761
  if (feedback) {
1761
1762
  setTimeout(function () {
1762
- var userMsg = { type: "user_message", text: feedback };
1763
+ var userMsg = { type: "user_message", text: feedback, planFeedback: true };
1763
1764
  session.history.push(userMsg);
1764
1765
  sm.appendToSessionFile(session, userMsg);
1765
1766
  sendToSession(session.localId, userMsg);
@@ -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
- var pending = session.pendingElicitations && session.pendingElicitations[msg.requestId];
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) ---
package/lib/public/app.js CHANGED
@@ -284,6 +284,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
284
284
  loadingMore: false,
285
285
  historyTotal: 0,
286
286
  replayingHistory: false,
287
+ historyReplayBatch: null,
287
288
  projectName: projectName,
288
289
  cwd: "",
289
290
  currentSlug: currentSlug,
@@ -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; }
@@ -24,6 +24,7 @@ import { clearPermissionModePending } from './permission-control.js';
24
24
  import { clearMcpPermissionModePending } from './mcp-ui.js';
25
25
  import { createWebSocketLifecycle } from './websocket-lifecycle.js';
26
26
  import { createWebSocketWatchdog } from './websocket-watchdog.js';
27
+ import { resetHistoryReplayBatch } from './history-replay-batch.js';
27
28
 
28
29
  var connectOverlay = null;
29
30
  var hasConnectedOnce = false;
@@ -324,6 +325,7 @@ function onConnected() {
324
325
 
325
326
  export function connect() {
326
327
  if (lifecycle.isOffline()) return;
328
+ resetHistoryReplayBatch();
327
329
  var attemptEpoch = lifecycle.beginAttempt();
328
330
  attemptStartedAt = Date.now();
329
331
  var ws = getWs();
@@ -14,6 +14,7 @@ import { handleClaySessionTarget } from './command-palette.js';
14
14
  import { handleDefaultAiMessage } from './default-ai.js';
15
15
  import { handleCursorSharingMessage } from './cursor-sharing-preference.js';
16
16
  import { handlePendingMessageQueueMessage } from './pending-message-queue.js';
17
+ import { receiveHistoryReplay } from './history-replay-batch.js';
17
18
 
18
19
  export function handleHomeProtocolMessage(msg) {
19
20
  if (handleIssueReference(msg)) return true;
@@ -82,6 +83,10 @@ export function handleHomeProtocolMessage(msg) {
82
83
  }
83
84
 
84
85
  export function processMessage(msg) {
86
+ receiveHistoryReplay(msg, dispatchMessage);
87
+ }
88
+
89
+ function dispatchMessage(msg) {
85
90
  if (handlePendingMessageQueueMessage(msg)) return;
86
91
  if (handleDefaultAiMessage(msg)) return;
87
92
  if (handleCursorSharingMessage(msg)) return;
@@ -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';
@@ -67,7 +67,7 @@ import { renderGeneratedImage, renderImageGenerationProgress, clearImageGenerati
67
67
  import { clearPairResultStatus, handlePairResultStatus } from './pair-result-status.js';
68
68
 
69
69
  // --- App module imports ---
70
- import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, resetStreamBuffer, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
70
+ import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, resetStreamBuffer, appendDelta, flushStreamBuffer, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
71
71
  import { setActivity, startUrgentBlink, stopUrgentBlink, blinkSessionDot, updateCrossProjectBlink } from './app-favicon.js';
72
72
  import { setStatus } from './app-connection.js';
73
73
  import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
@@ -251,16 +251,14 @@ export function processMessage(msg) {
251
251
  }
252
252
  updateContextPanel();
253
253
  updateUsagePanel();
254
- // Render + finalize any incomplete turn from the replayed history
255
- var _hs = store.snap();
256
- if (_hs.currentMsgEl && _hs.currentFullText) {
257
- var replayContentEl = _hs.currentMsgEl.querySelector(".md-content");
258
- if (replayContentEl) {
259
- replayContentEl.innerHTML = renderMarkdown(_hs.currentFullText);
260
- }
254
+ // Keep the active answer and tools attached so live deltas continue
255
+ // the restored turn instead of starting a second answer bubble.
256
+ if (store.get('sessionIsProcessing')) {
257
+ flushStreamBuffer();
258
+ } else {
259
+ markAllToolsDone();
260
+ finalizeAssistantBlock();
261
261
  }
262
- markAllToolsDone();
263
- finalizeAssistantBlock();
264
262
  stopUrgentBlink();
265
263
  // Clean up debate UI if debate is not active after replay
266
264
  if (!isDebateActive()) {
@@ -975,6 +973,12 @@ export function processMessage(msg) {
975
973
  if (msg.sessionId != null && String(msg.sessionId) !== String(store.get('activeSessionId'))) break;
976
974
  if (msg.clientMessageId && messagesEl.querySelector('[data-client-message-id="' + msg.clientMessageId + '"]')) break;
977
975
  if (!store.get('replayingHistory')) beginMarkdownTurn();
976
+ // A recorded user turn closes the previous one. Tools left open by an
977
+ // interrupted earlier turn must not keep spinning in a restored
978
+ // running session, where history_done keeps current tools active.
979
+ // Question answers, plan feedback and Driver delegations are injected
980
+ // mid-turn, so tools before them may still be running.
981
+ else if (!msg.askUserAnswer && !msg.planFeedback && !msg.delegated) markAllToolsDone();
978
982
  resetThinkingGroup();
979
983
  if (msg.planContent) {
980
984
  setPlanContent(msg.planContent);
@@ -1257,6 +1261,10 @@ export function processMessage(msg) {
1257
1261
  stopUrgentBlink();
1258
1262
  break;
1259
1263
 
1264
+ case "elicitation_error":
1265
+ markElicitationError(msg.requestId, msg.error, msg.expired === true);
1266
+ break;
1267
+
1260
1268
  case "user_dialog_request":
1261
1269
  renderUserDialogRequest(msg);
1262
1270
  startUrgentBlink();
@@ -168,7 +168,7 @@ export function keepActiveThinkingAtTail() {
168
168
  }
169
169
 
170
170
  export function scrollToBottom() {
171
- if (prependAnchor) return;
171
+ if (prependAnchor || store.get('replayingHistory')) return;
172
172
  var messagesEl = getMessagesEl();
173
173
  if (!messagesEl) return;
174
174
  var distFromBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
@@ -192,7 +192,7 @@ export function scrollToBottom() {
192
192
  }
193
193
 
194
194
  export function forceScrollToBottom() {
195
- if (prependAnchor) return;
195
+ if (prependAnchor || store.get('replayingHistory')) return;
196
196
  armStickyBottom(750);
197
197
  }
198
198
 
@@ -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
+ }