claudeup 4.35.1 → 4.36.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.
Files changed (58) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/scope-squares.test.tsx +165 -0
  3. package/src/__tests__/theme-adaptive-colors.test.ts +307 -0
  4. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  5. package/src/main.tsx +21 -5
  6. package/src/opentui.d.ts +21 -12
  7. package/src/ui/App.tsx +19 -12
  8. package/src/ui/adapters/pluginsAdapter.ts +174 -168
  9. package/src/ui/adapters/settingsAdapter.ts +119 -116
  10. package/src/ui/adapters/skillsAdapter.ts +203 -196
  11. package/src/ui/components/CategoryHeader.tsx +9 -8
  12. package/src/ui/components/EmptyFilterState.tsx +10 -5
  13. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  14. package/src/ui/components/ScopeIndicator.tsx +10 -6
  15. package/src/ui/components/ScrollableList.tsx +3 -2
  16. package/src/ui/components/SearchInput.tsx +2 -1
  17. package/src/ui/components/StyledText.tsx +5 -4
  18. package/src/ui/components/TabBar.tsx +4 -3
  19. package/src/ui/components/layout/FooterHints.tsx +37 -30
  20. package/src/ui/components/layout/Panel.tsx +6 -5
  21. package/src/ui/components/layout/ProgressBar.tsx +7 -6
  22. package/src/ui/components/layout/ScopeTabs.tsx +6 -5
  23. package/src/ui/components/layout/ScreenLayout.tsx +27 -24
  24. package/src/ui/components/layout/index.ts +3 -3
  25. package/src/ui/components/modals/ConfirmModal.tsx +12 -11
  26. package/src/ui/components/modals/InputModal.tsx +14 -6
  27. package/src/ui/components/modals/LoadingModal.tsx +6 -5
  28. package/src/ui/components/modals/MessageModal.tsx +9 -8
  29. package/src/ui/components/modals/SelectModal.tsx +11 -7
  30. package/src/ui/components/modals/VersionMismatchModal.tsx +14 -16
  31. package/src/ui/components/primitives/ActionHints.tsx +26 -26
  32. package/src/ui/components/primitives/DetailSection.tsx +13 -12
  33. package/src/ui/components/primitives/KeyValueLine.tsx +9 -8
  34. package/src/ui/components/primitives/ListCategoryRow.tsx +25 -27
  35. package/src/ui/components/primitives/MetaText.tsx +3 -3
  36. package/src/ui/components/primitives/ScopeDetail.tsx +48 -48
  37. package/src/ui/components/primitives/ScopeSquares.tsx +47 -22
  38. package/src/ui/components/primitives/SelectableRow.tsx +22 -16
  39. package/src/ui/hooks/useGitignoreModal.ts +78 -74
  40. package/src/ui/registry.ts +11 -11
  41. package/src/ui/renderers/cliToolRenderers.tsx +260 -203
  42. package/src/ui/renderers/gitignoreRenderers.tsx +43 -42
  43. package/src/ui/renderers/mcpRenderers.tsx +121 -117
  44. package/src/ui/renderers/pluginRenderers.tsx +528 -471
  45. package/src/ui/renderers/profileRenderers.tsx +346 -300
  46. package/src/ui/renderers/settingsRenderers.tsx +183 -176
  47. package/src/ui/renderers/skillRenderers.tsx +410 -326
  48. package/src/ui/screens/AliasScreen.tsx +1336 -1309
  49. package/src/ui/screens/CliToolsScreen.tsx +92 -40
  50. package/src/ui/screens/EnvVarsScreen.tsx +19 -13
  51. package/src/ui/screens/GitignoreScreen.tsx +510 -493
  52. package/src/ui/screens/McpRegistryScreen.tsx +28 -21
  53. package/src/ui/screens/McpScreen.tsx +12 -3
  54. package/src/ui/screens/PluginsScreen.tsx +17 -7
  55. package/src/ui/screens/ProfilesScreen.tsx +39 -23
  56. package/src/ui/screens/SkillsScreen.tsx +832 -688
  57. package/src/ui/theme-mode.ts +73 -0
  58. package/src/ui/theme.ts +147 -53
