claudeup 4.5.5 → 4.6.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.
- package/package.json +1 -1
- package/src/data/skill-repos.js +56 -0
- package/src/data/skill-repos.ts +70 -1
- package/src/services/claude-settings.js +31 -4
- package/src/services/claude-settings.ts +31 -4
- package/src/services/skills-manager.js +50 -2
- package/src/services/skills-manager.ts +65 -2
- package/src/types/index.ts +30 -1
- package/src/ui/adapters/skillsAdapter.js +57 -9
- package/src/ui/adapters/skillsAdapter.ts +72 -10
- package/src/ui/components/ScrollableList.js +8 -20
- package/src/ui/components/ScrollableList.tsx +16 -29
- package/src/ui/renderers/skillRenderers.js +72 -9
- package/src/ui/renderers/skillRenderers.tsx +176 -11
- package/src/ui/screens/PluginsScreen.js +1 -1
- package/src/ui/screens/PluginsScreen.tsx +1 -0
- package/src/ui/screens/SkillsScreen.js +177 -39
- package/src/ui/screens/SkillsScreen.tsx +199 -34
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SkillInfo } from "../../types/index.js";
|
|
1
|
+
import type { SkillInfo, SkillSetInfo } from "../../types/index.js";
|
|
2
2
|
|
|
3
3
|
// ─── Item types ───────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
@@ -19,9 +19,19 @@ export interface SkillSkillItem {
|
|
|
19
19
|
kind: "skill";
|
|
20
20
|
label: string;
|
|
21
21
|
skill: SkillInfo;
|
|
22
|
+
/** Extra indent level for child skills inside an expanded skill set */
|
|
23
|
+
indent?: number;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
export
|
|
26
|
+
export interface SkillSetItem {
|
|
27
|
+
id: string;
|
|
28
|
+
kind: "skillset";
|
|
29
|
+
label: string;
|
|
30
|
+
skillSet: SkillSetInfo;
|
|
31
|
+
expanded: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SkillBrowserItem = SkillCategoryItem | SkillSkillItem | SkillSetItem;
|
|
25
35
|
|
|
26
36
|
// ─── Adapter ─────────────────────────────────────────────────────────────────
|
|
27
37
|
|
|
@@ -32,6 +42,8 @@ export interface BuildSkillBrowserItemsArgs {
|
|
|
32
42
|
searchResults: SkillInfo[];
|
|
33
43
|
query: string;
|
|
34
44
|
isSearchLoading: boolean;
|
|
45
|
+
skillSets?: SkillSetInfo[];
|
|
46
|
+
expandedSets?: Set<string>;
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
/**
|
|
@@ -45,6 +57,8 @@ export function buildSkillBrowserItems({
|
|
|
45
57
|
searchResults,
|
|
46
58
|
query,
|
|
47
59
|
isSearchLoading,
|
|
60
|
+
skillSets = [],
|
|
61
|
+
expandedSets = new Set(),
|
|
48
62
|
}: BuildSkillBrowserItemsArgs): SkillBrowserItem[] {
|
|
49
63
|
const lowerQuery = query.toLowerCase();
|
|
50
64
|
const items: SkillBrowserItem[] = [];
|
|
@@ -63,7 +77,7 @@ export function buildSkillBrowserItems({
|
|
|
63
77
|
categoryKey: "installed",
|
|
64
78
|
count: installedFiltered.length,
|
|
65
79
|
tone: "purple",
|
|
66
|
-
star: "
|
|
80
|
+
star: "\u25CF ",
|
|
67
81
|
});
|
|
68
82
|
for (const skill of installedFiltered) {
|
|
69
83
|
items.push({
|
|
@@ -75,7 +89,16 @@ export function buildSkillBrowserItems({
|
|
|
75
89
|
}
|
|
76
90
|
}
|
|
77
91
|
|
|
78
|
-
// ── RECOMMENDED:
|
|
92
|
+
// ── RECOMMENDED: skill sets + individual skills, all as first-class items ──
|
|
93
|
+
const filteredSets = lowerQuery
|
|
94
|
+
? skillSets.filter((s) => {
|
|
95
|
+
if (s.name.toLowerCase().includes(lowerQuery)) return true;
|
|
96
|
+
if (s.description.toLowerCase().includes(lowerQuery)) return true;
|
|
97
|
+
if (s.loaded && s.skills.some((sk) => sk.name.toLowerCase().includes(lowerQuery))) return true;
|
|
98
|
+
return false;
|
|
99
|
+
})
|
|
100
|
+
: skillSets;
|
|
101
|
+
|
|
79
102
|
const filteredRec = lowerQuery
|
|
80
103
|
? recommended.filter(
|
|
81
104
|
(s) =>
|
|
@@ -84,16 +107,47 @@ export function buildSkillBrowserItems({
|
|
|
84
107
|
)
|
|
85
108
|
: recommended;
|
|
86
109
|
|
|
110
|
+
const recommendedCount = filteredSets.length + filteredRec.length;
|
|
111
|
+
|
|
87
112
|
items.push({
|
|
88
113
|
id: "cat:recommended",
|
|
89
114
|
kind: "category",
|
|
90
115
|
label: "Recommended",
|
|
91
116
|
title: "Recommended",
|
|
92
117
|
categoryKey: "recommended",
|
|
93
|
-
count:
|
|
118
|
+
count: recommendedCount,
|
|
94
119
|
tone: "green",
|
|
95
|
-
star: "
|
|
120
|
+
star: "\u2605 ",
|
|
96
121
|
});
|
|
122
|
+
|
|
123
|
+
// Skill sets first within recommended
|
|
124
|
+
for (const set of filteredSets) {
|
|
125
|
+
const isExpanded = expandedSets.has(set.id);
|
|
126
|
+
items.push({
|
|
127
|
+
id: `skillset:${set.id}`,
|
|
128
|
+
kind: "skillset",
|
|
129
|
+
label: set.name,
|
|
130
|
+
skillSet: set,
|
|
131
|
+
expanded: isExpanded,
|
|
132
|
+
});
|
|
133
|
+
// When expanded, show child skills as indented skill items
|
|
134
|
+
if (isExpanded && set.loaded) {
|
|
135
|
+
const childSkills = lowerQuery
|
|
136
|
+
? set.skills.filter((sk) => sk.name.toLowerCase().includes(lowerQuery))
|
|
137
|
+
: set.skills;
|
|
138
|
+
for (const skill of childSkills) {
|
|
139
|
+
items.push({
|
|
140
|
+
id: `skill:${skill.id}`,
|
|
141
|
+
kind: "skill",
|
|
142
|
+
label: skill.name,
|
|
143
|
+
skill,
|
|
144
|
+
indent: 2,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Then individual recommended skills
|
|
97
151
|
for (const skill of filteredRec) {
|
|
98
152
|
items.push({
|
|
99
153
|
id: `skill:${skill.id}`,
|
|
@@ -135,18 +189,26 @@ export function buildSkillBrowserItems({
|
|
|
135
189
|
}
|
|
136
190
|
|
|
137
191
|
// ── POPULAR (default, no search query) — only skills with meaningful stars ──
|
|
138
|
-
|
|
139
|
-
|
|
192
|
+
// Dedup by name — API can return same skill name from different repos
|
|
193
|
+
const seenPopular = new Set<string>();
|
|
194
|
+
const popularDeduped = popular
|
|
195
|
+
.filter((s) => (s.stars ?? 0) >= 5)
|
|
196
|
+
.filter((s) => {
|
|
197
|
+
if (seenPopular.has(s.name)) return false;
|
|
198
|
+
seenPopular.add(s.name);
|
|
199
|
+
return true;
|
|
200
|
+
});
|
|
201
|
+
if (popularDeduped.length > 0) {
|
|
140
202
|
items.push({
|
|
141
203
|
id: "cat:popular",
|
|
142
204
|
kind: "category",
|
|
143
205
|
label: "Popular",
|
|
144
206
|
title: "Popular",
|
|
145
207
|
categoryKey: "popular",
|
|
146
|
-
count:
|
|
208
|
+
count: popularDeduped.length,
|
|
147
209
|
tone: "teal",
|
|
148
210
|
});
|
|
149
|
-
for (const skill of
|
|
211
|
+
for (const skill of popularDeduped) {
|
|
150
212
|
items.push({
|
|
151
213
|
id: `skill:${skill.id}`,
|
|
152
214
|
kind: "skill",
|
|
@@ -1,22 +1,7 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect, useMemo } from "react";
|
|
3
|
-
|
|
4
|
-
export function ScrollableList({ items, selectedIndex, renderItem, maxHeight, showScrollIndicators = true, onSelect, focused = false, }) {
|
|
3
|
+
export function ScrollableList({ items, selectedIndex, renderItem, maxHeight, showScrollIndicators = true, getKey, }) {
|
|
5
4
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
6
|
-
// Handle keyboard navigation
|
|
7
|
-
useKeyboardHandler((input, key) => {
|
|
8
|
-
if (!focused || !onSelect)
|
|
9
|
-
return;
|
|
10
|
-
if (key.upArrow || input === "k") {
|
|
11
|
-
const newIndex = Math.max(0, selectedIndex - 1);
|
|
12
|
-
onSelect(newIndex);
|
|
13
|
-
}
|
|
14
|
-
else if (key.downArrow || input === "j") {
|
|
15
|
-
const newIndex = Math.min(items.length - 1, selectedIndex + 1);
|
|
16
|
-
onSelect(newIndex);
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
// Account for scroll indicators in available space
|
|
20
5
|
const hasItemsAbove = scrollOffset > 0;
|
|
21
6
|
const hasItemsBelow = scrollOffset + maxHeight < items.length;
|
|
22
7
|
const indicatorLines = (showScrollIndicators && hasItemsAbove ? 1 : 0) +
|
|
@@ -25,15 +10,18 @@ export function ScrollableList({ items, selectedIndex, renderItem, maxHeight, sh
|
|
|
25
10
|
// Adjust scroll offset to keep selected item visible
|
|
26
11
|
useEffect(() => {
|
|
27
12
|
if (selectedIndex < scrollOffset) {
|
|
28
|
-
// Selected is above viewport - scroll up
|
|
29
13
|
setScrollOffset(selectedIndex);
|
|
30
14
|
}
|
|
31
15
|
else if (selectedIndex >= scrollOffset + effectiveMaxHeight) {
|
|
32
|
-
// Selected is below viewport - scroll down
|
|
33
16
|
setScrollOffset(selectedIndex - effectiveMaxHeight + 1);
|
|
34
17
|
}
|
|
35
18
|
}, [selectedIndex, effectiveMaxHeight, scrollOffset]);
|
|
36
|
-
//
|
|
19
|
+
// Reset scroll when items change drastically (e.g. search)
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (scrollOffset >= items.length) {
|
|
22
|
+
setScrollOffset(Math.max(0, items.length - effectiveMaxHeight));
|
|
23
|
+
}
|
|
24
|
+
}, [items.length, scrollOffset, effectiveMaxHeight]);
|
|
37
25
|
const visibleItems = useMemo(() => {
|
|
38
26
|
const start = scrollOffset;
|
|
39
27
|
const end = Math.min(scrollOffset + effectiveMaxHeight, items.length);
|
|
@@ -43,6 +31,6 @@ export function ScrollableList({ items, selectedIndex, renderItem, maxHeight, sh
|
|
|
43
31
|
}));
|
|
44
32
|
}, [items, scrollOffset, effectiveMaxHeight]);
|
|
45
33
|
const itemsBelow = items.length - scrollOffset - effectiveMaxHeight;
|
|
46
|
-
return (_jsxs("box", { flexDirection: "column", children: [showScrollIndicators && hasItemsAbove && (_jsxs("text", { fg: "cyan", children: ["\u2191 ", scrollOffset, " more"] })), visibleItems.map(({ item, originalIndex }) => (_jsx("box", { width: "100%", overflow: "hidden", children: renderItem(item, originalIndex, originalIndex === selectedIndex) }, originalIndex))), showScrollIndicators && hasItemsBelow && (_jsxs("text", { fg: "cyan", children: ["\u2193 ", itemsBelow, " more"] }))] }));
|
|
34
|
+
return (_jsxs("box", { flexDirection: "column", children: [showScrollIndicators && hasItemsAbove && (_jsxs("text", { fg: "cyan", children: ["\u2191 ", scrollOffset, " more"] })), visibleItems.map(({ item, originalIndex }) => (_jsx("box", { width: "100%", overflow: "hidden", children: renderItem(item, originalIndex, originalIndex === selectedIndex) }, getKey ? getKey(item, originalIndex) : `${originalIndex}`))), showScrollIndicators && hasItemsBelow && (_jsxs("text", { fg: "cyan", children: ["\u2193 ", itemsBelow, " more"] }))] }));
|
|
47
35
|
}
|
|
48
36
|
export default ScrollableList;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React, { useState, useEffect, useMemo } from "react";
|
|
2
|
-
import { useKeyboardHandler } from "../hooks/useKeyboardHandler";
|
|
3
2
|
|
|
4
3
|
interface ScrollableListProps<T> {
|
|
5
4
|
/** Array of items to display */
|
|
@@ -8,14 +7,12 @@ interface ScrollableListProps<T> {
|
|
|
8
7
|
selectedIndex: number;
|
|
9
8
|
/** Render function for each item */
|
|
10
9
|
renderItem: (item: T, index: number, isSelected: boolean) => React.ReactNode;
|
|
11
|
-
/** Maximum visible height (number of lines)
|
|
10
|
+
/** Maximum visible height (number of lines) */
|
|
12
11
|
maxHeight: number;
|
|
13
12
|
/** Show scroll indicators */
|
|
14
13
|
showScrollIndicators?: boolean;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
/** Whether this list should receive keyboard input */
|
|
18
|
-
focused?: boolean;
|
|
14
|
+
/** Item key extractor */
|
|
15
|
+
getKey?: (item: T, index: number) => string;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
export function ScrollableList<T>({
|
|
@@ -24,25 +21,10 @@ export function ScrollableList<T>({
|
|
|
24
21
|
renderItem,
|
|
25
22
|
maxHeight,
|
|
26
23
|
showScrollIndicators = true,
|
|
27
|
-
|
|
28
|
-
focused = false,
|
|
24
|
+
getKey,
|
|
29
25
|
}: ScrollableListProps<T>) {
|
|
30
26
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
31
27
|
|
|
32
|
-
// Handle keyboard navigation
|
|
33
|
-
useKeyboardHandler((input, key) => {
|
|
34
|
-
if (!focused || !onSelect) return;
|
|
35
|
-
|
|
36
|
-
if (key.upArrow || input === "k") {
|
|
37
|
-
const newIndex = Math.max(0, selectedIndex - 1);
|
|
38
|
-
onSelect(newIndex);
|
|
39
|
-
} else if (key.downArrow || input === "j") {
|
|
40
|
-
const newIndex = Math.min(items.length - 1, selectedIndex + 1);
|
|
41
|
-
onSelect(newIndex);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// Account for scroll indicators in available space
|
|
46
28
|
const hasItemsAbove = scrollOffset > 0;
|
|
47
29
|
const hasItemsBelow = scrollOffset + maxHeight < items.length;
|
|
48
30
|
const indicatorLines =
|
|
@@ -53,15 +35,19 @@ export function ScrollableList<T>({
|
|
|
53
35
|
// Adjust scroll offset to keep selected item visible
|
|
54
36
|
useEffect(() => {
|
|
55
37
|
if (selectedIndex < scrollOffset) {
|
|
56
|
-
// Selected is above viewport - scroll up
|
|
57
38
|
setScrollOffset(selectedIndex);
|
|
58
39
|
} else if (selectedIndex >= scrollOffset + effectiveMaxHeight) {
|
|
59
|
-
// Selected is below viewport - scroll down
|
|
60
40
|
setScrollOffset(selectedIndex - effectiveMaxHeight + 1);
|
|
61
41
|
}
|
|
62
42
|
}, [selectedIndex, effectiveMaxHeight, scrollOffset]);
|
|
63
43
|
|
|
64
|
-
//
|
|
44
|
+
// Reset scroll when items change drastically (e.g. search)
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (scrollOffset >= items.length) {
|
|
47
|
+
setScrollOffset(Math.max(0, items.length - effectiveMaxHeight));
|
|
48
|
+
}
|
|
49
|
+
}, [items.length, scrollOffset, effectiveMaxHeight]);
|
|
50
|
+
|
|
65
51
|
const visibleItems = useMemo(() => {
|
|
66
52
|
const start = scrollOffset;
|
|
67
53
|
const end = Math.min(scrollOffset + effectiveMaxHeight, items.length);
|
|
@@ -75,19 +61,20 @@ export function ScrollableList<T>({
|
|
|
75
61
|
|
|
76
62
|
return (
|
|
77
63
|
<box flexDirection="column">
|
|
78
|
-
{/* Scroll up indicator */}
|
|
79
64
|
{showScrollIndicators && hasItemsAbove && (
|
|
80
65
|
<text fg="cyan">↑ {scrollOffset} more</text>
|
|
81
66
|
)}
|
|
82
67
|
|
|
83
|
-
{/* Visible items - strictly limited */}
|
|
84
68
|
{visibleItems.map(({ item, originalIndex }) => (
|
|
85
|
-
<box
|
|
69
|
+
<box
|
|
70
|
+
key={getKey ? getKey(item, originalIndex) : `${originalIndex}`}
|
|
71
|
+
width="100%"
|
|
72
|
+
overflow="hidden"
|
|
73
|
+
>
|
|
86
74
|
{renderItem(item, originalIndex, originalIndex === selectedIndex)}
|
|
87
75
|
</box>
|
|
88
76
|
))}
|
|
89
77
|
|
|
90
|
-
{/* Scroll down indicator */}
|
|
91
78
|
{showScrollIndicators && hasItemsBelow && (
|
|
92
79
|
<text fg="cyan">↓ {itemsBelow} more</text>
|
|
93
80
|
)}
|
|
@@ -1,16 +1,37 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { STAR_RELIABILITY_INFO } from "../../data/skill-repos.js";
|
|
2
3
|
import { SelectableRow, ListCategoryRow, ScopeSquares, MetaText, KeyValueLine, DetailSection, } from "../components/primitives/index.js";
|
|
3
4
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
4
|
-
function
|
|
5
|
+
function formatStarsNum(stars) {
|
|
5
6
|
if (!stars)
|
|
6
7
|
return "";
|
|
7
8
|
if (stars >= 1000000)
|
|
8
|
-
return
|
|
9
|
+
return `${(stars / 1000000).toFixed(1)}M`;
|
|
9
10
|
if (stars >= 10000)
|
|
10
|
-
return
|
|
11
|
+
return `${Math.round(stars / 1000)}K`;
|
|
11
12
|
if (stars >= 1000)
|
|
12
|
-
return
|
|
13
|
-
return
|
|
13
|
+
return `${(stars / 1000).toFixed(1)}K`;
|
|
14
|
+
return `${stars}`;
|
|
15
|
+
}
|
|
16
|
+
/** Star icon and color based on reliability classification */
|
|
17
|
+
function starIcon(reliability) {
|
|
18
|
+
if (reliability === "mega-repo")
|
|
19
|
+
return "☆";
|
|
20
|
+
if (reliability === "skill-dump")
|
|
21
|
+
return "☆";
|
|
22
|
+
return "★";
|
|
23
|
+
}
|
|
24
|
+
function starColor(reliability) {
|
|
25
|
+
if (reliability === "mega-repo")
|
|
26
|
+
return "gray";
|
|
27
|
+
if (reliability === "skill-dump")
|
|
28
|
+
return "#888800";
|
|
29
|
+
return "yellow";
|
|
30
|
+
}
|
|
31
|
+
function formatStars(stars, reliability) {
|
|
32
|
+
if (!stars)
|
|
33
|
+
return "";
|
|
34
|
+
return `${starIcon(reliability)} ${formatStarsNum(stars)}`;
|
|
14
35
|
}
|
|
15
36
|
// ─── Category renderer ────────────────────────────────────────────────────────
|
|
16
37
|
const categoryRenderer = {
|
|
@@ -37,24 +58,60 @@ const skillRenderer = {
|
|
|
37
58
|
const { skill } = item;
|
|
38
59
|
const hasUser = skill.installedScope === "user";
|
|
39
60
|
const hasProject = skill.installedScope === "project";
|
|
40
|
-
const
|
|
61
|
+
const reliability = skill.starReliability;
|
|
62
|
+
const starsStr = formatStars(skill.stars, reliability);
|
|
63
|
+
const sColor = starColor(reliability);
|
|
41
64
|
const displayName = truncateName(skill.name);
|
|
42
|
-
|
|
65
|
+
const indentLevel = item.indent ?? 1;
|
|
66
|
+
return (_jsxs(SelectableRow, { selected: isSelected, indent: indentLevel, children: [_jsx(ScopeSquares, { user: hasUser, project: hasProject, selected: isSelected }), _jsx("span", { children: " " }), _jsx("span", { fg: isSelected ? "white" : skill.installed ? "white" : "gray", children: displayName }), skill.hasUpdate ? _jsx(MetaText, { text: " \u2B06", tone: "warning" }) : null, starsStr ? _jsx("span", { fg: sColor, children: ` ${starsStr}` }) : null] }));
|
|
43
67
|
},
|
|
44
68
|
renderDetail: ({ item }) => {
|
|
45
69
|
const { skill } = item;
|
|
46
70
|
const fm = skill.frontmatter;
|
|
47
71
|
const description = fm?.description || skill.description || "Loading...";
|
|
48
|
-
const
|
|
49
|
-
|
|
72
|
+
const reliability = skill.starReliability;
|
|
73
|
+
const starsStr = formatStars(skill.stars, reliability);
|
|
74
|
+
const sColor = starColor(reliability);
|
|
75
|
+
const reliabilityInfo = reliability ? STAR_RELIABILITY_INFO[reliability] : null;
|
|
76
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { fg: "cyan", children: [_jsx("strong", { children: skill.name }), starsStr ? _jsxs("span", { fg: sColor, children: [" ", starsStr] }) : null] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "white", children: description }) }), fm?.category ? (_jsx(KeyValueLine, { label: "Category", value: _jsx("span", { fg: "cyan", children: fm.category }) })) : null, fm?.author ? (_jsx(KeyValueLine, { label: "Author", value: _jsx("span", { children: fm.author }) })) : null, fm?.version ? (_jsx(KeyValueLine, { label: "Version", value: _jsx("span", { children: fm.version }) })) : null, fm?.tags && fm.tags.length > 0 ? (_jsx(KeyValueLine, { label: "Tags", value: _jsx("span", { children: fm.tags.join(", ") }) })) : null, _jsxs(DetailSection, { children: [_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Source " }), _jsx("span", { fg: "#5c9aff", children: skill.source.repo })] }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: " " }), _jsx("span", { fg: "gray", children: skill.repoPath })] })] }), reliabilityInfo && reliability !== "dedicated" && (_jsx("box", { marginTop: 1, children: _jsxs("text", { children: [_jsxs("span", { fg: sColor, children: [starIcon(reliability), " "] }), _jsx("span", { fg: sColor, children: _jsx("strong", { children: reliabilityInfo.label }) }), _jsxs("span", { fg: "gray", children: [" \u2014 ", reliabilityInfo.description] })] }) })), _jsxs(DetailSection, { children: [_jsx("text", { children: "─".repeat(24) }), _jsx("text", { children: _jsx("strong", { children: "Scopes:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { bg: "cyan", fg: "black", children: " u " }), _jsx("span", { fg: skill.installedScope === "user" ? "cyan" : "gray", children: skill.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: skill.installedScope === "project" ? "green" : "gray", children: skill.installedScope === "project" ? " ● " : " ○ " }), _jsx("span", { fg: "green", children: "Project" }), _jsx("span", { fg: "gray", children: " .claude/skills/" })] })] })] }), skill.hasUpdate && (_jsx("box", { marginTop: 1, children: _jsx("text", { bg: "yellow", fg: "black", children: " UPDATE AVAILABLE " }) })), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: skill.installed
|
|
50
77
|
? "Press u/p to toggle scope"
|
|
51
78
|
: "Press u/p to install" }) }), _jsx("box", { children: _jsxs("text", { children: [_jsx("span", { bg: "#555555", fg: "white", children: " o " }), _jsx("span", { fg: "gray", children: " Open in browser" })] }) })] }));
|
|
52
79
|
},
|
|
53
80
|
};
|
|
81
|
+
// ─── Skill Set renderer ──────────────────────────────────────────────────────
|
|
82
|
+
const MAX_SET_NAME_LEN = 28;
|
|
83
|
+
const skillSetRenderer = {
|
|
84
|
+
renderRow: ({ item, isSelected }) => {
|
|
85
|
+
const { skillSet, expanded } = item;
|
|
86
|
+
const arrow = expanded ? "\u25BC" : "\u25B6";
|
|
87
|
+
const displayName = skillSet.name.length > MAX_SET_NAME_LEN
|
|
88
|
+
? skillSet.name.slice(0, MAX_SET_NAME_LEN - 1) + "\u2026"
|
|
89
|
+
: skillSet.name;
|
|
90
|
+
// Count installed child skills
|
|
91
|
+
const installedCount = skillSet.loaded
|
|
92
|
+
? skillSet.skills.filter((s) => s.installed).length
|
|
93
|
+
: 0;
|
|
94
|
+
const totalCount = skillSet.loaded ? skillSet.skills.length : "...";
|
|
95
|
+
const countStr = `(${installedCount}/${totalCount})`;
|
|
96
|
+
// Any child installed at user or project scope?
|
|
97
|
+
const hasUser = skillSet.loaded && skillSet.skills.some((s) => s.installedScope === "user");
|
|
98
|
+
const hasProject = skillSet.loaded && skillSet.skills.some((s) => s.installedScope === "project");
|
|
99
|
+
const starsStr = formatStars(skillSet.stars);
|
|
100
|
+
return (_jsxs(SelectableRow, { selected: isSelected, indent: 1, children: [_jsxs("span", { fg: isSelected ? "white" : "gray", children: [arrow, " "] }), _jsxs("span", { fg: isSelected ? "white" : "yellow", children: [skillSet.icon, " "] }), _jsx("span", { fg: isSelected ? "white" : "cyan", children: _jsx("strong", { children: displayName }) }), _jsxs("span", { fg: "gray", children: [" ", countStr] }), starsStr ? _jsx(MetaText, { text: ` ${starsStr}`, tone: "warning" }) : null, _jsx("span", { children: " " }), _jsx(ScopeSquares, { user: hasUser, project: hasProject, selected: isSelected })] }));
|
|
101
|
+
},
|
|
102
|
+
renderDetail: ({ item }) => {
|
|
103
|
+
const { skillSet, expanded } = item;
|
|
104
|
+
const starsStr = formatStars(skillSet.stars);
|
|
105
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { fg: "cyan", children: [_jsxs("strong", { children: [skillSet.icon, " ", skillSet.name] }), starsStr ? _jsxs("span", { fg: "yellow", children: [" ", starsStr] }) : null] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "white", children: skillSet.description }) }), _jsx(DetailSection, { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Source " }), _jsx("span", { fg: "#5c9aff", children: skillSet.repo })] }) }), skillSet.loading && (_jsx("box", { marginTop: 1, children: _jsx("text", { fg: "yellow", children: "Loading skills..." }) })), skillSet.error && (_jsx("box", { marginTop: 1, children: _jsxs("text", { fg: "red", children: ["Error: ", skillSet.error] }) })), skillSet.loaded && skillSet.skills.length > 0 && (_jsxs(DetailSection, { children: [_jsxs("text", { children: [_jsx("strong", { children: "Skills in this set:" }), _jsxs("span", { fg: "gray", children: [" ", "(", skillSet.skills.filter((s) => s.installed).length, "/", skillSet.skills.length, " installed)"] })] }), _jsx("box", { marginTop: 1, flexDirection: "column", children: skillSet.skills.map((s) => (_jsxs("text", { children: [_jsx("span", { fg: s.installedScope === "user" ? "cyan" : "gray", children: s.installedScope === "user" ? "\u25A0" : "\u25A1" }), _jsx("span", { fg: s.installedScope === "project" ? "green" : "gray", children: s.installedScope === "project" ? "\u25A0" : "\u25A1" }), _jsxs("span", { fg: s.installed ? "white" : "gray", children: [" ", s.name] })] }, s.id))) })] })), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: expanded
|
|
106
|
+
? "Press Enter to collapse \u2022 u/p to install all"
|
|
107
|
+
: "Press Enter to expand \u2022 u/p to install all" }) }), _jsx("box", { children: _jsxs("text", { children: [_jsx("span", { bg: "#555555", fg: "white", children: " o " }), _jsx("span", { fg: "gray", children: " Open in browser" })] }) })] }));
|
|
108
|
+
},
|
|
109
|
+
};
|
|
54
110
|
// ─── Registry ─────────────────────────────────────────────────────────────────
|
|
55
111
|
export const skillRenderers = {
|
|
56
112
|
category: categoryRenderer,
|
|
57
113
|
skill: skillRenderer,
|
|
114
|
+
skillset: skillSetRenderer,
|
|
58
115
|
};
|
|
59
116
|
/**
|
|
60
117
|
* Dispatch rendering by item kind.
|
|
@@ -63,6 +120,9 @@ export function renderSkillRow(item, _index, isSelected) {
|
|
|
63
120
|
if (item.kind === "category") {
|
|
64
121
|
return skillRenderers.category.renderRow({ item, isSelected });
|
|
65
122
|
}
|
|
123
|
+
if (item.kind === "skillset") {
|
|
124
|
+
return skillRenderers.skillset.renderRow({ item, isSelected });
|
|
125
|
+
}
|
|
66
126
|
return skillRenderers.skill.renderRow({ item, isSelected });
|
|
67
127
|
}
|
|
68
128
|
export function renderSkillDetail(item) {
|
|
@@ -71,5 +131,8 @@ export function renderSkillDetail(item) {
|
|
|
71
131
|
if (item.kind === "category") {
|
|
72
132
|
return skillRenderers.category.renderDetail({ item });
|
|
73
133
|
}
|
|
134
|
+
if (item.kind === "skillset") {
|
|
135
|
+
return skillRenderers.skillset.renderDetail({ item });
|
|
136
|
+
}
|
|
74
137
|
return skillRenderers.skill.renderDetail({ item });
|
|
75
138
|
}
|