clay-server 4.8.2 → 5.0.0-beta.1

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 (82) hide show
  1. package/LICENSES/README.md +13 -0
  2. package/LICENSES/open-iconic.txt +21 -0
  3. package/LICENSES/plantuml-salt.txt +674 -0
  4. package/lib/assets/salt-assets.json +15 -0
  5. package/lib/assets/salt-colors.json +156 -0
  6. package/lib/assets/salt-font-10.bin.gz +0 -0
  7. package/lib/assets/salt-font-13.bin.gz +0 -0
  8. package/lib/assets/salt-font-14.bin.gz +0 -0
  9. package/lib/assets/salt-font-16.bin.gz +0 -0
  10. package/lib/assets/salt-font-18.bin.gz +0 -0
  11. package/lib/assets/salt-font-20.bin.gz +0 -0
  12. package/lib/assets/salt-font-metrics.bin.gz +0 -0
  13. package/lib/assets/salt-font-metrics.json +101 -0
  14. package/lib/assets/salt-font-monospace-10.bin.gz +0 -0
  15. package/lib/assets/salt-font-monospace-13.bin.gz +0 -0
  16. package/lib/assets/salt-font-monospace-14.bin.gz +0 -0
  17. package/lib/assets/salt-font-monospace-16.bin.gz +0 -0
  18. package/lib/assets/salt-font-monospace-18.bin.gz +0 -0
  19. package/lib/assets/salt-font-monospace-20.bin.gz +0 -0
  20. package/lib/assets/salt-font-monospace.bin.gz +0 -0
  21. package/lib/assets/salt-icons.json +671 -0
  22. package/lib/bundled-skills/clay-sketch/SKILL.md +198 -0
  23. package/lib/mcp-browser-proxy.js +37 -0
  24. package/lib/mcp-connection-store.js +39 -0
  25. package/lib/mcp-network.js +46 -0
  26. package/lib/mcp-oauth.js +65 -0
  27. package/lib/mcp-proxy.js +32 -0
  28. package/lib/mcp-remote-connections.js +215 -0
  29. package/lib/mcp-session-access.js +15 -0
  30. package/lib/plantuml-renderer.js +43 -0
  31. package/lib/project-file-http.js +2 -0
  32. package/lib/project-mcp.js +130 -357
  33. package/lib/project-plantuml.js +48 -0
  34. package/lib/project-session-document.js +17 -1
  35. package/lib/project-user-message.js +2 -0
  36. package/lib/project-wireframe-http.js +88 -0
  37. package/lib/project.js +28 -45
  38. package/lib/public/app.js +7 -0
  39. package/lib/public/css/filebrowser.css +5 -0
  40. package/lib/public/css/mcp-connections.css +86 -0
  41. package/lib/public/css/skills-workbench.css +74 -0
  42. package/lib/public/css/wireframe.css +95 -0
  43. package/lib/public/index.html +16 -15
  44. package/lib/public/modules/app-messages.js +10 -2
  45. package/lib/public/modules/filebrowser.js +29 -3
  46. package/lib/public/modules/markdown.js +3 -1
  47. package/lib/public/modules/mcp-connection-view.js +136 -0
  48. package/lib/public/modules/mcp-permissions.js +122 -0
  49. package/lib/public/modules/mcp-ui.js +85 -461
  50. package/lib/public/modules/plantuml-preview.js +76 -0
  51. package/lib/public/modules/skills-actions.js +47 -79
  52. package/lib/public/modules/skills-detail.js +32 -145
  53. package/lib/public/modules/skills-list-view.js +29 -0
  54. package/lib/public/modules/skills-state.js +4 -2
  55. package/lib/public/modules/skills.js +104 -399
  56. package/lib/public/modules/split-view.js +14 -0
  57. package/lib/public/modules/vendor-login-browser.js +16 -5
  58. package/lib/public/modules/wireframe-chat.js +52 -0
  59. package/lib/public/modules/wireframe-render.js +69 -0
  60. package/lib/public/modules/wireframe-style.js +6 -0
  61. package/lib/public/modules/wireframe-viewport.js +70 -0
  62. package/lib/public/modules/wireframe-workbench.js +116 -0
  63. package/lib/public/style.css +3 -0
  64. package/lib/salt-chrome.js +36 -0
  65. package/lib/salt-hand.js +37 -0
  66. package/lib/salt-images.js +87 -0
  67. package/lib/salt-layout.js +60 -0
  68. package/lib/salt-options.js +23 -0
  69. package/lib/salt-overlays.js +45 -0
  70. package/lib/salt-parser.js +153 -0
  71. package/lib/salt-screen.js +111 -0
  72. package/lib/salt-svg-shapes.js +80 -0
  73. package/lib/salt-svg.js +164 -0
  74. package/lib/salt-text.js +112 -0
  75. package/lib/salt-tree.js +45 -0
  76. package/lib/sdk-bridge.js +5 -17
  77. package/lib/sdk-mcp-descriptors.js +2 -1
  78. package/lib/session-document-mcp-server.js +9 -0
  79. package/lib/vendor-login-browser-worker.js +18 -8
  80. package/lib/vendor-login-browser.js +10 -3
  81. package/lib/yoke/adapters/codex.js +1 -18
  82. package/package.json +6 -2
