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.
@@ -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
- const CATEGORY_COLORS = {
11
- recommended: "#2e7d32",
12
- frontend: "#1565c0",
13
- design: "#6a1b9a",
14
- media: "#e65100",
15
- security: "#b71c1c",
16
- debugging: "#00838f",
17
- database: "#4527a0",
18
- search: "#4e342e",
19
- general: "#333333",
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
- // Build flat list: recommended first (as their own category), then by repo
48
- const allItems = useMemo(() => {
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 skills = skillsState.skills.data;
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
- // Recommended section
60
- const recommendedSkills = filtered.filter((s) => RECOMMENDED_NAMES.has(s.name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())) || RECOMMENDED_SKILLS.some((r) => r.skillPath === s.name));
61
- if (recommendedSkills.length > 0) {
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: "cat:recommended",
64
- type: "category",
65
- label: "Recommended",
66
- categoryKey: "recommended",
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
- // Group remaining skills by repo
78
- const repoMap = new Map();
79
- for (const skill of filtered) {
80
- const isRec = recommendedSkills.includes(skill);
81
- if (isRec)
82
- continue;
83
- const existing = repoMap.get(skill.source.repo) || [];
84
- existing.push(skill);
85
- repoMap.set(skill.source.repo, existing);
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
- for (const [repo, repoSkills] of repoMap) {
88
- items.push({
89
- id: `cat:${repo}`,
90
- type: "category",
91
- label: `${repo} (${repoSkills.length})`,
92
- categoryKey: repo,
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: `skill:${skill.id}`,
97
- type: "skill",
98
- label: skill.name,
99
- skill,
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 (state.isSearching)
178
- return;
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
- else if (event.name === "down" || event.name === "j") {
183
- if (state.isSearching)
184
- return;
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
- else if (event.name === "u") {
189
- if (state.isSearching)
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
- else if (event.name === "d") {
220
- if (state.isSearching)
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
- else if (event.name === "r") {
227
- if (state.isSearching)
228
- return;
229
- fetchData();
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 === "escape") {
232
- if (skillsState.searchQuery) {
233
- dispatch({ type: "SKILLS_SET_SEARCH", query: "" });
234
- dispatch({ type: "SET_SEARCHING", isSearching: false });
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 === "backspace") {
238
- if (skillsState.searchQuery) {
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
- event.name.length === 1 &&
248
- !/[0-9]/.test(event.name) &&
249
- !event.ctrl &&
250
- !event.meta) {
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 catKey = item.categoryKey || "general";
260
- const isRec = catKey === "recommended";
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 scopeLabel = skill.installedScope
273
- ? skill.installedScope === "user"
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, " ", scopeLabel, " ", updateBadge, skill.name.padEnd(30), skill.source.repo, " "] }));
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: skill.installedScope === "user" ? "cyan" : skill.installedScope === "project" ? "green" : "gray", children: scopeLabel }), _jsx("span", { children: " " }), skill.hasUpdate && (_jsx("span", { bg: "yellow", fg: "black", children: updateBadge })), _jsx("span", { fg: "white", children: skill.name.padEnd(30) }), _jsx("span", { fg: "gray", children: skill.source.repo })] }));
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 }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Set GITHUB_TOKEN to increase rate limits." }) })] }));
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 catKey = selectedItem.categoryKey || "general";
297
- const color = CATEGORY_COLORS[catKey] ? "green" : "cyan";
298
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: color, children: _jsx("strong", { children: selectedItem.label }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Skills in this category" }) })] }));
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
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsx("strong", { children: selectedSkill.name }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "white", children: fm ? fm.description : "Loading..." }) }), 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"
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 totalCount = skills.length;
311
- const updateCount = skills.filter((s) => s.hasUpdate).length;
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: "\u2191\u2193:nav \u2502 u:user scope \u2502 p:project scope \u2502 Enter:install \u2502 d:uninstall \u2502 type to search", listPanel: skillsState.skills.status !== "success" ? (_jsx("text", { fg: "gray", children: skillsState.skills.status === "loading"
320
- ? "Loading skills..."
321
- : skillsState.skills.status === "error"
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;