claudeup 3.9.0 → 3.10.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 +15 -15
- package/src/data/skill-repos.ts +15 -15
- package/src/services/skills-manager.js +124 -56
- package/src/services/skills-manager.ts +131 -58
- package/src/types/index.ts +4 -0
- package/src/ui/App.js +9 -9
- package/src/ui/App.tsx +9 -9
- package/src/ui/components/EmptyFilterState.js +4 -0
- package/src/ui/components/EmptyFilterState.tsx +27 -0
- package/src/ui/components/TabBar.js +4 -4
- package/src/ui/components/TabBar.tsx +4 -4
- package/src/ui/screens/PluginsScreen.js +17 -35
- package/src/ui/screens/PluginsScreen.tsx +25 -43
- package/src/ui/screens/SkillsScreen.js +248 -143
- package/src/ui/screens/SkillsScreen.tsx +316 -163
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useEffect, useCallback, useMemo } from "react";
|
|
2
|
+
import { useEffect, useCallback, useMemo, useState, useRef } from "react";
|
|
3
3
|
import { useApp, useModal } from "../state/AppContext.js";
|
|
4
4
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
5
5
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
6
6
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
7
7
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
8
|
+
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
8
9
|
import { fetchAvailableSkills, fetchSkillFrontmatter, installSkill, uninstallSkill, } from "../../services/skills-manager.js";
|
|
10
|
+
import { searchSkills } from "../../services/skillsmp-client.js";
|
|
9
11
|
import { DEFAULT_SKILL_REPOS, RECOMMENDED_SKILLS } from "../../data/skill-repos.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
const RECOMMENDED_NAMES = new Set(RECOMMENDED_SKILLS.map((r) => r.name));
|
|
12
|
+
function formatStars(stars) {
|
|
13
|
+
if (!stars)
|
|
14
|
+
return "";
|
|
15
|
+
if (stars >= 1000000)
|
|
16
|
+
return `★ ${(stars / 1000000).toFixed(1)}M`;
|
|
17
|
+
if (stars >= 10000)
|
|
18
|
+
return `★ ${Math.round(stars / 1000)}K`;
|
|
19
|
+
if (stars >= 1000)
|
|
20
|
+
return `★ ${(stars / 1000).toFixed(1)}K`;
|
|
21
|
+
return `★ ${stars}`;
|
|
22
|
+
}
|
|
22
23
|
export function SkillsScreen() {
|
|
23
24
|
const { state, dispatch } = useApp();
|
|
24
25
|
const { skills: skillsState } = state;
|
|
@@ -44,64 +45,167 @@ export function SkillsScreen() {
|
|
|
44
45
|
useEffect(() => {
|
|
45
46
|
fetchData();
|
|
46
47
|
}, [fetchData, state.dataRefreshVersion]);
|
|
47
|
-
//
|
|
48
|
-
const
|
|
48
|
+
// Remote search: query Firebase API when user types (debounced, cached)
|
|
49
|
+
const [searchResults, setSearchResults] = useState([]);
|
|
50
|
+
const [isSearchLoading, setIsSearchLoading] = useState(false);
|
|
51
|
+
const searchTimerRef = useRef(null);
|
|
52
|
+
const searchCacheRef = useRef(new Map());
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const query = skillsState.searchQuery.trim();
|
|
55
|
+
if (query.length < 2) {
|
|
56
|
+
setSearchResults([]);
|
|
57
|
+
setIsSearchLoading(false);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Check cache first
|
|
61
|
+
const cached = searchCacheRef.current.get(query);
|
|
62
|
+
if (cached) {
|
|
63
|
+
setSearchResults(cached);
|
|
64
|
+
setIsSearchLoading(false);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
setIsSearchLoading(true);
|
|
68
|
+
if (searchTimerRef.current)
|
|
69
|
+
clearTimeout(searchTimerRef.current);
|
|
70
|
+
searchTimerRef.current = setTimeout(async () => {
|
|
71
|
+
try {
|
|
72
|
+
const results = await searchSkills(query, { limit: 30 });
|
|
73
|
+
const mapped = results.map((r) => {
|
|
74
|
+
const source = {
|
|
75
|
+
label: r.repo || "unknown",
|
|
76
|
+
repo: r.repo || "unknown",
|
|
77
|
+
skillsPath: "",
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
id: `remote:${r.repo}/${r.skillPath}`,
|
|
81
|
+
name: r.name,
|
|
82
|
+
description: r.description || "",
|
|
83
|
+
source,
|
|
84
|
+
repoPath: r.skillPath ? `${r.skillPath}/SKILL.md` : "SKILL.md",
|
|
85
|
+
gitBlobSha: "",
|
|
86
|
+
frontmatter: null,
|
|
87
|
+
installed: false,
|
|
88
|
+
installedScope: null,
|
|
89
|
+
hasUpdate: false,
|
|
90
|
+
stars: r.stars,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
searchCacheRef.current.set(query, mapped);
|
|
94
|
+
setSearchResults(mapped);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
setSearchResults([]);
|
|
98
|
+
}
|
|
99
|
+
setIsSearchLoading(false);
|
|
100
|
+
}, 400);
|
|
101
|
+
return () => {
|
|
102
|
+
if (searchTimerRef.current)
|
|
103
|
+
clearTimeout(searchTimerRef.current);
|
|
104
|
+
};
|
|
105
|
+
}, [skillsState.searchQuery]);
|
|
106
|
+
// Static recommended skills — always available, no API needed
|
|
107
|
+
const staticRecommended = useMemo(() => {
|
|
108
|
+
return RECOMMENDED_SKILLS.map((r) => ({
|
|
109
|
+
id: `rec:${r.repo}/${r.skillPath}`,
|
|
110
|
+
name: r.name,
|
|
111
|
+
description: r.description,
|
|
112
|
+
source: { label: r.repo, repo: r.repo, skillsPath: "" },
|
|
113
|
+
repoPath: `${r.skillPath}/SKILL.md`,
|
|
114
|
+
gitBlobSha: "",
|
|
115
|
+
frontmatter: null,
|
|
116
|
+
installed: false,
|
|
117
|
+
installedScope: null,
|
|
118
|
+
hasUpdate: false,
|
|
119
|
+
isRecommended: true,
|
|
120
|
+
stars: undefined,
|
|
121
|
+
}));
|
|
122
|
+
}, []);
|
|
123
|
+
// Merge static recommended with fetched data (to get install status + stars)
|
|
124
|
+
const mergedRecommended = useMemo(() => {
|
|
49
125
|
if (skillsState.skills.status !== "success")
|
|
50
|
-
return
|
|
51
|
-
const
|
|
126
|
+
return staticRecommended;
|
|
127
|
+
const fetched = skillsState.skills.data.filter((s) => s.isRecommended);
|
|
128
|
+
// Merge: keep fetched data (has stars + install status), fall back to static
|
|
129
|
+
return staticRecommended.map((staticSkill) => {
|
|
130
|
+
const match = fetched.find((f) => f.source.repo === staticSkill.source.repo && f.name === staticSkill.name);
|
|
131
|
+
return match || staticSkill;
|
|
132
|
+
});
|
|
133
|
+
}, [staticRecommended, skillsState.skills]);
|
|
134
|
+
// Build list: recommended always shown, then search results or popular
|
|
135
|
+
const allItems = useMemo(() => {
|
|
52
136
|
const query = skillsState.searchQuery.toLowerCase();
|
|
53
|
-
const filtered = query
|
|
54
|
-
? skills.filter((s) => s.name.toLowerCase().includes(query) ||
|
|
55
|
-
s.source.repo.toLowerCase().includes(query) ||
|
|
56
|
-
s.frontmatter?.description?.toLowerCase().includes(query))
|
|
57
|
-
: skills;
|
|
58
137
|
const items = [];
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
|
|
138
|
+
// ── RECOMMENDED: always shown, filtered when searching ──
|
|
139
|
+
const filteredRec = query
|
|
140
|
+
? mergedRecommended.filter((s) => s.name.toLowerCase().includes(query) ||
|
|
141
|
+
(s.description || "").toLowerCase().includes(query))
|
|
142
|
+
: mergedRecommended;
|
|
143
|
+
items.push({
|
|
144
|
+
id: "cat:recommended",
|
|
145
|
+
type: "category",
|
|
146
|
+
label: "Recommended",
|
|
147
|
+
categoryKey: "recommended",
|
|
148
|
+
});
|
|
149
|
+
for (const skill of filteredRec) {
|
|
62
150
|
items.push({
|
|
63
|
-
id:
|
|
64
|
-
type: "
|
|
65
|
-
label:
|
|
66
|
-
|
|
151
|
+
id: `skill:${skill.id}`,
|
|
152
|
+
type: "skill",
|
|
153
|
+
label: skill.name,
|
|
154
|
+
skill,
|
|
67
155
|
});
|
|
68
|
-
for (const skill of recommendedSkills) {
|
|
69
|
-
items.push({
|
|
70
|
-
id: `skill:${skill.id}`,
|
|
71
|
-
type: "skill",
|
|
72
|
-
label: skill.name,
|
|
73
|
-
skill,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
156
|
}
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
157
|
+
// ── SEARCH MODE ──
|
|
158
|
+
if (query.length >= 2) {
|
|
159
|
+
// Loading and no-results handled in listPanel, not as list items
|
|
160
|
+
if (!isSearchLoading && searchResults.length > 0) {
|
|
161
|
+
// Dedup against recommended
|
|
162
|
+
const recNames = new Set(mergedRecommended.map((s) => s.name));
|
|
163
|
+
const deduped = searchResults.filter((s) => !recNames.has(s.name));
|
|
164
|
+
if (deduped.length > 0) {
|
|
165
|
+
items.push({
|
|
166
|
+
id: "cat:search",
|
|
167
|
+
type: "category",
|
|
168
|
+
label: `Search (${deduped.length})`,
|
|
169
|
+
categoryKey: "popular",
|
|
170
|
+
});
|
|
171
|
+
for (const skill of deduped) {
|
|
172
|
+
items.push({
|
|
173
|
+
id: `skill:${skill.id}`,
|
|
174
|
+
type: "skill",
|
|
175
|
+
label: skill.name,
|
|
176
|
+
skill,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// No-results message handled in listPanel below, not as a list item
|
|
182
|
+
return items;
|
|
86
183
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
for (const skill of repoSkills) {
|
|
184
|
+
// ── POPULAR (default, no search query) ──
|
|
185
|
+
// Loading state handled in listPanel, not as category header
|
|
186
|
+
if (skillsState.skills.status === "success") {
|
|
187
|
+
const popularSkills = skillsState.skills.data
|
|
188
|
+
.filter((s) => !s.isRecommended)
|
|
189
|
+
.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
|
|
190
|
+
if (popularSkills.length > 0) {
|
|
95
191
|
items.push({
|
|
96
|
-
id:
|
|
97
|
-
type: "
|
|
98
|
-
label:
|
|
99
|
-
|
|
192
|
+
id: "cat:popular",
|
|
193
|
+
type: "category",
|
|
194
|
+
label: "Popular",
|
|
195
|
+
categoryKey: "popular",
|
|
100
196
|
});
|
|
197
|
+
for (const skill of popularSkills) {
|
|
198
|
+
items.push({
|
|
199
|
+
id: `skill:${skill.id}`,
|
|
200
|
+
type: "skill",
|
|
201
|
+
label: skill.name,
|
|
202
|
+
skill,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
101
205
|
}
|
|
102
206
|
}
|
|
103
207
|
return items;
|
|
104
|
-
}, [skillsState.skills, skillsState.searchQuery]);
|
|
208
|
+
}, [skillsState.skills, skillsState.searchQuery, searchResults, isSearchLoading, mergedRecommended]);
|
|
105
209
|
const selectableItems = useMemo(() => allItems.filter((item) => item.type === "skill" || item.type === "category"), [allItems]);
|
|
106
210
|
const selectedItem = selectableItems[skillsState.selectedIndex];
|
|
107
211
|
const selectedSkill = selectedItem?.type === "skill" ? selectedItem.skill : undefined;
|
|
@@ -169,96 +273,99 @@ export function SkillsScreen() {
|
|
|
169
273
|
await modal.message("Error", `Failed to uninstall: ${error}`, "error");
|
|
170
274
|
}
|
|
171
275
|
}, [selectedSkill, state.projectPath, dispatch, modal]);
|
|
172
|
-
// Keyboard handling
|
|
276
|
+
// Keyboard handling — same pattern as PluginsScreen
|
|
173
277
|
useKeyboard((event) => {
|
|
174
278
|
if (state.modal)
|
|
175
279
|
return;
|
|
280
|
+
const hasQuery = skillsState.searchQuery.length > 0;
|
|
281
|
+
// Escape: clear search
|
|
282
|
+
if (event.name === "escape") {
|
|
283
|
+
if (hasQuery || isSearchActive) {
|
|
284
|
+
dispatch({ type: "SKILLS_SET_SEARCH", query: "" });
|
|
285
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
286
|
+
dispatch({ type: "SKILLS_SELECT", index: 0 });
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// Backspace: remove last char
|
|
291
|
+
if (event.name === "backspace" || event.name === "delete") {
|
|
292
|
+
if (hasQuery) {
|
|
293
|
+
const newQuery = skillsState.searchQuery.slice(0, -1);
|
|
294
|
+
dispatch({ type: "SKILLS_SET_SEARCH", query: newQuery });
|
|
295
|
+
dispatch({ type: "SKILLS_SELECT", index: 0 });
|
|
296
|
+
if (!newQuery)
|
|
297
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
// Navigation — always works; exits search mode on navigate
|
|
176
302
|
if (event.name === "up" || event.name === "k") {
|
|
177
|
-
if (
|
|
178
|
-
|
|
303
|
+
if (isSearchActive)
|
|
304
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
179
305
|
const newIndex = Math.max(0, skillsState.selectedIndex - 1);
|
|
180
306
|
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
307
|
+
return;
|
|
181
308
|
}
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
309
|
+
if (event.name === "down" || event.name === "j") {
|
|
310
|
+
if (isSearchActive)
|
|
311
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
185
312
|
const newIndex = Math.min(Math.max(0, selectableItems.length - 1), skillsState.selectedIndex + 1);
|
|
186
313
|
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
314
|
+
return;
|
|
187
315
|
}
|
|
188
|
-
|
|
189
|
-
|
|
316
|
+
// Enter — install (always works)
|
|
317
|
+
if (event.name === "return" || event.name === "enter") {
|
|
318
|
+
if (isSearchActive) {
|
|
319
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
190
320
|
return;
|
|
191
|
-
if (selectedSkill) {
|
|
192
|
-
if (selectedSkill.installed && selectedSkill.installedScope === "user") {
|
|
193
|
-
handleUninstall();
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
handleInstall("user");
|
|
197
|
-
}
|
|
198
321
|
}
|
|
199
|
-
}
|
|
200
|
-
else if (event.name === "p") {
|
|
201
|
-
if (state.isSearching)
|
|
202
|
-
return;
|
|
203
|
-
if (selectedSkill) {
|
|
204
|
-
if (selectedSkill.installed && selectedSkill.installedScope === "project") {
|
|
205
|
-
handleUninstall();
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
handleInstall("project");
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
else if (event.name === "return" || event.name === "enter") {
|
|
213
|
-
if (state.isSearching)
|
|
214
|
-
return;
|
|
215
322
|
if (selectedSkill && !selectedSkill.installed) {
|
|
216
323
|
handleInstall("project");
|
|
217
324
|
}
|
|
325
|
+
return;
|
|
218
326
|
}
|
|
219
|
-
|
|
220
|
-
|
|
327
|
+
// When actively typing in search, letters go to the query
|
|
328
|
+
if (isSearchActive) {
|
|
329
|
+
if (event.name === "k" || event.name === "j") {
|
|
330
|
+
const delta = event.name === "k" ? -1 : 1;
|
|
331
|
+
const newIndex = Math.max(0, Math.min(selectableItems.length - 1, skillsState.selectedIndex + delta));
|
|
332
|
+
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
221
333
|
return;
|
|
222
|
-
if (selectedSkill?.installed) {
|
|
223
|
-
handleUninstall();
|
|
224
334
|
}
|
|
335
|
+
if (event.name && event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
|
|
336
|
+
dispatch({ type: "SKILLS_SET_SEARCH", query: skillsState.searchQuery + event.name });
|
|
337
|
+
dispatch({ type: "SKILLS_SELECT", index: 0 });
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
225
340
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
341
|
+
// Action shortcuts (work when not actively typing, even with filter visible)
|
|
342
|
+
if (event.name === "u" && selectedSkill) {
|
|
343
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "user")
|
|
344
|
+
handleUninstall();
|
|
345
|
+
else
|
|
346
|
+
handleInstall("user");
|
|
230
347
|
}
|
|
231
|
-
else if (event.name === "
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
348
|
+
else if (event.name === "p" && selectedSkill) {
|
|
349
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "project")
|
|
350
|
+
handleUninstall();
|
|
351
|
+
else
|
|
352
|
+
handleInstall("project");
|
|
236
353
|
}
|
|
237
|
-
else if (event.name === "
|
|
238
|
-
|
|
239
|
-
const newQuery = skillsState.searchQuery.slice(0, -1);
|
|
240
|
-
dispatch({ type: "SKILLS_SET_SEARCH", query: newQuery });
|
|
241
|
-
if (!newQuery) {
|
|
242
|
-
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
243
|
-
}
|
|
244
|
-
}
|
|
354
|
+
else if (event.name === "d" && selectedSkill?.installed) {
|
|
355
|
+
handleUninstall();
|
|
245
356
|
}
|
|
246
|
-
else if (event.name
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
// Inline search: type to filter
|
|
252
|
-
const newQuery = skillsState.searchQuery + event.name;
|
|
253
|
-
dispatch({ type: "SKILLS_SET_SEARCH", query: newQuery });
|
|
357
|
+
else if (event.name === "r") {
|
|
358
|
+
fetchData();
|
|
359
|
+
}
|
|
360
|
+
// "/" to enter search mode
|
|
361
|
+
else if (event.name === "/") {
|
|
254
362
|
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
255
363
|
}
|
|
256
364
|
});
|
|
257
365
|
const renderListItem = (item, _idx, isSelected) => {
|
|
258
366
|
if (item.type === "category") {
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
const bgColor = CATEGORY_COLORS[catKey] || CATEGORY_COLORS.general;
|
|
367
|
+
const isRec = item.categoryKey === "recommended";
|
|
368
|
+
const bgColor = isRec ? "green" : "cyan";
|
|
262
369
|
const star = isRec ? "★ " : "";
|
|
263
370
|
if (isSelected) {
|
|
264
371
|
return (_jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", star, item.label, " "] }) }));
|
|
@@ -269,16 +376,12 @@ export function SkillsScreen() {
|
|
|
269
376
|
const skill = item.skill;
|
|
270
377
|
const indicator = skill.installed ? "●" : "○";
|
|
271
378
|
const indicatorColor = skill.installed ? "cyan" : "gray";
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
? "[u]"
|
|
275
|
-
: "[p]"
|
|
276
|
-
: " ";
|
|
277
|
-
const updateBadge = skill.hasUpdate ? "[UPDT] " : "";
|
|
379
|
+
const scopeTag = skill.installedScope === "user" ? "u" : skill.installedScope === "project" ? "p" : "";
|
|
380
|
+
const starsStr = formatStars(skill.stars);
|
|
278
381
|
if (isSelected) {
|
|
279
|
-
return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", indicator, " ",
|
|
382
|
+
return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", indicator, " ", skill.name, skill.hasUpdate ? " ⬆" : "", scopeTag ? ` [${scopeTag}]` : "", starsStr ? ` ${starsStr}` : "", " "] }));
|
|
280
383
|
}
|
|
281
|
-
return (_jsxs("text", { children: [_jsxs("span", { fg: indicatorColor, children: [" ", indicator, " "] }), _jsx("span", { fg:
|
|
384
|
+
return (_jsxs("text", { children: [_jsxs("span", { fg: indicatorColor, children: [" ", indicator, " "] }), _jsx("span", { fg: "white", children: skill.name }), skill.hasUpdate && _jsx("span", { fg: "yellow", children: " \u2B06" }), scopeTag && (_jsxs("span", { fg: scopeTag === "u" ? "cyan" : "green", children: [" [", scopeTag, "]"] })), starsStr && (_jsxs("span", { fg: "yellow", children: [" ", starsStr] }))] }));
|
|
282
385
|
}
|
|
283
386
|
return _jsx("text", { fg: "gray", children: item.label });
|
|
284
387
|
};
|
|
@@ -287,39 +390,41 @@ export function SkillsScreen() {
|
|
|
287
390
|
return _jsx("text", { fg: "gray", children: "Loading skills..." });
|
|
288
391
|
}
|
|
289
392
|
if (skillsState.skills.status === "error") {
|
|
290
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "red", children: "Failed to load skills" }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: skillsState.skills.error.message }) })
|
|
393
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "red", children: "Failed to load skills" }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: skillsState.skills.error.message }) })] }));
|
|
291
394
|
}
|
|
292
395
|
if (!selectedItem) {
|
|
293
396
|
return _jsx("text", { fg: "gray", children: "Select a skill to see details" });
|
|
294
397
|
}
|
|
295
398
|
if (selectedItem.type === "category") {
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
|
|
399
|
+
const isRec = selectedItem.categoryKey === "recommended";
|
|
400
|
+
const isNoResults = selectedItem.categoryKey === "no-results";
|
|
401
|
+
if (isNoResults) {
|
|
402
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "yellow", children: _jsx("strong", { children: "No skills found" }) }), _jsx("box", { marginTop: 1, children: _jsxs("text", { fg: "gray", children: ["Nothing matched \"", skillsState.searchQuery, "\"."] }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Try a different search term, or if you think this is a mistake, create an issue at:" }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#5c9aff", children: "github.com/MadAppGang/magus/issues" }) }), _jsx("box", { marginTop: 2, children: _jsx("text", { fg: "gray", children: "Press Esc to clear the search." }) })] }));
|
|
403
|
+
}
|
|
404
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: isRec ? "green" : "cyan", children: _jsxs("strong", { children: [isRec ? "★ " : "", selectedItem.label] }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: isRec ? "Curated skills recommended for most projects" : "Popular skills sorted by stars" }) })] }));
|
|
299
405
|
}
|
|
300
406
|
if (!selectedSkill)
|
|
301
407
|
return null;
|
|
302
408
|
const fm = selectedSkill.frontmatter;
|
|
409
|
+
const description = fm?.description || selectedSkill.description || "Loading...";
|
|
303
410
|
const scopeColor = selectedSkill.installedScope === "user" ? "cyan" : "green";
|
|
304
|
-
|
|
411
|
+
const starsStr = formatStars(selectedSkill.stars);
|
|
412
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { fg: "cyan", children: [_jsx("strong", { children: selectedSkill.name }), starsStr && _jsxs("span", { fg: "yellow", children: [" ", starsStr] })] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "white", children: description }) }), fm?.category && (_jsx("box", { marginTop: 1, children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Category " }), _jsx("span", { fg: "cyan", children: fm.category })] }) })), fm?.author && (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Author " }), _jsx("span", { children: fm.author })] }) })), fm?.version && (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Version " }), _jsx("span", { children: fm.version })] }) })), fm?.tags && fm.tags.length > 0 && (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Tags " }), _jsx("span", { children: fm.tags.join(", ") })] }) })), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Source " }), _jsx("span", { fg: "#5c9aff", children: selectedSkill.source.repo })] }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: " " }), _jsx("span", { fg: "gray", children: selectedSkill.repoPath })] })] }), selectedSkill.installed && selectedSkill.installedScope && (_jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { children: "─".repeat(24) }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Installed " }), _jsxs("span", { fg: scopeColor, children: [selectedSkill.installedScope === "user"
|
|
305
413
|
? "~/.claude/skills/"
|
|
306
414
|
: ".claude/skills/", selectedSkill.name, "/"] })] })] })), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { children: "─".repeat(24) }), _jsx("text", { children: _jsx("strong", { children: "Install scope:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { bg: "cyan", fg: "black", children: " u " }), _jsx("span", { fg: selectedSkill.installedScope === "user" ? "cyan" : "gray", children: selectedSkill.installedScope === "user" ? " ● " : " ○ " }), _jsx("span", { fg: "cyan", children: "User" }), _jsx("span", { fg: "gray", children: " ~/.claude/skills/" })] }), _jsxs("text", { children: [_jsx("span", { bg: "green", fg: "black", children: " p " }), _jsx("span", { fg: selectedSkill.installedScope === "project" ? "green" : "gray", children: selectedSkill.installedScope === "project" ? " ● " : " ○ " }), _jsx("span", { fg: "green", children: "Project" }), _jsx("span", { fg: "gray", children: " .claude/skills/" })] })] })] }), selectedSkill.hasUpdate && (_jsx("box", { marginTop: 1, children: _jsx("text", { bg: "yellow", fg: "black", children: " UPDATE AVAILABLE " }) })), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [!selectedSkill.installed && (_jsx("text", { fg: "gray", children: "Press u/p to install in scope" })), selectedSkill.installed && (_jsx("text", { fg: "gray", children: "Press d to uninstall" }))] })] }));
|
|
307
415
|
};
|
|
308
416
|
const skills = skillsState.skills.status === "success" ? skillsState.skills.data : [];
|
|
309
417
|
const installedCount = skills.filter((s) => s.installed).length;
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Skills: " }), _jsxs("span", { fg: "cyan", children: [installedCount, " installed"] }), _jsxs("span", { fg: "gray", children: [" \u2502 ", totalCount, " available"] }), updateCount > 0 && (_jsxs("span", { fg: "yellow", children: [" \u2502 ", updateCount, " updates"] }))] }));
|
|
418
|
+
const query = skillsState.searchQuery.trim();
|
|
419
|
+
const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Skills: " }), _jsxs("span", { fg: "cyan", children: [installedCount, " installed"] }), query.length >= 2 && isSearchLoading && (_jsx("span", { fg: "yellow", children: " \u2502 searching..." })), query.length >= 2 && !isSearchLoading && searchResults.length > 0 && (_jsxs("span", { fg: "green", children: [" \u2502 ", searchResults.length, " found"] })), !query && (_jsx("span", { fg: "gray", children: " \u2502 89K+ searchable" }))] }));
|
|
313
420
|
return (_jsx(ScreenLayout, { title: "claudeup Skills", currentScreen: "skills", statusLine: statusContent, search: skillsState.searchQuery || isSearchActive
|
|
314
421
|
? {
|
|
315
422
|
isActive: isSearchActive,
|
|
316
423
|
query: skillsState.searchQuery,
|
|
317
424
|
placeholder: "type to search",
|
|
318
425
|
}
|
|
319
|
-
: undefined, footerHints:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
? "Error loading skills"
|
|
323
|
-
: "Press r to load skills" })) : (_jsx(ScrollableList, { items: selectableItems, selectedIndex: skillsState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight })), detailPanel: renderDetail() }));
|
|
426
|
+
: undefined, footerHints: isSearchActive
|
|
427
|
+
? "type to filter │ Enter:done │ Esc:clear"
|
|
428
|
+
: "u:user │ p:project │ d:uninstall │ /:search", listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items: selectableItems, selectedIndex: skillsState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), !query && skillsState.skills.status === "loading" && (_jsx("box", { marginTop: 2, paddingLeft: 2, children: _jsx("text", { fg: "yellow", children: "Loading popular skills..." }) })), query.length >= 2 && isSearchLoading && (_jsx("box", { marginTop: 2, paddingLeft: 2, children: _jsxs("text", { fg: "yellow", children: ["Searching for \"", skillsState.searchQuery, "\"..."] }) })), query.length >= 2 && !isSearchLoading && searchResults.length === 0 && (_jsx(EmptyFilterState, { query: skillsState.searchQuery, entityName: "skills" }))] }), detailPanel: renderDetail() }));
|
|
324
429
|
}
|
|
325
430
|
export default SkillsScreen;
|