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.
@@ -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 type SkillBrowserItem = SkillCategoryItem | SkillSkillItem;
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: always shown, filtered when searching ──
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: filteredRec.length,
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
- const popularWithStars = popular.filter((s) => (s.stars ?? 0) >= 5);
139
- if (popularWithStars.length > 0) {
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: popularWithStars.length,
208
+ count: popularDeduped.length,
147
209
  tone: "teal",
148
210
  });
149
- for (const skill of popularWithStars) {
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
- import { useKeyboardHandler } from "../hooks/useKeyboardHandler";
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
- // Calculate visible items - strictly limited to effectiveMaxHeight
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) - REQUIRED for proper rendering */
10
+ /** Maximum visible height (number of lines) */
12
11
  maxHeight: number;
13
12
  /** Show scroll indicators */
14
13
  showScrollIndicators?: boolean;
15
- /** Called when selection changes (arrow keys) */
16
- onSelect?: (index: number) => void;
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
- onSelect,
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
- // Calculate visible items - strictly limited to effectiveMaxHeight
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 key={originalIndex} width="100%" overflow="hidden">
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 formatStars(stars) {
5
+ function formatStarsNum(stars) {
5
6
  if (!stars)
6
7
  return "";
7
8
  if (stars >= 1000000)
8
- return `★ ${(stars / 1000000).toFixed(1)}M`;
9
+ return `${(stars / 1000000).toFixed(1)}M`;
9
10
  if (stars >= 10000)
10
- return `★ ${Math.round(stars / 1000)}K`;
11
+ return `${Math.round(stars / 1000)}K`;
11
12
  if (stars >= 1000)
12
- return `★ ${(stars / 1000).toFixed(1)}K`;
13
- return `★ ${stars}`;
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 starsStr = formatStars(skill.stars);
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
- return (_jsxs(SelectableRow, { selected: isSelected, indent: 1, 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(MetaText, { text: ` ${starsStr}`, tone: "warning" }) : null] }));
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 starsStr = formatStars(skill.stars);
49
- return (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { fg: "cyan", children: [_jsx("strong", { children: skill.name }), starsStr ? _jsxs("span", { fg: "yellow", 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 })] })] }), _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
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
  }