claudeck 1.1.1 → 1.2.0
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/README.md +30 -4
- package/config/skillsmp-config.json +5 -0
- package/db.js +248 -0
- package/package.json +11 -2
- package/public/css/panels/git-panel.css +220 -0
- package/public/css/panels/skills-manager.css +975 -0
- package/public/css/ui/input-history.css +109 -0
- package/public/css/ui/messages.css +51 -0
- package/public/css/ui/notification-bell.css +421 -0
- package/public/css/ui/sessions.css +41 -0
- package/public/css/ui/worktree.css +442 -0
- package/public/index.html +43 -10
- package/public/js/core/api.js +83 -0
- package/public/js/core/dom.js +15 -0
- package/public/js/features/background-sessions.js +11 -0
- package/public/js/features/chat.js +501 -3
- package/public/js/features/input-history.js +122 -0
- package/public/js/features/projects.js +16 -1
- package/public/js/features/sessions.js +77 -30
- package/public/js/main.js +3 -0
- package/public/js/panels/git-panel.js +385 -6
- package/public/js/panels/skills-manager.js +1005 -0
- package/public/js/ui/messages.js +58 -0
- package/public/js/ui/notification-bell.js +240 -0
- package/public/js/ui/notification-history.js +210 -0
- package/public/js/ui/parallel.js +11 -0
- package/public/js/ui/tab-sdk.js +1 -1
- package/public/style.css +4 -0
- package/server/agent-loop.js +13 -0
- package/server/notification-logger.js +27 -0
- package/server/routes/notifications.js +57 -1
- package/server/routes/sessions.js +41 -0
- package/server/routes/skills.js +454 -0
- package/server/routes/worktrees.js +93 -0
- package/server/utils/git-worktree.js +297 -0
- package/server/ws-handler.js +708 -629
- package/server.js +17 -1
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
// Skills Marketplace — SkillsMP integration panel
|
|
2
|
+
import { registerTab } from '../ui/tab-sdk.js';
|
|
3
|
+
import { registerCommand } from '../ui/commands.js';
|
|
4
|
+
|
|
5
|
+
const ICON = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 002 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><polyline points="7.5 4.21 12 6.81 16.5 4.21"/><polyline points="7.5 19.79 7.5 14.6 3 12"/><polyline points="21 12 16.5 14.6 16.5 19.79"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>';
|
|
6
|
+
|
|
7
|
+
const STAR_SVG = '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>';
|
|
8
|
+
const TRASH_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>';
|
|
9
|
+
const KEY_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 017.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>';
|
|
10
|
+
const SKILL_ICON_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>';
|
|
11
|
+
|
|
12
|
+
let ctx = null;
|
|
13
|
+
let activated = false;
|
|
14
|
+
let root = null;
|
|
15
|
+
let installedCache = [];
|
|
16
|
+
let currentPage = 1;
|
|
17
|
+
let lastSearchQuery = "";
|
|
18
|
+
let searchMode = "keyword";
|
|
19
|
+
let defaultScope = "project";
|
|
20
|
+
let quotaRemaining = null;
|
|
21
|
+
|
|
22
|
+
// ── Helpers ─────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function relativeTime(unixStr) {
|
|
25
|
+
const ts = Number(unixStr) * 1000;
|
|
26
|
+
if (!ts || isNaN(ts)) return "";
|
|
27
|
+
const diff = Date.now() - ts;
|
|
28
|
+
const mins = Math.floor(diff / 60000);
|
|
29
|
+
if (mins < 60) return `${mins}m ago`;
|
|
30
|
+
const hrs = Math.floor(mins / 60);
|
|
31
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
32
|
+
const days = Math.floor(hrs / 24);
|
|
33
|
+
if (days < 30) return `${days}d ago`;
|
|
34
|
+
const months = Math.floor(days / 30);
|
|
35
|
+
return `${months}mo ago`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isInstalled(skill) {
|
|
39
|
+
return installedCache.some(
|
|
40
|
+
(s) => s.dirName === skill.name || s.name === skill.name
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getProjectPath() {
|
|
45
|
+
return ctx ? ctx.getProjectPath() : "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Tab registration ────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
registerTab({
|
|
51
|
+
id: "skills",
|
|
52
|
+
title: "Skills",
|
|
53
|
+
icon: ICON,
|
|
54
|
+
lazy: true,
|
|
55
|
+
|
|
56
|
+
init(_ctx) {
|
|
57
|
+
ctx = _ctx;
|
|
58
|
+
root = document.createElement("div");
|
|
59
|
+
root.className = "skills-panel";
|
|
60
|
+
|
|
61
|
+
// Restore from localStorage
|
|
62
|
+
searchMode = localStorage.getItem("claudeck-skills-mode") || "keyword";
|
|
63
|
+
lastSearchQuery = localStorage.getItem("claudeck-skills-query") || "";
|
|
64
|
+
|
|
65
|
+
checkActivation();
|
|
66
|
+
|
|
67
|
+
ctx.on("projectChanged", () => {
|
|
68
|
+
if (activated) refreshInstalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return root;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
onActivate() {
|
|
75
|
+
if (activated) refreshInstalled();
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ── /skills command ─────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
registerCommand("skills", {
|
|
82
|
+
category: "app",
|
|
83
|
+
description: "Open Skills Marketplace",
|
|
84
|
+
execute() {
|
|
85
|
+
import("../ui/right-panel.js").then((m) => m.openRightPanel("skills"));
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── Activation check ────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
async function checkActivation() {
|
|
92
|
+
try {
|
|
93
|
+
const config = await ctx.api.fetchSkillsConfig();
|
|
94
|
+
activated = config.activated;
|
|
95
|
+
defaultScope = config.defaultScope || "project";
|
|
96
|
+
if (config.searchMode) searchMode = config.searchMode;
|
|
97
|
+
|
|
98
|
+
if (activated) {
|
|
99
|
+
renderMarketplace();
|
|
100
|
+
refreshInstalled();
|
|
101
|
+
} else {
|
|
102
|
+
renderActivationForm();
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
renderActivationForm();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Activation Form ─────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
function renderActivationForm() {
|
|
112
|
+
root.innerHTML = "";
|
|
113
|
+
const form = document.createElement("div");
|
|
114
|
+
form.className = "skills-activate";
|
|
115
|
+
form.innerHTML = `
|
|
116
|
+
<div class="skills-activate-icon">${KEY_SVG}</div>
|
|
117
|
+
<h3>Skills Marketplace</h3>
|
|
118
|
+
<p>Browse and install agent skills from SkillsMP. Skills extend Claude Code with custom slash commands, workflows, and domain knowledge.</p>
|
|
119
|
+
<a href="https://skillsmp.com/docs/api" target="_blank" rel="noopener">Get your free API key</a>
|
|
120
|
+
<input type="password" class="skills-activate-input" placeholder="sk_live_skillsmp_..." autocomplete="off">
|
|
121
|
+
<button class="skills-activate-btn">Activate</button>
|
|
122
|
+
<div class="skills-activate-error"></div>
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
const input = form.querySelector("input");
|
|
126
|
+
const btn = form.querySelector("button");
|
|
127
|
+
const errEl = form.querySelector(".skills-activate-error");
|
|
128
|
+
|
|
129
|
+
btn.addEventListener("click", async () => {
|
|
130
|
+
const key = input.value.trim();
|
|
131
|
+
if (!key) { errEl.textContent = "Please enter your API key"; return; }
|
|
132
|
+
btn.disabled = true;
|
|
133
|
+
btn.textContent = "Validating...";
|
|
134
|
+
errEl.textContent = "";
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const res = await ctx.api.saveSkillsConfig({ apiKey: key });
|
|
138
|
+
if (res.error) {
|
|
139
|
+
errEl.textContent = res.error;
|
|
140
|
+
btn.disabled = false;
|
|
141
|
+
btn.textContent = "Activate";
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
activated = true;
|
|
145
|
+
root.style.opacity = "0";
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
renderMarketplace();
|
|
148
|
+
refreshInstalled();
|
|
149
|
+
root.style.opacity = "1";
|
|
150
|
+
}, 150);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
errEl.textContent = err.message || "Activation failed";
|
|
153
|
+
btn.disabled = false;
|
|
154
|
+
btn.textContent = "Activate";
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
input.addEventListener("keydown", (e) => {
|
|
159
|
+
if (e.key === "Enter") btn.click();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
root.appendChild(form);
|
|
163
|
+
root.style.transition = "opacity 0.15s";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Marketplace UI ──────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
let browseContent, installedContent, settingsContent;
|
|
169
|
+
|
|
170
|
+
function renderMarketplace() {
|
|
171
|
+
root.innerHTML = "";
|
|
172
|
+
|
|
173
|
+
// Sub-tab bar
|
|
174
|
+
const tabBar = document.createElement("div");
|
|
175
|
+
tabBar.className = "skills-subtabs";
|
|
176
|
+
|
|
177
|
+
const tabs = [
|
|
178
|
+
{ id: "browse", label: "Browse" },
|
|
179
|
+
{ id: "installed", label: "Installed" },
|
|
180
|
+
{ id: "settings", label: "Settings" },
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const contents = {};
|
|
184
|
+
|
|
185
|
+
for (const t of tabs) {
|
|
186
|
+
const btn = document.createElement("button");
|
|
187
|
+
btn.className = "skills-subtab" + (t.id === "browse" ? " active" : "");
|
|
188
|
+
btn.textContent = t.label;
|
|
189
|
+
btn.dataset.subtab = t.id;
|
|
190
|
+
btn.addEventListener("click", () => {
|
|
191
|
+
tabBar.querySelectorAll(".skills-subtab").forEach((b) => b.classList.remove("active"));
|
|
192
|
+
btn.classList.add("active");
|
|
193
|
+
Object.values(contents).forEach((c) => c.classList.remove("active"));
|
|
194
|
+
contents[t.id].classList.add("active");
|
|
195
|
+
if (t.id !== "browse") clearBrowseSearch();
|
|
196
|
+
if (t.id === "installed") refreshInstalled();
|
|
197
|
+
if (t.id === "settings") renderSettings();
|
|
198
|
+
});
|
|
199
|
+
tabBar.appendChild(btn);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
root.appendChild(tabBar);
|
|
203
|
+
|
|
204
|
+
// Browse content
|
|
205
|
+
browseContent = document.createElement("div");
|
|
206
|
+
browseContent.className = "skills-subtab-content active";
|
|
207
|
+
contents.browse = browseContent;
|
|
208
|
+
renderBrowseTab();
|
|
209
|
+
root.appendChild(browseContent);
|
|
210
|
+
|
|
211
|
+
// Installed content
|
|
212
|
+
installedContent = document.createElement("div");
|
|
213
|
+
installedContent.className = "skills-subtab-content";
|
|
214
|
+
contents.installed = installedContent;
|
|
215
|
+
root.appendChild(installedContent);
|
|
216
|
+
|
|
217
|
+
// Settings content
|
|
218
|
+
settingsContent = document.createElement("div");
|
|
219
|
+
settingsContent.className = "skills-subtab-content";
|
|
220
|
+
contents.settings = settingsContent;
|
|
221
|
+
root.appendChild(settingsContent);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Browse Tab ──────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
let searchTimeout = null;
|
|
227
|
+
|
|
228
|
+
function renderBrowseTab() {
|
|
229
|
+
browseContent.innerHTML = "";
|
|
230
|
+
|
|
231
|
+
// Search bar
|
|
232
|
+
const searchBar = document.createElement("div");
|
|
233
|
+
searchBar.className = "skills-search-bar";
|
|
234
|
+
|
|
235
|
+
const searchInput = document.createElement("input");
|
|
236
|
+
searchInput.className = "skills-search-input";
|
|
237
|
+
searchInput.placeholder = "Search skills...";
|
|
238
|
+
searchInput.value = lastSearchQuery;
|
|
239
|
+
|
|
240
|
+
const modeToggle = document.createElement("div");
|
|
241
|
+
modeToggle.className = "skills-search-mode";
|
|
242
|
+
const kwBtn = document.createElement("button");
|
|
243
|
+
kwBtn.textContent = "Keyword";
|
|
244
|
+
kwBtn.title = "Fast keyword search (~200ms)";
|
|
245
|
+
kwBtn.className = searchMode === "keyword" ? "active" : "";
|
|
246
|
+
const aiBtn = document.createElement("button");
|
|
247
|
+
aiBtn.textContent = "Semantic";
|
|
248
|
+
aiBtn.title = "AI semantic search (~2.5s)";
|
|
249
|
+
aiBtn.className = searchMode === "ai" ? "active" : "";
|
|
250
|
+
|
|
251
|
+
modeToggle.appendChild(kwBtn);
|
|
252
|
+
modeToggle.appendChild(aiBtn);
|
|
253
|
+
searchBar.appendChild(searchInput);
|
|
254
|
+
searchBar.appendChild(modeToggle);
|
|
255
|
+
browseContent.appendChild(searchBar);
|
|
256
|
+
|
|
257
|
+
// Search mode hint
|
|
258
|
+
const HINTS = {
|
|
259
|
+
keyword: "Search by skill name or keyword — fast, exact matching",
|
|
260
|
+
ai: "Describe what you need in plain English — AI finds the best match",
|
|
261
|
+
};
|
|
262
|
+
const searchHint = document.createElement("div");
|
|
263
|
+
searchHint.className = "skills-search-hint";
|
|
264
|
+
searchHint.textContent = HINTS[searchMode];
|
|
265
|
+
browseContent.appendChild(searchHint);
|
|
266
|
+
|
|
267
|
+
function updateHint() {
|
|
268
|
+
searchHint.textContent = HINTS[searchMode];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
kwBtn.addEventListener("click", () => {
|
|
272
|
+
searchMode = "keyword";
|
|
273
|
+
localStorage.setItem("claudeck-skills-mode", searchMode);
|
|
274
|
+
kwBtn.classList.add("active");
|
|
275
|
+
aiBtn.classList.remove("active");
|
|
276
|
+
sortBar.style.display = "";
|
|
277
|
+
updateHint();
|
|
278
|
+
if (searchInput.value.trim()) doSearch(searchInput.value.trim());
|
|
279
|
+
});
|
|
280
|
+
aiBtn.addEventListener("click", () => {
|
|
281
|
+
searchMode = "ai";
|
|
282
|
+
localStorage.setItem("claudeck-skills-mode", searchMode);
|
|
283
|
+
aiBtn.classList.add("active");
|
|
284
|
+
kwBtn.classList.remove("active");
|
|
285
|
+
sortBar.style.display = "none";
|
|
286
|
+
updateHint();
|
|
287
|
+
if (searchInput.value.trim()) doSearch(searchInput.value.trim());
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Sort bar (keyword only)
|
|
291
|
+
const sortBar = document.createElement("div");
|
|
292
|
+
sortBar.className = "skills-sort-bar";
|
|
293
|
+
if (searchMode === "ai") sortBar.style.display = "none";
|
|
294
|
+
|
|
295
|
+
const sortLabel = document.createElement("label");
|
|
296
|
+
sortLabel.textContent = "Sort:";
|
|
297
|
+
const sortSelect = document.createElement("select");
|
|
298
|
+
sortSelect.className = "skills-sort-select";
|
|
299
|
+
sortSelect.innerHTML = '<option value="stars">Stars</option><option value="recent">Recent</option>';
|
|
300
|
+
sortSelect.addEventListener("change", () => {
|
|
301
|
+
if (searchInput.value.trim()) doSearch(searchInput.value.trim());
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
sortBar.appendChild(sortLabel);
|
|
305
|
+
sortBar.appendChild(sortSelect);
|
|
306
|
+
browseContent.appendChild(sortBar);
|
|
307
|
+
|
|
308
|
+
// Results container
|
|
309
|
+
const results = document.createElement("div");
|
|
310
|
+
results.className = "skills-results";
|
|
311
|
+
browseContent.appendChild(results);
|
|
312
|
+
|
|
313
|
+
// Pagination
|
|
314
|
+
const pagination = document.createElement("div");
|
|
315
|
+
pagination.className = "skills-pagination";
|
|
316
|
+
pagination.style.display = "none";
|
|
317
|
+
browseContent.appendChild(pagination);
|
|
318
|
+
|
|
319
|
+
// Search debounce
|
|
320
|
+
searchInput.addEventListener("input", () => {
|
|
321
|
+
clearTimeout(searchTimeout);
|
|
322
|
+
searchTimeout = setTimeout(() => {
|
|
323
|
+
const q = searchInput.value.trim();
|
|
324
|
+
localStorage.setItem("claudeck-skills-query", q);
|
|
325
|
+
lastSearchQuery = q;
|
|
326
|
+
currentPage = 1;
|
|
327
|
+
if (q) doSearch(q);
|
|
328
|
+
else {
|
|
329
|
+
results.innerHTML = "";
|
|
330
|
+
pagination.style.display = "none";
|
|
331
|
+
showInitialState(results);
|
|
332
|
+
}
|
|
333
|
+
}, 300);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
searchInput.addEventListener("keydown", (e) => {
|
|
337
|
+
if (e.key === "Enter") {
|
|
338
|
+
clearTimeout(searchTimeout);
|
|
339
|
+
const q = searchInput.value.trim();
|
|
340
|
+
if (q) { currentPage = 1; doSearch(q); }
|
|
341
|
+
} else if (e.key === "ArrowDown") {
|
|
342
|
+
e.preventDefault();
|
|
343
|
+
const firstCard = results.querySelector(".skill-card");
|
|
344
|
+
if (firstCard) firstCard.focus();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Keyboard navigation within results
|
|
349
|
+
results.addEventListener("keydown", (e) => {
|
|
350
|
+
const card = e.target.closest(".skill-card");
|
|
351
|
+
if (!card) return;
|
|
352
|
+
|
|
353
|
+
if (e.key === "ArrowDown") {
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
const next = card.nextElementSibling;
|
|
356
|
+
if (next?.classList.contains("skill-card")) next.focus();
|
|
357
|
+
} else if (e.key === "ArrowUp") {
|
|
358
|
+
e.preventDefault();
|
|
359
|
+
const prev = card.previousElementSibling;
|
|
360
|
+
if (prev?.classList.contains("skill-card")) prev.focus();
|
|
361
|
+
else searchInput.focus();
|
|
362
|
+
} else if (e.key === "Enter") {
|
|
363
|
+
e.preventDefault();
|
|
364
|
+
card.click();
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Auto-search if there's a saved query, otherwise show initial state
|
|
369
|
+
if (lastSearchQuery) {
|
|
370
|
+
doSearch(lastSearchQuery);
|
|
371
|
+
} else {
|
|
372
|
+
showInitialState(results);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── Search logic ────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
async function doSearch(q) {
|
|
378
|
+
results.innerHTML = "";
|
|
379
|
+
pagination.style.display = "none";
|
|
380
|
+
|
|
381
|
+
// Loading indicator with mode-aware message
|
|
382
|
+
const loader = document.createElement("div");
|
|
383
|
+
loader.className = "skills-search-loading";
|
|
384
|
+
const spinner = document.createElement("div");
|
|
385
|
+
spinner.className = "skills-spinner";
|
|
386
|
+
const loadMsg = document.createElement("span");
|
|
387
|
+
loadMsg.className = "skills-search-loading-text";
|
|
388
|
+
loadMsg.textContent = searchMode === "ai"
|
|
389
|
+
? `Finding skills matching "${q}"...`
|
|
390
|
+
: `Searching for "${q}"...`;
|
|
391
|
+
loader.appendChild(spinner);
|
|
392
|
+
loader.appendChild(loadMsg);
|
|
393
|
+
results.appendChild(loader);
|
|
394
|
+
|
|
395
|
+
// Skeleton cards
|
|
396
|
+
for (let i = 0; i < 4; i++) {
|
|
397
|
+
const skel = document.createElement("div");
|
|
398
|
+
skel.className = "skills-skeleton";
|
|
399
|
+
skel.innerHTML = `
|
|
400
|
+
<div class="skills-skeleton-header">
|
|
401
|
+
<div class="skills-skeleton-line" style="width:35%"></div>
|
|
402
|
+
<div class="skills-skeleton-line short" style="width:15%"></div>
|
|
403
|
+
</div>
|
|
404
|
+
<div class="skills-skeleton-line" style="width:90%"></div>
|
|
405
|
+
<div class="skills-skeleton-line" style="width:65%"></div>
|
|
406
|
+
<div class="skills-skeleton-footer">
|
|
407
|
+
<div class="skills-skeleton-line short" style="width:10%"></div>
|
|
408
|
+
<div class="skills-skeleton-line short" style="width:18%"></div>
|
|
409
|
+
</div>`;
|
|
410
|
+
results.appendChild(skel);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
if (searchMode === "ai") {
|
|
415
|
+
const { data } = await ctx.api.aiSearchSkills(q);
|
|
416
|
+
results.innerHTML = "";
|
|
417
|
+
|
|
418
|
+
if (!data.success && data.error) {
|
|
419
|
+
showErrorBanner(results, data.error.message || data.error);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const items = data.data?.data || [];
|
|
424
|
+
if (items.length === 0) {
|
|
425
|
+
showEmpty(results, `No results for "${q}"`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
for (const item of items) {
|
|
430
|
+
const skill = item.skill;
|
|
431
|
+
if (!skill) continue;
|
|
432
|
+
const card = createSkillCard(skill, Math.round((item.score || 0) * 100));
|
|
433
|
+
results.appendChild(card);
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
const { data, headers } = await ctx.api.searchSkills(q, currentPage, 20, sortSelect.value);
|
|
437
|
+
results.innerHTML = "";
|
|
438
|
+
|
|
439
|
+
// Track quota
|
|
440
|
+
const rem = headers.get("X-RateLimit-Daily-Remaining");
|
|
441
|
+
if (rem) quotaRemaining = Number(rem);
|
|
442
|
+
|
|
443
|
+
if (!data.success && data.error) {
|
|
444
|
+
showErrorBanner(results, data.error.message || data.error);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const skills = data.data?.skills || [];
|
|
449
|
+
const pag = data.data?.pagination;
|
|
450
|
+
|
|
451
|
+
if (skills.length === 0) {
|
|
452
|
+
showEmpty(results, `No results for "${q}"`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
for (const skill of skills) {
|
|
457
|
+
const card = createSkillCard(skill);
|
|
458
|
+
results.appendChild(card);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Pagination
|
|
462
|
+
if (pag && (pag.hasNext || pag.hasPrev)) {
|
|
463
|
+
pagination.style.display = "";
|
|
464
|
+
pagination.innerHTML = "";
|
|
465
|
+
const prevBtn = document.createElement("button");
|
|
466
|
+
prevBtn.textContent = "Previous";
|
|
467
|
+
prevBtn.disabled = !pag.hasPrev;
|
|
468
|
+
prevBtn.addEventListener("click", () => { currentPage--; doSearch(q); });
|
|
469
|
+
|
|
470
|
+
const info = document.createElement("span");
|
|
471
|
+
info.className = "skills-pagination-info";
|
|
472
|
+
info.textContent = `Page ${pag.page}${pag.totalPages ? ` of ${pag.totalPages}` : ""}`;
|
|
473
|
+
|
|
474
|
+
const nextBtn = document.createElement("button");
|
|
475
|
+
nextBtn.textContent = "Next";
|
|
476
|
+
nextBtn.disabled = !pag.hasNext;
|
|
477
|
+
nextBtn.addEventListener("click", () => { currentPage++; doSearch(q); });
|
|
478
|
+
|
|
479
|
+
pagination.appendChild(prevBtn);
|
|
480
|
+
pagination.appendChild(info);
|
|
481
|
+
pagination.appendChild(nextBtn);
|
|
482
|
+
} else {
|
|
483
|
+
pagination.style.display = "none";
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
} catch (err) {
|
|
487
|
+
results.innerHTML = "";
|
|
488
|
+
showErrorBanner(results, err.message || "Search failed");
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── Skill Card ──────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
function createSkillCard(skill, score) {
|
|
496
|
+
const card = document.createElement("div");
|
|
497
|
+
card.className = "skill-card";
|
|
498
|
+
card.tabIndex = 0;
|
|
499
|
+
|
|
500
|
+
const header = document.createElement("div");
|
|
501
|
+
header.className = "skill-card-header";
|
|
502
|
+
|
|
503
|
+
const nameEl = document.createElement("span");
|
|
504
|
+
nameEl.className = "skill-card-name";
|
|
505
|
+
nameEl.textContent = skill.name;
|
|
506
|
+
|
|
507
|
+
const authorEl = document.createElement("span");
|
|
508
|
+
authorEl.className = "skill-card-author";
|
|
509
|
+
authorEl.textContent = `@${skill.author}`;
|
|
510
|
+
|
|
511
|
+
header.appendChild(nameEl);
|
|
512
|
+
header.appendChild(authorEl);
|
|
513
|
+
|
|
514
|
+
const desc = document.createElement("div");
|
|
515
|
+
desc.className = "skill-card-desc";
|
|
516
|
+
desc.textContent = skill.description || "";
|
|
517
|
+
|
|
518
|
+
const footer = document.createElement("div");
|
|
519
|
+
footer.className = "skill-card-footer";
|
|
520
|
+
|
|
521
|
+
const stars = document.createElement("span");
|
|
522
|
+
stars.className = "skill-card-stars";
|
|
523
|
+
stars.innerHTML = `${STAR_SVG} ${skill.stars || 0}`;
|
|
524
|
+
|
|
525
|
+
const time = document.createElement("span");
|
|
526
|
+
time.className = "skill-card-time";
|
|
527
|
+
time.textContent = relativeTime(skill.updatedAt);
|
|
528
|
+
|
|
529
|
+
footer.appendChild(stars);
|
|
530
|
+
if (score !== undefined) {
|
|
531
|
+
const scoreEl = document.createElement("span");
|
|
532
|
+
scoreEl.className = "skill-card-score";
|
|
533
|
+
scoreEl.textContent = `${score}% match`;
|
|
534
|
+
footer.appendChild(scoreEl);
|
|
535
|
+
}
|
|
536
|
+
footer.appendChild(time);
|
|
537
|
+
|
|
538
|
+
// Install actions
|
|
539
|
+
const actions = document.createElement("div");
|
|
540
|
+
actions.className = "skill-card-actions";
|
|
541
|
+
|
|
542
|
+
const scopeSelect = document.createElement("select");
|
|
543
|
+
scopeSelect.className = "skill-scope-select";
|
|
544
|
+
scopeSelect.innerHTML = `<option value="global">Global</option>`;
|
|
545
|
+
const pp = getProjectPath();
|
|
546
|
+
if (pp) {
|
|
547
|
+
scopeSelect.innerHTML += `<option value="project">Project</option>`;
|
|
548
|
+
if (defaultScope === "project") scopeSelect.value = "project";
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const installBtn = document.createElement("button");
|
|
552
|
+
installBtn.className = "skill-install-btn";
|
|
553
|
+
|
|
554
|
+
if (isInstalled(skill)) {
|
|
555
|
+
installBtn.textContent = "Installed";
|
|
556
|
+
installBtn.classList.add("installed");
|
|
557
|
+
} else {
|
|
558
|
+
installBtn.textContent = "Install";
|
|
559
|
+
installBtn.addEventListener("click", async (e) => {
|
|
560
|
+
e.stopPropagation();
|
|
561
|
+
|
|
562
|
+
// Check for duplicate
|
|
563
|
+
const scope = scopeSelect.value;
|
|
564
|
+
const duplicate = installedCache.find(
|
|
565
|
+
(s) => (s.dirName === skill.name || s.name === skill.name) && s.scope === scope
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
if (duplicate) {
|
|
569
|
+
showConfirm({
|
|
570
|
+
title: `"${skill.name}" is already installed`,
|
|
571
|
+
message: `This will overwrite the existing skill in ${scope === "global" ? "global (~/.claude/)" : "project (.claude/)"} scope.`,
|
|
572
|
+
confirmLabel: "Overwrite",
|
|
573
|
+
danger: true,
|
|
574
|
+
onConfirm: () => doInstall(),
|
|
575
|
+
});
|
|
576
|
+
} else {
|
|
577
|
+
doInstall();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function doInstall() {
|
|
581
|
+
installBtn.disabled = true;
|
|
582
|
+
installBtn.textContent = "Installing...";
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
const res = await ctx.api.installSkill({
|
|
586
|
+
githubUrl: skill.githubUrl,
|
|
587
|
+
name: skill.name,
|
|
588
|
+
description: skill.description || "",
|
|
589
|
+
scope,
|
|
590
|
+
projectPath: getProjectPath(),
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
if (res.error) {
|
|
594
|
+
installBtn.textContent = "Error";
|
|
595
|
+
setTimeout(() => { installBtn.textContent = "Install"; installBtn.disabled = false; }, 2000);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
installBtn.textContent = "Installed";
|
|
600
|
+
installBtn.classList.add("installed");
|
|
601
|
+
installBtn.disabled = false;
|
|
602
|
+
|
|
603
|
+
showSkillToast(`Installed "${skill.name}"`, "success");
|
|
604
|
+
refreshInstalled();
|
|
605
|
+
refreshProjectCommands();
|
|
606
|
+
} catch {
|
|
607
|
+
installBtn.textContent = "Failed";
|
|
608
|
+
showSkillToast(`Failed to install "${skill.name}"`, "error");
|
|
609
|
+
setTimeout(() => { installBtn.textContent = "Install"; installBtn.disabled = false; }, 2000);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
actions.appendChild(scopeSelect);
|
|
616
|
+
actions.appendChild(installBtn);
|
|
617
|
+
footer.appendChild(actions);
|
|
618
|
+
|
|
619
|
+
card.appendChild(header);
|
|
620
|
+
card.appendChild(desc);
|
|
621
|
+
card.appendChild(footer);
|
|
622
|
+
|
|
623
|
+
// Detail expansion
|
|
624
|
+
const detail = document.createElement("div");
|
|
625
|
+
detail.className = "skill-card-detail";
|
|
626
|
+
|
|
627
|
+
const fullDesc = document.createElement("div");
|
|
628
|
+
fullDesc.className = "skill-card-desc";
|
|
629
|
+
fullDesc.style.cssText = "-webkit-line-clamp: unset; margin-bottom: 4px;";
|
|
630
|
+
fullDesc.textContent = skill.description || "";
|
|
631
|
+
|
|
632
|
+
const links = document.createElement("div");
|
|
633
|
+
links.className = "skill-card-detail-links";
|
|
634
|
+
if (skill.githubUrl) links.innerHTML += `<a href="${skill.githubUrl}" target="_blank" rel="noopener">GitHub</a>`;
|
|
635
|
+
if (skill.skillUrl) links.innerHTML += `<a href="${skill.skillUrl}" target="_blank" rel="noopener">SkillsMP</a>`;
|
|
636
|
+
|
|
637
|
+
const dateEl = document.createElement("div");
|
|
638
|
+
dateEl.className = "skill-card-detail-date";
|
|
639
|
+
if (skill.updatedAt) {
|
|
640
|
+
const d = new Date(Number(skill.updatedAt) * 1000);
|
|
641
|
+
dateEl.textContent = `Updated: ${d.toLocaleDateString()}`;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
detail.appendChild(fullDesc);
|
|
645
|
+
detail.appendChild(links);
|
|
646
|
+
detail.appendChild(dateEl);
|
|
647
|
+
card.appendChild(detail);
|
|
648
|
+
|
|
649
|
+
card.addEventListener("click", () => detail.classList.toggle("open"));
|
|
650
|
+
|
|
651
|
+
return card;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ── Installed Tab ───────────────────────────────────────
|
|
655
|
+
|
|
656
|
+
async function refreshInstalled() {
|
|
657
|
+
try {
|
|
658
|
+
installedCache = await ctx.api.fetchInstalledSkills(getProjectPath());
|
|
659
|
+
} catch {
|
|
660
|
+
installedCache = [];
|
|
661
|
+
}
|
|
662
|
+
if (installedContent) renderInstalledTab();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function renderInstalledTab() {
|
|
666
|
+
installedContent.innerHTML = "";
|
|
667
|
+
|
|
668
|
+
if (installedCache.length === 0) {
|
|
669
|
+
showEmpty(installedContent, "No skills installed");
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const projectSkills = installedCache.filter((s) => s.scope === "project");
|
|
674
|
+
const globalSkills = installedCache.filter((s) => s.scope === "global");
|
|
675
|
+
|
|
676
|
+
if (projectSkills.length > 0) {
|
|
677
|
+
const header = document.createElement("div");
|
|
678
|
+
header.className = "skills-scope-header";
|
|
679
|
+
header.textContent = "Project";
|
|
680
|
+
installedContent.appendChild(header);
|
|
681
|
+
for (const skill of projectSkills) installedContent.appendChild(createInstalledRow(skill));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (globalSkills.length > 0) {
|
|
685
|
+
const header = document.createElement("div");
|
|
686
|
+
header.className = "skills-scope-header";
|
|
687
|
+
header.textContent = "Global";
|
|
688
|
+
installedContent.appendChild(header);
|
|
689
|
+
for (const skill of globalSkills) installedContent.appendChild(createInstalledRow(skill));
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function createInstalledRow(skill) {
|
|
694
|
+
const row = document.createElement("div");
|
|
695
|
+
row.className = "skill-installed-row";
|
|
696
|
+
|
|
697
|
+
const info = document.createElement("div");
|
|
698
|
+
info.className = "skill-installed-info";
|
|
699
|
+
info.innerHTML = `<div class="skill-installed-name">${skill.name}</div><div class="skill-installed-desc">${skill.description || ""}</div>`;
|
|
700
|
+
|
|
701
|
+
const badge = document.createElement("span");
|
|
702
|
+
badge.className = `skill-scope-badge ${skill.scope}`;
|
|
703
|
+
badge.textContent = skill.scope;
|
|
704
|
+
|
|
705
|
+
// Toggle
|
|
706
|
+
const toggle = document.createElement("label");
|
|
707
|
+
toggle.className = "skill-toggle";
|
|
708
|
+
const checkbox = document.createElement("input");
|
|
709
|
+
checkbox.type = "checkbox";
|
|
710
|
+
checkbox.checked = skill.enabled;
|
|
711
|
+
const slider = document.createElement("span");
|
|
712
|
+
slider.className = "skill-toggle-slider";
|
|
713
|
+
toggle.appendChild(checkbox);
|
|
714
|
+
toggle.appendChild(slider);
|
|
715
|
+
|
|
716
|
+
checkbox.addEventListener("change", async () => {
|
|
717
|
+
try {
|
|
718
|
+
await ctx.api.toggleSkill(skill.dirName, skill.scope, getProjectPath());
|
|
719
|
+
refreshProjectCommands();
|
|
720
|
+
} catch {
|
|
721
|
+
checkbox.checked = !checkbox.checked;
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// Uninstall
|
|
726
|
+
const delBtn = document.createElement("button");
|
|
727
|
+
delBtn.className = "skill-uninstall-btn";
|
|
728
|
+
delBtn.title = "Uninstall";
|
|
729
|
+
delBtn.innerHTML = TRASH_SVG;
|
|
730
|
+
delBtn.addEventListener("click", () => {
|
|
731
|
+
showConfirm({
|
|
732
|
+
title: `Uninstall "${skill.name}"?`,
|
|
733
|
+
message: "This will remove the skill files from disk. You can reinstall it anytime from the marketplace.",
|
|
734
|
+
confirmLabel: "Uninstall",
|
|
735
|
+
danger: true,
|
|
736
|
+
onConfirm: async () => {
|
|
737
|
+
try {
|
|
738
|
+
await ctx.api.uninstallSkill(skill.dirName, skill.scope, getProjectPath());
|
|
739
|
+
showSkillToast(`Uninstalled "${skill.name}"`, "success");
|
|
740
|
+
refreshInstalled();
|
|
741
|
+
refreshProjectCommands();
|
|
742
|
+
} catch {
|
|
743
|
+
showSkillToast(`Failed to uninstall "${skill.name}"`, "error");
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
row.appendChild(info);
|
|
750
|
+
row.appendChild(badge);
|
|
751
|
+
row.appendChild(toggle);
|
|
752
|
+
row.appendChild(delBtn);
|
|
753
|
+
return row;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ── Settings Tab ────────────────────────────────────────
|
|
757
|
+
|
|
758
|
+
async function renderSettings() {
|
|
759
|
+
settingsContent.innerHTML = "";
|
|
760
|
+
const container = document.createElement("div");
|
|
761
|
+
container.className = "skills-settings";
|
|
762
|
+
|
|
763
|
+
// API Key section
|
|
764
|
+
let config;
|
|
765
|
+
try {
|
|
766
|
+
config = await ctx.api.fetchSkillsConfig();
|
|
767
|
+
} catch {
|
|
768
|
+
config = { apiKey: "", activated: false };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const keyGroup = document.createElement("div");
|
|
772
|
+
keyGroup.className = "skills-settings-group";
|
|
773
|
+
keyGroup.innerHTML = `<div class="skills-settings-label">API Key</div><div class="skills-settings-value">${config.apiKey || "Not set"}</div>`;
|
|
774
|
+
|
|
775
|
+
const keyBtns = document.createElement("div");
|
|
776
|
+
keyBtns.className = "skills-settings-row";
|
|
777
|
+
|
|
778
|
+
const changeBtn = document.createElement("button");
|
|
779
|
+
changeBtn.className = "skills-settings-btn";
|
|
780
|
+
changeBtn.textContent = "Change Key";
|
|
781
|
+
changeBtn.addEventListener("click", () => {
|
|
782
|
+
const key = prompt("Enter new SkillsMP API key:");
|
|
783
|
+
if (key === null) return;
|
|
784
|
+
ctx.api.saveSkillsConfig({ apiKey: key }).then((res) => {
|
|
785
|
+
if (res.error) alert(res.error);
|
|
786
|
+
else renderSettings();
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
const removeBtn = document.createElement("button");
|
|
791
|
+
removeBtn.className = "skills-settings-btn danger";
|
|
792
|
+
removeBtn.textContent = "Remove Key";
|
|
793
|
+
removeBtn.addEventListener("click", async () => {
|
|
794
|
+
showConfirm({
|
|
795
|
+
title: "Disable Skills Marketplace?",
|
|
796
|
+
message: "This will remove your API key. You can reactivate anytime by entering a new key.",
|
|
797
|
+
confirmLabel: "Disable",
|
|
798
|
+
danger: true,
|
|
799
|
+
onConfirm: async () => {
|
|
800
|
+
await ctx.api.saveSkillsConfig({ apiKey: "" });
|
|
801
|
+
activated = false;
|
|
802
|
+
root.style.opacity = "0";
|
|
803
|
+
setTimeout(() => {
|
|
804
|
+
renderActivationForm();
|
|
805
|
+
root.style.opacity = "1";
|
|
806
|
+
}, 150);
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
keyBtns.appendChild(changeBtn);
|
|
812
|
+
keyBtns.appendChild(removeBtn);
|
|
813
|
+
keyGroup.appendChild(keyBtns);
|
|
814
|
+
container.appendChild(keyGroup);
|
|
815
|
+
|
|
816
|
+
// Quota
|
|
817
|
+
if (quotaRemaining !== null) {
|
|
818
|
+
const quotaGroup = document.createElement("div");
|
|
819
|
+
quotaGroup.className = "skills-settings-group";
|
|
820
|
+
quotaGroup.innerHTML = `<div class="skills-settings-label">Daily Quota</div><div class="skills-quota">${quotaRemaining} requests remaining (resets midnight UTC)</div>`;
|
|
821
|
+
container.appendChild(quotaGroup);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Default scope
|
|
825
|
+
const scopeGroup = document.createElement("div");
|
|
826
|
+
scopeGroup.className = "skills-settings-group";
|
|
827
|
+
scopeGroup.innerHTML = '<div class="skills-settings-label">Default Install Scope</div>';
|
|
828
|
+
const scopeSelect = document.createElement("select");
|
|
829
|
+
scopeSelect.innerHTML = '<option value="project">Project (.claude/)</option><option value="global">Global (~/.claude/)</option>';
|
|
830
|
+
scopeSelect.value = defaultScope;
|
|
831
|
+
scopeSelect.addEventListener("change", () => {
|
|
832
|
+
defaultScope = scopeSelect.value;
|
|
833
|
+
ctx.api.saveSkillsConfig({ defaultScope });
|
|
834
|
+
});
|
|
835
|
+
scopeGroup.appendChild(scopeSelect);
|
|
836
|
+
container.appendChild(scopeGroup);
|
|
837
|
+
|
|
838
|
+
// Default search mode
|
|
839
|
+
const modeGroup = document.createElement("div");
|
|
840
|
+
modeGroup.className = "skills-settings-group";
|
|
841
|
+
modeGroup.innerHTML = '<div class="skills-settings-label">Default Search Mode</div>';
|
|
842
|
+
const modeSelect = document.createElement("select");
|
|
843
|
+
modeSelect.innerHTML = '<option value="keyword">Keyword</option><option value="ai">AI Semantic</option>';
|
|
844
|
+
modeSelect.value = searchMode;
|
|
845
|
+
modeSelect.addEventListener("change", () => {
|
|
846
|
+
searchMode = modeSelect.value;
|
|
847
|
+
localStorage.setItem("claudeck-skills-mode", searchMode);
|
|
848
|
+
ctx.api.saveSkillsConfig({ searchMode });
|
|
849
|
+
});
|
|
850
|
+
modeGroup.appendChild(modeSelect);
|
|
851
|
+
container.appendChild(modeGroup);
|
|
852
|
+
|
|
853
|
+
settingsContent.appendChild(container);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ── Shared helpers ──────────────────────────────────────
|
|
857
|
+
|
|
858
|
+
function showEmpty(container, text) {
|
|
859
|
+
const empty = document.createElement("div");
|
|
860
|
+
empty.className = "skills-empty";
|
|
861
|
+
empty.innerHTML = `${ICON}<span>${text}</span>`;
|
|
862
|
+
container.appendChild(empty);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function showInitialState(container) {
|
|
866
|
+
const el = document.createElement("div");
|
|
867
|
+
el.className = "skills-initial-state";
|
|
868
|
+
el.innerHTML = `
|
|
869
|
+
<div class="skills-initial-icon">
|
|
870
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
871
|
+
</div>
|
|
872
|
+
<div class="skills-initial-title">Discover agent skills</div>
|
|
873
|
+
<div class="skills-initial-desc">Search for skills by name, or switch to Semantic mode to describe what you need in plain English.</div>
|
|
874
|
+
<div class="skills-initial-examples">
|
|
875
|
+
<span class="skills-initial-example">code-review</span>
|
|
876
|
+
<span class="skills-initial-example">commit-message</span>
|
|
877
|
+
<span class="skills-initial-example">testing</span>
|
|
878
|
+
</div>
|
|
879
|
+
`;
|
|
880
|
+
|
|
881
|
+
// Clicking an example fills the search input
|
|
882
|
+
el.querySelectorAll(".skills-initial-example").forEach((tag) => {
|
|
883
|
+
tag.addEventListener("click", () => {
|
|
884
|
+
const input = browseContent?.querySelector(".skills-search-input");
|
|
885
|
+
if (input) {
|
|
886
|
+
input.value = tag.textContent;
|
|
887
|
+
input.dispatchEvent(new Event("input"));
|
|
888
|
+
input.focus();
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
container.appendChild(el);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function showErrorBanner(container, message) {
|
|
897
|
+
const banner = document.createElement("div");
|
|
898
|
+
banner.className = "skills-error-banner";
|
|
899
|
+
banner.innerHTML = `<span>${message}</span>`;
|
|
900
|
+
|
|
901
|
+
if (message.includes("API key") || message.includes("INVALID")) {
|
|
902
|
+
const btn = document.createElement("button");
|
|
903
|
+
btn.textContent = "Re-enter Key";
|
|
904
|
+
btn.addEventListener("click", () => {
|
|
905
|
+
root.querySelector('[data-subtab="settings"]')?.click();
|
|
906
|
+
});
|
|
907
|
+
banner.appendChild(btn);
|
|
908
|
+
} else if (message.includes("quota") || message.includes("QUOTA")) {
|
|
909
|
+
banner.innerHTML = `<span>Daily quota reached — try again tomorrow</span>`;
|
|
910
|
+
} else {
|
|
911
|
+
const btn = document.createElement("button");
|
|
912
|
+
btn.textContent = "Retry";
|
|
913
|
+
btn.addEventListener("click", () => {
|
|
914
|
+
banner.remove();
|
|
915
|
+
const q = lastSearchQuery;
|
|
916
|
+
if (q) {
|
|
917
|
+
const input = browseContent?.querySelector(".skills-search-input");
|
|
918
|
+
if (input) { input.value = q; input.dispatchEvent(new Event("input")); }
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
banner.appendChild(btn);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
container.prepend(banner);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function clearBrowseSearch() {
|
|
928
|
+
const input = browseContent?.querySelector(".skills-search-input");
|
|
929
|
+
if (input) input.value = "";
|
|
930
|
+
lastSearchQuery = "";
|
|
931
|
+
localStorage.removeItem("claudeck-skills-query");
|
|
932
|
+
const results = browseContent?.querySelector(".skills-results");
|
|
933
|
+
if (results) {
|
|
934
|
+
results.innerHTML = "";
|
|
935
|
+
showInitialState(results);
|
|
936
|
+
}
|
|
937
|
+
const pagination = browseContent?.querySelector(".skills-pagination");
|
|
938
|
+
if (pagination) pagination.style.display = "none";
|
|
939
|
+
currentPage = 1;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function showConfirm({ title, message, confirmLabel = "Confirm", danger = false, onConfirm }) {
|
|
943
|
+
const overlay = document.createElement("div");
|
|
944
|
+
overlay.className = "skills-confirm-overlay";
|
|
945
|
+
|
|
946
|
+
const dialog = document.createElement("div");
|
|
947
|
+
dialog.className = "skills-confirm-dialog";
|
|
948
|
+
|
|
949
|
+
dialog.innerHTML = `
|
|
950
|
+
<div class="skills-confirm-title">${title}</div>
|
|
951
|
+
<div class="skills-confirm-message">${message}</div>
|
|
952
|
+
<div class="skills-confirm-actions">
|
|
953
|
+
<button class="skills-confirm-cancel">Cancel</button>
|
|
954
|
+
<button class="skills-confirm-ok ${danger ? "danger" : ""}">${confirmLabel}</button>
|
|
955
|
+
</div>
|
|
956
|
+
`;
|
|
957
|
+
|
|
958
|
+
const close = () => overlay.remove();
|
|
959
|
+
dialog.querySelector(".skills-confirm-cancel").addEventListener("click", close);
|
|
960
|
+
dialog.querySelector(".skills-confirm-ok").addEventListener("click", () => {
|
|
961
|
+
close();
|
|
962
|
+
onConfirm();
|
|
963
|
+
});
|
|
964
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|
|
965
|
+
overlay.addEventListener("keydown", (e) => { if (e.key === "Escape") close(); });
|
|
966
|
+
|
|
967
|
+
overlay.appendChild(dialog);
|
|
968
|
+
document.body.appendChild(overlay);
|
|
969
|
+
dialog.querySelector(".skills-confirm-cancel").focus();
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function showSkillToast(message, type = "success") {
|
|
973
|
+
const container = document.getElementById("toast-container");
|
|
974
|
+
if (!container) return;
|
|
975
|
+
|
|
976
|
+
const toast = document.createElement("div");
|
|
977
|
+
toast.className = `bg-toast ${type === "error" ? "bg-toast-error" : ""}`;
|
|
978
|
+
toast.innerHTML = `
|
|
979
|
+
<span class="bg-toast-dot ${type === "error" ? "error" : ""}"></span>
|
|
980
|
+
<div class="bg-toast-body">
|
|
981
|
+
<div class="bg-toast-label"${type === "error" ? ' style="color:var(--error)"' : ""}>Skills Marketplace</div>
|
|
982
|
+
<div class="bg-toast-title">${message}</div>
|
|
983
|
+
</div>
|
|
984
|
+
<button class="bg-toast-close" title="Dismiss">×</button>
|
|
985
|
+
`;
|
|
986
|
+
|
|
987
|
+
toast.querySelector(".bg-toast-close").addEventListener("click", () => {
|
|
988
|
+
toast.classList.add("toast-exit");
|
|
989
|
+
toast.addEventListener("animationend", () => toast.remove());
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
container.appendChild(toast);
|
|
993
|
+
|
|
994
|
+
// Auto-dismiss after 4s
|
|
995
|
+
setTimeout(() => {
|
|
996
|
+
if (toast.parentNode) {
|
|
997
|
+
toast.classList.add("toast-exit");
|
|
998
|
+
toast.addEventListener("animationend", () => toast.remove());
|
|
999
|
+
}
|
|
1000
|
+
}, 4000);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function refreshProjectCommands() {
|
|
1004
|
+
import("../features/projects.js").then(({ loadProjectCommands }) => loadProjectCommands());
|
|
1005
|
+
}
|