claudeup 3.11.0 → 3.13.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/services/skills-manager.js +17 -6
- package/src/services/skills-manager.ts +21 -6
- package/src/ui/components/ScopeIndicator.js +30 -0
- package/src/ui/components/ScopeIndicator.tsx +57 -0
- package/src/ui/screens/PluginsScreen.js +112 -41
- package/src/ui/screens/PluginsScreen.tsx +131 -45
- package/src/ui/screens/SkillsScreen.js +12 -23
- package/src/ui/screens/SkillsScreen.tsx +17 -27
package/package.json
CHANGED
|
@@ -164,9 +164,11 @@ export async function fetchPopularSkills(limit = 30) {
|
|
|
164
164
|
export async function fetchAvailableSkills(_repos, projectPath) {
|
|
165
165
|
const userInstalled = await getInstalledSkillNames("user");
|
|
166
166
|
const projectInstalled = await getInstalledSkillNames("project", projectPath);
|
|
167
|
+
const slugify = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
167
168
|
const markInstalled = (skill) => {
|
|
168
|
-
const
|
|
169
|
-
const
|
|
169
|
+
const slug = slugify(skill.name);
|
|
170
|
+
const isUserInstalled = userInstalled.has(slug) || userInstalled.has(skill.name);
|
|
171
|
+
const isProjInstalled = projectInstalled.has(slug) || projectInstalled.has(skill.name);
|
|
170
172
|
const installed = isUserInstalled || isProjInstalled;
|
|
171
173
|
const installedScope = isProjInstalled
|
|
172
174
|
? "project"
|
|
@@ -256,16 +258,25 @@ export async function installSkill(skill, scope, projectPath) {
|
|
|
256
258
|
if (!content) {
|
|
257
259
|
throw new Error(`Failed to fetch skill: SKILL.md not found in ${repo}/${repoPath}`);
|
|
258
260
|
}
|
|
261
|
+
// Use slug for directory name — display names can have slashes/spaces
|
|
262
|
+
const dirName = skill.name
|
|
263
|
+
.toLowerCase()
|
|
264
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
265
|
+
.replace(/^-|-$/g, "");
|
|
259
266
|
const installDir = scope === "user"
|
|
260
|
-
? path.join(getUserSkillsDir(),
|
|
261
|
-
: path.join(getProjectSkillsDir(projectPath),
|
|
267
|
+
? path.join(getUserSkillsDir(), dirName)
|
|
268
|
+
: path.join(getProjectSkillsDir(projectPath), dirName);
|
|
262
269
|
await fs.ensureDir(installDir);
|
|
263
270
|
await fs.writeFile(path.join(installDir, "SKILL.md"), content, "utf8");
|
|
264
271
|
}
|
|
265
272
|
export async function uninstallSkill(skillName, scope, projectPath) {
|
|
273
|
+
const dirName = skillName
|
|
274
|
+
.toLowerCase()
|
|
275
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
276
|
+
.replace(/^-|-$/g, "");
|
|
266
277
|
const installDir = scope === "user"
|
|
267
|
-
? path.join(getUserSkillsDir(),
|
|
268
|
-
: path.join(getProjectSkillsDir(projectPath),
|
|
278
|
+
? path.join(getUserSkillsDir(), dirName)
|
|
279
|
+
: path.join(getProjectSkillsDir(projectPath), dirName);
|
|
269
280
|
const skillMdPath = path.join(installDir, "SKILL.md");
|
|
270
281
|
if (await fs.pathExists(skillMdPath)) {
|
|
271
282
|
await fs.remove(skillMdPath);
|
|
@@ -228,9 +228,13 @@ export async function fetchAvailableSkills(
|
|
|
228
228
|
const userInstalled = await getInstalledSkillNames("user");
|
|
229
229
|
const projectInstalled = await getInstalledSkillNames("project", projectPath);
|
|
230
230
|
|
|
231
|
+
const slugify = (name: string) =>
|
|
232
|
+
name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
233
|
+
|
|
231
234
|
const markInstalled = (skill: SkillInfo): SkillInfo => {
|
|
232
|
-
const
|
|
233
|
-
const
|
|
235
|
+
const slug = slugify(skill.name);
|
|
236
|
+
const isUserInstalled = userInstalled.has(slug) || userInstalled.has(skill.name);
|
|
237
|
+
const isProjInstalled = projectInstalled.has(slug) || projectInstalled.has(skill.name);
|
|
234
238
|
const installed = isUserInstalled || isProjInstalled;
|
|
235
239
|
const installedScope: "user" | "project" | null = isProjInstalled
|
|
236
240
|
? "project"
|
|
@@ -334,10 +338,16 @@ export async function installSkill(
|
|
|
334
338
|
);
|
|
335
339
|
}
|
|
336
340
|
|
|
341
|
+
// Use slug for directory name — display names can have slashes/spaces
|
|
342
|
+
const dirName = skill.name
|
|
343
|
+
.toLowerCase()
|
|
344
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
345
|
+
.replace(/^-|-$/g, "");
|
|
346
|
+
|
|
337
347
|
const installDir =
|
|
338
348
|
scope === "user"
|
|
339
|
-
? path.join(getUserSkillsDir(),
|
|
340
|
-
: path.join(getProjectSkillsDir(projectPath),
|
|
349
|
+
? path.join(getUserSkillsDir(), dirName)
|
|
350
|
+
: path.join(getProjectSkillsDir(projectPath), dirName);
|
|
341
351
|
|
|
342
352
|
await fs.ensureDir(installDir);
|
|
343
353
|
await fs.writeFile(path.join(installDir, "SKILL.md"), content, "utf8");
|
|
@@ -348,10 +358,15 @@ export async function uninstallSkill(
|
|
|
348
358
|
scope: "user" | "project",
|
|
349
359
|
projectPath?: string,
|
|
350
360
|
): Promise<void> {
|
|
361
|
+
const dirName = skillName
|
|
362
|
+
.toLowerCase()
|
|
363
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
364
|
+
.replace(/^-|-$/g, "");
|
|
365
|
+
|
|
351
366
|
const installDir =
|
|
352
367
|
scope === "user"
|
|
353
|
-
? path.join(getUserSkillsDir(),
|
|
354
|
-
: path.join(getProjectSkillsDir(projectPath),
|
|
368
|
+
? path.join(getUserSkillsDir(), dirName)
|
|
369
|
+
: path.join(getProjectSkillsDir(projectPath), dirName);
|
|
355
370
|
|
|
356
371
|
const skillMdPath = path.join(installDir, "SKILL.md");
|
|
357
372
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Scope indicator for list items.
|
|
4
|
+
* Shows colored letter badges when installed, empty space when not.
|
|
5
|
+
*
|
|
6
|
+
* Installed in user+project: u p
|
|
7
|
+
* Installed in project only: p
|
|
8
|
+
* Not installed: (3 spaces)
|
|
9
|
+
*
|
|
10
|
+
* Colors match the detail panel: cyan=user, green=project, yellow=local
|
|
11
|
+
*/
|
|
12
|
+
export function scopeIndicatorText(user, project, local) {
|
|
13
|
+
const segments = [
|
|
14
|
+
user
|
|
15
|
+
? { char: "u", bg: "cyan", fg: "black" }
|
|
16
|
+
: { char: " ", bg: "", fg: "" },
|
|
17
|
+
project
|
|
18
|
+
? { char: "p", bg: "green", fg: "black" }
|
|
19
|
+
: { char: " ", bg: "", fg: "" },
|
|
20
|
+
local
|
|
21
|
+
? { char: "l", bg: "yellow", fg: "black" }
|
|
22
|
+
: { char: " ", bg: "", fg: "" },
|
|
23
|
+
];
|
|
24
|
+
const text = segments.map((s) => s.char).join("");
|
|
25
|
+
return { text, segments };
|
|
26
|
+
}
|
|
27
|
+
export function ScopeIndicator({ user, project, local }) {
|
|
28
|
+
const { segments } = scopeIndicatorText(user, project, local);
|
|
29
|
+
return (_jsx("text", { children: segments.map((s, i) => s.bg ? (_jsx("span", { bg: s.bg, fg: s.fg, children: s.char }, i)) : (_jsx("span", { children: " " }, i))) }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scope indicator for list items.
|
|
5
|
+
* Shows colored letter badges when installed, empty space when not.
|
|
6
|
+
*
|
|
7
|
+
* Installed in user+project: u p
|
|
8
|
+
* Installed in project only: p
|
|
9
|
+
* Not installed: (3 spaces)
|
|
10
|
+
*
|
|
11
|
+
* Colors match the detail panel: cyan=user, green=project, yellow=local
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export function scopeIndicatorText(
|
|
15
|
+
user?: boolean,
|
|
16
|
+
project?: boolean,
|
|
17
|
+
local?: boolean,
|
|
18
|
+
): { text: string; segments: Array<{ char: string; bg: string; fg: string }> } {
|
|
19
|
+
const segments: Array<{ char: string; bg: string; fg: string }> = [
|
|
20
|
+
user
|
|
21
|
+
? { char: "u", bg: "cyan", fg: "black" }
|
|
22
|
+
: { char: " ", bg: "", fg: "" },
|
|
23
|
+
project
|
|
24
|
+
? { char: "p", bg: "green", fg: "black" }
|
|
25
|
+
: { char: " ", bg: "", fg: "" },
|
|
26
|
+
local
|
|
27
|
+
? { char: "l", bg: "yellow", fg: "black" }
|
|
28
|
+
: { char: " ", bg: "", fg: "" },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const text = segments.map((s) => s.char).join("");
|
|
32
|
+
return { text, segments };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ScopeIndicatorProps {
|
|
36
|
+
user?: boolean;
|
|
37
|
+
project?: boolean;
|
|
38
|
+
local?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ScopeIndicator({ user, project, local }: ScopeIndicatorProps) {
|
|
42
|
+
const { segments } = scopeIndicatorText(user, project, local);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<text>
|
|
46
|
+
{segments.map((s, i) =>
|
|
47
|
+
s.bg ? (
|
|
48
|
+
<span key={i} bg={s.bg} fg={s.fg}>
|
|
49
|
+
{s.char}
|
|
50
|
+
</span>
|
|
51
|
+
) : (
|
|
52
|
+
<span key={i}> </span>
|
|
53
|
+
),
|
|
54
|
+
)}
|
|
55
|
+
</text>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -16,6 +16,10 @@ import { saveInstalledPluginVersion } from "../../services/plugin-manager.js";
|
|
|
16
16
|
import { installPlugin as cliInstallPlugin, uninstallPlugin as cliUninstallPlugin, updatePlugin as cliUpdatePlugin, } from "../../services/claude-cli.js";
|
|
17
17
|
import { getPluginEnvRequirements, getPluginSourcePath, } from "../../services/plugin-mcp-config.js";
|
|
18
18
|
import { getPluginSetupFromSource, checkMissingDeps, installPluginDeps, } from "../../services/plugin-setup.js";
|
|
19
|
+
// Virtual marketplace name for the community sub-section of claude-plugins-official
|
|
20
|
+
const COMMUNITY_VIRTUAL_MARKETPLACE = "claude-plugins-official:community";
|
|
21
|
+
// The marketplace that gets split into Anthropic Official + Community sections
|
|
22
|
+
const SPLIT_MARKETPLACE = "claude-plugins-official";
|
|
19
23
|
export function PluginsScreen() {
|
|
20
24
|
const { state, dispatch } = useApp();
|
|
21
25
|
const { plugins: pluginsState } = state;
|
|
@@ -77,6 +81,64 @@ export function PluginsScreen() {
|
|
|
77
81
|
const isCollapsed = collapsed.has(marketplace.name);
|
|
78
82
|
const isEnabled = marketplacePlugins.length > 0 || marketplace.official;
|
|
79
83
|
const hasPlugins = marketplacePlugins.length > 0;
|
|
84
|
+
// Special handling: split claude-plugins-official into two sub-sections
|
|
85
|
+
if (marketplace.name === SPLIT_MARKETPLACE && hasPlugins) {
|
|
86
|
+
const anthropicPlugins = marketplacePlugins.filter((p) => p.author?.name?.toLowerCase() === "anthropic");
|
|
87
|
+
const communityPlugins = marketplacePlugins.filter((p) => p.author?.name?.toLowerCase() !== "anthropic");
|
|
88
|
+
// Sub-section 1: Anthropic Official (plugins by Anthropic)
|
|
89
|
+
const anthropicCollapsed = collapsed.has(marketplace.name);
|
|
90
|
+
const anthropicHasPlugins = anthropicPlugins.length > 0;
|
|
91
|
+
items.push({
|
|
92
|
+
id: `mp:${marketplace.name}`,
|
|
93
|
+
type: "category",
|
|
94
|
+
label: marketplace.displayName,
|
|
95
|
+
marketplace,
|
|
96
|
+
marketplaceEnabled: isEnabled,
|
|
97
|
+
pluginCount: anthropicPlugins.length,
|
|
98
|
+
isExpanded: !anthropicCollapsed && anthropicHasPlugins,
|
|
99
|
+
});
|
|
100
|
+
if (isEnabled && anthropicHasPlugins && !anthropicCollapsed) {
|
|
101
|
+
for (const plugin of anthropicPlugins) {
|
|
102
|
+
items.push({
|
|
103
|
+
id: `pl:${plugin.id}`,
|
|
104
|
+
type: "plugin",
|
|
105
|
+
label: plugin.name,
|
|
106
|
+
plugin,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Sub-section 2: Community (third-party plugins in same marketplace)
|
|
111
|
+
if (communityPlugins.length > 0) {
|
|
112
|
+
const communityVirtualMp = {
|
|
113
|
+
name: COMMUNITY_VIRTUAL_MARKETPLACE,
|
|
114
|
+
displayName: "Anthropic Official — 3rd Party",
|
|
115
|
+
source: marketplace.source,
|
|
116
|
+
description: "Third-party plugins in the Anthropic Official marketplace",
|
|
117
|
+
};
|
|
118
|
+
const communityCollapsed = collapsed.has(COMMUNITY_VIRTUAL_MARKETPLACE);
|
|
119
|
+
items.push({
|
|
120
|
+
id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
|
|
121
|
+
type: "category",
|
|
122
|
+
label: "Anthropic Official — 3rd Party",
|
|
123
|
+
marketplace: communityVirtualMp,
|
|
124
|
+
marketplaceEnabled: true,
|
|
125
|
+
pluginCount: communityPlugins.length,
|
|
126
|
+
isExpanded: !communityCollapsed,
|
|
127
|
+
isCommunitySection: true,
|
|
128
|
+
});
|
|
129
|
+
if (!communityCollapsed) {
|
|
130
|
+
for (const plugin of communityPlugins) {
|
|
131
|
+
items.push({
|
|
132
|
+
id: `pl:${plugin.id}`,
|
|
133
|
+
type: "plugin",
|
|
134
|
+
label: plugin.name,
|
|
135
|
+
plugin,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
80
142
|
// Category header (marketplace)
|
|
81
143
|
items.push({
|
|
82
144
|
id: `mp:${marketplace.name}`,
|
|
@@ -113,32 +175,44 @@ export function PluginsScreen() {
|
|
|
113
175
|
// Only search plugins, not categories
|
|
114
176
|
const pluginItems = allItems.filter((item) => item.type === "plugin");
|
|
115
177
|
const fuzzyResults = fuzzyFilter(pluginItems, query, (item) => item.label);
|
|
116
|
-
//
|
|
117
|
-
const
|
|
178
|
+
// Build a set of matched plugin item ids for O(1) lookup
|
|
179
|
+
const matchedPluginIds = new Set();
|
|
118
180
|
for (const result of fuzzyResults) {
|
|
119
|
-
|
|
120
|
-
|
|
181
|
+
matchedPluginIds.add(result.item.id);
|
|
182
|
+
}
|
|
183
|
+
// Walk allItems sequentially: track the current category section.
|
|
184
|
+
// For each category, include it only if any plugin under it matched.
|
|
185
|
+
// We build a map from category item id -> whether any plugin below matched.
|
|
186
|
+
const categoryHasMatch = new Map();
|
|
187
|
+
let currentCategoryId = null;
|
|
188
|
+
for (const item of allItems) {
|
|
189
|
+
if (item.type === "category") {
|
|
190
|
+
currentCategoryId = item.id;
|
|
191
|
+
if (!categoryHasMatch.has(item.id)) {
|
|
192
|
+
categoryHasMatch.set(item.id, false);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (item.type === "plugin" && currentCategoryId) {
|
|
196
|
+
if (matchedPluginIds.has(item.id)) {
|
|
197
|
+
categoryHasMatch.set(currentCategoryId, true);
|
|
198
|
+
}
|
|
121
199
|
}
|
|
122
200
|
}
|
|
123
201
|
const result = [];
|
|
124
|
-
let
|
|
202
|
+
let currentCatIncluded = false;
|
|
203
|
+
currentCategoryId = null;
|
|
125
204
|
for (const item of allItems) {
|
|
126
|
-
if (item.type === "category"
|
|
127
|
-
|
|
205
|
+
if (item.type === "category") {
|
|
206
|
+
currentCategoryId = item.id;
|
|
207
|
+
currentCatIncluded = categoryHasMatch.get(item.id) === true;
|
|
208
|
+
if (currentCatIncluded) {
|
|
128
209
|
result.push(item);
|
|
129
|
-
currentMarketplace = item.marketplace.name;
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
currentMarketplace = null;
|
|
133
210
|
}
|
|
134
211
|
}
|
|
135
|
-
else if (item.type === "plugin" &&
|
|
136
|
-
if (
|
|
137
|
-
// Check if this plugin matched
|
|
212
|
+
else if (item.type === "plugin" && currentCatIncluded) {
|
|
213
|
+
if (matchedPluginIds.has(item.id)) {
|
|
138
214
|
const matched = fuzzyResults.find((r) => r.item.id === item.id);
|
|
139
|
-
|
|
140
|
-
result.push({ ...item, _matches: matched.matches });
|
|
141
|
-
}
|
|
215
|
+
result.push({ ...item, _matches: matched?.matches });
|
|
142
216
|
}
|
|
143
217
|
}
|
|
144
218
|
}
|
|
@@ -634,9 +708,10 @@ export function PluginsScreen() {
|
|
|
634
708
|
installedVersion &&
|
|
635
709
|
latestVersion !== "0.0.0" &&
|
|
636
710
|
installedVersion !== latestVersion;
|
|
637
|
-
// Determine action
|
|
638
|
-
//
|
|
639
|
-
//
|
|
711
|
+
// Determine action for THIS scope:
|
|
712
|
+
// - installed in this scope + has update → update
|
|
713
|
+
// - installed in this scope → uninstall from this scope
|
|
714
|
+
// - not installed in this scope → install to this scope
|
|
640
715
|
let action;
|
|
641
716
|
if (isInstalledInScope && hasUpdateInScope) {
|
|
642
717
|
action = "update";
|
|
@@ -644,12 +719,8 @@ export function PluginsScreen() {
|
|
|
644
719
|
else if (isInstalledInScope) {
|
|
645
720
|
action = "uninstall";
|
|
646
721
|
}
|
|
647
|
-
else if (!isInstalledAnywhere) {
|
|
648
|
-
action = "install";
|
|
649
|
-
}
|
|
650
722
|
else {
|
|
651
|
-
|
|
652
|
-
action = "uninstall";
|
|
723
|
+
action = "install";
|
|
653
724
|
}
|
|
654
725
|
const actionLabel = action === "update"
|
|
655
726
|
? `Updating ${scopeLabel}`
|
|
@@ -777,7 +848,11 @@ export function PluginsScreen() {
|
|
|
777
848
|
let statusText = "";
|
|
778
849
|
let statusColor = "green";
|
|
779
850
|
if (item.marketplaceEnabled) {
|
|
780
|
-
if (
|
|
851
|
+
if (item.isCommunitySection) {
|
|
852
|
+
statusText = "3rd Party";
|
|
853
|
+
statusColor = "gray";
|
|
854
|
+
}
|
|
855
|
+
else if (mp.name === "claude-plugins-official") {
|
|
781
856
|
statusText = "★ Official";
|
|
782
857
|
statusColor = "yellow";
|
|
783
858
|
}
|
|
@@ -805,17 +880,12 @@ export function PluginsScreen() {
|
|
|
805
880
|
}
|
|
806
881
|
if (item.type === "plugin" && item.plugin) {
|
|
807
882
|
const plugin = item.plugin;
|
|
808
|
-
let statusIcon = "○";
|
|
809
|
-
let statusColor = "gray";
|
|
810
883
|
const isAnyScope = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
statusIcon = "●";
|
|
817
|
-
statusColor = "green";
|
|
818
|
-
}
|
|
884
|
+
// Build scope parts for colored rendering
|
|
885
|
+
const hasUser = plugin.userScope?.enabled;
|
|
886
|
+
const hasProject = plugin.projectScope?.enabled;
|
|
887
|
+
const hasLocal = plugin.localScope?.enabled;
|
|
888
|
+
const hasAnyScope = hasUser || hasProject || hasLocal;
|
|
819
889
|
// Build version string
|
|
820
890
|
let versionStr = "";
|
|
821
891
|
if (plugin.isOrphaned) {
|
|
@@ -831,19 +901,18 @@ export function PluginsScreen() {
|
|
|
831
901
|
const matches = item._matches;
|
|
832
902
|
const segments = matches ? highlightMatches(plugin.name, matches) : null;
|
|
833
903
|
if (isSelected) {
|
|
834
|
-
const
|
|
835
|
-
return (
|
|
904
|
+
const scopeStr = `[${hasUser ? "u" : "."}${hasProject ? "p" : "."}${hasLocal ? "l" : "."}]`;
|
|
905
|
+
return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", scopeStr, " ", plugin.name, versionStr, " "] }));
|
|
836
906
|
}
|
|
837
|
-
// For non-selected, render with colors
|
|
838
907
|
const displayName = segments
|
|
839
908
|
? segments.map((seg) => seg.text).join("")
|
|
840
909
|
: plugin.name;
|
|
841
910
|
if (plugin.isOrphaned) {
|
|
842
911
|
const ver = plugin.installedVersion && plugin.installedVersion !== "0.0.0"
|
|
843
912
|
? ` v${plugin.installedVersion}` : "";
|
|
844
|
-
return (_jsxs("text", { children: [
|
|
913
|
+
return (_jsxs("text", { children: [_jsx("span", { fg: "red", children: " [x..] " }), _jsx("span", { fg: "gray", children: displayName }), ver && _jsx("span", { fg: "yellow", children: ver }), _jsx("span", { fg: "red", children: " deprecated" })] }));
|
|
845
914
|
}
|
|
846
|
-
return (_jsxs("text", { children: [
|
|
915
|
+
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: hasLocal ? "yellow" : "#555555", children: hasLocal ? "l" : "." }), _jsx("span", { fg: "#555555", children: "]" }), _jsx("span", { children: " " }), _jsx("span", { fg: hasAnyScope ? "white" : "gray", children: displayName }), _jsx("span", { fg: plugin.hasUpdate ? "yellow" : "gray", children: versionStr })] }));
|
|
847
916
|
}
|
|
848
917
|
return _jsx("text", { fg: "gray", children: item.label });
|
|
849
918
|
};
|
|
@@ -857,6 +926,8 @@ export function PluginsScreen() {
|
|
|
857
926
|
const isEnabled = selectedItem.marketplaceEnabled;
|
|
858
927
|
// Get appropriate badge for marketplace type
|
|
859
928
|
const getBadge = () => {
|
|
929
|
+
if (selectedItem.isCommunitySection)
|
|
930
|
+
return " 3rd Party";
|
|
860
931
|
if (mp.name === "claude-plugins-official")
|
|
861
932
|
return " ★";
|
|
862
933
|
if (mp.name === "claude-code-plugins")
|
|
@@ -6,6 +6,7 @@ import { ScreenLayout } from "../components/layout/index.js";
|
|
|
6
6
|
import { CategoryHeader } from "../components/CategoryHeader.js";
|
|
7
7
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
8
8
|
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
9
|
+
import { scopeIndicatorText } from "../components/ScopeIndicator.js";
|
|
9
10
|
import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
|
|
10
11
|
import { getAllMarketplaces } from "../../data/marketplaces.js";
|
|
11
12
|
import {
|
|
@@ -41,6 +42,11 @@ import {
|
|
|
41
42
|
} from "../../services/plugin-setup.js";
|
|
42
43
|
import type { Marketplace } from "../../types/index.js";
|
|
43
44
|
|
|
45
|
+
// Virtual marketplace name for the community sub-section of claude-plugins-official
|
|
46
|
+
const COMMUNITY_VIRTUAL_MARKETPLACE = "claude-plugins-official:community";
|
|
47
|
+
// The marketplace that gets split into Anthropic Official + Community sections
|
|
48
|
+
const SPLIT_MARKETPLACE = "claude-plugins-official";
|
|
49
|
+
|
|
44
50
|
interface ListItem {
|
|
45
51
|
id: string;
|
|
46
52
|
type: "category" | "plugin";
|
|
@@ -50,6 +56,8 @@ interface ListItem {
|
|
|
50
56
|
plugin?: PluginInfo;
|
|
51
57
|
pluginCount?: number;
|
|
52
58
|
isExpanded?: boolean;
|
|
59
|
+
/** True for the virtual Community sub-section derived from claude-plugins-official */
|
|
60
|
+
isCommunitySection?: boolean;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
export function PluginsScreen() {
|
|
@@ -128,6 +136,72 @@ export function PluginsScreen() {
|
|
|
128
136
|
const isEnabled = marketplacePlugins.length > 0 || marketplace.official;
|
|
129
137
|
const hasPlugins = marketplacePlugins.length > 0;
|
|
130
138
|
|
|
139
|
+
// Special handling: split claude-plugins-official into two sub-sections
|
|
140
|
+
if (marketplace.name === SPLIT_MARKETPLACE && hasPlugins) {
|
|
141
|
+
const anthropicPlugins = marketplacePlugins.filter(
|
|
142
|
+
(p) => p.author?.name?.toLowerCase() === "anthropic",
|
|
143
|
+
);
|
|
144
|
+
const communityPlugins = marketplacePlugins.filter(
|
|
145
|
+
(p) => p.author?.name?.toLowerCase() !== "anthropic",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Sub-section 1: Anthropic Official (plugins by Anthropic)
|
|
149
|
+
const anthropicCollapsed = collapsed.has(marketplace.name);
|
|
150
|
+
const anthropicHasPlugins = anthropicPlugins.length > 0;
|
|
151
|
+
items.push({
|
|
152
|
+
id: `mp:${marketplace.name}`,
|
|
153
|
+
type: "category",
|
|
154
|
+
label: marketplace.displayName,
|
|
155
|
+
marketplace,
|
|
156
|
+
marketplaceEnabled: isEnabled,
|
|
157
|
+
pluginCount: anthropicPlugins.length,
|
|
158
|
+
isExpanded: !anthropicCollapsed && anthropicHasPlugins,
|
|
159
|
+
});
|
|
160
|
+
if (isEnabled && anthropicHasPlugins && !anthropicCollapsed) {
|
|
161
|
+
for (const plugin of anthropicPlugins) {
|
|
162
|
+
items.push({
|
|
163
|
+
id: `pl:${plugin.id}`,
|
|
164
|
+
type: "plugin",
|
|
165
|
+
label: plugin.name,
|
|
166
|
+
plugin,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Sub-section 2: Community (third-party plugins in same marketplace)
|
|
172
|
+
if (communityPlugins.length > 0) {
|
|
173
|
+
const communityVirtualMp: Marketplace = {
|
|
174
|
+
name: COMMUNITY_VIRTUAL_MARKETPLACE,
|
|
175
|
+
displayName: "Anthropic Official — 3rd Party",
|
|
176
|
+
source: marketplace.source,
|
|
177
|
+
description: "Third-party plugins in the Anthropic Official marketplace",
|
|
178
|
+
};
|
|
179
|
+
const communityCollapsed = collapsed.has(COMMUNITY_VIRTUAL_MARKETPLACE);
|
|
180
|
+
items.push({
|
|
181
|
+
id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
|
|
182
|
+
type: "category",
|
|
183
|
+
label: "Anthropic Official — 3rd Party",
|
|
184
|
+
marketplace: communityVirtualMp,
|
|
185
|
+
marketplaceEnabled: true,
|
|
186
|
+
pluginCount: communityPlugins.length,
|
|
187
|
+
isExpanded: !communityCollapsed,
|
|
188
|
+
isCommunitySection: true,
|
|
189
|
+
});
|
|
190
|
+
if (!communityCollapsed) {
|
|
191
|
+
for (const plugin of communityPlugins) {
|
|
192
|
+
items.push({
|
|
193
|
+
id: `pl:${plugin.id}`,
|
|
194
|
+
type: "plugin",
|
|
195
|
+
label: plugin.name,
|
|
196
|
+
plugin,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
131
205
|
// Category header (marketplace)
|
|
132
206
|
items.push({
|
|
133
207
|
id: `mp:${marketplace.name}`,
|
|
@@ -168,34 +242,47 @@ export function PluginsScreen() {
|
|
|
168
242
|
const pluginItems = allItems.filter((item) => item.type === "plugin");
|
|
169
243
|
const fuzzyResults = fuzzyFilter(pluginItems, query, (item) => item.label);
|
|
170
244
|
|
|
171
|
-
//
|
|
172
|
-
const
|
|
245
|
+
// Build a set of matched plugin item ids for O(1) lookup
|
|
246
|
+
const matchedPluginIds = new Set<string>();
|
|
173
247
|
for (const result of fuzzyResults) {
|
|
174
|
-
|
|
175
|
-
|
|
248
|
+
matchedPluginIds.add(result.item.id);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Walk allItems sequentially: track the current category section.
|
|
252
|
+
// For each category, include it only if any plugin under it matched.
|
|
253
|
+
// We build a map from category item id -> whether any plugin below matched.
|
|
254
|
+
const categoryHasMatch = new Map<string, boolean>();
|
|
255
|
+
let currentCategoryId: string | null = null;
|
|
256
|
+
for (const item of allItems) {
|
|
257
|
+
if (item.type === "category") {
|
|
258
|
+
currentCategoryId = item.id;
|
|
259
|
+
if (!categoryHasMatch.has(item.id)) {
|
|
260
|
+
categoryHasMatch.set(item.id, false);
|
|
261
|
+
}
|
|
262
|
+
} else if (item.type === "plugin" && currentCategoryId) {
|
|
263
|
+
if (matchedPluginIds.has(item.id)) {
|
|
264
|
+
categoryHasMatch.set(currentCategoryId, true);
|
|
265
|
+
}
|
|
176
266
|
}
|
|
177
267
|
}
|
|
178
268
|
|
|
179
269
|
const result: ListItem[] = [];
|
|
180
|
-
let
|
|
270
|
+
let currentCatIncluded = false;
|
|
271
|
+
currentCategoryId = null;
|
|
181
272
|
|
|
182
273
|
for (const item of allItems) {
|
|
183
|
-
if (item.type === "category"
|
|
184
|
-
|
|
274
|
+
if (item.type === "category") {
|
|
275
|
+
currentCategoryId = item.id;
|
|
276
|
+
currentCatIncluded = categoryHasMatch.get(item.id) === true;
|
|
277
|
+
if (currentCatIncluded) {
|
|
185
278
|
result.push(item);
|
|
186
|
-
currentMarketplace = item.marketplace.name;
|
|
187
|
-
} else {
|
|
188
|
-
currentMarketplace = null;
|
|
189
279
|
}
|
|
190
|
-
} else if (item.type === "plugin" &&
|
|
191
|
-
if (
|
|
192
|
-
// Check if this plugin matched
|
|
280
|
+
} else if (item.type === "plugin" && currentCatIncluded) {
|
|
281
|
+
if (matchedPluginIds.has(item.id)) {
|
|
193
282
|
const matched = fuzzyResults.find((r) => r.item.id === item.id);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
});
|
|
198
|
-
}
|
|
283
|
+
result.push({ ...item, _matches: matched?.matches } as ListItem & {
|
|
284
|
+
_matches?: number[];
|
|
285
|
+
});
|
|
199
286
|
}
|
|
200
287
|
}
|
|
201
288
|
}
|
|
@@ -811,19 +898,17 @@ export function PluginsScreen() {
|
|
|
811
898
|
latestVersion !== "0.0.0" &&
|
|
812
899
|
installedVersion !== latestVersion;
|
|
813
900
|
|
|
814
|
-
// Determine action
|
|
815
|
-
//
|
|
816
|
-
//
|
|
901
|
+
// Determine action for THIS scope:
|
|
902
|
+
// - installed in this scope + has update → update
|
|
903
|
+
// - installed in this scope → uninstall from this scope
|
|
904
|
+
// - not installed in this scope → install to this scope
|
|
817
905
|
let action: "update" | "install" | "uninstall";
|
|
818
906
|
if (isInstalledInScope && hasUpdateInScope) {
|
|
819
907
|
action = "update";
|
|
820
908
|
} else if (isInstalledInScope) {
|
|
821
909
|
action = "uninstall";
|
|
822
|
-
} else if (!isInstalledAnywhere) {
|
|
823
|
-
action = "install";
|
|
824
910
|
} else {
|
|
825
|
-
|
|
826
|
-
action = "uninstall";
|
|
911
|
+
action = "install";
|
|
827
912
|
}
|
|
828
913
|
|
|
829
914
|
const actionLabel =
|
|
@@ -1005,7 +1090,10 @@ export function PluginsScreen() {
|
|
|
1005
1090
|
let statusText = "";
|
|
1006
1091
|
let statusColor = "green";
|
|
1007
1092
|
if (item.marketplaceEnabled) {
|
|
1008
|
-
if (
|
|
1093
|
+
if (item.isCommunitySection) {
|
|
1094
|
+
statusText = "3rd Party";
|
|
1095
|
+
statusColor = "gray";
|
|
1096
|
+
} else if (mp.name === "claude-plugins-official") {
|
|
1009
1097
|
statusText = "★ Official";
|
|
1010
1098
|
statusColor = "yellow";
|
|
1011
1099
|
} else if (mp.name === "claude-code-plugins") {
|
|
@@ -1050,17 +1138,13 @@ export function PluginsScreen() {
|
|
|
1050
1138
|
|
|
1051
1139
|
if (item.type === "plugin" && item.plugin) {
|
|
1052
1140
|
const plugin = item.plugin;
|
|
1053
|
-
let statusIcon = "○";
|
|
1054
|
-
let statusColor = "gray";
|
|
1055
|
-
|
|
1056
1141
|
const isAnyScope = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
}
|
|
1142
|
+
|
|
1143
|
+
// Build scope parts for colored rendering
|
|
1144
|
+
const hasUser = plugin.userScope?.enabled;
|
|
1145
|
+
const hasProject = plugin.projectScope?.enabled;
|
|
1146
|
+
const hasLocal = plugin.localScope?.enabled;
|
|
1147
|
+
const hasAnyScope = hasUser || hasProject || hasLocal;
|
|
1064
1148
|
|
|
1065
1149
|
// Build version string
|
|
1066
1150
|
let versionStr = "";
|
|
@@ -1078,15 +1162,14 @@ export function PluginsScreen() {
|
|
|
1078
1162
|
const segments = matches ? highlightMatches(plugin.name, matches) : null;
|
|
1079
1163
|
|
|
1080
1164
|
if (isSelected) {
|
|
1081
|
-
const
|
|
1165
|
+
const scopeStr = `[${hasUser ? "u" : "."}${hasProject ? "p" : "."}${hasLocal ? "l" : "."}]`;
|
|
1082
1166
|
return (
|
|
1083
1167
|
<text bg="magenta" fg="white">
|
|
1084
|
-
{
|
|
1168
|
+
{" "}{scopeStr} {plugin.name}{versionStr}{" "}
|
|
1085
1169
|
</text>
|
|
1086
1170
|
);
|
|
1087
1171
|
}
|
|
1088
1172
|
|
|
1089
|
-
// For non-selected, render with colors
|
|
1090
1173
|
const displayName = segments
|
|
1091
1174
|
? segments.map((seg) => seg.text).join("")
|
|
1092
1175
|
: plugin.name;
|
|
@@ -1096,7 +1179,7 @@ export function PluginsScreen() {
|
|
|
1096
1179
|
? ` v${plugin.installedVersion}` : "";
|
|
1097
1180
|
return (
|
|
1098
1181
|
<text>
|
|
1099
|
-
<span fg="red">
|
|
1182
|
+
<span fg="red"> [x..] </span>
|
|
1100
1183
|
<span fg="gray">{displayName}</span>
|
|
1101
1184
|
{ver && <span fg="yellow">{ver}</span>}
|
|
1102
1185
|
<span fg="red"> deprecated</span>
|
|
@@ -1106,11 +1189,13 @@ export function PluginsScreen() {
|
|
|
1106
1189
|
|
|
1107
1190
|
return (
|
|
1108
1191
|
<text>
|
|
1109
|
-
<span fg=
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
</span>
|
|
1113
|
-
<span>
|
|
1192
|
+
<span fg="#555555"> [</span>
|
|
1193
|
+
<span fg={hasUser ? "cyan" : "#555555"}>{hasUser ? "u" : "."}</span>
|
|
1194
|
+
<span fg={hasProject ? "green" : "#555555"}>{hasProject ? "p" : "."}</span>
|
|
1195
|
+
<span fg={hasLocal ? "yellow" : "#555555"}>{hasLocal ? "l" : "."}</span>
|
|
1196
|
+
<span fg="#555555">]</span>
|
|
1197
|
+
<span> </span>
|
|
1198
|
+
<span fg={hasAnyScope ? "white" : "gray"}>{displayName}</span>
|
|
1114
1199
|
<span fg={plugin.hasUpdate ? "yellow" : "gray"}>{versionStr}</span>
|
|
1115
1200
|
</text>
|
|
1116
1201
|
);
|
|
@@ -1131,6 +1216,7 @@ export function PluginsScreen() {
|
|
|
1131
1216
|
|
|
1132
1217
|
// Get appropriate badge for marketplace type
|
|
1133
1218
|
const getBadge = () => {
|
|
1219
|
+
if (selectedItem.isCommunitySection) return " 3rd Party";
|
|
1134
1220
|
if (mp.name === "claude-plugins-official") return " ★";
|
|
1135
1221
|
if (mp.name === "claude-code-plugins") return " ⚠";
|
|
1136
1222
|
if (mp.official) return " ★";
|
|
@@ -229,21 +229,15 @@ export function SkillsScreen() {
|
|
|
229
229
|
try {
|
|
230
230
|
await installSkill(selectedSkill, scope, state.projectPath);
|
|
231
231
|
modal.hideModal();
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
name: selectedSkill.name,
|
|
235
|
-
updates: {
|
|
236
|
-
installed: true,
|
|
237
|
-
installedScope: scope,
|
|
238
|
-
},
|
|
239
|
-
});
|
|
232
|
+
// Refetch to pick up install status for all skills including recommended
|
|
233
|
+
await fetchData();
|
|
240
234
|
await modal.message("Installed", `${selectedSkill.name} installed to ${scope === "user" ? "~/.claude/skills/" : ".claude/skills/"}`, "success");
|
|
241
235
|
}
|
|
242
236
|
catch (error) {
|
|
243
237
|
modal.hideModal();
|
|
244
238
|
await modal.message("Error", `Failed to install: ${error}`, "error");
|
|
245
239
|
}
|
|
246
|
-
}, [selectedSkill, state.projectPath, dispatch, modal]);
|
|
240
|
+
}, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
|
|
247
241
|
// Uninstall handler
|
|
248
242
|
const handleUninstall = useCallback(async () => {
|
|
249
243
|
if (!selectedSkill || !selectedSkill.installed)
|
|
@@ -258,21 +252,15 @@ export function SkillsScreen() {
|
|
|
258
252
|
try {
|
|
259
253
|
await uninstallSkill(selectedSkill.name, scope, state.projectPath);
|
|
260
254
|
modal.hideModal();
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
name: selectedSkill.name,
|
|
264
|
-
updates: {
|
|
265
|
-
installed: false,
|
|
266
|
-
installedScope: null,
|
|
267
|
-
},
|
|
268
|
-
});
|
|
255
|
+
// Refetch to pick up uninstall status
|
|
256
|
+
await fetchData();
|
|
269
257
|
await modal.message("Uninstalled", `${selectedSkill.name} removed.`, "success");
|
|
270
258
|
}
|
|
271
259
|
catch (error) {
|
|
272
260
|
modal.hideModal();
|
|
273
261
|
await modal.message("Error", `Failed to uninstall: ${error}`, "error");
|
|
274
262
|
}
|
|
275
|
-
}, [selectedSkill, state.projectPath, dispatch, modal]);
|
|
263
|
+
}, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
|
|
276
264
|
// Keyboard handling — same pattern as PluginsScreen
|
|
277
265
|
useKeyboard((event) => {
|
|
278
266
|
if (state.modal)
|
|
@@ -374,14 +362,15 @@ export function SkillsScreen() {
|
|
|
374
362
|
}
|
|
375
363
|
if (item.type === "skill" && item.skill) {
|
|
376
364
|
const skill = item.skill;
|
|
377
|
-
const indicator = skill.installed ? "●" : "○";
|
|
378
|
-
const indicatorColor = skill.installed ? "cyan" : "gray";
|
|
379
|
-
const scopeTag = skill.installedScope === "user" ? "u" : skill.installedScope === "project" ? "p" : "";
|
|
380
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";
|
|
381
369
|
if (isSelected) {
|
|
382
|
-
|
|
370
|
+
const scopeStr = `[${hasUser ? "u" : "."}${hasProject ? "p" : "."}]`;
|
|
371
|
+
return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", scopeStr, " ", skill.name, skill.hasUpdate ? " ⬆" : "", starsStr ? ` ${starsStr}` : "", " "] }));
|
|
383
372
|
}
|
|
384
|
-
return (_jsxs("text", { children: [
|
|
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] }))] }));
|
|
385
374
|
}
|
|
386
375
|
return _jsx("text", { fg: "gray", children: item.label });
|
|
387
376
|
};
|
|
@@ -5,6 +5,7 @@ import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
|
5
5
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
6
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
7
7
|
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
8
|
+
import { scopeIndicatorText } from "../components/ScopeIndicator.js";
|
|
8
9
|
import {
|
|
9
10
|
fetchAvailableSkills,
|
|
10
11
|
fetchSkillFrontmatter,
|
|
@@ -271,14 +272,8 @@ export function SkillsScreen() {
|
|
|
271
272
|
try {
|
|
272
273
|
await installSkill(selectedSkill, scope, state.projectPath);
|
|
273
274
|
modal.hideModal();
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
name: selectedSkill.name,
|
|
277
|
-
updates: {
|
|
278
|
-
installed: true,
|
|
279
|
-
installedScope: scope,
|
|
280
|
-
},
|
|
281
|
-
});
|
|
275
|
+
// Refetch to pick up install status for all skills including recommended
|
|
276
|
+
await fetchData();
|
|
282
277
|
await modal.message(
|
|
283
278
|
"Installed",
|
|
284
279
|
`${selectedSkill.name} installed to ${scope === "user" ? "~/.claude/skills/" : ".claude/skills/"}`,
|
|
@@ -288,7 +283,7 @@ export function SkillsScreen() {
|
|
|
288
283
|
modal.hideModal();
|
|
289
284
|
await modal.message("Error", `Failed to install: ${error}`, "error");
|
|
290
285
|
}
|
|
291
|
-
}, [selectedSkill, state.projectPath, dispatch, modal]);
|
|
286
|
+
}, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
|
|
292
287
|
|
|
293
288
|
// Uninstall handler
|
|
294
289
|
const handleUninstall = useCallback(async () => {
|
|
@@ -307,20 +302,14 @@ export function SkillsScreen() {
|
|
|
307
302
|
try {
|
|
308
303
|
await uninstallSkill(selectedSkill.name, scope, state.projectPath);
|
|
309
304
|
modal.hideModal();
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
name: selectedSkill.name,
|
|
313
|
-
updates: {
|
|
314
|
-
installed: false,
|
|
315
|
-
installedScope: null,
|
|
316
|
-
},
|
|
317
|
-
});
|
|
305
|
+
// Refetch to pick up uninstall status
|
|
306
|
+
await fetchData();
|
|
318
307
|
await modal.message("Uninstalled", `${selectedSkill.name} removed.`, "success");
|
|
319
308
|
} catch (error) {
|
|
320
309
|
modal.hideModal();
|
|
321
310
|
await modal.message("Error", `Failed to uninstall: ${error}`, "error");
|
|
322
311
|
}
|
|
323
|
-
}, [selectedSkill, state.projectPath, dispatch, modal]);
|
|
312
|
+
}, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
|
|
324
313
|
|
|
325
314
|
// Keyboard handling — same pattern as PluginsScreen
|
|
326
315
|
useKeyboard((event) => {
|
|
@@ -437,27 +426,28 @@ export function SkillsScreen() {
|
|
|
437
426
|
|
|
438
427
|
if (item.type === "skill" && item.skill) {
|
|
439
428
|
const skill = item.skill;
|
|
440
|
-
const indicator = skill.installed ? "●" : "○";
|
|
441
|
-
const indicatorColor = skill.installed ? "cyan" : "gray";
|
|
442
|
-
const scopeTag = skill.installedScope === "user" ? "u" : skill.installedScope === "project" ? "p" : "";
|
|
443
429
|
const starsStr = formatStars(skill.stars);
|
|
430
|
+
const hasUser = skill.installedScope === "user";
|
|
431
|
+
const hasProject = skill.installedScope === "project";
|
|
432
|
+
const nameColor = skill.installed ? "white" : "gray";
|
|
444
433
|
|
|
445
434
|
if (isSelected) {
|
|
435
|
+
const scopeStr = `[${hasUser ? "u" : "."}${hasProject ? "p" : "."}]`;
|
|
446
436
|
return (
|
|
447
437
|
<text bg="magenta" fg="white">
|
|
448
|
-
{" "}{
|
|
438
|
+
{" "}{scopeStr} {skill.name}{skill.hasUpdate ? " ⬆" : ""}{starsStr ? ` ${starsStr}` : ""}{" "}
|
|
449
439
|
</text>
|
|
450
440
|
);
|
|
451
441
|
}
|
|
452
442
|
|
|
453
443
|
return (
|
|
454
444
|
<text>
|
|
455
|
-
<span fg=
|
|
456
|
-
<span fg="
|
|
445
|
+
<span fg="#555555"> [</span>
|
|
446
|
+
<span fg={hasUser ? "cyan" : "#555555"}>{hasUser ? "u" : "."}</span>
|
|
447
|
+
<span fg={hasProject ? "green" : "#555555"}>{hasProject ? "p" : "."}</span>
|
|
448
|
+
<span fg="#555555">] </span>
|
|
449
|
+
<span fg={nameColor}>{skill.name}</span>
|
|
457
450
|
{skill.hasUpdate && <span fg="yellow"> ⬆</span>}
|
|
458
|
-
{scopeTag && (
|
|
459
|
-
<span fg={scopeTag === "u" ? "cyan" : "green"}> [{scopeTag}]</span>
|
|
460
|
-
)}
|
|
461
451
|
{starsStr && (
|
|
462
452
|
<span fg="yellow">{" "}{starsStr}</span>
|
|
463
453
|
)}
|