claudeup 4.5.2 → 4.5.3
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/renderers/pluginRenderers.js +4 -4
- package/src/ui/renderers/pluginRenderers.tsx +4 -4
- package/src/ui/screens/PluginsScreen.js +1 -1
- package/src/ui/screens/PluginsScreen.tsx +1 -1
- package/src/ui/screens/SkillsScreen.js +67 -40
- package/src/ui/screens/SkillsScreen.tsx +60 -37
package/package.json
CHANGED
|
@@ -50,12 +50,12 @@ function pluginRow(item, isSelected) {
|
|
|
50
50
|
const hasProject = !!plugin.projectScope?.enabled;
|
|
51
51
|
const hasLocal = !!plugin.localScope?.enabled;
|
|
52
52
|
const hasAnyScope = hasUser || hasProject || hasLocal;
|
|
53
|
-
// Build version string
|
|
53
|
+
// Build version string — only show if plugin is installed in at least one scope
|
|
54
54
|
let versionStr = "";
|
|
55
55
|
if (plugin.isOrphaned) {
|
|
56
56
|
versionStr = " deprecated";
|
|
57
57
|
}
|
|
58
|
-
else if (plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
|
|
58
|
+
else if (hasAnyScope && plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
|
|
59
59
|
versionStr = ` v${plugin.installedVersion}`;
|
|
60
60
|
if (plugin.hasUpdate && plugin.version) {
|
|
61
61
|
versionStr += ` → v${plugin.version}`;
|
|
@@ -98,8 +98,8 @@ function pluginDetail(item) {
|
|
|
98
98
|
components.push(`${Object.keys(plugin.lspServers).length} LSP`);
|
|
99
99
|
}
|
|
100
100
|
const showVersion = plugin.version && plugin.version !== "0.0.0";
|
|
101
|
-
const showInstalledVersion = plugin.installedVersion && plugin.installedVersion !== "0.0.0";
|
|
102
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: _jsxs("strong", { children: [" ", plugin.name, plugin.hasUpdate ? " ⬆" : "", " "] }) }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: isInstalled ? theme.colors.success : theme.colors.muted, children: isInstalled ? "● Installed" : "○ Not installed" }) }), _jsx("box", { marginTop: 1, marginBottom: 1, children: _jsx("text", { fg: theme.colors.text, children: plugin.description }) }), showVersion ? (_jsx(KeyValueLine, { label: "Version", value: _jsxs("span", { children: [_jsxs("span", { fg: theme.colors.link, children: ["v", plugin.version] }), showInstalledVersion && plugin.installedVersion !== plugin.version ? (_jsxs("span", { children: [" (v", plugin.installedVersion, " installed)"] })) : null] }) })) : null, plugin.category ? (_jsx(KeyValueLine, { label: "Category", value: _jsx("span", { fg: theme.colors.accent, children: plugin.category }) })) : null, plugin.author ? (_jsx(KeyValueLine, { label: "Author", value: _jsx("span", { children: plugin.author.name }) })) : null, components.length > 0 ? (_jsx(KeyValueLine, { label: "Contains", value: _jsx("span", { fg: theme.colors.warning, children: components.join(" · ") }) })) : null, _jsxs(DetailSection, { children: [_jsx("text", { children: "─".repeat(24) }), _jsx("text", { children: _jsx("strong", { children: "Scopes:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsxs("span", { bg: theme.scopes.user, fg: "black", children: [" ", "u", " "] }), _jsx("span", { fg: plugin.userScope?.enabled ? theme.scopes.user : theme.colors.muted, children: plugin.userScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: theme.scopes.user, children: "User" }), _jsx("span", { children: " global" }), plugin.userScope?.version ? (_jsxs("span", { fg: theme.scopes.user, children: [" v", plugin.userScope.version] })) : null] }), _jsxs("text", { children: [_jsxs("span", { bg: theme.scopes.project, fg: "black", children: [" ", "p", " "] }), _jsx("span", { fg: plugin.projectScope?.enabled ? theme.scopes.project : theme.colors.muted, children: plugin.projectScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: theme.scopes.project, children: "Project" }), _jsx("span", { children: " team" }), plugin.projectScope?.version ? (_jsxs("span", { fg: theme.scopes.project, children: [" v", plugin.projectScope.version] })) : null] }), _jsxs("text", { children: [_jsxs("span", { bg: theme.scopes.local, fg: "black", children: [" ", "l", " "] }), _jsx("span", { fg: plugin.localScope?.enabled ? theme.scopes.local : theme.colors.muted, children: plugin.localScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: theme.scopes.local, children: "Local" }), _jsx("span", { children: " private" }), plugin.localScope?.version ? (_jsxs("span", { fg: theme.scopes.local, children: [" v", plugin.localScope.version] })) : null] })] })] }), isInstalled && plugin.hasUpdate ? (_jsx(ActionHints, { hints: [{ key: "U", label: `Update to v${plugin.version}`, tone: "primary" }] })) : null] }));
|
|
101
|
+
const showInstalledVersion = isInstalled && plugin.installedVersion && plugin.installedVersion !== "0.0.0";
|
|
102
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: _jsxs("strong", { children: [" ", plugin.name, isInstalled && plugin.hasUpdate ? " ⬆" : "", " "] }) }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: isInstalled ? theme.colors.success : theme.colors.muted, children: isInstalled ? "● Installed" : "○ Not installed" }) }), _jsx("box", { marginTop: 1, marginBottom: 1, children: _jsx("text", { fg: theme.colors.text, children: plugin.description }) }), showVersion ? (_jsx(KeyValueLine, { label: "Version", value: _jsxs("span", { children: [_jsxs("span", { fg: theme.colors.link, children: ["v", plugin.version] }), showInstalledVersion && plugin.installedVersion !== plugin.version ? (_jsxs("span", { children: [" (v", plugin.installedVersion, " installed)"] })) : null] }) })) : null, plugin.category ? (_jsx(KeyValueLine, { label: "Category", value: _jsx("span", { fg: theme.colors.accent, children: plugin.category }) })) : null, plugin.author ? (_jsx(KeyValueLine, { label: "Author", value: _jsx("span", { children: plugin.author.name }) })) : null, components.length > 0 ? (_jsx(KeyValueLine, { label: "Contains", value: _jsx("span", { fg: theme.colors.warning, children: components.join(" · ") }) })) : null, _jsxs(DetailSection, { children: [_jsx("text", { children: "─".repeat(24) }), _jsx("text", { children: _jsx("strong", { children: "Scopes:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsxs("span", { bg: theme.scopes.user, fg: "black", children: [" ", "u", " "] }), _jsx("span", { fg: plugin.userScope?.enabled ? theme.scopes.user : theme.colors.muted, children: plugin.userScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: theme.scopes.user, children: "User" }), _jsx("span", { children: " global" }), plugin.userScope?.version ? (_jsxs("span", { fg: theme.scopes.user, children: [" v", plugin.userScope.version] })) : null] }), _jsxs("text", { children: [_jsxs("span", { bg: theme.scopes.project, fg: "black", children: [" ", "p", " "] }), _jsx("span", { fg: plugin.projectScope?.enabled ? theme.scopes.project : theme.colors.muted, children: plugin.projectScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: theme.scopes.project, children: "Project" }), _jsx("span", { children: " team" }), plugin.projectScope?.version ? (_jsxs("span", { fg: theme.scopes.project, children: [" v", plugin.projectScope.version] })) : null] }), _jsxs("text", { children: [_jsxs("span", { bg: theme.scopes.local, fg: "black", children: [" ", "l", " "] }), _jsx("span", { fg: plugin.localScope?.enabled ? theme.scopes.local : theme.colors.muted, children: plugin.localScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: theme.scopes.local, children: "Local" }), _jsx("span", { children: " private" }), plugin.localScope?.version ? (_jsxs("span", { fg: theme.scopes.local, children: [" v", plugin.localScope.version] })) : null] })] })] }), isInstalled && plugin.hasUpdate ? (_jsx(ActionHints, { hints: [{ key: "U", label: `Update to v${plugin.version}`, tone: "primary" }] })) : null] }));
|
|
103
103
|
}
|
|
104
104
|
// ─── Public dispatch functions ────────────────────────────────────────────────
|
|
105
105
|
/**
|
|
@@ -113,11 +113,11 @@ function pluginRow(item: PluginPluginItem, isSelected: boolean): React.ReactNode
|
|
|
113
113
|
const hasLocal = !!plugin.localScope?.enabled;
|
|
114
114
|
const hasAnyScope = hasUser || hasProject || hasLocal;
|
|
115
115
|
|
|
116
|
-
// Build version string
|
|
116
|
+
// Build version string — only show if plugin is installed in at least one scope
|
|
117
117
|
let versionStr = "";
|
|
118
118
|
if (plugin.isOrphaned) {
|
|
119
119
|
versionStr = " deprecated";
|
|
120
|
-
} else if (plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
|
|
120
|
+
} else if (hasAnyScope && plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
|
|
121
121
|
versionStr = ` v${plugin.installedVersion}`;
|
|
122
122
|
if (plugin.hasUpdate && plugin.version) {
|
|
123
123
|
versionStr += ` → v${plugin.version}`;
|
|
@@ -210,7 +210,7 @@ function pluginDetail(item: PluginPluginItem): React.ReactNode {
|
|
|
210
210
|
|
|
211
211
|
const showVersion = plugin.version && plugin.version !== "0.0.0";
|
|
212
212
|
const showInstalledVersion =
|
|
213
|
-
plugin.installedVersion && plugin.installedVersion !== "0.0.0";
|
|
213
|
+
isInstalled && plugin.installedVersion && plugin.installedVersion !== "0.0.0";
|
|
214
214
|
|
|
215
215
|
return (
|
|
216
216
|
<box flexDirection="column">
|
|
@@ -220,7 +220,7 @@ function pluginDetail(item: PluginPluginItem): React.ReactNode {
|
|
|
220
220
|
<strong>
|
|
221
221
|
{" "}
|
|
222
222
|
{plugin.name}
|
|
223
|
-
{plugin.hasUpdate ? " ⬆" : ""}{" "}
|
|
223
|
+
{isInstalled && plugin.hasUpdate ? " ⬆" : ""}{" "}
|
|
224
224
|
</strong>
|
|
225
225
|
</text>
|
|
226
226
|
</box>
|
|
@@ -643,7 +643,7 @@ export function PluginsScreen() {
|
|
|
643
643
|
const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
|
|
644
644
|
const plugins = pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
|
|
645
645
|
const installedCount = plugins.filter((p) => p.enabled).length;
|
|
646
|
-
const updateCount = plugins.filter((p) => p.hasUpdate).length;
|
|
646
|
+
const updateCount = plugins.filter((p) => p.hasUpdate && (p.userScope?.enabled || p.projectScope?.enabled || p.localScope?.enabled)).length;
|
|
647
647
|
const subtitle = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} updates` : ""}`;
|
|
648
648
|
const searchPlaceholder = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} ⬆` : ""} │ / to search`;
|
|
649
649
|
return (_jsx(ScreenLayout, { title: "claudeup Plugins", subtitle: subtitle, currentScreen: "plugins", search: {
|
|
@@ -799,7 +799,7 @@ export function PluginsScreen() {
|
|
|
799
799
|
const plugins: PluginInfo[] =
|
|
800
800
|
pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
|
|
801
801
|
const installedCount = plugins.filter((p) => p.enabled).length;
|
|
802
|
-
const updateCount = plugins.filter((p) => p.hasUpdate).length;
|
|
802
|
+
const updateCount = plugins.filter((p) => p.hasUpdate && (p.userScope?.enabled || p.projectScope?.enabled || p.localScope?.enabled)).length;
|
|
803
803
|
const subtitle = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} updates` : ""}`;
|
|
804
804
|
const searchPlaceholder = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} ⬆` : ""} │ / to search`;
|
|
805
805
|
|
|
@@ -98,28 +98,28 @@ export function SkillsScreen() {
|
|
|
98
98
|
}, [skillsState.searchQuery]);
|
|
99
99
|
// ── Disk scan for installed skills ────────────────────────────────────────
|
|
100
100
|
const [installedFromDisk, setInstalledFromDisk] = useState({ user: new Set(), project: new Set() });
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
set.add(e);
|
|
114
|
-
}
|
|
101
|
+
const scanDisk = useCallback(async () => {
|
|
102
|
+
const user = new Set();
|
|
103
|
+
const project = new Set();
|
|
104
|
+
const userDir = path.join(os.homedir(), ".claude", "skills");
|
|
105
|
+
const projDir = path.join(state.projectPath || process.cwd(), ".claude", "skills");
|
|
106
|
+
for (const [dir, set] of [[userDir, user], [projDir, project]]) {
|
|
107
|
+
try {
|
|
108
|
+
if (await fs.pathExists(dir)) {
|
|
109
|
+
const entries = await fs.readdir(dir);
|
|
110
|
+
for (const e of entries) {
|
|
111
|
+
if (await fs.pathExists(path.join(dir, e, "SKILL.md")))
|
|
112
|
+
set.add(e);
|
|
115
113
|
}
|
|
116
114
|
}
|
|
117
|
-
catch { /* ignore */ }
|
|
118
115
|
}
|
|
119
|
-
|
|
116
|
+
catch { /* ignore */ }
|
|
120
117
|
}
|
|
118
|
+
setInstalledFromDisk({ user, project });
|
|
119
|
+
}, [state.projectPath]);
|
|
120
|
+
useEffect(() => {
|
|
121
121
|
scanDisk();
|
|
122
|
-
}, [
|
|
122
|
+
}, [scanDisk, state.dataRefreshVersion]);
|
|
123
123
|
// ── Derived data ──────────────────────────────────────────────────────────
|
|
124
124
|
const staticRecommended = useMemo(() => {
|
|
125
125
|
return RECOMMENDED_SKILLS.map((r) => {
|
|
@@ -150,8 +150,15 @@ export function SkillsScreen() {
|
|
|
150
150
|
const match = fetched.find((f) => f.source.repo === staticSkill.source.repo && f.name === staticSkill.name);
|
|
151
151
|
if (!match)
|
|
152
152
|
return staticSkill;
|
|
153
|
-
// Merge: prefer fetched data
|
|
154
|
-
|
|
153
|
+
// Merge: prefer fetched data for description/stars, but disk scan is
|
|
154
|
+
// authoritative for installed state (staticSkill reflects current disk)
|
|
155
|
+
return {
|
|
156
|
+
...staticSkill,
|
|
157
|
+
...match,
|
|
158
|
+
installed: staticSkill.installed,
|
|
159
|
+
installedScope: staticSkill.installedScope,
|
|
160
|
+
stars: match.stars || staticSkill.stars,
|
|
161
|
+
};
|
|
155
162
|
});
|
|
156
163
|
}, [staticRecommended, skillsState.skills]);
|
|
157
164
|
const installedSkills = useMemo(() => {
|
|
@@ -203,6 +210,15 @@ export function SkillsScreen() {
|
|
|
203
210
|
skillsState.searchQuery,
|
|
204
211
|
isSearchLoading,
|
|
205
212
|
]);
|
|
213
|
+
// Auto-correct selection if it lands on a category header (e.g. initial load, reset)
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
const item = allItems[skillsState.selectedIndex];
|
|
216
|
+
if (item && item.kind === "category") {
|
|
217
|
+
const firstSkill = allItems.findIndex((i) => i.kind === "skill");
|
|
218
|
+
if (firstSkill >= 0)
|
|
219
|
+
dispatch({ type: "SKILLS_SELECT", index: firstSkill });
|
|
220
|
+
}
|
|
221
|
+
}, [allItems, skillsState.selectedIndex, dispatch]);
|
|
206
222
|
const selectedItem = allItems[skillsState.selectedIndex];
|
|
207
223
|
const selectedSkill = selectedItem?.kind === "skill" ? selectedItem.skill : undefined;
|
|
208
224
|
// ── Lazy-load frontmatter for selected skill ───────────────────────────────
|
|
@@ -232,13 +248,13 @@ export function SkillsScreen() {
|
|
|
232
248
|
return;
|
|
233
249
|
try {
|
|
234
250
|
await installSkill(selectedSkill, scope, state.projectPath);
|
|
235
|
-
await
|
|
251
|
+
await scanDisk(); // Re-scan disk only — no network re-fetch needed
|
|
236
252
|
showStatus(`Installed ${selectedSkill.name} to ${scope}`);
|
|
237
253
|
}
|
|
238
254
|
catch (error) {
|
|
239
255
|
showStatus(`Failed: ${error}`, "error");
|
|
240
256
|
}
|
|
241
|
-
}, [selectedSkill, state.projectPath,
|
|
257
|
+
}, [selectedSkill, state.projectPath, scanDisk, showStatus]);
|
|
242
258
|
const handleUninstall = useCallback(async () => {
|
|
243
259
|
if (!selectedSkill || !selectedSkill.installed)
|
|
244
260
|
return;
|
|
@@ -247,13 +263,13 @@ export function SkillsScreen() {
|
|
|
247
263
|
return;
|
|
248
264
|
try {
|
|
249
265
|
await uninstallSkill(selectedSkill.name, scope, state.projectPath);
|
|
250
|
-
await
|
|
266
|
+
await scanDisk(); // Re-scan disk only — no network re-fetch needed
|
|
251
267
|
showStatus(`Removed ${selectedSkill.name} from ${scope}`);
|
|
252
268
|
}
|
|
253
269
|
catch (error) {
|
|
254
270
|
showStatus(`Failed: ${error}`, "error");
|
|
255
271
|
}
|
|
256
|
-
}, [selectedSkill, state.projectPath,
|
|
272
|
+
}, [selectedSkill, state.projectPath, scanDisk, showStatus]);
|
|
257
273
|
// ── Keyboard handling ─────────────────────────────────────────────────────
|
|
258
274
|
useKeyboard((event) => {
|
|
259
275
|
if (state.modal)
|
|
@@ -280,15 +296,23 @@ export function SkillsScreen() {
|
|
|
280
296
|
if (event.name === "up" || event.name === "k") {
|
|
281
297
|
if (isSearchActive)
|
|
282
298
|
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
283
|
-
|
|
284
|
-
|
|
299
|
+
let newIndex = skillsState.selectedIndex - 1;
|
|
300
|
+
// Skip category headers
|
|
301
|
+
while (newIndex >= 0 && allItems[newIndex]?.kind === "category")
|
|
302
|
+
newIndex--;
|
|
303
|
+
if (newIndex >= 0)
|
|
304
|
+
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
285
305
|
return;
|
|
286
306
|
}
|
|
287
307
|
if (event.name === "down" || event.name === "j") {
|
|
288
308
|
if (isSearchActive)
|
|
289
309
|
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
290
|
-
|
|
291
|
-
|
|
310
|
+
let newIndex = skillsState.selectedIndex + 1;
|
|
311
|
+
// Skip category headers
|
|
312
|
+
while (newIndex < allItems.length && allItems[newIndex]?.kind === "category")
|
|
313
|
+
newIndex++;
|
|
314
|
+
if (newIndex < allItems.length)
|
|
315
|
+
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
292
316
|
return;
|
|
293
317
|
}
|
|
294
318
|
if (event.name === "return" || event.name === "enter") {
|
|
@@ -301,6 +325,21 @@ export function SkillsScreen() {
|
|
|
301
325
|
}
|
|
302
326
|
return;
|
|
303
327
|
}
|
|
328
|
+
// Action keys work regardless of search mode when a skill is selected
|
|
329
|
+
if (event.name === "u" && selectedSkill) {
|
|
330
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "user")
|
|
331
|
+
handleUninstall();
|
|
332
|
+
else
|
|
333
|
+
handleInstall("user");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
else if (event.name === "p" && selectedSkill) {
|
|
337
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "project")
|
|
338
|
+
handleUninstall();
|
|
339
|
+
else
|
|
340
|
+
handleInstall("project");
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
304
343
|
if (isSearchActive) {
|
|
305
344
|
if (event.name === "k" || event.name === "j") {
|
|
306
345
|
const delta = event.name === "k" ? -1 : 1;
|
|
@@ -314,19 +353,7 @@ export function SkillsScreen() {
|
|
|
314
353
|
}
|
|
315
354
|
return;
|
|
316
355
|
}
|
|
317
|
-
if (event.name === "
|
|
318
|
-
if (selectedSkill.installed && selectedSkill.installedScope === "user")
|
|
319
|
-
handleUninstall();
|
|
320
|
-
else
|
|
321
|
-
handleInstall("user");
|
|
322
|
-
}
|
|
323
|
-
else if (event.name === "p" && selectedSkill) {
|
|
324
|
-
if (selectedSkill.installed && selectedSkill.installedScope === "project")
|
|
325
|
-
handleUninstall();
|
|
326
|
-
else
|
|
327
|
-
handleInstall("project");
|
|
328
|
-
}
|
|
329
|
-
else if (event.name === "r") {
|
|
356
|
+
if (event.name === "r") {
|
|
330
357
|
fetchData();
|
|
331
358
|
}
|
|
332
359
|
else if (event.name === "o" && selectedSkill) {
|
|
@@ -121,26 +121,27 @@ export function SkillsScreen() {
|
|
|
121
121
|
project: Set<string>;
|
|
122
122
|
}>({ user: new Set(), project: new Set() });
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (await fs.pathExists(path.join(dir, e, "SKILL.md"))) set.add(e);
|
|
136
|
-
}
|
|
124
|
+
const scanDisk = useCallback(async () => {
|
|
125
|
+
const user = new Set<string>();
|
|
126
|
+
const project = new Set<string>();
|
|
127
|
+
const userDir = path.join(os.homedir(), ".claude", "skills");
|
|
128
|
+
const projDir = path.join(state.projectPath || process.cwd(), ".claude", "skills");
|
|
129
|
+
for (const [dir, set] of [[userDir, user], [projDir, project]] as const) {
|
|
130
|
+
try {
|
|
131
|
+
if (await fs.pathExists(dir)) {
|
|
132
|
+
const entries = await fs.readdir(dir);
|
|
133
|
+
for (const e of entries) {
|
|
134
|
+
if (await fs.pathExists(path.join(dir, e, "SKILL.md"))) set.add(e);
|
|
137
135
|
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
setInstalledFromDisk({ user, project });
|
|
136
|
+
}
|
|
137
|
+
} catch { /* ignore */ }
|
|
141
138
|
}
|
|
139
|
+
setInstalledFromDisk({ user, project });
|
|
140
|
+
}, [state.projectPath]);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
142
143
|
scanDisk();
|
|
143
|
-
}, [
|
|
144
|
+
}, [scanDisk, state.dataRefreshVersion]);
|
|
144
145
|
|
|
145
146
|
// ── Derived data ──────────────────────────────────────────────────────────
|
|
146
147
|
|
|
@@ -174,8 +175,15 @@ export function SkillsScreen() {
|
|
|
174
175
|
(f) => f.source.repo === staticSkill.source.repo && f.name === staticSkill.name,
|
|
175
176
|
);
|
|
176
177
|
if (!match) return staticSkill;
|
|
177
|
-
// Merge: prefer fetched data
|
|
178
|
-
|
|
178
|
+
// Merge: prefer fetched data for description/stars, but disk scan is
|
|
179
|
+
// authoritative for installed state (staticSkill reflects current disk)
|
|
180
|
+
return {
|
|
181
|
+
...staticSkill,
|
|
182
|
+
...match,
|
|
183
|
+
installed: staticSkill.installed,
|
|
184
|
+
installedScope: staticSkill.installedScope,
|
|
185
|
+
stars: match.stars || staticSkill.stars,
|
|
186
|
+
};
|
|
179
187
|
});
|
|
180
188
|
}, [staticRecommended, skillsState.skills]);
|
|
181
189
|
|
|
@@ -234,6 +242,15 @@ export function SkillsScreen() {
|
|
|
234
242
|
],
|
|
235
243
|
);
|
|
236
244
|
|
|
245
|
+
// Auto-correct selection if it lands on a category header (e.g. initial load, reset)
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
const item = allItems[skillsState.selectedIndex];
|
|
248
|
+
if (item && item.kind === "category") {
|
|
249
|
+
const firstSkill = allItems.findIndex((i) => i.kind === "skill");
|
|
250
|
+
if (firstSkill >= 0) dispatch({ type: "SKILLS_SELECT", index: firstSkill });
|
|
251
|
+
}
|
|
252
|
+
}, [allItems, skillsState.selectedIndex, dispatch]);
|
|
253
|
+
|
|
237
254
|
const selectedItem = allItems[skillsState.selectedIndex];
|
|
238
255
|
const selectedSkill =
|
|
239
256
|
selectedItem?.kind === "skill" ? selectedItem.skill : undefined;
|
|
@@ -266,12 +283,12 @@ export function SkillsScreen() {
|
|
|
266
283
|
if (!selectedSkill) return;
|
|
267
284
|
try {
|
|
268
285
|
await installSkill(selectedSkill, scope, state.projectPath);
|
|
269
|
-
await
|
|
286
|
+
await scanDisk(); // Re-scan disk only — no network re-fetch needed
|
|
270
287
|
showStatus(`Installed ${selectedSkill.name} to ${scope}`);
|
|
271
288
|
} catch (error) {
|
|
272
289
|
showStatus(`Failed: ${error}`, "error");
|
|
273
290
|
}
|
|
274
|
-
}, [selectedSkill, state.projectPath,
|
|
291
|
+
}, [selectedSkill, state.projectPath, scanDisk, showStatus]);
|
|
275
292
|
|
|
276
293
|
const handleUninstall = useCallback(async () => {
|
|
277
294
|
if (!selectedSkill || !selectedSkill.installed) return;
|
|
@@ -280,12 +297,12 @@ export function SkillsScreen() {
|
|
|
280
297
|
|
|
281
298
|
try {
|
|
282
299
|
await uninstallSkill(selectedSkill.name, scope, state.projectPath);
|
|
283
|
-
await
|
|
300
|
+
await scanDisk(); // Re-scan disk only — no network re-fetch needed
|
|
284
301
|
showStatus(`Removed ${selectedSkill.name} from ${scope}`);
|
|
285
302
|
} catch (error) {
|
|
286
303
|
showStatus(`Failed: ${error}`, "error");
|
|
287
304
|
}
|
|
288
|
-
}, [selectedSkill, state.projectPath,
|
|
305
|
+
}, [selectedSkill, state.projectPath, scanDisk, showStatus]);
|
|
289
306
|
|
|
290
307
|
// ── Keyboard handling ─────────────────────────────────────────────────────
|
|
291
308
|
|
|
@@ -315,17 +332,18 @@ export function SkillsScreen() {
|
|
|
315
332
|
|
|
316
333
|
if (event.name === "up" || event.name === "k") {
|
|
317
334
|
if (isSearchActive) dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
318
|
-
|
|
319
|
-
|
|
335
|
+
let newIndex = skillsState.selectedIndex - 1;
|
|
336
|
+
// Skip category headers
|
|
337
|
+
while (newIndex >= 0 && allItems[newIndex]?.kind === "category") newIndex--;
|
|
338
|
+
if (newIndex >= 0) dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
320
339
|
return;
|
|
321
340
|
}
|
|
322
341
|
if (event.name === "down" || event.name === "j") {
|
|
323
342
|
if (isSearchActive) dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
);
|
|
328
|
-
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
343
|
+
let newIndex = skillsState.selectedIndex + 1;
|
|
344
|
+
// Skip category headers
|
|
345
|
+
while (newIndex < allItems.length && allItems[newIndex]?.kind === "category") newIndex++;
|
|
346
|
+
if (newIndex < allItems.length) dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
329
347
|
return;
|
|
330
348
|
}
|
|
331
349
|
|
|
@@ -340,6 +358,17 @@ export function SkillsScreen() {
|
|
|
340
358
|
return;
|
|
341
359
|
}
|
|
342
360
|
|
|
361
|
+
// Action keys work regardless of search mode when a skill is selected
|
|
362
|
+
if (event.name === "u" && selectedSkill) {
|
|
363
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "user") handleUninstall();
|
|
364
|
+
else handleInstall("user");
|
|
365
|
+
return;
|
|
366
|
+
} else if (event.name === "p" && selectedSkill) {
|
|
367
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "project") handleUninstall();
|
|
368
|
+
else handleInstall("project");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
343
372
|
if (isSearchActive) {
|
|
344
373
|
if (event.name === "k" || event.name === "j") {
|
|
345
374
|
const delta = event.name === "k" ? -1 : 1;
|
|
@@ -354,13 +383,7 @@ export function SkillsScreen() {
|
|
|
354
383
|
return;
|
|
355
384
|
}
|
|
356
385
|
|
|
357
|
-
if (event.name === "
|
|
358
|
-
if (selectedSkill.installed && selectedSkill.installedScope === "user") handleUninstall();
|
|
359
|
-
else handleInstall("user");
|
|
360
|
-
} else if (event.name === "p" && selectedSkill) {
|
|
361
|
-
if (selectedSkill.installed && selectedSkill.installedScope === "project") handleUninstall();
|
|
362
|
-
else handleInstall("project");
|
|
363
|
-
} else if (event.name === "r") {
|
|
386
|
+
if (event.name === "r") {
|
|
364
387
|
fetchData();
|
|
365
388
|
} else if (event.name === "o" && selectedSkill) {
|
|
366
389
|
const repo = selectedSkill.source.repo;
|