claudeup 4.35.0 → 4.36.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.
Files changed (63) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/git-worktree.test.ts +108 -0
  3. package/src/__tests__/scope-squares.test.tsx +165 -0
  4. package/src/__tests__/theme-adaptive-colors.test.ts +307 -0
  5. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  6. package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
  7. package/src/main.tsx +21 -5
  8. package/src/opentui.d.ts +21 -12
  9. package/src/services/claude-settings.ts +37 -7
  10. package/src/services/git-worktree.ts +129 -0
  11. package/src/services/plugin-manager.ts +10 -1
  12. package/src/ui/App.tsx +19 -12
  13. package/src/ui/adapters/pluginsAdapter.ts +174 -168
  14. package/src/ui/adapters/settingsAdapter.ts +119 -116
  15. package/src/ui/adapters/skillsAdapter.ts +203 -196
  16. package/src/ui/components/CategoryHeader.tsx +9 -8
  17. package/src/ui/components/EmptyFilterState.tsx +10 -5
  18. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  19. package/src/ui/components/ScopeIndicator.tsx +10 -6
  20. package/src/ui/components/ScrollableList.tsx +3 -2
  21. package/src/ui/components/SearchInput.tsx +2 -1
  22. package/src/ui/components/StyledText.tsx +5 -4
  23. package/src/ui/components/TabBar.tsx +4 -3
  24. package/src/ui/components/layout/FooterHints.tsx +37 -30
  25. package/src/ui/components/layout/Panel.tsx +6 -5
  26. package/src/ui/components/layout/ProgressBar.tsx +7 -6
  27. package/src/ui/components/layout/ScopeTabs.tsx +6 -5
  28. package/src/ui/components/layout/ScreenLayout.tsx +27 -24
  29. package/src/ui/components/layout/index.ts +3 -3
  30. package/src/ui/components/modals/ConfirmModal.tsx +12 -11
  31. package/src/ui/components/modals/InputModal.tsx +14 -6
  32. package/src/ui/components/modals/LoadingModal.tsx +6 -5
  33. package/src/ui/components/modals/MessageModal.tsx +9 -8
  34. package/src/ui/components/modals/SelectModal.tsx +11 -7
  35. package/src/ui/components/modals/VersionMismatchModal.tsx +14 -16
  36. package/src/ui/components/primitives/ActionHints.tsx +26 -26
  37. package/src/ui/components/primitives/DetailSection.tsx +13 -12
  38. package/src/ui/components/primitives/KeyValueLine.tsx +9 -8
  39. package/src/ui/components/primitives/ListCategoryRow.tsx +25 -27
  40. package/src/ui/components/primitives/MetaText.tsx +3 -3
  41. package/src/ui/components/primitives/ScopeDetail.tsx +48 -48
  42. package/src/ui/components/primitives/ScopeSquares.tsx +47 -22
  43. package/src/ui/components/primitives/SelectableRow.tsx +22 -16
  44. package/src/ui/hooks/useGitignoreModal.ts +78 -74
  45. package/src/ui/registry.ts +11 -11
  46. package/src/ui/renderers/cliToolRenderers.tsx +260 -203
  47. package/src/ui/renderers/gitignoreRenderers.tsx +43 -42
  48. package/src/ui/renderers/mcpRenderers.tsx +121 -117
  49. package/src/ui/renderers/pluginRenderers.tsx +528 -471
  50. package/src/ui/renderers/profileRenderers.tsx +346 -300
  51. package/src/ui/renderers/settingsRenderers.tsx +183 -176
  52. package/src/ui/renderers/skillRenderers.tsx +410 -326
  53. package/src/ui/screens/AliasScreen.tsx +1336 -1309
  54. package/src/ui/screens/CliToolsScreen.tsx +92 -40
  55. package/src/ui/screens/EnvVarsScreen.tsx +19 -13
  56. package/src/ui/screens/GitignoreScreen.tsx +510 -493
  57. package/src/ui/screens/McpRegistryScreen.tsx +28 -21
  58. package/src/ui/screens/McpScreen.tsx +12 -3
  59. package/src/ui/screens/PluginsScreen.tsx +17 -7
  60. package/src/ui/screens/ProfilesScreen.tsx +39 -23
  61. package/src/ui/screens/SkillsScreen.tsx +832 -688
  62. package/src/ui/theme-mode.ts +73 -0
  63. package/src/ui/theme.ts +147 -53
