claudeup 3.13.0 → 3.15.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 (31) hide show
  1. package/package.json +1 -1
  2. package/src/ui/adapters/skillsAdapter.js +105 -0
  3. package/src/ui/adapters/skillsAdapter.ts +159 -0
  4. package/src/ui/components/primitives/ActionHints.js +13 -0
  5. package/src/ui/components/primitives/ActionHints.tsx +41 -0
  6. package/src/ui/components/primitives/DetailSection.js +7 -0
  7. package/src/ui/components/primitives/DetailSection.tsx +22 -0
  8. package/src/ui/components/primitives/KeyValueLine.js +8 -0
  9. package/src/ui/components/primitives/KeyValueLine.tsx +19 -0
  10. package/src/ui/components/primitives/ListCategoryRow.js +8 -0
  11. package/src/ui/components/primitives/ListCategoryRow.tsx +38 -0
  12. package/src/ui/components/primitives/MetaText.js +8 -0
  13. package/src/ui/components/primitives/MetaText.tsx +14 -0
  14. package/src/ui/components/primitives/ScopeDetail.js +32 -0
  15. package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
  16. package/src/ui/components/primitives/ScopeSquares.js +11 -0
  17. package/src/ui/components/primitives/ScopeSquares.tsx +33 -0
  18. package/src/ui/components/primitives/SelectableRow.js +5 -0
  19. package/src/ui/components/primitives/SelectableRow.tsx +24 -0
  20. package/src/ui/components/primitives/index.js +8 -0
  21. package/src/ui/components/primitives/index.ts +9 -0
  22. package/src/ui/registry.js +1 -0
  23. package/src/ui/registry.ts +27 -0
  24. package/src/ui/renderers/skillRenderers.js +82 -0
  25. package/src/ui/renderers/skillRenderers.tsx +209 -0
  26. package/src/ui/screens/PluginsScreen.js +3 -4
  27. package/src/ui/screens/PluginsScreen.tsx +10 -8
  28. package/src/ui/screens/SkillsScreen.js +123 -177
  29. package/src/ui/screens/SkillsScreen.tsx +439 -700
  30. package/src/ui/theme.js +47 -0
  31. package/src/ui/theme.ts +53 -0
@@ -1,5 +1,8 @@
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
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import fs from "fs-extra";
3
6
  import { useApp, useModal } from "../state/AppContext.js";
4
7
  import { useDimensions } from "../state/DimensionsContext.js";
5
8
  import { useKeyboard } from "../hooks/useKeyboard.js";
@@ -9,17 +12,8 @@ import { EmptyFilterState } from "../components/EmptyFilterState.js";
9
12
  import { fetchAvailableSkills, fetchSkillFrontmatter, installSkill, uninstallSkill, } from "../../services/skills-manager.js";
10
13
  import { searchSkills } from "../../services/skillsmp-client.js";
