clay-server 4.8.0-beta.1 → 4.8.0-beta.3
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/bundled-skills/clay-agent-browser/SKILL.md +36 -0
- package/lib/clay-skill-install.js +33 -0
- package/lib/clay-skill-storage.js +49 -0
- package/lib/linear-api.js +62 -0
- package/lib/linear-store.js +52 -0
- package/lib/project-http.js +14 -5
- package/lib/project-linear-launch.js +70 -0
- package/lib/project-session-linear.js +197 -0
- package/lib/project-shared-browser.js +256 -0
- package/lib/project.js +74 -2
- package/lib/public/app.js +7 -0
- package/lib/public/css/filebrowser.css +2 -25
- package/lib/public/css/issues.css +1 -14
- package/lib/public/css/linear.css +35 -0
- package/lib/public/css/mcp-skills-workbench.css +51 -0
- package/lib/public/css/project-logs.css +0 -11
- package/lib/public/css/right-workbench.css +28 -0
- package/lib/public/css/scheduled-tasks.css +1 -1
- package/lib/public/css/settings-ui.css +295 -0
- package/lib/public/css/shared-browser-skins.css +28 -0
- package/lib/public/css/shared-browser.css +91 -0
- package/lib/public/css/sticky-notes.css +0 -11
- package/lib/public/index.html +50 -22
- package/lib/public/modules/app-messages.js +6 -0
- package/lib/public/modules/app-projects.js +1 -1
- package/lib/public/modules/extension-settings.js +110 -0
- package/lib/public/modules/issues.js +4 -1
- package/lib/public/modules/linear-panel.js +120 -0
- package/lib/public/modules/linear-settings.js +61 -0
- package/lib/public/modules/mcp-skills-workbench.js +67 -0
- package/lib/public/modules/mcp-ui.js +9 -12
- package/lib/public/modules/notifications.js +2 -107
- package/lib/public/modules/project-settings.js +19 -1
- package/lib/public/modules/session-github.js +15 -5
- package/lib/public/modules/shared-browser-activity.js +66 -0
- package/lib/public/modules/shared-browser-input.js +73 -0
- package/lib/public/modules/shared-browser-tabs.js +33 -0
- package/lib/public/modules/shared-browser-viewport-menu.js +76 -0
- package/lib/public/modules/shared-browser.js +214 -0
- package/lib/public/modules/sidebar-mobile.js +3 -3
- package/lib/public/modules/skills-actions.js +247 -0
- package/lib/public/modules/skills-detail.js +148 -0
- package/lib/public/modules/skills-state.js +10 -0
- package/lib/public/modules/skills.js +64 -459
- package/lib/public/modules/sticky-notes.js +23 -6
- package/lib/public/modules/tool-palette-order.js +11 -13
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/style.css +8 -0
- package/lib/sdk-bridge.js +22 -1
- package/lib/server-linear.js +50 -0
- package/lib/server.js +5 -0
- package/lib/sessions.js +7 -0
- package/lib/shared-browser-activity-history.js +17 -0
- package/lib/shared-browser-policy.js +26 -0
- package/lib/shared-browser-runtime.js +82 -0
- package/lib/shared-browser-tools.js +21 -0
- package/lib/shared-browser-worker.js +160 -0
- package/lib/ws-schema.js +10 -0
- package/lib/yoke/skill-discovery.js +22 -2
- package/package.json +1 -1
package/lib/public/index.html
CHANGED
|
@@ -57,28 +57,6 @@
|
|
|
57
57
|
<div id="top-bar">
|
|
58
58
|
<div class="top-bar-left-pills">
|
|
59
59
|
<button id="pwa-install-pill" class="top-bar-install-btn hidden" title="Open as app"><i data-lucide="download"></i> Open as app</button>
|
|
60
|
-
<div id="ext-pill-wrap" class="ext-pill-wrap">
|
|
61
|
-
<button id="ext-pill" class="top-bar-ext-btn hidden" title="Chrome Extension"><i data-lucide="puzzle"></i> Extension</button>
|
|
62
|
-
<div id="ext-popover" class="ext-popover">
|
|
63
|
-
<div class="ext-popover-header">
|
|
64
|
-
<div class="ext-popover-title">Clay for Chrome <span class="ext-experimental">Experimental</span></div>
|
|
65
|
-
<div class="ext-popover-sub">v0.1.0 · <a href="https://github.com/chadbyte/clay-chrome" target="_blank" rel="noopener">GitHub</a></div>
|
|
66
|
-
</div>
|
|
67
|
-
<div class="ext-popover-connected hidden" id="ext-connected-banner"><i data-lucide="check-circle"></i> Extension connected</div>
|
|
68
|
-
<div class="ext-popover-desc" id="ext-popover-desc">Connect your browser to Clay. Gives Claude visibility into open tabs, console, network, DOM, and screenshots.</div>
|
|
69
|
-
<button class="ext-popover-download" id="ext-download-btn"><i data-lucide="download"></i> Download Extension (.zip)</button>
|
|
70
|
-
<div class="ext-popover-status hidden" id="ext-download-status"></div>
|
|
71
|
-
<div class="ext-popover-divider" id="ext-popover-divider"></div>
|
|
72
|
-
<div class="ext-popover-guide-title" id="ext-popover-guide-title">Installation</div>
|
|
73
|
-
<div class="ext-popover-steps">
|
|
74
|
-
<div class="ext-popover-step"><span class="ext-snum">1</span> Download above, then unzip to a permanent folder.</div>
|
|
75
|
-
<div class="ext-popover-step"><span class="ext-snum">2</span> Open <code id="ext-copy-url" class="ext-popover-code" title="Click to copy">chrome://extensions</code></div>
|
|
76
|
-
<div class="ext-popover-step"><span class="ext-snum">3</span> Enable <strong>Developer mode</strong> (top-right toggle).</div>
|
|
77
|
-
<div class="ext-popover-step"><span class="ext-snum">4</span> <strong>Load unpacked</strong> > select the unzipped folder.</div>
|
|
78
|
-
<div class="ext-popover-step"><span class="ext-snum">5</span> Refresh this page. Done!</div>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
60
|
<div id="update-pill-wrap" class="top-bar-update hidden">
|
|
83
61
|
<button id="update-pill" class="top-bar-update-btn"><i data-lucide="arrow-up-circle"></i> <span id="update-version"></span> is available. Update now</button>
|
|
84
62
|
<div id="update-popover" class="top-bar-popover">
|
|
@@ -1037,6 +1015,7 @@
|
|
|
1037
1015
|
<option value="us-mates">Mates</option>
|
|
1038
1016
|
<option value="us-experimental">Experimental</option>
|
|
1039
1017
|
<option value="us-email">Email</option>
|
|
1018
|
+
<option value="us-integrations">Integrations</option>
|
|
1040
1019
|
</select>
|
|
1041
1020
|
<div class="us-modal-nav-items">
|
|
1042
1021
|
<button class="us-nav-item active" data-section="us-account"><span>Account</span></button>
|
|
@@ -1045,6 +1024,7 @@
|
|
|
1045
1024
|
<button class="us-nav-item" data-section="us-mates"><span>Mates</span></button>
|
|
1046
1025
|
<button class="us-nav-item" data-section="us-experimental"><span>Experimental</span></button>
|
|
1047
1026
|
<button class="us-nav-item" data-section="us-email"><span>Email</span></button>
|
|
1027
|
+
<button class="us-nav-item" data-section="us-integrations"><span>Integrations</span></button>
|
|
1048
1028
|
<div class="us-nav-separator"></div>
|
|
1049
1029
|
<button class="us-nav-item us-nav-danger" data-section="us-logout"><span>Log Out</span></button>
|
|
1050
1030
|
</div>
|
|
@@ -1215,6 +1195,24 @@
|
|
|
1215
1195
|
</div>
|
|
1216
1196
|
</div>
|
|
1217
1197
|
|
|
1198
|
+
<div class="us-section" data-section="us-integrations">
|
|
1199
|
+
<h2>Integrations</h2>
|
|
1200
|
+
<div class="settings-card"><div class="settings-field">
|
|
1201
|
+
<h3>Linear</h3>
|
|
1202
|
+
<p class="settings-hint">Connect your personal Linear API key to find issues and link them to conversations. Clay reads issues and comments; it does not change Linear.</p>
|
|
1203
|
+
<form id="us-linear-form" class="settings-form">
|
|
1204
|
+
<label class="settings-label" for="us-linear-key">Personal API key</label>
|
|
1205
|
+
<input id="us-linear-key" class="us-text-input" type="password" autocomplete="off" maxlength="4096" required placeholder="Linear API key" aria-describedby="us-linear-help us-linear-status">
|
|
1206
|
+
<p id="us-linear-help" class="settings-hint">Create a read-only key in Linear Settings → Security & access. Your key is encrypted and stored server-side for your account.</p>
|
|
1207
|
+
<div class="settings-form-actions">
|
|
1208
|
+
<button type="submit" class="us-btn">Connect</button>
|
|
1209
|
+
<button type="button" class="us-btn us-btn-secondary" id="us-linear-disconnect" disabled>Disconnect</button>
|
|
1210
|
+
</div>
|
|
1211
|
+
<p id="us-linear-status" class="settings-status" role="status" aria-live="polite"></p>
|
|
1212
|
+
</form>
|
|
1213
|
+
</div></div>
|
|
1214
|
+
</div>
|
|
1215
|
+
|
|
1218
1216
|
<!-- Email section -->
|
|
1219
1217
|
<div class="us-section" data-section="us-email">
|
|
1220
1218
|
<h2>Email Accounts</h2>
|
|
@@ -1768,6 +1766,36 @@
|
|
|
1768
1766
|
<span class="mcp-title"><i data-lucide="cable"></i> MCP Servers</span>
|
|
1769
1767
|
<button class="skills-close" id="mcp-modal-close"><i data-lucide="x"></i></button>
|
|
1770
1768
|
</div>
|
|
1769
|
+
<div id="ext-pill-wrap" class="ext-pill-wrap">
|
|
1770
|
+
<button id="ext-pill" class="top-bar-ext-btn" title="Chrome Extension" aria-expanded="false" aria-controls="ext-popover"><i data-lucide="puzzle"></i> Extension</button>
|
|
1771
|
+
<p class="ext-feature-summary">Connect your Chrome tabs and local MCP tools to Clay.</p>
|
|
1772
|
+
<div id="ext-popover" class="ext-popover">
|
|
1773
|
+
<div class="ext-popover-header">
|
|
1774
|
+
<div class="ext-popover-title">Clay for Chrome <span class="ext-experimental">Experimental</span></div>
|
|
1775
|
+
<div class="ext-popover-sub">v0.1.0 · <a href="https://github.com/chadbyte/clay-chrome" target="_blank" rel="noopener">GitHub</a></div>
|
|
1776
|
+
</div>
|
|
1777
|
+
<div class="ext-popover-connected hidden" id="ext-connected-banner"><i data-lucide="check-circle"></i> Extension connected</div>
|
|
1778
|
+
<div class="ext-popover-desc" id="ext-popover-desc">
|
|
1779
|
+
<p>With Clay for Chrome, your agent can:</p>
|
|
1780
|
+
<ul class="ext-feature-list">
|
|
1781
|
+
<li><strong>Work with your open tabs.</strong> Read pages, capture screenshots, click buttons and fill in forms.</li>
|
|
1782
|
+
<li><strong>Help debug a website.</strong> Inspect page structure, console errors and network requests.</li>
|
|
1783
|
+
<li><strong>Use MCP tools on your computer.</strong> Add MCP servers in the extension and connect them through the separately installed Clay MCP Bridge.</li>
|
|
1784
|
+
</ul>
|
|
1785
|
+
</div>
|
|
1786
|
+
<button class="ext-popover-download" id="ext-download-btn"><i data-lucide="download"></i> Download Extension (.zip)</button>
|
|
1787
|
+
<div class="ext-popover-status hidden" id="ext-download-status"></div>
|
|
1788
|
+
<div class="ext-popover-divider" id="ext-popover-divider"></div>
|
|
1789
|
+
<div class="ext-popover-guide-title" id="ext-popover-guide-title">Installation</div>
|
|
1790
|
+
<div class="ext-popover-steps">
|
|
1791
|
+
<div class="ext-popover-step"><span class="ext-snum">1</span> Download above, then unzip to a permanent folder.</div>
|
|
1792
|
+
<div class="ext-popover-step"><span class="ext-snum">2</span> Open <code id="ext-copy-url" class="ext-popover-code" title="Click to copy">chrome://extensions</code></div>
|
|
1793
|
+
<div class="ext-popover-step"><span class="ext-snum">3</span> Enable <strong>Developer mode</strong> (top-right toggle).</div>
|
|
1794
|
+
<div class="ext-popover-step"><span class="ext-snum">4</span> <strong>Load unpacked</strong> > select the unzipped folder.</div>
|
|
1795
|
+
<div class="ext-popover-step"><span class="ext-snum">5</span> Refresh this page. Done!</div>
|
|
1796
|
+
</div>
|
|
1797
|
+
</div>
|
|
1798
|
+
</div>
|
|
1771
1799
|
<div class="mcp-content" id="mcp-content">
|
|
1772
1800
|
<p class="mcp-no-servers">No MCP servers detected. Configure in Clay Chrome Extension.</p>
|
|
1773
1801
|
</div>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { handleLinearResult } from './linear-panel.js';
|
|
2
|
+
import { handleSharedBrowserMessage } from './shared-browser.js';
|
|
1
3
|
import { syncGithubSessions } from './session-github.js';
|
|
2
4
|
import { handleIssuesMessageInContext as handleIssuesMessage } from './issues-context.js';
|
|
3
5
|
// app-messages.js - WebSocket message router
|
|
@@ -104,6 +106,7 @@ var connectOverlay = document.getElementById("connect-overlay");
|
|
|
104
106
|
initMessageDeliveryUi();
|
|
105
107
|
|
|
106
108
|
export function processMessage(msg) {
|
|
109
|
+
if (handleSharedBrowserMessage(msg)) return;
|
|
107
110
|
if (msg.type === "vendor_login_browser_frame" || msg.type === "vendor_login_browser_status") {
|
|
108
111
|
handleLoginBrowserMessage(msg);
|
|
109
112
|
return;
|
|
@@ -814,6 +817,9 @@ export function processMessage(msg) {
|
|
|
814
817
|
handleRemoteSelection(msg);
|
|
815
818
|
break;
|
|
816
819
|
|
|
820
|
+
case "linear_result":
|
|
821
|
+
handleLinearResult(msg);
|
|
822
|
+
break;
|
|
817
823
|
case "pair_result_status":
|
|
818
824
|
handlePairResultStatus(msg);
|
|
819
825
|
break;
|
|
@@ -621,7 +621,7 @@ export function handleRemoveProjectResult(msg) {
|
|
|
621
621
|
var targetSlug = chooseProjectAfterRemoval(cachedProjects, msg.slug);
|
|
622
622
|
|
|
623
623
|
showToast(isWorktree ? "Worktree removed" : "Project removed", "success");
|
|
624
|
-
closeProjectSettings();
|
|
624
|
+
closeProjectSettings({ cleanup: true });
|
|
625
625
|
|
|
626
626
|
// Suppress disconnect overlay and reconnect by detaching the WS
|
|
627
627
|
var ws = getWs();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { openMcpSkillsWorkbench } from "./mcp-skills-workbench.js";
|
|
2
|
+
import { copyToClipboard, showToast } from "./utils.js";
|
|
3
|
+
import { iconHtml, refreshIcons } from "./icons.js";
|
|
4
|
+
|
|
5
|
+
export function initExtensionSettings() {
|
|
6
|
+
var extPillWrap = document.getElementById("ext-pill-wrap");
|
|
7
|
+
var extPillBtn = document.getElementById("ext-pill");
|
|
8
|
+
var extPopover = document.getElementById("ext-popover");
|
|
9
|
+
var extDownloadBtn = document.getElementById("ext-download-btn");
|
|
10
|
+
var extDownloadStatus = document.getElementById("ext-download-status");
|
|
11
|
+
var extCopyUrl = document.getElementById("ext-copy-url");
|
|
12
|
+
if (!extPillWrap || !extPillBtn || !extPopover) return;
|
|
13
|
+
|
|
14
|
+
// Detect extension connection via postMessage from content.js
|
|
15
|
+
|
|
16
|
+
var connectedBanner = document.getElementById("ext-connected-banner");
|
|
17
|
+
var extDivider = document.getElementById("ext-popover-divider");
|
|
18
|
+
var extGuideTitle = document.getElementById("ext-popover-guide-title");
|
|
19
|
+
var extSteps = extPopover.querySelector(".ext-popover-steps");
|
|
20
|
+
|
|
21
|
+
function setExtConnected() {
|
|
22
|
+
if (extPillBtn.classList.contains("ext-connected")) return;
|
|
23
|
+
extPillBtn.classList.add("ext-connected");
|
|
24
|
+
extPillBtn.innerHTML = '<i data-lucide="check"></i> Extension';
|
|
25
|
+
refreshIcons(extPillBtn);
|
|
26
|
+
if (connectedBanner) connectedBanner.classList.remove("hidden");
|
|
27
|
+
if (extDownloadBtn) extDownloadBtn.style.display = "none";
|
|
28
|
+
if (extDownloadStatus) extDownloadStatus.classList.add("hidden");
|
|
29
|
+
if (extDivider) extDivider.style.display = "none";
|
|
30
|
+
if (extGuideTitle) extGuideTitle.style.display = "none";
|
|
31
|
+
if (extSteps) extSteps.style.display = "none";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
window.addEventListener("message", function (event) {
|
|
35
|
+
if (event.source !== window) return;
|
|
36
|
+
if (!event.data || event.data.source !== "clay-chrome-extension") return;
|
|
37
|
+
setExtConnected();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Toggle popover
|
|
41
|
+
extPillBtn.addEventListener("click", function (e) {
|
|
42
|
+
e.stopPropagation();
|
|
43
|
+
openMcpSkillsWorkbench("mcp");
|
|
44
|
+
var expanded = extPopover.classList.toggle("visible");
|
|
45
|
+
extPillBtn.setAttribute("aria-expanded", String(expanded));
|
|
46
|
+
refreshIcons(extPopover);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
document.addEventListener("click", function (e) {
|
|
50
|
+
if (!extPopover.contains(e.target) && e.target !== extPillBtn && !extPillBtn.contains(e.target)) {
|
|
51
|
+
extPopover.classList.remove("visible");
|
|
52
|
+
extPillBtn.setAttribute("aria-expanded", "false");
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Download button
|
|
57
|
+
if (extDownloadBtn) {
|
|
58
|
+
extDownloadBtn.addEventListener("click", function (e) {
|
|
59
|
+
e.stopPropagation();
|
|
60
|
+
extDownloadBtn.disabled = true;
|
|
61
|
+
extDownloadBtn.innerHTML = iconHtml("loader") + " Downloading...";
|
|
62
|
+
refreshIcons(extDownloadBtn);
|
|
63
|
+
var loaderIcon = extDownloadBtn.querySelector(".lucide");
|
|
64
|
+
if (loaderIcon) loaderIcon.style.animation = "spin 1s linear infinite";
|
|
65
|
+
if (extDownloadStatus) {
|
|
66
|
+
extDownloadStatus.classList.remove("hidden");
|
|
67
|
+
extDownloadStatus.textContent = "Fetching from GitHub...";
|
|
68
|
+
extDownloadStatus.style.color = "";
|
|
69
|
+
}
|
|
70
|
+
fetch("/api/extension/download").then(function (resp) {
|
|
71
|
+
if (!resp.ok) throw new Error("Download failed (" + resp.status + ")");
|
|
72
|
+
return resp.blob();
|
|
73
|
+
}).then(function (blob) {
|
|
74
|
+
var url = URL.createObjectURL(blob);
|
|
75
|
+
var a = document.createElement("a");
|
|
76
|
+
a.href = url;
|
|
77
|
+
a.download = "clay-chrome-extension.zip";
|
|
78
|
+
document.body.appendChild(a);
|
|
79
|
+
a.click();
|
|
80
|
+
document.body.removeChild(a);
|
|
81
|
+
URL.revokeObjectURL(url);
|
|
82
|
+
if (extDownloadStatus) {
|
|
83
|
+
extDownloadStatus.textContent = "Download complete!";
|
|
84
|
+
extDownloadStatus.style.color = "var(--accent)";
|
|
85
|
+
}
|
|
86
|
+
showToast("Extension downloaded");
|
|
87
|
+
}).catch(function (err) {
|
|
88
|
+
if (extDownloadStatus) {
|
|
89
|
+
extDownloadStatus.textContent = "Failed: " + err.message;
|
|
90
|
+
extDownloadStatus.style.color = "var(--danger, #e53935)";
|
|
91
|
+
}
|
|
92
|
+
showToast("Download failed");
|
|
93
|
+
}).finally(function () {
|
|
94
|
+
extDownloadBtn.disabled = false;
|
|
95
|
+
extDownloadBtn.innerHTML = iconHtml("download") + " Download Extension (.zip)";
|
|
96
|
+
refreshIcons(extDownloadBtn);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Copy chrome://extensions URL
|
|
102
|
+
if (extCopyUrl) {
|
|
103
|
+
extCopyUrl.addEventListener("click", function (e) {
|
|
104
|
+
e.stopPropagation();
|
|
105
|
+
copyToClipboard("chrome://extensions").then(function () {
|
|
106
|
+
showToast("Copied chrome://extensions");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { openLinear, initLinearPanel } from './linear-panel.js';
|
|
1
2
|
import { store } from './store.js';
|
|
2
3
|
import { getWs } from './ws-ref.js';
|
|
3
4
|
import { showToast } from './utils.js';
|
|
@@ -162,13 +163,15 @@ export function openIssues(ref) {
|
|
|
162
163
|
else listing();
|
|
163
164
|
}
|
|
164
165
|
export function initIssues() {
|
|
166
|
+
initLinearPanel();
|
|
165
167
|
var host = document.getElementById('main-panels');
|
|
166
168
|
if (!host || panel()) return;
|
|
167
169
|
registerRightWorkbench("issues", closeIssues);
|
|
168
170
|
var el = document.createElement('section');
|
|
169
171
|
el.id = 'issues-panel'; el.className = 'hidden'; el.setAttribute('aria-label', 'Project Issues');
|
|
170
|
-
el.innerHTML = '<header class="issues-topbar"><button class="issues-icon-btn" data-issues-back aria-label="Back to issues" title="Back to issue list"><i data-lucide="arrow-left"></i></button><span class="issues-title"><i data-lucide="circle-dot"></i>Issues</span><span class="issues-subtitle">Manage issues through Driver chat.</span><div class="issues-window-actions"><button class="issues-icon-btn" data-issues-wide aria-label="Widen Issues" title="Widen Issues" aria-pressed="false"><i data-lucide="chevrons-left-right"></i></button><button class="issues-icon-btn" data-issues-full aria-label="Toggle Issues fullscreen" title="Toggle fullscreen" aria-pressed="false"><i data-lucide="maximize-2"></i></button><button class="issues-icon-btn" data-issues-close aria-label="Close Issues" title="Close Issues"><i data-lucide="x"></i></button></div></header><div class="issues-filters"><label class="issues-search"><i data-lucide="search"></i><input name="query" type="search" placeholder="Search issues" aria-label="Search issues"></label><select name="filter-status" aria-label="Filter status"><option value="">All statuses</option><option value="open">Open</option><option value="in_progress">In progress</option><option value="resolved">Resolved</option><option value="closed">Closed</option></select><select name="filter-type" aria-label="Filter type"><option value="">All types</option><option value="bug">Bug</option><option value="feature">Feature</option><option value="plan">Plan</option></select></div><p class="issues-notice" role="status"></p><main class="issues-content" role="region" aria-live="polite"></main><div class="issues-delete-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="issues-delete-heading"><div class="issues-delete-card"><h2 id="issues-delete-heading">Delete this issue?</h2><p>This removes the issue from the board but preserves its history.</p><p class="issues-delete-title"></p><p class="issues-delete-status" role="status"></p><div><button type="button" data-issue-delete-cancel>Cancel</button><button type="button" data-issue-delete-confirm>Delete issue</button></div></div></div>';
|
|
172
|
+
el.innerHTML = '<header class="issues-topbar"><button class="issues-icon-btn" data-issues-back aria-label="Back to issues" title="Back to issue list"><i data-lucide="arrow-left"></i></button><span class="issues-title"><i data-lucide="circle-dot"></i>Issues</span><span class="issues-subtitle">Manage issues through Driver chat.</span><button type="button" class="issues-icon-btn" data-open-linear>Linear</button><div class="issues-window-actions"><button class="issues-icon-btn" data-issues-wide aria-label="Widen Issues" title="Widen Issues" aria-pressed="false"><i data-lucide="chevrons-left-right"></i></button><button class="issues-icon-btn" data-issues-full aria-label="Toggle Issues fullscreen" title="Toggle fullscreen" aria-pressed="false"><i data-lucide="maximize-2"></i></button><button class="issues-icon-btn" data-issues-close aria-label="Close Issues" title="Close Issues"><i data-lucide="x"></i></button></div></header><div class="issues-filters"><label class="issues-search"><i data-lucide="search"></i><input name="query" type="search" placeholder="Search issues" aria-label="Search issues"></label><select name="filter-status" aria-label="Filter status"><option value="">All statuses</option><option value="open">Open</option><option value="in_progress">In progress</option><option value="resolved">Resolved</option><option value="closed">Closed</option></select><select name="filter-type" aria-label="Filter type"><option value="">All types</option><option value="bug">Bug</option><option value="feature">Feature</option><option value="plan">Plan</option></select></div><p class="issues-notice" role="status"></p><main class="issues-content" role="region" aria-live="polite"></main><div class="issues-delete-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="issues-delete-heading"><div class="issues-delete-card"><h2 id="issues-delete-heading">Delete this issue?</h2><p>This removes the issue from the board but preserves its history.</p><p class="issues-delete-title"></p><p class="issues-delete-status" role="status"></p><div><button type="button" data-issue-delete-cancel>Cancel</button><button type="button" data-issue-delete-confirm>Delete issue</button></div></div></div>';
|
|
171
173
|
host.appendChild(el);
|
|
174
|
+
el.querySelector('[data-open-linear]').addEventListener('click', function () { openLinear(); });
|
|
172
175
|
el.addEventListener('input', handleIssueGithubInput);
|
|
173
176
|
var button = document.getElementById('issues-btn');
|
|
174
177
|
if (button) button.addEventListener('click', function () { if (store.get('issuesOpen')) closeIssues(); else openIssues(); });
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { escapeHtml } from './utils.js';
|
|
4
|
+
import { renderMarkdown } from './markdown.js';
|
|
5
|
+
import { refreshIcons } from './icons.js';
|
|
6
|
+
import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
|
|
7
|
+
import { openUserSettings } from './user-settings.js';
|
|
8
|
+
|
|
9
|
+
function esc(value) { return escapeHtml(String(value || '')).replace(/"/g, '"').replace(/'/g, '''); }
|
|
10
|
+
function panel() { return document.getElementById('linear-panel'); }
|
|
11
|
+
function content() { return panel().querySelector('.linear-content'); }
|
|
12
|
+
function notice(text) { panel().querySelector('[role="status"]').textContent = text; }
|
|
13
|
+
function request(action, args) {
|
|
14
|
+
var ws = getWs();
|
|
15
|
+
if (!ws || ws.readyState !== 1) { notice('Reconnect to use Linear.'); return; }
|
|
16
|
+
if (action === 'linear_search' || (action === 'linear_read' && !args.cursor)) {
|
|
17
|
+
store.set({ linearIssue: null, linearMatches: [] }); content().textContent = '';
|
|
18
|
+
}
|
|
19
|
+
var id = crypto.randomUUID();
|
|
20
|
+
store.set({ linearRequest: { id: id, action: action, args: args, project: store.get('currentSlug'), socket: ws } });
|
|
21
|
+
notice('Loading…');
|
|
22
|
+
ws.send(JSON.stringify({ type: action, args: args, requestId: id }));
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
function prose(text) {
|
|
26
|
+
var element = document.createElement('template');
|
|
27
|
+
element.innerHTML = renderMarkdown(text || '');
|
|
28
|
+
// Private Linear attachments need authenticated delivery; keep them as links for now.
|
|
29
|
+
element.content.querySelectorAll('img').forEach(function (image) {
|
|
30
|
+
var link = document.createElement('a');
|
|
31
|
+
link.textContent = image.alt || 'View attachment in Linear';
|
|
32
|
+
link.href = (store.get('linearIssue') || {}).url || 'https://linear.app'; link.target = '_blank'; link.rel = 'noopener noreferrer';
|
|
33
|
+
image.replaceWith(link);
|
|
34
|
+
});
|
|
35
|
+
return DOMPurify.sanitize(element.innerHTML, { ALLOW_DATA_ATTR: false, FORBID_TAGS: ['form', 'button', 'input', 'select', 'textarea', 'iframe', 'video', 'audio'], FORBID_ATTR: ['id', 'class', 'style'] });
|
|
36
|
+
}
|
|
37
|
+
function renderIssue() {
|
|
38
|
+
var issue = store.get('linearIssue'), sessions = store.get('linearMatches') || [];
|
|
39
|
+
if (!issue) return;
|
|
40
|
+
var active = (store.get('githubWorkSessions') || []).find(function (item) { return item.id === store.get('activeSessionId'); });
|
|
41
|
+
var linked = !!(active && (active.linearLinks || []).some(function (item) { return item.url === issue.url; }));
|
|
42
|
+
var prs = active && linked ? (active.githubLinks || []).filter(function (item) { return item.kind === 'pr' && /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/[1-9][0-9]*$/.test(item.url); }) : [];
|
|
43
|
+
content().innerHTML = '<article class="issue-detail"><div class="issue-byline">' + esc(issue.identifier) + ' · ' + esc(issue.stateName) + '</div><h2 class="issue-doc-title">' + esc(issue.title) + '</h2>' +
|
|
44
|
+
'<div class="linear-actions"><button type="button" data-linear-link>' + (linked ? 'Unlink from this conversation' : 'Link to this conversation') + '</button>' +
|
|
45
|
+
(!sessions.length ? '<button type="button" data-linear-start>Start work</button>' : '') +
|
|
46
|
+
'<a target="_blank" rel="noopener noreferrer" href="' + esc(issue.url) + '">Open in Linear</a></div>' +
|
|
47
|
+
'<p class="settings-hint">Linking shares the issue title and status with this conversation’s viewers. Starting work adds the description and comments to a new conversation.</p>' +
|
|
48
|
+
(sessions.length ? '<section><h3>Linked conversations</h3>' + sessions.map(function (item) { return '<button type="button" class="issue-row" data-linear-session="' + item.sessionId + '">' + esc(item.title || 'Open conversation') + '</button>'; }).join('') + '</section>' : '') +
|
|
49
|
+
(prs.length ? '<section><h3>GitHub pull requests</h3>' + prs.map(function (pr) { return '<a target="_blank" rel="noopener noreferrer" href="' + esc(pr.url) + '">PR #' + esc(pr.number) + ' · ' + esc(pr.title) + '</a>'; }).join('<br>') + '</section>' : '') +
|
|
50
|
+
'<div class="issue-doc-body">' + prose(issue.description) + '</div><section class="issue-discussion"><h3>Comments</h3>' +
|
|
51
|
+
(issue.comments.length ? issue.comments.map(function (comment) { return '<article><strong>' + esc(comment.author) + '</strong><div class="issue-doc-body">' + prose(comment.body) + '</div></article>'; }).join('') : '<p>No comments yet.</p>') +
|
|
52
|
+
(issue.pageInfo && issue.pageInfo.hasNextPage ? '<button type="button" data-linear-more>More comments</button>' : '') + '</section></article>';
|
|
53
|
+
}
|
|
54
|
+
function showSearch() {
|
|
55
|
+
store.set({ linearIssue: null, linearRequest: null, linearMatches: [] });
|
|
56
|
+
content().innerHTML = '<p class="linear-empty">Search by title or identifier, or paste a Linear issue URL.</p>';
|
|
57
|
+
notice('');
|
|
58
|
+
panel().querySelector('input').focus();
|
|
59
|
+
}
|
|
60
|
+
export function closeLinear() {
|
|
61
|
+
if (panel()) panel().classList.add('hidden');
|
|
62
|
+
store.set({ linearOpen: false, linearRequest: null, linearIssue: null, linearMatches: [] });
|
|
63
|
+
releaseRightWorkbench('linear');
|
|
64
|
+
}
|
|
65
|
+
export function openLinear(url) {
|
|
66
|
+
initLinearPanel();
|
|
67
|
+
if (!panel() || store.get('isMate')) return;
|
|
68
|
+
claimRightWorkbench('linear');
|
|
69
|
+
panel().classList.remove('hidden'); store.set({ linearOpen: true });
|
|
70
|
+
if (url) { content().textContent = ''; request('linear_read', { url: url }); }
|
|
71
|
+
else showSearch();
|
|
72
|
+
}
|
|
73
|
+
export function handleLinearResult(msg) {
|
|
74
|
+
var pending = store.get('linearRequest');
|
|
75
|
+
if (!pending || !store.get('linearOpen') || pending.id !== msg.requestId || pending.project !== store.get('currentSlug') || msg.projectSlug !== pending.project || pending.socket !== getWs()) return;
|
|
76
|
+
store.set({ linearRequest: null });
|
|
77
|
+
panel().querySelectorAll('button').forEach(function (button) { button.disabled = false; });
|
|
78
|
+
if (msg.error) { notice(msg.error); return; }
|
|
79
|
+
notice('');
|
|
80
|
+
var result = msg.result || {};
|
|
81
|
+
if (msg.action === 'linear_search') {
|
|
82
|
+
store.set({ linearIssue: null });
|
|
83
|
+
content().innerHTML = (result.items || []).map(function (item) {
|
|
84
|
+
return '<button type="button" class="issue-row" data-linear-url="' + esc(item.url) + '"><small>' + esc(item.identifier) + ' · ' + esc(item.stateName) + '</small><strong>' + esc(item.title) + '</strong></button>';
|
|
85
|
+
}).join('') || '<p class="linear-empty">No matching issues.</p>';
|
|
86
|
+
} else if (msg.action === 'linear_read') {
|
|
87
|
+
var issue = result.issue, old = store.get('linearIssue');
|
|
88
|
+
if (pending.args.cursor && old && old.url === issue.url) issue.comments = old.comments.concat(issue.comments).filter(function (item, index, all) { return all.findIndex(function (other) { return item.id === other.id; }) === index; });
|
|
89
|
+
store.set({ linearIssue: issue, linearMatches: result.matchingSessions }); renderIssue();
|
|
90
|
+
} else if (msg.action === 'linear_link' || msg.action === 'linear_unlink') request('linear_read', { url: pending.args.url });
|
|
91
|
+
else if (msg.action === 'linear_start_work' || msg.action === 'linear_open_work') { request('linear_read', { url: pending.args.url }); }
|
|
92
|
+
}
|
|
93
|
+
export function initLinearPanel() {
|
|
94
|
+
var host = document.getElementById('main-panels');
|
|
95
|
+
if (!host || panel()) return;
|
|
96
|
+
var el = document.createElement('section'); el.id = 'linear-panel'; el.className = 'hidden'; el.setAttribute('aria-label', 'Linear issues');
|
|
97
|
+
el.innerHTML = '<header class="linear-topbar"><button type="button" data-linear-back aria-label="Back to Linear search"><i data-lucide="arrow-left"></i></button><strong>Linear</strong><button type="button" data-linear-settings>Connection</button><button type="button" data-linear-close aria-label="Close Linear"><i data-lucide="x"></i></button></header><form class="linear-search"><input type="search" aria-label="Search Linear issues" placeholder="Issue URL, identifier or title" maxlength="2048" required><button type="submit">Search</button></form><p class="linear-notice" role="status"></p><div class="linear-content"></div>';
|
|
98
|
+
host.appendChild(el); registerRightWorkbench('linear', closeLinear); refreshIcons(el);
|
|
99
|
+
el.querySelector('form').addEventListener('submit', function (event) { event.preventDefault(); request('linear_search', { query: el.querySelector('input').value }); });
|
|
100
|
+
el.addEventListener('click', function (event) {
|
|
101
|
+
var button = event.target.closest('button'); if (!button) return;
|
|
102
|
+
var issue = store.get('linearIssue');
|
|
103
|
+
if (button.hasAttribute('data-linear-close')) closeLinear();
|
|
104
|
+
else if (button.hasAttribute('data-linear-back')) showSearch();
|
|
105
|
+
else if (button.hasAttribute('data-linear-settings')) openUserSettings('us-integrations');
|
|
106
|
+
else if (button.dataset.linearUrl) { content().textContent = ''; request('linear_read', { url: button.dataset.linearUrl }); }
|
|
107
|
+
else if (issue && button.hasAttribute('data-linear-link')) {
|
|
108
|
+
var active = (store.get('githubWorkSessions') || []).find(function (item) { return item.id === store.get('activeSessionId'); });
|
|
109
|
+
var linked = active && (active.linearLinks || []).some(function (item) { return item.url === issue.url; });
|
|
110
|
+
button.disabled = true; request(linked ? 'linear_unlink' : 'linear_link', { url: issue.url, sessionId: store.get('activeSessionId') });
|
|
111
|
+
} else if (issue && button.dataset.linearSession) { button.disabled = true; request('linear_open_work', { url: issue.url, sessionId: Number(button.dataset.linearSession) }); }
|
|
112
|
+
else if (issue && button.hasAttribute('data-linear-start')) { button.disabled = true; request('linear_start_work', { url: issue.url }); }
|
|
113
|
+
else if (issue && button.hasAttribute('data-linear-more')) { button.disabled = true; request('linear_read', { url: issue.url, cursor: issue.pageInfo.endCursor }); }
|
|
114
|
+
});
|
|
115
|
+
document.addEventListener('keydown', function (event) { if (event.key === 'Escape' && store.get('linearOpen')) closeLinear(); });
|
|
116
|
+
store.subscribe(function (state, previous) {
|
|
117
|
+
if (state.currentSlug !== previous.currentSlug || state.myUserId !== previous.myUserId || state.dmMode !== previous.dmMode || (previous.connected && !state.connected)) closeLinear();
|
|
118
|
+
else if (state.linearOpen && state.githubWorkSessions !== previous.githubWorkSessions && state.linearIssue) renderIssue();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
|
|
3
|
+
export function clearLinearKeyInput() {
|
|
4
|
+
var input = document.getElementById('us-linear-key');
|
|
5
|
+
if (input) input.value = '';
|
|
6
|
+
}
|
|
7
|
+
function setLinearStatus(text, state) {
|
|
8
|
+
var status = document.getElementById('us-linear-status');
|
|
9
|
+
if (!status) return;
|
|
10
|
+
status.textContent = text;
|
|
11
|
+
if (state) status.dataset.state = state; else delete status.dataset.state;
|
|
12
|
+
}
|
|
13
|
+
export async function loadLinearConnection() {
|
|
14
|
+
var actor = store.get('myUserId');
|
|
15
|
+
var generation = (store.get('linearConnectionGeneration') || 0) + 1;
|
|
16
|
+
store.set({ linearConnectionGeneration: generation });
|
|
17
|
+
try {
|
|
18
|
+
var response = await fetch('/api/linear/connection');
|
|
19
|
+
if (!response.ok) throw new Error('Could not load Linear connection.');
|
|
20
|
+
var data = await response.json();
|
|
21
|
+
if (store.get('linearConnectionGeneration') !== generation || store.get('myUserId') !== actor) return;
|
|
22
|
+
store.set({ linearConnection: data });
|
|
23
|
+
setLinearStatus(data.connected ? 'Connected to ' + data.workspace.name : 'Not connected', data.connected ? 'connected' : '');
|
|
24
|
+
var disconnect = document.getElementById('us-linear-disconnect');
|
|
25
|
+
if (disconnect) disconnect.disabled = !data.connected;
|
|
26
|
+
} catch (error) { if (store.get('linearConnectionGeneration') === generation) setLinearStatus(error.message, 'error'); }
|
|
27
|
+
}
|
|
28
|
+
export function initLinearSettings() {
|
|
29
|
+
var form = document.getElementById('us-linear-form');
|
|
30
|
+
if (!form || store.get('linearSettingsBound')) return;
|
|
31
|
+
store.set({ linearSettingsBound: true });
|
|
32
|
+
async function save(disconnect) {
|
|
33
|
+
if (store.get('linearConnectionSaving')) return;
|
|
34
|
+
var actor = store.get('myUserId');
|
|
35
|
+
store.set({ linearConnectionSaving: true, linearConnectionGeneration: (store.get('linearConnectionGeneration') || 0) + 1 });
|
|
36
|
+
var input = document.getElementById('us-linear-key');
|
|
37
|
+
var buttons = form.querySelectorAll('button');
|
|
38
|
+
buttons.forEach(function (button) { button.disabled = true; });
|
|
39
|
+
setLinearStatus(disconnect ? 'Disconnecting…' : 'Connecting…', '');
|
|
40
|
+
try {
|
|
41
|
+
var response = await fetch('/api/linear/connection', { method: disconnect ? 'DELETE' : 'PUT',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' }, body: disconnect ? undefined : JSON.stringify({ key: input.value }) });
|
|
43
|
+
clearLinearKeyInput();
|
|
44
|
+
if (store.get('myUserId') !== actor) return;
|
|
45
|
+
var data = await response.json();
|
|
46
|
+
if (!response.ok) throw new Error(data.error || 'Could not update Linear connection.');
|
|
47
|
+
store.set({ linearConnection: data, githubWorkRefresh: null });
|
|
48
|
+
setLinearStatus(data.connected ? 'Connected to ' + data.workspace.name : 'Disconnected', data.connected ? 'connected' : '');
|
|
49
|
+
} catch (error) { clearLinearKeyInput(); setLinearStatus(error.message, 'error'); }
|
|
50
|
+
finally {
|
|
51
|
+
store.set({ linearConnectionSaving: false });
|
|
52
|
+
buttons.forEach(function (button) { button.disabled = false; });
|
|
53
|
+
document.getElementById('us-linear-disconnect').disabled = !(store.get('linearConnection') || {}).connected;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
store.subscribe(function (state, previous) {
|
|
57
|
+
if (state.myUserId !== previous.myUserId) { clearLinearKeyInput(); store.set({ linearConnection: null, linearConnectionGeneration: (state.linearConnectionGeneration || 0) + 1 }); }
|
|
58
|
+
});
|
|
59
|
+
form.addEventListener('submit', function (event) { event.preventDefault(); save(false); });
|
|
60
|
+
document.getElementById('us-linear-disconnect').addEventListener('click', function () { save(true); });
|
|
61
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
|
+
import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
|
|
4
|
+
|
|
5
|
+
function state() { return store.get('mcpSkillsWorkbench') || {}; }
|
|
6
|
+
function patch(value) { store.set({ mcpSkillsWorkbench: Object.assign({}, state(), value) }); }
|
|
7
|
+
function skillsAllowed() { var permissions = store.get('permissions'); return !permissions || permissions.skills !== false; }
|
|
8
|
+
export function ensureMcpSkillsWorkbench() {
|
|
9
|
+
var panel = document.getElementById('mcp-skills-workbench');
|
|
10
|
+
if (panel) return panel;
|
|
11
|
+
var root = document.getElementById('main-panels');
|
|
12
|
+
if (!root) return null;
|
|
13
|
+
panel = document.createElement('section'); panel.id = 'mcp-skills-workbench'; panel.className = 'mcp-skills-workbench hidden'; panel.setAttribute('aria-label', 'MCP / Skills');
|
|
14
|
+
panel.innerHTML = '<header class="mcp-skills-header"><strong>' + iconHtml('blocks') + 'MCP / Skills</strong><div>' +
|
|
15
|
+
'<button type="button" data-workbench="wide" aria-label="Widen MCP / Skills">' + iconHtml('chevrons-left-right') + '</button>' +
|
|
16
|
+
'<button type="button" data-workbench="fullscreen" aria-label="Toggle MCP / Skills fullscreen">' + iconHtml('maximize-2') + '</button>' +
|
|
17
|
+
'<button type="button" data-workbench="close" aria-label="Close MCP / Skills">' + iconHtml('x') + '</button></div></header>' +
|
|
18
|
+
'<div class="mcp-skills-tabs" role="tablist" aria-label="MCP / Skills sections"><button id="mcp-workbench-tab" role="tab" data-section="mcp" aria-controls="mcp-modal">MCP Servers</button><button id="skills-workbench-tab" role="tab" data-section="skills" aria-controls="skills-modal">Skills</button></div>';
|
|
19
|
+
['mcp', 'skills'].forEach(function (name) {
|
|
20
|
+
var section = document.getElementById(name + '-modal');
|
|
21
|
+
if (!section) return;
|
|
22
|
+
var backdrop = section.querySelector('.confirm-backdrop'); if (backdrop) backdrop.remove();
|
|
23
|
+
var dialog = section.querySelector('.confirm-dialog'); if (dialog) dialog.classList.remove('confirm-dialog');
|
|
24
|
+
section.setAttribute('role', 'tabpanel'); section.setAttribute('aria-labelledby', name + '-workbench-tab');
|
|
25
|
+
panel.appendChild(section);
|
|
26
|
+
});
|
|
27
|
+
root.appendChild(panel);
|
|
28
|
+
panel.querySelector('[data-workbench="close"]').onclick = closeMcpSkillsWorkbench;
|
|
29
|
+
['wide', 'fullscreen'].forEach(function (key) { panel.querySelector('[data-workbench="' + key + '"]').onclick = function () { var value = {}; value[key] = !state()[key]; patch(value); }; });
|
|
30
|
+
panel.querySelectorAll('[data-section]').forEach(function (button) {
|
|
31
|
+
button.onclick = function () { openMcpSkillsWorkbench(button.dataset.section); };
|
|
32
|
+
button.onkeydown = function (event) {
|
|
33
|
+
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
|
34
|
+
event.preventDefault(); var tab = button.dataset.section === 'mcp' && skillsAllowed() ? 'skills' : 'mcp';
|
|
35
|
+
if (event.key === 'Home') tab = 'mcp'; if (event.key === 'End' && skillsAllowed()) tab = 'skills';
|
|
36
|
+
openMcpSkillsWorkbench(tab); panel.querySelector('[data-section="' + tab + '"]').focus();
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
// Compatibility targets for older mobile entry points; never palette tiles.
|
|
40
|
+
['skills-btn', 'mate-skills-btn'].forEach(function (id) {
|
|
41
|
+
if (document.getElementById(id)) return;
|
|
42
|
+
var button = document.createElement('button'); button.id = id; button.hidden = true;
|
|
43
|
+
button.onclick = function () { openMcpSkillsWorkbench('skills'); }; root.appendChild(button);
|
|
44
|
+
});
|
|
45
|
+
refreshIcons(panel); render(); return panel;
|
|
46
|
+
}
|
|
47
|
+
export function openMcpSkillsWorkbench(tab) {
|
|
48
|
+
if (!ensureMcpSkillsWorkbench()) return;
|
|
49
|
+
tab = tab || state().tab || 'mcp';
|
|
50
|
+
if (tab === 'skills' && !skillsAllowed()) tab = 'mcp';
|
|
51
|
+
claimRightWorkbench('mcp-skills'); patch({ open: true, tab: tab });
|
|
52
|
+
}
|
|
53
|
+
export function closeMcpSkillsWorkbench() { patch({ open: false }); releaseRightWorkbench('mcp-skills'); }
|
|
54
|
+
function render() {
|
|
55
|
+
var panel = document.getElementById('mcp-skills-workbench'); if (!panel) return;
|
|
56
|
+
var current = state(); var tab = current.tab || 'mcp';
|
|
57
|
+
panel.classList.toggle('hidden', !current.open); panel.classList.toggle('workbench-wide', !!current.wide); panel.classList.toggle('panel-fullscreen', !!current.fullscreen);
|
|
58
|
+
['wide', 'fullscreen'].forEach(function (key) { panel.querySelector('[data-workbench="' + key + '"]').setAttribute('aria-pressed', String(!!current[key])); });
|
|
59
|
+
panel.querySelectorAll('[data-section]').forEach(function (button) { var selected = button.dataset.section === tab; button.setAttribute('aria-selected', String(selected)); button.tabIndex = selected ? 0 : -1; button.hidden = button.dataset.section === 'skills' && !skillsAllowed(); });
|
|
60
|
+
['mcp', 'skills'].forEach(function (name) { var section = document.getElementById(name + '-modal'); if (section) section.classList.toggle('hidden', !current.open || name !== tab); });
|
|
61
|
+
}
|
|
62
|
+
registerRightWorkbench('mcp-skills', closeMcpSkillsWorkbench);
|
|
63
|
+
store.subscribe(function (current, previous) {
|
|
64
|
+
if (current.currentSlug !== previous.currentSlug || current.myUserId !== previous.myUserId || current.dmMode !== previous.dmMode) { closeMcpSkillsWorkbench(); return; }
|
|
65
|
+
if (current.permissions !== previous.permissions && !skillsAllowed() && state().tab === 'skills') { patch({ tab: 'mcp' }); return; }
|
|
66
|
+
if (current.mcpSkillsWorkbench !== previous.mcpSkillsWorkbench || current.permissions !== previous.permissions) render();
|
|
67
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ensureMcpSkillsWorkbench, openMcpSkillsWorkbench, closeMcpSkillsWorkbench } from './mcp-skills-workbench.js';
|
|
1
2
|
// mcp-ui.js - MCP Servers modal (sidebar button + panel)
|
|
2
3
|
// Renders available MCP servers with per-project toggle checkboxes.
|
|
3
4
|
|
|
@@ -69,6 +70,7 @@ export function mcpPermissionModeApplicability(state) {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
export function initMcp() {
|
|
73
|
+
ensureMcpSkillsWorkbench();
|
|
72
74
|
modal = document.getElementById("mcp-modal");
|
|
73
75
|
contentEl = document.getElementById("mcp-content");
|
|
74
76
|
|
|
@@ -87,6 +89,7 @@ export function initMcp() {
|
|
|
87
89
|
state.currentMode !== prev.currentMode || state.effectivePermissionMode !== prev.effectivePermissionMode ||
|
|
88
90
|
state.permissionCapabilities !== prev.permissionCapabilities || state.mcpPermissionModeOverrides !== prev.mcpPermissionModeOverrides ||
|
|
89
91
|
state.mcpPermissionModePendingByTarget !== prev.mcpPermissionModePendingByTarget || state.connected !== prev.connected;
|
|
92
|
+
changed = changed || state.mcpSkillsWorkbench !== prev.mcpSkillsWorkbench;
|
|
90
93
|
if (changed && modal && !modal.classList.contains("hidden")) renderMcpServerList();
|
|
91
94
|
});
|
|
92
95
|
|
|
@@ -126,14 +129,14 @@ export function getMcpServers() {
|
|
|
126
129
|
|
|
127
130
|
function openMcpModal() {
|
|
128
131
|
if (!modal) return;
|
|
129
|
-
|
|
132
|
+
openMcpSkillsWorkbench("mcp");
|
|
130
133
|
refreshIcons(modal);
|
|
131
134
|
renderMcpServerList();
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
function closeMcpModal() {
|
|
135
138
|
if (!modal) return;
|
|
136
|
-
|
|
139
|
+
closeMcpSkillsWorkbench();
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
function updateBadge() {
|
|
@@ -272,11 +275,8 @@ function renderMcpServerList() {
|
|
|
272
275
|
label: "Setup Extension",
|
|
273
276
|
icon: "puzzle",
|
|
274
277
|
onClick: function () {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
var extPill = document.getElementById("ext-pill");
|
|
278
|
-
if (extPill) extPill.click();
|
|
279
|
-
}, 100);
|
|
278
|
+
var extPill = document.getElementById("ext-pill");
|
|
279
|
+
if (extPill && extPill.getAttribute("aria-expanded") !== "true") extPill.click();
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
}));
|
|
@@ -317,11 +317,8 @@ function renderMcpServerList() {
|
|
|
317
317
|
label: "Open Extension popup to add servers",
|
|
318
318
|
icon: "puzzle",
|
|
319
319
|
onClick: function () {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
var extPill = document.getElementById("ext-pill");
|
|
323
|
-
if (extPill) extPill.click();
|
|
324
|
-
}, 100);
|
|
320
|
+
var extPill = document.getElementById("ext-pill");
|
|
321
|
+
if (extPill && extPill.getAttribute("aria-expanded") !== "true") extPill.click();
|
|
325
322
|
}
|
|
326
323
|
} : null
|
|
327
324
|
}));
|