clay-server 4.1.0 → 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/daemon-project-access.js +14 -0
- package/lib/daemon.js +7 -0
- package/lib/project-connection.js +5 -2
- package/lib/project-full-access.js +69 -56
- package/lib/project-issue-launch.js +3 -1
- package/lib/project-sessions.js +158 -32
- package/lib/project-user-message.js +5 -2
- package/lib/project.js +5 -0
- package/lib/public/app.js +7 -0
- package/lib/public/css/command-palette.css +51 -33
- package/lib/public/css/filebrowser.css +20 -0
- package/lib/public/css/issues.css +4 -4
- 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 +18 -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 +24 -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-issue-links.js +1 -0
- package/lib/public/modules/clay-log-links.js +5 -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/project-session-visibility-settings.js +138 -0
- package/lib/public/modules/project-settings.js +5 -0
- package/lib/public/modules/sidebar-projects.js +14 -3
- package/lib/public/modules/split-view.js +29 -29
- package/lib/sdk-bridge.js +95 -40
- package/lib/server.js +8 -1
- package/lib/session-permission-mode.js +42 -0
- package/lib/session-visibility.js +62 -1
- package/lib/sessions.js +16 -1
- package/lib/ws-schema.js +4 -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
|
@@ -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;
|
|
@@ -28,6 +29,7 @@ function createLogLink(ref, labelText) {
|
|
|
28
29
|
var button = document.createElement("button");
|
|
29
30
|
button.type = "button";
|
|
30
31
|
button.className = "clayos-log-link";
|
|
32
|
+
button.classList.add("clayos-record-link");
|
|
31
33
|
button.contentEditable = "false";
|
|
32
34
|
button.dataset.logRef = ref;
|
|
33
35
|
button.setAttribute("aria-label", "Open Project Log" + (labelText ? " " + labelText : ""));
|
|
@@ -79,4 +81,7 @@ export function enhanceClayLogLinks(root) {
|
|
|
79
81
|
fragment.appendChild(document.createTextNode(text.slice(offset)));
|
|
80
82
|
nodes[i].parentNode.replaceChild(fragment, nodes[i]);
|
|
81
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);
|
|
82
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
|
}
|
|
@@ -42,6 +42,8 @@ export function forwardPaneIssueReference(ref, projectSlug) {
|
|
|
42
42
|
return true;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export { forwardPaneFileReference } from './pane-file-bridge.js';
|
|
46
|
+
|
|
45
47
|
// Toggle without setContextView: panes use a transient override and must not
|
|
46
48
|
// rewrite the user's server-persisted main-view preference.
|
|
47
49
|
function togglePaneContextPanel() {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
|
|
3
|
+
function paneFrame(host, source) {
|
|
4
|
+
if (!host || !source) return null;
|
|
5
|
+
var frames = host.querySelectorAll(".split-pane-frame");
|
|
6
|
+
for (var i = 0; i < frames.length; i++) {
|
|
7
|
+
if (frames[i].contentWindow === source) return frames[i];
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function matchingPane(frame, split) {
|
|
13
|
+
if (!frame || !split || !split.panes) return null;
|
|
14
|
+
for (var i = 0; i < split.panes.length; i++) {
|
|
15
|
+
var pane = split.panes[i];
|
|
16
|
+
if (String(pane.slug) === String(frame.dataset.projectSlug) && String(pane.sessionId) === String(frame.dataset.sessionId)) return pane;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasLivePaneSource(frame, pane) {
|
|
22
|
+
if (!frame || !pane || typeof frame.src !== "string") return false;
|
|
23
|
+
var expected = "/p/" + encodeURIComponent(pane.slug) + "/?pane=1&session=" + encodeURIComponent(pane.sessionId);
|
|
24
|
+
try { return new URL(expected, window.location.href).href === frame.src; } catch (e) { return false; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validLocation(value) {
|
|
28
|
+
return value == null || (Number.isSafeInteger(value) && value > 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function revealFilesPanel() {
|
|
32
|
+
var filesPanel = document.getElementById("sidebar-panel-files");
|
|
33
|
+
if (!filesPanel || !filesPanel.classList.contains("hidden")) return false;
|
|
34
|
+
var filesButton = document.getElementById("file-browser-btn");
|
|
35
|
+
if (!filesButton) return false;
|
|
36
|
+
filesButton.click();
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function forwardPaneFileReference(path, opts) {
|
|
41
|
+
if (!store.get('paneMode') || window.parent === window || typeof path !== "string" || !path) return false;
|
|
42
|
+
window.parent.postMessage({
|
|
43
|
+
type: "clay-pane-open-file",
|
|
44
|
+
path: path,
|
|
45
|
+
line: opts && opts.line != null ? opts.line : null,
|
|
46
|
+
column: opts && opts.column != null ? opts.column : null,
|
|
47
|
+
projectSlug: opts && opts.projectSlug ? opts.projectSlug : store.get('currentSlug'),
|
|
48
|
+
sessionId: opts && opts.sessionId != null ? String(opts.sessionId) : (store.get('activeSessionId') != null ? String(store.get('activeSessionId')) : null),
|
|
49
|
+
}, window.location.origin);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function handlePaneFileMessage(event, host, openFile, openFiles) {
|
|
54
|
+
if (!event || event.origin !== window.location.origin || !host) return false;
|
|
55
|
+
var msg = event.data;
|
|
56
|
+
if (!msg || msg.type !== "clay-pane-open-file") return false;
|
|
57
|
+
var frame = paneFrame(host, event.source);
|
|
58
|
+
var state = store.snap();
|
|
59
|
+
var pane = matchingPane(frame, state.splitPanes);
|
|
60
|
+
if (!pane || !hasLivePaneSource(frame, pane) || String(pane.slug) !== String(state.currentSlug) || msg.projectSlug !== frame.dataset.projectSlug || String(msg.sessionId) !== String(frame.dataset.sessionId) || !validLocation(msg.line) || !validLocation(msg.column)) return true;
|
|
61
|
+
if (typeof msg.path !== "string" || !msg.path) return true;
|
|
62
|
+
if (!state.connected || typeof openFile !== "function") return true;
|
|
63
|
+
// The frame/session association was validated above. The host browser owns
|
|
64
|
+
// the read, so do not apply the pane session as a host-session restriction;
|
|
65
|
+
// the normal authenticated project filesystem checks remain authoritative.
|
|
66
|
+
var result = openFile(msg.path, { line: msg.line, column: msg.column, projectSlug: msg.projectSlug });
|
|
67
|
+
if (result !== false && typeof openFiles === "function") openFiles();
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
@@ -2,6 +2,12 @@ export function activeQueueItems(items) {
|
|
|
2
2
|
return (items || []).filter(function (item) { return item.state === "pending" || item.state === "claimed"; });
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
export function hasFutureScheduledPending(items) {
|
|
6
|
+
items = items || [];
|
|
7
|
+
for (var i = 0; i < items.length; i++) if (items[i].state === "pending" && items[i].schedule && items[i].schedule.ready !== true) return true;
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
export function moveQueueId(ids, id, direction, targetId) {
|
|
6
12
|
var next = (ids || []).slice();
|
|
7
13
|
var from = next.indexOf(id);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Queue recovery keeps an authorized queue scope distinct from WebSocket transport.
|
|
2
|
+
|
|
3
|
+
function sameId(a, b) { return String(a) === String(b); }
|
|
4
|
+
|
|
5
|
+
export function emptyQueue() { return { projectSlug: null, sessionId: null, revision: 0, paused: false, items: [], loading: false, error: "" }; }
|
|
6
|
+
|
|
7
|
+
export function queueContext(state) {
|
|
8
|
+
state = state || {};
|
|
9
|
+
var available = !!state.currentSlug && !!state.activeSessionId && state.activeSessionMode !== "tui" && !state.dmMode && !state.mateProjectSlug;
|
|
10
|
+
return {
|
|
11
|
+
connected: !!state.connected,
|
|
12
|
+
projectSlug: state.currentSlug || null,
|
|
13
|
+
sessionId: state.activeSessionId || null,
|
|
14
|
+
accountId: state.myUserId || "default",
|
|
15
|
+
available: available,
|
|
16
|
+
enabled: available && !!state.connected,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sameQueueContext(a, b) {
|
|
21
|
+
return !!a && !!b && a.projectSlug === b.projectSlug && sameId(a.sessionId, b.sessionId) && a.accountId === b.accountId;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function queueScopeChanged(state, previous) {
|
|
25
|
+
var next = queueContext(state);
|
|
26
|
+
var prior = queueContext(previous);
|
|
27
|
+
return !sameQueueContext(next, prior) || next.available !== prior.available || !!state.isMultiUserMode !== !!previous.isMultiUserMode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function queueReconnected(state, previous) {
|
|
31
|
+
return !!state.connected && !previous.connected && !queueScopeChanged(state, previous);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function queueCanRender(queue, target) {
|
|
35
|
+
return !!target && target.available && !!queue && queue.projectSlug === target.projectSlug && sameId(queue.sessionId, target.sessionId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createQueueHydrationRecovery() {
|
|
39
|
+
var timer = null;
|
|
40
|
+
var requestId = null;
|
|
41
|
+
function clear(id) {
|
|
42
|
+
if (id && requestId !== id) return;
|
|
43
|
+
if (timer) clearTimeout(timer);
|
|
44
|
+
timer = null;
|
|
45
|
+
requestId = null;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
schedule: function (id, retry) {
|
|
49
|
+
clear();
|
|
50
|
+
requestId = id;
|
|
51
|
+
timer = setTimeout(function () {
|
|
52
|
+
timer = null;
|
|
53
|
+
requestId = null;
|
|
54
|
+
retry();
|
|
55
|
+
}, 10000);
|
|
56
|
+
if (timer && timer.unref) timer.unref();
|
|
57
|
+
},
|
|
58
|
+
clear: clear,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function canonicalQueue(queue, result) {
|
|
63
|
+
if (queue && queue.projectSlug === result.projectSlug && sameId(queue.sessionId, result.sessionId) && Number(result.revision) < Number(queue.revision)) return null;
|
|
64
|
+
return {
|
|
65
|
+
projectSlug: result.projectSlug,
|
|
66
|
+
sessionId: result.sessionId,
|
|
67
|
+
revision: Number(result.revision) || 0,
|
|
68
|
+
paused: result.paused === true,
|
|
69
|
+
items: Array.isArray(result.items) ? result.items : queue.items || [],
|
|
70
|
+
loading: false,
|
|
71
|
+
error: "",
|
|
72
|
+
};
|
|
73
|
+
}
|