claudeup 3.14.0 → 3.16.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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/data/skill-repos.js +11 -0
  3. package/src/data/skill-repos.ts +12 -0
  4. package/src/services/skills-manager.js +40 -13
  5. package/src/services/skills-manager.ts +38 -16
  6. package/src/ui/adapters/skillsAdapter.js +106 -0
  7. package/src/ui/adapters/skillsAdapter.ts +160 -0
  8. package/src/ui/components/primitives/ActionHints.js +13 -0
  9. package/src/ui/components/primitives/ActionHints.tsx +41 -0
  10. package/src/ui/components/primitives/DetailSection.js +7 -0
  11. package/src/ui/components/primitives/DetailSection.tsx +22 -0
  12. package/src/ui/components/primitives/KeyValueLine.js +8 -0
  13. package/src/ui/components/primitives/KeyValueLine.tsx +19 -0
  14. package/src/ui/components/primitives/ListCategoryRow.js +8 -0
  15. package/src/ui/components/primitives/ListCategoryRow.tsx +38 -0
  16. package/src/ui/components/primitives/MetaText.js +8 -0
  17. package/src/ui/components/primitives/MetaText.tsx +14 -0
  18. package/src/ui/components/primitives/ScopeDetail.js +32 -0
  19. package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
  20. package/src/ui/components/primitives/ScopeSquares.js +11 -0
  21. package/src/ui/components/primitives/ScopeSquares.tsx +33 -0
  22. package/src/ui/components/primitives/SelectableRow.js +5 -0
  23. package/src/ui/components/primitives/SelectableRow.tsx +24 -0
  24. package/src/ui/components/primitives/index.js +8 -0
  25. package/src/ui/components/primitives/index.ts +9 -0
  26. package/src/ui/registry.js +1 -0
  27. package/src/ui/registry.ts +27 -0
  28. package/src/ui/renderers/skillRenderers.js +75 -0
  29. package/src/ui/renderers/skillRenderers.tsx +220 -0
  30. package/src/ui/screens/SkillsScreen.js +46 -195
  31. package/src/ui/screens/SkillsScreen.tsx +436 -796
  32. package/src/ui/theme.js +47 -0
  33. package/src/ui/theme.ts +53 -0
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
2
  import { useEffect, useCallback, useMemo, useState, useRef } from "react";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -12,17 +12,8 @@ import { EmptyFilterState } from "../components/EmptyFilterState.js";
12
12
  import { fetchAvailableSkills, fetchSkillFrontmatter, installSkill, uninstallSkill, } from "../../services/skills-manager.js";
13
13
  import { searchSkills } from "../../services/skillsmp-client.js";
14
14
  import { DEFAULT_SKILL_REPOS, RECOMMENDED_SKILLS } from "../../data/skill-repos.js";