@@ -1,7 +1,7 @@
1
1
  import { iconHtml, refreshIcons } from './icons.js';
2
- import { escapeHtml, copyToClipboard, showToast } from './utils.js';
2
+ import { escapeHtml, showToast } from './utils.js';
3
3
  import { store } from './store.js';
4
- import { skillState, content, basePath, getInstalledInfo, scopeLabel } from './skills-state.js';
4
+ import { skillState, content, basePath, getInstalledInfo, scopeLabel, patchSkills } from './skills-state.js';
5
5
  import { getWs } from './ws-ref.js';
6
6
  function reloadActiveSessionSkills() { var ws = getWs(); if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'reload_skills' })); }
7
7
  function installSkill(source, skill, scope) {
@@ -19,11 +19,13 @@ function installSkill(source, skill, scope) {
19
19
  method: "POST",
20
20
  headers: { "Content-Type": "application/json" },
21
21
  body: JSON.stringify({ url: url, skill: skill, scope: scope }),
22
- }).catch(function () {
22
+ }).then(function (res) { if (!res.ok) throw new Error('Request rejected'); }).catch(function () {
23
+ showToast('Could not start installation. Try again.', 'error');
23
24
  // Re-enable buttons on fetch error
24
25
  for (var j = 0; j < btns.length; j++) {
25
26
  btns[j].disabled = false;
26
27
  btns[j].classList.remove("installing");
28
+ btns[j].textContent = "Add to " + scopeLabel(btns[j].dataset.scope).toLowerCase();
27
29
  }
28
30
  });
29
31
  }
@@ -45,10 +47,10 @@ export function handleSkillInstalled(msg) {
45
47
  }
46
48
 
47
49
  // Invalidate skills data cache so list refreshes with updated badges
48
- skillState().skillsData = {};
50
+ patchSkills({ skillsData: {}, searchCache: {}, inventoryLoaded: false });
49
51
 
50
52
  // If we're on the detail view for this skill, re-render it
51
- if (skillState().currentView === "detail") {
53
+ if (skillState().currentView === "detail" && skillState().selectedSkill === skill) {
52
54
  var detailEl = content().querySelector(".skills-detail");
53
55
  if (detailEl) {
54
56
  // Re-render the install section in the sidebar
@@ -67,12 +69,13 @@ export function handleSkillInstalled(msg) {
67
69
  // Show error toast
68
70
  showToast("Failed to install " + skill + (msg.error ? ": " + msg.error : ""), "error");
69
71
  // Re-enable buttons
72
+ if (skillState().selectedSkill !== skill) return;
70
73
  var btns = content().querySelectorAll(".skills-install-btn.installing");
71
74
  for (var i = 0; i < btns.length; i++) {
72
75
  btns[i].disabled = false;
73
76
  btns[i].classList.remove("installing");
74
77
  // We can't easily restore the original text, so just set generic labels
75
- btns[i].innerHTML = iconHtml("download") + " Install";
78
+ btns[i].innerHTML = iconHtml("plus") + " Add to " + scopeLabel(btns[i].dataset.scope).toLowerCase();
76
79
  }
77
80
  }
78
81
  }
@@ -90,11 +93,12 @@ export function uninstallSkill(skill, scope) {
90
93
  method: "POST",
91
94
  headers: { "Content-Type": "application/json" },
92
95
  body: JSON.stringify({ skill: skill, scope: scope }),
93
- }).catch(function () {
96
+ }).then(function (res) { if (!res.ok) throw new Error('Request rejected'); }).catch(function () {
97
+ showToast('Could not remove this skill. Try again.', 'error');
94
98
  if (unBtn) {
95
99
  unBtn.disabled = false;
96
100
  unBtn.classList.remove("uninstalling");
97
- unBtn.innerHTML = iconHtml("x");
101
+ unBtn.textContent = "Remove";
98
102
  }
99
103
  });
100
104
  }
