claudeup 3.9.0 → 3.11.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 +32 -47
- package/src/ui/screens/PluginsScreen.tsx +39 -59
- package/src/ui/screens/SkillsScreen.js +248 -143
- package/src/ui/screens/SkillsScreen.tsx +316 -163
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import React, { useEffect, useCallback, useMemo } from "react";
|
|
1
|
+
import React, { useEffect, useCallback, useMemo, useState, useRef } from "react";
|
|
2
2
|
import { useApp, useModal } from "../state/AppContext.js";
|
|
3
3
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
4
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
5
5
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
6
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
7
|
+
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
7
8
|
import {
|
|
8
9
|
fetchAvailableSkills,
|
|
9
10
|
fetchSkillFrontmatter,
|
|
10
11
|
installSkill,
|
|
11
12
|
uninstallSkill,
|
|
12
13
|
} from "../../services/skills-manager.js";
|
|
14
|
+
import { searchSkills } from "../../services/skillsmp-client.js";
|
|
13
15
|
import { DEFAULT_SKILL_REPOS, RECOMMENDED_SKILLS } from "../../data/skill-repos.js";
|
|
14
|
-
import type { SkillInfo } from "../../types/index.js";
|
|
16
|
+
import type { SkillInfo, SkillSource } from "../../types/index.js";
|
|
15
17
|
|
|
16
18
|
interface SkillListItem {
|
|
17
19
|
id: string;
|
|
@@ -21,19 +23,13 @@ interface SkillListItem {
|
|
|
21
23
|
categoryKey?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
database: "#4527a0",
|
|
32
|
-
search: "#4e342e",
|
|
33
|
-
general: "#333333",
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const RECOMMENDED_NAMES = new Set(RECOMMENDED_SKILLS.map((r) => r.name));
|
|
26
|
+
function formatStars(stars?: number): string {
|
|
27
|
+
if (!stars) return "";
|
|
28
|
+
if (stars >= 1000000) return `★ ${(stars / 1000000).toFixed(1)}M`;
|
|
29
|
+
if (stars >= 10000) return `★ ${Math.round(stars / 1000)}K`;
|
|
30
|
+
if (stars >= 1000) return `★ ${(stars / 1000).toFixed(1)}K`;
|
|
31
|
+
return `★ ${stars}`;
|
|
32
|
+
}
|
|
37
33
|
|
|
38
34
|
export function SkillsScreen() {
|
|
39
35
|
const { state, dispatch } = useApp();
|
|
@@ -67,77 +63,184 @@ export function SkillsScreen() {
|
|
|
67
63
|
fetchData();
|
|
68
64
|
}, [fetchData, state.dataRefreshVersion]);
|
|
69
65
|
|
|
70
|
-
//
|
|
71
|
-
const
|
|
72
|
-
|
|
66
|
+
// Remote search: query Firebase API when user types (debounced, cached)
|
|
67
|
+
const [searchResults, setSearchResults] = useState<SkillInfo[]>([]);
|
|
68
|
+
const [isSearchLoading, setIsSearchLoading] = useState(false);
|
|
69
|
+
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
70
|
+
const searchCacheRef = useRef<Map<string, SkillInfo[]>>(new Map());
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const query = skillsState.searchQuery.trim();
|
|
74
|
+
if (query.length < 2) {
|
|
75
|
+
setSearchResults([]);
|
|
76
|
+
setIsSearchLoading(false);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
73
79
|
|
|
74
|
-
|
|
80
|
+
// Check cache first
|
|
81
|
+
const cached = searchCacheRef.current.get(query);
|
|
82
|
+
if (cached) {
|
|
83
|
+
setSearchResults(cached);
|
|
84
|
+
setIsSearchLoading(false);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setIsSearchLoading(true);
|
|
89
|
+
|
|
90
|
+
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
|
|
91
|
+
searchTimerRef.current = setTimeout(async () => {
|
|
92
|
+
try {
|
|
93
|
+
const results = await searchSkills(query, { limit: 30 });
|
|
94
|
+
const mapped: SkillInfo[] = results.map((r) => {
|
|
95
|
+
const source: SkillSource = {
|
|
96
|
+
label: r.repo || "unknown",
|
|
97
|
+
repo: r.repo || "unknown",
|
|
98
|
+
skillsPath: "",
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
id: `remote:${r.repo}/${r.skillPath}`,
|
|
102
|
+
name: r.name,
|
|
103
|
+
description: r.description || "",
|
|
104
|
+
source,
|
|
105
|
+
repoPath: r.skillPath ? `${r.skillPath}/SKILL.md` : "SKILL.md",
|
|
106
|
+
gitBlobSha: "",
|
|
107
|
+
frontmatter: null,
|
|
108
|
+
installed: false,
|
|
109
|
+
installedScope: null,
|
|
110
|
+
hasUpdate: false,
|
|
111
|
+
stars: r.stars,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
searchCacheRef.current.set(query, mapped);
|
|
115
|
+
setSearchResults(mapped);
|
|
116
|
+
} catch {
|
|
117
|
+
setSearchResults([]);
|
|
118
|
+
}
|
|
119
|
+
setIsSearchLoading(false);
|
|
120
|
+
}, 400);
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
|
|
124
|
+
};
|
|
125
|
+
}, [skillsState.searchQuery]);
|
|
126
|
+
|
|
127
|
+
// Static recommended skills — always available, no API needed
|
|
128
|
+
const staticRecommended = useMemo((): SkillInfo[] => {
|
|
129
|
+
return RECOMMENDED_SKILLS.map((r) => ({
|
|
130
|
+
id: `rec:${r.repo}/${r.skillPath}`,
|
|
131
|
+
name: r.name,
|
|
132
|
+
description: r.description,
|
|
133
|
+
source: { label: r.repo, repo: r.repo, skillsPath: "" },
|
|
134
|
+
repoPath: `${r.skillPath}/SKILL.md`,
|
|
135
|
+
gitBlobSha: "",
|
|
136
|
+
frontmatter: null,
|
|
137
|
+
installed: false,
|
|
138
|
+
installedScope: null,
|
|
139
|
+
hasUpdate: false,
|
|
140
|
+
isRecommended: true,
|
|
141
|
+
stars: undefined,
|
|
142
|
+
}));
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
// Merge static recommended with fetched data (to get install status + stars)
|
|
146
|
+
const mergedRecommended = useMemo((): SkillInfo[] => {
|
|
147
|
+
if (skillsState.skills.status !== "success") return staticRecommended;
|
|
148
|
+
const fetched = skillsState.skills.data.filter((s) => s.isRecommended);
|
|
149
|
+
// Merge: keep fetched data (has stars + install status), fall back to static
|
|
150
|
+
return staticRecommended.map((staticSkill) => {
|
|
151
|
+
const match = fetched.find(
|
|
152
|
+
(f) => f.source.repo === staticSkill.source.repo && f.name === staticSkill.name,
|
|
153
|
+
);
|
|
154
|
+
return match || staticSkill;
|
|
155
|
+
});
|
|
156
|
+
}, [staticRecommended, skillsState.skills]);
|
|
157
|
+
|
|
158
|
+
// Build list: recommended always shown, then search results or popular
|
|
159
|
+
const allItems = useMemo((): SkillListItem[] => {
|
|
75
160
|
const query = skillsState.searchQuery.toLowerCase();
|
|
161
|
+
const items: SkillListItem[] = [];
|
|
76
162
|
|
|
77
|
-
|
|
78
|
-
|
|
163
|
+
// ── RECOMMENDED: always shown, filtered when searching ──
|
|
164
|
+
const filteredRec = query
|
|
165
|
+
? mergedRecommended.filter(
|
|
79
166
|
(s) =>
|
|
80
167
|
s.name.toLowerCase().includes(query) ||
|
|
81
|
-
s.
|
|
82
|
-
s.frontmatter?.description?.toLowerCase().includes(query),
|
|
168
|
+
(s.description || "").toLowerCase().includes(query),
|
|
83
169
|
)
|
|
84
|
-
:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
if (recommendedSkills.length > 0) {
|
|
170
|
+
: mergedRecommended;
|
|
171
|
+
|
|
172
|
+
items.push({
|
|
173
|
+
id: "cat:recommended",
|
|
174
|
+
type: "category",
|
|
175
|
+
label: "Recommended",
|
|
176
|
+
categoryKey: "recommended",
|
|
177
|
+
});
|
|
178
|
+
for (const skill of filteredRec) {
|
|
96
179
|
items.push({
|
|
97
|
-
id:
|
|
98
|
-
type: "
|
|
99
|
-
label:
|
|
100
|
-
|
|
180
|
+
id: `skill:${skill.id}`,
|
|
181
|
+
type: "skill",
|
|
182
|
+
label: skill.name,
|
|
183
|
+
skill,
|
|
101
184
|
});
|
|
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
185
|
}
|
|
111
186
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
187
|
+
// ── SEARCH MODE ──
|
|
188
|
+
if (query.length >= 2) {
|
|
189
|
+
// Loading and no-results handled in listPanel, not as list items
|
|
190
|
+
|
|
191
|
+
if (!isSearchLoading && searchResults.length > 0) {
|
|
192
|
+
// Dedup against recommended
|
|
193
|
+
const recNames = new Set(mergedRecommended.map((s) => s.name));
|
|
194
|
+
const deduped = searchResults.filter((s) => !recNames.has(s.name));
|
|
195
|
+
if (deduped.length > 0) {
|
|
196
|
+
items.push({
|
|
197
|
+
id: "cat:search",
|
|
198
|
+
type: "category",
|
|
199
|
+
label: `Search (${deduped.length})`,
|
|
200
|
+
categoryKey: "popular",
|
|
201
|
+
});
|
|
202
|
+
for (const skill of deduped) {
|
|
203
|
+
items.push({
|
|
204
|
+
id: `skill:${skill.id}`,
|
|
205
|
+
type: "skill",
|
|
206
|
+
label: skill.name,
|
|
207
|
+
skill,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// No-results message handled in listPanel below, not as a list item
|
|
213
|
+
return items;
|
|
120
214
|
}
|
|
121
215
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
216
|
+
// ── POPULAR (default, no search query) ──
|
|
217
|
+
// Loading state handled in listPanel, not as category header
|
|
218
|
+
|
|
219
|
+
if (skillsState.skills.status === "success") {
|
|
220
|
+
const popularSkills = skillsState.skills.data
|
|
221
|
+
.filter((s) => !s.isRecommended)
|
|
222
|
+
.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
|
|
223
|
+
|
|
224
|
+
if (popularSkills.length > 0) {
|
|
130
225
|
items.push({
|
|
131
|
-
id:
|
|
132
|
-
type: "
|
|
133
|
-
label:
|
|
134
|
-
|
|
226
|
+
id: "cat:popular",
|
|
227
|
+
type: "category",
|
|
228
|
+
label: "Popular",
|
|
229
|
+
categoryKey: "popular",
|
|
135
230
|
});
|
|
231
|
+
for (const skill of popularSkills) {
|
|
232
|
+
items.push({
|
|
233
|
+
id: `skill:${skill.id}`,
|
|
234
|
+
type: "skill",
|
|
235
|
+
label: skill.name,
|
|
236
|
+
skill,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
136
239
|
}
|
|
137
240
|
}
|
|
138
241
|
|
|
139
242
|
return items;
|
|
140
|
-
}, [skillsState.skills, skillsState.searchQuery]);
|
|
243
|
+
}, [skillsState.skills, skillsState.searchQuery, searchResults, isSearchLoading, mergedRecommended]);
|
|
141
244
|
|
|
142
245
|
const selectableItems = useMemo(
|
|
143
246
|
() => allItems.filter((item) => item.type === "skill" || item.type === "category"),
|
|
@@ -219,75 +322,91 @@ export function SkillsScreen() {
|
|
|
219
322
|
}
|
|
220
323
|
}, [selectedSkill, state.projectPath, dispatch, modal]);
|
|
221
324
|
|
|
222
|
-
// Keyboard handling
|
|
325
|
+
// Keyboard handling — same pattern as PluginsScreen
|
|
223
326
|
useKeyboard((event) => {
|
|
224
327
|
if (state.modal) return;
|
|
225
328
|
|
|
329
|
+
const hasQuery = skillsState.searchQuery.length > 0;
|
|
330
|
+
|
|
331
|
+
// Escape: clear search
|
|
332
|
+
if (event.name === "escape") {
|
|
333
|
+
if (hasQuery || isSearchActive) {
|
|
334
|
+
dispatch({ type: "SKILLS_SET_SEARCH", query: "" });
|
|
335
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
336
|
+
dispatch({ type: "SKILLS_SELECT", index: 0 });
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Backspace: remove last char
|
|
342
|
+
if (event.name === "backspace" || event.name === "delete") {
|
|
343
|
+
if (hasQuery) {
|
|
344
|
+
const newQuery = skillsState.searchQuery.slice(0, -1);
|
|
345
|
+
dispatch({ type: "SKILLS_SET_SEARCH", query: newQuery });
|
|
346
|
+
dispatch({ type: "SKILLS_SELECT", index: 0 });
|
|
347
|
+
if (!newQuery) dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Navigation — always works; exits search mode on navigate
|
|
226
353
|
if (event.name === "up" || event.name === "k") {
|
|
227
|
-
if (
|
|
354
|
+
if (isSearchActive) dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
228
355
|
const newIndex = Math.max(0, skillsState.selectedIndex - 1);
|
|
229
356
|
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
230
|
-
|
|
231
|
-
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (event.name === "down" || event.name === "j") {
|
|
360
|
+
if (isSearchActive) dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
232
361
|
const newIndex = Math.min(
|
|
233
362
|
Math.max(0, selectableItems.length - 1),
|
|
234
363
|
skillsState.selectedIndex + 1,
|
|
235
364
|
);
|
|
236
365
|
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Enter — install (always works)
|
|
370
|
+
if (event.name === "return" || event.name === "enter") {
|
|
371
|
+
if (isSearchActive) {
|
|
372
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
373
|
+
return;
|
|
254
374
|
}
|
|
255
|
-
} else if (event.name === "return" || event.name === "enter") {
|
|
256
|
-
if (state.isSearching) return;
|
|
257
375
|
if (selectedSkill && !selectedSkill.installed) {
|
|
258
376
|
handleInstall("project");
|
|
259
377
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// When actively typing in search, letters go to the query
|
|
382
|
+
if (isSearchActive) {
|
|
383
|
+
if (event.name === "k" || event.name === "j") {
|
|
384
|
+
const delta = event.name === "k" ? -1 : 1;
|
|
385
|
+
const newIndex = Math.max(0, Math.min(selectableItems.length - 1, skillsState.selectedIndex + delta));
|
|
386
|
+
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
387
|
+
return;
|
|
264
388
|
}
|
|
389
|
+
if (event.name && event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
|
|
390
|
+
dispatch({ type: "SKILLS_SET_SEARCH", query: skillsState.searchQuery + event.name });
|
|
391
|
+
dispatch({ type: "SKILLS_SELECT", index: 0 });
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Action shortcuts (work when not actively typing, even with filter visible)
|
|
397
|
+
if (event.name === "u" && selectedSkill) {
|
|
398
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "user") handleUninstall();
|
|
399
|
+
else handleInstall("user");
|
|
400
|
+
} else if (event.name === "p" && selectedSkill) {
|
|
401
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "project") handleUninstall();
|
|
402
|
+
else handleInstall("project");
|
|
403
|
+
} else if (event.name === "d" && selectedSkill?.installed) {
|
|
404
|
+
handleUninstall();
|
|
265
405
|
} else if (event.name === "r") {
|
|
266
|
-
if (state.isSearching) return;
|
|
267
406
|
fetchData();
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
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 });
|
|
407
|
+
}
|
|
408
|
+
// "/" to enter search mode
|
|
409
|
+
else if (event.name === "/") {
|
|
291
410
|
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
292
411
|
}
|
|
293
412
|
});
|
|
@@ -298,9 +417,8 @@ export function SkillsScreen() {
|
|
|
298
417
|
isSelected: boolean,
|
|
299
418
|
) => {
|
|
300
419
|
if (item.type === "category") {
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
const bgColor = CATEGORY_COLORS[catKey] || CATEGORY_COLORS.general;
|
|
420
|
+
const isRec = item.categoryKey === "recommended";
|
|
421
|
+
const bgColor = isRec ? "green" : "cyan";
|
|
304
422
|
const star = isRec ? "★ " : "";
|
|
305
423
|
|
|
306
424
|
if (isSelected) {
|
|
@@ -321,18 +439,13 @@ export function SkillsScreen() {
|
|
|
321
439
|
const skill = item.skill;
|
|
322
440
|
const indicator = skill.installed ? "●" : "○";
|
|
323
441
|
const indicatorColor = skill.installed ? "cyan" : "gray";
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
? "[u]"
|
|
327
|
-
: "[p]"
|
|
328
|
-
: " ";
|
|
329
|
-
const updateBadge = skill.hasUpdate ? "[UPDT] " : "";
|
|
442
|
+
const scopeTag = skill.installedScope === "user" ? "u" : skill.installedScope === "project" ? "p" : "";
|
|
443
|
+
const starsStr = formatStars(skill.stars);
|
|
330
444
|
|
|
331
445
|
if (isSelected) {
|
|
332
446
|
return (
|
|
333
447
|
<text bg="magenta" fg="white">
|
|
334
|
-
{" "}
|
|
335
|
-
{indicator} {scopeLabel} {updateBadge}{skill.name.padEnd(30)}{skill.source.repo}{" "}
|
|
448
|
+
{" "}{indicator} {skill.name}{skill.hasUpdate ? " ⬆" : ""}{scopeTag ? ` [${scopeTag}]` : ""}{starsStr ? ` ${starsStr}` : ""}{" "}
|
|
336
449
|
</text>
|
|
337
450
|
);
|
|
338
451
|
}
|
|
@@ -340,15 +453,14 @@ export function SkillsScreen() {
|
|
|
340
453
|
return (
|
|
341
454
|
<text>
|
|
342
455
|
<span fg={indicatorColor}> {indicator} </span>
|
|
343
|
-
<span fg=
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
456
|
+
<span fg="white">{skill.name}</span>
|
|
457
|
+
{skill.hasUpdate && <span fg="yellow"> ⬆</span>}
|
|
458
|
+
{scopeTag && (
|
|
459
|
+
<span fg={scopeTag === "u" ? "cyan" : "green"}> [{scopeTag}]</span>
|
|
460
|
+
)}
|
|
461
|
+
{starsStr && (
|
|
462
|
+
<span fg="yellow">{" "}{starsStr}</span>
|
|
349
463
|
)}
|
|
350
|
-
<span fg="white">{skill.name.padEnd(30)}</span>
|
|
351
|
-
<span fg="gray">{skill.source.repo}</span>
|
|
352
464
|
</text>
|
|
353
465
|
);
|
|
354
466
|
}
|
|
@@ -368,9 +480,6 @@ export function SkillsScreen() {
|
|
|
368
480
|
<box marginTop={1}>
|
|
369
481
|
<text fg="gray">{skillsState.skills.error.message}</text>
|
|
370
482
|
</box>
|
|
371
|
-
<box marginTop={1}>
|
|
372
|
-
<text fg="gray">Set GITHUB_TOKEN to increase rate limits.</text>
|
|
373
|
-
</box>
|
|
374
483
|
</box>
|
|
375
484
|
);
|
|
376
485
|
}
|
|
@@ -380,15 +489,44 @@ export function SkillsScreen() {
|
|
|
380
489
|
}
|
|
381
490
|
|
|
382
491
|
if (selectedItem.type === "category") {
|
|
383
|
-
const
|
|
384
|
-
const
|
|
492
|
+
const isRec = selectedItem.categoryKey === "recommended";
|
|
493
|
+
const isNoResults = selectedItem.categoryKey === "no-results";
|
|
494
|
+
|
|
495
|
+
if (isNoResults) {
|
|
496
|
+
return (
|
|
497
|
+
<box flexDirection="column">
|
|
498
|
+
<text fg="yellow">
|
|
499
|
+
<strong>No skills found</strong>
|
|
500
|
+
</text>
|
|
501
|
+
<box marginTop={1}>
|
|
502
|
+
<text fg="gray">
|
|
503
|
+
Nothing matched "{skillsState.searchQuery}".
|
|
504
|
+
</text>
|
|
505
|
+
</box>
|
|
506
|
+
<box marginTop={1}>
|
|
507
|
+
<text fg="gray">
|
|
508
|
+
Try a different search term, or if you think this is a mistake, create an issue at:
|
|
509
|
+
</text>
|
|
510
|
+
</box>
|
|
511
|
+
<box marginTop={1}>
|
|
512
|
+
<text fg="#5c9aff">github.com/MadAppGang/magus/issues</text>
|
|
513
|
+
</box>
|
|
514
|
+
<box marginTop={2}>
|
|
515
|
+
<text fg="gray">Press Esc to clear the search.</text>
|
|
516
|
+
</box>
|
|
517
|
+
</box>
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
385
521
|
return (
|
|
386
522
|
<box flexDirection="column">
|
|
387
|
-
<text fg={
|
|
388
|
-
<strong>{selectedItem.label}</strong>
|
|
523
|
+
<text fg={isRec ? "green" : "cyan"}>
|
|
524
|
+
<strong>{isRec ? "★ " : ""}{selectedItem.label}</strong>
|
|
389
525
|
</text>
|
|
390
526
|
<box marginTop={1}>
|
|
391
|
-
<text fg="gray">
|
|
527
|
+
<text fg="gray">
|
|
528
|
+
{isRec ? "Curated skills recommended for most projects" : "Popular skills sorted by stars"}
|
|
529
|
+
</text>
|
|
392
530
|
</box>
|
|
393
531
|
</box>
|
|
394
532
|
);
|
|
@@ -397,17 +535,20 @@ export function SkillsScreen() {
|
|
|
397
535
|
if (!selectedSkill) return null;
|
|
398
536
|
|
|
399
537
|
const fm = selectedSkill.frontmatter;
|
|
538
|
+
const description = fm?.description || selectedSkill.description || "Loading...";
|
|
400
539
|
const scopeColor = selectedSkill.installedScope === "user" ? "cyan" : "green";
|
|
540
|
+
const starsStr = formatStars(selectedSkill.stars);
|
|
401
541
|
|
|
402
542
|
return (
|
|
403
543
|
<box flexDirection="column">
|
|
404
544
|
<text fg="cyan">
|
|
405
545
|
<strong>{selectedSkill.name}</strong>
|
|
546
|
+
{starsStr && <span fg="yellow"> {starsStr}</span>}
|
|
406
547
|
</text>
|
|
407
548
|
|
|
408
549
|
<box marginTop={1}>
|
|
409
550
|
<text fg="white">
|
|
410
|
-
{
|
|
551
|
+
{description}
|
|
411
552
|
</text>
|
|
412
553
|
</box>
|
|
413
554
|
|
|
@@ -519,16 +660,20 @@ export function SkillsScreen() {
|
|
|
519
660
|
const skills =
|
|
520
661
|
skillsState.skills.status === "success" ? skillsState.skills.data : [];
|
|
521
662
|
const installedCount = skills.filter((s) => s.installed).length;
|
|
522
|
-
const
|
|
523
|
-
const updateCount = skills.filter((s) => s.hasUpdate).length;
|
|
663
|
+
const query = skillsState.searchQuery.trim();
|
|
524
664
|
|
|
525
665
|
const statusContent = (
|
|
526
666
|
<text>
|
|
527
667
|
<span fg="gray">Skills: </span>
|
|
528
668
|
<span fg="cyan">{installedCount} installed</span>
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
669
|
+
{query.length >= 2 && isSearchLoading && (
|
|
670
|
+
<span fg="yellow"> │ searching...</span>
|
|
671
|
+
)}
|
|
672
|
+
{query.length >= 2 && !isSearchLoading && searchResults.length > 0 && (
|
|
673
|
+
<span fg="green"> │ {searchResults.length} found</span>
|
|
674
|
+
)}
|
|
675
|
+
{!query && (
|
|
676
|
+
<span fg="gray"> │ 89K+ searchable</span>
|
|
532
677
|
)}
|
|
533
678
|
</text>
|
|
534
679
|
);
|
|
@@ -547,24 +692,32 @@ export function SkillsScreen() {
|
|
|
547
692
|
}
|
|
548
693
|
: undefined
|
|
549
694
|
}
|
|
550
|
-
footerHints=
|
|
695
|
+
footerHints={isSearchActive
|
|
696
|
+
? "type to filter │ Enter:done │ Esc:clear"
|
|
697
|
+
: "u:user │ p:project │ d:uninstall │ /:search"
|
|
698
|
+
}
|
|
551
699
|
listPanel={
|
|
552
|
-
|
|
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
|
-
) : (
|
|
700
|
+
<box flexDirection="column">
|
|
561
701
|
<ScrollableList
|
|
562
702
|
items={selectableItems}
|
|
563
703
|
selectedIndex={skillsState.selectedIndex}
|
|
564
704
|
renderItem={renderListItem}
|
|
565
705
|
maxHeight={dimensions.listPanelHeight}
|
|
566
706
|
/>
|
|
567
|
-
|
|
707
|
+
{!query && skillsState.skills.status === "loading" && (
|
|
708
|
+
<box marginTop={2} paddingLeft={2}>
|
|
709
|
+
<text fg="yellow">Loading popular skills...</text>
|
|
710
|
+
</box>
|
|
711
|
+
)}
|
|
712
|
+
{query.length >= 2 && isSearchLoading && (
|
|
713
|
+
<box marginTop={2} paddingLeft={2}>
|
|
714
|
+
<text fg="yellow">Searching for "{skillsState.searchQuery}"...</text>
|
|
715
|
+
</box>
|
|
716
|
+
)}
|
|
717
|
+
{query.length >= 2 && !isSearchLoading && searchResults.length === 0 && (
|
|
718
|
+
<EmptyFilterState query={skillsState.searchQuery} entityName="skills" />
|
|
719
|
+
)}
|
|
720
|
+
</box>
|
|
568
721
|
}
|
|
569
722
|
detailPanel={renderDetail()}
|
|
570
723
|
/>
|