claudeup 3.16.0 → 4.0.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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/data/predefined-profiles.js +191 -0
  3. package/src/data/predefined-profiles.ts +205 -0
  4. package/src/ui/adapters/pluginsAdapter.js +139 -0
  5. package/src/ui/adapters/pluginsAdapter.ts +202 -0
  6. package/src/ui/adapters/settingsAdapter.js +111 -0
  7. package/src/ui/adapters/settingsAdapter.ts +165 -0
  8. package/src/ui/components/ScrollableList.js +4 -4
  9. package/src/ui/components/ScrollableList.tsx +4 -4
  10. package/src/ui/components/SearchInput.js +2 -2
  11. package/src/ui/components/SearchInput.tsx +3 -3
  12. package/src/ui/components/StyledText.js +1 -1
  13. package/src/ui/components/StyledText.tsx +5 -1
  14. package/src/ui/components/layout/ProgressBar.js +1 -1
  15. package/src/ui/components/layout/ProgressBar.tsx +1 -5
  16. package/src/ui/components/modals/InputModal.tsx +1 -6
  17. package/src/ui/components/modals/LoadingModal.js +1 -1
  18. package/src/ui/components/modals/LoadingModal.tsx +1 -3
  19. package/src/ui/hooks/index.js +3 -3
  20. package/src/ui/hooks/index.ts +3 -3
  21. package/src/ui/hooks/useKeyboard.ts +1 -3
  22. package/src/ui/hooks/useKeyboardHandler.js +9 -9
  23. package/src/ui/hooks/useKeyboardHandler.ts +9 -9
  24. package/src/ui/renderers/cliToolRenderers.js +33 -0
  25. package/src/ui/renderers/cliToolRenderers.tsx +153 -0
  26. package/src/ui/renderers/mcpRenderers.js +26 -0
  27. package/src/ui/renderers/mcpRenderers.tsx +145 -0
  28. package/src/ui/renderers/pluginRenderers.js +124 -0
  29. package/src/ui/renderers/pluginRenderers.tsx +362 -0
  30. package/src/ui/renderers/profileRenderers.js +172 -0
  31. package/src/ui/renderers/profileRenderers.tsx +410 -0
  32. package/src/ui/renderers/settingsRenderers.js +69 -0
  33. package/src/ui/renderers/settingsRenderers.tsx +205 -0
  34. package/src/ui/screens/CliToolsScreen.js +14 -58
  35. package/src/ui/screens/CliToolsScreen.tsx +36 -196
  36. package/src/ui/screens/EnvVarsScreen.js +12 -168
  37. package/src/ui/screens/EnvVarsScreen.tsx +16 -327
  38. package/src/ui/screens/McpScreen.js +12 -62
  39. package/src/ui/screens/McpScreen.tsx +21 -190
  40. package/src/ui/screens/PluginsScreen.js +52 -425
  41. package/src/ui/screens/PluginsScreen.tsx +70 -758
  42. package/src/ui/screens/ProfilesScreen.js +104 -68
  43. package/src/ui/screens/ProfilesScreen.tsx +147 -221
  44. package/src/ui/screens/SkillsScreen.js +16 -16
  45. package/src/ui/screens/SkillsScreen.tsx +20 -23
@@ -4,10 +4,9 @@ import { useApp, useModal, useProgress } from "../state/AppContext.js";
4
4
  import { useDimensions } from "../state/DimensionsContext.js";
5
5
  import { useKeyboard } from "../hooks/useKeyboard.js";
6
6
  import { ScreenLayout } from "../components/layout/index.js";
7
- import { CategoryHeader } from "../components/CategoryHeader.js";
8
7
  import { ScrollableList } from "../components/ScrollableList.js";
9
8
  import { EmptyFilterState } from "../components/EmptyFilterState.js";
10
- import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
9
+ import { fuzzyFilter } from "../../utils/fuzzy-search.js";
11
10
  import { getAllMarketplaces } from "../../data/marketplaces.js";
12
11
  import { getAvailablePlugins, refreshAllMarketplaces, clearMarketplaceCache, getLocalMarketplacesInfo, } from "../../services/plugin-manager.js";
13
12
  import { setMcpEnvVar, getMcpEnvVars, readSettings, saveGlobalInstalledPluginVersion, saveLocalInstalledPluginVersion, } from "../../services/claude-settings.js";
@@ -16,10 +15,8 @@ import { saveInstalledPluginVersion } from "../../services/plugin-manager.js";
16
15
  import { installPlugin as cliInstallPlugin, uninstallPlugin as cliUninstallPlugin, updatePlugin as cliUpdatePlugin, } from "../../services/claude-cli.js";
17
16
  import { getPluginEnvRequirements, getPluginSourcePath, } from "../../services/plugin-mcp-config.js";
18
17
  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";
