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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "3.10.0",
3
+ "version": "3.12.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -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
- // Include parent categories for matched plugins
117
- const matchedMarketplaces = new Set();
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
- if (result.item.plugin) {
120
- matchedMarketplaces.add(result.item.plugin.marketplace);
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 currentMarketplace = null;
202
+ let currentCatIncluded = false;
203
+ currentCategoryId = null;
125
204
  for (const item of allItems) {
126
- if (item.type === "category" && item.marketplace) {
127
- if (matchedMarketplaces.has(item.marketplace.name)) {
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" && item.plugin) {
136
- if (currentMarketplace === item.plugin.marketplace) {
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
- if (matched) {
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: update if available, otherwise toggle install/uninstall
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 (mp.name === "claude-plugins-official") {
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 (plugin.enabled) {
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.installedVersion;
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 && (_jsxs("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] })] })), _jsxs("box", { children: [_jsxs("text", { bg: "red", fg: "white", children: [" ", "d", " "] }), _jsx("text", { children: " Uninstall" })] })] }))] }));
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:scope │ U:update │ a:all │ d:remove │ s:profile │ /:search";
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
- // Include parent categories for matched plugins
172
- const matchedMarketplaces = new Set<string>();
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
- if (result.item.plugin) {
175
- matchedMarketplaces.add(result.item.plugin.marketplace);
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 currentMarketplace: string | null = null;
269
+ let currentCatIncluded = false;
270
+ currentCategoryId = null;
181
271
 
182
272
  for (const item of allItems) {
183
- if (item.type === "category" && item.marketplace) {
184
- if (matchedMarketplaces.has(item.marketplace.name)) {
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" && item.plugin) {
191
- if (currentMarketplace === item.plugin.marketplace) {
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
- if (matched) {
195
- result.push({ ...item, _matches: matched.matches } as ListItem & {
196
- _matches?: number[];
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: update if available, otherwise toggle install/uninstall
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 (mp.name === "claude-plugins-official") {
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 (plugin.enabled) {
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.installedVersion;
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:scope │ U:update │ a:all │ d:remove │ s:profile │ /:search";
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";