15
- function formatStars(stars) {
16
- if (!stars)
17
- return "";
18
- if (stars >= 1000000)
19
- return `★ ${(stars / 1000000).toFixed(1)}M`;
20
- if (stars >= 10000)
21
- return `★ ${Math.round(stars / 1000)}K`;
22
- if (stars >= 1000)
23
- return `★ ${(stars / 1000).toFixed(1)}K`;
24
- return `★ ${stars}`;
25
- }
15
+ import { buildSkillBrowserItems } from "../adapters/skillsAdapter.js";
16
+ import { renderSkillRow, renderSkillDetail } from "../renderers/skillRenderers.js";
26
17
  export function SkillsScreen() {
27
18
  const { state, dispatch } = useApp();
28
19
  const { skills: skillsState } = state;
@@ -31,7 +22,7 @@ export function SkillsScreen() {
31
22
  const isSearchActive = state.isSearching &&
32
23
  state.currentRoute.screen === "skills" &&
33
24
  !state.modal;
34
- // Fetch data
25
+ // ── Data fetching ─────────────────────────────────────────────────────────
35
26
  const fetchData = useCallback(async () => {
36
27
  dispatch({ type: "SKILLS_DATA_LOADING" });
37
28
  try {
@@ -48,7 +39,7 @@ export function SkillsScreen() {
48
39
  useEffect(() => {
49
40
  fetchData();
50
41
  }, [fetchData, state.dataRefreshVersion]);
51
- // Remote search: query Firebase API when user types (debounced, cached)
42
+ // ── Remote search (debounced, cached) ─────────────────────────────────────
52
43
  const [searchResults, setSearchResults] = useState([]);
53
44
  const [isSearchLoading, setIsSearchLoading] = useState(false);
54
45
  const searchTimerRef = useRef(null);
@@ -60,7 +51,6 @@ export function SkillsScreen() {
60
51
  setIsSearchLoading(false);
61
52
  return;
62
53
  }
63
- // Check cache first
64
54
  const cached = searchCacheRef.current.get(query);
65
55
  if (cached) {
66
56
  setSearchResults(cached);
@@ -106,7 +96,7 @@ export function SkillsScreen() {
106
96
  clearTimeout(searchTimerRef.current);
107
97
  };
108
98
  }, [skillsState.searchQuery]);
109
- // Scan installed skills from disk (instant, no API)
99
+ // ── Disk scan for installed skills ────────────────────────────────────────
110
100
  const [installedFromDisk, setInstalledFromDisk] = useState({ user: new Set(), project: new Set() });
111
101
  useEffect(() => {
112
102
  async function scanDisk() {
@@ -130,8 +120,7 @@ export function SkillsScreen() {
130
120
  }
131
121
  scanDisk();
132
122
  }, [state.projectPath, state.dataRefreshVersion]);
133
- // Static recommended skills — always available, no API needed
134
- // Enriched with disk-based install status immediately
123
+ // ── Derived data ──────────────────────────────────────────────────────────
135
124
  const staticRecommended = useMemo(() => {
136
125
  return RECOMMENDED_SKILLS.map((r) => {
137
126
  const slug = r.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -149,22 +138,22 @@ export function SkillsScreen() {
149
138
  installedScope: isProj ? "project" : isUser ? "user" : null,
150
139
  hasUpdate: false,
151
140
  isRecommended: true,
152
- stars: undefined,
141
+ stars: r.stars,
153
142
  };
154
143
  });
155
144
  }, [installedFromDisk]);
156
- // Merge static recommended with fetched data (to get install status + stars)
157
145
  const mergedRecommended = useMemo(() => {
158
146
  if (skillsState.skills.status !== "success")
159
147
  return staticRecommended;
160
148
  const fetched = skillsState.skills.data.filter((s) => s.isRecommended);
161
- // Merge: keep fetched data (has stars + install status), fall back to static
162
149
  return staticRecommended.map((staticSkill) => {
163
150
  const match = fetched.find((f) => f.source.repo === staticSkill.source.repo && f.name === staticSkill.name);
164
- return match || staticSkill;
151
+ if (!match)
152
+ return staticSkill;
153
+ // Merge: prefer fetched data but keep static stars as fallback
154
+ return { ...staticSkill, ...match, stars: match.stars || staticSkill.stars };
165
155
  });
166
156
  }, [staticRecommended, skillsState.skills]);
167
- // Build installed skills list from disk (always available)
168
157
  const installedSkills = useMemo(() => {
169
158
  const all = [];
170
159
  for (const [scope, names] of [
@@ -172,7 +161,6 @@ export function SkillsScreen() {
172
161
  ["project", installedFromDisk.project],
173
162
  ]) {
174
163
  for (const name of names) {
175
- // Skip if already in the list (avoid dupes)
176
164
  if (all.some((s) => s.name === name))
177
165
  continue;
178
166
  all.push({
@@ -192,105 +180,32 @@ export function SkillsScreen() {
192
180
  }
193
181
  return all;
194
182
  }, [installedFromDisk]);
195
- // Build list: installed first, then recommended, then search/popular
196
- const allItems = useMemo(() => {
197
- const query = skillsState.searchQuery.toLowerCase();
198
- const items = [];
199
- // ── INSTALLED: always shown at top (if any) ──
200
- const installedFiltered = query
201
- ? installedSkills.filter((s) => s.name.toLowerCase().includes(query))
202
- : installedSkills;
203
- if (installedFiltered.length > 0) {
204
- items.push({
205
- id: "cat:installed",
206
- type: "category",
207
- label: `Installed (${installedFiltered.length})`,
208
- categoryKey: "installed",
209
- });
210
- for (const skill of installedFiltered) {
211
- items.push({
212
- id: `skill:${skill.id}`,
213
- type: "skill",
214
- label: skill.name,
215
- skill,
216
- });
217
- }
218
- }
219
- // ── RECOMMENDED: always shown, filtered when searching ──
220
- const filteredRec = query
221
- ? mergedRecommended.filter((s) => s.name.toLowerCase().includes(query) ||
222
- (s.description || "").toLowerCase().includes(query))
223
- : mergedRecommended;
224
- items.push({
225
- id: "cat:recommended",
226
- type: "category",
227
- label: "Recommended",
228
- categoryKey: "recommended",
229
- });
230
- for (const skill of filteredRec) {
231
- items.push({
232
- id: `skill:${skill.id}`,
233
- type: "skill",
234
- label: skill.name,
235
- skill,
236
- });
237
- }
238
- // ── SEARCH MODE ──
239
- if (query.length >= 2) {
240
- // Loading and no-results handled in listPanel, not as list items
241
- if (!isSearchLoading && searchResults.length > 0) {
242
- // Dedup against recommended
243
- const recNames = new Set(mergedRecommended.map((s) => s.name));
244
- const deduped = searchResults.filter((s) => !recNames.has(s.name));
245
- if (deduped.length > 0) {
246
- items.push({
247
- id: "cat:search",
248
- type: "category",
249
- label: `Search (${deduped.length})`,
250
- categoryKey: "popular",
251
- });
252
- for (const skill of deduped) {
253
- items.push({
254
- id: `skill:${skill.id}`,
255
- type: "skill",
256
- label: skill.name,
257
- skill,
258
- });
259
- }
260
- }
261
- }
262
- // No-results message handled in listPanel below, not as a list item
263
- return items;
264
- }
265
- // ── POPULAR (default, no search query) ──
266
- // Loading state handled in listPanel, not as category header
267
- if (skillsState.skills.status === "success") {
268
- const popularSkills = skillsState.skills.data
269
- .filter((s) => !s.isRecommended)
270
- .sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
271
- if (popularSkills.length > 0) {
272
- items.push({
273
- id: "cat:popular",
274
- type: "category",
275
- label: "Popular",
276
- categoryKey: "popular",
277
- });
278
- for (const skill of popularSkills) {
279
- items.push({
280
- id: `skill:${skill.id}`,
281
- type: "skill",
282
- label: skill.name,
283
- skill,
284
- });
285
- }
286
- }
287
- }
288
- return items;
289
- }, [skillsState.skills, skillsState.searchQuery, searchResults, isSearchLoading, mergedRecommended]);
290
- const selectableItems = useMemo(() => allItems.filter((item) => item.type === "skill" || item.type === "category"), [allItems]);
291
- const selectedItem = selectableItems[skillsState.selectedIndex];
292
- const selectedSkill = selectedItem?.type === "skill" ? selectedItem.skill : undefined;
293
- // Lazy-load frontmatter for selected skill
183
+ const popularSkills = useMemo(() => {
184
+ if (skillsState.skills.status !== "success")
185
+ return [];
186
+ return skillsState.skills.data
187
+ .filter((s) => !s.isRecommended)
188
+ .sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
189
+ }, [skillsState.skills]);
190
+ // ── List items (built by adapter) ─────────────────────────────────────────
191
+ const allItems = useMemo(() => buildSkillBrowserItems({
192
+ recommended: mergedRecommended,
193
+ popular: popularSkills,
194
+ installed: installedSkills,
195
+ searchResults,
196
+ query: skillsState.searchQuery,
197
+ isSearchLoading,
198
+ }), [
199
+ mergedRecommended,
200
+ popularSkills,
201
+ installedSkills,
202
+ searchResults,
203
+ skillsState.searchQuery,
204
+ isSearchLoading,
205
+ ]);
206
+ const selectedItem = allItems[skillsState.selectedIndex];
207
+ const selectedSkill = selectedItem?.kind === "skill" ? selectedItem.skill : undefined;
208
+ // ── Lazy-load frontmatter for selected skill ───────────────────────────────
294
209
  useEffect(() => {
295
210
  if (!selectedSkill || selectedSkill.frontmatter)
296
211
  return;
@@ -302,7 +217,7 @@ export function SkillsScreen() {
302
217
  });
303
218
  }).catch(() => { });
304
219
  }, [selectedSkill?.id, dispatch]);
305
- // Install handler
220
+ // ── Action handlers ───────────────────────────────────────────────────────
306
221
  const handleInstall = useCallback(async (scope) => {
307
222
  if (!selectedSkill)
308
223
  return;
@@ -310,7 +225,6 @@ export function SkillsScreen() {
310
225
  try {
311
226
  await installSkill(selectedSkill, scope, state.projectPath);
312
227
  modal.hideModal();
313
- // Refetch to pick up install status for all skills including recommended
314
228
  await fetchData();
315
229
  await modal.message("Installed", `${selectedSkill.name} installed to ${scope === "user" ? "~/.claude/skills/" : ".claude/skills/"}`, "success");
316
230
  }
@@ -319,7 +233,6 @@ export function SkillsScreen() {
319
233
  await modal.message("Error", `Failed to install: ${error}`, "error");
320
234
  }
321
235
  }, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
322
- // Uninstall handler
323
236
  const handleUninstall = useCallback(async () => {
324
237
  if (!selectedSkill || !selectedSkill.installed)
325
238
  return;
@@ -333,7 +246,6 @@ export function SkillsScreen() {
333
246
  try {
334
247
  await uninstallSkill(selectedSkill.name, scope, state.projectPath);
335
248
  modal.hideModal();
336
- // Refetch to pick up uninstall status
337
249
  await fetchData();
338
250
  await modal.message("Uninstalled", `${selectedSkill.name} removed.`, "success");
339
251
  }
@@ -342,12 +254,11 @@ export function SkillsScreen() {
342
254
  await modal.message("Error", `Failed to uninstall: ${error}`, "error");
343
255
  }
344
256
  }, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
345
- // Keyboard handling — same pattern as PluginsScreen
257
+ // ── Keyboard handling ─────────────────────────────────────────────────────
346
258
  useKeyboard((event) => {
347
259
  if (state.modal)
348
260
  return;
349
261
  const hasQuery = skillsState.searchQuery.length > 0;
350
- // Escape: clear search
351
262
  if (event.name === "escape") {
352
263
  if (hasQuery || isSearchActive) {
353
264
  dispatch({ type: "SKILLS_SET_SEARCH", query: "" });
@@ -356,7 +267,6 @@ export function SkillsScreen() {
356
267
  }
357
268
  return;
358
269
  }
359
- // Backspace: remove last char
360
270
  if (event.name === "backspace" || event.name === "delete") {
361
271
  if (hasQuery) {
362
272
  const newQuery = skillsState.searchQuery.slice(0, -1);
@@ -367,7 +277,6 @@ export function SkillsScreen() {
367
277
  }
368
278
  return;
369
279
  }
370
- // Navigation — always works; exits search mode on navigate
371
280
  if (event.name === "up" || event.name === "k") {
372
281
  if (isSearchActive)
373
282
  dispatch({ type: "SET_SEARCHING", isSearching: false });
@@ -378,11 +287,10 @@ export function SkillsScreen() {
378
287
  if (event.name === "down" || event.name === "j") {
379
288
  if (isSearchActive)
380
289
  dispatch({ type: "SET_SEARCHING", isSearching: false });
381
- const newIndex = Math.min(Math.max(0, selectableItems.length - 1), skillsState.selectedIndex + 1);
290
+ const newIndex = Math.min(Math.max(0, allItems.length - 1), skillsState.selectedIndex + 1);
382
291
  dispatch({ type: "SKILLS_SELECT", index: newIndex });
383
292
  return;
384
293
  }
385
- // Enter — install (always works)
386
294
  if (event.name === "return" || event.name === "enter") {
387
295
  if (isSearchActive) {
388
296
  dispatch({ type: "SET_SEARCHING", isSearching: false });
@@ -393,11 +301,10 @@ export function SkillsScreen() {
393
301
  }
394
302
  return;
395
303
  }
396
- // When actively typing in search, letters go to the query
397
304
  if (isSearchActive) {
398
305
  if (event.name === "k" || event.name === "j") {
399
306
  const delta = event.name === "k" ? -1 : 1;
400
- const newIndex = Math.max(0, Math.min(selectableItems.length - 1, skillsState.selectedIndex + delta));
307
+ const newIndex = Math.max(0, Math.min(allItems.length - 1, skillsState.selectedIndex + delta));
401
308
  dispatch({ type: "SKILLS_SELECT", index: newIndex });
402
309
  return;
403
310
  }
@@ -407,7 +314,6 @@ export function SkillsScreen() {
407
314
  }
408
315
  return;
409
316
  }
410
- // Action shortcuts (work when not actively typing, even with filter visible)
411
317
  if (event.name === "u" && selectedSkill) {
412
318
  if (selectedSkill.installed && selectedSkill.installedScope === "user")
413
319
  handleUninstall();
@@ -420,14 +326,10 @@ export function SkillsScreen() {
420
326
  else
421
327
  handleInstall("project");
422
328
  }
423
- else if (event.name === "d" && selectedSkill?.installed) {
424
- handleUninstall();
425
- }
426
329
  else if (event.name === "r") {
427
330
  fetchData();
428
331
  }
429
332
  else if (event.name === "o" && selectedSkill) {
430
- // Open in browser
431
333
  const repo = selectedSkill.source.repo;
432
334
  const repoPath = selectedSkill.repoPath?.replace("/SKILL.md", "") || "";
433
335
  if (repo && repo !== "local") {
@@ -440,67 +342,16 @@ export function SkillsScreen() {
440
342
  });
441
343
  }
442
344
  }
443
- // "/" to enter search mode
444
345
  else if (event.name === "/") {
445
346
  dispatch({ type: "SET_SEARCHING", isSearching: true });
446
347
  }
447
348
  });
448
- const renderListItem = (item, _idx, isSelected) => {
449
- if (item.type === "category") {
450
- const isRec = item.categoryKey === "recommended";
451
- const isInstalled = item.categoryKey === "installed";
452
- const bgColor = isInstalled ? "#7e57c2" : isRec ? "#2e7d32" : "#00695c";
453
- const star = isRec ? "★ " : isInstalled ? "● " : "";
454
- if (isSelected) {
455
- return (_jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", star, item.label, " "] }) }));
456
- }
457
- return (_jsx("text", { bg: bgColor, fg: "white", children: _jsxs("strong", { children: [" ", star, item.label, " "] }) }));
458
- }
459
- if (item.type === "skill" && item.skill) {
460
- const skill = item.skill;
461
- const starsStr = formatStars(skill.stars);
462
- const hasUser = skill.installedScope === "user";
463
- const hasProject = skill.installedScope === "project";
464
- const nameColor = skill.installed ? "white" : "gray";
465
- if (isSelected) {
466
- return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", _jsx("span", { children: hasUser ? "■" : "□" }), _jsx("span", { children: hasProject ? "■" : "□" }), " ", skill.name, skill.hasUpdate ? " ⬆" : "", starsStr ? ` ${starsStr}` : "", " "] }));
467
- }
468
- return (_jsxs("text", { children: [_jsx("span", { children: " " }), _jsx("span", { fg: hasUser ? "cyan" : "#333333", children: "\u25A0" }), _jsx("span", { fg: hasProject ? "green" : "#333333", children: "\u25A0" }), _jsx("span", { children: " " }), _jsx("span", { fg: nameColor, children: skill.name }), skill.hasUpdate && _jsx("span", { fg: "yellow", children: " \u2B06" }), starsStr && (_jsxs("span", { fg: "yellow", children: [" ", starsStr] }))] }));
469
- }
470
- return _jsx("text", { fg: "gray", children: item.label });
471
- };
472
- const renderDetail = () => {
473
- if (skillsState.skills.status === "loading") {
474
- return _jsx("text", { fg: "gray", children: "Loading skills..." });
475
- }
476
- if (skillsState.skills.status === "error") {
477
- 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 }) })] }));
478
- }
479
- if (!selectedItem) {
480
- return _jsx("text", { fg: "gray", children: "Select a skill to see details" });
481
- }
482
- if (selectedItem.type === "category") {
483
- const isRec = selectedItem.categoryKey === "recommended";
484
- const isNoResults = selectedItem.categoryKey === "no-results";
485
- if (isNoResults) {
486
- 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." }) })] }));
487
- }
488
- 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" }) })] }));
489
- }
490
- if (!selectedSkill)
491
- return null;
492
- const fm = selectedSkill.frontmatter;
493
- const description = fm?.description || selectedSkill.description || "Loading...";
494
- const scopeColor = selectedSkill.installedScope === "user" ? "cyan" : "green";
495
- const starsStr = formatStars(selectedSkill.stars);
496
- 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"
497
- ? "~/.claude/skills/"
498
- : ".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" }))] })] }));
499
- };
349
+ // ── Status line ───────────────────────────────────────────────────────────
500
350
  const skills = skillsState.skills.status === "success" ? skillsState.skills.data : [];
501
351
  const installedCount = skills.filter((s) => s.installed).length;
502
352
  const query = skillsState.searchQuery.trim();
503
- 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" }))] }));
353
+ 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" })] }));
354
+ // ── Render ────────────────────────────────────────────────────────────────
504
355
  return (_jsx(ScreenLayout, { title: "claudeup Skills", currentScreen: "skills", statusLine: statusContent, search: skillsState.searchQuery || isSearchActive
505
356
  ? {
506
357
  isActive: isSearchActive,
@@ -509,6 +360,6 @@ export function SkillsScreen() {
509
360
  }
510
361
  : undefined, footerHints: isSearchActive
511
362
  ? "type to filter │ Enter:done │ Esc:clear"
512
- : "u:user │ p:project │ o:open │ /: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() }));
363
+ : "u:user │ p:project │ o:open │ /:search", listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items: allItems, selectedIndex: skillsState.selectedIndex, renderItem: renderSkillRow, 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: renderSkillDetail(selectedItem) }));
513
364
  }
514
365
  export default SkillsScreen;