claudeup 3.10.0 → 3.12.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/screens/PluginsScreen.js +114 -31
- package/src/ui/screens/PluginsScreen.tsx +124 -36
package/package.json
CHANGED
|
@@ -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: "Community",
|
|
115
|
+
source: marketplace.source,
|
|
116
|
+
description: "Third-party plugins from the community",
|
|
117
|
+
};
|
|
118
|
+
const communityCollapsed = collapsed.has(COMMUNITY_VIRTUAL_MARKETPLACE);
|
|
119
|
+
items.push({
|
|
120
|
+
id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
|
|
121
|
+
type: "category",
|
|
122
|
+
label: "Community",
|
|
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
|
}
|
|
@@ -251,8 +325,6 @@ export function PluginsScreen() {
|
|
|
251
325
|
handleUpdate();
|
|
252
326
|
else if (event.name === "a")
|
|
253
327
|
handleUpdateAll();
|
|
254
|
-
else if (event.name === "d")
|
|
255
|
-
handleUninstall();
|
|
256
328
|
else if (event.name === "s")
|
|
257
329
|
handleSaveAsProfile();
|
|
258
330
|
// "/" to enter search mode
|
|
@@ -621,7 +693,7 @@ export function PluginsScreen() {
|
|
|
621
693
|
const plugin = item.plugin;
|
|
622
694
|
const latestVersion = plugin.version || "0.0.0";
|
|
623
695
|
const scopeLabel = scope === "user" ? "User" : scope === "project" ? "Project" : "Local";
|
|
624
|
-
// Check if installed in this scope
|
|
696
|
+
// Check if installed in this specific scope
|
|
625
697
|
const scopeData = scope === "user"
|
|
626
698
|
? plugin.userScope
|
|
627
699
|
: scope === "project"
|
|
@@ -629,12 +701,16 @@ export function PluginsScreen() {
|
|
|
629
701
|
: plugin.localScope;
|
|
630
702
|
const isInstalledInScope = scopeData?.enabled;
|
|
631
703
|
const installedVersion = scopeData?.version;
|
|
704
|
+
// Also check if installed in ANY scope (for the toggle behavior)
|
|
705
|
+
const isInstalledAnywhere = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
|
|
632
706
|
// Check if this scope has an update available
|
|
633
707
|
const hasUpdateInScope = isInstalledInScope &&
|
|
634
708
|
installedVersion &&
|
|
635
709
|
latestVersion !== "0.0.0" &&
|
|
636
710
|
installedVersion !== latestVersion;
|
|
637
|
-
// Determine action:
|
|
711
|
+
// Determine action: if installed in this scope → uninstall
|
|
712
|
+
// If installed anywhere else but not this scope → uninstall from detected scope
|
|
713
|
+
// Otherwise → install
|
|
638
714
|
let action;
|
|
639
715
|
if (isInstalledInScope && hasUpdateInScope) {
|
|
640
716
|
action = "update";
|
|
@@ -642,9 +718,13 @@ export function PluginsScreen() {
|
|
|
642
718
|
else if (isInstalledInScope) {
|
|
643
719
|
action = "uninstall";
|
|
644
720
|
}
|
|
645
|
-
else {
|
|
721
|
+
else if (!isInstalledAnywhere) {
|
|
646
722
|
action = "install";
|
|
647
723
|
}
|
|
724
|
+
else {
|
|
725
|
+
// Installed in a different scope — uninstall from the scope it's actually in
|
|
726
|
+
action = "uninstall";
|
|
727
|
+
}
|
|
648
728
|
const actionLabel = action === "update"
|
|
649
729
|
? `Updating ${scopeLabel}`
|
|
650
730
|
: action === "install"
|
|
@@ -771,7 +851,11 @@ export function PluginsScreen() {
|
|
|
771
851
|
let statusText = "";
|
|
772
852
|
let statusColor = "green";
|
|
773
853
|
if (item.marketplaceEnabled) {
|
|
774
|
-
if (
|
|
854
|
+
if (item.isCommunitySection) {
|
|
855
|
+
statusText = "3rd Party";
|
|
856
|
+
statusColor = "gray";
|
|
857
|
+
}
|
|
858
|
+
else if (mp.name === "claude-plugins-official") {
|
|
775
859
|
statusText = "★ Official";
|
|
776
860
|
statusColor = "yellow";
|
|
777
861
|
}
|
|
@@ -801,18 +885,15 @@ export function PluginsScreen() {
|
|
|
801
885
|
const plugin = item.plugin;
|
|
802
886
|
let statusIcon = "○";
|
|
803
887
|
let statusColor = "gray";
|
|
888
|
+
const isAnyScope = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
|
|
804
889
|
if (plugin.isOrphaned) {
|
|
805
890
|
statusIcon = "x";
|
|
806
891
|
statusColor = "red";
|
|
807
892
|
}
|
|
808
|
-
else if (
|
|
893
|
+
else if (isAnyScope) {
|
|
809
894
|
statusIcon = "●";
|
|
810
895
|
statusColor = "green";
|
|
811
896
|
}
|
|
812
|
-
else if (plugin.installedVersion) {
|
|
813
|
-
statusIcon = "●";
|
|
814
|
-
statusColor = "yellow";
|
|
815
|
-
}
|
|
816
897
|
// Build version string
|
|
817
898
|
let versionStr = "";
|
|
818
899
|
if (plugin.isOrphaned) {
|
|
@@ -854,6 +935,8 @@ export function PluginsScreen() {
|
|
|
854
935
|
const isEnabled = selectedItem.marketplaceEnabled;
|
|
855
936
|
// Get appropriate badge for marketplace type
|
|
856
937
|
const getBadge = () => {
|
|
938
|
+
if (selectedItem.isCommunitySection)
|
|
939
|
+
return " 3rd Party";
|
|
857
940
|
if (mp.name === "claude-plugins-official")
|
|
858
941
|
return " ★";
|
|
859
942
|
if (mp.name === "claude-code-plugins")
|
|
@@ -881,7 +964,7 @@ export function PluginsScreen() {
|
|
|
881
964
|
}
|
|
882
965
|
if (selectedItem.type === "plugin" && selectedItem.plugin) {
|
|
883
966
|
const plugin = selectedItem.plugin;
|
|
884
|
-
const isInstalled = plugin.enabled || plugin.
|
|
967
|
+
const isInstalled = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
|
|
885
968
|
// Orphaned/deprecated plugin
|
|
886
969
|
if (plugin.isOrphaned) {
|
|
887
970
|
return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: "yellow", fg: "black", children: _jsxs("strong", { children: [" ", plugin.name, " \u2014 DEPRECATED "] }) }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "yellow", children: "This plugin is no longer in the marketplace." }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "It was removed from the marketplace but still referenced in your settings. Press d to uninstall and clean up." }) }), isInstalled && (_jsx("box", { flexDirection: "column", marginTop: 2, children: _jsxs("box", { children: [_jsx("text", { bg: "red", fg: "white", children: " d " }), _jsx("text", { children: " Uninstall (recommended)" })] }) }))] }));
|
|
@@ -903,13 +986,13 @@ export function PluginsScreen() {
|
|
|
903
986
|
const showVersion = plugin.version && plugin.version !== "0.0.0";
|
|
904
987
|
const showInstalledVersion = plugin.installedVersion && plugin.installedVersion !== "0.0.0";
|
|
905
988
|
return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", plugin.name, plugin.hasUpdate ? " ⬆" : "", " "] }) }) }), _jsx("box", { marginTop: 1, children: isInstalled ? (_jsx("text", { fg: "green", children: "\u25CF Installed" })) : (_jsx("text", { fg: "gray", children: "\u25CB Not installed" })) }), _jsx("box", { marginTop: 1, marginBottom: 1, children: _jsx("text", { fg: "white", children: plugin.description }) }), showVersion && (_jsxs("text", { children: [_jsx("span", { children: "Version " }), _jsxs("span", { fg: "#5c9aff", children: ["v", plugin.version] }), showInstalledVersion &&
|
|
906
|
-
plugin.installedVersion !== plugin.version && (_jsxs("span", { children: [" (v", plugin.installedVersion, " installed)"] }))] })), plugin.category && (_jsxs("text", { children: [_jsx("span", { children: "Category " }), _jsx("span", { fg: "magenta", children: plugin.category })] })), plugin.author && (_jsxs("text", { children: [_jsx("span", { children: "Author " }), _jsx("span", { children: plugin.author.name })] })), components.length > 0 && (_jsxs("text", { children: [_jsx("span", { children: "Contains " }), _jsx("span", { fg: "yellow", children: components.join(" · ") })] })), _jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx("text", { children: _jsx("strong", { children: "Scopes:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsxs("span", { bg: "cyan", fg: "black", children: [" ", "u", " "] }), _jsx("span", { fg: plugin.userScope?.enabled ? "cyan" : "gray", children: plugin.userScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: "cyan", children: "User" }), _jsx("span", { children: " global" }), plugin.userScope?.version && (_jsxs("span", { fg: "cyan", children: [" v", plugin.userScope.version] }))] }), _jsxs("text", { children: [_jsxs("span", { bg: "green", fg: "black", children: [" ", "p", " "] }), _jsx("span", { fg: plugin.projectScope?.enabled ? "green" : "gray", children: plugin.projectScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: "green", children: "Project" }), _jsx("span", { children: " team" }), plugin.projectScope?.version && (_jsxs("span", { fg: "green", children: [" v", plugin.projectScope.version] }))] }), _jsxs("text", { children: [_jsxs("span", { bg: "yellow", fg: "black", children: [" ", "l", " "] }), _jsx("span", { fg: plugin.localScope?.enabled ? "yellow" : "gray", children: plugin.localScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: "yellow", children: "Local" }), _jsx("span", { children: " private" }), plugin.localScope?.version && (_jsxs("span", { fg: "yellow", children: [" v", plugin.localScope.version] }))] })] })] }), isInstalled && (
|
|
989
|
+
plugin.installedVersion !== plugin.version && (_jsxs("span", { children: [" (v", plugin.installedVersion, " installed)"] }))] })), plugin.category && (_jsxs("text", { children: [_jsx("span", { children: "Category " }), _jsx("span", { fg: "magenta", children: plugin.category })] })), plugin.author && (_jsxs("text", { children: [_jsx("span", { children: "Author " }), _jsx("span", { children: plugin.author.name })] })), components.length > 0 && (_jsxs("text", { children: [_jsx("span", { children: "Contains " }), _jsx("span", { fg: "yellow", children: components.join(" · ") })] })), _jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx("text", { children: _jsx("strong", { children: "Scopes:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsxs("span", { bg: "cyan", fg: "black", children: [" ", "u", " "] }), _jsx("span", { fg: plugin.userScope?.enabled ? "cyan" : "gray", children: plugin.userScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: "cyan", children: "User" }), _jsx("span", { children: " global" }), plugin.userScope?.version && (_jsxs("span", { fg: "cyan", children: [" v", plugin.userScope.version] }))] }), _jsxs("text", { children: [_jsxs("span", { bg: "green", fg: "black", children: [" ", "p", " "] }), _jsx("span", { fg: plugin.projectScope?.enabled ? "green" : "gray", children: plugin.projectScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: "green", children: "Project" }), _jsx("span", { children: " team" }), plugin.projectScope?.version && (_jsxs("span", { fg: "green", children: [" v", plugin.projectScope.version] }))] }), _jsxs("text", { children: [_jsxs("span", { bg: "yellow", fg: "black", children: [" ", "l", " "] }), _jsx("span", { fg: plugin.localScope?.enabled ? "yellow" : "gray", children: plugin.localScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: "yellow", children: "Local" }), _jsx("span", { children: " private" }), plugin.localScope?.version && (_jsxs("span", { fg: "yellow", children: [" v", plugin.localScope.version] }))] })] })] }), isInstalled && (_jsx("box", { flexDirection: "column", marginTop: 1, children: plugin.hasUpdate && (_jsxs("box", { children: [_jsxs("text", { bg: "magenta", fg: "white", children: [" ", "U", " "] }), _jsxs("text", { children: [" Update to v", plugin.version] })] })) }))] }));
|
|
907
990
|
}
|
|
908
991
|
return null;
|
|
909
992
|
};
|
|
910
993
|
const footerHints = isSearchActive
|
|
911
994
|
? "type to filter │ Enter:done │ Esc:clear"
|
|
912
|
-
: "u/p/l:
|
|
995
|
+
: "u/p/l:toggle │ U:update │ a:all │ s:profile │ /:search";
|
|
913
996
|
// Calculate status for subtitle
|
|
914
997
|
const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
|
|
915
998
|
const plugins = pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
|
|
@@ -41,6 +41,11 @@ import {
|
|
|
41
41
|
} from "../../services/plugin-setup.js";
|
|
42
42
|
import type { Marketplace } from "../../types/index.js";
|
|
43
43
|
|
|
44
|
+
// Virtual marketplace name for the community sub-section of claude-plugins-official
|
|
45
|
+
const COMMUNITY_VIRTUAL_MARKETPLACE = "claude-plugins-official:community";
|
|
46
|
+
// The marketplace that gets split into Anthropic Official + Community sections
|
|
47
|
+
const SPLIT_MARKETPLACE = "claude-plugins-official";
|
|
48
|
+
|
|
44
49
|
interface ListItem {
|
|
45
50
|
id: string;
|
|
46
51
|
type: "category" | "plugin";
|
|
@@ -50,6 +55,8 @@ interface ListItem {
|
|
|
50
55
|
plugin?: PluginInfo;
|
|
51
56
|
pluginCount?: number;
|
|
52
57
|
isExpanded?: boolean;
|
|
58
|
+
/** True for the virtual Community sub-section derived from claude-plugins-official */
|
|
59
|
+
isCommunitySection?: boolean;
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
export function PluginsScreen() {
|
|
@@ -128,6 +135,72 @@ export function PluginsScreen() {
|
|
|
128
135
|
const isEnabled = marketplacePlugins.length > 0 || marketplace.official;
|
|
129
136
|
const hasPlugins = marketplacePlugins.length > 0;
|
|
130
137
|
|
|
138
|
+
// Special handling: split claude-plugins-official into two sub-sections
|
|
139
|
+
if (marketplace.name === SPLIT_MARKETPLACE && hasPlugins) {
|
|
140
|
+
const anthropicPlugins = marketplacePlugins.filter(
|
|
141
|
+
(p) => p.author?.name?.toLowerCase() === "anthropic",
|
|
142
|
+
);
|
|
143
|
+
const communityPlugins = marketplacePlugins.filter(
|
|
144
|
+
(p) => p.author?.name?.toLowerCase() !== "anthropic",
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Sub-section 1: Anthropic Official (plugins by Anthropic)
|
|
148
|
+
const anthropicCollapsed = collapsed.has(marketplace.name);
|
|
149
|
+
const anthropicHasPlugins = anthropicPlugins.length > 0;
|
|
150
|
+
items.push({
|
|
151
|
+
id: `mp:${marketplace.name}`,
|
|
152
|
+
type: "category",
|
|
153
|
+
label: marketplace.displayName,
|
|
154
|
+
marketplace,
|
|
155
|
+
marketplaceEnabled: isEnabled,
|
|
156
|
+
pluginCount: anthropicPlugins.length,
|
|
157
|
+
isExpanded: !anthropicCollapsed && anthropicHasPlugins,
|
|
158
|
+
});
|
|
159
|
+
if (isEnabled && anthropicHasPlugins && !anthropicCollapsed) {
|
|
160
|
+
for (const plugin of anthropicPlugins) {
|
|
161
|
+
items.push({
|
|
162
|
+
id: `pl:${plugin.id}`,
|
|
163
|
+
type: "plugin",
|
|
164
|
+
label: plugin.name,
|
|
165
|
+
plugin,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Sub-section 2: Community (third-party plugins in same marketplace)
|
|
171
|
+
if (communityPlugins.length > 0) {
|
|
172
|
+
const communityVirtualMp: Marketplace = {
|
|
173
|
+
name: COMMUNITY_VIRTUAL_MARKETPLACE,
|
|
174
|
+
displayName: "Community",
|
|
175
|
+
source: marketplace.source,
|
|
176
|
+
description: "Third-party plugins from the community",
|
|
177
|
+
};
|
|
178
|
+
const communityCollapsed = collapsed.has(COMMUNITY_VIRTUAL_MARKETPLACE);
|
|
179
|
+
items.push({
|
|
180
|
+
id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
|
|
181
|
+
type: "category",
|
|
182
|
+
label: "Community",
|
|
183
|
+
marketplace: communityVirtualMp,
|
|
184
|
+
marketplaceEnabled: true,
|
|
185
|
+
pluginCount: communityPlugins.length,
|
|
186
|
+
isExpanded: !communityCollapsed,
|
|
187
|
+
isCommunitySection: true,
|
|
188
|
+
});
|
|
189
|
+
if (!communityCollapsed) {
|
|
190
|
+
for (const plugin of communityPlugins) {
|
|
191
|
+
items.push({
|
|
192
|
+
id: `pl:${plugin.id}`,
|
|
193
|
+
type: "plugin",
|
|
194
|
+
label: plugin.name,
|
|
195
|
+
plugin,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
131
204
|
// Category header (marketplace)
|
|
132
205
|
items.push({
|
|
133
206
|
id: `mp:${marketplace.name}`,
|
|
@@ -168,34 +241,47 @@ export function PluginsScreen() {
|
|
|
168
241
|
const pluginItems = allItems.filter((item) => item.type === "plugin");
|
|
169
242
|
const fuzzyResults = fuzzyFilter(pluginItems, query, (item) => item.label);
|
|
170
243
|
|
|
171
|
-
//
|
|
172
|
-
const
|
|
244
|
+
// Build a set of matched plugin item ids for O(1) lookup
|
|
245
|
+
const matchedPluginIds = new Set<string>();
|
|
173
246
|
for (const result of fuzzyResults) {
|
|
174
|
-
|
|
175
|
-
|
|
247
|
+
matchedPluginIds.add(result.item.id);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Walk allItems sequentially: track the current category section.
|
|
251
|
+
// For each category, include it only if any plugin under it matched.
|
|
252
|
+
// We build a map from category item id -> whether any plugin below matched.
|
|
253
|
+
const categoryHasMatch = new Map<string, boolean>();
|
|
254
|
+
let currentCategoryId: string | null = null;
|
|
255
|
+
for (const item of allItems) {
|
|
256
|
+
if (item.type === "category") {
|
|
257
|
+
currentCategoryId = item.id;
|
|
258
|
+
if (!categoryHasMatch.has(item.id)) {
|
|
259
|
+
categoryHasMatch.set(item.id, false);
|
|
260
|
+
}
|
|
261
|
+
} else if (item.type === "plugin" && currentCategoryId) {
|
|
262
|
+
if (matchedPluginIds.has(item.id)) {
|
|
263
|
+
categoryHasMatch.set(currentCategoryId, true);
|
|
264
|
+
}
|
|
176
265
|
}
|
|
177
266
|
}
|
|
178
267
|
|
|
179
268
|
const result: ListItem[] = [];
|
|
180
|
-
let
|
|
269
|
+
let currentCatIncluded = false;
|
|
270
|
+
currentCategoryId = null;
|
|
181
271
|
|
|
182
272
|
for (const item of allItems) {
|
|
183
|
-
if (item.type === "category"
|
|
184
|
-
|
|
273
|
+
if (item.type === "category") {
|
|
274
|
+
currentCategoryId = item.id;
|
|
275
|
+
currentCatIncluded = categoryHasMatch.get(item.id) === true;
|
|
276
|
+
if (currentCatIncluded) {
|
|
185
277
|
result.push(item);
|
|
186
|
-
currentMarketplace = item.marketplace.name;
|
|
187
|
-
} else {
|
|
188
|
-
currentMarketplace = null;
|
|
189
278
|
}
|
|
190
|
-
} else if (item.type === "plugin" &&
|
|
191
|
-
if (
|
|
192
|
-
// Check if this plugin matched
|
|
279
|
+
} else if (item.type === "plugin" && currentCatIncluded) {
|
|
280
|
+
if (matchedPluginIds.has(item.id)) {
|
|
193
281
|
const matched = fuzzyResults.find((r) => r.item.id === item.id);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
});
|
|
198
|
-
}
|
|
282
|
+
result.push({ ...item, _matches: matched?.matches } as ListItem & {
|
|
283
|
+
_matches?: number[];
|
|
284
|
+
});
|
|
199
285
|
}
|
|
200
286
|
}
|
|
201
287
|
}
|
|
@@ -317,7 +403,6 @@ export function PluginsScreen() {
|
|
|
317
403
|
else if (event.name === "l") handleScopeToggle("local");
|
|
318
404
|
else if (event.name === "U") handleUpdate();
|
|
319
405
|
else if (event.name === "a") handleUpdateAll();
|
|
320
|
-
else if (event.name === "d") handleUninstall();
|
|
321
406
|
else if (event.name === "s") handleSaveAsProfile();
|
|
322
407
|
// "/" to enter search mode
|
|
323
408
|
else if (event.name === "/") {
|
|
@@ -792,7 +877,7 @@ export function PluginsScreen() {
|
|
|
792
877
|
const scopeLabel =
|
|
793
878
|
scope === "user" ? "User" : scope === "project" ? "Project" : "Local";
|
|
794
879
|
|
|
795
|
-
// Check if installed in this scope
|
|
880
|
+
// Check if installed in this specific scope
|
|
796
881
|
const scopeData =
|
|
797
882
|
scope === "user"
|
|
798
883
|
? plugin.userScope
|
|
@@ -802,6 +887,9 @@ export function PluginsScreen() {
|
|
|
802
887
|
const isInstalledInScope = scopeData?.enabled;
|
|
803
888
|
const installedVersion = scopeData?.version;
|
|
804
889
|
|
|
890
|
+
// Also check if installed in ANY scope (for the toggle behavior)
|
|
891
|
+
const isInstalledAnywhere = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
|
|
892
|
+
|
|
805
893
|
// Check if this scope has an update available
|
|
806
894
|
const hasUpdateInScope =
|
|
807
895
|
isInstalledInScope &&
|
|
@@ -809,14 +897,19 @@ export function PluginsScreen() {
|
|
|
809
897
|
latestVersion !== "0.0.0" &&
|
|
810
898
|
installedVersion !== latestVersion;
|
|
811
899
|
|
|
812
|
-
// Determine action:
|
|
900
|
+
// Determine action: if installed in this scope → uninstall
|
|
901
|
+
// If installed anywhere else but not this scope → uninstall from detected scope
|
|
902
|
+
// Otherwise → install
|
|
813
903
|
let action: "update" | "install" | "uninstall";
|
|
814
904
|
if (isInstalledInScope && hasUpdateInScope) {
|
|
815
905
|
action = "update";
|
|
816
906
|
} else if (isInstalledInScope) {
|
|
817
907
|
action = "uninstall";
|
|
818
|
-
} else {
|
|
908
|
+
} else if (!isInstalledAnywhere) {
|
|
819
909
|
action = "install";
|
|
910
|
+
} else {
|
|
911
|
+
// Installed in a different scope — uninstall from the scope it's actually in
|
|
912
|
+
action = "uninstall";
|
|
820
913
|
}
|
|
821
914
|
|
|
822
915
|
const actionLabel =
|
|
@@ -998,7 +1091,10 @@ export function PluginsScreen() {
|
|
|
998
1091
|
let statusText = "";
|
|
999
1092
|
let statusColor = "green";
|
|
1000
1093
|
if (item.marketplaceEnabled) {
|
|
1001
|
-
if (
|
|
1094
|
+
if (item.isCommunitySection) {
|
|
1095
|
+
statusText = "3rd Party";
|
|
1096
|
+
statusColor = "gray";
|
|
1097
|
+
} else if (mp.name === "claude-plugins-official") {
|
|
1002
1098
|
statusText = "★ Official";
|
|
1003
1099
|
statusColor = "yellow";
|
|
1004
1100
|
} else if (mp.name === "claude-code-plugins") {
|
|
@@ -1046,15 +1142,13 @@ export function PluginsScreen() {
|
|
|
1046
1142
|
let statusIcon = "○";
|
|
1047
1143
|
let statusColor = "gray";
|
|
1048
1144
|
|
|
1145
|
+
const isAnyScope = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
|
|
1049
1146
|
if (plugin.isOrphaned) {
|
|
1050
1147
|
statusIcon = "x";
|
|
1051
1148
|
statusColor = "red";
|
|
1052
|
-
} else if (
|
|
1149
|
+
} else if (isAnyScope) {
|
|
1053
1150
|
statusIcon = "●";
|
|
1054
1151
|
statusColor = "green";
|
|
1055
|
-
} else if (plugin.installedVersion) {
|
|
1056
|
-
statusIcon = "●";
|
|
1057
|
-
statusColor = "yellow";
|
|
1058
1152
|
}
|
|
1059
1153
|
|
|
1060
1154
|
// Build version string
|
|
@@ -1126,6 +1220,7 @@ export function PluginsScreen() {
|
|
|
1126
1220
|
|
|
1127
1221
|
// Get appropriate badge for marketplace type
|
|
1128
1222
|
const getBadge = () => {
|
|
1223
|
+
if (selectedItem.isCommunitySection) return " 3rd Party";
|
|
1129
1224
|
if (mp.name === "claude-plugins-official") return " ★";
|
|
1130
1225
|
if (mp.name === "claude-code-plugins") return " ⚠";
|
|
1131
1226
|
if (mp.official) return " ★";
|
|
@@ -1178,7 +1273,7 @@ export function PluginsScreen() {
|
|
|
1178
1273
|
|
|
1179
1274
|
if (selectedItem.type === "plugin" && selectedItem.plugin) {
|
|
1180
1275
|
const plugin = selectedItem.plugin;
|
|
1181
|
-
const isInstalled = plugin.enabled || plugin.
|
|
1276
|
+
const isInstalled = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
|
|
1182
1277
|
|
|
1183
1278
|
// Orphaned/deprecated plugin
|
|
1184
1279
|
if (plugin.isOrphaned) {
|
|
@@ -1345,13 +1440,6 @@ export function PluginsScreen() {
|
|
|
1345
1440
|
<text> Update to v{plugin.version}</text>
|
|
1346
1441
|
</box>
|
|
1347
1442
|
)}
|
|
1348
|
-
<box>
|
|
1349
|
-
<text bg="red" fg="white">
|
|
1350
|
-
{" "}
|
|
1351
|
-
d{" "}
|
|
1352
|
-
</text>
|
|
1353
|
-
<text> Uninstall</text>
|
|
1354
|
-
</box>
|
|
1355
1443
|
</box>
|
|
1356
1444
|
)}
|
|
1357
1445
|
</box>
|
|
@@ -1363,7 +1451,7 @@ export function PluginsScreen() {
|
|
|
1363
1451
|
|
|
1364
1452
|
const footerHints = isSearchActive
|
|
1365
1453
|
? "type to filter │ Enter:done │ Esc:clear"
|
|
1366
|
-
: "u/p/l:
|
|
1454
|
+
: "u/p/l:toggle │ U:update │ a:all │ s:profile │ /:search";
|
|
1367
1455
|
|
|
1368
1456
|
// Calculate status for subtitle
|
|
1369
1457
|
const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
|