claudeup 4.5.4 → 4.6.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.
@@ -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
  }
@@ -1,6 +1,8 @@
1
1
  import React from "react";
2
2
  import type { ItemRenderer } from "../registry.js";
3
- import type { SkillBrowserItem, SkillCategoryItem, SkillSkillItem } from "../adapters/skillsAdapter.js";
3
+ import type { SkillBrowserItem, SkillCategoryItem, SkillSkillItem, SkillSetItem } from "../adapters/skillsAdapter.js";
4
+ import type { StarReliability } from "../../types/index.js";
5
+ import { STAR_RELIABILITY_INFO } from "../../data/skill-repos.js";
4
6
  import {
5
7
  SelectableRow,
6
8
  ListCategoryRow,
@@ -14,12 +16,30 @@ import {
14
16
 
15
17
  // ─── Helpers ──────────────────────────────────────────────────────────────────
16
18
 
17
- function formatStars(stars?: number): string {
19
+ function formatStarsNum(stars?: number): string {
18
20
  if (!stars) return "";
19
- if (stars >= 1000000) return `★ ${(stars / 1000000).toFixed(1)}M`;
20
- if (stars >= 10000) return `★ ${Math.round(stars / 1000)}K`;
21
- if (stars >= 1000) return `★ ${(stars / 1000).toFixed(1)}K`;
22
- return `★ ${stars}`;
21
+ if (stars >= 1000000) return `${(stars / 1000000).toFixed(1)}M`;
22
+ if (stars >= 10000) return `${Math.round(stars / 1000)}K`;
23
+ if (stars >= 1000) return `${(stars / 1000).toFixed(1)}K`;
24
+ return `${stars}`;
25
+ }
26
+
27
+ /** Star icon and color based on reliability classification */
28
+ function starIcon(reliability?: StarReliability): string {
29
+ if (reliability === "mega-repo") return "☆";
30
+ if (reliability === "skill-dump") return "☆";
31
+ return "★";
32
+ }
33
+
34
+ function starColor(reliability?: StarReliability): string {
35
+ if (reliability === "mega-repo") return "gray";
36
+ if (reliability === "skill-dump") return "#888800";
37
+ return "yellow";
38
+ }
39
+
40
+ function formatStars(stars?: number, reliability?: StarReliability): string {
41
+ if (!stars) return "";
42
+ return `${starIcon(reliability)} ${formatStarsNum(stars)}`;
23
43
  }
24
44
 
25
45
  // ─── Category renderer ────────────────────────────────────────────────────────
@@ -75,18 +95,21 @@ const skillRenderer: ItemRenderer<SkillSkillItem> = {
75
95
  const { skill } = item;
76
96
  const hasUser = skill.installedScope === "user";
77
97
  const hasProject = skill.installedScope === "project";
78
- const starsStr = formatStars(skill.stars);
98
+ const reliability = skill.starReliability;
99
+ const starsStr = formatStars(skill.stars, reliability);
100
+ const sColor = starColor(reliability);
79
101
  const displayName = truncateName(skill.name);
102
+ const indentLevel = item.indent ?? 1;
80
103
 
81
104
  return (
82
- <SelectableRow selected={isSelected} indent={1}>
105
+ <SelectableRow selected={isSelected} indent={indentLevel}>
83
106
  <ScopeSquares user={hasUser} project={hasProject} selected={isSelected} />
84
107
  <span> </span>
85
108
  <span fg={isSelected ? "white" : skill.installed ? "white" : "gray"}>
86
109
  {displayName}
87
110
  </span>
88
111
  {skill.hasUpdate ? <MetaText text=" ⬆" tone="warning" /> : null}
89
- {starsStr ? <MetaText text={` ${starsStr}`} tone="warning" /> : null}
112
+ {starsStr ? <span fg={sColor}>{` ${starsStr}`}</span> : null}
90
113
  </SelectableRow>
91
114
  );
92
115
  },
@@ -95,13 +118,16 @@ const skillRenderer: ItemRenderer<SkillSkillItem> = {
95
118
  const { skill } = item;
96
119
  const fm = skill.frontmatter;
97
120
  const description = fm?.description || skill.description || "Loading...";
98
- const starsStr = formatStars(skill.stars);
121
+ const reliability = skill.starReliability;
122
+ const starsStr = formatStars(skill.stars, reliability);
123
+ const sColor = starColor(reliability);
124
+ const reliabilityInfo = reliability ? STAR_RELIABILITY_INFO[reliability] : null;
99
125
 
100
126
  return (
101
127
  <box flexDirection="column">
102
128
  <text fg="cyan">
103
129
  <strong>{skill.name}</strong>
104
- {starsStr ? <span fg="yellow"> {starsStr}</span> : null}
130
+ {starsStr ? <span fg={sColor}> {starsStr}</span> : null}
105
131
  </text>
106
132
 
107
133
  <box marginTop={1}>
@@ -138,6 +164,16 @@ const skillRenderer: ItemRenderer<SkillSkillItem> = {
138
164
  </text>
139
165
  </DetailSection>
140
166
 
167
+ {reliabilityInfo && reliability !== "dedicated" && (
168
+ <box marginTop={1}>
169
+ <text>
170
+ <span fg={sColor}>{starIcon(reliability)} </span>
171
+ <span fg={sColor}><strong>{reliabilityInfo.label}</strong></span>
172
+ <span fg="gray"> — {reliabilityInfo.description}</span>
173
+ </text>
174
+ </box>
175
+ )}
176
+
141
177
  <DetailSection>
142
178
  <text>{"─".repeat(24)}</text>
143
179
  <text><strong>Scopes:</strong></text>
@@ -185,14 +221,137 @@ const skillRenderer: ItemRenderer<SkillSkillItem> = {
185
221
  },
186
222
  };
187
223
 
224
+ // ─── Skill Set renderer ──────────────────────────────────────────────────────
225
+
226
+ const MAX_SET_NAME_LEN = 28;
227
+
228
+ const skillSetRenderer: ItemRenderer<SkillSetItem> = {
229
+ renderRow: ({ item, isSelected }) => {
230
+ const { skillSet, expanded } = item;
231
+ const arrow = expanded ? "\u25BC" : "\u25B6";
232
+ const displayName =
233
+ skillSet.name.length > MAX_SET_NAME_LEN
234
+ ? skillSet.name.slice(0, MAX_SET_NAME_LEN - 1) + "\u2026"
235
+ : skillSet.name;
236
+
237
+ // Count installed child skills
238
+ const installedCount = skillSet.loaded
239
+ ? skillSet.skills.filter((s) => s.installed).length
240
+ : 0;
241
+ const totalCount = skillSet.loaded ? skillSet.skills.length : "...";
242
+ const countStr = `(${installedCount}/${totalCount})`;
243
+
244
+ // Any child installed at user or project scope?
245
+ const hasUser = skillSet.loaded && skillSet.skills.some((s) => s.installedScope === "user");
246
+ const hasProject = skillSet.loaded && skillSet.skills.some((s) => s.installedScope === "project");
247
+
248
+ const starsStr = formatStars(skillSet.stars);
249
+
250
+ return (
251
+ <SelectableRow selected={isSelected} indent={1}>
252
+ <span fg={isSelected ? "white" : "gray"}>{arrow} </span>
253
+ <span fg={isSelected ? "white" : "yellow"}>{skillSet.icon} </span>
254
+ <span fg={isSelected ? "white" : "cyan"}>
255
+ <strong>{displayName}</strong>
256
+ </span>
257
+ <span fg="gray"> {countStr}</span>
258
+ {starsStr ? <MetaText text={` ${starsStr}`} tone="warning" /> : null}
259
+ <span> </span>
260
+ <ScopeSquares user={hasUser} project={hasProject} selected={isSelected} />
261
+ </SelectableRow>
262
+ );
263
+ },
264
+
265
+ renderDetail: ({ item }) => {
266
+ const { skillSet, expanded } = item;
267
+ const starsStr = formatStars(skillSet.stars);
268
+
269
+ return (
270
+ <box flexDirection="column">
271
+ <text fg="cyan">
272
+ <strong>
273
+ {skillSet.icon} {skillSet.name}
274
+ </strong>
275
+ {starsStr ? <span fg="yellow"> {starsStr}</span> : null}
276
+ </text>
277
+
278
+ <box marginTop={1}>
279
+ <text fg="white">{skillSet.description}</text>
280
+ </box>
281
+
282
+ <DetailSection>
283
+ <text>
284
+ <span fg="gray">Source </span>
285
+ <span fg="#5c9aff">{skillSet.repo}</span>
286
+ </text>
287
+ </DetailSection>
288
+
289
+ {skillSet.loading && (
290
+ <box marginTop={1}>
291
+ <text fg="yellow">Loading skills...</text>
292
+ </box>
293
+ )}
294
+
295
+ {skillSet.error && (
296
+ <box marginTop={1}>
297
+ <text fg="red">Error: {skillSet.error}</text>
298
+ </box>
299
+ )}
300
+
301
+ {skillSet.loaded && skillSet.skills.length > 0 && (
302
+ <DetailSection>
303
+ <text>
304
+ <strong>Skills in this set:</strong>
305
+ <span fg="gray">
306
+ {" "}
307
+ ({skillSet.skills.filter((s) => s.installed).length}/
308
+ {skillSet.skills.length} installed)
309
+ </span>
310
+ </text>
311
+ <box marginTop={1} flexDirection="column">
312
+ {skillSet.skills.map((s) => (
313
+ <text key={s.id}>
314
+ <span fg={s.installedScope === "user" ? "cyan" : "gray"}>
315
+ {s.installedScope === "user" ? "\u25A0" : "\u25A1"}
316
+ </span>
317
+ <span fg={s.installedScope === "project" ? "green" : "gray"}>
318
+ {s.installedScope === "project" ? "\u25A0" : "\u25A1"}
319
+ </span>
320
+ <span fg={s.installed ? "white" : "gray"}> {s.name}</span>
321
+ </text>
322
+ ))}
323
+ </box>
324
+ </DetailSection>
325
+ )}
326
+
327
+ <box marginTop={1}>
328
+ <text fg="gray">
329
+ {expanded
330
+ ? "Press Enter to collapse \u2022 u/p to install all"
331
+ : "Press Enter to expand \u2022 u/p to install all"}
332
+ </text>
333
+ </box>
334
+ <box>
335
+ <text>
336
+ <span bg="#555555" fg="white"> o </span>
337
+ <span fg="gray"> Open in browser</span>
338
+ </text>
339
+ </box>
340
+ </box>
341
+ );
342
+ },
343
+ };
344
+
188
345
  // ─── Registry ─────────────────────────────────────────────────────────────────
189
346
 
190
347
  export const skillRenderers: {
191
348
  category: ItemRenderer<SkillCategoryItem>;
192
349
  skill: ItemRenderer<SkillSkillItem>;
350
+ skillset: ItemRenderer<SkillSetItem>;
193
351
  } = {
194
352
  category: categoryRenderer,
195
353
  skill: skillRenderer,
354
+ skillset: skillSetRenderer,
196
355
  };
197
356
 
198
357
  /**
@@ -206,6 +365,9 @@ export function renderSkillRow(
206
365
  if (item.kind === "category") {
207
366
  return skillRenderers.category.renderRow({ item, isSelected });
208
367
  }
368
+ if (item.kind === "skillset") {
369
+ return skillRenderers.skillset.renderRow({ item, isSelected });
370
+ }
209
371
  return skillRenderers.skill.renderRow({ item, isSelected });
210
372
  }
211
373
 
@@ -216,5 +378,8 @@ export function renderSkillDetail(
216
378
  if (item.kind === "category") {
217
379
  return skillRenderers.category.renderDetail({ item });
218
380
  }
381
+ if (item.kind === "skillset") {
382
+ return skillRenderers.skillset.renderDetail({ item });
383
+ }
219
384
  return skillRenderers.skill.renderDetail({ item });
220
385
  }
@@ -650,6 +650,6 @@ export function PluginsScreen() {
650
650
  isActive: isSearchActive,
651
651
  query: pluginsState.searchQuery,
652
652
  placeholder: searchPlaceholder,
653
- }, footerHints: footerHints, listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items: selectableItems, selectedIndex: pluginsState.selectedIndex, renderItem: renderPluginRow, maxHeight: dimensions.listPanelHeight }), pluginsState.searchQuery && selectableItems.length === 0 && (_jsx(EmptyFilterState, { query: pluginsState.searchQuery, entityName: "plugins" }))] }), detailPanel: renderPluginDetail(selectedItem, pluginsState.collapsedMarketplaces) }));
653
+ }, footerHints: footerHints, listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items: selectableItems, selectedIndex: pluginsState.selectedIndex, renderItem: renderPluginRow, maxHeight: dimensions.listPanelHeight, getKey: (item) => item.id }), pluginsState.searchQuery && selectableItems.length === 0 && (_jsx(EmptyFilterState, { query: pluginsState.searchQuery, entityName: "plugins" }))] }), detailPanel: renderPluginDetail(selectedItem, pluginsState.collapsedMarketplaces) }));
654
654
  }
655
655
  export default PluginsScreen;
@@ -821,6 +821,7 @@ export function PluginsScreen() {
821
821
  selectedIndex={pluginsState.selectedIndex}
822
822
  renderItem={renderPluginRow}
823
823
  maxHeight={dimensions.listPanelHeight}
824
+ getKey={(item) => item.id}
824
825
  />
825
826
  {pluginsState.searchQuery && selectableItems.length === 0 && (
826
827
  <EmptyFilterState query={pluginsState.searchQuery} entityName="plugins" />