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.
- package/package.json +4 -4
- package/src/__tests__/git-worktree.test.ts +108 -0
- package/src/__tests__/scope-squares.test.tsx +165 -0
- package/src/__tests__/theme-adaptive-colors.test.ts +307 -0
- package/src/__tests__/uppercase-keybindings.test.ts +101 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/main.tsx +21 -5
- package/src/opentui.d.ts +21 -12
- package/src/services/claude-settings.ts +37 -7
- package/src/services/git-worktree.ts +129 -0
- package/src/services/plugin-manager.ts +10 -1
- package/src/ui/App.tsx +19 -12
- package/src/ui/adapters/pluginsAdapter.ts +174 -168
- package/src/ui/adapters/settingsAdapter.ts +119 -116
- package/src/ui/adapters/skillsAdapter.ts +203 -196
- package/src/ui/components/CategoryHeader.tsx +9 -8
- package/src/ui/components/EmptyFilterState.tsx +10 -5
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +10 -6
- package/src/ui/components/ScrollableList.tsx +3 -2
- package/src/ui/components/SearchInput.tsx +2 -1
- package/src/ui/components/StyledText.tsx +5 -4
- package/src/ui/components/TabBar.tsx +4 -3
- package/src/ui/components/layout/FooterHints.tsx +37 -30
- package/src/ui/components/layout/Panel.tsx +6 -5
- package/src/ui/components/layout/ProgressBar.tsx +7 -6
- package/src/ui/components/layout/ScopeTabs.tsx +6 -5
- package/src/ui/components/layout/ScreenLayout.tsx +27 -24
- package/src/ui/components/layout/index.ts +3 -3
- package/src/ui/components/modals/ConfirmModal.tsx +12 -11
- package/src/ui/components/modals/InputModal.tsx +14 -6
- package/src/ui/components/modals/LoadingModal.tsx +6 -5
- package/src/ui/components/modals/MessageModal.tsx +9 -8
- package/src/ui/components/modals/SelectModal.tsx +11 -7
- package/src/ui/components/modals/VersionMismatchModal.tsx +14 -16
- package/src/ui/components/primitives/ActionHints.tsx +26 -26
- package/src/ui/components/primitives/DetailSection.tsx +13 -12
- package/src/ui/components/primitives/KeyValueLine.tsx +9 -8
- package/src/ui/components/primitives/ListCategoryRow.tsx +25 -27
- package/src/ui/components/primitives/MetaText.tsx +3 -3
- package/src/ui/components/primitives/ScopeDetail.tsx +48 -48
- package/src/ui/components/primitives/ScopeSquares.tsx +47 -22
- package/src/ui/components/primitives/SelectableRow.tsx +22 -16
- package/src/ui/hooks/useGitignoreModal.ts +78 -74
- package/src/ui/registry.ts +11 -11
- package/src/ui/renderers/cliToolRenderers.tsx +260 -203
- package/src/ui/renderers/gitignoreRenderers.tsx +43 -42
- package/src/ui/renderers/mcpRenderers.tsx +121 -117
- package/src/ui/renderers/pluginRenderers.tsx +528 -471
- package/src/ui/renderers/profileRenderers.tsx +346 -300
- package/src/ui/renderers/settingsRenderers.tsx +183 -176
- package/src/ui/renderers/skillRenderers.tsx +410 -326
- package/src/ui/screens/AliasScreen.tsx +1336 -1309
- package/src/ui/screens/CliToolsScreen.tsx +92 -40
- package/src/ui/screens/EnvVarsScreen.tsx +19 -13
- package/src/ui/screens/GitignoreScreen.tsx +510 -493
- package/src/ui/screens/McpRegistryScreen.tsx +28 -21
- package/src/ui/screens/McpScreen.tsx +12 -3
- package/src/ui/screens/PluginsScreen.tsx +17 -7
- package/src/ui/screens/ProfilesScreen.tsx +39 -23
- package/src/ui/screens/SkillsScreen.tsx +832 -688
- package/src/ui/theme-mode.ts +73 -0
- 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 {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
if (reliability === "mega-repo") return "☆";
|
|
36
|
+
if (reliability === "skill-dump") return "☆";
|
|
37
|
+
return "★";
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
function starColor(reliability?: StarReliability):
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
431
|
+
category: ItemRenderer<SkillCategoryItem>;
|
|
432
|
+
skill: ItemRenderer<SkillSkillItem>;
|
|
433
|
+
skillset: ItemRenderer<SkillSetItem>;
|
|
351
434
|
} = {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
444
|
+
item: SkillBrowserItem,
|
|
445
|
+
_index: number,
|
|
446
|
+
isSelected: boolean,
|
|
364
447
|
): React.ReactNode {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
458
|
+
item: SkillBrowserItem | undefined,
|
|
376
459
|
): React.ReactNode {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
}
|