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.
- package/lib/project-connection.js +1 -1
- package/lib/project-elicitation-response.js +42 -0
- package/lib/project-sessions.js +3 -15
- package/lib/public/app.js +1 -0
- package/lib/public/css/elicitation-form.css +33 -0
- package/lib/public/modules/app-connection.js +2 -0
- package/lib/public/modules/app-message-router.js +5 -0
- package/lib/public/modules/app-messages.js +19 -11
- package/lib/public/modules/chat-render-runtime.js +2 -2
- package/lib/public/modules/elicitation-form.js +260 -0
- package/lib/public/modules/elicitation-schema.js +139 -0
- package/lib/public/modules/history-replay-batch.js +113 -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
|
@@ -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 };
|
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);
|
|
@@ -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
|
-
|
|
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
|
-
//
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
}
|