clay-server 4.8.2 → 5.0.0-beta.1
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/LICENSES/README.md +13 -0
- package/LICENSES/open-iconic.txt +21 -0
- package/LICENSES/plantuml-salt.txt +674 -0
- package/lib/assets/salt-assets.json +15 -0
- package/lib/assets/salt-colors.json +156 -0
- package/lib/assets/salt-font-10.bin.gz +0 -0
- package/lib/assets/salt-font-13.bin.gz +0 -0
- package/lib/assets/salt-font-14.bin.gz +0 -0
- package/lib/assets/salt-font-16.bin.gz +0 -0
- package/lib/assets/salt-font-18.bin.gz +0 -0
- package/lib/assets/salt-font-20.bin.gz +0 -0
- package/lib/assets/salt-font-metrics.bin.gz +0 -0
- package/lib/assets/salt-font-metrics.json +101 -0
- package/lib/assets/salt-font-monospace-10.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-13.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-14.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-16.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-18.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-20.bin.gz +0 -0
- package/lib/assets/salt-font-monospace.bin.gz +0 -0
- package/lib/assets/salt-icons.json +671 -0
- package/lib/bundled-skills/clay-sketch/SKILL.md +198 -0
- package/lib/mcp-browser-proxy.js +37 -0
- package/lib/mcp-connection-store.js +39 -0
- package/lib/mcp-network.js +46 -0
- package/lib/mcp-oauth.js +65 -0
- package/lib/mcp-proxy.js +32 -0
- package/lib/mcp-remote-connections.js +215 -0
- package/lib/mcp-session-access.js +15 -0
- package/lib/plantuml-renderer.js +43 -0
- package/lib/project-file-http.js +2 -0
- package/lib/project-mcp.js +130 -357
- package/lib/project-plantuml.js +48 -0
- package/lib/project-session-document.js +17 -1
- package/lib/project-user-message.js +2 -0
- package/lib/project-wireframe-http.js +88 -0
- package/lib/project.js +28 -45
- package/lib/public/app.js +7 -0
- package/lib/public/css/filebrowser.css +5 -0
- package/lib/public/css/mcp-connections.css +86 -0
- package/lib/public/css/skills-workbench.css +74 -0
- package/lib/public/css/wireframe.css +95 -0
- package/lib/public/index.html +16 -15
- package/lib/public/modules/app-messages.js +10 -2
- package/lib/public/modules/filebrowser.js +29 -3
- package/lib/public/modules/markdown.js +3 -1
- package/lib/public/modules/mcp-connection-view.js +136 -0
- package/lib/public/modules/mcp-permissions.js +122 -0
- package/lib/public/modules/mcp-ui.js +85 -461
- package/lib/public/modules/plantuml-preview.js +76 -0
- package/lib/public/modules/skills-actions.js +47 -79
- package/lib/public/modules/skills-detail.js +32 -145
- package/lib/public/modules/skills-list-view.js +29 -0
- package/lib/public/modules/skills-state.js +4 -2
- package/lib/public/modules/skills.js +104 -399
- package/lib/public/modules/split-view.js +14 -0
- package/lib/public/modules/vendor-login-browser.js +16 -5
- package/lib/public/modules/wireframe-chat.js +52 -0
- package/lib/public/modules/wireframe-render.js +69 -0
- package/lib/public/modules/wireframe-style.js +6 -0
- package/lib/public/modules/wireframe-viewport.js +70 -0
- package/lib/public/modules/wireframe-workbench.js +116 -0
- package/lib/public/style.css +3 -0
- package/lib/salt-chrome.js +36 -0
- package/lib/salt-hand.js +37 -0
- package/lib/salt-images.js +87 -0
- package/lib/salt-layout.js +60 -0
- package/lib/salt-options.js +23 -0
- package/lib/salt-overlays.js +45 -0
- package/lib/salt-parser.js +153 -0
- package/lib/salt-screen.js +111 -0
- package/lib/salt-svg-shapes.js +80 -0
- package/lib/salt-svg.js +164 -0
- package/lib/salt-text.js +112 -0
- package/lib/salt-tree.js +45 -0
- package/lib/sdk-bridge.js +5 -17
- package/lib/sdk-mcp-descriptors.js +2 -1
- package/lib/session-document-mcp-server.js +9 -0
- package/lib/vendor-login-browser-worker.js +18 -8
- package/lib/vendor-login-browser.js +10 -3
- package/lib/yoke/adapters/codex.js +1 -18
- package/package.json +6 -2
|
@@ -12,6 +12,7 @@ import { enterMarkdownSlides, exitMarkdownSlides, handleMarkdownSlideKey, syncMa
|
|
|
12
12
|
import { initFileViewerTabs, openFileViewerTab, previewFileViewerTab, updateFileViewerTab, closeFileViewerTab, focusedFileViewerTab, focusedFileViewerTabData, clearFileViewerTabs } from './filebrowser-tabs.js';
|
|
13
13
|
import { initFileBrowserContextMenu, downloadProjectFile } from './filebrowser-context-menu.js';
|
|
14
14
|
import { registerClayFileLinkOpener } from './clay-file-links.js';
|
|
15
|
+
import { isPlantUmlFile, renderPlantUmlPreview, cancelPlantUmlPreview } from './plantuml-preview.js';
|
|
15
16
|
|
|
16
17
|
var ctx;
|
|
17
18
|
var showDropHint = function () {};
|
|
@@ -195,8 +196,12 @@ export function initFileBrowser(_ctx) {
|
|
|
195
196
|
|
|
196
197
|
// Markdown render toggle
|
|
197
198
|
document.getElementById("file-viewer-render").addEventListener("click", function () {
|
|
198
|
-
if (
|
|
199
|
+
if (currentContent == null || (!currentIsMarkdown && !currentIsSvg && !store.get("fileViewerIsPlantUml"))) return;
|
|
199
200
|
isRendered = !isRendered;
|
|
201
|
+
if (store.get("fileViewerIsPlantUml")) {
|
|
202
|
+
renderPlantUmlBody();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
200
205
|
if (currentIsSvg) {
|
|
201
206
|
renderSvgBody();
|
|
202
207
|
return;
|
|
@@ -485,6 +490,7 @@ export function closeFileViewer() {
|
|
|
485
490
|
}
|
|
486
491
|
|
|
487
492
|
function teardownFileViewer() {
|
|
493
|
+
cancelPlantUmlPreview();
|
|
488
494
|
if (currentFilePath && isFollowingMarkdown(currentFilePath)) cancelMarkdownFollow();
|
|
489
495
|
sendUnwatch();
|
|
490
496
|
inlineDiffActive = false;
|
|
@@ -522,6 +528,8 @@ export function resetFileBrowser() {
|
|
|
522
528
|
isRendered = false;
|
|
523
529
|
currentIsMarkdown = false;
|
|
524
530
|
currentIsSvg = false;
|
|
531
|
+
store.set({ fileViewerIsPlantUml: false });
|
|
532
|
+
cancelPlantUmlPreview();
|
|
525
533
|
historyVisible = false;
|
|
526
534
|
currentHistoryEntries = [];
|
|
527
535
|
pendingNavigate = null;
|
|
@@ -1157,6 +1165,8 @@ function showFileContent(msg, expectedRevision) {
|
|
|
1157
1165
|
currentFilePath = msg.path;
|
|
1158
1166
|
currentIsMarkdown = false;
|
|
1159
1167
|
currentIsSvg = false;
|
|
1168
|
+
store.set({ fileViewerIsPlantUml: false });
|
|
1169
|
+
cancelPlantUmlPreview();
|
|
1160
1170
|
if (!keepRenderState) isRendered = false;
|
|
1161
1171
|
var requestedExt = msg.path.split(".").pop().toLowerCase();
|
|
1162
1172
|
if (pendingRenderedOpen && (requestedExt === "md" || requestedExt === "mdx")) {
|
|
@@ -1183,13 +1193,14 @@ function showFileContent(msg, expectedRevision) {
|
|
|
1183
1193
|
lightweightPreview = (msg.size || 0) > FILE_RICH_PREVIEW_MAX_BYTES;
|
|
1184
1194
|
currentIsMarkdown = !lightweightPreview && (ext === "md" || ext === "mdx");
|
|
1185
1195
|
currentIsSvg = !lightweightPreview && ext === "svg";
|
|
1196
|
+
store.set({ fileViewerIsPlantUml: !lightweightPreview && isPlantUmlFile(msg.path) });
|
|
1186
1197
|
if (lightweightPreview) isRendered = false;
|
|
1187
1198
|
if (pendingRenderedOpen && currentIsMarkdown) isRendered = true;
|
|
1188
1199
|
|
|
1189
|
-
if (currentIsMarkdown || currentIsSvg) {
|
|
1200
|
+
if (currentIsMarkdown || currentIsSvg || store.get("fileViewerIsPlantUml")) {
|
|
1190
1201
|
renderBtn.classList.remove("hidden");
|
|
1191
1202
|
renderBtn.title = currentIsSvg ? "Show SVG source" : "Render markdown";
|
|
1192
|
-
copyBtn.title = currentIsSvg ? "Copy SVG source" : "Copy Markdown source";
|
|
1203
|
+
copyBtn.title = store.get("fileViewerIsPlantUml") ? "Copy PlantUML source" : currentIsSvg ? "Copy SVG source" : "Copy Markdown source";
|
|
1193
1204
|
} else {
|
|
1194
1205
|
copyBtn.title = "Copy contents";
|
|
1195
1206
|
}
|
|
@@ -1203,6 +1214,9 @@ function showFileContent(msg, expectedRevision) {
|
|
|
1203
1214
|
? (previousContent == null ? "" : previousContent)
|
|
1204
1215
|
: null;
|
|
1205
1216
|
renderBody(transitionFrom, refreshSlideIndex);
|
|
1217
|
+
} else if (store.get("fileViewerIsPlantUml")) {
|
|
1218
|
+
if (!keepRenderState) isRendered = !store.get("pendingFileNavigation");
|
|
1219
|
+
renderPlantUmlBody();
|
|
1206
1220
|
} else if (currentIsSvg) {
|
|
1207
1221
|
if (!keepRenderState) isRendered = true;
|
|
1208
1222
|
renderSvgBody();
|
|
@@ -1269,6 +1283,16 @@ function showFileContent(msg, expectedRevision) {
|
|
|
1269
1283
|
}
|
|
1270
1284
|
}
|
|
1271
1285
|
|
|
1286
|
+
function renderPlantUmlBody() {
|
|
1287
|
+
var bodyEl = document.getElementById("file-viewer-body");
|
|
1288
|
+
var renderBtn = document.getElementById("file-viewer-render");
|
|
1289
|
+
cancelPlantUmlPreview();
|
|
1290
|
+
renderBtn.classList.toggle("active", isRendered);
|
|
1291
|
+
renderBtn.title = isRendered ? "Show source" : "Show diagram preview";
|
|
1292
|
+
if (isRendered) renderPlantUmlPreview(bodyEl, currentContent, currentFilePath);
|
|
1293
|
+
else renderCodeWithLineNumbers(bodyEl, currentContent, "plaintext");
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1272
1296
|
function renderSvgBody() {
|
|
1273
1297
|
var bodyEl = document.getElementById("file-viewer-body");
|
|
1274
1298
|
var renderBtn = document.getElementById("file-viewer-render");
|
|
@@ -1626,6 +1650,8 @@ function rerenderFileContent() {
|
|
|
1626
1650
|
|
|
1627
1651
|
if (currentIsMarkdown) {
|
|
1628
1652
|
renderBody();
|
|
1653
|
+
} else if (store.get("fileViewerIsPlantUml")) {
|
|
1654
|
+
renderPlantUmlBody();
|
|
1629
1655
|
} else if (currentIsSvg) {
|
|
1630
1656
|
renderSvgBody();
|
|
1631
1657
|
} else {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { renderWireframeBlocks } from './wireframe-chat.js';
|
|
1
2
|
import { copyToClipboard, escapeHtml } from './utils.js';
|
|
2
3
|
import { refreshIcons } from './icons.js';
|
|
3
4
|
import { getMermaidThemeVars } from './theme.js';
|
|
@@ -128,7 +129,7 @@ export function highlightCodeBlocks(el) {
|
|
|
128
129
|
// messages container, so unhighlighted blocks (selector :not(.hljs))
|
|
129
130
|
// are picked up in a single pass.
|
|
130
131
|
if (store.get('replayingHistory')) return;
|
|
131
|
-
el.querySelectorAll("pre code:not(.hljs):not(.language-mermaid)").forEach(function (block) {
|
|
132
|
+
el.querySelectorAll("pre code:not(.hljs):not(.language-mermaid):not(.language-plantuml):not(.language-puml):not(.language-salt):not(.language-clay-sketch)").forEach(function (block) {
|
|
132
133
|
var cls = Array.from(block.classList).find(function (c) { return c.startsWith("language-"); });
|
|
133
134
|
if (cls) {
|
|
134
135
|
var lang = cls.replace("language-", "");
|
|
@@ -167,6 +168,7 @@ export function renderMermaidBlocks(el) {
|
|
|
167
168
|
// Same rationale as highlightCodeBlocks: defer mermaid renders until
|
|
168
169
|
// history_done so they don't cause cascading layout shifts during replay.
|
|
169
170
|
if (store.get('replayingHistory')) return;
|
|
171
|
+
renderWireframeBlocks(el);
|
|
170
172
|
var blocks = el.querySelectorAll("pre code.language-mermaid");
|
|
171
173
|
blocks.forEach(function (codeEl) {
|
|
172
174
|
var pre = codeEl.parentElement;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { escapeHtml } from './utils.js';
|
|
2
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
|
+
import { store } from './store.js';
|
|
4
|
+
import { mcpPermissionModeApplicability, sendMcpPermissionMode } from './mcp-permissions.js';
|
|
5
|
+
|
|
6
|
+
var labels = { ready: 'Ready', idle: 'Saved · connects when used', connecting: 'Connecting…', auth_required: 'Sign-in required', unavailable: 'Unavailable' };
|
|
7
|
+
function text(value) { return escapeHtml(String(value || '')); }
|
|
8
|
+
function button(label, action, primary, icon) {
|
|
9
|
+
return '<button type="button" class="mc-button' + (primary ? ' mc-primary' : '') + '" data-action="' + action + '">' + (icon ? iconHtml(icon) : '') + text(label) + '</button>';
|
|
10
|
+
}
|
|
11
|
+
function heading(title, subtitle, icon) {
|
|
12
|
+
return '<div class="mc-heading">' + (icon ? '<span class="mc-emblem">' + iconHtml(icon) + '</span>' : '') + '<h2 tabindex="-1">' + text(title) + '</h2><p>' + text(subtitle) + '</p></div>';
|
|
13
|
+
}
|
|
14
|
+
function status(row) { return '<span class="mc-status" data-status="' + text(row.status) + '"><span aria-hidden="true"></span>' + text(labels[row.status] || 'Unavailable') + '</span>'; }
|
|
15
|
+
function switchControl(row, busy) {
|
|
16
|
+
return '<button type="button" class="mc-switch" role="switch" aria-checked="' + !!row.projectEnabled + '" aria-label="Enable ' + text(row.label || row.name) + ' in this project" data-toggle="' + text(row.id) + '"' + (busy ? ' disabled' : '') + '><span></span></button>';
|
|
17
|
+
}
|
|
18
|
+
function rows(servers, busy) {
|
|
19
|
+
return '<div class="mc-list">' + servers.map(function (row) {
|
|
20
|
+
return '<div class="mc-row"><button type="button" class="mc-row-open" data-open="' + text(row.id) + '"><span class="mc-server-icon">' + iconHtml(row.source === 'url' ? 'globe' : 'monitor') + '</span><span class="mc-row-info"><strong>' + text(row.label || row.name) + '</strong><span>' + (row.source === 'url' ? 'Remote' : 'Your computer') + ' · ' + row.toolCount + ' tools</span>' + status(row) + '</span></button>' + switchControl(row, busy) + '</div>';
|
|
21
|
+
}).join('') + '</div>';
|
|
22
|
+
}
|
|
23
|
+
function search(value, placeholder) { return '<label class="mc-search">' + iconHtml('search') + '<input type="search" data-search value="' + text(value) + '" placeholder="' + placeholder + '" aria-label="' + placeholder + '"></label>'; }
|
|
24
|
+
function listView(state) {
|
|
25
|
+
if (!state.loaded) return heading('Your connections', 'Loading your MCP servers…', 'cable');
|
|
26
|
+
if (!state.servers.length) return '<div class="mc-empty">' + heading('Connect tools to Clay', 'Let your agent work with the services and tools you choose.', 'cable') + button('Add connection', 'add', true, 'plus') + '<p>Connect a remote service or tools on your computer.</p></div>';
|
|
27
|
+
var servers = state.servers.filter(function (row) { return (row.label || row.name).toLowerCase().includes((state.search || '').toLowerCase()); });
|
|
28
|
+
return search(state.search, 'Search servers…') + '<div class="mc-section-heading"><h2>Your connections</h2>' + button('Add', 'add', false, 'plus') + '</div>' + rows(servers, !!state.pending) + (!servers.length ? '<p class="mc-muted">No matching connections.</p>' : '') + '<p class="mc-footnote">Enabled connections are available to your private sessions in this project. In multi-user Clay, shared conversations cannot use personal connections.</p>';
|
|
29
|
+
}
|
|
30
|
+
function addView() {
|
|
31
|
+
return heading('Add connection', 'Choose where your tools are available.') + '<div class="mc-choices">' +
|
|
32
|
+
'<button class="mc-choice" data-action="remote"><span class="mc-choice-icon">' + iconHtml('globe') + '</span><span><strong>Remote MCP URL</strong><small>Connect a hosted service using its MCP server address.</small></span>' + iconHtml('arrow-right') + '</button>' +
|
|
33
|
+
'<button class="mc-choice" data-action="computer"><span class="mc-choice-icon">' + iconHtml('monitor') + '</span><span><strong>On your computer</strong><small>Connect local tools through the Clay browser extension.</small></span>' + iconHtml('arrow-right') + '</button></div>';
|
|
34
|
+
}
|
|
35
|
+
function remoteView(state) {
|
|
36
|
+
return heading('Connect a remote server', 'Use the MCP address supplied by your service provider.', 'globe') +
|
|
37
|
+
'<form data-form="remote" class="mc-form"><label>Server URL<input name="url" type="url" required maxlength="4096" placeholder="https://mcp.example.com/mcp" value="' + text(store.get('mcpConnectionDraft')) + '" autocomplete="off" spellcheck="false"></label>' +
|
|
38
|
+
'<p class="mc-muted">Public HTTPS servers are supported. You can sign in with Clay’s private browser if the service requires it.</p><button class="mc-button mc-primary" type="submit"' + (state.pending ? ' disabled' : '') + '>' + (state.pending ? 'Checking server…' : 'Continue') + '</button></form>';
|
|
39
|
+
}
|
|
40
|
+
function computerView(state) {
|
|
41
|
+
var servers = state.servers.filter(function (row) { return row.source === 'computer'; });
|
|
42
|
+
var command = 'npx clay-mcp-bridge install ' + (state.extensionId || '<extension-id>');
|
|
43
|
+
return heading('Tools on your computer', 'Your local tools connect through the Clay extension.', 'monitor') +
|
|
44
|
+
'<ol class="mc-steps"><li><span class="mc-step-marker">' + (state.extensionConnected ? iconHtml('check') : '1') + '</span><div><strong>Browser extension</strong><p>' + (state.extensionConnected ? 'Connected to this browser.' : 'Install Clay for Chrome, then refresh this page.') + '</p>' + (!state.extensionConnected ? button('Set up extension', 'setup', false, 'puzzle') : '') + '</div></li>' +
|
|
45
|
+
'<li><span class="mc-step-marker">' + (state.hostConnected ? iconHtml('check') : '2') + '</span><div><strong>Local bridge</strong><p>' + (state.hostConnected ? 'Connected to your computer.' : 'Run this command on your computer, then restart your browser.') + '</p>' + (!state.hostConnected ? '<div class="mc-command"><code>' + text(command) + '</code>' + button('Copy', 'copy', false) + '</div>' : '') + '</div></li>' +
|
|
46
|
+
'<li><span class="mc-step-marker">3</span><div><strong>Choose your tools</strong><p>Add servers from the extension popup. They appear here when connected.</p></div></li></ol>' + rows(servers, !!state.pending) + button('Refresh connections', 'refresh', false, 'refresh-cw');
|
|
47
|
+
}
|
|
48
|
+
function permissionsView(row) {
|
|
49
|
+
var current = store.snap(), availability = mcpPermissionModeApplicability(current);
|
|
50
|
+
var selected = (current.mcpPermissionModeOverrides || {})[row.name] || '';
|
|
51
|
+
var pending = Object.values(current.mcpPermissionModePendingByTarget || {}).some(function (item) { return item.serverName === row.name && item.sessionId === current.activeSessionId; });
|
|
52
|
+
return '<div class="mc-permissions"><h3>Tool approvals</h3><p>Calls follow the active session’s permission policy. Enabling a connection does not automatically approve every action.</p><label>AI review<select data-permission' + (!availability.applicable || pending ? ' disabled' : '') + '>' +
|
|
53
|
+
[['', 'Use session default'], ['auto', 'AI review'], ['default', 'Ask every time']].map(function (item) { return '<option value="' + item[0] + '"' + (selected === item[0] ? ' selected' : '') + '>' + item[1] + '</option>'; }).join('') + '</select></label>' + (!availability.applicable ? '<p class="mc-muted">' + text(availability.reason) + '</p>' : '') + '</div>';
|
|
54
|
+
}
|
|
55
|
+
function detailView(state, row) {
|
|
56
|
+
var remote = row.source === 'url';
|
|
57
|
+
var html = heading(row.label || row.name, remote ? 'Remote MCP connection' : 'Connected through your browser extension', remote ? 'globe' : 'monitor');
|
|
58
|
+
html += status(row) + '<div class="mc-enable"><span>Enabled in this project</span>' + switchControl(row, !!state.pending) + '</div>';
|
|
59
|
+
html += '<dl class="mc-metadata"><dt>Access</dt><dd>Your account only</dd><dt>' + (remote ? 'Server' : 'Location') + '</dt><dd>' + (remote ? text(row.url) : 'Your computer') + '</dd></dl>';
|
|
60
|
+
if (row.error) html += '<div class="mc-alert" role="status">' + iconHtml('circle-alert') + '<span>' + text(row.error) + '</span></div>';
|
|
61
|
+
if (remote && row.status === 'auth_required') html += '<div class="mc-auth">' + button('Sign in with Clay browser', 'signin', true, 'log-in') + '<p class="mc-muted">Only you can view and control this sign-in browser.</p></div>';
|
|
62
|
+
if (remote) html += '<details class="mc-token"><summary>Use an access token</summary><form data-form="token" class="mc-form"><label>Access token<input name="token" type="password" required maxlength="8192" autocomplete="new-password" placeholder="Paste your access token"></label><button type="submit" class="mc-button">Save and connect</button></form></details>';
|
|
63
|
+
html += '<div class="mc-detail-tabs" role="tablist" aria-label="Connection details"><button role="tab" data-tab="tools" aria-selected="' + (state.detailTab !== 'permissions') + '">Tools (' + row.toolCount + ')</button><button role="tab" data-tab="permissions" aria-selected="' + (state.detailTab === 'permissions') + '">Permissions</button></div>';
|
|
64
|
+
if (state.detailTab === 'permissions') html += permissionsView(row);
|
|
65
|
+
else {
|
|
66
|
+
html += search(state.search, 'Search tools…');
|
|
67
|
+
var tools = (row.tools || []).filter(function (tool) { return (tool.name + ' ' + (tool.description || '')).toLowerCase().includes((state.search || '').toLowerCase()); });
|
|
68
|
+
html += '<div class="mc-tools">' + tools.map(function (tool) { return '<details><summary>' + text(tool.name) + '</summary><p>' + text(tool.description || 'No description provided.') + '</p><h4>Inputs</h4><pre>' + text(JSON.stringify(tool.inputSchema || {}, null, 2)) + '</pre></details>'; }).join('') + (!tools.length ? '<p class="mc-muted">' + (row.toolCount ? 'No matching tools.' : 'No tools available. Test the connection to refresh its catalog.') + '</p>' : '') + '</div>';
|
|
69
|
+
}
|
|
70
|
+
html += '<div class="mc-detail-actions">' + (remote ? button('Test connection', 'test', false, 'refresh-cw') + (row.status === 'auth_required' ? '' : button('Sign in again', 'signin', false, 'log-in')) : button('Computer setup', 'computer', false, 'monitor')) + '</div>';
|
|
71
|
+
if (remote) html += '<details class="mc-manage"><summary>Manage connection</summary><form data-form="rename" class="mc-form"><label>Name<input name="label" required maxlength="100" value="' + text(row.label) + '"></label><button type="submit" class="mc-button">Save name</button></form>' + button('Remove connection', 'remove', false, 'trash-2') + '</details>';
|
|
72
|
+
return html;
|
|
73
|
+
}
|
|
74
|
+
export function renderConnections(root, state, events) {
|
|
75
|
+
var sameScreen = root.dataset.screenKey === (state.screen || 'list') + ':' + (state.selected || '');
|
|
76
|
+
var disclosures = sameScreen ? Array.from(root.querySelectorAll('details[open]')).map(function (el) { return el.className || el.querySelector('summary').textContent; }) : [];
|
|
77
|
+
var drafts = {};
|
|
78
|
+
if (sameScreen) root.querySelectorAll('input[name]').forEach(function (el) { drafts[el.name] = el.value; });
|
|
79
|
+
var focused = root.contains(document.activeElement) ? document.activeElement : null;
|
|
80
|
+
var searchFocused = focused && focused.hasAttribute('data-search');
|
|
81
|
+
var start = focused && focused.tagName === 'INPUT' ? focused.selectionStart : null;
|
|
82
|
+
var focusedName = focused && focused.name;
|
|
83
|
+
var row = state.servers.find(function (item) { return item.id === state.selected; });
|
|
84
|
+
var screen = state.screen || 'list';
|
|
85
|
+
var html = screen !== 'list' ? '<button type="button" class="mc-back" data-action="back">' + iconHtml('arrow-left') + 'MCP Servers</button>' : '';
|
|
86
|
+
if (state.error) html += '<div class="mc-alert" role="alert">' + iconHtml('circle-alert') + '<span>' + text(state.error) + '</span></div>';
|
|
87
|
+
if (state.notice) html += '<p class="mc-notice" role="status">' + text(state.notice) + '</p>';
|
|
88
|
+
if (screen === 'add') html += addView();
|
|
89
|
+
else if (screen === 'remote') html += remoteView(state);
|
|
90
|
+
else if (screen === 'computer') html += computerView(state);
|
|
91
|
+
else if (screen === 'remove' && row) html += heading('Remove ' + row.label + '?', 'This removes saved credentials and disables its tools in this project.') + '<div role="group" aria-label="Confirm removal">' + button('Cancel', 'cancel-remove', false) + button('Remove connection', 'confirm-remove', true, 'trash-2') + '</div>';
|
|
92
|
+
else if (screen === 'detail' && row) html += detailView(state, row);
|
|
93
|
+
else html += listView(state);
|
|
94
|
+
if (state.pending && screen !== 'remote') html += '<p class="mc-working" role="status">' + iconHtml('loader-circle') + 'Updating connection…</p>';
|
|
95
|
+
root.innerHTML = '<div class="mc-screen">' + html + '</div>';
|
|
96
|
+
root.dataset.screenKey = screen + ':' + (state.selected || '');
|
|
97
|
+
root.querySelectorAll('details').forEach(function (el) { el.open = disclosures.indexOf(el.className || el.querySelector('summary').textContent) >= 0; });
|
|
98
|
+
root.querySelectorAll('input[name]').forEach(function (el) {
|
|
99
|
+
if (Object.prototype.hasOwnProperty.call(drafts, el.name)) el.value = drafts[el.name];
|
|
100
|
+
if (focusedName === el.name) { el.focus(); try { el.setSelectionRange(start, start); } catch (error) {} }
|
|
101
|
+
});
|
|
102
|
+
if (!sameScreen) { var title = root.querySelector('h2'); if (title) title.focus({ preventScroll: true }); }
|
|
103
|
+
root.querySelectorAll('[data-action]').forEach(function (el) {
|
|
104
|
+
var action = el.dataset.action;
|
|
105
|
+
if (action === 'copy' && !state.extensionId) el.disabled = true;
|
|
106
|
+
if (state.pending && ['signin', 'test', 'remove', 'confirm-remove'].includes(action)) el.disabled = true;
|
|
107
|
+
el.onclick = function () {
|
|
108
|
+
if (action === 'back') events.navigate('list');
|
|
109
|
+
else if (['add', 'remote', 'computer'].includes(action)) events.navigate(action);
|
|
110
|
+
else if (action === 'remove' && row) events.remove(row);
|
|
111
|
+
else if (action === 'cancel-remove') events.navigate('detail', row.id);
|
|
112
|
+
else if (action === 'confirm-remove') events.action('remove', { id: row.id });
|
|
113
|
+
else if (action === 'refresh') events.refresh();
|
|
114
|
+
else if (action === 'setup') events.setup();
|
|
115
|
+
else if (action === 'copy') navigator.clipboard.writeText('npx clay-mcp-bridge install ' + (state.extensionId || '<extension-id>')).then(function () { el.textContent = 'Copied'; }).catch(function () { el.textContent = 'Select command to copy'; });
|
|
116
|
+
else events.action(action);
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
root.querySelectorAll('[data-open]').forEach(function (el) { el.onclick = function () { events.navigate('detail', el.dataset.open); }; });
|
|
120
|
+
root.querySelectorAll('[data-toggle]').forEach(function (el) { el.onclick = function () { var item = state.servers.find(function (server) { return server.id === el.dataset.toggle; }); if (item) events.toggle(item, !item.projectEnabled); }; });
|
|
121
|
+
root.querySelectorAll('[data-tab]').forEach(function (el) { el.onclick = function () { store.set({ mcpConnections: Object.assign({}, store.get('mcpConnections'), { detailTab: el.dataset.tab, search: '' }) }); }; });
|
|
122
|
+
var input = root.querySelector('[data-search]');
|
|
123
|
+
if (input) { input.oninput = function () { events.search(input.value); }; if (searchFocused) { input.focus(); try { input.setSelectionRange(start, start); } catch (error) {} } }
|
|
124
|
+
var urlInput = root.querySelector('[name="url"]'); if (urlInput) urlInput.oninput = function () { events.draft(urlInput.value); };
|
|
125
|
+
root.querySelectorAll('form').forEach(function (form) {
|
|
126
|
+
form.onsubmit = function (event) {
|
|
127
|
+
event.preventDefault(); if (state.pending) return;
|
|
128
|
+
if (form.dataset.form === 'remote') events.action('add', { url: form.elements.url.value });
|
|
129
|
+
if (form.dataset.form === 'token') { var token = form.elements.token.value; form.reset(); events.action('token', { token: token }); }
|
|
130
|
+
if (form.dataset.form === 'rename') events.action('rename', { label: form.elements.label.value });
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
var permission = root.querySelector('[data-permission]');
|
|
134
|
+
if (permission) permission.onchange = function () { sendMcpPermissionMode(store.get('currentSlug'), store.get('activeSessionId'), row.name, permission.value); };
|
|
135
|
+
refreshIcons(root);
|
|
136
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { showToast } from './utils.js';
|
|
4
|
+
|
|
5
|
+
var MCP_PERMISSION_TIMEOUT_MS = 10000;
|
|
6
|
+
var _permissionModeTimers = {};
|
|
7
|
+
|
|
8
|
+
function pendingKey(projectSlug, sessionId, requestId) {
|
|
9
|
+
return String(projectSlug) + ":" + String(sessionId) + ":" + String(requestId);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pendingMap() {
|
|
13
|
+
return store.get('mcpPermissionModePendingByTarget') || {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function setPendingMap(next) {
|
|
17
|
+
store.set({ mcpPermissionModePendingByTarget: next });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findPending(requestId) {
|
|
21
|
+
var pending = pendingMap();
|
|
22
|
+
var keys = Object.keys(pending);
|
|
23
|
+
for (var i = 0; i < keys.length; i++) {
|
|
24
|
+
if (pending[keys[i]].requestId === requestId) return { key: keys[i], value: pending[keys[i]] };
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clearPendingRequest(requestId) {
|
|
30
|
+
var found = findPending(requestId);
|
|
31
|
+
if (!found) return null;
|
|
32
|
+
if (_permissionModeTimers[requestId]) clearTimeout(_permissionModeTimers[requestId]);
|
|
33
|
+
delete _permissionModeTimers[requestId];
|
|
34
|
+
var next = Object.assign({}, pendingMap());
|
|
35
|
+
delete next[found.key];
|
|
36
|
+
setPendingMap(next);
|
|
37
|
+
return found.value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function clearMcpPermissionModePending() {
|
|
41
|
+
var requestIds = Object.keys(_permissionModeTimers);
|
|
42
|
+
for (var i = 0; i < requestIds.length; i++) clearTimeout(_permissionModeTimers[requestIds[i]]);
|
|
43
|
+
_permissionModeTimers = {};
|
|
44
|
+
if (Object.keys(pendingMap()).length) setPendingMap({});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function mcpPermissionModeApplicability(state) {
|
|
48
|
+
if (!state.permissionCapabilities || state.permissionCapabilities.mcpOverride !== true) {
|
|
49
|
+
return { applicable: false, reason: "AI review is unavailable for this SDK session." };
|
|
50
|
+
}
|
|
51
|
+
if (state.effectivePermissionMode === "auto" || state.effectivePermissionMode === "bypassPermissions") {
|
|
52
|
+
return { applicable: true, reason: "" };
|
|
53
|
+
}
|
|
54
|
+
if (!state.effectivePermissionMode && state.currentMode === "auto") {
|
|
55
|
+
return { applicable: false, reason: "Waiting for the SDK to confirm Auto." };
|
|
56
|
+
}
|
|
57
|
+
return { applicable: false, reason: "AI review requires an active Auto session." };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function sendMcpPermissionMode(projectSlug, sessionId, serverName, mode) {
|
|
61
|
+
var state = store.snap();
|
|
62
|
+
var applicability = mcpPermissionModeApplicability(state);
|
|
63
|
+
var ws = getWs();
|
|
64
|
+
if (!state.connected || state.currentSlug !== projectSlug || state.activeSessionId !== sessionId || !applicability.applicable || !ws || ws.readyState !== 1) {
|
|
65
|
+
showToast(applicability.applicable ? "Reconnect before changing AI review policy." : applicability.reason, "error");
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
var pending = pendingMap();
|
|
69
|
+
var pendingKeys = Object.keys(pending);
|
|
70
|
+
for (var i = 0; i < pendingKeys.length; i++) {
|
|
71
|
+
var item = pending[pendingKeys[i]];
|
|
72
|
+
if (item.projectSlug === projectSlug && item.sessionId === sessionId && item.serverName === serverName) return false;
|
|
73
|
+
}
|
|
74
|
+
var requestId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : String(Date.now());
|
|
75
|
+
var key = pendingKey(projectSlug, sessionId, requestId);
|
|
76
|
+
var overrides = state.mcpPermissionModeOverrides || {};
|
|
77
|
+
var next = Object.assign({}, pending);
|
|
78
|
+
next[key] = {
|
|
79
|
+
projectSlug: projectSlug,
|
|
80
|
+
sessionId: sessionId,
|
|
81
|
+
requestId: requestId,
|
|
82
|
+
serverName: serverName,
|
|
83
|
+
mode: mode || "",
|
|
84
|
+
previousMode: overrides[serverName] || ""
|
|
85
|
+
};
|
|
86
|
+
setPendingMap(next);
|
|
87
|
+
_permissionModeTimers[requestId] = setTimeout(function () {
|
|
88
|
+
if (!clearPendingRequest(requestId)) return;
|
|
89
|
+
showToast("Changing AI review policy timed out.", "error");
|
|
90
|
+
}, MCP_PERMISSION_TIMEOUT_MS);
|
|
91
|
+
try {
|
|
92
|
+
var result = ws.send(JSON.stringify({
|
|
93
|
+
type: "set_mcp_permission_mode_override",
|
|
94
|
+
projectSlug: projectSlug,
|
|
95
|
+
sessionId: sessionId,
|
|
96
|
+
requestId: requestId,
|
|
97
|
+
serverName: serverName,
|
|
98
|
+
mode: mode || null,
|
|
99
|
+
}));
|
|
100
|
+
if (result === false) throw new Error("WebSocket send failed");
|
|
101
|
+
} catch (error) {
|
|
102
|
+
clearPendingRequest(requestId);
|
|
103
|
+
showToast("Failed to send the AI review policy change.", "error");
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function handleMcpPermissionModeResult(msg) {
|
|
110
|
+
var found = findPending(msg.requestId);
|
|
111
|
+
if (!found) return;
|
|
112
|
+
var pending = found.value;
|
|
113
|
+
if (msg.projectSlug && msg.projectSlug !== pending.projectSlug) return;
|
|
114
|
+
if (msg.sessionId !== pending.sessionId || msg.serverName !== pending.serverName) return;
|
|
115
|
+
clearPendingRequest(msg.requestId);
|
|
116
|
+
if (pending.projectSlug !== store.get('currentSlug') || pending.sessionId !== store.get('activeSessionId')) return;
|
|
117
|
+
if (!msg.ok) {
|
|
118
|
+
showToast(msg.error || "Failed to change AI review policy.", "error");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
store.set({ mcpPermissionModeOverrides: msg.mcpPermissionModeOverrides || {} });
|
|
122
|
+
}
|