@@ -118,10 +122,10 @@ export function handleSkillUninstalled(msg) {
118
122
  }
119
123
 
120
124
  // Invalidate skills data cache so list refreshes
121
- skillState().skillsData = {};
125
+ patchSkills({ skillsData: {}, searchCache: {}, inventoryLoaded: false });
122
126
 
123
127
  // If we're on the detail view, re-render buttons
124
- if (skillState().currentView === "detail") {
128
+ if (skillState().currentView === "detail" && skillState().selectedSkill === skill) {
125
129
  var detailEl = content().querySelector(".skills-detail");
126
130
  if (detailEl) {
127
131
  updateDetailInstallButtons(skill);
@@ -138,90 +142,45 @@ export function handleSkillUninstalled(msg) {
138
142
  } else {
139
143
  showToast("Failed to uninstall " + skill + (msg.error ? ": " + msg.error : ""), "error");
140
144
  // Re-enable the uninstall button
145
+ if (skillState().selectedSkill !== skill) return;
141
146
  var unBtn = content().querySelector('.skills-uninstall-btn[data-scope="' + scope + '"]');
142
147
  if (unBtn) {
143
148
  unBtn.disabled = false;
144
149
  unBtn.classList.remove("uninstalling");
145
- unBtn.innerHTML = iconHtml("x");
150
+ unBtn.textContent = "Remove";
146
151
  refreshIcons(unBtn);
147
152
  }
148
153
  }
149
154
  }
150
155
 
151
156
  function updateDetailInstallButtons(skillId) {
157
+ if (skillState().selectedSkill !== skillId) return;
158
+ var data = skillState().detailData || {}, block = content().querySelector('[data-skill-actions]');
159
+ var note = content().querySelector('.skills-local-note');
160
+ if (note) note.hidden = !getInstalledInfo(skillId);
161
+ if (!block) return;
162
+ block.innerHTML = data._installed ? buildLocalActionsHtml(skillId) : buildInstallButtonsHtml(skillId, data._source || '', skillId);
163
+ refreshIcons(); attachInstallHandlers(block, data._source || '', skillId);
164
+ }
165
+
166
+ export function buildLocalActionsHtml(skillId) {
152
167
  var info = getInstalledInfo(skillId);
153
- var sidebar = content().querySelector(".skills-detail-sidebar");
154
- if (!sidebar) return;
155
-
156
- // Find the install actions or installed status block (last .skills-meta-block)
157
- var blocks = sidebar.querySelectorAll(".skills-meta-block");
158
- var lastBlock = blocks[blocks.length - 1];
159
- if (!lastBlock) return;
160
-
161
- // Get source/skill from the current detail view data attributes
162
- var sourceEl = sidebar.querySelector("[data-detail-source]");
163
- if (!sourceEl) sourceEl = content().querySelector("[data-detail-source]");
164
- var source = sourceEl ? sourceEl.dataset.detailSource : "";
165
- var skill = sourceEl ? sourceEl.dataset.detailSkill : skillId;
166
-
167
- lastBlock.innerHTML = buildInstallButtonsHtml(skillId, source, skill);
168
- refreshIcons(lastBlock);
169
- attachInstallHandlers(lastBlock, source, skill);
168
+ if (!info) return '<p class="skills-removed-status">Removed from your library.</p>';
169
+ if (info.source === 'clay-builtin') return '<div class="skills-installed-status">' + iconHtml('circle-check') + ' Built in to Clay</div>';
170
+ var scopes = info.scope === 'both' ? ['project', 'global'] : [info.scope];
171
+ return scopes.map(function (scope) { return '<div class="skills-installed-row"><span class="skills-installed-status compact">' + iconHtml('circle-check') + ' ' + scopeLabel(scope) + '</span><button type="button" class="skills-uninstall-btn" data-scope="' + scope + '" aria-label="Remove from ' + scopeLabel(scope) + '">Remove</button></div>'; }).join('');
170
172
  }
171
173
 
172
174
  export function buildInstallButtonsHtml(skillId, source, skill) {
173
175
  var info = getInstalledInfo(skillId);
174
- if (info && info.source === "clay-builtin") return '<div class="skills-installed-status">' + iconHtml("circle-check") + ' Built in to Clay</div>';
175
-
176
+ if (info && info.source === 'clay-builtin') return buildLocalActionsHtml(skillId);
176
177
  var html = '<div class="skills-install-actions" data-detail-source="' + escapeHtml(source) + '" data-detail-skill="' + escapeHtml(skill) + '">';
177
-
178
- if (info && info.scope === "both") {
179
- // Both scopes installed — show status + uninstall for each
180
- html += '<div class="skills-installed-row">' +
181
- '<div class="skills-installed-status compact">' +
182
- iconHtml("circle-check") + ' Installed (Project)' +
183
- '</div>' +
184
- '<button class="skills-uninstall-btn" data-scope="project" title="Uninstall (Project)">' +
185
- iconHtml("x") +
186
- '</button>' +
187
- '</div>';
188
- html += '<div class="skills-installed-row">' +
189
- '<div class="skills-installed-status compact">' +
190
- iconHtml("circle-check") + ' Installed (Global)' +
191
- '</div>' +
192
- '<button class="skills-uninstall-btn" data-scope="global" title="Uninstall (Global)">' +
193
- iconHtml("x") +
194
- '</button>' +
195
- '</div>';
196
- } else if (info) {
197
- // One scope installed — show status with uninstall + install button for other scope
198
- html += '<div class="skills-installed-row">' +
199
- '<div class="skills-installed-status compact">' +
200
- iconHtml("circle-check") + ' Installed (' + scopeLabel(info.scope) + ')' +
201
- '</div>' +
202
- '<button class="skills-uninstall-btn" data-scope="' + escapeHtml(info.scope) + '" title="Uninstall (' + scopeLabel(info.scope) + ')">' +
203
- iconHtml("x") +
204
- '</button>' +
205
- '</div>';
206
-
207
- // Show install button for the other scope
208
- var otherScope = info.scope === "global" ? "project" : "global";
209
- var otherLabel = info.scope === "global" ? "Project" : "Global";
210
- html += '<button class="skills-install-btn secondary" data-scope="' + otherScope + '">' +
211
- iconHtml("download") + ' Install (' + otherLabel + ')' +
212
- '</button>';
213
- } else {
214
- // Not installed — show both install buttons
215
- html += '<button class="skills-install-btn" data-scope="project">' +
216
- iconHtml("download") + ' Install (Project)' +
217
- '</button>';
218
- html += '<button class="skills-install-btn secondary" data-scope="global">' +
219
- iconHtml("download") + ' Install (Global)' +
220
- '</button>';
221
- }
222
-
223
- html += '</div>';
224
- return html;
178
+ if (info) html += buildLocalActionsHtml(skillId);
179
+ ['project', 'global'].forEach(function (scope) {
180
+ if (info && (info.scope === scope || info.scope === 'both')) return;
181
+ html += '<button type="button" class="skills-install-btn' + (scope === 'global' ? ' secondary' : '') + '" data-scope="' + scope + '">' + iconHtml('plus') + ' Add to ' + scopeLabel(scope).toLowerCase() + '</button>';
182
+ });
183
+ return html + '</div>';
225
184
  }
226
185
 
227
186
  export function attachInstallHandlers(container, source, skill) {
@@ -239,7 +198,16 @@ export function attachInstallHandlers(container, source, skill) {
239
198
  (function (btn) {
240
199
  btn.addEventListener("click", function () {
241
200
  var scope = btn.dataset.scope;
242
- uninstallSkill(skill, scope);
201
+ if (btn.dataset.confirmed === 'true') {
202
+ btn.dataset.confirmed = '';
203
+ var existingCancel = btn.parentElement.querySelector('.skills-cancel-remove');
204
+ if (existingCancel) existingCancel.remove();
205
+ uninstallSkill(skill, scope); return;
206
+ }
207
+ btn.dataset.confirmed = 'true'; btn.textContent = 'Confirm removal';
208
+ var cancel = document.createElement('button'); cancel.type = 'button'; cancel.className = 'skills-cancel-remove'; cancel.textContent = 'Cancel';
209
+ cancel.onclick = function () { btn.dataset.confirmed = ''; btn.textContent = 'Remove'; cancel.remove(); };
210
+ btn.after(cancel);
243
211
  });
244
212
  })(unBtns[j]);
245
213
  }
@@ -1,148 +1,35 @@
1
1
  import { iconHtml, refreshIcons } from './icons.js';
2
- import { escapeHtml, copyToClipboard, showToast } from './utils.js';
3
- import { store } from './store.js';
4
- import { skillState, content, basePath, getInstalledInfo, scopeLabel } from './skills-state.js';
5
- import { buildInstallButtonsHtml, attachInstallHandlers } from './skills-actions.js';
2
+ import { escapeHtml, copyToClipboard } from './utils.js';
3
+ import { skillState, content, getInstalledInfo, scopeLabel, patchSkills } from './skills-state.js';
4
+ import { buildInstallButtonsHtml, buildLocalActionsHtml, attachInstallHandlers } from './skills-actions.js';
5
+ function text(value) { return escapeHtml(String(value || '')); }
6
6
  export function renderDetail(data) {
7
- var name = data.name || data._skill || "Unknown";
8
- var desc = data.description || "";
9
- var cmd = data.command || "npx skills add https://github.com/" + data._source + " --skill " + data._skill;
10
- var skillId = data._skill || name;
11
- var info = getInstalledInfo(skillId);
12
-
13
- // Show back button in header
14
- var backBtn = document.getElementById("skills-back-btn");
15
- if (backBtn) backBtn.classList.remove("hidden");
16
-
17
- var html = '<div class="skills-detail">';
18
-
19
- // --- Main content (left) ---
20
- html += '<div class="skills-detail-main">';
21
- html += '<div class="skills-detail-name">' + escapeHtml(name) + '</div>';
22
-
23
- if (desc) {
24
- html += '<div class="skills-detail-desc">' + escapeHtml(desc) + '</div>';
25
- }
26
-
27
- html += '<div class="skills-detail-cmd">' +
28
- '<code>' + escapeHtml(cmd) + '</code>' +
29
- '<button class="skills-copy-btn" data-cmd="' + escapeHtml(cmd) + '">' + iconHtml("copy") + '</button>' +
30
- '</div>';
31
-
32
- // SKILL.md content (already rendered HTML from skills.sh)
33
- if (data.skillMd) {
34
- html += '<div class="skills-detail-md-wrap">' +
35
- '<div class="skills-detail-section-title">SKILL.md</div>' +
36
- '<div class="skills-detail-md">' + sanitizeSkillHtml(data.skillMd) + '</div>' +
37
- '</div>';
38
- }
39
-
40
- html += '</div>'; // end main
41
-
42
- // --- Sidebar (right) ---
43
- html += '<div class="skills-detail-sidebar">';
44
-
45
- // Weekly installs
46
- if (data.weeklyInstalls) {
47
- html += '<div class="skills-meta-block">' +
48
- '<div class="skills-meta-label">Weekly Installs</div>' +
49
- '<div class="skills-meta-value">' + escapeHtml(data.weeklyInstalls) + '</div>' +
50
- '</div>';
51
- }
52
-
53
- // Repository
54
- if (data.repository || data._source) {
55
- var repo = data.repository || data._source;
56
- html += '<div class="skills-meta-block">' +
57
- '<div class="skills-meta-label">Repository</div>' +
58
- '<div class="skills-meta-value small">' +
59
- '<div class="skills-meta-repo">' +
60
- iconHtml("external-link") +
61
- '<a href="https://github.com/' + escapeHtml(repo) + '" target="_blank" rel="noopener">' + escapeHtml(repo) + '</a>' +
62
- '</div>' +
63
- '</div>' +
64
- '</div>';
65
- }
66
-
67
- // GitHub stars
68
- if (data.githubStars) {
69
- html += '<div class="skills-meta-block">' +
70
- '<div class="skills-meta-label">GitHub Stars</div>' +
71
- '<div class="skills-meta-value">' + escapeHtml(data.githubStars) + '</div>' +
72
- '</div>';
73
- }
74
-
75
- // First seen
76
- if (data.firstSeen) {
77
- html += '<div class="skills-meta-block">' +
78
- '<div class="skills-meta-label">First Seen</div>' +
79
- '<div class="skills-meta-value small">' + escapeHtml(data.firstSeen) + '</div>' +
80
- '</div>';
81
- }
82
-
83
- // Security audits
84
- if (data.audits && data.audits.length) {
85
- html += '<div class="skills-meta-block">' +
86
- '<div class="skills-meta-label">Security Audits</div>' +
87
- '<div class="skills-audit-list">';
88
- for (var a = 0; a < data.audits.length; a++) {
89
- var audit = data.audits[a];
90
- html += '<div class="skills-audit-item">' +
91
- '<span>' + escapeHtml(audit.name) + '</span>' +
92
- '<span class="skills-audit-badge ' + escapeHtml(audit.status) + '">' + escapeHtml(audit.status.toUpperCase()) + '</span>' +
93
- '</div>';
94
- }
95
- html += '</div></div>';
96
- }
97
-
98
- // Installed on
99
- if (data.installedOn && data.installedOn.length) {
100
- html += '<div class="skills-meta-block">' +
101
- '<div class="skills-meta-label">Installed On</div>' +
102
- '<div class="skills-platform-list">';
103
- for (var p = 0; p < data.installedOn.length; p++) {
104
- var plat = data.installedOn[p];
105
- html += '<div class="skills-platform-item">' +
106
- '<span>' + escapeHtml(plat.name) + '</span>' +
107
- '<span class="skills-platform-count">' + escapeHtml(plat.installs) + '</span>' +
108
- '</div>';
109
- }
110
- html += '</div></div>';
111
- }
112
-
113
- // Install buttons or installed status
114
- html += '<div class="skills-meta-block">' +
115
- buildInstallButtonsHtml(skillId, data._source, data._skill) +
116
- '</div>';
117
-
118
- html += '</div>'; // end sidebar
119
- html += '</div>'; // end detail
120
-
121
- content().innerHTML = html;
122
- refreshIcons(content());
123
-
124
- // Copy button handler
125
- var copyBtn = content().querySelector(".skills-copy-btn");
126
- if (copyBtn) {
127
- copyBtn.addEventListener("click", function (e) {
128
- e.stopPropagation();
129
- var cmdText = copyBtn.dataset.cmd;
130
- copyToClipboard(cmdText);
131
- copyBtn.classList.add("copied");
132
- setTimeout(function () { copyBtn.classList.remove("copied"); }, 1500);
133
- });
134
- }
135
-
136
- // Install button handlers
137
- var installActions = content().querySelector(".skills-install-actions");
138
- if (installActions) {
139
- attachInstallHandlers(installActions, data._source, data._skill);
140
- }
141
- }
142
-
143
- function sanitizeSkillHtml(rawHtml) {
144
- if (typeof DOMPurify !== "undefined") {
145
- return DOMPurify.sanitize(rawHtml);
146
- }
147
- return rawHtml;
7
+ patchSkills({ detailData: data });
8
+ var name = data.name || data._skill || 'Skill', skill = data._skill || name;
9
+ var info = getInstalledInfo(skill), builtin = info && info.source === 'clay-builtin';
10
+ var source = data._source || data.repository || '';
11
+ var cmd = data.command || 'npx skills add https://github.com/' + source + ' --skill ' + skill;
12
+ var html = '<article class="skills-detail" data-local="' + !!data._installed + '"><header class="skills-detail-heading"><span class="skills-detail-icon">' + iconHtml(builtin ? 'sparkles' : 'book-open') + '</span><h2 class="skills-detail-name" tabindex="-1">' + text(name) + '</h2>';
13
+ if (data.description) html += '<p class="skills-detail-desc">' + text(data.description) + '</p>';
14
+ html += '<div class="skills-detail-byline">' + (builtin ? 'Included with Clay' : data._installed ? text(scopeLabel(info && info.scope || data.scope)) : text(source)) + (data.weeklyInstalls ? '<span>' + text(data.weeklyInstalls) + ' weekly installs</span>' : '') + '</div></header>';
15
+ html += '<section class="skills-placement"><div class="skills-placement-heading"><h3>' + (builtin ? 'Ready to use' : data._installed ? 'Availability' : 'Add to your workflow') + '</h3><p>' + (builtin ? 'This skill comes with Clay and is maintained with the app.' : 'Project skills stay with this workspace. Global skills are available across your projects.') + '</p></div><div data-skill-actions>' + (data._installed ? buildLocalActionsHtml(skill) : buildInstallButtonsHtml(skill, source, skill)) + '</div></section>';
16
+ if (data.skillMd) html += '<section class="skills-detail-md-wrap"><h3 class="skills-detail-section-title">Instructions</h3><div class="skills-detail-md">' + (typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(data.skillMd) : text(data.skillMd)) + '</div></section>';
17
+ if (data._installed && !data.skillMd) html += '<div class="skills-local-note">' + iconHtml('circle-check') + '<p>Your agent can discover this skill when it is relevant to a task. You can also ask for it by name.</p></div>';
18
+ if (source || data.path || data.version || data.audits || data.githubStars || data.firstSeen || data.installedOn) {
19
+ html += '<details class="skills-metadata"><summary>Skill information</summary><dl>';
20
+ if (source) html += '<dt>Repository</dt><dd><a href="https://github.com/' + text(source) + '" target="_blank" rel="noopener noreferrer">' + text(source) + '</a></dd>';
21
+ if (data.version) html += '<dt>Version</dt><dd>' + text(data.version) + '</dd>';
22
+ if (data.path) html += '<dt>Location</dt><dd><code>' + text(data.path) + '</code></dd>';
23
+ if (data.githubStars) html += '<dt>GitHub stars</dt><dd>' + text(data.githubStars) + '</dd>';
24
+ if (data.firstSeen) html += '<dt>First seen</dt><dd>' + text(data.firstSeen) + '</dd>';
25
+ (data.audits || []).forEach(function (audit) { html += '<dt>' + text(audit.name) + '</dt><dd>' + text(audit.status) + '</dd>'; });
26
+ (data.installedOn || []).forEach(function (platform) { html += '<dt>' + text(platform.name) + '</dt><dd>' + text(platform.installs) + ' installs</dd>'; });
27
+ html += '</dl></details>';
28
+ }
29
+ if (!data._installed) html += '<details class="skills-manual"><summary>Install from your terminal</summary><div class="skills-detail-cmd"><code>' + text(cmd) + '</code><button type="button" class="skills-copy-btn" aria-label="Copy installation command">' + iconHtml('copy') + '</button></div></details>';
30
+ html += '</article>';
31
+ content().innerHTML = html; refreshIcons();
32
+ var copy = content().querySelector('.skills-copy-btn'); if (copy) copy.onclick = function () { copyToClipboard(cmd); };
33
+ attachInstallHandlers(content().querySelector('[data-skill-actions]'), source, skill);
34
+ content().querySelector('h2').focus({ preventScroll: true });
148
35
  }
@@ -0,0 +1,29 @@
1
+ import { iconHtml, refreshIcons } from './icons.js';
2
+ import { escapeHtml } from './utils.js';
3
+ import { content, skillState, getInstalledInfo, scopeLabel } from './skills-state.js';
4
+
5
+ function text(value) { return escapeHtml(String(value || '')); }
6
+ function count(value) { return typeof value === 'number' ? new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 }).format(value) : text(value || '0'); }
7
+ export function renderSkillList(skills, openSkill, discover) {
8
+ var state = skillState(), installed = state.activeTab === 'installed';
9
+ var query = state.searchQuery.toLowerCase();
10
+ if (installed) skills = Object.keys(state.installedSkills).sort().map(function (name) { return Object.assign({ name: name, skillId: name }, state.installedSkills[name]); }).filter(function (row) { return (row.name + ' ' + (row.description || '')).toLowerCase().includes(query); });
11
+ var html = '<div class="skills-list-intro"><span>' + (installed ? 'Available to your agent' : state.searchQuery ? 'Search results' : 'Explore community skills') + '</span><span>' + skills.length + (skills.length === 1 ? ' skill' : ' skills') + '</span></div>';
12
+ if (!skills.length) {
13
+ html += '<div class="skills-library-empty">' + iconHtml(query ? 'search' : 'book-open') + '<h3>' + (query ? 'No matching skills' : 'Build your skill library') + '</h3><p>' + (query ? 'Try a different name or a shorter search.' : 'Discover reusable workflows and add the ones that fit your work.') + '</p>' + (!query && installed ? '<button type="button" class="skills-browse-btn">Discover skills ' + iconHtml('arrow-right') + '</button>' : '') + '</div>';
14
+ } else html += '<div class="skills-library-list">' + skills.map(function (row, index) {
15
+ var info = installed ? row : getInstalledInfo(row.skillId || row.name);
16
+ var builtin = info && info.source === 'clay-builtin';
17
+ return '<button type="button" class="skills-library-row" data-skill-index="' + index + '"><span class="skills-library-icon">' + iconHtml(builtin ? 'sparkles' : 'book-open') + '</span><span class="skills-library-info"><span class="skills-library-name">' + text(row.name) + '</span>' + (row.description ? '<span class="skills-library-desc">' + text(row.description) + '</span>' : '') + '<span class="skills-library-meta">' + (installed ? (builtin ? 'Included with Clay' : text(scopeLabel(row.scope))) : text(row.source)) + '</span></span><span class="skills-library-end">' + (info ? '<span class="skills-library-badge">' + (builtin ? 'Built-in' : installed ? '' : 'Installed') + '</span>' : '<span class="skills-library-installs">' + iconHtml('download') + count(row.installs) + '</span>') + iconHtml('chevron-right') + '</span></button>';
18
+ }).join('') + '</div>';
19
+ if (!installed) html += '<p class="skills-catalog-credit">Community catalog from <a href="https://skills.sh" target="_blank" rel="noopener noreferrer">skills.sh</a></p>';
20
+ content().innerHTML = html;
21
+ content().querySelectorAll('[data-skill-index]').forEach(function (button) { button.onclick = function () { openSkill(skills[Number(button.dataset.skillIndex)], installed); }; });
22
+ var browse = content().querySelector('.skills-browse-btn'); if (browse) browse.onclick = discover;
23
+ refreshIcons();
24
+ }
25
+ export function renderSkillFeedback(message, retry) {
26
+ content().innerHTML = '<div class="skills-library-empty" role="status">' + iconHtml(retry ? 'circle-alert' : 'loader-circle') + '<p>' + text(message) + '</p>' + (retry ? '<button type="button" class="skills-browse-btn">Try again</button>' : '') + '</div>';
27
+ var button = content().querySelector('button'); if (button) button.onclick = retry;
28
+ refreshIcons();
29
+ }
@@ -1,10 +1,12 @@
1
1
  import { store } from './store.js';
2
2
  export function skillState() {
3
3
  var value = store.get('skillsUi');
4
- if (!value) { value = { activeTab: 'installed', currentView: 'list', skillsData: {}, installedSkills: {}, searchQuery: '', searchTimer: null, searchCache: {} }; store.set({ skillsUi: value }); }
4
+ if (!value) { value = { activeTab: 'installed', discoverySort: 'all', requestVersion: 0, currentView: 'list', skillsData: {}, installedSkills: {}, searchQuery: '', searchTimer: null, searchCache: {} }; store.set({ skillsUi: value }); }
5
5
  return value;
6
6
  }
7
7
  export function content() { return document.getElementById('skills-content'); }
8
8
  export function basePath() { return store.get('basePath') || ''; }
9
9
  export function getInstalledInfo(skillId) { return skillState().installedSkills[skillId] || null; }
10
- export function scopeLabel(scope) { return scope === 'both' ? 'Global + Project' : scope === 'project' ? 'Project' : 'Global'; }
10
+ export function scopeLabel(scope) { return scope === 'both' ? 'This project + All projects' : scope === 'project' ? 'This project' : 'All projects'; }
11
+
12
+ export function patchSkills(values) { store.set({ skillsUi: Object.assign({}, skillState(), values) }); }