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.
Files changed (47) hide show
  1. package/lib/daemon-project-access.js +14 -0
  2. package/lib/daemon.js +7 -0
  3. package/lib/project-connection.js +5 -2
  4. package/lib/project-full-access.js +69 -56
  5. package/lib/project-issue-launch.js +3 -1
  6. package/lib/project-sessions.js +158 -32
  7. package/lib/project-user-message.js +5 -2
  8. package/lib/project.js +5 -0
  9. package/lib/public/app.js +7 -0
  10. package/lib/public/css/command-palette.css +51 -33
  11. package/lib/public/css/filebrowser.css +20 -0
  12. package/lib/public/css/issues.css +4 -4
  13. package/lib/public/css/messages.css +27 -0
  14. package/lib/public/css/overlays.css +60 -0
  15. package/lib/public/css/pane.css +21 -4
  16. package/lib/public/css/pending-message-queue.css +5 -0
  17. package/lib/public/index.html +18 -3
  18. package/lib/public/modules/app-connection.js +4 -0
  19. package/lib/public/modules/app-header.js +18 -25
  20. package/lib/public/modules/app-messages.js +24 -7
  21. package/lib/public/modules/app-panels.js +22 -11
  22. package/lib/public/modules/clay-file-links.js +7 -0
  23. package/lib/public/modules/clay-issue-links.js +1 -0
  24. package/lib/public/modules/clay-log-links.js +5 -0
  25. package/lib/public/modules/github-links.js +115 -0
  26. package/lib/public/modules/markdown.js +9 -2
  27. package/lib/public/modules/mcp-ui.js +154 -17
  28. package/lib/public/modules/pane-bridge.js +2 -0
  29. package/lib/public/modules/pane-file-bridge.js +69 -0
  30. package/lib/public/modules/pending-message-queue-model.js +6 -0
  31. package/lib/public/modules/pending-message-queue-recovery.js +73 -0
  32. package/lib/public/modules/pending-message-queue.js +67 -64
  33. package/lib/public/modules/permission-control.js +187 -0
  34. package/lib/public/modules/project-session-visibility-settings.js +138 -0
  35. package/lib/public/modules/project-settings.js +5 -0
  36. package/lib/public/modules/sidebar-projects.js +14 -3
  37. package/lib/public/modules/split-view.js +29 -29
  38. package/lib/sdk-bridge.js +95 -40
  39. package/lib/server.js +8 -1
  40. package/lib/session-permission-mode.js +42 -0
  41. package/lib/session-visibility.js +62 -1
  42. package/lib/sessions.js +16 -1
  43. package/lib/ws-schema.js +4 -0
  44. package/lib/yoke/adapters/claude-worker-policy.js +67 -0
  45. package/lib/yoke/adapters/claude-worker.js +47 -7
  46. package/lib/yoke/adapters/claude.js +114 -6
  47. 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(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/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 = "Permission mode for this server (this session only)";
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 = "Auto-approve";
229
+ optAuto.textContent = "AI review";
230
+ modeSel.appendChild(optInherit);
153
231
  modeSel.appendChild(optAsk);
154
232
  modeSel.appendChild(optAuto);
155
- modeSel.value = server.permissionModeOverride === "auto" ? "auto" : "default";
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(); e.preventDefault(); });
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
- var mode = e.target.value; // "default" | "auto"
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 && ws.readyState === 1) {
333
- ws.send(JSON.stringify({
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
- serverName: name,
336
- mode: mode,
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
+ }