18
+ import { buildPluginBrowserItems, } from "../adapters/pluginsAdapter.js";
19
+ import { renderPluginRow, renderPluginDetail } from "../renderers/pluginRenderers.js";
23
20
  export function PluginsScreen() {
24
21
  const { state, dispatch } = useApp();
25
22
  const { plugins: pluginsState } = state;
@@ -35,7 +32,6 @@ export function PluginsScreen() {
35
32
  try {
36
33
  const localMarketplaces = await getLocalMarketplacesInfo();
37
34
  const allMarketplaces = getAllMarketplaces(localMarketplaces);
38
- // Always use getAvailablePlugins which fetches all scope data
39
35
  const pluginData = await getAvailablePlugins(state.projectPath);
40
36
  dispatch({
41
37
  type: "PLUGINS_DATA_SUCCESS",
@@ -60,108 +56,11 @@ export function PluginsScreen() {
60
56
  pluginsState.plugins.status !== "success") {
61
57
  return [];
62
58
  }
63
- const marketplaces = pluginsState.marketplaces.data;
64
- const plugins = pluginsState.plugins.data;
65
- const collapsed = pluginsState.collapsedMarketplaces;
66
- const pluginsByMarketplace = new Map();
67
- for (const plugin of plugins) {
68
- const existing = pluginsByMarketplace.get(plugin.marketplace) || [];
69
- existing.push(plugin);
70
- pluginsByMarketplace.set(plugin.marketplace, existing);
71
- }
72
- // Sort marketplaces: deprecated ones go to the bottom
73
- const sortedMarketplaces = [...marketplaces].sort((a, b) => {
74
- const aDeprecated = a.name === "claude-code-plugins" ? 1 : 0;
75
- const bDeprecated = b.name === "claude-code-plugins" ? 1 : 0;
76
- return aDeprecated - bDeprecated;
59
+ return buildPluginBrowserItems({
60
+ marketplaces: pluginsState.marketplaces.data,
61
+ plugins: pluginsState.plugins.data,
62
+ collapsedMarketplaces: pluginsState.collapsedMarketplaces,
77
63
  });
78
- const items = [];
79
- for (const marketplace of sortedMarketplaces) {
80
- const marketplacePlugins = pluginsByMarketplace.get(marketplace.name) || [];
81
- const isCollapsed = collapsed.has(marketplace.name);
82
- const isEnabled = marketplacePlugins.length > 0 || marketplace.official;
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
- }
142
- // Category header (marketplace)
143
- items.push({
144
- id: `mp:${marketplace.name}`,
145
- type: "category",
146
- label: marketplace.displayName,
147
- marketplace,
148
- marketplaceEnabled: isEnabled,
149
- pluginCount: marketplacePlugins.length,
150
- isExpanded: !isCollapsed && hasPlugins,
151
- });
152
- // Plugins under this marketplace (if expanded)
153
- if (isEnabled && hasPlugins && !isCollapsed) {
154
- for (const plugin of marketplacePlugins) {
155
- items.push({
156
- id: `pl:${plugin.id}`,
157
- type: "plugin",
158
- label: plugin.name,
159
- plugin,
160
- });
161
- }
162
- }
163
- }
164
- return items;
165
64
  }, [
166
65
  pluginsState.marketplaces,
167
66
  pluginsState.plugins,
@@ -173,26 +72,23 @@ export function PluginsScreen() {
173
72
  if (!query)
174
73
  return allItems;
175
74
  // Only search plugins, not categories
176
- const pluginItems = allItems.filter((item) => item.type === "plugin");
75
+ const pluginItems = allItems.filter((item) => item.kind === "plugin");
177
76
  const fuzzyResults = fuzzyFilter(pluginItems, query, (item) => item.label);
178
- // Build a set of matched plugin item ids for O(1) lookup
179
77
  const matchedPluginIds = new Set();
180
78
  for (const result of fuzzyResults) {
181
79
  matchedPluginIds.add(result.item.id);
182
80
  }
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.
81
+ // Walk allItems sequentially: include a category only if any plugin below matched
186
82
  const categoryHasMatch = new Map();
187
83
  let currentCategoryId = null;
188
84
  for (const item of allItems) {
189
- if (item.type === "category") {
85
+ if (item.kind === "category") {
190
86
  currentCategoryId = item.id;
191
87
  if (!categoryHasMatch.has(item.id)) {
192
88
  categoryHasMatch.set(item.id, false);
193
89
  }
194
90
  }
195
- else if (item.type === "plugin" && currentCategoryId) {
91
+ else if (item.kind === "plugin" && currentCategoryId) {
196
92
  if (matchedPluginIds.has(item.id)) {
197
93
  categoryHasMatch.set(currentCategoryId, true);
198
94
  }
@@ -202,96 +98,80 @@ export function PluginsScreen() {
202
98
  let currentCatIncluded = false;
203
99
  currentCategoryId = null;
204
100
  for (const item of allItems) {
205
- if (item.type === "category") {
101
+ if (item.kind === "category") {
206
102
  currentCategoryId = item.id;
207
103
  currentCatIncluded = categoryHasMatch.get(item.id) === true;
208
- if (currentCatIncluded) {
104
+ if (currentCatIncluded)
209
105
  result.push(item);
210
- }
211
106
  }
212
- else if (item.type === "plugin" && currentCatIncluded) {
107
+ else if (item.kind === "plugin" && currentCatIncluded) {
213
108
  if (matchedPluginIds.has(item.id)) {
214
109
  const matched = fuzzyResults.find((r) => r.item.id === item.id);
215
- result.push({ ...item, _matches: matched?.matches });
110
+ result.push({ ...item, matches: matched?.matches });
216
111
  }
217
112
  }
218
113
  }
219
114
  return result;
220
115
  }, [allItems, pluginsState.searchQuery]);
221
- // Only selectable items (plugins, not categories)
222
- const selectableItems = useMemo(() => {
223
- return filteredItems.filter((item) => item.type === "plugin" || item.type === "category");
224
- }, [filteredItems]);
225
- // Keyboard handling — inline search with live filtering
116
+ const selectableItems = useMemo(() => filteredItems, [filteredItems]);
117
+ // Keyboard handling
226
118
  useKeyboard((event) => {
227
119
  if (state.modal)
228
120
  return;
229
121
  const hasQuery = pluginsState.searchQuery.length > 0;
230
- // Escape: always clear search state fully
231
122
  if (event.name === "escape") {
232
123
  if (hasQuery || isSearchActive) {
233
124
  dispatch({ type: "PLUGINS_SET_SEARCH", query: "" });
234
125
  dispatch({ type: "SET_SEARCHING", isSearching: false });
235
126
  dispatch({ type: "PLUGINS_SELECT", index: 0 });
236
127
  }
237
- // Don't return — let GlobalKeyHandler handle Escape too (for quit)
238
128
  return;
239
129
  }
240
- // Backspace: remove last char from search query
241
130
  if (event.name === "backspace" || event.name === "delete") {
242
131
  if (hasQuery) {
243
132
  const newQuery = pluginsState.searchQuery.slice(0, -1);
244
133
  dispatch({ type: "PLUGINS_SET_SEARCH", query: newQuery });
245
134
  dispatch({ type: "PLUGINS_SELECT", index: 0 });
246
- // If query becomes empty, exit search mode
247
135
  if (newQuery.length === 0) {
248
136
  dispatch({ type: "SET_SEARCHING", isSearching: false });
249
137
  }
250
138
  }
251
139
  return;
252
140
  }
253
- // Navigation — always works; exits search mode on navigate
254
141
  if (event.name === "up" || event.name === "k") {
255
142
  if (isSearchActive)
256
143
  dispatch({ type: "SET_SEARCHING", isSearching: false });
257
- const newIndex = Math.max(0, pluginsState.selectedIndex - 1);
258
- dispatch({ type: "PLUGINS_SELECT", index: newIndex });
144
+ dispatch({ type: "PLUGINS_SELECT", index: Math.max(0, pluginsState.selectedIndex - 1) });
259
145
  return;
260
146
  }
261
147
  if (event.name === "down" || event.name === "j") {
262
148
  if (isSearchActive)
263
149
  dispatch({ type: "SET_SEARCHING", isSearching: false });
264
- const newIndex = Math.min(selectableItems.length - 1, pluginsState.selectedIndex + 1);
265
- dispatch({ type: "PLUGINS_SELECT", index: newIndex });
150
+ dispatch({
151
+ type: "PLUGINS_SELECT",
152
+ index: Math.min(selectableItems.length - 1, pluginsState.selectedIndex + 1),
153
+ });
266
154
  return;
267
155
  }
268
- // Enter exit search mode (keep filter active) + select/install
269
- if (event.name === "enter") {
156
+ if (event.name === "enter" || event.name === "return") {
270
157
  if (isSearchActive) {
271
158
  dispatch({ type: "SET_SEARCHING", isSearching: false });
272
- // Keep the query — filter stays active, shortcuts resume
273
159
  return;
274
160
  }
275
161
  handleSelect();
276
162
  return;
277
163
  }
278
- // Collapse/expand marketplace — always works
279
164
  if ((event.name === "left" ||
280
165
  event.name === "right" ||
281
166
  event.name === "<" ||
282
167
  event.name === ">") &&
283
- selectableItems[pluginsState.selectedIndex]?.marketplace) {
168
+ selectableItems[pluginsState.selectedIndex]?.kind === "category") {
284
169
  const item = selectableItems[pluginsState.selectedIndex];
285
- if (item?.marketplace) {
286
- dispatch({
287
- type: "PLUGINS_TOGGLE_MARKETPLACE",
288
- name: item.marketplace.name,
289
- });
170
+ if (item?.kind === "category") {
171
+ dispatch({ type: "PLUGINS_TOGGLE_MARKETPLACE", name: item.marketplace.name });
290
172
  }
291
173
  return;
292
174
  }
293
- // When actively typing in search, letters go to the query
294
- // After Enter (isSearchActive=false, hasQuery=true), shortcuts resume
295
175
  if (isSearchActive) {
296
176
  if (event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
297
177
  dispatch({
@@ -302,13 +182,10 @@ export function PluginsScreen() {
302
182
  }
303
183
  return;
304
184
  }
305
- // Action shortcuts work when not actively typing (even with filter visible)
306
- // Start explicit search mode with /
307
185
  if (event.name === "/") {
308
186
  dispatch({ type: "SET_SEARCHING", isSearching: true });
309
187
  return;
310
188
  }
311
- // Action shortcuts (only when query is empty)
312
189
  if (event.name === "r")
313
190
  handleRefresh();
314
191
  else if (event.name === "n")
@@ -327,12 +204,8 @@ export function PluginsScreen() {
327
204
  handleUpdateAll();
328
205
  else if (event.name === "s")
329
206
  handleSaveAsProfile();
330
- // "/" to enter search mode
331
- else if (event.name === "/") {
332
- dispatch({ type: "SET_SEARCHING", isSearching: true });
333
- }
334
207
  });
335
- // Handle actions
208
+ // ── Action handlers ────────────────────────────────────────────────────────
336
209
  const handleRefresh = async () => {
337
210
  progress.show("Refreshing cache...");
338
211
  try {
@@ -341,7 +214,6 @@ export function PluginsScreen() {
341
214
  });
342
215
  clearMarketplaceCache();
343
216
  progress.hide();
344
- // Build message
345
217
  let message = "Cache refreshed.\n\n" +
346
218
  "To update marketplaces from GitHub, run in Claude Code:\n" +
347
219
  " /plugin marketplace update\n\n" +
@@ -393,49 +265,39 @@ export function PluginsScreen() {
393
265
  await modal.message("Team Configuration", helpText, "info");
394
266
  };
395
267
  /**
396
- * Collect environment variables required by a plugin's MCP servers
397
- * Prompts user for missing values and saves to local settings
268
+ * Collect environment variables required by a plugin's MCP servers.
398
269
  */
399
270
  const collectPluginEnvVars = async (pluginName, marketplace) => {
400
271
  try {
401
- // Get plugin source path from marketplace manifest
402
272
  const pluginSource = await getPluginSourcePath(marketplace, pluginName);
403
273
  if (!pluginSource)
404
- return true; // No source path, nothing to configure
405
- // Get env var requirements from plugin's MCP config
274
+ return true;
406
275
  const requirements = await getPluginEnvRequirements(marketplace, pluginSource);
407
276
  if (requirements.length === 0)
408
- return true; // No env vars needed
409
- // Get existing env vars
277
+ return true;
410
278
  const existingEnvVars = await getMcpEnvVars(state.projectPath);
411
279
  const missingVars = requirements.filter((req) => !existingEnvVars[req.name] && !process.env[req.name]);
412
280
  if (missingVars.length === 0)
413
- return true; // All vars already configured
414
- // Ask user if they want to configure MCP server env vars now
281
+ return true;
415
282
  const serverNames = [...new Set(missingVars.map((v) => v.serverName))];
416
283
  const wantToConfigure = await modal.confirm("Configure MCP Servers?", `This plugin includes MCP servers (${serverNames.join(", ")}) that need ${missingVars.length} environment variable(s).\n\nConfigure now?`);
417
284
  if (!wantToConfigure) {
418
285
  await modal.message("Skipped Configuration", "You can configure these variables later in the Environment Variables screen (press 4).", "info");
419
- return true; // Still installed, just not configured
286
+ return true;
420
287
  }
421
- // Collect each missing env var
422
288
  for (const req of missingVars) {
423
- // Check if value exists in process.env
424
289
  const existingProcessEnv = process.env[req.name];
425
290
  if (existingProcessEnv) {
426
291
  const useExisting = await modal.confirm(`Use ${req.name}?`, `${req.name} is already set in your environment.\n\nUse the existing value?`);
427
292
  if (useExisting) {
428
- // Store reference to env var instead of literal value
429
293
  await setMcpEnvVar(req.name, `\${${req.name}}`, state.projectPath);
430
294
  continue;
431
295
  }
432
296
  }
433
- // Prompt for value
434
297
  const value = await modal.input(`Configure ${req.serverName}`, `${req.label} (required):`, "");
435
298
  if (value === null) {
436
- // User cancelled
437
299
  await modal.message("Configuration Incomplete", `Skipped remaining configuration.\nYou can configure these later in Environment Variables (press 4).`, "info");
438
- return true; // Still installed
300
+ return true;
439
301
  }
440
302
  if (value) {
441
303
  await setMcpEnvVar(req.name, value, state.projectPath);
@@ -445,12 +307,11 @@ export function PluginsScreen() {
445
307
  }
446
308
  catch (error) {
447
309
  console.error("Error collecting plugin env vars:", error);
448
- return true; // Don't block installation on config errors
310
+ return true;
449
311
  }
450
312
  };
451
313
  /**
452
- * Install system dependencies required by a plugin's MCP servers
453
- * Auto-detects available package managers (uv/pip, brew, npm, cargo)
314
+ * Install system dependencies required by a plugin's MCP servers.
454
315
  */
455
316
  const installPluginSystemDeps = async (pluginName, marketplace) => {
456
317
  try {
@@ -465,7 +326,6 @@ export function PluginsScreen() {
465
326
  0;
466
327
  if (!hasMissing)
467
328
  return;
468
- // Build description of what will be installed
469
329
  const parts = [];
470
330
  if (missing.pip?.length)
471
331
  parts.push(`pip: ${missing.pip.join(", ")}`);
@@ -482,9 +342,7 @@ export function PluginsScreen() {
482
342
  const result = await installPluginDeps(missing);
483
343
  modal.hideModal();
484
344
  if (result.failed.length > 0) {
485
- const failMsg = result.failed
486
- .map((f) => `${f.pkg}: ${f.error}`)
487
- .join("\n");
345
+ const failMsg = result.failed.map((f) => `${f.pkg}: ${f.error}`).join("\n");
488
346
  await modal.message("Partial Install", `Installed: ${result.installed.length}\nFailed:\n${failMsg}`, "error");
489
347
  }
490
348
  else if (result.installed.length > 0) {
@@ -496,9 +354,7 @@ export function PluginsScreen() {
496
354
  }
497
355
  };
498
356
  /**
499
- * Save the installed version to settings after CLI install/update.
500
- * Claude CLI doesn't update installedPluginVersions in settings.json,
501
- * so we do it ourselves to keep the TUI version display accurate.
357
+ * Save installed plugin version to settings after CLI install/update.
502
358
  */
503
359
  const saveVersionAfterInstall = async (pluginId, version, scope) => {
504
360
  try {
@@ -520,44 +376,36 @@ export function PluginsScreen() {
520
376
  const item = selectableItems[pluginsState.selectedIndex];
521
377
  if (!item)
522
378
  return;
523
- if (item.type === "category" && item.marketplace) {
379
+ if (item.kind === "category") {
524
380
  const mp = item.marketplace;
525
381
  if (item.marketplaceEnabled) {
526
382
  const isCollapsed = pluginsState.collapsedMarketplaces.has(mp.name);
527
- // If collapsed, expand first (even if no plugins - they might load)
528
383
  if (isCollapsed) {
529
384
  dispatch({ type: "PLUGINS_TOGGLE_MARKETPLACE", name: mp.name });
530
385
  }
531
- else if (item.pluginCount && item.pluginCount > 0) {
532
- // If expanded with plugins, collapse
386
+ else if (item.pluginCount > 0) {
533
387
  dispatch({ type: "PLUGINS_TOGGLE_MARKETPLACE", name: mp.name });
534
388
  }
535
389
  else {
536
- // If expanded with no plugins, show removal instructions
537
390
  await modal.message(`Remove ${mp.displayName}?`, `To remove this marketplace, run in Claude Code:\n\n` +
538
391
  ` /plugin marketplace remove ${mp.name}\n\n` +
539
392
  `After removing, refresh claudeup with 'r' to update the display.`, "info");
540
393
  }
541
394
  }
542
395
  else {
543
- // Show add marketplace instructions
544
396
  await modal.message(`Add ${mp.displayName}?`, `To add this marketplace, run in your terminal:\n\n` +
545
397
  ` claude plugin marketplace add ${mp.source.repo || mp.name}\n\n` +
546
398
  `Auto-update is enabled by default.\n\n` +
547
399
  `After adding, refresh claudeup with 'r' to see it.`, "info");
548
400
  }
549
401
  }
550
- else if (item.type === "plugin" && item.plugin) {
402
+ else if (item.kind === "plugin") {
551
403
  const plugin = item.plugin;
552
404
  const latestVersion = plugin.version || "0.0.0";
553
- // Build scope options with status info
554
405
  const buildScopeLabel = (name, scope, desc) => {
555
406
  const installed = scope?.enabled;
556
407
  const ver = scope?.version;
557
- const hasUpdate = ver &&
558
- latestVersion &&
559
- ver !== latestVersion &&
560
- latestVersion !== "0.0.0";
408
+ const hasUpdate = ver && latestVersion && ver !== latestVersion && latestVersion !== "0.0.0";
561
409
  let label = installed ? `● ${name}` : `○ ${name}`;
562
410
  label += ` (${desc})`;
563
411
  if (ver)
@@ -567,23 +415,13 @@ export function PluginsScreen() {
567
415
  return label;
568
416
  };
569
417
  const scopeOptions = [
570
- {
571
- label: buildScopeLabel("User", plugin.userScope, "global"),
572
- value: "user",
573
- },
574
- {
575
- label: buildScopeLabel("Project", plugin.projectScope, "team"),
576
- value: "project",
577
- },
578
- {
579
- label: buildScopeLabel("Local", plugin.localScope, "private"),
580
- value: "local",
581
- },
418
+ { label: buildScopeLabel("User", plugin.userScope, "global"), value: "user" },
419
+ { label: buildScopeLabel("Project", plugin.projectScope, "team"), value: "project" },
420
+ { label: buildScopeLabel("Local", plugin.localScope, "private"), value: "local" },
582
421
  ];
583
422
  const scopeValue = await modal.select(plugin.name, `Select scope to toggle:`, scopeOptions);
584
423
  if (scopeValue === null)
585
- return; // Cancelled
586
- // Determine action based on selected scope's current state
424
+ return;
587
425
  const selectedScope = scopeValue === "user"
588
426
  ? plugin.userScope
589
427
  : scopeValue === "project"
@@ -591,17 +429,11 @@ export function PluginsScreen() {
591
429
  : plugin.localScope;
592
430
  const isInstalledInScope = selectedScope?.enabled;
593
431
  const installedVersion = selectedScope?.version;
594
- const scopeLabel = scopeValue === "user"
595
- ? "User"
596
- : scopeValue === "project"
597
- ? "Project"
598
- : "Local";
599
- // Check if this scope has an update available
432
+ const scopeLabel = scopeValue === "user" ? "User" : scopeValue === "project" ? "Project" : "Local";
600
433
  const hasUpdateInScope = isInstalledInScope &&
601
434
  installedVersion &&
602
435
  latestVersion !== "0.0.0" &&
603
436
  installedVersion !== latestVersion;
604
- // Determine action: update if available, otherwise toggle
605
437
  let action;
606
438
  if (isInstalledInScope && hasUpdateInScope) {
607
439
  action = "update";
@@ -630,7 +462,6 @@ export function PluginsScreen() {
630
462
  else {
631
463
  await cliInstallPlugin(plugin.id, scope);
632
464
  await saveVersionAfterInstall(plugin.id, latestVersion, scope);
633
- // On fresh install, configure env vars and install system deps
634
465
  modal.hideModal();
635
466
  await collectPluginEnvVars(plugin.name, plugin.marketplace);
636
467
  await installPluginSystemDeps(plugin.name, plugin.marketplace);
@@ -648,7 +479,7 @@ export function PluginsScreen() {
648
479
  };
649
480
  const handleUpdate = async () => {
650
481
  const item = selectableItems[pluginsState.selectedIndex];
651
- if (!item || item.type !== "plugin" || !item.plugin?.hasUpdate)
482
+ if (!item || item.kind !== "plugin" || !item.plugin.hasUpdate)
652
483
  return;
653
484
  const plugin = item.plugin;
654
485
  const scope = pluginsState.scope === "global" ? "user" : "project";
@@ -685,15 +516,13 @@ export function PluginsScreen() {
685
516
  await modal.message("Error", `Failed to update: ${error}`, "error");
686
517
  }
687
518
  };
688
- // Scope-specific toggle (install if not installed, uninstall if installed)
689
519
  const handleScopeToggle = async (scope) => {
690
520
  const item = selectableItems[pluginsState.selectedIndex];
691
- if (!item || item.type !== "plugin" || !item.plugin)
521
+ if (!item || item.kind !== "plugin")
692
522
  return;
693
523
  const plugin = item.plugin;
694
524
  const latestVersion = plugin.version || "0.0.0";
695
525
  const scopeLabel = scope === "user" ? "User" : scope === "project" ? "Project" : "Local";
696
- // Check if installed in this specific scope
697
526
  const scopeData = scope === "user"
698
527
  ? plugin.userScope
699
528
  : scope === "project"
@@ -701,17 +530,10 @@ export function PluginsScreen() {
701
530
  : plugin.localScope;
702
531
  const isInstalledInScope = scopeData?.enabled;
703
532
  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;
706
- // Check if this scope has an update available
707
533
  const hasUpdateInScope = isInstalledInScope &&
708
534
  installedVersion &&
709
535
  latestVersion !== "0.0.0" &&
710
536
  installedVersion !== latestVersion;
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
715
537
  let action;
716
538
  if (isInstalledInScope && hasUpdateInScope) {
717
539
  action = "update";
@@ -739,7 +561,6 @@ export function PluginsScreen() {
739
561
  else {
740
562
  await cliInstallPlugin(plugin.id, scope);
741
563
  await saveVersionAfterInstall(plugin.id, latestVersion, scope);
742
- // On fresh install, configure env vars and install system deps
743
564
  modal.hideModal();
744
565
  await collectPluginEnvVars(plugin.name, plugin.marketplace);
745
566
  await installPluginSystemDeps(plugin.name, plugin.marketplace);
@@ -755,21 +576,14 @@ export function PluginsScreen() {
755
576
  }
756
577
  };
757
578
  const handleSaveAsProfile = async () => {
758
- // Read current enabledPlugins from project settings
759
579
  const settings = await readSettings(state.projectPath);
760
580
  const enabledPlugins = settings.enabledPlugins ?? {};
761
581
  const name = await modal.input("Save Profile", "Profile name:");
762
582
  if (name === null || !name.trim())
763
583
  return;
764
584
  const scopeChoice = await modal.select("Save to scope", "Where should this profile be saved?", [
765
- {
766
- label: "User~/.claude/profiles.json (available everywhere)",
767
- value: "user",
768
- },
769
- {
770
- label: "Project — .claude/profiles.json (shared with team via git)",
771
- value: "project",
772
- },
585
+ { label: "User — ~/.claude/profiles.json (available everywhere)", value: "user" },
586
+ { label: "Project.claude/profiles.json (shared with team via git)", value: "project" },
773
587
  ]);
774
588
  if (scopeChoice === null)
775
589
  return;
@@ -785,216 +599,29 @@ export function PluginsScreen() {
785
599
  await modal.message("Error", `Failed to save profile: ${error}`, "error");
786
600
  }
787
601
  };
788
- const handleUninstall = async () => {
789
- const item = selectableItems[pluginsState.selectedIndex];
790
- if (!item || item.type !== "plugin" || !item.plugin)
791
- return;
792
- const plugin = item.plugin;
793
- // Build list of scopes where plugin is installed
794
- const installedScopes = [];
795
- if (plugin.userScope?.enabled) {
796
- const ver = plugin.userScope.version
797
- ? ` v${plugin.userScope.version}`
798
- : "";
799
- installedScopes.push({ label: `User (global)${ver}`, value: "user" });
800
- }
801
- if (plugin.projectScope?.enabled) {
802
- const ver = plugin.projectScope.version
803
- ? ` v${plugin.projectScope.version}`
804
- : "";
805
- installedScopes.push({ label: `Project${ver}`, value: "project" });
806
- }
807
- if (plugin.localScope?.enabled) {
808
- const ver = plugin.localScope.version
809
- ? ` v${plugin.localScope.version}`
810
- : "";
811
- installedScopes.push({ label: `Local${ver}`, value: "local" });
812
- }
813
- if (installedScopes.length === 0) {
814
- await modal.message("Not Installed", `${plugin.name} is not installed in any scope.`, "info");
815
- return;
816
- }
817
- const scopeValue = await modal.select(`Uninstall ${plugin.name}`, `Installed in ${installedScopes.length} scope(s):`, installedScopes);
818
- if (scopeValue === null)
819
- return; // Cancelled
820
- modal.loading(`Uninstalling ${plugin.name}...`);
821
- try {
822
- await cliUninstallPlugin(plugin.id, scopeValue, state.projectPath);
823
- modal.hideModal();
824
- fetchData();
825
- }
826
- catch (error) {
827
- modal.hideModal();
828
- await modal.message("Error", `Failed to uninstall: ${error}`, "error");
829
- }
830
- };
831
- // Render loading state
602
+ // ── Render ─────────────────────────────────────────────────────────────────
832
603
  if (pluginsState.marketplaces.status === "loading" ||
833
604
  pluginsState.plugins.status === "loading") {
834
605
  return (_jsxs("box", { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [_jsx("text", { fg: "#7e57c2", children: _jsx("strong", { children: "claudeup Plugins" }) }), _jsx("text", { fg: "gray", children: "Loading..." })] }));
835
606
  }
836
- // Render error state
837
607
  if (pluginsState.marketplaces.status === "error" ||
838
608
  pluginsState.plugins.status === "error") {
839
609
  return (_jsxs("box", { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [_jsx("text", { fg: "#7e57c2", children: _jsx("strong", { children: "claudeup Plugins" }) }), _jsx("text", { fg: "red", children: "Error loading data" })] }));
840
610
  }
841
- // Get selected item for detail panel
842
611
  const selectedItem = selectableItems[pluginsState.selectedIndex];
843
- // Render item with fuzzy highlight support
844
- const renderListItem = (item, _idx, isSelected) => {
845
- if (item.type === "category" && item.marketplace) {
846
- const mp = item.marketplace;
847
- // Differentiate marketplace types with appropriate badges
848
- let statusText = "";
849
- let statusColor = "green";
850
- if (item.marketplaceEnabled) {
851
- if (item.isCommunitySection) {
852
- statusText = "3rd Party";
853
- statusColor = "gray";
854
- }
855
- else if (mp.name === "claude-plugins-official") {
856
- statusText = "★ Official";
857
- statusColor = "yellow";
858
- }
859
- else if (mp.name === "claude-code-plugins") {
860
- statusText = "⚠ Deprecated";
861
- statusColor = "gray";
862
- }
863
- else if (mp.official) {
864
- statusText = "★ Official";
865
- statusColor = "yellow";
866
- }
867
- else {
868
- statusText = "✓ Added";
869
- statusColor = "green";
870
- }
871
- }
872
- if (isSelected) {
873
- const arrow = item.isExpanded ? "▼" : "▶";
874
- const count = item.pluginCount !== undefined && item.pluginCount > 0
875
- ? ` (${item.pluginCount})`
876
- : "";
877
- return (_jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", arrow, " ", mp.displayName, count, " "] }) }));
878
- }
879
- return (_jsx(CategoryHeader, { title: mp.displayName, expanded: item.isExpanded, count: item.pluginCount, status: statusText, statusColor: statusColor }));
880
- }
881
- if (item.type === "plugin" && item.plugin) {
882
- const plugin = item.plugin;
883
- const isAnyScope = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
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;
889
- // Build version string
890
- let versionStr = "";
891
- if (plugin.isOrphaned) {
892
- versionStr = " deprecated";
893
- }
894
- else if (plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
895
- versionStr = ` v${plugin.installedVersion}`;
896
- if (plugin.hasUpdate && plugin.version) {
897
- versionStr += ` → v${plugin.version}`;
898
- }
899
- }
900
- // Get fuzzy match highlights if available
901
- const matches = item._matches;
902
- const segments = matches ? highlightMatches(plugin.name, matches) : null;
903
- if (isSelected) {
904
- return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", _jsx("span", { children: hasUser ? "■" : "□" }), _jsx("span", { children: hasProject ? "■" : "□" }), _jsx("span", { children: hasLocal ? "■" : "□" }), " ", plugin.name, versionStr, " "] }));
905
- }
906
- const displayName = segments
907
- ? segments.map((seg) => seg.text).join("")
908
- : plugin.name;
909
- if (plugin.isOrphaned) {
910
- const ver = plugin.installedVersion && plugin.installedVersion !== "0.0.0"
911
- ? ` v${plugin.installedVersion}` : "";
912
- return (_jsxs("text", { children: [_jsx("span", { fg: "red", children: " \u25A0\u25A0\u25A0 " }), _jsx("span", { fg: "gray", children: displayName }), ver && _jsx("span", { fg: "yellow", children: ver }), _jsx("span", { fg: "red", children: " deprecated" })] }));
913
- }
914
- return (_jsxs("text", { children: [_jsx("span", { children: " " }), _jsx("span", { fg: hasUser ? "cyan" : "#333333", children: "\u25A0" }), _jsx("span", { fg: hasProject ? "green" : "#333333", children: "\u25A0" }), _jsx("span", { fg: hasLocal ? "yellow" : "#333333", children: "\u25A0" }), _jsx("span", { children: " " }), _jsx("span", { fg: hasAnyScope ? "white" : "gray", children: displayName }), _jsx("span", { fg: plugin.hasUpdate ? "yellow" : "gray", children: versionStr })] }));
915
- }
916
- return _jsx("text", { fg: "gray", children: item.label });
917
- };
918
- // Render detail content - compact to fit in available space
919
- const renderDetail = () => {
920
- if (!selectedItem) {
921
- return _jsx("text", { fg: "gray", children: "Select an item" });
922
- }
923
- if (selectedItem.type === "category" && selectedItem.marketplace) {
924
- const mp = selectedItem.marketplace;
925
- const isEnabled = selectedItem.marketplaceEnabled;
926
- // Get appropriate badge for marketplace type
927
- const getBadge = () => {
928
- if (selectedItem.isCommunitySection)
929
- return " 3rd Party";
930
- if (mp.name === "claude-plugins-official")
931
- return " ★";
932
- if (mp.name === "claude-code-plugins")
933
- return " ⚠";
934
- if (mp.official)
935
- return " ★";
936
- return "";
937
- };
938
- // Determine action hint based on state
939
- const isCollapsed = pluginsState.collapsedMarketplaces.has(mp.name);
940
- const hasPlugins = (selectedItem.pluginCount || 0) > 0;
941
- let actionHint = "Add";
942
- if (isEnabled) {
943
- if (isCollapsed) {
944
- actionHint = "Expand";
945
- }
946
- else if (hasPlugins) {
947
- actionHint = "Collapse";
948
- }
949
- else {
950
- actionHint = "Remove";
951
- }
952
- }
953
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsxs("strong", { children: [mp.displayName, getBadge()] }) }), _jsx("text", { fg: "gray", children: mp.description || "No description" }), _jsx("text", { fg: isEnabled ? "green" : "gray", children: isEnabled ? "● Added" : "○ Not added" }), _jsxs("text", { fg: "#5c9aff", children: ["github.com/", mp.source.repo] }), _jsxs("text", { children: ["Plugins: ", selectedItem.pluginCount || 0] }), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: isEnabled ? "cyan" : "green", fg: "black", children: [" ", "Enter", " "] }), _jsxs("text", { fg: "gray", children: [" ", actionHint] })] }), isEnabled && (_jsx("box", { children: _jsx("text", { fg: "gray", children: "\u2190 \u2192 to expand/collapse" }) }))] }));
954
- }
955
- if (selectedItem.type === "plugin" && selectedItem.plugin) {
956
- const plugin = selectedItem.plugin;
957
- const isInstalled = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
958
- // Orphaned/deprecated plugin
959
- if (plugin.isOrphaned) {
960
- 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)" })] }) }))] }));
961
- }
962
- // Build component counts
963
- const components = [];
964
- if (plugin.agents?.length)
965
- components.push(`${plugin.agents.length} agents`);
966
- if (plugin.commands?.length)
967
- components.push(`${plugin.commands.length} commands`);
968
- if (plugin.skills?.length)
969
- components.push(`${plugin.skills.length} skills`);
970
- if (plugin.mcpServers?.length)
971
- components.push(`${plugin.mcpServers.length} MCP`);
972
- if (plugin.lspServers && Object.keys(plugin.lspServers).length) {
973
- components.push(`${Object.keys(plugin.lspServers).length} LSP`);
974
- }
975
- // Show version only if valid (not null, not 0.0.0)
976
- const showVersion = plugin.version && plugin.version !== "0.0.0";
977
- const showInstalledVersion = plugin.installedVersion && plugin.installedVersion !== "0.0.0";
978
- 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 &&
979
- 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] })] })) }))] }));
980
- }
981
- return null;
982
- };
983
612
  const footerHints = isSearchActive
984
613
  ? "type to filter │ Enter:done │ Esc:clear"
985
614
  : "u/p/l:toggle │ U:update │ a:all │ s:profile │ /:search";
986
- // Calculate status for subtitle
987
615
  const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
988
616
  const plugins = pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
989
617
  const installedCount = plugins.filter((p) => p.enabled).length;
990
618
  const updateCount = plugins.filter((p) => p.hasUpdate).length;
991
619
  const subtitle = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} updates` : ""}`;
992
- // Search placeholder shows status when not searching
993
620
  const searchPlaceholder = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} ⬆` : ""} │ / to search`;
994
621
  return (_jsx(ScreenLayout, { title: "claudeup Plugins", subtitle: subtitle, currentScreen: "plugins", search: {
995
622
  isActive: isSearchActive,
996
623
  query: pluginsState.searchQuery,
997
624
  placeholder: searchPlaceholder,
998
- }, footerHints: footerHints, listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items: selectableItems, selectedIndex: pluginsState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), pluginsState.searchQuery && selectableItems.length === 0 && (_jsx(EmptyFilterState, { query: pluginsState.searchQuery, entityName: "plugins" }))] }), detailPanel: renderDetail() }));
625
+ }, footerHints: footerHints, listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items: selectableItems, selectedIndex: pluginsState.selectedIndex, renderItem: renderPluginRow, maxHeight: dimensions.listPanelHeight }), pluginsState.searchQuery && selectableItems.length === 0 && (_jsx(EmptyFilterState, { query: pluginsState.searchQuery, entityName: "plugins" }))] }), detailPanel: renderPluginDetail(selectedItem, pluginsState.collapsedMarketplaces) }));
999
626
  }
1000
627
  export default PluginsScreen;