package/src/opentui.d.ts CHANGED
@@ -8,6 +8,15 @@
8
8
  */
9
9
 
10
10
  import type { ReactNode } from "react";
11
+ import type { RGBA } from "@opentui/core";
12
+
13
+ /**
14
+ * What OpenTUI actually accepts for a colour: a CSS string OR an RGBA, which is
15
+ * how adaptive colours are expressed (RGBA.defaultForeground(), RGBA.fromIndex()).
16
+ * These declarations previously said `string`, which made the terminal-resolved
17
+ * forms a type error and quietly pushed every call site towards absolute hex.
18
+ */
19
+ type ColorProp = string | RGBA;
11
20
 
12
21
  declare global {
13
22
  namespace JSX {
@@ -28,7 +37,7 @@ declare global {
28
37
  u: { children?: ReactNode };
29
38
  dim: { children?: ReactNode };
30
39
  a: { href?: string; children?: ReactNode };
31
- span: { fg?: string; bg?: string; children?: ReactNode };
40
+ span: { fg?: ColorProp; bg?: ColorProp; children?: ReactNode };
32
41
  }
33
42
  }
34
43
  }
@@ -37,12 +46,12 @@ interface BaseBoxProps {
37
46
  // Borders
38
47
  border?: boolean;
39
48
  borderStyle?: "single" | "double" | "rounded" | "bold";
40
- borderColor?: string;
49
+ borderColor?: ColorProp;
41
50
  title?: string;
42
51
  titleAlignment?: "left" | "center" | "right";
43
52
 
44
53
  // Colors
45
- backgroundColor?: string;
54
+ backgroundColor?: ColorProp;
46
55
 
47
56
  // Layout (Flexbox)
48
57
  flexDirection?: "row" | "column";
@@ -94,8 +103,8 @@ interface BoxProps extends BaseBoxProps {
94
103
 
95
104
  interface TextProps {
96
105
  content?: string;
97
- fg?: string;
98
- bg?: string;
106
+ fg?: ColorProp;
107
+ bg?: ColorProp;
99
108
  selectable?: boolean;
100
109
  children?: ReactNode;
101
110
  }
@@ -108,10 +117,10 @@ interface InputProps {
108
117
  placeholder?: string;
109
118
  focused?: boolean;
110
119
  width?: number;
111
- backgroundColor?: string;
112
- textColor?: string;
113
- cursorColor?: string;
114
- focusedBackgroundColor?: string;
120
+ backgroundColor?: ColorProp;
121
+ textColor?: ColorProp;
122
+ cursorColor?: ColorProp;
123
+ focusedBackgroundColor?: ColorProp;
115
124
  }
116
125
 
117
126
  interface SelectOption {
@@ -150,8 +159,8 @@ interface ScrollboxProps {
150
159
  scrollbarOptions?: {
151
160
  showArrows?: boolean;
152
161
  trackOptions?: {
153
- foregroundColor?: string;
154
- backgroundColor?: string;
162
+ foregroundColor?: ColorProp;
163
+ backgroundColor?: ColorProp;
155
164
  };
156
165
  };
157
166
  };
@@ -161,7 +170,7 @@ interface ScrollboxProps {
161
170
  interface AsciiFontProps {
162
171
  text: string;
163
172
  font?: "tiny" | "block" | "slick" | "shade";
164
- color?: string;
173
+ color?: ColorProp;
165
174
  }
166
175
 
167
176
  interface CodeProps {
package/src/ui/App.tsx CHANGED
@@ -49,6 +49,7 @@ import {
49
49
  } from "../services/version-check.js";
50
50
  import { useKeyboardHandler } from "./hooks/useKeyboardHandler.js";
51
51
  import { ProgressBar } from "./components/layout/ProgressBar.js";
52
+ import { theme } from "./theme.js";
52
53
 
53
54
  export const VERSION = getCurrentVersion();
54
55
 
@@ -238,15 +239,15 @@ function UpdateBanner({ result }: { result: VersionCheckResult }) {
238
239
 
239
240
  return (
240
241
  <box paddingLeft={1} paddingRight={1}>
241
- <text bg="yellow" fg="black">
242
+ <text bg={theme.colors.warning} fg={theme.hints.fg}>
242
243
  <strong> UPDATE </strong>
243
244
  </text>
244
- <text fg="yellow">
245
+ <text fg={theme.colors.warning}>
245
246
  {" "}
246
247
  v{result.currentVersion} → v{result.latestVersion}
247
248
  </text>
248
- <text fg="gray"> Run: </text>
249
- <text fg="cyan">claudeup update</text>
249
+ <text fg={theme.colors.muted}> Run: </text>
250
+ <text fg={theme.colors.info}>claudeup update</text>
250
251
  </box>
251
252
  );
252
253
  }
@@ -299,12 +300,12 @@ function AppContentInner({
299
300
  {updateInfo?.updateAvailable && <UpdateBanner result={updateInfo} />}
300
301
  {recoveryReport && (
301
302
  <box paddingLeft={1} paddingRight={1}>
302
- <text fg="green">✓ Fixed: {recoveryReport}</text>
303
+ <text fg={theme.colors.success}>✓ Fixed: {recoveryReport}</text>
303
304
  </box>
304
305
  )}
305
306
  {showDebug && (
306
307
  <box paddingLeft={1} paddingRight={1}>
307
- <text fg="#888888">
308
+ <text fg={theme.colors.muted}>
308
309
  DEBUG: {dimensions.terminalWidth}x{dimensions.terminalHeight} |
309
310
  content={dimensions.contentHeight} | screen=
310
311
  {state.currentRoute.screen}
@@ -340,7 +341,9 @@ function AppContent({ onExit }: AppContentProps) {
340
341
  const [showDebug, setShowDebug] = useState(false);
341
342
  const [updateInfo, setUpdateInfo] = useState<VersionCheckResult | null>(null);
342
343
  const [recoveryReport, setRecoveryReport] = useState<string | null>(null);
343
- const [mismatchData, setMismatchData] = useState<VersionMismatchInfo[] | null>(null);
344
+ const [mismatchData, setMismatchData] = useState<
345
+ VersionMismatchInfo[] | null
346
+ >(null);
344
347
  const mismatchModal = useMismatchModal();
345
348
  const [gitignoreData, setGitignoreData] = useState<{
346
349
  violationCount: number;
@@ -472,8 +475,13 @@ function AppContent({ onExit }: AppContentProps) {
472
475
  const result = await checkGitignore(process.cwd());
473
476
  if (result.violationCount > 0) {
474
477
  const s = await loadGitignoreState(process.cwd());
475
- const safeCount = s.violations.filter((v) => v.severity === "safe").length;
476
- setGitignoreData({ violationCount: result.violationCount, safeCount });
478
+ const safeCount = s.violations.filter(
479
+ (v) => v.severity === "safe",
480
+ ).length;
481
+ setGitignoreData({
482
+ violationCount: result.violationCount,
483
+ safeCount,
484
+ });
477
485
  }
478
486
  } catch {
479
487
  // non-fatal
@@ -491,8 +499,7 @@ function AppContent({ onExit }: AppContentProps) {
491
499
  }, [dispatch]);
492
500
 
493
501
  // Count transient banners for dimension calculation
494
- const transientBannerCount =
495
- recoveryReport ? 1 : 0;
502
+ const transientBannerCount = recoveryReport ? 1 : 0;
496
503
 
497
504
  return (
498
505
  <DimensionsProvider
@@ -507,7 +514,7 @@ function AppContent({ onExit }: AppContentProps) {
507
514
  updateInfo={updateInfo}
508
515
  onExit={onExit}
509
516
  recoveryReport={recoveryReport}
510
- />
517
+ />
511
518
  </DimensionsProvider>
512
519
  );
513
520
  }
@@ -2,34 +2,35 @@ import type { Marketplace } from "../../types/index.js";
2
2
  import type { PluginInfo } from "../../services/plugin-manager.js";
3
3
 
4
4
  // Virtual marketplace name for the community sub-section of claude-plugins-official
5
- export const COMMUNITY_VIRTUAL_MARKETPLACE = "claude-plugins-official:community";
5
+ export const COMMUNITY_VIRTUAL_MARKETPLACE =
6
+ "claude-plugins-official:community";
6
7
  // The marketplace that gets split into Anthropic Official + Community sections
7
8
  export const SPLIT_MARKETPLACE = "claude-plugins-official";
8
9
 
9
10
  // ─── Item types ───────────────────────────────────────────────────────────────
10
11
 
11
12
  export interface PluginCategoryItem {
12
- id: string;
13
- kind: "category";
14
- label: string;
15
- marketplace: Marketplace;
16
- marketplaceEnabled: boolean;
17
- pluginCount: number;
18
- isExpanded: boolean;
19
- isCommunitySection?: boolean;
20
- /** Visual tone for the category row */
21
- tone: "yellow" | "gray" | "green" | "red" | "purple" | "teal" | "byline";
22
- /** Badge text shown on category row (e.g. "★ Official") */
23
- badge?: string;
13
+ id: string;
14
+ kind: "category";
15
+ label: string;
16
+ marketplace: Marketplace;
17
+ marketplaceEnabled: boolean;
18
+ pluginCount: number;
19
+ isExpanded: boolean;
20
+ isCommunitySection?: boolean;
21
+ /** Visual tone for the category row */
22
+ tone: "yellow" | "gray" | "green" | "red" | "purple" | "teal" | "byline";
23
+ /** Badge text shown on category row (e.g. "★ Official") */
24
+ badge?: string;
24
25
  }
25
26
 
26
27
  export interface PluginPluginItem {
27
- id: string;
28
- kind: "plugin";
29
- label: string;
30
- plugin: PluginInfo;
31
- /** Fuzzy match highlight indices, if search is active */
32
- matches?: number[];
28
+ id: string;
29
+ kind: "plugin";
30
+ label: string;
31
+ plugin: PluginInfo;
32
+ /** Fuzzy match highlight indices, if search is active */
33
+ matches?: number[];
33
34
  }
34
35
 
35
36
  export type PluginBrowserItem = PluginCategoryItem | PluginPluginItem;
@@ -37,38 +38,38 @@ export type PluginBrowserItem = PluginCategoryItem | PluginPluginItem;
37
38
  // ─── Adapter ─────────────────────────────────────────────────────────────────
38
39
 
39
40
  export interface BuildPluginBrowserItemsArgs {
40
- marketplaces: Marketplace[];
41
- plugins: PluginInfo[];
42
- collapsedMarketplaces: Set<string>;
41
+ marketplaces: Marketplace[];
42
+ plugins: PluginInfo[];
43
+ collapsedMarketplaces: Set<string>;
43
44
  }
44
45
 
45
46
  /**
46
47
  * Derives tone and badge for a category item based on marketplace identity.
47
48
  */
48
49
  function categoryStyling(
49
- mp: Marketplace,
50
- isCommunitySection: boolean,
51
- marketplaceEnabled: boolean,
50
+ mp: Marketplace,
51
+ isCommunitySection: boolean,
52
+ marketplaceEnabled: boolean,
52
53
  ): { tone: PluginCategoryItem["tone"]; badge?: string } {
53
- if (!marketplaceEnabled) {
54
- return { tone: "gray" };
55
- }
56
- if (isCommunitySection) {
57
- return { tone: "gray", badge: "3rd Party" };
58
- }
59
- if (mp.name === "claude-plugins-official") {
60
- return { tone: "yellow", badge: "★ Official" };
61
- }
62
- if (mp.name === "claude-code-plugins") {
63
- return { tone: "red", badge: "⚠ Deprecated" };
64
- }
65
- if (mp.official) {
66
- return { tone: "yellow", badge: "★ Official" };
67
- }
68
- if (isMadAppGang(mp)) {
69
- return { tone: "byline", badge: "by MadAppGang" };
70
- }
71
- return { tone: "green", badge: "✓ Added" };
54
+ if (!marketplaceEnabled) {
55
+ return { tone: "gray" };
56
+ }
57
+ if (isCommunitySection) {
58
+ return { tone: "gray", badge: "3rd Party" };
59
+ }
60
+ if (mp.name === "claude-plugins-official") {
61
+ return { tone: "yellow", badge: "★ Official" };
62
+ }
63
+ if (mp.name === "claude-code-plugins") {
64
+ return { tone: "red", badge: "⚠ Deprecated" };
65
+ }
66
+ if (mp.official) {
67
+ return { tone: "yellow", badge: "★ Official" };
68
+ }
69
+ if (isMadAppGang(mp)) {
70
+ return { tone: "byline", badge: "by MadAppGang" };
71
+ }
72
+ return { tone: "green", badge: "✓ Added" };
72
73
  }
73
74
 
74
75
  /**
@@ -78,7 +79,7 @@ function categoryStyling(
78
79
  * marketplace sort uses to float MadAppGang entries to the top.
79
80
  */
80
81
  function isMadAppGang(mp: Marketplace): boolean {
81
- return (mp.source.repo ?? "").toLowerCase().startsWith("madappgang/");
82
+ return (mp.source.repo ?? "").toLowerCase().startsWith("madappgang/");
82
83
  }
83
84
 
84
85
  /**
@@ -86,128 +87,133 @@ function isMadAppGang(mp: Marketplace): boolean {
86
87
  * Extracted from PluginsScreen so it can be tested independently.
87
88
  */
88
89
  export function buildPluginBrowserItems({
89
- marketplaces,
90
- plugins,
91
- collapsedMarketplaces,
90
+ marketplaces,
91
+ plugins,
92
+ collapsedMarketplaces,
92
93
  }: BuildPluginBrowserItemsArgs): PluginBrowserItem[] {
93
- const pluginsByMarketplace = new Map<string, PluginInfo[]>();
94
- for (const plugin of plugins) {
95
- const existing = pluginsByMarketplace.get(plugin.marketplace) || [];
96
- existing.push(plugin);
97
- pluginsByMarketplace.set(plugin.marketplace, existing);
98
- }
99
-
100
- // Sort marketplaces: our own (magus*) first, deprecated last, others between.
101
- const rank = (m: Marketplace) =>
102
- m.owned ? 0 : m.deprecated || m.name === "claude-code-plugins" ? 2 : 1;
103
- const sortedMarketplaces = [...marketplaces].sort((a, b) => rank(a) - rank(b));
104
-
105
- const items: PluginBrowserItem[] = [];
106
-
107
- for (const marketplace of sortedMarketplaces) {
108
- const marketplacePlugins = pluginsByMarketplace.get(marketplace.name) || [];
109
- const isCollapsed = collapsedMarketplaces.has(marketplace.name);
110
- const isEnabled = marketplacePlugins.length > 0 || !!marketplace.official;
111
- const hasPlugins = marketplacePlugins.length > 0;
112
-
113
- // Special handling: split claude-plugins-official into two sub-sections
114
- if (marketplace.name === SPLIT_MARKETPLACE && hasPlugins) {
115
- const anthropicPlugins = marketplacePlugins.filter(
116
- (p) => p.author?.name?.toLowerCase() === "anthropic",
117
- );
118
- const communityPlugins = marketplacePlugins.filter(
119
- (p) => p.author?.name?.toLowerCase() !== "anthropic",
120
- );
121
-
122
- // Sub-section 1: Anthropic Official (plugins by Anthropic)
123
- const anthropicCollapsed = collapsedMarketplaces.has(marketplace.name);
124
- const anthropicHasPlugins = anthropicPlugins.length > 0;
125
- const anthropicStyle = categoryStyling(marketplace, false, isEnabled);
126
- items.push({
127
- id: `mp:${marketplace.name}`,
128
- kind: "category",
129
- label: marketplace.displayName,
130
- marketplace,
131
- marketplaceEnabled: isEnabled,
132
- pluginCount: anthropicPlugins.length,
133
- isExpanded: !anthropicCollapsed && anthropicHasPlugins,
134
- tone: anthropicStyle.tone,
135
- badge: anthropicStyle.badge,
136
- });
137
- if (isEnabled && anthropicHasPlugins && !anthropicCollapsed) {
138
- for (const plugin of anthropicPlugins) {
139
- items.push({
140
- id: `pl:${plugin.id}`,
141
- kind: "plugin",
142
- label: plugin.name,
143
- plugin,
144
- });
145
- }
146
- }
147
-
148
- // Sub-section 2: Community (third-party plugins in same marketplace)
149
- if (communityPlugins.length > 0) {
150
- const communityVirtualMp: Marketplace = {
151
- name: COMMUNITY_VIRTUAL_MARKETPLACE,
152
- displayName: "Anthropic Official — 3rd Party",
153
- source: marketplace.source,
154
- description: "Third-party plugins in the Anthropic Official marketplace",
155
- };
156
- const communityCollapsed = collapsedMarketplaces.has(COMMUNITY_VIRTUAL_MARKETPLACE);
157
- const communityStyle = categoryStyling(communityVirtualMp, true, true);
158
- items.push({
159
- id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
160
- kind: "category",
161
- label: "Anthropic Official — 3rd Party",
162
- marketplace: communityVirtualMp,
163
- marketplaceEnabled: true,
164
- pluginCount: communityPlugins.length,
165
- isExpanded: !communityCollapsed,
166
- isCommunitySection: true,
167
- tone: communityStyle.tone,
168
- badge: communityStyle.badge,
169
- });
170
- if (!communityCollapsed) {
171
- for (const plugin of communityPlugins) {
172
- items.push({
173
- id: `pl:${plugin.id}`,
174
- kind: "plugin",
175
- label: plugin.name,
176
- plugin,
177
- });
178
- }
179
- }
180
- }
181
-
182
- continue;
183
- }
184
-
185
- // Category header (marketplace)
186
- const style = categoryStyling(marketplace, false, isEnabled);
187
- items.push({
188
- id: `mp:${marketplace.name}`,
189
- kind: "category",
190
- label: marketplace.displayName,
191
- marketplace,
192
- marketplaceEnabled: isEnabled,
193
- pluginCount: marketplacePlugins.length,
194
- isExpanded: !isCollapsed && hasPlugins,
195
- tone: style.tone,
196
- badge: style.badge,
197
- });
198
-
199
- // Plugins under this marketplace (if expanded)
200
- if (isEnabled && hasPlugins && !isCollapsed) {
201
- for (const plugin of marketplacePlugins) {
202
- items.push({
203
- id: `pl:${plugin.id}`,
204
- kind: "plugin",
205
- label: plugin.name,
206
- plugin,
207
- });
208
- }
209
- }
210
- }
211
-
212
- return items;
94
+ const pluginsByMarketplace = new Map<string, PluginInfo[]>();
95
+ for (const plugin of plugins) {
96
+ const existing = pluginsByMarketplace.get(plugin.marketplace) || [];
97
+ existing.push(plugin);
98
+ pluginsByMarketplace.set(plugin.marketplace, existing);
99
+ }
100
+
101
+ // Sort marketplaces: our own (magus*) first, deprecated last, others between.
102
+ const rank = (m: Marketplace) =>
103
+ m.owned ? 0 : m.deprecated || m.name === "claude-code-plugins" ? 2 : 1;
104
+ const sortedMarketplaces = [...marketplaces].sort(
105
+ (a, b) => rank(a) - rank(b),
106
+ );
107
+
108
+ const items: PluginBrowserItem[] = [];
109
+
110
+ for (const marketplace of sortedMarketplaces) {
111
+ const marketplacePlugins = pluginsByMarketplace.get(marketplace.name) || [];
112
+ const isCollapsed = collapsedMarketplaces.has(marketplace.name);
113
+ const isEnabled = marketplacePlugins.length > 0 || !!marketplace.official;
114
+ const hasPlugins = marketplacePlugins.length > 0;
115
+
116
+ // Special handling: split claude-plugins-official into two sub-sections
117
+ if (marketplace.name === SPLIT_MARKETPLACE && hasPlugins) {
118
+ const anthropicPlugins = marketplacePlugins.filter(
119
+ (p) => p.author?.name?.toLowerCase() === "anthropic",
120
+ );
121
+ const communityPlugins = marketplacePlugins.filter(
122
+ (p) => p.author?.name?.toLowerCase() !== "anthropic",
123
+ );
124
+
125
+ // Sub-section 1: Anthropic Official (plugins by Anthropic)
126
+ const anthropicCollapsed = collapsedMarketplaces.has(marketplace.name);
127
+ const anthropicHasPlugins = anthropicPlugins.length > 0;
128
+ const anthropicStyle = categoryStyling(marketplace, false, isEnabled);
129
+ items.push({
130
+ id: `mp:${marketplace.name}`,
131
+ kind: "category",
132
+ label: marketplace.displayName,
133
+ marketplace,
134
+ marketplaceEnabled: isEnabled,
135
+ pluginCount: anthropicPlugins.length,
136
+ isExpanded: !anthropicCollapsed && anthropicHasPlugins,
137
+ tone: anthropicStyle.tone,
138
+ badge: anthropicStyle.badge,
139
+ });
140
+ if (isEnabled && anthropicHasPlugins && !anthropicCollapsed) {
141
+ for (const plugin of anthropicPlugins) {
142
+ items.push({
143
+ id: `pl:${plugin.id}`,
144
+ kind: "plugin",
145
+ label: plugin.name,
146
+ plugin,
147
+ });
148
+ }
149
+ }
150
+
151
+ // Sub-section 2: Community (third-party plugins in same marketplace)
152
+ if (communityPlugins.length > 0) {
153
+ const communityVirtualMp: Marketplace = {
154
+ name: COMMUNITY_VIRTUAL_MARKETPLACE,
155
+ displayName: "Anthropic Official 3rd Party",
156
+ source: marketplace.source,
157
+ description:
158
+ "Third-party plugins in the Anthropic Official marketplace",
159
+ };
160
+ const communityCollapsed = collapsedMarketplaces.has(
161
+ COMMUNITY_VIRTUAL_MARKETPLACE,
162
+ );
163
+ const communityStyle = categoryStyling(communityVirtualMp, true, true);
164
+ items.push({
165
+ id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
166
+ kind: "category",
167
+ label: "Anthropic Official — 3rd Party",
168
+ marketplace: communityVirtualMp,
169
+ marketplaceEnabled: true,
170
+ pluginCount: communityPlugins.length,
171
+ isExpanded: !communityCollapsed,
172
+ isCommunitySection: true,
173
+ tone: communityStyle.tone,
174
+ badge: communityStyle.badge,
175
+ });
176
+ if (!communityCollapsed) {
177
+ for (const plugin of communityPlugins) {
178
+ items.push({
179
+ id: `pl:${plugin.id}`,
180
+ kind: "plugin",
181
+ label: plugin.name,
182
+ plugin,
183
+ });
184
+ }
185
+ }
186
+ }
187
+
188
+ continue;
189
+ }
190
+
191
+ // Category header (marketplace)
192
+ const style = categoryStyling(marketplace, false, isEnabled);
193
+ items.push({
194
+ id: `mp:${marketplace.name}`,
195
+ kind: "category",
196
+ label: marketplace.displayName,
197
+ marketplace,
198
+ marketplaceEnabled: isEnabled,
199
+ pluginCount: marketplacePlugins.length,
200
+ isExpanded: !isCollapsed && hasPlugins,
201
+ tone: style.tone,
202
+ badge: style.badge,
203
+ });
204
+
205
+ // Plugins under this marketplace (if expanded)
206
+ if (isEnabled && hasPlugins && !isCollapsed) {
207
+ for (const plugin of marketplacePlugins) {
208
+ items.push({
209
+ id: `pl:${plugin.id}`,
210
+ kind: "plugin",
211
+ label: plugin.name,
212
+ plugin,
213
+ });
214
+ }
215
+ }
216
+ }
217
+
218
+ return items;
213
219
  }