@@ -1,83 +1,89 @@
1
1
  import React from "react";
2
2
  import type { ItemRenderer } from "../registry.js";
3
- import type { SkillBrowserItem, SkillCategoryItem, SkillSkillItem, SkillSetItem } from "../adapters/skillsAdapter.js";
3
+ import type {
4
+ SkillBrowserItem,
5
+ SkillCategoryItem,
6
+ SkillSkillItem,
7
+ SkillSetItem,
8
+ } from "../adapters/skillsAdapter.js";
4
9
  import type { StarReliability } from "../../types/index.js";
5
10
  import { STAR_RELIABILITY_INFO } from "../../data/skill-repos.js";
6
11
  import {
7
- SelectableRow,
8
- ListCategoryRow,
9
- ScopeSquares,
10
- ScopeDetail,
11
- ActionHints,
12
- MetaText,
13
- KeyValueLine,
14
- DetailSection,
12
+ SelectableRow,
13
+ ListCategoryRow,
14
+ ScopeSquares,
15
+ ScopeDetail,
16
+ ActionHints,
17
+ MetaText,
18
+ KeyValueLine,
19
+ DetailSection,
15
20
  } from "../components/primitives/index.js";
21
+ import { theme, type UiColor } from "../theme.js";
16
22
 
17
23
  // ─── Helpers ──────────────────────────────────────────────────────────────────
18
24
 
