clay-server 4.8.0-beta.1 → 4.8.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 (44) hide show
  1. package/lib/bundled-skills/clay-agent-browser/SKILL.md +36 -0
  2. package/lib/clay-skill-install.js +33 -0
  3. package/lib/clay-skill-storage.js +49 -0
  4. package/lib/project-http.js +14 -5
  5. package/lib/project-shared-browser.js +256 -0
  6. package/lib/project.js +35 -1
  7. package/lib/public/app.js +7 -0
  8. package/lib/public/css/filebrowser.css +2 -25
  9. package/lib/public/css/issues.css +1 -14
  10. package/lib/public/css/mcp-skills-workbench.css +51 -0
  11. package/lib/public/css/project-logs.css +0 -11
  12. package/lib/public/css/right-workbench.css +28 -0
  13. package/lib/public/css/scheduled-tasks.css +1 -1
  14. package/lib/public/css/shared-browser-skins.css +28 -0
  15. package/lib/public/css/shared-browser.css +91 -0
  16. package/lib/public/css/sticky-notes.css +0 -11
  17. package/lib/public/index.html +30 -22
  18. package/lib/public/modules/app-messages.js +2 -0
  19. package/lib/public/modules/extension-settings.js +110 -0
  20. package/lib/public/modules/mcp-skills-workbench.js +67 -0
  21. package/lib/public/modules/mcp-ui.js +9 -12
  22. package/lib/public/modules/notifications.js +2 -107
  23. package/lib/public/modules/shared-browser-activity.js +66 -0
  24. package/lib/public/modules/shared-browser-input.js +73 -0
  25. package/lib/public/modules/shared-browser-tabs.js +33 -0
  26. package/lib/public/modules/shared-browser-viewport-menu.js +76 -0
  27. package/lib/public/modules/shared-browser.js +214 -0
  28. package/lib/public/modules/sidebar-mobile.js +3 -3
  29. package/lib/public/modules/skills-actions.js +247 -0
  30. package/lib/public/modules/skills-detail.js +148 -0
  31. package/lib/public/modules/skills-state.js +10 -0
  32. package/lib/public/modules/skills.js +64 -459
  33. package/lib/public/modules/tool-palette-order.js +11 -13
  34. package/lib/public/style.css +6 -0
  35. package/lib/sdk-bridge.js +14 -1
  36. package/lib/sessions.js +2 -0
  37. package/lib/shared-browser-activity-history.js +17 -0
  38. package/lib/shared-browser-policy.js +26 -0
  39. package/lib/shared-browser-runtime.js +82 -0
  40. package/lib/shared-browser-tools.js +21 -0
  41. package/lib/shared-browser-worker.js +160 -0
  42. package/lib/ws-schema.js +10 -0
  43. package/lib/yoke/skill-discovery.js +22 -2
  44. 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, copyToClipboard, showToast } from './utils.js';
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.key === "/" && modal && !modal.classList.contains("hidden") && currentView === "list") {
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 getInstalledInfo(skillId) {
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 openSkillsModal() {
152
+ function activateSkills() {
167
153
  if (!modal) return;
168
- modal.classList.remove("hidden");
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
- // Always refresh installed status on open
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
- modal.classList.add("hidden");
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
  }