11
14
  import { DEFAULT_SKILL_REPOS, RECOMMENDED_SKILLS } from "../../data/skill-repos.js";
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
- }
15
+ import { buildSkillBrowserItems } from "../adapters/skillsAdapter.js";
16
+ import { renderSkillRow, renderSkillDetail } from "../renderers/skillRenderers.js";
23
17
  export function SkillsScreen() {
24
18
  const { state, dispatch } = useApp();
25
19
  const { skills: skillsState } = state;
@@ -28,7 +22,7 @@ export function SkillsScreen() {
28
22
  const isSearchActive = state.isSearching &&
29
23
  state.currentRoute.screen === "skills" &&
30
24
  !state.modal;
31
- // Fetch data
25
+ // ── Data fetching ─────────────────────────────────────────────────────────
32
26
  const fetchData = useCallback(async () => {
33
27
  dispatch({ type: "SKILLS_DATA_LOADING" });
34
28
  try {
@@ -45,7 +39,7 @@ export function SkillsScreen() {
45
39
  useEffect(() => {
46
40
  fetchData();
47
41
  }, [fetchData, state.dataRefreshVersion]);
48
- // Remote search: query Firebase API when user types (debounced, cached)
42
+ // ── Remote search (debounced, cached) ─────────────────────────────────────
49
43
  const [searchResults, setSearchResults] = useState([]);
50
44
  const [isSearchLoading, setIsSearchLoading] = useState(false);
51
45
  const searchTimerRef = useRef(null);
@@ -57,7 +51,6 @@ export function SkillsScreen() {
57
51
  setIsSearchLoading(false);
58
52
  return;
59
53
  }
60
- // Check cache first
61
54
  const cached = searchCacheRef.current.get(query);
62
55
  if (cached) {
63
56
  setSearchResults(cached);
@@ -103,113 +96,113 @@ export function SkillsScreen() {
103
96
  clearTimeout(searchTimerRef.current);
104
97
  };
105
98
  }, [skillsState.searchQuery]);
106
- // Static recommended skills always available, no API needed
99
+ // ── Disk scan for installed skills ────────────────────────────────────────
100
+ const [installedFromDisk, setInstalledFromDisk] = useState({ user: new Set(), project: new Set() });
101
+ useEffect(() => {
102
+ async function scanDisk() {
103
+ const user = new Set();
104
+ const project = new Set();
105
+ const userDir = path.join(os.homedir(), ".claude", "skills");
106
+ const projDir = path.join(state.projectPath || process.cwd(), ".claude", "skills");
107
+ for (const [dir, set] of [[userDir, user], [projDir, project]]) {
108
+ try {
109
+ if (await fs.pathExists(dir)) {
110
+ const entries = await fs.readdir(dir);
111
+ for (const e of entries) {
112
+ if (await fs.pathExists(path.join(dir, e, "SKILL.md")))
113
+ set.add(e);
114
+ }
115
+ }
116
+ }
117
+ catch { /* ignore */ }
118
+ }
119
+ setInstalledFromDisk({ user, project });
120
+ }
121
+ scanDisk();
122
+ }, [state.projectPath, state.dataRefreshVersion]);
123
+ // ── Derived data ──────────────────────────────────────────────────────────
107
124
  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)
125
+ return RECOMMENDED_SKILLS.map((r) => {
126
+ const slug = r.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
127
+ const isUser = installedFromDisk.user.has(slug) || installedFromDisk.user.has(r.name);
128
+ const isProj = installedFromDisk.project.has(slug) || installedFromDisk.project.has(r.name);
129
+ return {
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: isUser || isProj,
138
+ installedScope: isProj ? "project" : isUser ? "user" : null,
139
+ hasUpdate: false,
140
+ isRecommended: true,
141
+ stars: undefined,
142
+ };
143
+ });
144
+ }, [installedFromDisk]);
124
145
  const mergedRecommended = useMemo(() => {
125
146
  if (skillsState.skills.status !== "success")
126
147
  return staticRecommended;
127
148
  const fetched = skillsState.skills.data.filter((s) => s.isRecommended);
128
- // Merge: keep fetched data (has stars + install status), fall back to static
129
149
  return staticRecommended.map((staticSkill) => {
130
150
  const match = fetched.find((f) => f.source.repo === staticSkill.source.repo && f.name === staticSkill.name);
131
151
  return match || staticSkill;
132
152
  });
133
153
  }, [staticRecommended, skillsState.skills]);
134
- // Build list: recommended always shown, then search results or popular
135
- const allItems = useMemo(() => {
136
- const query = skillsState.searchQuery.toLowerCase();
137
- const items = [];
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) {
150
- items.push({
151
- id: `skill:${skill.id}`,
152
- type: "skill",
153
- label: skill.name,
154
- skill,
155
- });
156
- }
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;
183
- }
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) {
191
- items.push({
192
- id: "cat:popular",
193
- type: "category",
194
- label: "Popular",
195
- categoryKey: "popular",
154
+ const installedSkills = useMemo(() => {
155
+ const all = [];
156
+ for (const [scope, names] of [
157
+ ["user", installedFromDisk.user],
158
+ ["project", installedFromDisk.project],
159
+ ]) {
160
+ for (const name of names) {
161
+ if (all.some((s) => s.name === name))
162
+ continue;
163
+ all.push({
164
+ id: `installed:${scope}/${name}`,
165
+ name,
166
+ description: "",
167
+ source: { label: "local", repo: "local", skillsPath: "" },
168
+ repoPath: "",
169
+ gitBlobSha: "",
170
+ frontmatter: null,
171
+ installed: true,
172
+ installedScope: scope,
173
+ hasUpdate: false,
174
+ stars: undefined,
196
175
  });
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
- }
205
176
  }
206
177
  }
207
- return items;
208
- }, [skillsState.skills, skillsState.searchQuery, searchResults, isSearchLoading, mergedRecommended]);
209
- const selectableItems = useMemo(() => allItems.filter((item) => item.type === "skill" || item.type === "category"), [allItems]);
210
- const selectedItem = selectableItems[skillsState.selectedIndex];
211
- const selectedSkill = selectedItem?.type === "skill" ? selectedItem.skill : undefined;
212
- // Lazy-load frontmatter for selected skill
178
+ return all;
179
+ }, [installedFromDisk]);
180
+ const popularSkills = useMemo(() => {
181
+ if (skillsState.skills.status !== "success")
182
+ return [];
183
+ return skillsState.skills.data
184
+ .filter((s) => !s.isRecommended)
185
+ .sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
186
+ }, [skillsState.skills]);
187
+ // ── List items (built by adapter) ─────────────────────────────────────────
188
+ const allItems = useMemo(() => buildSkillBrowserItems({
189
+ recommended: mergedRecommended,
190
+ popular: popularSkills,
191
+ installed: installedSkills,
192
+ searchResults,
193
+ query: skillsState.searchQuery,
194
+ isSearchLoading,
195
+ }), [
196
+ mergedRecommended,
197
+ popularSkills,
198
+ installedSkills,
199
+ searchResults,
200
+ skillsState.searchQuery,
201
+ isSearchLoading,
202
+ ]);
203
+ const selectedItem = allItems[skillsState.selectedIndex];
204
+ const selectedSkill = selectedItem?.kind === "skill" ? selectedItem.skill : undefined;
205
+ // ── Lazy-load frontmatter for selected skill ───────────────────────────────
213
206
  useEffect(() => {
214
207
  if (!selectedSkill || selectedSkill.frontmatter)
215
208
  return;
@@ -221,7 +214,7 @@ export function SkillsScreen() {
221
214
  });
222
215
  }).catch(() => { });
