claudeup 3.8.0 → 3.9.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 +1 -1
- package/src/data/skill-repos.js +86 -0
- package/src/data/skill-repos.ts +97 -0
- package/src/services/skills-manager.js +239 -0
- package/src/services/skills-manager.ts +328 -0
- package/src/services/skillsmp-client.js +67 -0
- package/src/services/skillsmp-client.ts +89 -0
- package/src/types/index.ts +67 -1
- package/src/ui/App.js +8 -2
- package/src/ui/App.tsx +7 -1
- package/src/ui/components/TabBar.js +1 -0
- package/src/ui/components/TabBar.tsx +1 -0
- package/src/ui/screens/SkillsScreen.js +325 -0
- package/src/ui/screens/SkillsScreen.tsx +574 -0
- package/src/ui/screens/index.js +1 -0
- package/src/ui/screens/index.ts +1 -0
- package/src/ui/state/reducer.js +88 -0
- package/src/ui/state/reducer.ts +99 -0
- package/src/ui/state/types.ts +27 -2
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import React, { useEffect, useCallback, useMemo } from "react";
|
|
2
|
+
import { useApp, useModal } from "../state/AppContext.js";
|
|
3
|
+
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
|
+
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
5
|
+
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
7
|
+
import {
|
|
8
|
+
fetchAvailableSkills,
|
|
9
|
+
fetchSkillFrontmatter,
|
|
10
|
+
installSkill,
|
|
11
|
+
uninstallSkill,
|
|
12
|
+
} from "../../services/skills-manager.js";
|
|
13
|
+
import { DEFAULT_SKILL_REPOS, RECOMMENDED_SKILLS } from "../../data/skill-repos.js";
|
|
14
|
+
import type { SkillInfo } from "../../types/index.js";
|
|
15
|
+
|
|
16
|
+
interface SkillListItem {
|
|
17
|
+
id: string;
|
|
18
|
+
type: "category" | "skill";
|
|
19
|
+
label: string;
|
|
20
|
+
skill?: SkillInfo;
|
|
21
|
+
categoryKey?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CATEGORY_COLORS: Record<string, string> = {
|
|
25
|
+
recommended: "#2e7d32",
|
|
26
|
+
frontend: "#1565c0",
|
|
27
|
+
design: "#6a1b9a",
|
|
28
|
+
media: "#e65100",
|
|
29
|
+
security: "#b71c1c",
|
|
30
|
+
debugging: "#00838f",
|
|
31
|
+
database: "#4527a0",
|
|
32
|
+
search: "#4e342e",
|
|
33
|
+
general: "#333333",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const RECOMMENDED_NAMES = new Set(RECOMMENDED_SKILLS.map((r) => r.name));
|
|
37
|
+
|
|
38
|
+
export function SkillsScreen() {
|
|
39
|
+
const { state, dispatch } = useApp();
|
|
40
|
+
const { skills: skillsState } = state;
|
|
41
|
+
const modal = useModal();
|
|
42
|
+
const dimensions = useDimensions();
|
|
43
|
+
|
|
44
|
+
const isSearchActive =
|
|
45
|
+
state.isSearching &&
|
|
46
|
+
state.currentRoute.screen === "skills" &&
|
|
47
|
+
!state.modal;
|
|
48
|
+
|
|
49
|
+
// Fetch data
|
|
50
|
+
const fetchData = useCallback(async () => {
|
|
51
|
+
dispatch({ type: "SKILLS_DATA_LOADING" });
|
|
52
|
+
try {
|
|
53
|
+
const skills = await fetchAvailableSkills(
|
|
54
|
+
DEFAULT_SKILL_REPOS,
|
|
55
|
+
state.projectPath,
|
|
56
|
+
);
|
|
57
|
+
dispatch({ type: "SKILLS_DATA_SUCCESS", skills });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
dispatch({
|
|
60
|
+
type: "SKILLS_DATA_ERROR",
|
|
61
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}, [dispatch, state.projectPath]);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
fetchData();
|
|
68
|
+
}, [fetchData, state.dataRefreshVersion]);
|
|
69
|
+
|
|
70
|
+
// Build flat list: recommended first (as their own category), then by repo
|
|
71
|
+
const allItems = useMemo((): SkillListItem[] => {
|
|
72
|
+
if (skillsState.skills.status !== "success") return [];
|
|
73
|
+
|
|
74
|
+
const skills = skillsState.skills.data;
|
|
75
|
+
const query = skillsState.searchQuery.toLowerCase();
|
|
76
|
+
|
|
77
|
+
const filtered = query
|
|
78
|
+
? skills.filter(
|
|
79
|
+
(s) =>
|
|
80
|
+
s.name.toLowerCase().includes(query) ||
|
|
81
|
+
s.source.repo.toLowerCase().includes(query) ||
|
|
82
|
+
s.frontmatter?.description?.toLowerCase().includes(query),
|
|
83
|
+
)
|
|
84
|
+
: skills;
|
|
85
|
+
|
|
86
|
+
const items: SkillListItem[] = [];
|
|
87
|
+
|
|
88
|
+
// Recommended section
|
|
89
|
+
const recommendedSkills = filtered.filter((s) =>
|
|
90
|
+
RECOMMENDED_NAMES.has(
|
|
91
|
+
s.name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
92
|
+
) || RECOMMENDED_SKILLS.some((r) => r.skillPath === s.name),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (recommendedSkills.length > 0) {
|
|
96
|
+
items.push({
|
|
97
|
+
id: "cat:recommended",
|
|
98
|
+
type: "category",
|
|
99
|
+
label: "Recommended",
|
|
100
|
+
categoryKey: "recommended",
|
|
101
|
+
});
|
|
102
|
+
for (const skill of recommendedSkills) {
|
|
103
|
+
items.push({
|
|
104
|
+
id: `skill:${skill.id}`,
|
|
105
|
+
type: "skill",
|
|
106
|
+
label: skill.name,
|
|
107
|
+
skill,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Group remaining skills by repo
|
|
113
|
+
const repoMap = new Map<string, SkillInfo[]>();
|
|
114
|
+
for (const skill of filtered) {
|
|
115
|
+
const isRec = recommendedSkills.includes(skill);
|
|
116
|
+
if (isRec) continue;
|
|
117
|
+
const existing = repoMap.get(skill.source.repo) || [];
|
|
118
|
+
existing.push(skill);
|
|
119
|
+
repoMap.set(skill.source.repo, existing);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const [repo, repoSkills] of repoMap) {
|
|
123
|
+
items.push({
|
|
124
|
+
id: `cat:${repo}`,
|
|
125
|
+
type: "category",
|
|
126
|
+
label: `${repo} (${repoSkills.length})`,
|
|
127
|
+
categoryKey: repo,
|
|
128
|
+
});
|
|
129
|
+
for (const skill of repoSkills) {
|
|
130
|
+
items.push({
|
|
131
|
+
id: `skill:${skill.id}`,
|
|
132
|
+
type: "skill",
|
|
133
|
+
label: skill.name,
|
|
134
|
+
skill,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return items;
|
|
140
|
+
}, [skillsState.skills, skillsState.searchQuery]);
|
|
141
|
+
|
|
142
|
+
const selectableItems = useMemo(
|
|
143
|
+
() => allItems.filter((item) => item.type === "skill" || item.type === "category"),
|
|
144
|
+
[allItems],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const selectedItem = selectableItems[skillsState.selectedIndex];
|
|
148
|
+
const selectedSkill = selectedItem?.type === "skill" ? selectedItem.skill : undefined;
|
|
149
|
+
|
|
150
|
+
// Lazy-load frontmatter for selected skill
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!selectedSkill || selectedSkill.frontmatter) return;
|
|
153
|
+
|
|
154
|
+
fetchSkillFrontmatter(selectedSkill).then((fm) => {
|
|
155
|
+
dispatch({
|
|
156
|
+
type: "SKILLS_UPDATE_ITEM",
|
|
157
|
+
name: selectedSkill.name,
|
|
158
|
+
updates: { frontmatter: fm },
|
|
159
|
+
});
|
|
160
|
+
}).catch(() => {});
|
|
161
|
+
}, [selectedSkill?.id, dispatch]);
|
|
162
|
+
|
|
163
|
+
// Install handler
|
|
164
|
+
const handleInstall = useCallback(async (scope: "user" | "project") => {
|
|
165
|
+
if (!selectedSkill) return;
|
|
166
|
+
|
|
167
|
+
modal.loading(`Installing ${selectedSkill.name}...`);
|
|
168
|
+
try {
|
|
169
|
+
await installSkill(selectedSkill, scope, state.projectPath);
|
|
170
|
+
modal.hideModal();
|
|
171
|
+
dispatch({
|
|
172
|
+
type: "SKILLS_UPDATE_ITEM",
|
|
173
|
+
name: selectedSkill.name,
|
|
174
|
+
updates: {
|
|
175
|
+
installed: true,
|
|
176
|
+
installedScope: scope,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
await modal.message(
|
|
180
|
+
"Installed",
|
|
181
|
+
`${selectedSkill.name} installed to ${scope === "user" ? "~/.claude/skills/" : ".claude/skills/"}`,
|
|
182
|
+
"success",
|
|
183
|
+
);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
modal.hideModal();
|
|
186
|
+
await modal.message("Error", `Failed to install: ${error}`, "error");
|
|
187
|
+
}
|
|
188
|
+
}, [selectedSkill, state.projectPath, dispatch, modal]);
|
|
189
|
+
|
|
190
|
+
// Uninstall handler
|
|
191
|
+
const handleUninstall = useCallback(async () => {
|
|
192
|
+
if (!selectedSkill || !selectedSkill.installed) return;
|
|
193
|
+
|
|
194
|
+
const scope = selectedSkill.installedScope;
|
|
195
|
+
if (!scope) return;
|
|
196
|
+
|
|
197
|
+
const confirmed = await modal.confirm(
|
|
198
|
+
`Uninstall "${selectedSkill.name}"?`,
|
|
199
|
+
`This will remove it from the ${scope} scope.`,
|
|
200
|
+
);
|
|
201
|
+
if (!confirmed) return;
|
|
202
|
+
|
|
203
|
+
modal.loading(`Uninstalling ${selectedSkill.name}...`);
|
|
204
|
+
try {
|
|
205
|
+
await uninstallSkill(selectedSkill.name, scope, state.projectPath);
|
|
206
|
+
modal.hideModal();
|
|
207
|
+
dispatch({
|
|
208
|
+
type: "SKILLS_UPDATE_ITEM",
|
|
209
|
+
name: selectedSkill.name,
|
|
210
|
+
updates: {
|
|
211
|
+
installed: false,
|
|
212
|
+
installedScope: null,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
await modal.message("Uninstalled", `${selectedSkill.name} removed.`, "success");
|
|
216
|
+
} catch (error) {
|
|
217
|
+
modal.hideModal();
|
|
218
|
+
await modal.message("Error", `Failed to uninstall: ${error}`, "error");
|
|
219
|
+
}
|
|
220
|
+
}, [selectedSkill, state.projectPath, dispatch, modal]);
|
|
221
|
+
|
|
222
|
+
// Keyboard handling
|
|
223
|
+
useKeyboard((event) => {
|
|
224
|
+
if (state.modal) return;
|
|
225
|
+
|
|
226
|
+
if (event.name === "up" || event.name === "k") {
|
|
227
|
+
if (state.isSearching) return;
|
|
228
|
+
const newIndex = Math.max(0, skillsState.selectedIndex - 1);
|
|
229
|
+
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
230
|
+
} else if (event.name === "down" || event.name === "j") {
|
|
231
|
+
if (state.isSearching) return;
|
|
232
|
+
const newIndex = Math.min(
|
|
233
|
+
Math.max(0, selectableItems.length - 1),
|
|
234
|
+
skillsState.selectedIndex + 1,
|
|
235
|
+
);
|
|
236
|
+
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
237
|
+
} else if (event.name === "u") {
|
|
238
|
+
if (state.isSearching) return;
|
|
239
|
+
if (selectedSkill) {
|
|
240
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "user") {
|
|
241
|
+
handleUninstall();
|
|
242
|
+
} else {
|
|
243
|
+
handleInstall("user");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} else if (event.name === "p") {
|
|
247
|
+
if (state.isSearching) return;
|
|
248
|
+
if (selectedSkill) {
|
|
249
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "project") {
|
|
250
|
+
handleUninstall();
|
|
251
|
+
} else {
|
|
252
|
+
handleInstall("project");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else if (event.name === "return" || event.name === "enter") {
|
|
256
|
+
if (state.isSearching) return;
|
|
257
|
+
if (selectedSkill && !selectedSkill.installed) {
|
|
258
|
+
handleInstall("project");
|
|
259
|
+
}
|
|
260
|
+
} else if (event.name === "d") {
|
|
261
|
+
if (state.isSearching) return;
|
|
262
|
+
if (selectedSkill?.installed) {
|
|
263
|
+
handleUninstall();
|
|
264
|
+
}
|
|
265
|
+
} else if (event.name === "r") {
|
|
266
|
+
if (state.isSearching) return;
|
|
267
|
+
fetchData();
|
|
268
|
+
} else if (event.name === "escape") {
|
|
269
|
+
if (skillsState.searchQuery) {
|
|
270
|
+
dispatch({ type: "SKILLS_SET_SEARCH", query: "" });
|
|
271
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
272
|
+
}
|
|
273
|
+
} else if (event.name === "backspace") {
|
|
274
|
+
if (skillsState.searchQuery) {
|
|
275
|
+
const newQuery = skillsState.searchQuery.slice(0, -1);
|
|
276
|
+
dispatch({ type: "SKILLS_SET_SEARCH", query: newQuery });
|
|
277
|
+
if (!newQuery) {
|
|
278
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else if (
|
|
282
|
+
event.name &&
|
|
283
|
+
event.name.length === 1 &&
|
|
284
|
+
!/[0-9]/.test(event.name) &&
|
|
285
|
+
!event.ctrl &&
|
|
286
|
+
!event.meta
|
|
287
|
+
) {
|
|
288
|
+
// Inline search: type to filter
|
|
289
|
+
const newQuery = skillsState.searchQuery + event.name;
|
|
290
|
+
dispatch({ type: "SKILLS_SET_SEARCH", query: newQuery });
|
|
291
|
+
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const renderListItem = (
|
|
296
|
+
item: SkillListItem,
|
|
297
|
+
_idx: number,
|
|
298
|
+
isSelected: boolean,
|
|
299
|
+
) => {
|
|
300
|
+
if (item.type === "category") {
|
|
301
|
+
const catKey = item.categoryKey || "general";
|
|
302
|
+
const isRec = catKey === "recommended";
|
|
303
|
+
const bgColor = CATEGORY_COLORS[catKey] || CATEGORY_COLORS.general;
|
|
304
|
+
const star = isRec ? "★ " : "";
|
|
305
|
+
|
|
306
|
+
if (isSelected) {
|
|
307
|
+
return (
|
|
308
|
+
<text bg="magenta" fg="white">
|
|
309
|
+
<strong> {star}{item.label} </strong>
|
|
310
|
+
</text>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
return (
|
|
314
|
+
<text bg={bgColor} fg="white">
|
|
315
|
+
<strong> {star}{item.label} </strong>
|
|
316
|
+
</text>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (item.type === "skill" && item.skill) {
|
|
321
|
+
const skill = item.skill;
|
|
322
|
+
const indicator = skill.installed ? "●" : "○";
|
|
323
|
+
const indicatorColor = skill.installed ? "cyan" : "gray";
|
|
324
|
+
const scopeLabel = skill.installedScope
|
|
325
|
+
? skill.installedScope === "user"
|
|
326
|
+
? "[u]"
|
|
327
|
+
: "[p]"
|
|
328
|
+
: " ";
|
|
329
|
+
const updateBadge = skill.hasUpdate ? "[UPDT] " : "";
|
|
330
|
+
|
|
331
|
+
if (isSelected) {
|
|
332
|
+
return (
|
|
333
|
+
<text bg="magenta" fg="white">
|
|
334
|
+
{" "}
|
|
335
|
+
{indicator} {scopeLabel} {updateBadge}{skill.name.padEnd(30)}{skill.source.repo}{" "}
|
|
336
|
+
</text>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<text>
|
|
342
|
+
<span fg={indicatorColor}> {indicator} </span>
|
|
343
|
+
<span fg={skill.installedScope === "user" ? "cyan" : skill.installedScope === "project" ? "green" : "gray"}>
|
|
344
|
+
{scopeLabel}
|
|
345
|
+
</span>
|
|
346
|
+
<span> </span>
|
|
347
|
+
{skill.hasUpdate && (
|
|
348
|
+
<span bg="yellow" fg="black">{updateBadge}</span>
|
|
349
|
+
)}
|
|
350
|
+
<span fg="white">{skill.name.padEnd(30)}</span>
|
|
351
|
+
<span fg="gray">{skill.source.repo}</span>
|
|
352
|
+
</text>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return <text fg="gray">{item.label}</text>;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const renderDetail = () => {
|
|
360
|
+
if (skillsState.skills.status === "loading") {
|
|
361
|
+
return <text fg="gray">Loading skills...</text>;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (skillsState.skills.status === "error") {
|
|
365
|
+
return (
|
|
366
|
+
<box flexDirection="column">
|
|
367
|
+
<text fg="red">Failed to load skills</text>
|
|
368
|
+
<box marginTop={1}>
|
|
369
|
+
<text fg="gray">{skillsState.skills.error.message}</text>
|
|
370
|
+
</box>
|
|
371
|
+
<box marginTop={1}>
|
|
372
|
+
<text fg="gray">Set GITHUB_TOKEN to increase rate limits.</text>
|
|
373
|
+
</box>
|
|
374
|
+
</box>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!selectedItem) {
|
|
379
|
+
return <text fg="gray">Select a skill to see details</text>;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (selectedItem.type === "category") {
|
|
383
|
+
const catKey = selectedItem.categoryKey || "general";
|
|
384
|
+
const color = CATEGORY_COLORS[catKey] ? "green" : "cyan";
|
|
385
|
+
return (
|
|
386
|
+
<box flexDirection="column">
|
|
387
|
+
<text fg={color}>
|
|
388
|
+
<strong>{selectedItem.label}</strong>
|
|
389
|
+
</text>
|
|
390
|
+
<box marginTop={1}>
|
|
391
|
+
<text fg="gray">Skills in this category</text>
|
|
392
|
+
</box>
|
|
393
|
+
</box>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!selectedSkill) return null;
|
|
398
|
+
|
|
399
|
+
const fm = selectedSkill.frontmatter;
|
|
400
|
+
const scopeColor = selectedSkill.installedScope === "user" ? "cyan" : "green";
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<box flexDirection="column">
|
|
404
|
+
<text fg="cyan">
|
|
405
|
+
<strong>{selectedSkill.name}</strong>
|
|
406
|
+
</text>
|
|
407
|
+
|
|
408
|
+
<box marginTop={1}>
|
|
409
|
+
<text fg="white">
|
|
410
|
+
{fm ? fm.description : "Loading..."}
|
|
411
|
+
</text>
|
|
412
|
+
</box>
|
|
413
|
+
|
|
414
|
+
{fm?.category && (
|
|
415
|
+
<box marginTop={1}>
|
|
416
|
+
<text>
|
|
417
|
+
<span fg="gray">Category </span>
|
|
418
|
+
<span fg="cyan">{fm.category}</span>
|
|
419
|
+
</text>
|
|
420
|
+
</box>
|
|
421
|
+
)}
|
|
422
|
+
|
|
423
|
+
{fm?.author && (
|
|
424
|
+
<box>
|
|
425
|
+
<text>
|
|
426
|
+
<span fg="gray">Author </span>
|
|
427
|
+
<span>{fm.author}</span>
|
|
428
|
+
</text>
|
|
429
|
+
</box>
|
|
430
|
+
)}
|
|
431
|
+
|
|
432
|
+
{fm?.version && (
|
|
433
|
+
<box>
|
|
434
|
+
<text>
|
|
435
|
+
<span fg="gray">Version </span>
|
|
436
|
+
<span>{fm.version}</span>
|
|
437
|
+
</text>
|
|
438
|
+
</box>
|
|
439
|
+
)}
|
|
440
|
+
|
|
441
|
+
{fm?.tags && fm.tags.length > 0 && (
|
|
442
|
+
<box>
|
|
443
|
+
<text>
|
|
444
|
+
<span fg="gray">Tags </span>
|
|
445
|
+
<span>{fm.tags.join(", ")}</span>
|
|
446
|
+
</text>
|
|
447
|
+
</box>
|
|
448
|
+
)}
|
|
449
|
+
|
|
450
|
+
<box marginTop={1} flexDirection="column">
|
|
451
|
+
<text>
|
|
452
|
+
<span fg="gray">Source </span>
|
|
453
|
+
<span fg="#5c9aff">{selectedSkill.source.repo}</span>
|
|
454
|
+
</text>
|
|
455
|
+
<text>
|
|
456
|
+
<span fg="gray"> </span>
|
|
457
|
+
<span fg="gray">{selectedSkill.repoPath}</span>
|
|
458
|
+
</text>
|
|
459
|
+
</box>
|
|
460
|
+
|
|
461
|
+
{selectedSkill.installed && selectedSkill.installedScope && (
|
|
462
|
+
<box marginTop={1} flexDirection="column">
|
|
463
|
+
<text>{"─".repeat(24)}</text>
|
|
464
|
+
<text>
|
|
465
|
+
<span fg="gray">Installed </span>
|
|
466
|
+
<span fg={scopeColor}>
|
|
467
|
+
{selectedSkill.installedScope === "user"
|
|
468
|
+
? "~/.claude/skills/"
|
|
469
|
+
: ".claude/skills/"}
|
|
470
|
+
{selectedSkill.name}/
|
|
471
|
+
</span>
|
|
472
|
+
</text>
|
|
473
|
+
</box>
|
|
474
|
+
)}
|
|
475
|
+
|
|
476
|
+
<box marginTop={1} flexDirection="column">
|
|
477
|
+
<text>{"─".repeat(24)}</text>
|
|
478
|
+
<text>
|
|
479
|
+
<strong>Install scope:</strong>
|
|
480
|
+
</text>
|
|
481
|
+
<box marginTop={1} flexDirection="column">
|
|
482
|
+
<text>
|
|
483
|
+
<span bg="cyan" fg="black"> u </span>
|
|
484
|
+
<span fg={selectedSkill.installedScope === "user" ? "cyan" : "gray"}>
|
|
485
|
+
{selectedSkill.installedScope === "user" ? " ● " : " ○ "}
|
|
486
|
+
</span>
|
|
487
|
+
<span fg="cyan">User</span>
|
|
488
|
+
<span fg="gray"> ~/.claude/skills/</span>
|
|
489
|
+
</text>
|
|
490
|
+
<text>
|
|
491
|
+
<span bg="green" fg="black"> p </span>
|
|
492
|
+
<span fg={selectedSkill.installedScope === "project" ? "green" : "gray"}>
|
|
493
|
+
{selectedSkill.installedScope === "project" ? " ● " : " ○ "}
|
|
494
|
+
</span>
|
|
495
|
+
<span fg="green">Project</span>
|
|
496
|
+
<span fg="gray"> .claude/skills/</span>
|
|
497
|
+
</text>
|
|
498
|
+
</box>
|
|
499
|
+
</box>
|
|
500
|
+
|
|
501
|
+
{selectedSkill.hasUpdate && (
|
|
502
|
+
<box marginTop={1}>
|
|
503
|
+
<text bg="yellow" fg="black"> UPDATE AVAILABLE </text>
|
|
504
|
+
</box>
|
|
505
|
+
)}
|
|
506
|
+
|
|
507
|
+
<box marginTop={1} flexDirection="column">
|
|
508
|
+
{!selectedSkill.installed && (
|
|
509
|
+
<text fg="gray">Press u/p to install in scope</text>
|
|
510
|
+
)}
|
|
511
|
+
{selectedSkill.installed && (
|
|
512
|
+
<text fg="gray">Press d to uninstall</text>
|
|
513
|
+
)}
|
|
514
|
+
</box>
|
|
515
|
+
</box>
|
|
516
|
+
);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const skills =
|
|
520
|
+
skillsState.skills.status === "success" ? skillsState.skills.data : [];
|
|
521
|
+
const installedCount = skills.filter((s) => s.installed).length;
|
|
522
|
+
const totalCount = skills.length;
|
|
523
|
+
const updateCount = skills.filter((s) => s.hasUpdate).length;
|
|
524
|
+
|
|
525
|
+
const statusContent = (
|
|
526
|
+
<text>
|
|
527
|
+
<span fg="gray">Skills: </span>
|
|
528
|
+
<span fg="cyan">{installedCount} installed</span>
|
|
529
|
+
<span fg="gray"> │ {totalCount} available</span>
|
|
530
|
+
{updateCount > 0 && (
|
|
531
|
+
<span fg="yellow"> │ {updateCount} updates</span>
|
|
532
|
+
)}
|
|
533
|
+
</text>
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
return (
|
|
537
|
+
<ScreenLayout
|
|
538
|
+
title="claudeup Skills"
|
|
539
|
+
currentScreen="skills"
|
|
540
|
+
statusLine={statusContent}
|
|
541
|
+
search={
|
|
542
|
+
skillsState.searchQuery || isSearchActive
|
|
543
|
+
? {
|
|
544
|
+
isActive: isSearchActive,
|
|
545
|
+
query: skillsState.searchQuery,
|
|
546
|
+
placeholder: "type to search",
|
|
547
|
+
}
|
|
548
|
+
: undefined
|
|
549
|
+
}
|
|
550
|
+
footerHints="↑↓:nav │ u:user scope │ p:project scope │ Enter:install │ d:uninstall │ type to search"
|
|
551
|
+
listPanel={
|
|
552
|
+
skillsState.skills.status !== "success" ? (
|
|
553
|
+
<text fg="gray">
|
|
554
|
+
{skillsState.skills.status === "loading"
|
|
555
|
+
? "Loading skills..."
|
|
556
|
+
: skillsState.skills.status === "error"
|
|
557
|
+
? "Error loading skills"
|
|
558
|
+
: "Press r to load skills"}
|
|
559
|
+
</text>
|
|
560
|
+
) : (
|
|
561
|
+
<ScrollableList
|
|
562
|
+
items={selectableItems}
|
|
563
|
+
selectedIndex={skillsState.selectedIndex}
|
|
564
|
+
renderItem={renderListItem}
|
|
565
|
+
maxHeight={dimensions.listPanelHeight}
|
|
566
|
+
/>
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
detailPanel={renderDetail()}
|
|
570
|
+
/>
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export default SkillsScreen;
|
package/src/ui/screens/index.js
CHANGED
|
@@ -5,3 +5,4 @@ export { SettingsScreen } from "./EnvVarsScreen.js";
|
|
|
5
5
|
export { CliToolsScreen } from "./CliToolsScreen.js";
|
|
6
6
|
export { ModelSelectorScreen } from "./ModelSelectorScreen.js";
|
|
7
7
|
export { ProfilesScreen } from "./ProfilesScreen.js";
|
|
8
|
+
export { SkillsScreen } from "./SkillsScreen.js";
|
package/src/ui/screens/index.ts
CHANGED
|
@@ -5,3 +5,4 @@ export { SettingsScreen } from "./EnvVarsScreen.js";
|
|
|
5
5
|
export { CliToolsScreen } from "./CliToolsScreen.js";
|
|
6
6
|
export { ModelSelectorScreen } from "./ModelSelectorScreen.js";
|
|
7
7
|
export { ProfilesScreen } from "./ProfilesScreen.js";
|
|
8
|
+
export { SkillsScreen } from "./SkillsScreen.js";
|
package/src/ui/state/reducer.js
CHANGED
|
@@ -51,6 +51,13 @@ export const initialState = {
|
|
|
51
51
|
selectedIndex: 0,
|
|
52
52
|
profiles: { status: "idle" },
|
|
53
53
|
},
|
|
54
|
+
skills: {
|
|
55
|
+
scope: "user",
|
|
56
|
+
selectedIndex: 0,
|
|
57
|
+
searchQuery: "",
|
|
58
|
+
skills: { status: "idle" },
|
|
59
|
+
updateStatus: null,
|
|
60
|
+
},
|
|
54
61
|
};
|
|
55
62
|
export function appReducer(state, action) {
|
|
56
63
|
switch (action.type) {
|
|
@@ -424,6 +431,87 @@ export function appReducer(state, action) {
|
|
|
424
431
|
},
|
|
425
432
|
};
|
|
426
433
|
// =========================================================================
|
|
434
|
+
// Skills Screen
|
|
435
|
+
// =========================================================================
|
|
436
|
+
case "SKILLS_SET_SCOPE":
|
|
437
|
+
return {
|
|
438
|
+
...state,
|
|
439
|
+
skills: {
|
|
440
|
+
...state.skills,
|
|
441
|
+
scope: action.scope,
|
|
442
|
+
selectedIndex: 0,
|
|
443
|
+
skills: { status: "loading" },
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
case "SKILLS_TOGGLE_SCOPE":
|
|
447
|
+
return appReducer(state, {
|
|
448
|
+
type: "SKILLS_SET_SCOPE",
|
|
449
|
+
scope: state.skills.scope === "user" ? "project" : "user",
|
|
450
|
+
});
|
|
451
|
+
case "SKILLS_SELECT":
|
|
452
|
+
return {
|
|
453
|
+
...state,
|
|
454
|
+
skills: {
|
|
455
|
+
...state.skills,
|
|
456
|
+
selectedIndex: action.index,
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
case "SKILLS_SET_SEARCH":
|
|
460
|
+
return {
|
|
461
|
+
...state,
|
|
462
|
+
skills: {
|
|
463
|
+
...state.skills,
|
|
464
|
+
searchQuery: action.query,
|
|
465
|
+
selectedIndex: 0,
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
case "SKILLS_DATA_LOADING":
|
|
469
|
+
return {
|
|
470
|
+
...state,
|
|
471
|
+
skills: {
|
|
472
|
+
...state.skills,
|
|
473
|
+
skills: { status: "loading" },
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
case "SKILLS_DATA_SUCCESS":
|
|
477
|
+
return {
|
|
478
|
+
...state,
|
|
479
|
+
skills: {
|
|
480
|
+
...state.skills,
|
|
481
|
+
skills: { status: "success", data: action.skills },
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
case "SKILLS_DATA_ERROR":
|
|
485
|
+
return {
|
|
486
|
+
...state,
|
|
487
|
+
skills: {
|
|
488
|
+
...state.skills,
|
|
489
|
+
skills: { status: "error", error: action.error },
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
case "SKILLS_UPDATE_STATUS":
|
|
493
|
+
return {
|
|
494
|
+
...state,
|
|
495
|
+
skills: {
|
|
496
|
+
...state.skills,
|
|
497
|
+
updateStatus: action.updates,
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
case "SKILLS_UPDATE_ITEM": {
|
|
501
|
+
if (state.skills.skills.status !== "success")
|
|
502
|
+
return state;
|
|
503
|
+
return {
|
|
504
|
+
...state,
|
|
505
|
+
skills: {
|
|
506
|
+
...state.skills,
|
|
507
|
+
skills: {
|
|
508
|
+
status: "success",
|
|
509
|
+
data: state.skills.skills.data.map((s) => s.name === action.name ? { ...s, ...action.updates } : s),
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
// =========================================================================
|
|
427
515
|
// Modals
|
|
428
516
|
// =========================================================================
|
|
429
517
|
case "SHOW_MODAL":
|