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
|
@@ -1,29 +1,14 @@
|
|
|
1
|
+
import { skillState, basePath, getInstalledInfo, scopeLabel } from './skills-state.js';
|
|
2
|
+
import { renderDetail } from './skills-detail.js';
|
|
3
|
+
import { uninstallSkill } from './skills-actions.js';
|
|
4
|
+
export { handleSkillInstalled, handleSkillUninstalled } from './skills-actions.js';
|
|
5
|
+
import { openMcpSkillsWorkbench, closeMcpSkillsWorkbench } from './mcp-skills-workbench.js';
|
|
1
6
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
2
|
-
import { escapeHtml
|
|
7
|
+
import { escapeHtml } from './utils.js';
|
|
3
8
|
import { store } from './store.js';
|
|
4
|
-
import { getWs } from './ws-ref.js';
|
|
5
|
-
|
|
6
|
-
// Ask the active session to hot-reload skills so a just-installed/uninstalled
|
|
7
|
-
// skill takes effect without restarting the session. No-op if no live session.
|
|
8
|
-
function reloadActiveSessionSkills() {
|
|
9
|
-
var ws = getWs();
|
|
10
|
-
if (ws && ws.readyState === 1) {
|
|
11
|
-
ws.send(JSON.stringify({ type: "reload_skills" }));
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
9
|
|
|
15
10
|
var modal;
|
|
16
11
|
var contentEl;
|
|
17
|
-
var activeTab = "all";
|
|
18
|
-
var currentView = "list"; // "list" or "detail"
|
|
19
|
-
var skillsData = {}; // cache per tab
|
|
20
|
-
var installedSkills = {}; // { skillName: { scope: "global"|"project"|"both" } }
|
|
21
|
-
var searchQuery = "";
|
|
22
|
-
var searchTimer = null;
|
|
23
|
-
var searchCache = {}; // cache per query
|
|
24
|
-
|
|
25
|
-
function basePath() { return store.get('basePath') || ""; }
|
|
26
|
-
|
|
27
12
|
export function initSkills() {
|
|
28
13
|
|
|
29
14
|
modal = document.getElementById("skills-modal");
|
|
@@ -44,6 +29,7 @@ export function initSkills() {
|
|
|
44
29
|
}
|
|
45
30
|
|
|
46
31
|
if (backBtn) {
|
|
32
|
+
backBtn.setAttribute("aria-label", "Back to skills");
|
|
47
33
|
backBtn.addEventListener("click", showListView);
|
|
48
34
|
}
|
|
49
35
|
|
|
@@ -51,6 +37,12 @@ export function initSkills() {
|
|
|
51
37
|
backdrop.addEventListener("click", closeSkillsModal);
|
|
52
38
|
}
|
|
53
39
|
|
|
40
|
+
store.subscribe(function (state, previous) {
|
|
41
|
+
var next = state.mcpSkillsWorkbench || {}; var old = previous.mcpSkillsWorkbench || {};
|
|
42
|
+
if (state.skillsRefreshVersion !== previous.skillsRefreshVersion && skillState().activeTab === 'installed' && skillState().currentView === 'list') loadInstalledSkills();
|
|
43
|
+
if (next.open && next.tab === 'skills' && (!old.open || old.tab !== 'skills')) activateSkills();
|
|
44
|
+
});
|
|
45
|
+
|
|
54
46
|
// Search input
|
|
55
47
|
var searchInput = document.getElementById("skills-search-input");
|
|
56
48
|
var searchHint = document.getElementById("skills-search-hint");
|
|
@@ -66,22 +58,23 @@ export function initSkills() {
|
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
if (searchInput) {
|
|
61
|
+
searchInput.setAttribute("aria-label", "Search skills");
|
|
69
62
|
searchInput.addEventListener("input", function () {
|
|
70
63
|
var q = searchInput.value.trim();
|
|
71
|
-
searchQuery = q;
|
|
64
|
+
skillState().searchQuery = q;
|
|
72
65
|
updateSearchControls();
|
|
73
|
-
if (searchTimer) clearTimeout(searchTimer);
|
|
66
|
+
if (skillState().searchTimer) clearTimeout(skillState().searchTimer);
|
|
74
67
|
if (!q) {
|
|
75
68
|
// Clear search — restore tab view
|
|
76
69
|
var tabsEl = modal.querySelector(".skills-tabs");
|
|
77
70
|
if (tabsEl) tabsEl.style.display = "";
|
|
78
|
-
loadSkills(activeTab);
|
|
71
|
+
loadSkills(skillState().activeTab);
|
|
79
72
|
return;
|
|
80
73
|
}
|
|
81
74
|
// Hide tabs during search
|
|
82
75
|
var tabsEl2 = modal.querySelector(".skills-tabs");
|
|
83
76
|
if (tabsEl2) tabsEl2.style.display = "none";
|
|
84
|
-
searchTimer = setTimeout(function () {
|
|
77
|
+
skillState().searchTimer = setTimeout(function () {
|
|
85
78
|
loadSearchResults(q);
|
|
86
79
|
}, 300);
|
|
87
80
|
});
|
|
@@ -90,19 +83,20 @@ export function initSkills() {
|
|
|
90
83
|
if (searchClear) {
|
|
91
84
|
searchClear.addEventListener("click", function () {
|
|
92
85
|
if (searchInput) { searchInput.value = ""; }
|
|
93
|
-
searchQuery = "";
|
|
86
|
+
skillState().searchQuery = "";
|
|
94
87
|
updateSearchControls();
|
|
95
|
-
if (searchTimer) clearTimeout(searchTimer);
|
|
88
|
+
if (skillState().searchTimer) clearTimeout(skillState().searchTimer);
|
|
96
89
|
var tabsEl = modal.querySelector(".skills-tabs");
|
|
97
90
|
if (tabsEl) tabsEl.style.display = "";
|
|
98
|
-
loadSkills(activeTab);
|
|
91
|
+
loadSkills(skillState().activeTab);
|
|
99
92
|
if (searchInput) searchInput.focus();
|
|
100
93
|
});
|
|
101
94
|
}
|
|
102
95
|
|
|
103
96
|
// "/" key focuses search when modal is open
|
|
104
97
|
document.addEventListener("keydown", function (e) {
|
|
105
|
-
if (e.
|
|
98
|
+
if (e.target && (e.target.matches("input, textarea") || e.target.isContentEditable)) return;
|
|
99
|
+
if (e.key === "/" && modal && !modal.classList.contains("hidden") && skillState().currentView === "list") {
|
|
106
100
|
if (document.activeElement !== searchInput) {
|
|
107
101
|
e.preventDefault();
|
|
108
102
|
if (searchInput) searchInput.focus();
|
|
@@ -116,11 +110,11 @@ export function initSkills() {
|
|
|
116
110
|
(function (tab) {
|
|
117
111
|
tab.addEventListener("click", function () {
|
|
118
112
|
var tabName = tab.dataset.tab;
|
|
119
|
-
if (tabName === activeTab && currentView === "list" && !searchQuery) return;
|
|
120
|
-
activeTab = tabName;
|
|
121
|
-
currentView = "list";
|
|
113
|
+
if (tabName === skillState().activeTab && skillState().currentView === "list" && !skillState().searchQuery) return;
|
|
114
|
+
skillState().activeTab = tabName;
|
|
115
|
+
skillState().currentView = "list";
|
|
122
116
|
// Clear search when switching tabs
|
|
123
|
-
searchQuery = "";
|
|
117
|
+
skillState().searchQuery = "";
|
|
124
118
|
var si = document.getElementById("skills-search-input");
|
|
125
119
|
if (si) si.value = "";
|
|
126
120
|
updateSearchControls();
|
|
@@ -133,7 +127,7 @@ export function initSkills() {
|
|
|
133
127
|
// Esc key
|
|
134
128
|
document.addEventListener("keydown", function (e) {
|
|
135
129
|
if (e.key === "Escape" && modal && !modal.classList.contains("hidden")) {
|
|
136
|
-
if (currentView === "detail") {
|
|
130
|
+
if (skillState().currentView === "detail") {
|
|
137
131
|
showListView();
|
|
138
132
|
} else {
|
|
139
133
|
closeSkillsModal();
|
|
@@ -146,28 +140,23 @@ function fetchInstalledSkills() {
|
|
|
146
140
|
return fetch(basePath() + "api/installed-skills")
|
|
147
141
|
.then(function (res) { return res.json(); })
|
|
148
142
|
.then(function (data) {
|
|
149
|
-
installedSkills = data.installed || {};
|
|
143
|
+
skillState().installedSkills = data.installed || {};
|
|
150
144
|
})
|
|
151
145
|
.catch(function () {
|
|
152
|
-
installedSkills = {};
|
|
146
|
+
skillState().installedSkills = {};
|
|
153
147
|
});
|
|
154
148
|
}
|
|
155
149
|
|
|
156
|
-
function
|
|
157
|
-
return installedSkills[skillId] || null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function scopeLabel(scope) {
|
|
161
|
-
if (scope === "both") return "Global + Project";
|
|
162
|
-
if (scope === "project") return "Project";
|
|
163
|
-
return "Global";
|
|
164
|
-
}
|
|
150
|
+
function openSkillsModal() { openMcpSkillsWorkbench("skills"); }
|
|
165
151
|
|
|
166
|
-
function
|
|
152
|
+
function activateSkills() {
|
|
167
153
|
if (!modal) return;
|
|
168
|
-
|
|
154
|
+
var target = String(store.get('currentSlug')) + ':' + String(store.get('myUserId'));
|
|
155
|
+
if (skillState().loadedFor === target) return;
|
|
156
|
+
skillState().loadedFor = target;
|
|
157
|
+
updateTabUI();
|
|
169
158
|
refreshIcons(modal);
|
|
170
|
-
currentView = "list";
|
|
159
|
+
skillState().currentView = "list";
|
|
171
160
|
var backBtn = document.getElementById("skills-back-btn");
|
|
172
161
|
if (backBtn) backBtn.classList.add("hidden");
|
|
173
162
|
var tabsEl = modal.querySelector(".skills-tabs");
|
|
@@ -175,22 +164,22 @@ function openSkillsModal() {
|
|
|
175
164
|
var searchEl = document.getElementById("skills-search");
|
|
176
165
|
if (searchEl) searchEl.style.display = "";
|
|
177
166
|
var searchInput = document.getElementById("skills-search-input");
|
|
178
|
-
if (searchInput) { searchInput.value = ""; searchQuery = ""; }
|
|
179
|
-
//
|
|
167
|
+
if (searchInput) { searchInput.value = ""; skillState().searchQuery = ""; }
|
|
168
|
+
// Load the current project inventory once; installation events refresh it.
|
|
180
169
|
fetchInstalledSkills().then(function () {
|
|
181
|
-
loadSkills(activeTab);
|
|
170
|
+
loadSkills(skillState().activeTab);
|
|
182
171
|
});
|
|
183
172
|
}
|
|
184
173
|
|
|
185
174
|
function closeSkillsModal() {
|
|
186
175
|
if (!modal) return;
|
|
187
|
-
|
|
176
|
+
closeMcpSkillsWorkbench();
|
|
188
177
|
}
|
|
189
178
|
|
|
190
179
|
function updateTabUI() {
|
|
191
180
|
var tabs = modal.querySelectorAll(".skills-tab");
|
|
192
181
|
for (var i = 0; i < tabs.length; i++) {
|
|
193
|
-
if (tabs[i].dataset.tab === activeTab) {
|
|
182
|
+
if (tabs[i].dataset.tab === skillState().activeTab) {
|
|
194
183
|
tabs[i].classList.add("active");
|
|
195
184
|
} else {
|
|
196
185
|
tabs[i].classList.remove("active");
|
|
@@ -201,7 +190,7 @@ function updateTabUI() {
|
|
|
201
190
|
function loadSkills(tab) {
|
|
202
191
|
// Always ensure tabs and search are visible when loading list view
|
|
203
192
|
var tabsEl = modal.querySelector(".skills-tabs");
|
|
204
|
-
if (tabsEl && !searchQuery) tabsEl.style.display = "";
|
|
193
|
+
if (tabsEl && !skillState().searchQuery) tabsEl.style.display = "";
|
|
205
194
|
var searchEl = document.getElementById("skills-search");
|
|
206
195
|
if (searchEl) searchEl.style.display = "";
|
|
207
196
|
|
|
@@ -212,8 +201,8 @@ function loadSkills(tab) {
|
|
|
212
201
|
}
|
|
213
202
|
|
|
214
203
|
// Check cache
|
|
215
|
-
if (skillsData[tab]) {
|
|
216
|
-
renderList(skillsData[tab], tab);
|
|
204
|
+
if (skillState().skillsData[tab]) {
|
|
205
|
+
renderList(skillState().skillsData[tab], tab);
|
|
217
206
|
return;
|
|
218
207
|
}
|
|
219
208
|
|
|
@@ -223,7 +212,7 @@ function loadSkills(tab) {
|
|
|
223
212
|
.then(function (res) { return res.json(); })
|
|
224
213
|
.then(function (data) {
|
|
225
214
|
var skills = data.skills || [];
|
|
226
|
-
skillsData[tab] = skills;
|
|
215
|
+
skillState().skillsData[tab] = skills;
|
|
227
216
|
renderList(skills, tab);
|
|
228
217
|
})
|
|
229
218
|
.catch(function (err) {
|
|
@@ -237,8 +226,8 @@ function loadInstalledSkills() {
|
|
|
237
226
|
fetch(basePath() + "api/installed-skills")
|
|
238
227
|
.then(function (res) { return res.json(); })
|
|
239
228
|
.then(function (data) {
|
|
240
|
-
installedSkills = data.installed || {};
|
|
241
|
-
renderInstalledList(installedSkills);
|
|
229
|
+
skillState().installedSkills = data.installed || {};
|
|
230
|
+
renderInstalledList(skillState().installedSkills);
|
|
242
231
|
})
|
|
243
232
|
.catch(function () {
|
|
244
233
|
contentEl.innerHTML = '<div class="skills-empty">Failed to load installed skills</div>';
|
|
@@ -263,12 +252,12 @@ function renderInstalledList(installed) {
|
|
|
263
252
|
'<div class="skills-installed-item-info">' +
|
|
264
253
|
'<div class="skills-installed-item-header">' +
|
|
265
254
|
'<span class="skills-installed-item-name">' + escapeHtml(name) + '</span>' +
|
|
266
|
-
'<span class="skills-installed-badge">' + iconHtml("check") + ' ' + scopeLabel(info.scope) + '</span>' +
|
|
255
|
+
'<span class="skills-installed-badge">' + iconHtml("check") + ' ' + (info.source === "clay-builtin" ? "Built-in" : scopeLabel(info.scope)) + '</span>' +
|
|
267
256
|
'</div>' +
|
|
268
257
|
(desc ? '<div class="skills-installed-item-desc">' + escapeHtml(desc) + '</div>' : '') +
|
|
269
258
|
'</div>' +
|
|
270
259
|
'<div class="skills-installed-item-actions">' +
|
|
271
|
-
buildUninstallButtons(name, info.scope) +
|
|
260
|
+
(info.source === "clay-builtin" ? "" : buildUninstallButtons(name, info.scope)) +
|
|
272
261
|
'</div>' +
|
|
273
262
|
'</div>';
|
|
274
263
|
}
|
|
@@ -301,11 +290,11 @@ function buildUninstallButtons(skillName, scope) {
|
|
|
301
290
|
|
|
302
291
|
function loadSearchResults(q) {
|
|
303
292
|
// Stale check — if query changed since this was scheduled, skip
|
|
304
|
-
if (q !== searchQuery) return;
|
|
293
|
+
if (q !== skillState().searchQuery) return;
|
|
305
294
|
|
|
306
295
|
// Check cache
|
|
307
|
-
if (searchCache[q]) {
|
|
308
|
-
renderList(searchCache[q], "search");
|
|
296
|
+
if (skillState().searchCache[q]) {
|
|
297
|
+
renderList(skillState().searchCache[q], "search");
|
|
309
298
|
return;
|
|
310
299
|
}
|
|
311
300
|
|
|
@@ -314,13 +303,13 @@ function loadSearchResults(q) {
|
|
|
314
303
|
fetch("/api/skills/search?q=" + encodeURIComponent(q))
|
|
315
304
|
.then(function (res) { return res.json(); })
|
|
316
305
|
.then(function (data) {
|
|
317
|
-
if (q !== searchQuery) return; // stale
|
|
306
|
+
if (q !== skillState().searchQuery) return; // stale
|
|
318
307
|
var skills = data.skills || [];
|
|
319
|
-
searchCache[q] = skills;
|
|
308
|
+
skillState().searchCache[q] = skills;
|
|
320
309
|
renderList(skills, "search");
|
|
321
310
|
})
|
|
322
311
|
.catch(function (err) {
|
|
323
|
-
if (q !== searchQuery) return;
|
|
312
|
+
if (q !== skillState().searchQuery) return;
|
|
324
313
|
contentEl.innerHTML = '<div class="skills-empty">Search failed</div>';
|
|
325
314
|
});
|
|
326
315
|
}
|
|
@@ -334,7 +323,7 @@ function formatInstalls(n) {
|
|
|
334
323
|
|
|
335
324
|
function renderList(skills, tab) {
|
|
336
325
|
if (!skills.length) {
|
|
337
|
-
contentEl.innerHTML = '<div class="skills-empty">' + (searchQuery ? "No matching skills" : "No skills found") + '</div>';
|
|
326
|
+
contentEl.innerHTML = '<div class="skills-empty">' + (skillState().searchQuery ? "No matching skills" : "No skills found") + '</div>';
|
|
338
327
|
return;
|
|
339
328
|
}
|
|
340
329
|
|
|
@@ -353,7 +342,7 @@ function renderList(skills, tab) {
|
|
|
353
342
|
|
|
354
343
|
var installedBadge = "";
|
|
355
344
|
if (info) {
|
|
356
|
-
installedBadge = '<span class="skills-installed-badge">' + iconHtml("check") + ' ' + scopeLabel(info.scope) + '</span>';
|
|
345
|
+
installedBadge = '<span class="skills-installed-badge">' + iconHtml("check") + ' ' + (info.source === "clay-builtin" ? "Built-in" : scopeLabel(info.scope)) + '</span>';
|
|
357
346
|
}
|
|
358
347
|
|
|
359
348
|
html += '<div class="skills-item' + (info ? " installed" : "") + '" data-source="' + escapeHtml(s.source) + '" data-skill="' + escapeHtml(s.skillId) + '">' +
|
|
@@ -387,7 +376,7 @@ function renderList(skills, tab) {
|
|
|
387
376
|
}
|
|
388
377
|
|
|
389
378
|
function loadDetail(source, skill) {
|
|
390
|
-
currentView = "detail";
|
|
379
|
+
skillState().currentView = "detail";
|
|
391
380
|
|
|
392
381
|
// Hide tabs and search
|
|
393
382
|
var tabsEl = modal.querySelector(".skills-tabs");
|
|
@@ -410,400 +399,16 @@ function loadDetail(source, skill) {
|
|
|
410
399
|
}
|
|
411
400
|
|
|
412
401
|
function showListView() {
|
|
413
|
-
currentView = "list";
|
|
402
|
+
skillState().currentView = "list";
|
|
414
403
|
var tabsEl = modal.querySelector(".skills-tabs");
|
|
415
|
-
if (tabsEl) tabsEl.style.display = searchQuery ? "none" : "";
|
|
404
|
+
if (tabsEl) tabsEl.style.display = skillState().searchQuery ? "none" : "";
|
|
416
405
|
var searchEl = document.getElementById("skills-search");
|
|
417
406
|
if (searchEl) searchEl.style.display = "";
|
|
418
407
|
var backBtn = document.getElementById("skills-back-btn");
|
|
419
408
|
if (backBtn) backBtn.classList.add("hidden");
|
|
420
|
-
if (searchQuery) {
|
|
421
|
-
loadSearchResults(searchQuery);
|
|
409
|
+
if (skillState().searchQuery) {
|
|
410
|
+
loadSearchResults(skillState().searchQuery);
|
|
422
411
|
} else {
|
|
423
|
-
loadSkills(activeTab);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function renderDetail(data) {
|
|
428
|
-
var name = data.name || data._skill || "Unknown";
|
|
429
|
-
var desc = data.description || "";
|
|
430
|
-
var cmd = data.command || "npx skills add https://github.com/" + data._source + " --skill " + data._skill;
|
|
431
|
-
var skillId = data._skill || name;
|
|
432
|
-
var info = getInstalledInfo(skillId);
|
|
433
|
-
|
|
434
|
-
// Show back button in header
|
|
435
|
-
var backBtn = document.getElementById("skills-back-btn");
|
|
436
|
-
if (backBtn) backBtn.classList.remove("hidden");
|
|
437
|
-
|
|
438
|
-
var html = '<div class="skills-detail">';
|
|
439
|
-
|
|
440
|
-
// --- Main content (left) ---
|
|
441
|
-
html += '<div class="skills-detail-main">';
|
|
442
|
-
html += '<div class="skills-detail-name">' + escapeHtml(name) + '</div>';
|
|
443
|
-
|
|
444
|
-
if (desc) {
|
|
445
|
-
html += '<div class="skills-detail-desc">' + escapeHtml(desc) + '</div>';
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
html += '<div class="skills-detail-cmd">' +
|
|
449
|
-
'<code>' + escapeHtml(cmd) + '</code>' +
|
|
450
|
-
'<button class="skills-copy-btn" data-cmd="' + escapeHtml(cmd) + '">' + iconHtml("copy") + '</button>' +
|
|
451
|
-
'</div>';
|
|
452
|
-
|
|
453
|
-
// SKILL.md content (already rendered HTML from skills.sh)
|
|
454
|
-
if (data.skillMd) {
|
|
455
|
-
html += '<div class="skills-detail-md-wrap">' +
|
|
456
|
-
'<div class="skills-detail-section-title">SKILL.md</div>' +
|
|
457
|
-
'<div class="skills-detail-md">' + sanitizeSkillHtml(data.skillMd) + '</div>' +
|
|
458
|
-
'</div>';
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
html += '</div>'; // end main
|
|
462
|
-
|
|
463
|
-
// --- Sidebar (right) ---
|
|
464
|
-
html += '<div class="skills-detail-sidebar">';
|
|
465
|
-
|
|
466
|
-
// Weekly installs
|
|
467
|
-
if (data.weeklyInstalls) {
|
|
468
|
-
html += '<div class="skills-meta-block">' +
|
|
469
|
-
'<div class="skills-meta-label">Weekly Installs</div>' +
|
|
470
|
-
'<div class="skills-meta-value">' + escapeHtml(data.weeklyInstalls) + '</div>' +
|
|
471
|
-
'</div>';
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Repository
|
|
475
|
-
if (data.repository || data._source) {
|
|
476
|
-
var repo = data.repository || data._source;
|
|
477
|
-
html += '<div class="skills-meta-block">' +
|
|
478
|
-
'<div class="skills-meta-label">Repository</div>' +
|
|
479
|
-
'<div class="skills-meta-value small">' +
|
|
480
|
-
'<div class="skills-meta-repo">' +
|
|
481
|
-
iconHtml("external-link") +
|
|
482
|
-
'<a href="https://github.com/' + escapeHtml(repo) + '" target="_blank" rel="noopener">' + escapeHtml(repo) + '</a>' +
|
|
483
|
-
'</div>' +
|
|
484
|
-
'</div>' +
|
|
485
|
-
'</div>';
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// GitHub stars
|
|
489
|
-
if (data.githubStars) {
|
|
490
|
-
html += '<div class="skills-meta-block">' +
|
|
491
|
-
'<div class="skills-meta-label">GitHub Stars</div>' +
|
|
492
|
-
'<div class="skills-meta-value">' + escapeHtml(data.githubStars) + '</div>' +
|
|
493
|
-
'</div>';
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// First seen
|
|
497
|
-
if (data.firstSeen) {
|
|
498
|
-
html += '<div class="skills-meta-block">' +
|
|
499
|
-
'<div class="skills-meta-label">First Seen</div>' +
|
|
500
|
-
'<div class="skills-meta-value small">' + escapeHtml(data.firstSeen) + '</div>' +
|
|
501
|
-
'</div>';
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Security audits
|
|
505
|
-
if (data.audits && data.audits.length) {
|
|
506
|
-
html += '<div class="skills-meta-block">' +
|
|
507
|
-
'<div class="skills-meta-label">Security Audits</div>' +
|
|
508
|
-
'<div class="skills-audit-list">';
|
|
509
|
-
for (var a = 0; a < data.audits.length; a++) {
|
|
510
|
-
var audit = data.audits[a];
|
|
511
|
-
html += '<div class="skills-audit-item">' +
|
|
512
|
-
'<span>' + escapeHtml(audit.name) + '</span>' +
|
|
513
|
-
'<span class="skills-audit-badge ' + escapeHtml(audit.status) + '">' + escapeHtml(audit.status.toUpperCase()) + '</span>' +
|
|
514
|
-
'</div>';
|
|
515
|
-
}
|
|
516
|
-
html += '</div></div>';
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Installed on
|
|
520
|
-
if (data.installedOn && data.installedOn.length) {
|
|
521
|
-
html += '<div class="skills-meta-block">' +
|
|
522
|
-
'<div class="skills-meta-label">Installed On</div>' +
|
|
523
|
-
'<div class="skills-platform-list">';
|
|
524
|
-
for (var p = 0; p < data.installedOn.length; p++) {
|
|
525
|
-
var plat = data.installedOn[p];
|
|
526
|
-
html += '<div class="skills-platform-item">' +
|
|
527
|
-
'<span>' + escapeHtml(plat.name) + '</span>' +
|
|
528
|
-
'<span class="skills-platform-count">' + escapeHtml(plat.installs) + '</span>' +
|
|
529
|
-
'</div>';
|
|
530
|
-
}
|
|
531
|
-
html += '</div></div>';
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Install buttons or installed status
|
|
535
|
-
html += '<div class="skills-meta-block">' +
|
|
536
|
-
buildInstallButtonsHtml(skillId, data._source, data._skill) +
|
|
537
|
-
'</div>';
|
|
538
|
-
|
|
539
|
-
html += '</div>'; // end sidebar
|
|
540
|
-
html += '</div>'; // end detail
|
|
541
|
-
|
|
542
|
-
contentEl.innerHTML = html;
|
|
543
|
-
refreshIcons(contentEl);
|
|
544
|
-
|
|
545
|
-
// Copy button handler
|
|
546
|
-
var copyBtn = contentEl.querySelector(".skills-copy-btn");
|
|
547
|
-
if (copyBtn) {
|
|
548
|
-
copyBtn.addEventListener("click", function (e) {
|
|
549
|
-
e.stopPropagation();
|
|
550
|
-
var cmdText = copyBtn.dataset.cmd;
|
|
551
|
-
copyToClipboard(cmdText);
|
|
552
|
-
copyBtn.classList.add("copied");
|
|
553
|
-
setTimeout(function () { copyBtn.classList.remove("copied"); }, 1500);
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// Install button handlers
|
|
558
|
-
var installActions = contentEl.querySelector(".skills-install-actions");
|
|
559
|
-
if (installActions) {
|
|
560
|
-
attachInstallHandlers(installActions, data._source, data._skill);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function installSkill(source, skill, scope) {
|
|
565
|
-
var url = "https://github.com/" + source;
|
|
566
|
-
|
|
567
|
-
// Set all install buttons in the detail view to installing state
|
|
568
|
-
var btns = contentEl.querySelectorAll(".skills-install-btn:not(.installed-state)");
|
|
569
|
-
for (var i = 0; i < btns.length; i++) {
|
|
570
|
-
btns[i].disabled = true;
|
|
571
|
-
btns[i].classList.add("installing");
|
|
572
|
-
btns[i].innerHTML = '<div class="skills-btn-spinner"></div> Installing...';
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
fetch(basePath() + "api/install-skill", {
|
|
576
|
-
method: "POST",
|
|
577
|
-
headers: { "Content-Type": "application/json" },
|
|
578
|
-
body: JSON.stringify({ url: url, skill: skill, scope: scope }),
|
|
579
|
-
}).catch(function () {
|
|
580
|
-
// Re-enable buttons on fetch error
|
|
581
|
-
for (var j = 0; j < btns.length; j++) {
|
|
582
|
-
btns[j].disabled = false;
|
|
583
|
-
btns[j].classList.remove("installing");
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
export function handleSkillInstalled(msg) {
|
|
589
|
-
var skill = msg.skill;
|
|
590
|
-
var scope = msg.scope;
|
|
591
|
-
var success = msg.success;
|
|
592
|
-
|
|
593
|
-
if (success) {
|
|
594
|
-
// Update installedSkills cache
|
|
595
|
-
var existing = installedSkills[skill];
|
|
596
|
-
if (existing) {
|
|
597
|
-
if (existing.scope !== scope && existing.scope !== "both") {
|
|
598
|
-
existing.scope = "both";
|
|
599
|
-
}
|
|
600
|
-
} else {
|
|
601
|
-
installedSkills[skill] = { scope: scope };
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Invalidate skills data cache so list refreshes with updated badges
|
|
605
|
-
skillsData = {};
|
|
606
|
-
|
|
607
|
-
// If we're on the detail view for this skill, re-render it
|
|
608
|
-
if (currentView === "detail") {
|
|
609
|
-
var detailEl = contentEl.querySelector(".skills-detail");
|
|
610
|
-
if (detailEl) {
|
|
611
|
-
// Re-render the install section in the sidebar
|
|
612
|
-
updateDetailInstallButtons(skill);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// If we're on the installed tab, refresh it
|
|
617
|
-
if (activeTab === "installed" && currentView === "list") {
|
|
618
|
-
loadInstalledSkills();
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Hot-reload skills in the active session so it's usable immediately
|
|
622
|
-
reloadActiveSessionSkills();
|
|
623
|
-
} else {
|
|
624
|
-
// Show error toast
|
|
625
|
-
showToast("Failed to install " + skill + (msg.error ? ": " + msg.error : ""), "error");
|
|
626
|
-
// Re-enable buttons
|
|
627
|
-
var btns = contentEl.querySelectorAll(".skills-install-btn.installing");
|
|
628
|
-
for (var i = 0; i < btns.length; i++) {
|
|
629
|
-
btns[i].disabled = false;
|
|
630
|
-
btns[i].classList.remove("installing");
|
|
631
|
-
// We can't easily restore the original text, so just set generic labels
|
|
632
|
-
btns[i].innerHTML = iconHtml("download") + " Install";
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function uninstallSkill(skill, scope) {
|
|
638
|
-
// Set the uninstall button to loading state
|
|
639
|
-
var unBtn = contentEl.querySelector('.skills-uninstall-btn[data-scope="' + scope + '"]');
|
|
640
|
-
if (unBtn) {
|
|
641
|
-
unBtn.disabled = true;
|
|
642
|
-
unBtn.classList.add("uninstalling");
|
|
643
|
-
unBtn.innerHTML = '<div class="skills-btn-spinner small"></div>';
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
fetch(basePath() + "api/uninstall-skill", {
|
|
647
|
-
method: "POST",
|
|
648
|
-
headers: { "Content-Type": "application/json" },
|
|
649
|
-
body: JSON.stringify({ skill: skill, scope: scope }),
|
|
650
|
-
}).catch(function () {
|
|
651
|
-
if (unBtn) {
|
|
652
|
-
unBtn.disabled = false;
|
|
653
|
-
unBtn.classList.remove("uninstalling");
|
|
654
|
-
unBtn.innerHTML = iconHtml("x");
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
export function handleSkillUninstalled(msg) {
|
|
660
|
-
var skill = msg.skill;
|
|
661
|
-
var scope = msg.scope;
|
|
662
|
-
var success = msg.success;
|
|
663
|
-
|
|
664
|
-
if (success) {
|
|
665
|
-
// Update installedSkills cache
|
|
666
|
-
var existing = installedSkills[skill];
|
|
667
|
-
if (existing) {
|
|
668
|
-
if (existing.scope === "both") {
|
|
669
|
-
// Downgrade from "both" to whichever scope remains
|
|
670
|
-
existing.scope = scope === "global" ? "project" : "global";
|
|
671
|
-
} else {
|
|
672
|
-
// Fully uninstalled
|
|
673
|
-
delete installedSkills[skill];
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Invalidate skills data cache so list refreshes
|
|
678
|
-
skillsData = {};
|
|
679
|
-
|
|
680
|
-
// If we're on the detail view, re-render buttons
|
|
681
|
-
if (currentView === "detail") {
|
|
682
|
-
var detailEl = contentEl.querySelector(".skills-detail");
|
|
683
|
-
if (detailEl) {
|
|
684
|
-
updateDetailInstallButtons(skill);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// If we're on the installed tab, refresh it
|
|
689
|
-
if (activeTab === "installed" && currentView === "list") {
|
|
690
|
-
loadInstalledSkills();
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// Hot-reload skills in the active session so the change applies immediately
|
|
694
|
-
reloadActiveSessionSkills();
|
|
695
|
-
} else {
|
|
696
|
-
showToast("Failed to uninstall " + skill + (msg.error ? ": " + msg.error : ""), "error");
|
|
697
|
-
// Re-enable the uninstall button
|
|
698
|
-
var unBtn = contentEl.querySelector('.skills-uninstall-btn[data-scope="' + scope + '"]');
|
|
699
|
-
if (unBtn) {
|
|
700
|
-
unBtn.disabled = false;
|
|
701
|
-
unBtn.classList.remove("uninstalling");
|
|
702
|
-
unBtn.innerHTML = iconHtml("x");
|
|
703
|
-
refreshIcons(unBtn);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function updateDetailInstallButtons(skillId) {
|
|
709
|
-
var info = getInstalledInfo(skillId);
|
|
710
|
-
var sidebar = contentEl.querySelector(".skills-detail-sidebar");
|
|
711
|
-
if (!sidebar) return;
|
|
712
|
-
|
|
713
|
-
// Find the install actions or installed status block (last .skills-meta-block)
|
|
714
|
-
var blocks = sidebar.querySelectorAll(".skills-meta-block");
|
|
715
|
-
var lastBlock = blocks[blocks.length - 1];
|
|
716
|
-
if (!lastBlock) return;
|
|
717
|
-
|
|
718
|
-
// Get source/skill from the current detail view data attributes
|
|
719
|
-
var sourceEl = sidebar.querySelector("[data-detail-source]");
|
|
720
|
-
if (!sourceEl) sourceEl = contentEl.querySelector("[data-detail-source]");
|
|
721
|
-
var source = sourceEl ? sourceEl.dataset.detailSource : "";
|
|
722
|
-
var skill = sourceEl ? sourceEl.dataset.detailSkill : skillId;
|
|
723
|
-
|
|
724
|
-
lastBlock.innerHTML = buildInstallButtonsHtml(skillId, source, skill);
|
|
725
|
-
refreshIcons(lastBlock);
|
|
726
|
-
attachInstallHandlers(lastBlock, source, skill);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function buildInstallButtonsHtml(skillId, source, skill) {
|
|
730
|
-
var info = getInstalledInfo(skillId);
|
|
731
|
-
|
|
732
|
-
var html = '<div class="skills-install-actions" data-detail-source="' + escapeHtml(source) + '" data-detail-skill="' + escapeHtml(skill) + '">';
|
|
733
|
-
|
|
734
|
-
if (info && info.scope === "both") {
|
|
735
|
-
// Both scopes installed — show status + uninstall for each
|
|
736
|
-
html += '<div class="skills-installed-row">' +
|
|
737
|
-
'<div class="skills-installed-status compact">' +
|
|
738
|
-
iconHtml("circle-check") + ' Installed (Project)' +
|
|
739
|
-
'</div>' +
|
|
740
|
-
'<button class="skills-uninstall-btn" data-scope="project" title="Uninstall (Project)">' +
|
|
741
|
-
iconHtml("x") +
|
|
742
|
-
'</button>' +
|
|
743
|
-
'</div>';
|
|
744
|
-
html += '<div class="skills-installed-row">' +
|
|
745
|
-
'<div class="skills-installed-status compact">' +
|
|
746
|
-
iconHtml("circle-check") + ' Installed (Global)' +
|
|
747
|
-
'</div>' +
|
|
748
|
-
'<button class="skills-uninstall-btn" data-scope="global" title="Uninstall (Global)">' +
|
|
749
|
-
iconHtml("x") +
|
|
750
|
-
'</button>' +
|
|
751
|
-
'</div>';
|
|
752
|
-
} else if (info) {
|
|
753
|
-
// One scope installed — show status with uninstall + install button for other scope
|
|
754
|
-
html += '<div class="skills-installed-row">' +
|
|
755
|
-
'<div class="skills-installed-status compact">' +
|
|
756
|
-
iconHtml("circle-check") + ' Installed (' + scopeLabel(info.scope) + ')' +
|
|
757
|
-
'</div>' +
|
|
758
|
-
'<button class="skills-uninstall-btn" data-scope="' + escapeHtml(info.scope) + '" title="Uninstall (' + scopeLabel(info.scope) + ')">' +
|
|
759
|
-
iconHtml("x") +
|
|
760
|
-
'</button>' +
|
|
761
|
-
'</div>';
|
|
762
|
-
|
|
763
|
-
// Show install button for the other scope
|
|
764
|
-
var otherScope = info.scope === "global" ? "project" : "global";
|
|
765
|
-
var otherLabel = info.scope === "global" ? "Project" : "Global";
|
|
766
|
-
html += '<button class="skills-install-btn secondary" data-scope="' + otherScope + '">' +
|
|
767
|
-
iconHtml("download") + ' Install (' + otherLabel + ')' +
|
|
768
|
-
'</button>';
|
|
769
|
-
} else {
|
|
770
|
-
// Not installed — show both install buttons
|
|
771
|
-
html += '<button class="skills-install-btn" data-scope="project">' +
|
|
772
|
-
iconHtml("download") + ' Install (Project)' +
|
|
773
|
-
'</button>';
|
|
774
|
-
html += '<button class="skills-install-btn secondary" data-scope="global">' +
|
|
775
|
-
iconHtml("download") + ' Install (Global)' +
|
|
776
|
-
'</button>';
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
html += '</div>';
|
|
780
|
-
return html;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
function attachInstallHandlers(container, source, skill) {
|
|
784
|
-
var btns = container.querySelectorAll(".skills-install-btn");
|
|
785
|
-
for (var i = 0; i < btns.length; i++) {
|
|
786
|
-
(function (btn) {
|
|
787
|
-
btn.addEventListener("click", function () {
|
|
788
|
-
var scope = btn.dataset.scope;
|
|
789
|
-
installSkill(source, skill, scope);
|
|
790
|
-
});
|
|
791
|
-
})(btns[i]);
|
|
792
|
-
}
|
|
793
|
-
var unBtns = container.querySelectorAll(".skills-uninstall-btn");
|
|
794
|
-
for (var j = 0; j < unBtns.length; j++) {
|
|
795
|
-
(function (btn) {
|
|
796
|
-
btn.addEventListener("click", function () {
|
|
797
|
-
var scope = btn.dataset.scope;
|
|
798
|
-
uninstallSkill(skill, scope);
|
|
799
|
-
});
|
|
800
|
-
})(unBtns[j]);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
function sanitizeSkillHtml(rawHtml) {
|
|
805
|
-
if (typeof DOMPurify !== "undefined") {
|
|
806
|
-
return DOMPurify.sanitize(rawHtml);
|
|
412
|
+
loadSkills(skillState().activeTab);
|
|
807
413
|
}
|
|
808
|
-
return rawHtml;
|
|
809
414
|
}
|