223
216
  }, [selectedSkill?.id, dispatch]);
224
- // Install handler
217
+ // ── Action handlers ───────────────────────────────────────────────────────
225
218
  const handleInstall = useCallback(async (scope) => {
226
219
  if (!selectedSkill)
227
220
  return;
@@ -229,7 +222,6 @@ export function SkillsScreen() {
229
222
  try {
230
223
  await installSkill(selectedSkill, scope, state.projectPath);
231
224
  modal.hideModal();
232
- // Refetch to pick up install status for all skills including recommended
233
225
  await fetchData();
234
226
  await modal.message("Installed", `${selectedSkill.name} installed to ${scope === "user" ? "~/.claude/skills/" : ".claude/skills/"}`, "success");
235
227
  }
@@ -238,7 +230,6 @@ export function SkillsScreen() {
238
230
  await modal.message("Error", `Failed to install: ${error}`, "error");
239
231
  }
240
232
  }, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
241
- // Uninstall handler
242
233
  const handleUninstall = useCallback(async () => {
243
234
  if (!selectedSkill || !selectedSkill.installed)
244
235
  return;
@@ -252,7 +243,6 @@ export function SkillsScreen() {
252
243
  try {
253
244
  await uninstallSkill(selectedSkill.name, scope, state.projectPath);
254
245
  modal.hideModal();
255
- // Refetch to pick up uninstall status
256
246
  await fetchData();
257
247
  await modal.message("Uninstalled", `${selectedSkill.name} removed.`, "success");
258
248
  }
@@ -261,12 +251,11 @@ export function SkillsScreen() {
261
251
  await modal.message("Error", `Failed to uninstall: ${error}`, "error");
262
252
  }
263
253
  }, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
264
- // Keyboard handling — same pattern as PluginsScreen
254
+ // ── Keyboard handling ─────────────────────────────────────────────────────
265
255
  useKeyboard((event) => {
266
256
  if (state.modal)
267
257
  return;
268
258
  const hasQuery = skillsState.searchQuery.length > 0;
269
- // Escape: clear search
270
259
  if (event.name === "escape") {
271
260
  if (hasQuery || isSearchActive) {
272
261
  dispatch({ type: "SKILLS_SET_SEARCH", query: "" });
@@ -275,7 +264,6 @@ export function SkillsScreen() {
275
264
  }
276
265
  return;
277
266
  }
278
- // Backspace: remove last char
279
267
  if (event.name === "backspace" || event.name === "delete") {
280
268
  if (hasQuery) {
281
269
  const newQuery = skillsState.searchQuery.slice(0, -1);
@@ -286,7 +274,6 @@ export function SkillsScreen() {
286
274
  }
287
275
  return;
288
276
  }
289
- // Navigation — always works; exits search mode on navigate
290
277
  if (event.name === "up" || event.name === "k") {
291
278
  if (isSearchActive)
292
279
  dispatch({ type: "SET_SEARCHING", isSearching: false });
@@ -297,11 +284,10 @@ export function SkillsScreen() {
297
284
  if (event.name === "down" || event.name === "j") {
298
285
  if (isSearchActive)
299
286
  dispatch({ type: "SET_SEARCHING", isSearching: false });
300
- const newIndex = Math.min(Math.max(0, selectableItems.length - 1), skillsState.selectedIndex + 1);
287
+ const newIndex = Math.min(Math.max(0, allItems.length - 1), skillsState.selectedIndex + 1);
301
288
  dispatch({ type: "SKILLS_SELECT", index: newIndex });
302
289
  return;
303
290
  }
304
- // Enter — install (always works)
305
291
  if (event.name === "return" || event.name === "enter") {
306
292
  if (isSearchActive) {
307
293
  dispatch({ type: "SET_SEARCHING", isSearching: false });
@@ -312,11 +298,10 @@ export function SkillsScreen() {
312
298
  }
313
299
  return;
314
300
  }
315
- // When actively typing in search, letters go to the query
316
301
  if (isSearchActive) {
317
302
  if (event.name === "k" || event.name === "j") {
318
303
  const delta = event.name === "k" ? -1 : 1;
319
- const newIndex = Math.max(0, Math.min(selectableItems.length - 1, skillsState.selectedIndex + delta));
304
+ const newIndex = Math.max(0, Math.min(allItems.length - 1, skillsState.selectedIndex + delta));
320
305
  dispatch({ type: "SKILLS_SELECT", index: newIndex });
321
306
  return;
322
307
  }
@@ -326,7 +311,6 @@ export function SkillsScreen() {
326
311
  }
327
312
  return;
328
313
  }
329
- // Action shortcuts (work when not actively typing, even with filter visible)
330
314
  if (event.name === "u" && selectedSkill) {
331
315
  if (selectedSkill.installed && selectedSkill.installedScope === "user")
332
316
  handleUninstall();
@@ -345,67 +329,29 @@ export function SkillsScreen() {
345
329
  else if (event.name === "r") {
346
330
  fetchData();
347
331
  }
348
- // "/" to enter search mode
332
+ else if (event.name === "o" && selectedSkill) {
333
+ const repo = selectedSkill.source.repo;
334
+ const repoPath = selectedSkill.repoPath?.replace("/SKILL.md", "") || "";
335
+ if (repo && repo !== "local") {
336
+ const url = `https://github.com/${repo}/tree/main/${repoPath}`;
337
+ import("node:child_process").then(({ execSync: exec }) => {
338
+ try {
339
+ exec(`open "${url}"`);
340
+ }
341
+ catch { /* ignore */ }
342
+ });
343
+ }
344
+ }
349
345
  else if (event.name === "/") {
350
346
  dispatch({ type: "SET_SEARCHING", isSearching: true });
351
347
  }
352
348
  });
353
- const renderListItem = (item, _idx, isSelected) => {
354
- if (item.type === "category") {
355
- const isRec = item.categoryKey === "recommended";
356
- const bgColor = isRec ? "green" : "cyan";
357
- const star = isRec ? "★ " : "";
358
- if (isSelected) {
359
- return (_jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", star, item.label, " "] }) }));
360
- }
361
- return (_jsx("text", { bg: bgColor, fg: "white", children: _jsxs("strong", { children: [" ", star, item.label, " "] }) }));
362
- }
363
- if (item.type === "skill" && item.skill) {
364
- const skill = item.skill;
365
- const starsStr = formatStars(skill.stars);
366
- const hasUser = skill.installedScope === "user";
367
- const hasProject = skill.installedScope === "project";
368
- const nameColor = skill.installed ? "white" : "gray";
369
- if (isSelected) {
370
- const scopeStr = `[${hasUser ? "u" : "."}${hasProject ? "p" : "."}]`;
371
- return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", scopeStr, " ", skill.name, skill.hasUpdate ? " ⬆" : "", starsStr ? ` ${starsStr}` : "", " "] }));
372
- }
373
- return (_jsxs("text", { children: [_jsx("span", { fg: "#555555", children: " [" }), _jsx("span", { fg: hasUser ? "cyan" : "#555555", children: hasUser ? "u" : "." }), _jsx("span", { fg: hasProject ? "green" : "#555555", children: hasProject ? "p" : "." }), _jsx("span", { fg: "#555555", children: "] " }), _jsx("span", { fg: nameColor, children: skill.name }), skill.hasUpdate && _jsx("span", { fg: "yellow", children: " \u2B06" }), starsStr && (_jsxs("span", { fg: "yellow", children: [" ", starsStr] }))] }));
374
- }
375
- return _jsx("text", { fg: "gray", children: item.label });
376
- };
377
- const renderDetail = () => {
378
- if (skillsState.skills.status === "loading") {
379
- return _jsx("text", { fg: "gray", children: "Loading skills..." });
380
- }
381
- if (skillsState.skills.status === "error") {
382
- 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 }) })] }));
383
- }
384
- if (!selectedItem) {
385
- return _jsx("text", { fg: "gray", children: "Select a skill to see details" });
386
- }
387
- if (selectedItem.type === "category") {
388
- const isRec = selectedItem.categoryKey === "recommended";
389
- const isNoResults = selectedItem.categoryKey === "no-results";
390
- if (isNoResults) {
391
- 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." }) })] }));
392
- }
393
- 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" }) })] }));
394
- }
395
- if (!selectedSkill)
396
- return null;
397
- const fm = selectedSkill.frontmatter;
398
- const description = fm?.description || selectedSkill.description || "Loading...";
399
- const scopeColor = selectedSkill.installedScope === "user" ? "cyan" : "green";
400
- const starsStr = formatStars(selectedSkill.stars);
401
- 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"
402
- ? "~/.claude/skills/"
403
- : ".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" }))] })] }));
404
- };
349
+ // ── Status line ───────────────────────────────────────────────────────────
405
350
  const skills = skillsState.skills.status === "success" ? skillsState.skills.data : [];
406
351
  const installedCount = skills.filter((s) => s.installed).length;
407
352
  const query = skillsState.searchQuery.trim();
408
- 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 ────────────────────────────────────────────────────────────────
409
355
  return (_jsx(ScreenLayout, { title: "claudeup Skills", currentScreen: "skills", statusLine: statusContent, search: skillsState.searchQuery || isSearchActive
410
356
  ? {
411
357
  isActive: isSearchActive,
@@ -414,6 +360,6 @@ export function SkillsScreen() {
414
360
  }
415
361
  : undefined, footerHints: isSearchActive
416
362
  ? "type to filter │ Enter:done │ Esc:clear"
417
- : "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() }));
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) }));
418
364
  }
419
365
  export default SkillsScreen;