claudeup 3.14.0 → 3.16.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/package.json +1 -1
- package/src/data/skill-repos.js +11 -0
- package/src/data/skill-repos.ts +12 -0
- package/src/services/skills-manager.js +40 -13
- package/src/services/skills-manager.ts +38 -16
- package/src/ui/adapters/skillsAdapter.js +106 -0
- package/src/ui/adapters/skillsAdapter.ts +160 -0
- package/src/ui/components/primitives/ActionHints.js +13 -0
- package/src/ui/components/primitives/ActionHints.tsx +41 -0
- package/src/ui/components/primitives/DetailSection.js +7 -0
- package/src/ui/components/primitives/DetailSection.tsx +22 -0
- package/src/ui/components/primitives/KeyValueLine.js +8 -0
- package/src/ui/components/primitives/KeyValueLine.tsx +19 -0
- package/src/ui/components/primitives/ListCategoryRow.js +8 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +38 -0
- package/src/ui/components/primitives/MetaText.js +8 -0
- package/src/ui/components/primitives/MetaText.tsx +14 -0
- package/src/ui/components/primitives/ScopeDetail.js +32 -0
- package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
- package/src/ui/components/primitives/ScopeSquares.js +11 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +33 -0
- package/src/ui/components/primitives/SelectableRow.js +5 -0
- package/src/ui/components/primitives/SelectableRow.tsx +24 -0
- package/src/ui/components/primitives/index.js +8 -0
- package/src/ui/components/primitives/index.ts +9 -0
- package/src/ui/registry.js +1 -0
- package/src/ui/registry.ts +27 -0
- package/src/ui/renderers/skillRenderers.js +75 -0
- package/src/ui/renderers/skillRenderers.tsx +220 -0
- package/src/ui/screens/SkillsScreen.js +46 -195
- package/src/ui/screens/SkillsScreen.tsx +436 -796
- package/src/ui/theme.js +47 -0
- package/src/ui/theme.ts +53 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
2
|
import { useEffect, useCallback, useMemo, useState, useRef } from "react";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
@@ -12,17 +12,8 @@ import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
|
12
12
|
import { fetchAvailableSkills, fetchSkillFrontmatter, installSkill, uninstallSkill, } from "../../services/skills-manager.js";
|
|
13
13
|
import { searchSkills } from "../../services/skillsmp-client.js";
|
|
14
14
|
import { DEFAULT_SKILL_REPOS, RECOMMENDED_SKILLS } from "../../data/skill-repos.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return "";
|
|
18
|
-
if (stars >= 1000000)
|
|
19
|
-
return `★ ${(stars / 1000000).toFixed(1)}M`;
|
|
20
|
-
if (stars >= 10000)
|
|
21
|
-
return `★ ${Math.round(stars / 1000)}K`;
|
|
22
|
-
if (stars >= 1000)
|
|
23
|
-
return `★ ${(stars / 1000).toFixed(1)}K`;
|
|
24
|
-
return `★ ${stars}`;
|
|
25
|
-
}
|
|
15
|
+
import { buildSkillBrowserItems } from "../adapters/skillsAdapter.js";
|
|
16
|
+
import { renderSkillRow, renderSkillDetail } from "../renderers/skillRenderers.js";
|
|
26
17
|
export function SkillsScreen() {
|
|
27
18
|
const { state, dispatch } = useApp();
|
|
28
19
|
const { skills: skillsState } = state;
|
|
@@ -31,7 +22,7 @@ export function SkillsScreen() {
|
|
|
31
22
|
const isSearchActive = state.isSearching &&
|
|
32
23
|
state.currentRoute.screen === "skills" &&
|
|
33
24
|
!state.modal;
|
|
34
|
-
//
|
|
25
|
+
// ── Data fetching ─────────────────────────────────────────────────────────
|
|
35
26
|
const fetchData = useCallback(async () => {
|
|
36
27
|
dispatch({ type: "SKILLS_DATA_LOADING" });
|
|
37
28
|
try {
|
|
@@ -48,7 +39,7 @@ export function SkillsScreen() {
|
|
|
48
39
|
useEffect(() => {
|
|
49
40
|
fetchData();
|
|
50
41
|
}, [fetchData, state.dataRefreshVersion]);
|
|
51
|
-
// Remote search
|
|
42
|
+
// ── Remote search (debounced, cached) ─────────────────────────────────────
|
|
52
43
|
const [searchResults, setSearchResults] = useState([]);
|
|
53
44
|
const [isSearchLoading, setIsSearchLoading] = useState(false);
|
|
54
45
|
const searchTimerRef = useRef(null);
|
|
@@ -60,7 +51,6 @@ export function SkillsScreen() {
|
|
|
60
51
|
setIsSearchLoading(false);
|
|
61
52
|
return;
|
|
62
53
|
}
|
|
63
|
-
// Check cache first
|
|
64
54
|
const cached = searchCacheRef.current.get(query);
|
|
65
55
|
if (cached) {
|
|
66
56
|
setSearchResults(cached);
|
|
@@ -106,7 +96,7 @@ export function SkillsScreen() {
|
|
|
106
96
|
clearTimeout(searchTimerRef.current);
|
|
107
97
|
};
|
|
108
98
|
}, [skillsState.searchQuery]);
|
|
109
|
-
//
|
|
99
|
+
// ── Disk scan for installed skills ────────────────────────────────────────
|
|
110
100
|
const [installedFromDisk, setInstalledFromDisk] = useState({ user: new Set(), project: new Set() });
|
|
111
101
|
useEffect(() => {
|
|
112
102
|
async function scanDisk() {
|
|
@@ -130,8 +120,7 @@ export function SkillsScreen() {
|
|
|
130
120
|
}
|
|
131
121
|
scanDisk();
|
|
132
122
|
}, [state.projectPath, state.dataRefreshVersion]);
|
|
133
|
-
//
|
|
134
|
-
// Enriched with disk-based install status immediately
|
|
123
|
+
// ── Derived data ──────────────────────────────────────────────────────────
|
|
135
124
|
const staticRecommended = useMemo(() => {
|
|
136
125
|
return RECOMMENDED_SKILLS.map((r) => {
|
|
137
126
|
const slug = r.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -149,22 +138,22 @@ export function SkillsScreen() {
|
|
|
149
138
|
installedScope: isProj ? "project" : isUser ? "user" : null,
|
|
150
139
|
hasUpdate: false,
|
|
151
140
|
isRecommended: true,
|
|
152
|
-
stars:
|
|
141
|
+
stars: r.stars,
|
|
153
142
|
};
|
|
154
143
|
});
|
|
155
144
|
}, [installedFromDisk]);
|
|
156
|
-
// Merge static recommended with fetched data (to get install status + stars)
|
|
157
145
|
const mergedRecommended = useMemo(() => {
|
|
158
146
|
if (skillsState.skills.status !== "success")
|
|
159
147
|
return staticRecommended;
|
|
160
148
|
const fetched = skillsState.skills.data.filter((s) => s.isRecommended);
|
|
161
|
-
// Merge: keep fetched data (has stars + install status), fall back to static
|
|
162
149
|
return staticRecommended.map((staticSkill) => {
|
|
163
150
|
const match = fetched.find((f) => f.source.repo === staticSkill.source.repo && f.name === staticSkill.name);
|
|
164
|
-
|
|
151
|
+
if (!match)
|
|
152
|
+
return staticSkill;
|
|
153
|
+
// Merge: prefer fetched data but keep static stars as fallback
|
|
154
|
+
return { ...staticSkill, ...match, stars: match.stars || staticSkill.stars };
|
|
165
155
|
});
|
|
166
156
|
}, [staticRecommended, skillsState.skills]);
|
|
167
|
-
// Build installed skills list from disk (always available)
|
|
168
157
|
const installedSkills = useMemo(() => {
|
|
169
158
|
const all = [];
|
|
170
159
|
for (const [scope, names] of [
|
|
@@ -172,7 +161,6 @@ export function SkillsScreen() {
|
|
|
172
161
|
["project", installedFromDisk.project],
|
|
173
162
|
]) {
|
|
174
163
|
for (const name of names) {
|
|
175
|
-
// Skip if already in the list (avoid dupes)
|
|
176
164
|
if (all.some((s) => s.name === name))
|
|
177
165
|
continue;
|
|
178
166
|
all.push({
|
|
@@ -192,105 +180,32 @@ export function SkillsScreen() {
|
|
|
192
180
|
}
|
|
193
181
|
return all;
|
|
194
182
|
}, [installedFromDisk]);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
? mergedRecommended.filter((s) => s.name.toLowerCase().includes(query) ||
|
|
222
|
-
(s.description || "").toLowerCase().includes(query))
|
|
223
|
-
: mergedRecommended;
|
|
224
|
-
items.push({
|
|
225
|
-
id: "cat:recommended",
|
|
226
|
-
type: "category",
|
|
227
|
-
label: "Recommended",
|
|
228
|
-
categoryKey: "recommended",
|
|
229
|
-
});
|
|
230
|
-
for (const skill of filteredRec) {
|
|
231
|
-
items.push({
|
|
232
|
-
id: `skill:${skill.id}`,
|
|
233
|
-
type: "skill",
|
|
234
|
-
label: skill.name,
|
|
235
|
-
skill,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
// ── SEARCH MODE ──
|
|
239
|
-
if (query.length >= 2) {
|
|
240
|
-
// Loading and no-results handled in listPanel, not as list items
|
|
241
|
-
if (!isSearchLoading && searchResults.length > 0) {
|
|
242
|
-
// Dedup against recommended
|
|
243
|
-
const recNames = new Set(mergedRecommended.map((s) => s.name));
|
|
244
|
-
const deduped = searchResults.filter((s) => !recNames.has(s.name));
|
|
245
|
-
if (deduped.length > 0) {
|
|
246
|
-
items.push({
|
|
247
|
-
id: "cat:search",
|
|
248
|
-
type: "category",
|
|
249
|
-
label: `Search (${deduped.length})`,
|
|
250
|
-
categoryKey: "popular",
|
|
251
|
-
});
|
|
252
|
-
for (const skill of deduped) {
|
|
253
|
-
items.push({
|
|
254
|
-
id: `skill:${skill.id}`,
|
|
255
|
-
type: "skill",
|
|
256
|
-
label: skill.name,
|
|
257
|
-
skill,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
// No-results message handled in listPanel below, not as a list item
|
|
263
|
-
return items;
|
|
264
|
-
}
|
|
265
|
-
// ── POPULAR (default, no search query) ──
|
|
266
|
-
// Loading state handled in listPanel, not as category header
|
|
267
|
-
if (skillsState.skills.status === "success") {
|
|
268
|
-
const popularSkills = skillsState.skills.data
|
|
269
|
-
.filter((s) => !s.isRecommended)
|
|
270
|
-
.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
|
|
271
|
-
if (popularSkills.length > 0) {
|
|
272
|
-
items.push({
|
|
273
|
-
id: "cat:popular",
|
|
274
|
-
type: "category",
|
|
275
|
-
label: "Popular",
|
|
276
|
-
categoryKey: "popular",
|
|
277
|
-
});
|
|
278
|
-
for (const skill of popularSkills) {
|
|
279
|
-
items.push({
|
|
280
|
-
id: `skill:${skill.id}`,
|
|
281
|
-
type: "skill",
|
|
282
|
-
label: skill.name,
|
|
283
|
-
skill,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return items;
|
|
289
|
-
}, [skillsState.skills, skillsState.searchQuery, searchResults, isSearchLoading, mergedRecommended]);
|
|
290
|
-
const selectableItems = useMemo(() => allItems.filter((item) => item.type === "skill" || item.type === "category"), [allItems]);
|
|
291
|
-
const selectedItem = selectableItems[skillsState.selectedIndex];
|
|
292
|
-
const selectedSkill = selectedItem?.type === "skill" ? selectedItem.skill : undefined;
|
|
293
|
-
// Lazy-load frontmatter for selected skill
|
|
183
|
+
const popularSkills = useMemo(() => {
|
|
184
|
+
if (skillsState.skills.status !== "success")
|
|
185
|
+
return [];
|
|
186
|
+
return skillsState.skills.data
|
|
187
|
+
.filter((s) => !s.isRecommended)
|
|
188
|
+
.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
|
|
189
|
+
}, [skillsState.skills]);
|
|
190
|
+
// ── List items (built by adapter) ─────────────────────────────────────────
|
|
191
|
+
const allItems = useMemo(() => buildSkillBrowserItems({
|
|
192
|
+
recommended: mergedRecommended,
|
|
193
|
+
popular: popularSkills,
|
|
194
|
+
installed: installedSkills,
|
|
195
|
+
searchResults,
|
|
196
|
+
query: skillsState.searchQuery,
|
|
197
|
+
isSearchLoading,
|
|
198
|
+
}), [
|
|
199
|
+
mergedRecommended,
|
|
200
|
+
popularSkills,
|
|
201
|
+
installedSkills,
|
|
202
|
+
searchResults,
|
|
203
|
+
skillsState.searchQuery,
|
|
204
|
+
isSearchLoading,
|
|
205
|
+
]);
|
|
206
|
+
const selectedItem = allItems[skillsState.selectedIndex];
|
|
207
|
+
const selectedSkill = selectedItem?.kind === "skill" ? selectedItem.skill : undefined;
|
|
208
|
+
// ── Lazy-load frontmatter for selected skill ───────────────────────────────
|
|
294
209
|
useEffect(() => {
|
|
295
210
|
if (!selectedSkill || selectedSkill.frontmatter)
|
|
296
211
|
return;
|
|
@@ -302,7 +217,7 @@ export function SkillsScreen() {
|
|
|
302
217
|
});
|
|
303
218
|
}).catch(() => { });
|
|
304
219
|
}, [selectedSkill?.id, dispatch]);
|
|
305
|
-
//
|
|
220
|
+
// ── Action handlers ───────────────────────────────────────────────────────
|
|
306
221
|
const handleInstall = useCallback(async (scope) => {
|
|
307
222
|
if (!selectedSkill)
|
|
308
223
|
return;
|
|
@@ -310,7 +225,6 @@ export function SkillsScreen() {
|
|
|
310
225
|
try {
|
|
311
226
|
await installSkill(selectedSkill, scope, state.projectPath);
|
|
312
227
|
modal.hideModal();
|
|
313
|
-
// Refetch to pick up install status for all skills including recommended
|
|
314
228
|
await fetchData();
|
|
315
229
|
await modal.message("Installed", `${selectedSkill.name} installed to ${scope === "user" ? "~/.claude/skills/" : ".claude/skills/"}`, "success");
|
|
316
230
|
}
|
|
@@ -319,7 +233,6 @@ export function SkillsScreen() {
|
|
|
319
233
|
await modal.message("Error", `Failed to install: ${error}`, "error");
|
|
320
234
|
}
|
|
321
235
|
}, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
|
|
322
|
-
// Uninstall handler
|
|
323
236
|
const handleUninstall = useCallback(async () => {
|
|
324
237
|
if (!selectedSkill || !selectedSkill.installed)
|
|
325
238
|
return;
|
|
@@ -333,7 +246,6 @@ export function SkillsScreen() {
|
|
|
333
246
|
try {
|
|
334
247
|
await uninstallSkill(selectedSkill.name, scope, state.projectPath);
|
|
335
248
|
modal.hideModal();
|
|
336
|
-
// Refetch to pick up uninstall status
|
|
337
249
|
await fetchData();
|
|
338
250
|
await modal.message("Uninstalled", `${selectedSkill.name} removed.`, "success");
|
|
339
251
|
}
|
|
@@ -342,12 +254,11 @@ export function SkillsScreen() {
|
|
|
342
254
|
await modal.message("Error", `Failed to uninstall: ${error}`, "error");
|
|
343
255
|
}
|
|
344
256
|
}, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
|
|
345
|
-
// Keyboard handling
|
|
257
|
+
// ── Keyboard handling ─────────────────────────────────────────────────────
|
|
346
258
|
useKeyboard((event) => {
|
|
347
259
|
if (state.modal)
|
|
348
260
|
return;
|
|
349
261
|
const hasQuery = skillsState.searchQuery.length > 0;
|
|
350
|
-
// Escape: clear search
|
|
351
262
|
if (event.name === "escape") {
|
|
352
263
|
if (hasQuery || isSearchActive) {
|
|
353
264
|
dispatch({ type: "SKILLS_SET_SEARCH", query: "" });
|
|
@@ -356,7 +267,6 @@ export function SkillsScreen() {
|
|
|
356
267
|
}
|
|
357
268
|
return;
|
|
358
269
|
}
|
|
359
|
-
// Backspace: remove last char
|
|
360
270
|
if (event.name === "backspace" || event.name === "delete") {
|
|
361
271
|
if (hasQuery) {
|
|
362
272
|
const newQuery = skillsState.searchQuery.slice(0, -1);
|
|
@@ -367,7 +277,6 @@ export function SkillsScreen() {
|
|
|
367
277
|
}
|
|
368
278
|
return;
|
|
369
279
|
}
|
|
370
|
-
// Navigation — always works; exits search mode on navigate
|
|
371
280
|
if (event.name === "up" || event.name === "k") {
|
|
372
281
|
if (isSearchActive)
|
|
373
282
|
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
@@ -378,11 +287,10 @@ export function SkillsScreen() {
|
|
|
378
287
|
if (event.name === "down" || event.name === "j") {
|
|
379
288
|
if (isSearchActive)
|
|
380
289
|
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
381
|
-
const newIndex = Math.min(Math.max(0,
|
|
290
|
+
const newIndex = Math.min(Math.max(0, allItems.length - 1), skillsState.selectedIndex + 1);
|
|
382
291
|
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
383
292
|
return;
|
|
384
293
|
}
|
|
385
|
-
// Enter — install (always works)
|
|
386
294
|
if (event.name === "return" || event.name === "enter") {
|
|
387
295
|
if (isSearchActive) {
|
|
388
296
|
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
@@ -393,11 +301,10 @@ export function SkillsScreen() {
|
|
|
393
301
|
}
|
|
394
302
|
return;
|
|
395
303
|
}
|
|
396
|
-
// When actively typing in search, letters go to the query
|
|
397
304
|
if (isSearchActive) {
|
|
398
305
|
if (event.name === "k" || event.name === "j") {
|
|
399
306
|
const delta = event.name === "k" ? -1 : 1;
|
|
400
|
-
const newIndex = Math.max(0, Math.min(
|
|
307
|
+
const newIndex = Math.max(0, Math.min(allItems.length - 1, skillsState.selectedIndex + delta));
|
|
401
308
|
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
402
309
|
return;
|
|
403
310
|
}
|
|
@@ -407,7 +314,6 @@ export function SkillsScreen() {
|
|
|
407
314
|
}
|
|
408
315
|
return;
|
|
409
316
|
}
|
|
410
|
-
// Action shortcuts (work when not actively typing, even with filter visible)
|
|
411
317
|
if (event.name === "u" && selectedSkill) {
|
|
412
318
|
if (selectedSkill.installed && selectedSkill.installedScope === "user")
|
|
413
319
|
handleUninstall();
|
|
@@ -420,14 +326,10 @@ export function SkillsScreen() {
|
|
|
420
326
|
else
|
|
421
327
|
handleInstall("project");
|
|
422
328
|
}
|
|
423
|
-
else if (event.name === "d" && selectedSkill?.installed) {
|
|
424
|
-
handleUninstall();
|
|
425
|
-
}
|
|
426
329
|
else if (event.name === "r") {
|
|
427
330
|
fetchData();
|
|
428
331
|
}
|
|
429
332
|
else if (event.name === "o" && selectedSkill) {
|
|
430
|
-
// Open in browser
|
|
431
333
|
const repo = selectedSkill.source.repo;
|
|
432
334
|
const repoPath = selectedSkill.repoPath?.replace("/SKILL.md", "") || "";
|
|
433
335
|
if (repo && repo !== "local") {
|
|
@@ -440,67 +342,16 @@ export function SkillsScreen() {
|
|
|
440
342
|
});
|
|
441
343
|
}
|
|
442
344
|
}
|
|
443
|
-
// "/" to enter search mode
|
|
444
345
|
else if (event.name === "/") {
|
|
445
346
|
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
446
347
|
}
|
|
447
348
|
});
|
|
448
|
-
|
|
449
|
-
if (item.type === "category") {
|
|
450
|
-
const isRec = item.categoryKey === "recommended";
|
|
451
|
-
const isInstalled = item.categoryKey === "installed";
|
|
452
|
-
const bgColor = isInstalled ? "#7e57c2" : isRec ? "#2e7d32" : "#00695c";
|
|
453
|
-
const star = isRec ? "★ " : isInstalled ? "● " : "";
|
|
454
|
-
if (isSelected) {
|
|
455
|
-
return (_jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", star, item.label, " "] }) }));
|
|
456
|
-
}
|
|
457
|
-
return (_jsx("text", { bg: bgColor, fg: "white", children: _jsxs("strong", { children: [" ", star, item.label, " "] }) }));
|
|
458
|
-
}
|
|
459
|
-
if (item.type === "skill" && item.skill) {
|
|
460
|
-
const skill = item.skill;
|
|
461
|
-
const starsStr = formatStars(skill.stars);
|
|
462
|
-
const hasUser = skill.installedScope === "user";
|
|
463
|
-
const hasProject = skill.installedScope === "project";
|
|
464
|
-
const nameColor = skill.installed ? "white" : "gray";
|
|
465
|
-
if (isSelected) {
|
|
466
|
-
return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", _jsx("span", { children: hasUser ? "■" : "□" }), _jsx("span", { children: hasProject ? "■" : "□" }), " ", skill.name, skill.hasUpdate ? " ⬆" : "", starsStr ? ` ${starsStr}` : "", " "] }));
|
|
467
|
-
}
|
|
468
|
-
return (_jsxs("text", { children: [_jsx("span", { children: " " }), _jsx("span", { fg: hasUser ? "cyan" : "#333333", children: "\u25A0" }), _jsx("span", { fg: hasProject ? "green" : "#333333", children: "\u25A0" }), _jsx("span", { children: " " }), _jsx("span", { fg: nameColor, children: skill.name }), skill.hasUpdate && _jsx("span", { fg: "yellow", children: " \u2B06" }), starsStr && (_jsxs("span", { fg: "yellow", children: [" ", starsStr] }))] }));
|
|
469
|
-
}
|
|
470
|
-
return _jsx("text", { fg: "gray", children: item.label });
|
|
471
|
-
};
|
|
472
|
-
const renderDetail = () => {
|
|
473
|
-
if (skillsState.skills.status === "loading") {
|
|
474
|
-
return _jsx("text", { fg: "gray", children: "Loading skills..." });
|
|
475
|
-
}
|
|
476
|
-
if (skillsState.skills.status === "error") {
|
|
477
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "red", children: "Failed to load skills" }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: skillsState.skills.error.message }) })] }));
|
|
478
|
-
}
|
|
479
|
-
if (!selectedItem) {
|
|
480
|
-
return _jsx("text", { fg: "gray", children: "Select a skill to see details" });
|
|
481
|
-
}
|
|
482
|
-
if (selectedItem.type === "category") {
|
|
483
|
-
const isRec = selectedItem.categoryKey === "recommended";
|
|
484
|
-
const isNoResults = selectedItem.categoryKey === "no-results";
|
|
485
|
-
if (isNoResults) {
|
|
486
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "yellow", children: _jsx("strong", { children: "No skills found" }) }), _jsx("box", { marginTop: 1, children: _jsxs("text", { fg: "gray", children: ["Nothing matched \"", skillsState.searchQuery, "\"."] }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Try a different search term, or if you think this is a mistake, create an issue at:" }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#5c9aff", children: "github.com/MadAppGang/magus/issues" }) }), _jsx("box", { marginTop: 2, children: _jsx("text", { fg: "gray", children: "Press Esc to clear the search." }) })] }));
|
|
487
|
-
}
|
|
488
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: isRec ? "green" : "cyan", children: _jsxs("strong", { children: [isRec ? "★ " : "", selectedItem.label] }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: isRec ? "Curated skills recommended for most projects" : "Popular skills sorted by stars" }) })] }));
|
|
489
|
-
}
|
|
490
|
-
if (!selectedSkill)
|
|
491
|
-
return null;
|
|
492
|
-
const fm = selectedSkill.frontmatter;
|
|
493
|
-
const description = fm?.description || selectedSkill.description || "Loading...";
|
|
494
|
-
const scopeColor = selectedSkill.installedScope === "user" ? "cyan" : "green";
|
|
495
|
-
const starsStr = formatStars(selectedSkill.stars);
|
|
496
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { fg: "cyan", children: [_jsx("strong", { children: selectedSkill.name }), starsStr && _jsxs("span", { fg: "yellow", children: [" ", starsStr] })] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "white", children: description }) }), fm?.category && (_jsx("box", { marginTop: 1, children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Category " }), _jsx("span", { fg: "cyan", children: fm.category })] }) })), fm?.author && (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Author " }), _jsx("span", { children: fm.author })] }) })), fm?.version && (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Version " }), _jsx("span", { children: fm.version })] }) })), fm?.tags && fm.tags.length > 0 && (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Tags " }), _jsx("span", { children: fm.tags.join(", ") })] }) })), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Source " }), _jsx("span", { fg: "#5c9aff", children: selectedSkill.source.repo })] }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: " " }), _jsx("span", { fg: "gray", children: selectedSkill.repoPath })] })] }), selectedSkill.installed && selectedSkill.installedScope && (_jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { children: "─".repeat(24) }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Installed " }), _jsxs("span", { fg: scopeColor, children: [selectedSkill.installedScope === "user"
|
|
497
|
-
? "~/.claude/skills/"
|
|
498
|
-
: ".claude/skills/", selectedSkill.name, "/"] })] })] })), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { children: "─".repeat(24) }), _jsx("text", { children: _jsx("strong", { children: "Install scope:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { bg: "cyan", fg: "black", children: " u " }), _jsx("span", { fg: selectedSkill.installedScope === "user" ? "cyan" : "gray", children: selectedSkill.installedScope === "user" ? " ● " : " ○ " }), _jsx("span", { fg: "cyan", children: "User" }), _jsx("span", { fg: "gray", children: " ~/.claude/skills/" })] }), _jsxs("text", { children: [_jsx("span", { bg: "green", fg: "black", children: " p " }), _jsx("span", { fg: selectedSkill.installedScope === "project" ? "green" : "gray", children: selectedSkill.installedScope === "project" ? " ● " : " ○ " }), _jsx("span", { fg: "green", children: "Project" }), _jsx("span", { fg: "gray", children: " .claude/skills/" })] })] })] }), selectedSkill.hasUpdate && (_jsx("box", { marginTop: 1, children: _jsx("text", { bg: "yellow", fg: "black", children: " UPDATE AVAILABLE " }) })), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [!selectedSkill.installed && (_jsx("text", { fg: "gray", children: "Press u/p to install in scope" })), selectedSkill.installed && (_jsx("text", { fg: "gray", children: "Press d to uninstall" }))] })] }));
|
|
499
|
-
};
|
|
349
|
+
// ── Status line ───────────────────────────────────────────────────────────
|
|
500
350
|
const skills = skillsState.skills.status === "success" ? skillsState.skills.data : [];
|
|
501
351
|
const installedCount = skills.filter((s) => s.installed).length;
|
|
502
352
|
const query = skillsState.searchQuery.trim();
|
|
503
|
-
const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Skills: " }), _jsxs("span", { fg: "cyan", children: [installedCount, " installed"] }), query.length >= 2 && isSearchLoading && (_jsx("span", { fg: "yellow", children: " \u2502 searching..." })), query.length >= 2 && !isSearchLoading && searchResults.length > 0 && (_jsxs("span", { fg: "green", children: [" \u2502 ", searchResults.length, " found"] })), !query &&
|
|
353
|
+
const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Skills: " }), _jsxs("span", { fg: "cyan", children: [installedCount, " installed"] }), query.length >= 2 && isSearchLoading && (_jsx("span", { fg: "yellow", children: " \u2502 searching..." })), query.length >= 2 && !isSearchLoading && searchResults.length > 0 && (_jsxs("span", { fg: "green", children: [" \u2502 ", searchResults.length, " found"] })), !query && _jsx("span", { fg: "gray", children: " \u2502 89K+ searchable" })] }));
|
|
354
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
504
355
|
return (_jsx(ScreenLayout, { title: "claudeup Skills", currentScreen: "skills", statusLine: statusContent, search: skillsState.searchQuery || isSearchActive
|
|
505
356
|
? {
|
|
506
357
|
isActive: isSearchActive,
|
|
@@ -509,6 +360,6 @@ export function SkillsScreen() {
|
|
|
509
360
|
}
|
|
510
361
|
: undefined, footerHints: isSearchActive
|
|
511
362
|
? "type to filter │ Enter:done │ Esc:clear"
|
|
512
|
-
: "u:user │ p:project │ o:open │ /:search", listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items:
|
|
363
|
+
: "u:user │ p:project │ o:open │ /:search", listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items: allItems, selectedIndex: skillsState.selectedIndex, renderItem: renderSkillRow, maxHeight: dimensions.listPanelHeight }), !query && skillsState.skills.status === "loading" && (_jsx("box", { marginTop: 2, paddingLeft: 2, children: _jsx("text", { fg: "yellow", children: "Loading popular skills..." }) })), query.length >= 2 && isSearchLoading && (_jsx("box", { marginTop: 2, paddingLeft: 2, children: _jsxs("text", { fg: "yellow", children: ["Searching for \"", skillsState.searchQuery, "\"..."] }) })), query.length >= 2 && !isSearchLoading && searchResults.length === 0 && (_jsx(EmptyFilterState, { query: skillsState.searchQuery, entityName: "skills" }))] }), detailPanel: renderSkillDetail(selectedItem) }));
|
|
513
364
|
}
|
|
514
365
|
export default SkillsScreen;
|