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.
- package/package.json +1 -1
- package/src/ui/adapters/skillsAdapter.js +105 -0
- package/src/ui/adapters/skillsAdapter.ts +159 -0
- package/src/ui/components/primitives/ActionHints.js +13 -0
- package/src/ui/components/primitives/ActionHints.tsx +41 -0
- package/src/ui/components/primitives/DetailSection.js +7 -0
- package/src/ui/components/primitives/DetailSection.tsx +22 -0
- package/src/ui/components/primitives/KeyValueLine.js +8 -0
- package/src/ui/components/primitives/KeyValueLine.tsx +19 -0
- package/src/ui/components/primitives/ListCategoryRow.js +8 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +38 -0
- package/src/ui/components/primitives/MetaText.js +8 -0
- package/src/ui/components/primitives/MetaText.tsx +14 -0
- package/src/ui/components/primitives/ScopeDetail.js +32 -0
- package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
- package/src/ui/components/primitives/ScopeSquares.js +11 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +33 -0
- package/src/ui/components/primitives/SelectableRow.js +5 -0
- package/src/ui/components/primitives/SelectableRow.tsx +24 -0
- package/src/ui/components/primitives/index.js +8 -0
- package/src/ui/components/primitives/index.ts +9 -0
- package/src/ui/registry.js +1 -0
- package/src/ui/registry.ts +27 -0
- package/src/ui/renderers/skillRenderers.js +82 -0
- package/src/ui/renderers/skillRenderers.tsx +209 -0
- package/src/ui/screens/PluginsScreen.js +3 -4
- package/src/ui/screens/PluginsScreen.tsx +10 -8
- package/src/ui/screens/SkillsScreen.js +123 -177
- package/src/ui/screens/SkillsScreen.tsx +439 -700
- package/src/ui/theme.js +47 -0
- package/src/ui/theme.ts +53 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
(s.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
208
|
-
}, [
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
//
|
|
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
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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 │
|
|
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;
|