claudeup 4.5.5 → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/data/skill-repos.js +56 -0
- package/src/data/skill-repos.ts +70 -1
- package/src/services/claude-settings.js +31 -4
- package/src/services/claude-settings.ts +31 -4
- package/src/services/skills-manager.js +50 -2
- package/src/services/skills-manager.ts +65 -2
- package/src/types/index.ts +30 -1
- package/src/ui/adapters/skillsAdapter.js +57 -9
- package/src/ui/adapters/skillsAdapter.ts +72 -10
- package/src/ui/components/ScrollableList.js +8 -20
- package/src/ui/components/ScrollableList.tsx +16 -29
- package/src/ui/renderers/skillRenderers.js +72 -9
- package/src/ui/renderers/skillRenderers.tsx +176 -11
- package/src/ui/screens/PluginsScreen.js +1 -1
- package/src/ui/screens/PluginsScreen.tsx +1 -0
- package/src/ui/screens/SkillsScreen.js +177 -39
- package/src/ui/screens/SkillsScreen.tsx +199 -34
|
@@ -6,19 +6,22 @@ import { useApp, useModal } from "../state/AppContext.js";
|
|
|
6
6
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
7
7
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
8
8
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
11
11
|
import {
|
|
12
12
|
fetchAvailableSkills,
|
|
13
13
|
fetchSkillFrontmatter,
|
|
14
|
+
fetchSkillSetSkills,
|
|
14
15
|
installSkill,
|
|
15
16
|
uninstallSkill,
|
|
16
17
|
} from "../../services/skills-manager.js";
|
|
17
18
|
import { searchSkills } from "../../services/skillsmp-client.js";
|
|
18
|
-
import { DEFAULT_SKILL_REPOS, RECOMMENDED_SKILLS } from "../../data/skill-repos.js";
|
|
19
|
-
import type { SkillInfo, SkillSource } from "../../types/index.js";
|
|
19
|
+
import { DEFAULT_SKILL_REPOS, RECOMMENDED_SKILLS, RECOMMENDED_SKILL_SETS, classifyStarReliability } from "../../data/skill-repos.js";
|
|
20
|
+
import type { SkillInfo, SkillSetInfo, SkillSource } from "../../types/index.js";
|
|
20
21
|
import { buildSkillBrowserItems } from "../adapters/skillsAdapter.js";
|
|
22
|
+
import type { SkillBrowserItem } from "../adapters/skillsAdapter.js";
|
|
21
23
|
import { renderSkillRow, renderSkillDetail } from "../renderers/skillRenderers.js";
|
|
24
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
22
25
|
|
|
23
26
|
export function SkillsScreen() {
|
|
24
27
|
const { state, dispatch } = useApp();
|
|
@@ -99,6 +102,7 @@ export function SkillsScreen() {
|
|
|
99
102
|
installedScope: null,
|
|
100
103
|
hasUpdate: false,
|
|
101
104
|
stars: r.stars,
|
|
105
|
+
starReliability: classifyStarReliability(r.repo || "unknown", r.stars),
|
|
102
106
|
};
|
|
103
107
|
});
|
|
104
108
|
searchCacheRef.current.set(query, mapped);
|
|
@@ -143,6 +147,128 @@ export function SkillsScreen() {
|
|
|
143
147
|
scanDisk();
|
|
144
148
|
}, [scanDisk, state.dataRefreshVersion]);
|
|
145
149
|
|
|
150
|
+
// ── Skill Sets state ──────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
const [skillSets, setSkillSets] = useState<SkillSetInfo[]>(() =>
|
|
153
|
+
RECOMMENDED_SKILL_SETS.map((rs) => ({
|
|
154
|
+
id: rs.repo,
|
|
155
|
+
name: rs.name,
|
|
156
|
+
description: rs.description,
|
|
157
|
+
repo: rs.repo,
|
|
158
|
+
icon: rs.icon,
|
|
159
|
+
stars: rs.stars,
|
|
160
|
+
skills: [],
|
|
161
|
+
loaded: false,
|
|
162
|
+
loading: false,
|
|
163
|
+
})),
|
|
164
|
+
);
|
|
165
|
+
const [expandedSets, setExpandedSets] = useState<Set<string>>(new Set());
|
|
166
|
+
|
|
167
|
+
// Re-mark installed status on child skills when disk state changes
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
setSkillSets((prev) =>
|
|
170
|
+
prev.map((set) => {
|
|
171
|
+
if (!set.loaded) return set;
|
|
172
|
+
const updatedSkills = set.skills.map((skill) => {
|
|
173
|
+
const slug = skill.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
174
|
+
const isUser = installedFromDisk.user.has(slug) || installedFromDisk.user.has(skill.name);
|
|
175
|
+
const isProj = installedFromDisk.project.has(slug) || installedFromDisk.project.has(skill.name);
|
|
176
|
+
return {
|
|
177
|
+
...skill,
|
|
178
|
+
installed: isUser || isProj,
|
|
179
|
+
installedScope: isProj ? "project" as const : isUser ? "user" as const : null,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
return { ...set, skills: updatedSkills };
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
}, [installedFromDisk]);
|
|
186
|
+
|
|
187
|
+
const handleToggleSet = useCallback(
|
|
188
|
+
async (setId: string) => {
|
|
189
|
+
const isExpanded = expandedSets.has(setId);
|
|
190
|
+
const newExpanded = new Set(expandedSets);
|
|
191
|
+
if (isExpanded) {
|
|
192
|
+
newExpanded.delete(setId);
|
|
193
|
+
} else {
|
|
194
|
+
newExpanded.add(setId);
|
|
195
|
+
}
|
|
196
|
+
setExpandedSets(newExpanded);
|
|
197
|
+
|
|
198
|
+
// Fetch skills on first expand
|
|
199
|
+
const set = skillSets.find((s) => s.id === setId);
|
|
200
|
+
if (!isExpanded && set && !set.loaded && !set.loading) {
|
|
201
|
+
setSkillSets((prev) =>
|
|
202
|
+
prev.map((s) => (s.id === setId ? { ...s, loading: true } : s)),
|
|
203
|
+
);
|
|
204
|
+
try {
|
|
205
|
+
const skills = await fetchSkillSetSkills(set.repo, state.projectPath);
|
|
206
|
+
setSkillSets((prev) =>
|
|
207
|
+
prev.map((s) =>
|
|
208
|
+
s.id === setId
|
|
209
|
+
? { ...s, skills, loaded: true, loading: false, error: undefined }
|
|
210
|
+
: s,
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
setSkillSets((prev) =>
|
|
215
|
+
prev.map((s) =>
|
|
216
|
+
s.id === setId
|
|
217
|
+
? {
|
|
218
|
+
...s,
|
|
219
|
+
loading: false,
|
|
220
|
+
error: error instanceof Error ? error.message : String(error),
|
|
221
|
+
}
|
|
222
|
+
: s,
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
[expandedSets, skillSets, state.projectPath],
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Status bar message (auto-clears)
|
|
232
|
+
const [statusMsg, setStatusMsg] = useState<{ text: string; tone: "success" | "error" } | null>(null);
|
|
233
|
+
const statusTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
234
|
+
const showStatus = useCallback((text: string, tone: "success" | "error" = "success") => {
|
|
235
|
+
setStatusMsg({ text, tone });
|
|
236
|
+
if (statusTimerRef.current) clearTimeout(statusTimerRef.current);
|
|
237
|
+
statusTimerRef.current = setTimeout(() => setStatusMsg(null), 3000);
|
|
238
|
+
}, []);
|
|
239
|
+
|
|
240
|
+
const handleInstallAllFromSet = useCallback(
|
|
241
|
+
async (setId: string, scope: "user" | "project") => {
|
|
242
|
+
const set = skillSets.find((s) => s.id === setId);
|
|
243
|
+
if (!set || !set.loaded) return;
|
|
244
|
+
|
|
245
|
+
const toInstall = set.skills.filter((s) => !s.installed);
|
|
246
|
+
if (toInstall.length === 0) {
|
|
247
|
+
showStatus(`All ${set.name} skills already installed`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
showStatus(`Installing ${toInstall.length} skills from ${set.name}...`);
|
|
252
|
+
let installed = 0;
|
|
253
|
+
let failed = 0;
|
|
254
|
+
for (const skill of toInstall) {
|
|
255
|
+
try {
|
|
256
|
+
await installSkill(skill, scope, state.projectPath);
|
|
257
|
+
installed++;
|
|
258
|
+
} catch {
|
|
259
|
+
failed++;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
await scanDisk();
|
|
263
|
+
if (failed > 0) {
|
|
264
|
+
showStatus(`Installed ${installed}/${toInstall.length} (${failed} failed)`, "error");
|
|
265
|
+
} else {
|
|
266
|
+
showStatus(`Installed ${installed} skills from ${set.name} to ${scope}`);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
[skillSets, state.projectPath, scanDisk, showStatus],
|
|
270
|
+
);
|
|
271
|
+
|
|
146
272
|
// ── Derived data ──────────────────────────────────────────────────────────
|
|
147
273
|
|
|
148
274
|
const staticRecommended = useMemo((): SkillInfo[] => {
|
|
@@ -163,6 +289,7 @@ export function SkillsScreen() {
|
|
|
163
289
|
hasUpdate: false,
|
|
164
290
|
isRecommended: true,
|
|
165
291
|
stars: r.stars,
|
|
292
|
+
starReliability: classifyStarReliability(r.repo, r.stars),
|
|
166
293
|
};
|
|
167
294
|
});
|
|
168
295
|
}, [installedFromDisk]);
|
|
@@ -231,6 +358,8 @@ export function SkillsScreen() {
|
|
|
231
358
|
searchResults,
|
|
232
359
|
query: skillsState.searchQuery,
|
|
233
360
|
isSearchLoading,
|
|
361
|
+
skillSets,
|
|
362
|
+
expandedSets,
|
|
234
363
|
}),
|
|
235
364
|
[
|
|
236
365
|
mergedRecommended,
|
|
@@ -239,6 +368,8 @@ export function SkillsScreen() {
|
|
|
239
368
|
searchResults,
|
|
240
369
|
skillsState.searchQuery,
|
|
241
370
|
isSearchLoading,
|
|
371
|
+
skillSets,
|
|
372
|
+
expandedSets,
|
|
242
373
|
],
|
|
243
374
|
);
|
|
244
375
|
|
|
@@ -246,39 +377,49 @@ export function SkillsScreen() {
|
|
|
246
377
|
useEffect(() => {
|
|
247
378
|
const item = allItems[skillsState.selectedIndex];
|
|
248
379
|
if (item && item.kind === "category") {
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
380
|
+
const firstSelectable = allItems.findIndex((i) => i.kind === "skill" || i.kind === "skillset");
|
|
381
|
+
if (firstSelectable >= 0) dispatch({ type: "SKILLS_SELECT", index: firstSelectable });
|
|
251
382
|
}
|
|
252
383
|
}, [allItems, skillsState.selectedIndex, dispatch]);
|
|
253
384
|
|
|
254
|
-
const selectedItem = allItems[skillsState.selectedIndex];
|
|
385
|
+
const selectedItem: SkillBrowserItem | undefined = allItems[skillsState.selectedIndex];
|
|
255
386
|
const selectedSkill =
|
|
256
387
|
selectedItem?.kind === "skill" ? selectedItem.skill : undefined;
|
|
388
|
+
const selectedSet =
|
|
389
|
+
selectedItem?.kind === "skillset" ? selectedItem.skillSet : undefined;
|
|
257
390
|
|
|
258
391
|
// ── Lazy-load frontmatter for selected skill ───────────────────────────────
|
|
259
392
|
|
|
393
|
+
// Check if selected skill belongs to a skill set (lives in local state, not reducer)
|
|
394
|
+
const isSkillSetChild = selectedSkill
|
|
395
|
+
? skillSets.some((s) => s.loaded && s.skills.some((sk) => sk.id === selectedSkill.id))
|
|
396
|
+
: false;
|
|
397
|
+
|
|
260
398
|
useEffect(() => {
|
|
261
399
|
if (!selectedSkill || selectedSkill.frontmatter) return;
|
|
262
400
|
fetchSkillFrontmatter(selectedSkill).then((fm) => {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
401
|
+
if (isSkillSetChild) {
|
|
402
|
+
// Update the local skillSets state
|
|
403
|
+
setSkillSets((prev) =>
|
|
404
|
+
prev.map((set) => ({
|
|
405
|
+
...set,
|
|
406
|
+
skills: set.skills.map((sk) =>
|
|
407
|
+
sk.id === selectedSkill.id ? { ...sk, frontmatter: fm } : sk,
|
|
408
|
+
),
|
|
409
|
+
})),
|
|
410
|
+
);
|
|
411
|
+
} else {
|
|
412
|
+
dispatch({
|
|
413
|
+
type: "SKILLS_UPDATE_ITEM",
|
|
414
|
+
name: selectedSkill.name,
|
|
415
|
+
updates: { frontmatter: fm },
|
|
416
|
+
});
|
|
417
|
+
}
|
|
268
418
|
}).catch(() => {});
|
|
269
|
-
}, [selectedSkill?.id, dispatch]);
|
|
419
|
+
}, [selectedSkill?.id, isSkillSetChild, dispatch]);
|
|
270
420
|
|
|
271
421
|
// ── Action handlers ───────────────────────────────────────────────────────
|
|
272
422
|
|
|
273
|
-
// Status bar message (auto-clears)
|
|
274
|
-
const [statusMsg, setStatusMsg] = useState<{ text: string; tone: "success" | "error" } | null>(null);
|
|
275
|
-
const statusTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
276
|
-
const showStatus = useCallback((text: string, tone: "success" | "error" = "success") => {
|
|
277
|
-
setStatusMsg({ text, tone });
|
|
278
|
-
if (statusTimerRef.current) clearTimeout(statusTimerRef.current);
|
|
279
|
-
statusTimerRef.current = setTimeout(() => setStatusMsg(null), 3000);
|
|
280
|
-
}, []);
|
|
281
|
-
|
|
282
423
|
const handleInstall = useCallback(async (scope: "user" | "project") => {
|
|
283
424
|
if (!selectedSkill) return;
|
|
284
425
|
try {
|
|
@@ -352,6 +493,10 @@ export function SkillsScreen() {
|
|
|
352
493
|
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
353
494
|
return;
|
|
354
495
|
}
|
|
496
|
+
if (selectedSet) {
|
|
497
|
+
handleToggleSet(selectedSet.id);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
355
500
|
if (selectedSkill && !selectedSkill.installed) {
|
|
356
501
|
handleInstall("project");
|
|
357
502
|
}
|
|
@@ -360,14 +505,26 @@ export function SkillsScreen() {
|
|
|
360
505
|
|
|
361
506
|
// Action keys only when NOT actively typing in search
|
|
362
507
|
if (!isSearchActive) {
|
|
363
|
-
if (event.name === "u"
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (selectedSkill
|
|
369
|
-
|
|
370
|
-
|
|
508
|
+
if (event.name === "u") {
|
|
509
|
+
if (selectedSet) {
|
|
510
|
+
handleInstallAllFromSet(selectedSet.id, "user");
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (selectedSkill) {
|
|
514
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "user") handleUninstall();
|
|
515
|
+
else handleInstall("user");
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
} else if (event.name === "p") {
|
|
519
|
+
if (selectedSet) {
|
|
520
|
+
handleInstallAllFromSet(selectedSet.id, "project");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (selectedSkill) {
|
|
524
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "project") handleUninstall();
|
|
525
|
+
else handleInstall("project");
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
371
528
|
}
|
|
372
529
|
}
|
|
373
530
|
|
|
@@ -387,14 +544,21 @@ export function SkillsScreen() {
|
|
|
387
544
|
|
|
388
545
|
if (event.name === "r") {
|
|
389
546
|
fetchData();
|
|
390
|
-
} else if (event.name === "o" && selectedSkill) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (repo && repo !== "local") {
|
|
394
|
-
const url = `https://github.com/${repo}/tree/main/${repoPath}`;
|
|
547
|
+
} else if (event.name === "o" && (selectedSkill || selectedSet)) {
|
|
548
|
+
if (selectedSet) {
|
|
549
|
+
const url = `https://github.com/${selectedSet.repo}`;
|
|
395
550
|
import("node:child_process").then(({ execSync: exec }) => {
|
|
396
551
|
try { exec(`open "${url}"`); } catch { /* ignore */ }
|
|
397
552
|
});
|
|
553
|
+
} else if (selectedSkill) {
|
|
554
|
+
const repo = selectedSkill.source.repo;
|
|
555
|
+
const repoPath = selectedSkill.repoPath?.replace("/SKILL.md", "") || "";
|
|
556
|
+
if (repo && repo !== "local") {
|
|
557
|
+
const url = `https://github.com/${repo}/tree/main/${repoPath}`;
|
|
558
|
+
import("node:child_process").then(({ execSync: exec }) => {
|
|
559
|
+
try { exec(`open "${url}"`); } catch { /* ignore */ }
|
|
560
|
+
});
|
|
561
|
+
}
|
|
398
562
|
}
|
|
399
563
|
} else if (event.name === "/") {
|
|
400
564
|
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
@@ -454,6 +618,7 @@ export function SkillsScreen() {
|
|
|
454
618
|
selectedIndex={skillsState.selectedIndex}
|
|
455
619
|
renderItem={renderSkillRow}
|
|
456
620
|
maxHeight={dimensions.listPanelHeight}
|
|
621
|
+
getKey={(item, index) => `${index}:${item.id}`}
|
|
457
622
|
/>
|
|
458
623
|
{!query && skillsState.skills.status === "loading" && (
|
|
459
624
|
<box marginTop={2} paddingLeft={2}>
|