19
25
  function formatStarsNum(stars?: number): string {
20
- if (!stars) return "";
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}`;
26
+ if (!stars) return "";
27
+ if (stars >= 1000000) return `${(stars / 1000000).toFixed(1)}M`;
28
+ if (stars >= 10000) return `${Math.round(stars / 1000)}K`;
29
+ if (stars >= 1000) return `${(stars / 1000).toFixed(1)}K`;
30
+ return `${stars}`;
25
31
  }
26
32
 
27
33
  /** Star icon and color based on reliability classification */
28
34
  function starIcon(reliability?: StarReliability): string {
29
- if (reliability === "mega-repo") return "☆";
30
- if (reliability === "skill-dump") return "☆";
31
- return "★";
35
+ if (reliability === "mega-repo") return "☆";
36
+ if (reliability === "skill-dump") return "☆";
37
+ return "★";
32
38
  }
33
39
 
34
- function starColor(reliability?: StarReliability): string {
35
- if (reliability === "mega-repo") return "gray";
36
- if (reliability === "skill-dump") return "#888800";
37
- return "yellow";
40
+ function starColor(reliability?: StarReliability): UiColor {
41
+ if (reliability === "mega-repo") return theme.colors.muted;
42
+ if (reliability === "skill-dump") return theme.colors.warning;
43
+ return theme.colors.warning;
38
44
  }
39
45
 
40
46
  function formatStars(stars?: number, reliability?: StarReliability): string {
41
- if (!stars) return "";
42
- return `${starIcon(reliability)} ${formatStarsNum(stars)}`;
47
+ if (!stars) return "";
48
+ return `${starIcon(reliability)} ${formatStarsNum(stars)}`;
43
49
  }
44
50
 
45
51
  // ─── Category renderer ────────────────────────────────────────────────────────
46
52
 
47
53
  const categoryRenderer: ItemRenderer<SkillCategoryItem> = {
48
- renderRow: ({ item, isSelected }) => {
49
- const label = `${item.star ?? ""}${item.title}`;
50
- return (
51
- <ListCategoryRow
52
- selected={isSelected}
53
- title={label}
54
- count={item.count}
55
- badge={item.badge}
56
- tone={item.tone}
57
- />
58
- );
59
- },
60
-
61
- renderDetail: ({ item }) => {
62
- const isRec = item.categoryKey === "recommended";
63
- return (
64
- <box flexDirection="column">
65
- <text fg={isRec ? "green" : "cyan"}>
66
- <strong>
67
- {item.star ?? ""}
68
- {item.title}
69
- </strong>
70
- </text>
71
- <box marginTop={1}>
72
- <text fg="gray">
73
- {isRec
74
- ? "Curated skills recommended for most projects"
75
- : "Popular skills sorted by stars"}
76
- </text>
77
- </box>
78
- </box>
79
- );
80
- },
54
+ renderRow: ({ item, isSelected }) => {
55
+ const label = `${item.star ?? ""}${item.title}`;
56
+ return (
57
+ <ListCategoryRow
58
+ selected={isSelected}
59
+ title={label}
60
+ count={item.count}
61
+ badge={item.badge}
62
+ tone={item.tone}
63
+ />
64
+ );
65
+ },
66
+
67
+ renderDetail: ({ item }) => {
68
+ const isRec = item.categoryKey === "recommended";
69
+ return (
70
+ <box flexDirection="column">
71
+ <text fg={isRec ? theme.colors.success : theme.colors.info}>
72
+ <strong>
73
+ {item.star ?? ""}
74
+ {item.title}
75
+ </strong>
76
+ </text>
77
+ <box marginTop={1}>
78
+ <text fg={theme.colors.muted}>
79
+ {isRec
80
+ ? "Curated skills recommended for most projects"
81
+ : "Popular skills sorted by stars"}
82
+ </text>
83
+ </box>
84
+ </box>
85
+ );
86
+ },
81
87
  };
82
88
 
83
89
  // ─── Skill renderer ───────────────────────────────────────────────────────────
@@ -85,140 +91,185 @@ const categoryRenderer: ItemRenderer<SkillCategoryItem> = {
85
91
  const MAX_SKILL_NAME_LEN = 35;
86
92
 
87
93
  function truncateName(name: string): string {
88
- return name.length > MAX_SKILL_NAME_LEN
89
- ? name.slice(0, MAX_SKILL_NAME_LEN - 1) + "\u2026"
90
- : name;
94
+ return name.length > MAX_SKILL_NAME_LEN
95
+ ? name.slice(0, MAX_SKILL_NAME_LEN - 1) + "\u2026"
96
+ : name;
91
97
  }
92
98
 
93
99
  const skillRenderer: ItemRenderer<SkillSkillItem> = {
94
- renderRow: ({ item, isSelected }) => {
95
- const { skill } = item;
96
- const hasUser = skill.installedScope === "user";
97
- const hasProject = skill.installedScope === "project";
98
- const reliability = skill.starReliability;
99
- const starsStr = formatStars(skill.stars, reliability);
100
- const sColor = starColor(reliability);
101
- const displayName = truncateName(skill.name);
102
- const indentLevel = item.indent ?? 1;
103
-
104
- return (
105
- <SelectableRow selected={isSelected} indent={indentLevel}>
106
- <ScopeSquares user={hasUser} project={hasProject} selected={isSelected} />
107
- <span> </span>
108
- <span fg={isSelected ? "white" : skill.installed ? "white" : "gray"}>
109
- {displayName}
110
- </span>
111
- {skill.hasUpdate ? <MetaText text=" ⬆" tone="warning" /> : null}
112
- {starsStr ? <span fg={sColor}>{` ${starsStr}`}</span> : null}
113
- </SelectableRow>
114
- );
115
- },
116
-
117
- renderDetail: ({ item }) => {
118
- const { skill } = item;
119
- const fm = skill.frontmatter;
120
- const description = fm?.description || skill.description || "Loading...";
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;
125
-
126
- return (
127
- <box flexDirection="column">
128
- <text fg="cyan">
129
- <strong>{skill.name}</strong>
130
- {starsStr ? <span fg={sColor}> {starsStr}</span> : null}
131
- </text>
132
-
133
- <box marginTop={1}>
134
- <text fg="white">{description}</text>
135
- </box>
136
-
137
- {fm?.category ? (
138
- <KeyValueLine
139
- label="Category"
140
- value={<span fg="cyan">{fm.category}</span>}
141
- />
142
- ) : null}
143
- {fm?.author ? (
144
- <KeyValueLine label="Author" value={<span>{fm.author}</span>} />
145
- ) : null}
146
- {fm?.version ? (
147
- <KeyValueLine label="Version" value={<span>{fm.version}</span>} />
148
- ) : null}
149
- {fm?.tags && fm.tags.length > 0 ? (
150
- <KeyValueLine
151
- label="Tags"
152
- value={<span>{(fm.tags as string[]).join(", ")}</span>}
153
- />
154
- ) : null}
155
-
156
- <DetailSection>
157
- <text>
158
- <span fg="gray">Source </span>
159
- <span fg="#5c9aff">{skill.source.repo}</span>
160
- </text>
161
- <text>
162
- <span fg="gray">{" "}</span>
163
- <span fg="gray">{skill.repoPath}</span>
164
- </text>
165
- </DetailSection>
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
-
177
- <DetailSection>
178
- <text>{"─".repeat(24)}</text>
179
- <text><strong>Scopes:</strong></text>
180
- <box marginTop={1} flexDirection="column">
181
- <text>
182
- <span bg="cyan" fg="black"> u </span>
183
- <span fg={skill.installedScope === "user" ? "cyan" : "gray"}>
184
- {skill.installedScope === "user" ? " ● " : " ○ "}
185
- </span>
186
- <span fg="cyan">User</span>
187
- <span fg="gray"> ~/.claude/skills/</span>
188
- </text>
189
- <text>
190
- <span bg="green" fg="black"> p </span>
191
- <span fg={skill.installedScope === "project" ? "green" : "gray"}>
192
- {skill.installedScope === "project" ? " ● " : " ○ "}
193
- </span>
194
- <span fg="green">Project</span>
195
- <span fg="gray"> .claude/skills/</span>
196
- </text>
197
- </box>
198
- </DetailSection>
199
-
200
- {skill.hasUpdate && (
201
- <box marginTop={1}>
202
- <text bg="yellow" fg="black"> UPDATE AVAILABLE </text>
203
- </box>
204
- )}
205
-
206
- <box marginTop={1}>
207
- <text fg="gray">
208
- {skill.installed
209
- ? "Press u/p to toggle scope"
210
- : "Press u/p to install"}
211
- </text>
212
- </box>
213
- <box>
214
- <text>
215
- <span bg="#555555" fg="white"> o </span>
216
- <span fg="gray"> Open in browser</span>
217
- </text>
218
- </box>
219
- </box>
220
- );
221
- },
100
+ renderRow: ({ item, isSelected }) => {
101
+ const { skill } = item;
102
+ const hasUser = skill.installedScope === "user";
103
+ const hasProject = skill.installedScope === "project";
104
+ const reliability = skill.starReliability;
105
+ const starsStr = formatStars(skill.stars, reliability);
106
+ const sColor = starColor(reliability);
107
+ const displayName = truncateName(skill.name);
108
+ const indentLevel = item.indent ?? 1;
109
+
110
+ return (
111
+ <SelectableRow selected={isSelected} indent={indentLevel}>
112
+ <ScopeSquares
113
+ user={hasUser}
114
+ project={hasProject}
115
+ selected={isSelected}
116
+ />
117
+ <span> </span>
118
+ <span
119
+ fg={
120
+ isSelected
121
+ ? theme.colors.text
122
+ : skill.installed
123
+ ? theme.colors.text
124
+ : theme.colors.muted
125
+ }
126
+ >
127
+ {displayName}
128
+ </span>
129
+ {skill.hasUpdate ? <MetaText text=" ⬆" tone="warning" /> : null}
130
+ {starsStr ? <span fg={sColor}>{` ${starsStr}`}</span> : null}
131
+ </SelectableRow>
132
+ );
133
+ },
134
+
135
+ renderDetail: ({ item }) => {
136
+ const { skill } = item;
137
+ const fm = skill.frontmatter;
138
+ const description = fm?.description || skill.description || "Loading...";
139
+ const reliability = skill.starReliability;
140
+ const starsStr = formatStars(skill.stars, reliability);
141
+ const sColor = starColor(reliability);
142
+ const reliabilityInfo = reliability
143
+ ? STAR_RELIABILITY_INFO[reliability]
144
+ : null;
145
+
146
+ return (
147
+ <box flexDirection="column">
148
+ <text fg={theme.colors.info}>
149
+ <strong>{skill.name}</strong>
150
+ {starsStr ? <span fg={sColor}> {starsStr}</span> : null}
151
+ </text>
152
+
153
+ <box marginTop={1}>
154
+ <text fg={theme.colors.text}>{description}</text>
155
+ </box>
156
+
157
+ {fm?.category ? (
158
+ <KeyValueLine
159
+ label="Category"
160
+ value={<span fg={theme.colors.info}>{fm.category}</span>}
161
+ />
162
+ ) : null}
163
+ {fm?.author ? (
164
+ <KeyValueLine label="Author" value={<span>{fm.author}</span>} />
165
+ ) : null}
166
+ {fm?.version ? (
167
+ <KeyValueLine label="Version" value={<span>{fm.version}</span>} />
168
+ ) : null}
169
+ {fm?.tags && fm.tags.length > 0 ? (
170
+ <KeyValueLine
171
+ label="Tags"
172
+ value={<span>{(fm.tags as string[]).join(", ")}</span>}
173
+ />
174
+ ) : null}
175
+
176
+ <DetailSection>
177
+ <text fg={theme.colors.text}>
178
+ <span fg={theme.colors.muted}>Source </span>
179
+ <span fg={theme.colors.link}>{skill.source.repo}</span>
180
+ </text>
181
+ <text fg={theme.colors.text}>
182
+ <span fg={theme.colors.muted}>{" "}</span>
183
+ <span fg={theme.colors.muted}>{skill.repoPath}</span>
184
+ </text>
185
+ </DetailSection>
186
+
187
+ {reliabilityInfo && reliability !== "dedicated" && (
188
+ <box marginTop={1}>
189
+ <text fg={theme.colors.text}>
190
+ <span fg={sColor}>{starIcon(reliability)} </span>
191
+ <span fg={sColor}>
192
+ <strong>{reliabilityInfo.label}</strong>
193
+ </span>
194
+ <span fg={theme.colors.muted}>
195
+ {" "}
196
+ {reliabilityInfo.description}
197
+ </span>
198
+ </text>
199
+ </box>
200
+ )}
201
+
202
+ <DetailSection>
203
+ <text fg={theme.colors.text}>{"─".repeat(24)}</text>
204
+ <text fg={theme.colors.text}>
205
+ <strong>Scopes:</strong>
206
+ </text>
207
+ <box marginTop={1} flexDirection="column">
208
+ <text fg={theme.colors.text}>
209
+ <span bg={theme.colors.info} fg={theme.hints.fg}>
210
+ {" "}
211
+ u{" "}
212
+ </span>
213
+ <span
214
+ fg={
215
+ skill.installedScope === "user"
216
+ ? theme.colors.info
217
+ : theme.colors.muted
218
+ }
219
+ >
220
+ {skill.installedScope === "user" ? " ● " : " ○ "}
221
+ </span>
222
+ <span fg={theme.colors.info}>User</span>
223
+ <span fg={theme.colors.muted}> ~/.claude/skills/</span>
224
+ </text>
225
+ <text fg={theme.colors.text}>
226
+ <span bg={theme.colors.success} fg={theme.hints.fg}>
227
+ {" "}
228
+ p{" "}
229
+ </span>
230
+ <span
231
+ fg={
232
+ skill.installedScope === "project"
233
+ ? theme.colors.success
234
+ : theme.colors.muted
235
+ }
236
+ >
237
+ {skill.installedScope === "project" ? " ● " : " ○ "}
238
+ </span>
239
+ <span fg={theme.colors.success}>Project</span>
240
+ <span fg={theme.colors.muted}> .claude/skills/</span>
241
+ </text>
242
+ </box>
243
+ </DetailSection>
244
+
245
+ {skill.hasUpdate && (
246
+ <box marginTop={1}>
247
+ <text bg={theme.colors.warning} fg={theme.hints.fg}>
248
+ {" "}
249
+ UPDATE AVAILABLE{" "}
250
+ </text>
251
+ </box>
252
+ )}
253
+
254
+ <box marginTop={1}>
255
+ <text fg={theme.colors.muted}>
256
+ {skill.installed
257
+ ? "Press u/p to toggle scope"
258
+ : "Press u/p to install"}
259
+ </text>
260
+ </box>
261
+ <box>
262
+ <text fg={theme.colors.text}>
263
+ <span bg={theme.colors.dim} fg={theme.hints.fg}>
264
+ {" "}
265
+ o{" "}
266
+ </span>
267
+ <span fg={theme.colors.muted}> Open in browser</span>
268
+ </text>
269
+ </box>
270
+ </box>
271
+ );
272
+ },
222
273
  };
223
274
 
224
275
  // ─── Skill Set renderer ──────────────────────────────────────────────────────
@@ -226,160 +277,193 @@ const skillRenderer: ItemRenderer<SkillSkillItem> = {
226
277
  const MAX_SET_NAME_LEN = 28;
227
278
 
228
279
  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
- },
280
+ renderRow: ({ item, isSelected }) => {
281
+ const { skillSet, expanded } = item;
282
+ const arrow = expanded ? "\u25BC" : "\u25B6";
283
+ const displayName =
284
+ skillSet.name.length > MAX_SET_NAME_LEN
285
+ ? skillSet.name.slice(0, MAX_SET_NAME_LEN - 1) + "\u2026"
286
+ : skillSet.name;
287
+
288
+ // Count installed child skills
289
+ const installedCount = skillSet.loaded
290
+ ? skillSet.skills.filter((s) => s.installed).length
291
+ : 0;
292
+ const totalCount = skillSet.loaded ? skillSet.skills.length : "...";
293
+ const countStr = `(${installedCount}/${totalCount})`;
294
+
295
+ // Any child installed at user or project scope?
296
+ const hasUser =
297
+ skillSet.loaded &&
298
+ skillSet.skills.some((s) => s.installedScope === "user");
299
+ const hasProject =
300
+ skillSet.loaded &&
301
+ skillSet.skills.some((s) => s.installedScope === "project");
302
+
303
+ const starsStr = formatStars(skillSet.stars);
304
+
305
+ return (
306
+ <SelectableRow selected={isSelected} indent={1}>
307
+ <span fg={isSelected ? theme.colors.text : theme.colors.muted}>
308
+ {arrow}{" "}
309
+ </span>
310
+ <span fg={isSelected ? theme.colors.text : theme.colors.warning}>
311
+ {skillSet.icon}{" "}
312
+ </span>
313
+ <span fg={isSelected ? theme.colors.text : theme.colors.info}>
314
+ <strong>{displayName}</strong>
315
+ </span>
316
+ <span fg={theme.colors.muted}> {countStr}</span>
317
+ {starsStr ? <MetaText text={` ${starsStr}`} tone="warning" /> : null}
318
+ <span> </span>
319
+ <ScopeSquares
320
+ user={hasUser}
321
+ project={hasProject}
322
+ selected={isSelected}
323
+ />
324
+ </SelectableRow>
325
+ );
326
+ },
327
+
328
+ renderDetail: ({ item }) => {
329
+ const { skillSet, expanded } = item;
330
+ const starsStr = formatStars(skillSet.stars);
331
+
332
+ return (
333
+ <box flexDirection="column">
334
+ <text fg={theme.colors.info}>
335
+ <strong>
336
+ {skillSet.icon} {skillSet.name}
337
+ </strong>
338
+ {starsStr ? <span fg={theme.colors.warning}> {starsStr}</span> : null}
339
+ </text>
340
+
341
+ <box marginTop={1}>
342
+ <text fg={theme.colors.text}>{skillSet.description}</text>
343
+ </box>
344
+
345
+ <DetailSection>
346
+ <text fg={theme.colors.text}>
347
+ <span fg={theme.colors.muted}>Source </span>
348
+ <span fg={theme.colors.link}>{skillSet.repo}</span>
349
+ </text>
350
+ </DetailSection>
351
+
352
+ {skillSet.loading && (
353
+ <box marginTop={1}>
354
+ <text fg={theme.colors.warning}>Loading skills...</text>
355
+ </box>
356
+ )}
357
+
358
+ {skillSet.error && (
359
+ <box marginTop={1}>
360
+ <text fg={theme.colors.danger}>Error: {skillSet.error}</text>
361
+ </box>
362
+ )}
363
+
364
+ {skillSet.loaded && skillSet.skills.length > 0 && (
365
+ <DetailSection>
366
+ <text fg={theme.colors.text}>
367
+ <strong>Skills in this set:</strong>
368
+ <span fg={theme.colors.muted}>
369
+ {" "}
370
+ ({skillSet.skills.filter((s) => s.installed).length}/
371
+ {skillSet.skills.length} installed)
372
+ </span>
373
+ </text>
374
+ <box marginTop={1} flexDirection="column">
375
+ {skillSet.skills.map((s) => (
376
+ <text key={s.id}>
377
+ <span
378
+ fg={
379
+ s.installedScope === "user"
380
+ ? theme.colors.info
381
+ : theme.colors.muted
382
+ }
383
+ >
384
+ {s.installedScope === "user" ? "\u25A0" : "\u25A1"}
385
+ </span>
386
+ <span
387
+ fg={
388
+ s.installedScope === "project"
389
+ ? theme.colors.success
390
+ : theme.colors.muted
391
+ }
392
+ >
393
+ {s.installedScope === "project" ? "\u25A0" : "\u25A1"}
394
+ </span>
395
+ <span
396
+ fg={s.installed ? theme.colors.text : theme.colors.muted}
397
+ >
398
+ {" "}
399
+ {s.name}
400
+ </span>
401
+ </text>
402
+ ))}
403
+ </box>
404
+ </DetailSection>
405
+ )}
406
+
407
+ <box marginTop={1}>
408
+ <text fg={theme.colors.muted}>
409
+ {expanded
410
+ ? "Press Enter to collapse \u2022 u/p to install all"
411
+ : "Press Enter to expand \u2022 u/p to install all"}
412
+ </text>
413
+ </box>
414
+ <box>
415
+ <text fg={theme.colors.text}>
416
+ <span bg={theme.colors.dim} fg={theme.hints.fg}>
417
+ {" "}
418
+ o{" "}
419
+ </span>
420
+ <span fg={theme.colors.muted}> Open in browser</span>
421
+ </text>
422
+ </box>
423
+ </box>
424
+ );
425
+ },
343
426
  };
344
427
 
345
428
  // ─── Registry ─────────────────────────────────────────────────────────────────
346
429
 
347
430
  export const skillRenderers: {
348
- category: ItemRenderer<SkillCategoryItem>;
349
- skill: ItemRenderer<SkillSkillItem>;
350
- skillset: ItemRenderer<SkillSetItem>;
431
+ category: ItemRenderer<SkillCategoryItem>;
432
+ skill: ItemRenderer<SkillSkillItem>;
433
+ skillset: ItemRenderer<SkillSetItem>;
351
434
  } = {
352
- category: categoryRenderer,
353
- skill: skillRenderer,
354
- skillset: skillSetRenderer,
435
+ category: categoryRenderer,
436
+ skill: skillRenderer,
437
+ skillset: skillSetRenderer,
355
438
  };
356
439
 
357
440
  /**
358
441
  * Dispatch rendering by item kind.
359
442
  */
360
443
  export function renderSkillRow(
361
- item: SkillBrowserItem,
362
- _index: number,
363
- isSelected: boolean,
444
+ item: SkillBrowserItem,
445
+ _index: number,
446
+ isSelected: boolean,
364
447
  ): React.ReactNode {
365
- if (item.kind === "category") {
366
- return skillRenderers.category.renderRow({ item, isSelected });
367
- }
368
- if (item.kind === "skillset") {
369
- return skillRenderers.skillset.renderRow({ item, isSelected });
370
- }
371
- return skillRenderers.skill.renderRow({ item, isSelected });
448
+ if (item.kind === "category") {
449
+ return skillRenderers.category.renderRow({ item, isSelected });
450
+ }
451
+ if (item.kind === "skillset") {
452
+ return skillRenderers.skillset.renderRow({ item, isSelected });
453
+ }
454
+ return skillRenderers.skill.renderRow({ item, isSelected });
372
455
  }
373
456
 
374
457
  export function renderSkillDetail(
375
- item: SkillBrowserItem | undefined,
458
+ item: SkillBrowserItem | undefined,
376
459
  ): React.ReactNode {
377
- if (!item) return <text fg="gray">Select a skill to see details</text>;
378
- if (item.kind === "category") {
379
- return skillRenderers.category.renderDetail({ item });
380
- }
381
- if (item.kind === "skillset") {
382
- return skillRenderers.skillset.renderDetail({ item });
383
- }
384
- return skillRenderers.skill.renderDetail({ item });
460
+ if (!item)
461
+ return <text fg={theme.colors.muted}>Select a skill to see details</text>;
462
+ if (item.kind === "category") {
463
+ return skillRenderers.category.renderDetail({ item });
464
+ }
465
+ if (item.kind === "skillset") {
466
+ return skillRenderers.skillset.renderDetail({ item });
467
+ }
468
+ return skillRenderers.skill.renderDetail({ item });
385
469
  }