clay-server 4.2.0-beta.1 → 4.2.0-beta.2
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 +5 -2
- package/lib/project-full-access.js +69 -56
- package/lib/project-sessions.js +126 -26
- package/lib/project-user-message.js +5 -2
- package/lib/project.js +1 -0
- package/lib/public/app.js +6 -0
- package/lib/public/css/filebrowser.css +20 -0
- package/lib/public/css/messages.css +27 -0
- package/lib/public/css/overlays.css +60 -0
- package/lib/public/css/pane.css +21 -4
- package/lib/public/css/pending-message-queue.css +5 -0
- package/lib/public/index.html +8 -3
- package/lib/public/modules/app-connection.js +4 -0
- package/lib/public/modules/app-header.js +18 -25
- package/lib/public/modules/app-messages.js +17 -7
- package/lib/public/modules/app-panels.js +22 -11
- package/lib/public/modules/clay-file-links.js +7 -0
- package/lib/public/modules/clay-log-links.js +4 -0
- package/lib/public/modules/github-links.js +115 -0
- package/lib/public/modules/markdown.js +9 -2
- package/lib/public/modules/mcp-ui.js +154 -17
- package/lib/public/modules/pane-bridge.js +2 -0
- package/lib/public/modules/pane-file-bridge.js +69 -0
- package/lib/public/modules/pending-message-queue-model.js +6 -0
- package/lib/public/modules/pending-message-queue-recovery.js +73 -0
- package/lib/public/modules/pending-message-queue.js +67 -64
- package/lib/public/modules/permission-control.js +187 -0
- package/lib/public/modules/split-view.js +29 -29
- package/lib/sdk-bridge.js +95 -40
- package/lib/session-permission-mode.js +42 -0
- package/lib/sessions.js +14 -1
- package/lib/ws-schema.js +2 -0
- package/lib/yoke/adapters/claude-worker-policy.js +67 -0
- package/lib/yoke/adapters/claude-worker.js +47 -7
- package/lib/yoke/adapters/claude.js +114 -6
- package/package.json +1 -1
package/lib/public/index.html
CHANGED
|
@@ -408,9 +408,14 @@
|
|
|
408
408
|
<span class="header-title" id="header-title">Connecting...</span>
|
|
409
409
|
<button id="header-rename-btn" type="button" title="Rename session"><i data-lucide="pencil"></i></button>
|
|
410
410
|
<button id="header-info-btn" type="button" title="Session info"><i data-lucide="info"></i></button>
|
|
411
|
-
<
|
|
412
|
-
<
|
|
413
|
-
|
|
411
|
+
<div id="header-full-access-btn" class="session-permission-control hidden" role="group" aria-label="Session permission mode">
|
|
412
|
+
<div class="session-permission-segmented">
|
|
413
|
+
<button type="button" data-permission-mode="default">Ask</button>
|
|
414
|
+
<button type="button" data-permission-mode="auto">Auto</button>
|
|
415
|
+
<button type="button" data-permission-mode="bypassPermissions" aria-label="Skip permissions"><span class="permission-label-long">Skip permissions</span><span class="permission-label-short" aria-hidden="true">Skip</span></button>
|
|
416
|
+
</div>
|
|
417
|
+
<span class="session-permission-status hidden" aria-live="polite"></span>
|
|
418
|
+
</div>
|
|
414
419
|
<button type="button" id="header-tui-close-btn" class="header-tui-close-btn hidden" title="Close terminal (keeps history, resume anytime)" aria-label="Close terminal"><i data-lucide="power"></i></button>
|
|
415
420
|
<div id="mate-mobile-title" class="mate-mobile-title hidden">
|
|
416
421
|
<img id="mate-mobile-avatar" class="mate-mobile-avatar" alt="">
|
|
@@ -20,6 +20,8 @@ import { beginDefaultAiConnection, requestDefaultAi } from './default-ai.js';
|
|
|
20
20
|
import { beginContextViewConnection, requestContextView } from './context-view-preference.js';
|
|
21
21
|
import { beginDefaultVendorConnection, requestDefaultVendor } from './default-vendor.js';
|
|
22
22
|
import { clearProjectSplitState } from './split-session-boundary.js';
|
|
23
|
+
import { clearPermissionModePending } from './permission-control.js';
|
|
24
|
+
import { clearMcpPermissionModePending } from './mcp-ui.js';
|
|
23
25
|
|
|
24
26
|
var reconnectTimer = null;
|
|
25
27
|
var reconnectDelay = 1000;
|
|
@@ -165,6 +167,8 @@ export function setStatus(status) {
|
|
|
165
167
|
} else if (status === "processing") {
|
|
166
168
|
store.set({ processing: true });
|
|
167
169
|
} else {
|
|
170
|
+
clearPermissionModePending();
|
|
171
|
+
clearMcpPermissionModePending();
|
|
168
172
|
store.set({ connected: false, processing: false });
|
|
169
173
|
}
|
|
170
174
|
}
|
|
@@ -12,7 +12,7 @@ import { saveToolState, resetToolState, restoreToolState } from './tools.js';
|
|
|
12
12
|
import { getSessionUsage, setSessionUsage, getContextData, setContextData, updateContextPanel, updateUsagePanel } from './app-panels.js';
|
|
13
13
|
import { onHistoryPrepended as onSessionSearchHistoryPrepended } from './session-search.js';
|
|
14
14
|
import { getCachedSessions } from './sidebar-sessions.js';
|
|
15
|
-
import {
|
|
15
|
+
import { bindPermissionControl, renderPermissionControl } from './permission-control.js';
|
|
16
16
|
|
|
17
17
|
// --- Module-owned state ---
|
|
18
18
|
var sessionInfoPopover = null;
|
|
@@ -24,37 +24,30 @@ export function initHeader() {
|
|
|
24
24
|
var headerInfoBtn = document.getElementById("header-info-btn");
|
|
25
25
|
var headerFullAccessBtn = document.getElementById("header-full-access-btn");
|
|
26
26
|
|
|
27
|
-
function
|
|
28
|
-
if (!headerFullAccessBtn) return;
|
|
27
|
+
function permissionControlState() {
|
|
29
28
|
var s = store.snap();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
return {
|
|
30
|
+
projectSlug: s.currentSlug,
|
|
31
|
+
sessionId: s.activeSessionId,
|
|
32
|
+
vendor: s.currentVendor || "claude",
|
|
33
|
+
permissionMode: s.currentMode,
|
|
34
|
+
effectivePermissionMode: s.effectivePermissionMode,
|
|
35
|
+
permissionCapabilities: s.permissionCapabilities,
|
|
36
|
+
connected: s.connected,
|
|
37
|
+
globalPermissionModeForced: s.skipPermsEnabled === true,
|
|
38
|
+
visible: !s.dmMode && !s.splitPanes && s.activeSessionMode === "gui" && !!s.activeSessionId,
|
|
39
|
+
};
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (ws && ws.readyState === 1 && id) {
|
|
42
|
-
ws.send(JSON.stringify({ type: "set_session_full_access", id: id, enabled: enabled }));
|
|
43
|
-
}
|
|
42
|
+
function updateFullAccessButton() {
|
|
43
|
+
if (!headerFullAccessBtn) return;
|
|
44
|
+
renderPermissionControl(headerFullAccessBtn, permissionControlState());
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
if (headerFullAccessBtn) {
|
|
47
|
-
headerFullAccessBtn
|
|
48
|
-
if (store.get('sessionFullAccess')) {
|
|
49
|
-
sendFullAccess(false);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
showConfirm("Skip permission prompts for this session? Tool requests will be approved automatically.", function () {
|
|
53
|
-
sendFullAccess(true);
|
|
54
|
-
}, "Skip Permissions", true);
|
|
55
|
-
});
|
|
48
|
+
bindPermissionControl(headerFullAccessBtn, permissionControlState);
|
|
56
49
|
store.subscribe(function (state, prev) {
|
|
57
|
-
if (state.
|
|
50
|
+
if (state.currentMode !== prev.currentMode || state.effectivePermissionMode !== prev.effectivePermissionMode || state.permissionCapabilities !== prev.permissionCapabilities || state.permissionModePendingByTarget !== prev.permissionModePendingByTarget || state.connected !== prev.connected || state.activeSessionId !== prev.activeSessionId || state.currentSlug !== prev.currentSlug || state.currentVendor !== prev.currentVendor ||
|
|
58
51
|
state.activeSessionMode !== prev.activeSessionMode ||
|
|
59
52
|
state.dmMode !== prev.dmMode || state.splitPanes !== prev.splitPanes ||
|
|
60
53
|
state.skipPermsEnabled !== prev.skipPermsEnabled) updateFullAccessButton();
|
|
@@ -54,7 +54,8 @@ import { showRewindModal, onRewindComplete, setRewindMode, onRewindError, clearP
|
|
|
54
54
|
import { checkAdminAccess } from './admin.js';
|
|
55
55
|
import { mateAvatarUrl } from './avatar.js';
|
|
56
56
|
import { showImageModal, sendExtensionCommand, handleMcpToolCallMessage } from './app-misc.js';
|
|
57
|
-
import { handleMcpServersState } from './mcp-ui.js';
|
|
57
|
+
import { handleMcpServersState, handleMcpPermissionModeResult } from './mcp-ui.js';
|
|
58
|
+
import { handlePermissionModeResult } from './permission-control.js';
|
|
58
59
|
import { handleShellCommandResult } from './shell-command.js';
|
|
59
60
|
import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, isSchedulerOpen, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles } from './scheduler.js';
|
|
60
61
|
import { handleScheduledTaskMessage, handleScheduledTaskSessionSwitched } from './scheduled-tasks.js';
|
|
@@ -374,11 +375,9 @@ export function processMessage(msg) {
|
|
|
374
375
|
if (msg.ownerLocked !== undefined) store.set({ ownerLocked: !!msg.ownerLocked });
|
|
375
376
|
if (msg.osUsers !== undefined) store.set({ isOsUsers: !!msg.osUsers });
|
|
376
377
|
if (msg.lanHost) window.__lanHost = msg.lanHost;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (spBanner) spBanner.classList.remove("hidden");
|
|
381
|
-
}
|
|
378
|
+
store.set({ skipPermsEnabled: msg.dangerouslySkipPermissions === true });
|
|
379
|
+
var spBanner = document.getElementById("skip-perms-pill");
|
|
380
|
+
if (spBanner) spBanner.classList.toggle("hidden", msg.dangerouslySkipPermissions !== true);
|
|
382
381
|
updateProjectList(msg);
|
|
383
382
|
refreshProjectSessionVisibilitySettings(msg.projects);
|
|
384
383
|
break;
|
|
@@ -555,6 +554,9 @@ export function processMessage(msg) {
|
|
|
555
554
|
}
|
|
556
555
|
}
|
|
557
556
|
if (msg.mode) _cs.currentMode = msg.mode;
|
|
557
|
+
if (Object.prototype.hasOwnProperty.call(msg, 'effectivePermissionMode')) _cs.effectivePermissionMode = msg.effectivePermissionMode;
|
|
558
|
+
if (msg.permissionCapabilities) _cs.permissionCapabilities = msg.permissionCapabilities;
|
|
559
|
+
if (msg.mcpPermissionModeOverrides) _cs.mcpPermissionModeOverrides = msg.mcpPermissionModeOverrides;
|
|
558
560
|
if (msg.effort) _cs.currentEffort = msg.effort;
|
|
559
561
|
if (msg.betas) _cs.currentBetas = msg.betas;
|
|
560
562
|
if (msg.thinking) _cs.currentThinking = msg.thinking;
|
|
@@ -823,7 +825,7 @@ export function processMessage(msg) {
|
|
|
823
825
|
var _effectiveTerminalId = (typeof msg.runtimeTerminalId === "number")
|
|
824
826
|
? msg.runtimeTerminalId
|
|
825
827
|
: (typeof msg.terminalId === "number" ? msg.terminalId : null);
|
|
826
|
-
store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, currentModel: msg.model || "", currentEffort: msg.effort || store.get('currentEffort'), vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory, sessionVendorBound: !!msg.vendor || !!msg.hasHistory, sessionFullAccess: msg.permissionMode === "bypassPermissions", currentMode: msg.permissionMode || store.get('currentMode') });
|
|
828
|
+
store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, currentModel: msg.model || "", currentEffort: msg.effort || store.get('currentEffort'), vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory, sessionVendorBound: !!msg.vendor || !!msg.hasHistory, sessionFullAccess: msg.permissionMode === "bypassPermissions", currentMode: msg.permissionMode || store.get('currentMode'), effectivePermissionMode: msg.effectivePermissionMode || null, permissionCapabilities: msg.permissionCapabilities || { auto: false, mcpOverride: false }, mcpPermissionModeOverrides: msg.mcpPermissionModeOverrides || {} });
|
|
827
829
|
handleScheduledTaskSessionSwitched(msg);
|
|
828
830
|
requestLoopInterviewState();
|
|
829
831
|
replayPendingMessages(msg.id);
|
|
@@ -1598,6 +1600,14 @@ export function processMessage(msg) {
|
|
|
1598
1600
|
handleMcpServersState(msg);
|
|
1599
1601
|
break;
|
|
1600
1602
|
|
|
1603
|
+
case "mcp_permission_mode_result":
|
|
1604
|
+
handleMcpPermissionModeResult(msg);
|
|
1605
|
+
break;
|
|
1606
|
+
|
|
1607
|
+
case "permission_mode_result":
|
|
1608
|
+
handlePermissionModeResult(msg);
|
|
1609
|
+
break;
|
|
1610
|
+
|
|
1601
1611
|
case "term_created":
|
|
1602
1612
|
// Login terminals never come through here: they are created by the
|
|
1603
1613
|
// server-owned flow and announced with vendor_login_ready instead.
|
|
@@ -8,6 +8,7 @@ import { getWs } from './ws-ref.js';
|
|
|
8
8
|
import { getContextView as getStoredContextView, getEffectiveContextView, setContextView as setStoredContextView } from './context-view-preference.js';
|
|
9
9
|
import { reportPaneContext } from './pane-bridge.js';
|
|
10
10
|
import { setupModelPicker, renderModelPicker, requestVendorModels, prepareModelPickerOpen, modelDisplayName } from './model-picker.js';
|
|
11
|
+
import { sendPermissionMode } from './permission-control.js';
|
|
11
12
|
|
|
12
13
|
// --- Module-owned state (not in store) ---
|
|
13
14
|
var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
@@ -85,11 +86,12 @@ var contextMiniLabel = null;
|
|
|
85
86
|
|
|
86
87
|
// --- Constants ---
|
|
87
88
|
var MODE_OPTIONS = [
|
|
88
|
-
{ value: "default", label: "
|
|
89
|
+
{ value: "default", label: "Ask" },
|
|
89
90
|
{ value: "plan", label: "Plan" },
|
|
90
91
|
{ value: "acceptEdits", label: "Auto-accept edits" },
|
|
91
92
|
];
|
|
92
|
-
var MODE_FULL_AUTO = { value: "
|
|
93
|
+
var MODE_FULL_AUTO = { value: "auto", label: "Auto" };
|
|
94
|
+
var MODE_SKIP = { value: "bypassPermissions", label: "Skip permissions" };
|
|
93
95
|
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
94
96
|
var EFFORT_LEVELS_BY_VENDOR = {
|
|
95
97
|
claude: ["low", "medium", "high", "xhigh", "max"],
|
|
@@ -142,7 +144,8 @@ function modeDisplayName(value) {
|
|
|
142
144
|
for (var i = 0; i < MODE_OPTIONS.length; i++) {
|
|
143
145
|
if (MODE_OPTIONS[i].value === value) return MODE_OPTIONS[i].label;
|
|
144
146
|
}
|
|
145
|
-
if (value === "
|
|
147
|
+
if (value === "auto") return "Auto";
|
|
148
|
+
if (value === "bypassPermissions") return "Skip permissions";
|
|
146
149
|
if (value === "dontAsk") return "Don\u2019t ask";
|
|
147
150
|
return value;
|
|
148
151
|
}
|
|
@@ -196,22 +199,29 @@ function rebuildModeList() {
|
|
|
196
199
|
if (!configModeList) return;
|
|
197
200
|
configModeList.innerHTML = "";
|
|
198
201
|
var options = MODE_OPTIONS.slice();
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
+
options.push(MODE_FULL_AUTO);
|
|
203
|
+
options.push(MODE_SKIP);
|
|
204
|
+
var autoSupported = store.get('permissionCapabilities').auto === true;
|
|
205
|
+
var globallyForced = store.get('skipPermsEnabled') === true;
|
|
202
206
|
for (var i = 0; i < options.length; i++) {
|
|
203
207
|
var opt = options[i];
|
|
204
208
|
var btn = document.createElement("button");
|
|
205
209
|
btn.className = "config-radio-item";
|
|
206
|
-
if (opt.value === store.get('currentMode')) btn.classList.add("active");
|
|
210
|
+
if (opt.value === (globallyForced ? "bypassPermissions" : store.get('currentMode'))) btn.classList.add("active");
|
|
207
211
|
btn.dataset.mode = opt.value;
|
|
208
212
|
btn.textContent = opt.label;
|
|
213
|
+
if (opt.value === "auto" && !autoSupported) {
|
|
214
|
+
btn.disabled = true;
|
|
215
|
+
btn.title = "Auto permissions require a Claude session.";
|
|
216
|
+
btn.setAttribute("aria-description", "Unavailable because this is not a Claude session.");
|
|
217
|
+
}
|
|
218
|
+
if (globallyForced) {
|
|
219
|
+
btn.disabled = true;
|
|
220
|
+
btn.title = "Skip permissions is forced by the server setting.";
|
|
221
|
+
}
|
|
209
222
|
btn.addEventListener("click", function () {
|
|
210
223
|
var mode = this.dataset.mode;
|
|
211
|
-
|
|
212
|
-
if (ws && ws.readyState === 1) {
|
|
213
|
-
ws.send(JSON.stringify({ type: "set_permission_mode", mode: mode }));
|
|
214
|
-
}
|
|
224
|
+
sendPermissionMode(store.get('currentSlug'), store.get('activeSessionId'), mode);
|
|
215
225
|
configPopover.classList.add("hidden");
|
|
216
226
|
configChip.classList.remove("active");
|
|
217
227
|
});
|
|
@@ -420,6 +430,7 @@ export function initPanels() {
|
|
|
420
430
|
state.codexApproval !== prev.codexApproval ||
|
|
421
431
|
state.codexSandbox !== prev.codexSandbox ||
|
|
422
432
|
state.codexWebSearch !== prev.codexWebSearch ||
|
|
433
|
+
state.skipPermsEnabled !== prev.skipPermsEnabled ||
|
|
423
434
|
state.sessionHasHistory !== prev.sessionHasHistory ||
|
|
424
435
|
state.activeSessionMode !== prev.activeSessionMode) {
|
|
425
436
|
updateConfigChip();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { escapeHtml } from './utils.js';
|
|
4
4
|
import { refreshIcons } from './icons.js';
|
|
5
5
|
import { store } from './store.js';
|
|
6
|
+
import { forwardPaneFileReference } from './pane-bridge.js';
|
|
6
7
|
|
|
7
8
|
var openFileHandler = null;
|
|
8
9
|
var delegatedBinding = false;
|
|
@@ -99,6 +100,12 @@ function bindDelegatedHandler() {
|
|
|
99
100
|
document.addEventListener("click", function (event) {
|
|
100
101
|
var button = event.target && event.target.closest ? event.target.closest(".clay-file-link") : null;
|
|
101
102
|
if (!button) return;
|
|
103
|
+
if (store.get('paneMode') && forwardPaneFileReference(button.dataset.filePath, {
|
|
104
|
+
line: button.dataset.fileLine ? Number(button.dataset.fileLine) : null,
|
|
105
|
+
column: button.dataset.fileColumn ? Number(button.dataset.fileColumn) : null,
|
|
106
|
+
projectSlug: button.dataset.fileProjectSlug || null,
|
|
107
|
+
sessionId: button.dataset.fileSessionId || null
|
|
108
|
+
})) return;
|
|
102
109
|
if (!openFileHandler) { showUnavailable(button); return; }
|
|
103
110
|
var result = openFileHandler(button.dataset.filePath, {
|
|
104
111
|
line: button.dataset.fileLine ? Number(button.dataset.fileLine) : null,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { enhanceClayIssueLinks, isExactClayIssueReference } from './clay-issue-links.js';
|
|
2
|
+
import { enhanceGithubLinks } from './github-links.js';
|
|
2
3
|
|
|
3
4
|
export function renderClayRecordLink(href, text) {
|
|
4
5
|
if (!isExactClayLogReference(href) && !isExactClayIssueReference(href)) return null;
|
|
@@ -80,4 +81,7 @@ export function enhanceClayLogLinks(root) {
|
|
|
80
81
|
fragment.appendChild(document.createTextNode(text.slice(offset)));
|
|
81
82
|
nodes[i].parentNode.replaceChild(fragment, nodes[i]);
|
|
82
83
|
}
|
|
84
|
+
// Issues and Logs call this enhancer directly, without the transcript
|
|
85
|
+
// session-link pass. Keep external GitHub chips common to those surfaces.
|
|
86
|
+
enhanceGithubLinks(root);
|
|
83
87
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { escapeHtml } from './utils.js';
|
|
2
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
|
+
|
|
4
|
+
var GITLAB_RESERVED = { admin: true, dashboard: true, explore: true, groups: true, help: true, profile: true, projects: true, public: true, search: true, snippets: true, users: true };
|
|
5
|
+
|
|
6
|
+
function parseTarget(href, provider) {
|
|
7
|
+
if (typeof href !== "string" || !href) return null;
|
|
8
|
+
var url;
|
|
9
|
+
try { url = new URL(href); } catch (e) { return null; }
|
|
10
|
+
if (url.protocol !== "https:" || url.hostname !== provider.host || url.port || url.username || url.password) return null;
|
|
11
|
+
if (url.pathname.indexOf("//") !== -1 || /%(?:2f|5c)/i.test(url.pathname)) return null;
|
|
12
|
+
var parts = url.pathname.split("/").filter(function (part) { return part !== ""; });
|
|
13
|
+
var repoParts = parts;
|
|
14
|
+
var resourceParts = [];
|
|
15
|
+
if (provider.name === "github") {
|
|
16
|
+
repoParts = parts.slice(0, 2); resourceParts = parts.slice(2);
|
|
17
|
+
} else if (provider.separator) {
|
|
18
|
+
var separator = parts.indexOf(provider.separator);
|
|
19
|
+
if (separator !== -1) { repoParts = parts.slice(0, separator); resourceParts = parts.slice(separator + 1); }
|
|
20
|
+
}
|
|
21
|
+
if (repoParts.length < 2 || (provider.reserved && provider.reserved[repoParts[0].toLowerCase()])) return null;
|
|
22
|
+
if (provider.ownerPattern && !provider.ownerPattern.test(repoParts[0])) return null;
|
|
23
|
+
if (provider.repoPattern && !provider.repoPattern.test(repoParts[repoParts.length - 1])) return null;
|
|
24
|
+
for (var i = 0; i < repoParts.length; i++) {
|
|
25
|
+
if (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,99})$/.test(repoParts[i])) return null;
|
|
26
|
+
}
|
|
27
|
+
if (provider.name === "github" && repoParts.length !== 2) return null;
|
|
28
|
+
if (provider.name === "gitlab" && resourceParts.length > 2) return null;
|
|
29
|
+
var target = { url: url, provider: provider.name, icon: provider.icon, cssClass: provider.cssClass, owner: repoParts.slice(0, -1).join("/"), repo: repoParts[repoParts.length - 1], kind: "repository", label: repoParts.join("/") };
|
|
30
|
+
if (!resourceParts.length) return target;
|
|
31
|
+
if (provider.name === "github") {
|
|
32
|
+
if (resourceParts.length === 1 && (resourceParts[0] === "issues" || resourceParts[0] === "pulls")) {
|
|
33
|
+
target.kind = resourceParts[0]; target.label = resourceParts[0] === "issues" ? "Issues" : "Pull requests"; return target;
|
|
34
|
+
}
|
|
35
|
+
if (resourceParts.length === 2 && (resourceParts[0] === "issues" || resourceParts[0] === "pull") && /^\d+$/.test(resourceParts[1])) {
|
|
36
|
+
target.kind = resourceParts[0] === "pull" ? "pull" : "issue"; target.number = resourceParts[1]; target.label = target.kind === "pull" ? "PR #" + target.number : "Issue #" + target.number; return target;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (resourceParts.length === 1 && (resourceParts[0] === "issues" || resourceParts[0] === "merge_requests")) {
|
|
41
|
+
target.kind = resourceParts[0] === "issues" ? "issues" : "merge_requests"; target.label = target.kind === "issues" ? "Issues" : "Merge requests"; return target;
|
|
42
|
+
}
|
|
43
|
+
if (resourceParts.length === 2 && (resourceParts[0] === "issues" || resourceParts[0] === "merge_requests") && /^\d+$/.test(resourceParts[1])) {
|
|
44
|
+
target.kind = resourceParts[0] === "issues" ? "issue" : "merge_request"; target.number = resourceParts[1]; target.label = target.kind === "issue" ? "Issue #" + target.number : "MR !" + target.number; return target;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var GITHUB = { name: "github", displayName: "GitHub", host: "github.com", icon: "github", cssClass: "github-link-chip", ownerPattern: /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/, repoPattern: /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,99})$/ };
|
|
50
|
+
var GITLAB = { name: "gitlab", displayName: "GitLab", host: "gitlab.com", icon: "gitlab", cssClass: "gitlab-link-chip", separator: "-", reserved: GITLAB_RESERVED };
|
|
51
|
+
|
|
52
|
+
function plainLabel(text) {
|
|
53
|
+
return String(text || "").replace(/<[^>]*>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, " ").trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasCustomLabel(target, text) {
|
|
57
|
+
var label = plainLabel(text);
|
|
58
|
+
return !!label && label !== target.url.href;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function accessibleLabel(target, text) {
|
|
62
|
+
var location = target.owner + "/" + target.repo;
|
|
63
|
+
var detail = target.kind === "repository" ? "" : " · " + target.label;
|
|
64
|
+
return hasCustomLabel(target, text) ? plainLabel(text) + " · " + location + detail : location + detail;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function chipMarkup(target, text) {
|
|
68
|
+
var custom = hasCustomLabel(target, text);
|
|
69
|
+
var primary = custom ? plainLabel(text) : target.owner + "/" + target.repo;
|
|
70
|
+
var meta = custom ? target.owner + "/" + target.repo + (target.kind === "repository" ? "" : " · " + target.label) : (target.kind === "repository" ? "" : target.label);
|
|
71
|
+
return iconHtml(target.icon, "github-link-icon") + '<span class="github-link-main">' + escapeHtml(primary) + '</span>' +
|
|
72
|
+
(meta && primary !== target.label ? '<span class="github-link-kind">' + escapeHtml(meta) + '</span>' : "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function renderHostedLink(href, title, text, provider) {
|
|
76
|
+
var target = parseTarget(href, provider);
|
|
77
|
+
if (!target) return null;
|
|
78
|
+
return '<a class="' + provider.cssClass + ' hosted-link-chip" href="' + escapeHtml(target.url.href) + '" target="_blank" rel="noopener noreferrer"' +
|
|
79
|
+
(title ? ' title="' + escapeHtml(title) + '"' : ' title="Open ' + escapeHtml(accessibleLabel(target, text)) + ' on ' + provider.displayName + '"') +
|
|
80
|
+
' aria-label="Open ' + escapeHtml(accessibleLabel(target, text)) + ' on ' + provider.displayName + '">' + chipMarkup(target, text) + '</a>';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function githubLinkTarget(href) {
|
|
84
|
+
var target = parseTarget(href, GITHUB);
|
|
85
|
+
if (!target) return null;
|
|
86
|
+
return { owner: target.owner, repo: target.repo, kind: target.kind, number: target.number || null, href: target.url.href, label: target.label };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function gitlabLinkTarget(href) {
|
|
90
|
+
var target = parseTarget(href, GITLAB);
|
|
91
|
+
if (!target) return null;
|
|
92
|
+
return { owner: target.owner, repo: target.repo, kind: target.kind, number: target.number || null, href: target.url.href, label: target.label };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function renderGithubLink(href, title, text) { return renderHostedLink(href, title, text, GITHUB); }
|
|
96
|
+
export function renderGitlabLink(href, title, text) { return renderHostedLink(href, title, text, GITLAB); }
|
|
97
|
+
|
|
98
|
+
export function enhanceGithubLinks(root) {
|
|
99
|
+
if (!root || typeof document === "undefined") return;
|
|
100
|
+
var anchors = root.querySelectorAll("a[href]");
|
|
101
|
+
for (var i = 0; i < anchors.length; i++) {
|
|
102
|
+
var anchor = anchors[i];
|
|
103
|
+
if (anchor.closest("code, pre")) continue;
|
|
104
|
+
var target = parseTarget(anchor.getAttribute("href"), anchor.classList.contains("gitlab-link-chip") ? GITLAB : GITHUB);
|
|
105
|
+
if (!target) { target = parseTarget(anchor.getAttribute("href"), GITLAB); if (!target) continue; }
|
|
106
|
+
var text = anchor.textContent || "";
|
|
107
|
+
if (anchor.classList.contains("github-link-chip") || anchor.classList.contains("gitlab-link-chip")) {
|
|
108
|
+
anchor.setAttribute("target", "_blank"); anchor.setAttribute("rel", "noopener noreferrer"); continue;
|
|
109
|
+
}
|
|
110
|
+
anchor.classList.add("hosted-link-chip", target.cssClass); anchor.setAttribute("target", "_blank"); anchor.setAttribute("rel", "noopener noreferrer");
|
|
111
|
+
anchor.setAttribute("aria-label", "Open " + accessibleLabel(target, text) + " on " + (target.provider === "gitlab" ? "GitLab" : "GitHub"));
|
|
112
|
+
anchor.innerHTML = chipMarkup(target, text);
|
|
113
|
+
}
|
|
114
|
+
refreshIcons();
|
|
115
|
+
}
|
|
@@ -4,6 +4,7 @@ import { getMermaidThemeVars } from './theme.js';
|
|
|
4
4
|
import { store } from './store.js';
|
|
5
5
|
import { enhanceClayLogLinks, renderClayRecordLink } from './clay-log-links.js';
|
|
6
6
|
import { renderLocalFileLink, enhanceClayFileLinks } from './clay-file-links.js';
|
|
7
|
+
import { renderGithubLink, renderGitlabLink } from './github-links.js';
|
|
7
8
|
|
|
8
9
|
// Initialize markdown parser
|
|
9
10
|
marked.use({ gfm: true, breaks: false });
|
|
@@ -11,11 +12,17 @@ marked.use({ gfm: true, breaks: false });
|
|
|
11
12
|
// Open external links in a new tab
|
|
12
13
|
marked.use({
|
|
13
14
|
renderer: {
|
|
14
|
-
link({ href, title, text }) {
|
|
15
|
+
link({ href, title, tokens, text }) {
|
|
15
16
|
var recordLink = renderClayRecordLink(href, text);
|
|
16
17
|
if (recordLink) return recordLink;
|
|
17
18
|
var localLink = renderLocalFileLink(href, title, text);
|
|
18
19
|
if (localLink) return localLink;
|
|
20
|
+
var githubText = text;
|
|
21
|
+
if (tokens && this.parser && this.parser.parseInline) githubText = this.parser.parseInline(tokens);
|
|
22
|
+
var githubLink = renderGithubLink(href, title, githubText);
|
|
23
|
+
if (githubLink) return githubLink;
|
|
24
|
+
var gitlabLink = renderGitlabLink(href, title, githubText);
|
|
25
|
+
if (gitlabLink) return gitlabLink;
|
|
19
26
|
var isExternal = href && (href.startsWith('http://') || href.startsWith('https://'));
|
|
20
27
|
var titleAttr = title ? ' title="' + title + '"' : '';
|
|
21
28
|
var targetAttr = isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
|
|
@@ -56,7 +63,7 @@ export function renderMarkdown(text) {
|
|
|
56
63
|
.replace(/[\u201C\u201D]/g, '"')
|
|
57
64
|
.replace(/[\u2018\u2019]/g, "'")
|
|
58
65
|
.replace(/\*\*[ \t]+([^*\n]+?)[ \t]+\*\*/g, "**$1**"));
|
|
59
|
-
return DOMPurify.sanitize(marked.parse(normalized));
|
|
66
|
+
return DOMPurify.sanitize(marked.parse(normalized), { ADD_ATTR: ["target", "rel"] });
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
export function enhanceClaySessionLinks(root) {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { getWs } from './ws-ref.js';
|
|
5
5
|
import { refreshIcons } from './icons.js';
|
|
6
6
|
import { setHttpMcpServers } from './app-misc.js';
|
|
7
|
+
import { store } from './store.js';
|
|
8
|
+
import { showToast } from './utils.js';
|
|
7
9
|
|
|
8
10
|
var modal = null;
|
|
9
11
|
var contentEl = null;
|
|
@@ -11,6 +13,60 @@ var _mcpServers = []; // { name, transport, toolCount, extensionEnabled, project
|
|
|
11
13
|
var _extensionConnected = false;
|
|
12
14
|
var _nativeHostConnected = false;
|
|
13
15
|
var _extensionId = null;
|
|
16
|
+
var MCP_PERMISSION_TIMEOUT_MS = 10000;
|
|
17
|
+
var _permissionModeTimers = {};
|
|
18
|
+
|
|
19
|
+
function pendingKey(projectSlug, sessionId, requestId) {
|
|
20
|
+
return String(projectSlug) + ":" + String(sessionId) + ":" + String(requestId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pendingMap() {
|
|
24
|
+
return store.get('mcpPermissionModePendingByTarget') || {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setPendingMap(next) {
|
|
28
|
+
store.set({ mcpPermissionModePendingByTarget: next });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function findPending(requestId) {
|
|
32
|
+
var pending = pendingMap();
|
|
33
|
+
var keys = Object.keys(pending);
|
|
34
|
+
for (var i = 0; i < keys.length; i++) {
|
|
35
|
+
if (pending[keys[i]].requestId === requestId) return { key: keys[i], value: pending[keys[i]] };
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function clearPendingRequest(requestId) {
|
|
41
|
+
var found = findPending(requestId);
|
|
42
|
+
if (!found) return null;
|
|
43
|
+
if (_permissionModeTimers[requestId]) clearTimeout(_permissionModeTimers[requestId]);
|
|
44
|
+
delete _permissionModeTimers[requestId];
|
|
45
|
+
var next = Object.assign({}, pendingMap());
|
|
46
|
+
delete next[found.key];
|
|
47
|
+
setPendingMap(next);
|
|
48
|
+
return found.value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function clearMcpPermissionModePending() {
|
|
52
|
+
var requestIds = Object.keys(_permissionModeTimers);
|
|
53
|
+
for (var i = 0; i < requestIds.length; i++) clearTimeout(_permissionModeTimers[requestIds[i]]);
|
|
54
|
+
_permissionModeTimers = {};
|
|
55
|
+
if (Object.keys(pendingMap()).length) setPendingMap({});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function mcpPermissionModeApplicability(state) {
|
|
59
|
+
if (!state.permissionCapabilities || state.permissionCapabilities.mcpOverride !== true) {
|
|
60
|
+
return { applicable: false, reason: "AI review is unavailable for this SDK session." };
|
|
61
|
+
}
|
|
62
|
+
if (state.effectivePermissionMode === "auto" || state.effectivePermissionMode === "bypassPermissions") {
|
|
63
|
+
return { applicable: true, reason: "" };
|
|
64
|
+
}
|
|
65
|
+
if (!state.effectivePermissionMode && state.currentMode === "auto") {
|
|
66
|
+
return { applicable: false, reason: "Waiting for the SDK to confirm Auto." };
|
|
67
|
+
}
|
|
68
|
+
return { applicable: false, reason: "AI review requires an active Auto session." };
|
|
69
|
+
}
|
|
14
70
|
|
|
15
71
|
export function initMcp() {
|
|
16
72
|
modal = document.getElementById("mcp-modal");
|
|
@@ -26,6 +82,14 @@ export function initMcp() {
|
|
|
26
82
|
if (closeBtn) closeBtn.addEventListener("click", closeMcpModal);
|
|
27
83
|
if (backdrop) backdrop.addEventListener("click", closeMcpModal);
|
|
28
84
|
|
|
85
|
+
store.subscribe(function (state, prev) {
|
|
86
|
+
var changed = state.activeSessionId !== prev.activeSessionId || state.currentSlug !== prev.currentSlug ||
|
|
87
|
+
state.currentMode !== prev.currentMode || state.effectivePermissionMode !== prev.effectivePermissionMode ||
|
|
88
|
+
state.permissionCapabilities !== prev.permissionCapabilities || state.mcpPermissionModeOverrides !== prev.mcpPermissionModeOverrides ||
|
|
89
|
+
state.mcpPermissionModePendingByTarget !== prev.mcpPermissionModePendingByTarget || state.connected !== prev.connected;
|
|
90
|
+
if (changed && modal && !modal.classList.contains("hidden")) renderMcpServerList();
|
|
91
|
+
});
|
|
92
|
+
|
|
29
93
|
// ESC to close
|
|
30
94
|
document.addEventListener("keydown", function (e) {
|
|
31
95
|
if (e.key === "Escape" && modal && !modal.classList.contains("hidden")) {
|
|
@@ -140,21 +204,47 @@ function renderMcpServerList() {
|
|
|
140
204
|
|
|
141
205
|
// Per-server permission mode override (applies to the active session only).
|
|
142
206
|
if (server.projectEnabled) {
|
|
207
|
+
var permissionState = store.snap();
|
|
208
|
+
var applicability = mcpPermissionModeApplicability(permissionState);
|
|
209
|
+
if (!applicability.applicable) {
|
|
210
|
+
var unavailable = document.createElement("span");
|
|
211
|
+
unavailable.className = "mcp-server-perm-reason";
|
|
212
|
+
unavailable.textContent = applicability.reason;
|
|
213
|
+
row.appendChild(unavailable);
|
|
214
|
+
contentEl.appendChild(row);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
143
217
|
var modeSel = document.createElement("select");
|
|
144
218
|
modeSel.className = "mcp-server-perm-mode";
|
|
145
219
|
modeSel.dataset.serverName = server.name;
|
|
146
|
-
modeSel.title = "
|
|
220
|
+
modeSel.title = "AI review policy for this server (this session only)";
|
|
221
|
+
var optInherit = document.createElement("option");
|
|
222
|
+
optInherit.value = "";
|
|
223
|
+
optInherit.textContent = "Inherit";
|
|
147
224
|
var optAsk = document.createElement("option");
|
|
148
225
|
optAsk.value = "default";
|
|
149
226
|
optAsk.textContent = "Ask";
|
|
150
227
|
var optAuto = document.createElement("option");
|
|
151
228
|
optAuto.value = "auto";
|
|
152
|
-
optAuto.textContent = "
|
|
229
|
+
optAuto.textContent = "AI review";
|
|
230
|
+
modeSel.appendChild(optInherit);
|
|
153
231
|
modeSel.appendChild(optAsk);
|
|
154
232
|
modeSel.appendChild(optAuto);
|
|
155
|
-
|
|
233
|
+
var overrides = store.get('mcpPermissionModeOverrides') || {};
|
|
234
|
+
modeSel.value = overrides[server.name] === "auto" || overrides[server.name] === "default" ? overrides[server.name] : "";
|
|
235
|
+
var pending = pendingMap();
|
|
236
|
+
var pendingKeys = Object.keys(pending);
|
|
237
|
+
for (var p = 0; p < pendingKeys.length; p++) {
|
|
238
|
+
var pendingValue = pending[pendingKeys[p]];
|
|
239
|
+
if (pendingValue.projectSlug === permissionState.currentSlug && pendingValue.sessionId === permissionState.activeSessionId && pendingValue.serverName === server.name) {
|
|
240
|
+
modeSel.value = pendingValue.mode;
|
|
241
|
+
modeSel.disabled = true;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!permissionState.connected) modeSel.disabled = true;
|
|
156
246
|
// The row is a <label>; keep select interactions from toggling the checkbox.
|
|
157
|
-
modeSel.addEventListener("click", function (e) { e.stopPropagation();
|
|
247
|
+
modeSel.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
158
248
|
modeSel.addEventListener("change", onPermissionModeChange);
|
|
159
249
|
row.appendChild(modeSel);
|
|
160
250
|
}
|
|
@@ -318,22 +408,69 @@ function onToggle(e) {
|
|
|
318
408
|
|
|
319
409
|
function onPermissionModeChange(e) {
|
|
320
410
|
var name = e.target.dataset.serverName;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// Remember locally so a re-render keeps the selection.
|
|
324
|
-
for (var i = 0; i < _mcpServers.length; i++) {
|
|
325
|
-
if (_mcpServers[i].name === name) {
|
|
326
|
-
_mcpServers[i].permissionModeOverride = mode;
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
411
|
+
sendMcpPermissionMode(store.get('currentSlug'), store.get('activeSessionId'), name, e.target.value);
|
|
412
|
+
}
|
|
330
413
|
|
|
414
|
+
export function sendMcpPermissionMode(projectSlug, sessionId, serverName, mode) {
|
|
415
|
+
var state = store.snap();
|
|
416
|
+
var applicability = mcpPermissionModeApplicability(state);
|
|
331
417
|
var ws = getWs();
|
|
332
|
-
if (ws
|
|
333
|
-
|
|
418
|
+
if (!state.connected || state.currentSlug !== projectSlug || state.activeSessionId !== sessionId || !applicability.applicable || !ws || ws.readyState !== 1) {
|
|
419
|
+
showToast(applicability.applicable ? "Reconnect before changing AI review policy." : applicability.reason, "error");
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
var pending = pendingMap();
|
|
423
|
+
var pendingKeys = Object.keys(pending);
|
|
424
|
+
for (var i = 0; i < pendingKeys.length; i++) {
|
|
425
|
+
var item = pending[pendingKeys[i]];
|
|
426
|
+
if (item.projectSlug === projectSlug && item.sessionId === sessionId && item.serverName === serverName) return false;
|
|
427
|
+
}
|
|
428
|
+
var requestId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : String(Date.now());
|
|
429
|
+
var key = pendingKey(projectSlug, sessionId, requestId);
|
|
430
|
+
var overrides = state.mcpPermissionModeOverrides || {};
|
|
431
|
+
var next = Object.assign({}, pending);
|
|
432
|
+
next[key] = {
|
|
433
|
+
projectSlug: projectSlug,
|
|
434
|
+
sessionId: sessionId,
|
|
435
|
+
requestId: requestId,
|
|
436
|
+
serverName: serverName,
|
|
437
|
+
mode: mode || "",
|
|
438
|
+
previousMode: overrides[serverName] || ""
|
|
439
|
+
};
|
|
440
|
+
setPendingMap(next);
|
|
441
|
+
_permissionModeTimers[requestId] = setTimeout(function () {
|
|
442
|
+
if (!clearPendingRequest(requestId)) return;
|
|
443
|
+
showToast("Changing AI review policy timed out.", "error");
|
|
444
|
+
}, MCP_PERMISSION_TIMEOUT_MS);
|
|
445
|
+
try {
|
|
446
|
+
var result = ws.send(JSON.stringify({
|
|
334
447
|
type: "set_mcp_permission_mode_override",
|
|
335
|
-
|
|
336
|
-
|
|
448
|
+
projectSlug: projectSlug,
|
|
449
|
+
sessionId: sessionId,
|
|
450
|
+
requestId: requestId,
|
|
451
|
+
serverName: serverName,
|
|
452
|
+
mode: mode || null,
|
|
337
453
|
}));
|
|
454
|
+
if (result === false) throw new Error("WebSocket send failed");
|
|
455
|
+
} catch (error) {
|
|
456
|
+
clearPendingRequest(requestId);
|
|
457
|
+
showToast("Failed to send the AI review policy change.", "error");
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function handleMcpPermissionModeResult(msg) {
|
|
464
|
+
var found = findPending(msg.requestId);
|
|
465
|
+
if (!found) return;
|
|
466
|
+
var pending = found.value;
|
|
467
|
+
if (msg.projectSlug && msg.projectSlug !== pending.projectSlug) return;
|
|
468
|
+
if (msg.sessionId !== pending.sessionId || msg.serverName !== pending.serverName) return;
|
|
469
|
+
clearPendingRequest(msg.requestId);
|
|
470
|
+
if (pending.projectSlug !== store.get('currentSlug') || pending.sessionId !== store.get('activeSessionId')) return;
|
|
471
|
+
if (!msg.ok) {
|
|
472
|
+
showToast(msg.error || "Failed to change AI review policy.", "error");
|
|
473
|
+
return;
|
|
338
474
|
}
|
|
475
|
+
store.set({ mcpPermissionModeOverrides: msg.mcpPermissionModeOverrides || {} });
|
|
339
476
|
}
|