claudeup 3.13.0 → 3.14.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
CHANGED
|
@@ -901,8 +901,7 @@ export function PluginsScreen() {
|
|
|
901
901
|
const matches = item._matches;
|
|
902
902
|
const segments = matches ? highlightMatches(plugin.name, matches) : null;
|
|
903
903
|
if (isSelected) {
|
|
904
|
-
|
|
905
|
-
return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", scopeStr, " ", plugin.name, versionStr, " "] }));
|
|
904
|
+
return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", _jsx("span", { children: hasUser ? "■" : "□" }), _jsx("span", { children: hasProject ? "■" : "□" }), _jsx("span", { children: hasLocal ? "■" : "□" }), " ", plugin.name, versionStr, " "] }));
|
|
906
905
|
}
|
|
907
906
|
const displayName = segments
|
|
908
907
|
? segments.map((seg) => seg.text).join("")
|
|
@@ -910,9 +909,9 @@ export function PluginsScreen() {
|
|
|
910
909
|
if (plugin.isOrphaned) {
|
|
911
910
|
const ver = plugin.installedVersion && plugin.installedVersion !== "0.0.0"
|
|
912
911
|
? ` v${plugin.installedVersion}` : "";
|
|
913
|
-
return (_jsxs("text", { children: [_jsx("span", { fg: "red", children: "
|
|
912
|
+
return (_jsxs("text", { children: [_jsx("span", { fg: "red", children: " \u25A0\u25A0\u25A0 " }), _jsx("span", { fg: "gray", children: displayName }), ver && _jsx("span", { fg: "yellow", children: ver }), _jsx("span", { fg: "red", children: " deprecated" })] }));
|
|
914
913
|
}
|
|
915
|
-
return (_jsxs("text", { children: [_jsx("span", {
|
|
914
|
+
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", { fg: hasLocal ? "yellow" : "#333333", children: "\u25A0" }), _jsx("span", { children: " " }), _jsx("span", { fg: hasAnyScope ? "white" : "gray", children: displayName }), _jsx("span", { fg: plugin.hasUpdate ? "yellow" : "gray", children: versionStr })] }));
|
|
916
915
|
}
|
|
917
916
|
return _jsx("text", { fg: "gray", children: item.label });
|
|
918
917
|
};
|
|
@@ -1162,10 +1162,13 @@ export function PluginsScreen() {
|
|
|
1162
1162
|
const segments = matches ? highlightMatches(plugin.name, matches) : null;
|
|
1163
1163
|
|
|
1164
1164
|
if (isSelected) {
|
|
1165
|
-
const scopeStr = `[${hasUser ? "u" : "."}${hasProject ? "p" : "."}${hasLocal ? "l" : "."}]`;
|
|
1166
1165
|
return (
|
|
1167
1166
|
<text bg="magenta" fg="white">
|
|
1168
|
-
{" "}
|
|
1167
|
+
{" "}
|
|
1168
|
+
<span>{hasUser ? "■" : "□"}</span>
|
|
1169
|
+
<span>{hasProject ? "■" : "□"}</span>
|
|
1170
|
+
<span>{hasLocal ? "■" : "□"}</span>
|
|
1171
|
+
{" "}{plugin.name}{versionStr}{" "}
|
|
1169
1172
|
</text>
|
|
1170
1173
|
);
|
|
1171
1174
|
}
|
|
@@ -1179,7 +1182,7 @@ export function PluginsScreen() {
|
|
|
1179
1182
|
? ` v${plugin.installedVersion}` : "";
|
|
1180
1183
|
return (
|
|
1181
1184
|
<text>
|
|
1182
|
-
<span fg="red">
|
|
1185
|
+
<span fg="red"> ■■■ </span>
|
|
1183
1186
|
<span fg="gray">{displayName}</span>
|
|
1184
1187
|
{ver && <span fg="yellow">{ver}</span>}
|
|
1185
1188
|
<span fg="red"> deprecated</span>
|
|
@@ -1189,11 +1192,10 @@ export function PluginsScreen() {
|
|
|
1189
1192
|
|
|
1190
1193
|
return (
|
|
1191
1194
|
<text>
|
|
1192
|
-
<span
|
|
1193
|
-
<span fg={hasUser ? "cyan" : "#
|
|
1194
|
-
<span fg={hasProject ? "green" : "#
|
|
1195
|
-
<span fg={hasLocal ? "yellow" : "#
|
|
1196
|
-
<span fg="#555555">]</span>
|
|
1195
|
+
<span> </span>
|
|
1196
|
+
<span fg={hasUser ? "cyan" : "#333333"}>■</span>
|
|
1197
|
+
<span fg={hasProject ? "green" : "#333333"}>■</span>
|
|
1198
|
+
<span fg={hasLocal ? "yellow" : "#333333"}>■</span>
|
|
1197
1199
|
<span> </span>
|
|
1198
1200
|
<span fg={hasAnyScope ? "white" : "gray"}>{displayName}</span>
|
|
1199
1201
|
<span fg={plugin.hasUpdate ? "yellow" : "gray"}>{versionStr}</span>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } 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";
|
|
@@ -103,23 +106,53 @@ export function SkillsScreen() {
|
|
|
103
106
|
clearTimeout(searchTimerRef.current);
|
|
104
107
|
};
|
|
105
108
|
}, [skillsState.searchQuery]);
|
|
109
|
+
// Scan installed skills from disk (instant, no API)
|
|
110
|
+
const [installedFromDisk, setInstalledFromDisk] = useState({ user: new Set(), project: new Set() });
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
async function scanDisk() {
|
|
113
|
+
const user = new Set();
|
|
114
|
+
const project = new Set();
|
|
115
|
+
const userDir = path.join(os.homedir(), ".claude", "skills");
|
|
116
|
+
const projDir = path.join(state.projectPath || process.cwd(), ".claude", "skills");
|
|
117
|
+
for (const [dir, set] of [[userDir, user], [projDir, project]]) {
|
|
118
|
+
try {
|
|
119
|
+
if (await fs.pathExists(dir)) {
|
|
120
|
+
const entries = await fs.readdir(dir);
|
|
121
|
+
for (const e of entries) {
|
|
122
|
+
if (await fs.pathExists(path.join(dir, e, "SKILL.md")))
|
|
123
|
+
set.add(e);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch { /* ignore */ }
|
|
128
|
+
}
|
|
129
|
+
setInstalledFromDisk({ user, project });
|
|
130
|
+
}
|
|
131
|
+
scanDisk();
|
|
132
|
+
}, [state.projectPath, state.dataRefreshVersion]);
|
|
106
133
|
// Static recommended skills — always available, no API needed
|
|
134
|
+
// Enriched with disk-based install status immediately
|
|
107
135
|
const staticRecommended = useMemo(() => {
|
|
108
|
-
return RECOMMENDED_SKILLS.map((r) =>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
136
|
+
return RECOMMENDED_SKILLS.map((r) => {
|
|
137
|
+
const slug = r.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
138
|
+
const isUser = installedFromDisk.user.has(slug) || installedFromDisk.user.has(r.name);
|
|
139
|
+
const isProj = installedFromDisk.project.has(slug) || installedFromDisk.project.has(r.name);
|
|
140
|
+
return {
|
|
141
|
+
id: `rec:${r.repo}/${r.skillPath}`,
|
|
142
|
+
name: r.name,
|
|
143
|
+
description: r.description,
|
|
144
|
+
source: { label: r.repo, repo: r.repo, skillsPath: "" },
|
|
145
|
+
repoPath: `${r.skillPath}/SKILL.md`,
|
|
146
|
+
gitBlobSha: "",
|
|
147
|
+
frontmatter: null,
|
|
148
|
+
installed: isUser || isProj,
|
|
149
|
+
installedScope: isProj ? "project" : isUser ? "user" : null,
|
|
150
|
+
hasUpdate: false,
|
|
151
|
+
isRecommended: true,
|
|
152
|
+
stars: undefined,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}, [installedFromDisk]);
|
|
123
156
|
// Merge static recommended with fetched data (to get install status + stars)
|
|
124
157
|
const mergedRecommended = useMemo(() => {
|
|
125
158
|
if (skillsState.skills.status !== "success")
|
|
@@ -131,10 +164,58 @@ export function SkillsScreen() {
|
|
|
131
164
|
return match || staticSkill;
|
|
132
165
|
});
|
|
133
166
|
}, [staticRecommended, skillsState.skills]);
|
|
134
|
-
// Build
|
|
167
|
+
// Build installed skills list from disk (always available)
|
|
168
|
+
const installedSkills = useMemo(() => {
|
|
169
|
+
const all = [];
|
|
170
|
+
for (const [scope, names] of [
|
|
171
|
+
["user", installedFromDisk.user],
|
|
172
|
+
["project", installedFromDisk.project],
|
|
173
|
+
]) {
|
|
174
|
+
for (const name of names) {
|
|
175
|
+
// Skip if already in the list (avoid dupes)
|
|
176
|
+
if (all.some((s) => s.name === name))
|
|
177
|
+
continue;
|
|
178
|
+
all.push({
|
|
179
|
+
id: `installed:${scope}/${name}`,
|
|
180
|
+
name,
|
|
181
|
+
description: "",
|
|
182
|
+
source: { label: "local", repo: "local", skillsPath: "" },
|
|
183
|
+
repoPath: "",
|
|
184
|
+
gitBlobSha: "",
|
|
185
|
+
frontmatter: null,
|
|
186
|
+
installed: true,
|
|
187
|
+
installedScope: scope,
|
|
188
|
+
hasUpdate: false,
|
|
189
|
+
stars: undefined,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return all;
|
|
194
|
+
}, [installedFromDisk]);
|
|
195
|
+
// Build list: installed first, then recommended, then search/popular
|
|
135
196
|
const allItems = useMemo(() => {
|
|
136
197
|
const query = skillsState.searchQuery.toLowerCase();
|
|
137
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
|
+
}
|
|
138
219
|
// ── RECOMMENDED: always shown, filtered when searching ──
|
|
139
220
|
const filteredRec = query
|
|
140
221
|
? mergedRecommended.filter((s) => s.name.toLowerCase().includes(query) ||
|
|
@@ -345,6 +426,20 @@ export function SkillsScreen() {
|
|
|
345
426
|
else if (event.name === "r") {
|
|
346
427
|
fetchData();
|
|
347
428
|
}
|
|
429
|
+
else if (event.name === "o" && selectedSkill) {
|
|
430
|
+
// Open in browser
|
|
431
|
+
const repo = selectedSkill.source.repo;
|
|
432
|
+
const repoPath = selectedSkill.repoPath?.replace("/SKILL.md", "") || "";
|
|
433
|
+
if (repo && repo !== "local") {
|
|
434
|
+
const url = `https://github.com/${repo}/tree/main/${repoPath}`;
|
|
435
|
+
import("node:child_process").then(({ execSync: exec }) => {
|
|
436
|
+
try {
|
|
437
|
+
exec(`open "${url}"`);
|
|
438
|
+
}
|
|
439
|
+
catch { /* ignore */ }
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
348
443
|
// "/" to enter search mode
|
|
349
444
|
else if (event.name === "/") {
|
|
350
445
|
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
@@ -353,8 +448,9 @@ export function SkillsScreen() {
|
|
|
353
448
|
const renderListItem = (item, _idx, isSelected) => {
|
|
354
449
|
if (item.type === "category") {
|
|
355
450
|
const isRec = item.categoryKey === "recommended";
|
|
356
|
-
const
|
|
357
|
-
const
|
|
451
|
+
const isInstalled = item.categoryKey === "installed";
|
|
452
|
+
const bgColor = isInstalled ? "#7e57c2" : isRec ? "#2e7d32" : "#00695c";
|
|
453
|
+
const star = isRec ? "★ " : isInstalled ? "● " : "";
|
|
358
454
|
if (isSelected) {
|
|
359
455
|
return (_jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", star, item.label, " "] }) }));
|
|
360
456
|
}
|
|
@@ -367,10 +463,9 @@ export function SkillsScreen() {
|
|
|
367
463
|
const hasProject = skill.installedScope === "project";
|
|
368
464
|
const nameColor = skill.installed ? "white" : "gray";
|
|
369
465
|
if (isSelected) {
|
|
370
|
-
|
|
371
|
-
return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", scopeStr, " ", skill.name, skill.hasUpdate ? " ⬆" : "", starsStr ? ` ${starsStr}` : "", " "] }));
|
|
466
|
+
return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", _jsx("span", { children: hasUser ? "■" : "□" }), _jsx("span", { children: hasProject ? "■" : "□" }), " ", skill.name, skill.hasUpdate ? " ⬆" : "", starsStr ? ` ${starsStr}` : "", " "] }));
|
|
372
467
|
}
|
|
373
|
-
return (_jsxs("text", { children: [_jsx("span", {
|
|
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] }))] }));
|
|
374
469
|
}
|
|
375
470
|
return _jsx("text", { fg: "gray", children: item.label });
|
|
376
471
|
};
|
|
@@ -414,6 +509,6 @@ export function SkillsScreen() {
|
|
|
414
509
|
}
|
|
415
510
|
: undefined, footerHints: isSearchActive
|
|
416
511
|
? "type to filter │ Enter:done │ Esc:clear"
|
|
417
|
-
: "u:user │ p:project │
|
|
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() }));
|
|
418
513
|
}
|
|
419
514
|
export default SkillsScreen;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useCallback, useMemo, useState, useRef } from "react";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "fs-extra";
|
|
2
5
|
import { useApp, useModal } from "../state/AppContext.js";
|
|
3
6
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
7
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
@@ -125,23 +128,56 @@ export function SkillsScreen() {
|
|
|
125
128
|
};
|
|
126
129
|
}, [skillsState.searchQuery]);
|
|
127
130
|
|
|
131
|
+
// Scan installed skills from disk (instant, no API)
|
|
132
|
+
const [installedFromDisk, setInstalledFromDisk] = useState<{
|
|
133
|
+
user: Set<string>;
|
|
134
|
+
project: Set<string>;
|
|
135
|
+
}>({ user: new Set(), project: new Set() });
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
async function scanDisk() {
|
|
139
|
+
const user = new Set<string>();
|
|
140
|
+
const project = new Set<string>();
|
|
141
|
+
const userDir = path.join(os.homedir(), ".claude", "skills");
|
|
142
|
+
const projDir = path.join(state.projectPath || process.cwd(), ".claude", "skills");
|
|
143
|
+
for (const [dir, set] of [[userDir, user], [projDir, project]] as const) {
|
|
144
|
+
try {
|
|
145
|
+
if (await fs.pathExists(dir)) {
|
|
146
|
+
const entries = await fs.readdir(dir);
|
|
147
|
+
for (const e of entries) {
|
|
148
|
+
if (await fs.pathExists(path.join(dir, e, "SKILL.md"))) set.add(e);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch { /* ignore */ }
|
|
152
|
+
}
|
|
153
|
+
setInstalledFromDisk({ user, project });
|
|
154
|
+
}
|
|
155
|
+
scanDisk();
|
|
156
|
+
}, [state.projectPath, state.dataRefreshVersion]);
|
|
157
|
+
|
|
128
158
|
// Static recommended skills — always available, no API needed
|
|
159
|
+
// Enriched with disk-based install status immediately
|
|
129
160
|
const staticRecommended = useMemo((): SkillInfo[] => {
|
|
130
|
-
return RECOMMENDED_SKILLS.map((r) =>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
161
|
+
return RECOMMENDED_SKILLS.map((r) => {
|
|
162
|
+
const slug = r.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
163
|
+
const isUser = installedFromDisk.user.has(slug) || installedFromDisk.user.has(r.name);
|
|
164
|
+
const isProj = installedFromDisk.project.has(slug) || installedFromDisk.project.has(r.name);
|
|
165
|
+
return {
|
|
166
|
+
id: `rec:${r.repo}/${r.skillPath}`,
|
|
167
|
+
name: r.name,
|
|
168
|
+
description: r.description,
|
|
169
|
+
source: { label: r.repo, repo: r.repo, skillsPath: "" },
|
|
170
|
+
repoPath: `${r.skillPath}/SKILL.md`,
|
|
171
|
+
gitBlobSha: "",
|
|
172
|
+
frontmatter: null,
|
|
173
|
+
installed: isUser || isProj,
|
|
174
|
+
installedScope: isProj ? "project" : isUser ? "user" : null,
|
|
175
|
+
hasUpdate: false,
|
|
176
|
+
isRecommended: true,
|
|
177
|
+
stars: undefined,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
}, [installedFromDisk]);
|
|
145
181
|
|
|
146
182
|
// Merge static recommended with fetched data (to get install status + stars)
|
|
147
183
|
const mergedRecommended = useMemo((): SkillInfo[] => {
|
|
@@ -156,11 +192,61 @@ export function SkillsScreen() {
|
|
|
156
192
|
});
|
|
157
193
|
}, [staticRecommended, skillsState.skills]);
|
|
158
194
|
|
|
159
|
-
// Build
|
|
195
|
+
// Build installed skills list from disk (always available)
|
|
196
|
+
const installedSkills = useMemo((): SkillInfo[] => {
|
|
197
|
+
const all: SkillInfo[] = [];
|
|
198
|
+
for (const [scope, names] of [
|
|
199
|
+
["user", installedFromDisk.user],
|
|
200
|
+
["project", installedFromDisk.project],
|
|
201
|
+
] as const) {
|
|
202
|
+
for (const name of names) {
|
|
203
|
+
// Skip if already in the list (avoid dupes)
|
|
204
|
+
if (all.some((s) => s.name === name)) continue;
|
|
205
|
+
all.push({
|
|
206
|
+
id: `installed:${scope}/${name}`,
|
|
207
|
+
name,
|
|
208
|
+
description: "",
|
|
209
|
+
source: { label: "local", repo: "local", skillsPath: "" },
|
|
210
|
+
repoPath: "",
|
|
211
|
+
gitBlobSha: "",
|
|
212
|
+
frontmatter: null,
|
|
213
|
+
installed: true,
|
|
214
|
+
installedScope: scope,
|
|
215
|
+
hasUpdate: false,
|
|
216
|
+
stars: undefined,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return all;
|
|
221
|
+
}, [installedFromDisk]);
|
|
222
|
+
|
|
223
|
+
// Build list: installed first, then recommended, then search/popular
|
|
160
224
|
const allItems = useMemo((): SkillListItem[] => {
|
|
161
225
|
const query = skillsState.searchQuery.toLowerCase();
|
|
162
226
|
const items: SkillListItem[] = [];
|
|
163
227
|
|
|
228
|
+
// ── INSTALLED: always shown at top (if any) ──
|
|
229
|
+
const installedFiltered = query
|
|
230
|
+
? installedSkills.filter((s) => s.name.toLowerCase().includes(query))
|
|
231
|
+
: installedSkills;
|
|
232
|
+
|
|
233
|
+
if (installedFiltered.length > 0) {
|
|
234
|
+
items.push({
|
|
235
|
+
id: "cat:installed",
|
|
236
|
+
type: "category",
|
|
237
|
+
label: `Installed (${installedFiltered.length})`,
|
|
238
|
+
categoryKey: "installed",
|
|
239
|
+
});
|
|
240
|
+
for (const skill of installedFiltered) {
|
|
241
|
+
items.push({
|
|
242
|
+
id: `skill:${skill.id}`,
|
|
243
|
+
type: "skill",
|
|
244
|
+
label: skill.name,
|
|
245
|
+
skill,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
164
250
|
// ── RECOMMENDED: always shown, filtered when searching ──
|
|
165
251
|
const filteredRec = query
|
|
166
252
|
? mergedRecommended.filter(
|
|
@@ -393,6 +479,16 @@ export function SkillsScreen() {
|
|
|
393
479
|
handleUninstall();
|
|
394
480
|
} else if (event.name === "r") {
|
|
395
481
|
fetchData();
|
|
482
|
+
} else if (event.name === "o" && selectedSkill) {
|
|
483
|
+
// Open in browser
|
|
484
|
+
const repo = selectedSkill.source.repo;
|
|
485
|
+
const repoPath = selectedSkill.repoPath?.replace("/SKILL.md", "") || "";
|
|
486
|
+
if (repo && repo !== "local") {
|
|
487
|
+
const url = `https://github.com/${repo}/tree/main/${repoPath}`;
|
|
488
|
+
import("node:child_process").then(({ execSync: exec }) => {
|
|
489
|
+
try { exec(`open "${url}"`); } catch { /* ignore */ }
|
|
490
|
+
});
|
|
491
|
+
}
|
|
396
492
|
}
|
|
397
493
|
// "/" to enter search mode
|
|
398
494
|
else if (event.name === "/") {
|
|
@@ -407,8 +503,9 @@ export function SkillsScreen() {
|
|
|
407
503
|
) => {
|
|
408
504
|
if (item.type === "category") {
|
|
409
505
|
const isRec = item.categoryKey === "recommended";
|
|
410
|
-
const
|
|
411
|
-
const
|
|
506
|
+
const isInstalled = item.categoryKey === "installed";
|
|
507
|
+
const bgColor = isInstalled ? "#7e57c2" : isRec ? "#2e7d32" : "#00695c";
|
|
508
|
+
const star = isRec ? "★ " : isInstalled ? "● " : "";
|
|
412
509
|
|
|
413
510
|
if (isSelected) {
|
|
414
511
|
return (
|
|
@@ -432,20 +529,22 @@ export function SkillsScreen() {
|
|
|
432
529
|
const nameColor = skill.installed ? "white" : "gray";
|
|
433
530
|
|
|
434
531
|
if (isSelected) {
|
|
435
|
-
const scopeStr = `[${hasUser ? "u" : "."}${hasProject ? "p" : "."}]`;
|
|
436
532
|
return (
|
|
437
533
|
<text bg="magenta" fg="white">
|
|
438
|
-
{" "}
|
|
534
|
+
{" "}
|
|
535
|
+
<span>{hasUser ? "■" : "□"}</span>
|
|
536
|
+
<span>{hasProject ? "■" : "□"}</span>
|
|
537
|
+
{" "}{skill.name}{skill.hasUpdate ? " ⬆" : ""}{starsStr ? ` ${starsStr}` : ""}{" "}
|
|
439
538
|
</text>
|
|
440
539
|
);
|
|
441
540
|
}
|
|
442
541
|
|
|
443
542
|
return (
|
|
444
543
|
<text>
|
|
445
|
-
<span
|
|
446
|
-
<span fg={hasUser ? "cyan" : "#
|
|
447
|
-
<span fg={hasProject ? "green" : "#
|
|
448
|
-
<span
|
|
544
|
+
<span> </span>
|
|
545
|
+
<span fg={hasUser ? "cyan" : "#333333"}>■</span>
|
|
546
|
+
<span fg={hasProject ? "green" : "#333333"}>■</span>
|
|
547
|
+
<span> </span>
|
|
449
548
|
<span fg={nameColor}>{skill.name}</span>
|
|
450
549
|
{skill.hasUpdate && <span fg="yellow"> ⬆</span>}
|
|
451
550
|
{starsStr && (
|
|
@@ -684,7 +783,7 @@ export function SkillsScreen() {
|
|
|
684
783
|
}
|
|
685
784
|
footerHints={isSearchActive
|
|
686
785
|
? "type to filter │ Enter:done │ Esc:clear"
|
|
687
|
-
: "u:user │ p:project │
|
|
786
|
+
: "u:user │ p:project │ o:open │ /:search"
|
|
688
787
|
}
|
|
689
788
|
listPanel={
|
|
690
789
|
<box flexDirection="column">
|