@laststance/claude-plugin-dashboard 0.2.2 → 0.3.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.
@@ -26,15 +26,27 @@ export default function PluginList({ plugins, selectedIndex, visibleCount = 15,
26
26
  const visiblePlugins = plugins.slice(startIndex, endIndex);
27
27
  const hasMore = endIndex < plugins.length;
28
28
  const hasPrevious = startIndex > 0;
29
- return (_jsxs(Box, { flexDirection: "column", children: [hasPrevious && _jsxs(Text, { dimColor: true, children: ["\u2191 ", startIndex, " more above"] }), visiblePlugins.map((plugin, index) => {
29
+ // Calculate fixed height: each item is 2 lines + 1 line for top indicator + 1 line for bottom indicator
30
+ // Total fixed lines: visibleCount * 2 (items) + 2 (indicators)
31
+ const totalLines = visibleCount * 2 + 2;
32
+ // Lines used by items
33
+ const itemLines = visiblePlugins.length * 2;
34
+ // Lines used by indicators (always rendered, but may be empty)
35
+ const topIndicatorLine = 1;
36
+ const bottomIndicatorLine = 1;
37
+ // Calculate padding lines needed to maintain fixed height
38
+ const usedLines = itemLines + topIndicatorLine + bottomIndicatorLine;
39
+ const paddingLines = Math.max(0, totalLines - usedLines);
40
+ return (_jsxs(Box, { flexDirection: "column", height: totalLines, children: [_jsx(Text, { dimColor: true, children: hasPrevious ? `↑ ${startIndex} more above` : ' ' }), visiblePlugins.map((plugin, index) => {
30
41
  const actualIndex = startIndex + index;
31
42
  const isSelected = actualIndex === selectedIndex;
32
- return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected && isFocused ? (_jsx(Text, { color: "cyan", children: '>' })) : isSelected ? (_jsx(Text, { color: "gray", children: '›' })) : (_jsx(Text, { children: " " })) }), _jsx(Box, { width: 2, children: _jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected && isFocused
33
- ? 'cyan'
34
- : isSelected
35
- ? 'gray'
36
- : 'white', children: plugin.name }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { color: "gray", children: truncate(plugin.marketplace, 20) }), plugin.installCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { color: "gray", children: [formatCount(plugin.installCount), " installs"] })] }))] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: truncate(plugin.description, 60) })] })] }, plugin.id));
37
- }), hasMore && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", plugins.length - endIndex, " more below"] }))] }));
43
+ return (_jsxs(Box, { paddingX: 1, height: 2, children: [_jsx(Box, { width: 2, children: isSelected && isFocused ? (_jsx(Text, { color: "cyan", children: '>' })) : isSelected ? (_jsx(Text, { color: "gray", children: '›' })) : (_jsx(Text, { children: " " })) }), _jsx(Box, { width: 2, children: _jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }) }), _jsxs(Box, { flexDirection: "column", overflow: "hidden", children: [_jsx(Box, { height: 1, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { bold: true, color: isSelected && isFocused
44
+ ? 'cyan'
45
+ : isSelected
46
+ ? 'gray'
47
+ : 'white', children: plugin.name }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsx(Text, { color: "gray", children: truncate(plugin.marketplace, 20) }), plugin.installCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsxs(Text, { color: "gray", children: [formatCount(plugin.installCount), " installs"] })] }))] }) }), _jsx(Box, { height: 1, children: _jsx(Text, { dimColor: true, wrap: "truncate", children: truncate(plugin.description, 60) }) })] })] }, plugin.id));
48
+ }), paddingLines > 0 &&
49
+ Array.from({ length: paddingLines }).map((_, i) => (_jsx(Text, { children: " " }, `pad-${i}`))), _jsx(Text, { dimColor: true, children: hasMore ? `↓ ${plugins.length - endIndex} more below` : ' ' })] }));
38
50
  }
39
51
  /**
40
52
  * Truncate text to max length with ellipsis
@@ -3,7 +3,7 @@
3
3
  * Parses plugin.json and scans plugin directory structure to identify
4
4
  * skills, commands, agents, hooks, MCP servers, and LSP servers
5
5
  */
6
- import type { PluginComponents } from '../types/index.js';
6
+ import type { PluginComponents, PluginComponentsDetailed } from '../types/index.js';
7
7
  /**
8
8
  * Detect all component types for a plugin at the given install path
9
9
  * @param installPath - Absolute path to the installed plugin directory
@@ -15,6 +15,17 @@ import type { PluginComponents } from '../types/index.js';
15
15
  * // => { skills: 5, commands: 2, mcpServers: 1 }
16
16
  */
17
17
  export declare function detectPluginComponents(installPath: string): PluginComponents | undefined;
18
+ /**
19
+ * Detect detailed components for an installed plugin
20
+ * Reads skills/, commands/, agents/ directories and parses plugin.json
21
+ * @param installPath - Absolute path to installed plugin directory
22
+ * @returns Detailed component info with names and descriptions
23
+ * - Returns undefined if path doesn't exist or has no components
24
+ * @example
25
+ * detectComponentsDetailed('/path/to/plugin')
26
+ * // => { skills: [{ name: 'xlsx', description: '...', type: 'skill' }] }
27
+ */
28
+ export declare function detectComponentsDetailed(installPath: string): PluginComponentsDetailed | undefined;
18
29
  /**
19
30
  * Check if a plugin has any components
20
31
  * @param components - PluginComponents object
@@ -57,6 +57,244 @@ export function detectPluginComponents(installPath) {
57
57
  }
58
58
  return components;
59
59
  }
60
+ /**
61
+ * Detect detailed components for an installed plugin
62
+ * Reads skills/, commands/, agents/ directories and parses plugin.json
63
+ * @param installPath - Absolute path to installed plugin directory
64
+ * @returns Detailed component info with names and descriptions
65
+ * - Returns undefined if path doesn't exist or has no components
66
+ * @example
67
+ * detectComponentsDetailed('/path/to/plugin')
68
+ * // => { skills: [{ name: 'xlsx', description: '...', type: 'skill' }] }
69
+ */
70
+ export function detectComponentsDetailed(installPath) {
71
+ if (!directoryExists(installPath)) {
72
+ return undefined;
73
+ }
74
+ const detailed = {};
75
+ // Skills: Read directory names + SKILL.md frontmatter
76
+ const skills = getSkillDetails(installPath);
77
+ if (skills.length > 0) {
78
+ detailed.skills = skills;
79
+ }
80
+ // Commands: Read .md filenames
81
+ const commands = getMarkdownFileDetails(installPath, 'commands', 'command');
82
+ if (commands.length > 0) {
83
+ detailed.commands = commands;
84
+ }
85
+ // Agents: Read .md filenames
86
+ const agents = getMarkdownFileDetails(installPath, 'agents', 'agent');
87
+ if (agents.length > 0) {
88
+ detailed.agents = agents;
89
+ }
90
+ // Hooks: Read event names from hooks.json or hooks/ directory
91
+ const hooks = getHookNames(installPath);
92
+ if (hooks.length > 0) {
93
+ detailed.hooks = hooks;
94
+ }
95
+ // MCP Servers: Read plugin.json mcpServers keys
96
+ const mcpServers = getMcpServerNames(installPath);
97
+ if (mcpServers.length > 0) {
98
+ detailed.mcpServers = mcpServers;
99
+ }
100
+ // LSP Servers: Read .lsp.json keys
101
+ const lspServers = getLspServerNames(installPath);
102
+ if (lspServers.length > 0) {
103
+ detailed.lspServers = lspServers;
104
+ }
105
+ // Return undefined if no components detected
106
+ if (Object.keys(detailed).length === 0) {
107
+ return undefined;
108
+ }
109
+ return detailed;
110
+ }
111
+ /**
112
+ * Get skill details from skills/ directory
113
+ * Reads directory names and parses SKILL.md frontmatter for descriptions
114
+ * @param installPath - Plugin install path
115
+ * @returns Array of ComponentInfo for each skill
116
+ */
117
+ function getSkillDetails(installPath) {
118
+ const skillsPath = path.join(installPath, 'skills');
119
+ if (!directoryExists(skillsPath)) {
120
+ return [];
121
+ }
122
+ try {
123
+ const entries = fs.readdirSync(skillsPath, { withFileTypes: true });
124
+ return entries
125
+ .filter((entry) => entry.isDirectory())
126
+ .map((entry) => {
127
+ const skillMdPath = path.join(skillsPath, entry.name, 'SKILL.md');
128
+ const description = parseSkillMdDescription(skillMdPath);
129
+ return {
130
+ name: entry.name,
131
+ description,
132
+ type: 'skill',
133
+ };
134
+ });
135
+ }
136
+ catch {
137
+ return [];
138
+ }
139
+ }
140
+ /**
141
+ * Parse SKILL.md frontmatter for description
142
+ * Looks for `description:` field in YAML frontmatter
143
+ * @param skillMdPath - Path to SKILL.md file
144
+ * @returns Description string or undefined
145
+ */
146
+ function parseSkillMdDescription(skillMdPath) {
147
+ if (!fileExists(skillMdPath)) {
148
+ return undefined;
149
+ }
150
+ try {
151
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
152
+ // Match YAML frontmatter description field
153
+ const match = content.match(/^---\n[\s\S]*?description:\s*["']?(.+?)["']?\s*\n[\s\S]*?---/m);
154
+ return match?.[1]?.trim();
155
+ }
156
+ catch {
157
+ return undefined;
158
+ }
159
+ }
160
+ /**
161
+ * Get component details from .md files in a directory
162
+ * Uses filename (minus extension) as component name
163
+ * @param installPath - Plugin install path
164
+ * @param subdir - Subdirectory name ('commands' or 'agents')
165
+ * @param type - Component type
166
+ * @returns Array of ComponentInfo
167
+ */
168
+ function getMarkdownFileDetails(installPath, subdir, type) {
169
+ const dirPath = path.join(installPath, subdir);
170
+ if (!directoryExists(dirPath)) {
171
+ return [];
172
+ }
173
+ try {
174
+ const files = fs.readdirSync(dirPath);
175
+ return files
176
+ .filter((file) => file.endsWith('.md'))
177
+ .map((file) => {
178
+ const name = file.replace(/\.md$/, '');
179
+ const filePath = path.join(dirPath, file);
180
+ const description = parseFirstLineDescription(filePath);
181
+ return {
182
+ name,
183
+ description,
184
+ type,
185
+ };
186
+ });
187
+ }
188
+ catch {
189
+ return [];
190
+ }
191
+ }
192
+ /**
193
+ * Parse first non-empty line of a markdown file as description
194
+ * Properly skips YAML frontmatter block and strips heading markers
195
+ * @param filePath - Path to markdown file
196
+ * @returns First non-frontmatter, non-empty line or undefined
197
+ * @example
198
+ * // File: "---\nname: test\n---\n# My Title\n"
199
+ * parseFirstLineDescription(path) // => "My Title"
200
+ */
201
+ function parseFirstLineDescription(filePath) {
202
+ try {
203
+ const content = fs.readFileSync(filePath, 'utf-8');
204
+ const lines = content.split('\n');
205
+ let inFrontmatter = false;
206
+ let frontmatterClosed = false;
207
+ for (const line of lines) {
208
+ const trimmed = line.trim();
209
+ // Detect frontmatter delimiter
210
+ if (trimmed === '---') {
211
+ if (!inFrontmatter && !frontmatterClosed) {
212
+ // Opening delimiter
213
+ inFrontmatter = true;
214
+ continue;
215
+ }
216
+ else if (inFrontmatter) {
217
+ // Closing delimiter
218
+ inFrontmatter = false;
219
+ frontmatterClosed = true;
220
+ continue;
221
+ }
222
+ }
223
+ // Skip lines inside frontmatter
224
+ if (inFrontmatter) {
225
+ continue;
226
+ }
227
+ // Skip empty lines
228
+ if (!trimmed) {
229
+ continue;
230
+ }
231
+ // Found first content line - remove heading markers and return
232
+ return trimmed.replace(/^#+\s*/, '');
233
+ }
234
+ return undefined;
235
+ }
236
+ catch {
237
+ return undefined;
238
+ }
239
+ }
240
+ /**
241
+ * Get hook event names from hooks configuration
242
+ * @param installPath - Plugin install path
243
+ * @returns Array of hook event names
244
+ */
245
+ function getHookNames(installPath) {
246
+ // Try hooks.json first
247
+ const hooksJsonPath = path.join(installPath, 'hooks.json');
248
+ const hooksJson = readJsonFile(hooksJsonPath);
249
+ if (hooksJson) {
250
+ return Object.keys(hooksJson);
251
+ }
252
+ // Try hooks/ directory
253
+ const hooksDir = path.join(installPath, 'hooks');
254
+ if (directoryExists(hooksDir)) {
255
+ try {
256
+ const files = fs.readdirSync(hooksDir);
257
+ return files
258
+ .filter((f) => f.endsWith('.json') || f.endsWith('.js'))
259
+ .map((f) => f.replace(/\.(json|js)$/, ''));
260
+ }
261
+ catch {
262
+ return [];
263
+ }
264
+ }
265
+ return [];
266
+ }
267
+ /**
268
+ * Get MCP server names from plugin.json
269
+ * @param installPath - Plugin install path
270
+ * @returns Array of MCP server names
271
+ */
272
+ function getMcpServerNames(installPath) {
273
+ const pluginJsonPaths = [
274
+ path.join(installPath, '.claude-plugin', 'plugin.json'),
275
+ path.join(installPath, 'plugin.json'),
276
+ ];
277
+ for (const pluginJsonPath of pluginJsonPaths) {
278
+ const pluginJson = readJsonFile(pluginJsonPath);
279
+ if (pluginJson?.mcpServers) {
280
+ return Object.keys(pluginJson.mcpServers);
281
+ }
282
+ }
283
+ return [];
284
+ }
285
+ /**
286
+ * Get LSP server language IDs from .lsp.json
287
+ * @param installPath - Plugin install path
288
+ * @returns Array of language IDs
289
+ */
290
+ function getLspServerNames(installPath) {
291
+ const lspJsonPath = path.join(installPath, '.lsp.json');
292
+ const lspConfig = readJsonFile(lspJsonPath);
293
+ if (!lspConfig) {
294
+ return [];
295
+ }
296
+ return Object.keys(lspConfig);
297
+ }
60
298
  /**
61
299
  * Count skill directories in the skills/ folder
62
300
  * Skills are stored as subdirectories with SKILL.md files
@@ -42,3 +42,20 @@ export declare function removeMarketplace(name: string): Promise<MarketplaceActi
42
42
  * updateMarketplace()
43
43
  */
44
44
  export declare function updateMarketplace(name?: string): Promise<MarketplaceActionResult>;
45
+ /**
46
+ * Execute a marketplace CLI command with generic args and messages
47
+ * @param args - CLI arguments to pass to claude command
48
+ * @param successMessage - Message to return on success
49
+ * @param failureMessage - Message to return on failure
50
+ * @returns Promise resolving to action result
51
+ */
52
+ /**
53
+ * Toggle auto-update setting for a marketplace
54
+ * @param marketplaceId - The marketplace identifier
55
+ * @param currentValue - Current auto-update state
56
+ * @returns Promise resolving to action result with new state
57
+ * @example
58
+ * toggleAutoUpdate('claude-plugins-official', false) // enables auto-update
59
+ * toggleAutoUpdate('claude-plugins-official', true) // disables auto-update
60
+ */
61
+ export declare function toggleAutoUpdate(marketplaceId: string, currentValue: boolean): Promise<MarketplaceActionResult>;
@@ -52,6 +52,24 @@ export function updateMarketplace(name) {
52
52
  * @param failureMessage - Message to return on failure
53
53
  * @returns Promise resolving to action result
54
54
  */
55
+ /**
56
+ * Toggle auto-update setting for a marketplace
57
+ * @param marketplaceId - The marketplace identifier
58
+ * @param currentValue - Current auto-update state
59
+ * @returns Promise resolving to action result with new state
60
+ * @example
61
+ * toggleAutoUpdate('claude-plugins-official', false) // enables auto-update
62
+ * toggleAutoUpdate('claude-plugins-official', true) // disables auto-update
63
+ */
64
+ export function toggleAutoUpdate(marketplaceId, currentValue) {
65
+ const newValue = !currentValue;
66
+ const args = newValue
67
+ ? ['plugin', 'marketplace', 'auto-update', 'enable', marketplaceId]
68
+ : ['plugin', 'marketplace', 'auto-update', 'disable', marketplaceId];
69
+ return executeMarketplaceCommand(args, newValue
70
+ ? `Enabled auto-update for ${marketplaceId}`
71
+ : `Disabled auto-update for ${marketplaceId}`, `Failed to toggle auto-update for ${marketplaceId}`);
72
+ }
55
73
  function executeMarketplaceCommand(args, successMessage, failureMessage) {
56
74
  return new Promise((resolve) => {
57
75
  const child = spawn('claude', args, {
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { readJsonFile, directoryExists, listDirectories, } from './fileService.js';
6
6
  import { getEnabledPlugins } from './settingsService.js';
7
- import { detectPluginComponents } from './componentService.js';
7
+ import { detectPluginComponents, detectComponentsDetailed, } from './componentService.js';
8
8
  import { PATHS, getMarketplaceJsonPath } from '../utils/paths.js';
9
9
  /**
10
10
  * Load all plugins from all marketplaces
@@ -45,10 +45,20 @@ export function loadAllPlugins() {
45
45
  for (const plugin of manifest.plugins) {
46
46
  const pluginId = `${plugin.name}@${marketplace}`;
47
47
  const installedEntry = installedMap.get(pluginId);
48
- // Detect components for installed plugins
48
+ // Detect components for installed plugins (counts)
49
49
  const components = installedEntry
50
50
  ? detectPluginComponents(installedEntry.installPath)
51
51
  : undefined;
52
+ // Detect detailed components
53
+ // - For installed: read from file system (names + descriptions)
54
+ // - For not installed: parse from marketplace JSON (names only)
55
+ let componentsDetailed;
56
+ if (installedEntry) {
57
+ componentsDetailed = detectComponentsDetailed(installedEntry.installPath);
58
+ }
59
+ else {
60
+ componentsDetailed = parseMarketplaceComponents(plugin);
61
+ }
52
62
  plugins.push({
53
63
  id: pluginId,
54
64
  name: plugin.name,
@@ -67,6 +77,7 @@ export function loadAllPlugins() {
67
77
  isLocal: installedEntry?.isLocal,
68
78
  gitCommitSha: installedEntry?.gitCommitSha,
69
79
  components,
80
+ componentsDetailed,
70
81
  });
71
82
  }
72
83
  }
@@ -120,6 +131,7 @@ export function loadMarketplaces() {
120
131
  installLocation: data.installLocation,
121
132
  lastUpdated: data.lastUpdated,
122
133
  pluginCount,
134
+ autoUpdate: data.autoUpdate ?? false,
123
135
  });
124
136
  }
125
137
  // Sort by plugin count (descending)
@@ -208,3 +220,67 @@ export function getPluginStatistics() {
208
220
  marketplaces: marketplaces.length,
209
221
  };
210
222
  }
223
+ /**
224
+ * Parse component names from marketplace plugin entry
225
+ * Extracts component names from skills[], agents[], commands[] arrays
226
+ * @param plugin - Marketplace plugin entry
227
+ * @returns Detailed components with names only (no descriptions)
228
+ * @example
229
+ * parseMarketplaceComponents({ skills: ['./skills/xlsx', './skills/docx'] })
230
+ * // => { skills: [{ name: 'xlsx', type: 'skill' }, { name: 'docx', type: 'skill' }] }
231
+ */
232
+ function parseMarketplaceComponents(plugin) {
233
+ const detailed = {};
234
+ // Parse skills array (e.g., ["./skills/xlsx", "./skills/docx"])
235
+ if (plugin.skills && Array.isArray(plugin.skills)) {
236
+ const skills = plugin.skills
237
+ .map((skillPath) => extractComponentName(skillPath, 'skill'))
238
+ .filter((item) => item !== null);
239
+ if (skills.length > 0) {
240
+ detailed.skills = skills;
241
+ }
242
+ }
243
+ // Parse agents array (if available in marketplace)
244
+ if (plugin.agents && Array.isArray(plugin.agents)) {
245
+ const agents = plugin.agents
246
+ .map((agentPath) => extractComponentName(agentPath, 'agent'))
247
+ .filter((item) => item !== null);
248
+ if (agents.length > 0) {
249
+ detailed.agents = agents;
250
+ }
251
+ }
252
+ // Parse commands array (if available in marketplace)
253
+ if (plugin.commands && Array.isArray(plugin.commands)) {
254
+ const commands = plugin.commands
255
+ .map((cmdPath) => extractComponentName(cmdPath, 'command'))
256
+ .filter((item) => item !== null);
257
+ if (commands.length > 0) {
258
+ detailed.commands = commands;
259
+ }
260
+ }
261
+ // Return undefined if no components found
262
+ if (Object.keys(detailed).length === 0) {
263
+ return undefined;
264
+ }
265
+ return detailed;
266
+ }
267
+ /**
268
+ * Extract component name from path string
269
+ * @param pathStr - Path like "./skills/xlsx" or just "xlsx"
270
+ * @param type - Component type
271
+ * @returns ComponentInfo or null if invalid
272
+ */
273
+ function extractComponentName(pathStr, type) {
274
+ if (typeof pathStr !== 'string' || !pathStr.trim()) {
275
+ return null;
276
+ }
277
+ // Extract basename from path
278
+ // "./skills/xlsx" -> "xlsx"
279
+ // "xlsx" -> "xlsx"
280
+ const parts = pathStr.split('/');
281
+ const name = parts[parts.length - 1]?.trim();
282
+ if (!name) {
283
+ return null;
284
+ }
285
+ return { name, type };
286
+ }
@@ -22,5 +22,5 @@ import SortDropdown from '../components/SortDropdown.js';
22
22
  */
23
23
  export default function DiscoverTab({ plugins, selectedIndex, searchQuery, sortBy, sortOrder, focusZone = 'list', }) {
24
24
  const selectedPlugin = plugins[selectedIndex] ?? null;
25
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Discover plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsx(SortDropdown, { sortBy: sortBy, sortOrder: sortOrder })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12, isFocused: focusZone === 'list' }) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginDetail, { plugin: selectedPlugin }) })] })] }));
25
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Discover plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsx(SortDropdown, { sortBy: sortBy, sortOrder: sortOrder })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search..." }) }), _jsxs(Box, { flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", children: _jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12, isFocused: focusZone === 'list' }) }), _jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", children: _jsx(PluginDetail, { plugin: selectedPlugin }, selectedPlugin?.id ?? 'none') })] })] }));
26
26
  }
@@ -20,7 +20,7 @@ import SearchInput from '../components/SearchInput.js';
20
20
  export default function EnabledTab({ plugins, selectedIndex, searchQuery = '', focusZone = 'list', }) {
21
21
  // Plugins are already filtered by parent, use directly
22
22
  const selectedPlugin = plugins[selectedIndex] ?? null;
23
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Enabled plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { dimColor: true, children: "Currently active in Claude Code" })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search enabled plugins..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: plugins.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: searchQuery ? 'No matching plugins' : 'No enabled plugins' }), _jsx(Text, { dimColor: true, children: searchQuery
23
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Enabled plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { dimColor: true, children: "Currently active in Claude Code" })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search enabled plugins..." }) }), _jsxs(Box, { flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", children: plugins.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: searchQuery ? 'No matching plugins' : 'No enabled plugins' }), _jsx(Text, { dimColor: true, children: searchQuery
24
24
  ? 'Try a different search term'
25
- : 'Enable plugins in the Installed tab or use /plugin enable' })] })) : (_jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12, isFocused: focusZone === 'list' })) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginDetail, { plugin: selectedPlugin }) })] })] }));
25
+ : 'Enable plugins in the Installed tab or use /plugin enable' })] })) : (_jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12, isFocused: focusZone === 'list' })) }), _jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", children: _jsx(PluginDetail, { plugin: selectedPlugin }, selectedPlugin?.id ?? 'none') })] })] }));
26
26
  }
@@ -22,7 +22,7 @@ export default function InstalledTab({ plugins, selectedIndex, searchQuery = '',
22
22
  // Count enabled/disabled
23
23
  const enabledCount = plugins.filter((p) => p.isEnabled).length;
24
24
  const disabledCount = plugins.length - enabledCount;
25
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Installed plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { color: "green", children: ["\u25CF ", enabledCount, " enabled"] }), _jsxs(Text, { color: "yellow", children: ["\u25D0 ", disabledCount, " disabled"] })] })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search installed plugins..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: plugins.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: searchQuery ? 'No matching plugins' : 'No plugins installed' }), _jsx(Text, { dimColor: true, children: searchQuery
25
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Installed plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { color: "green", children: ["\u25CF ", enabledCount, " enabled"] }), _jsxs(Text, { color: "yellow", children: ["\u25D0 ", disabledCount, " disabled"] })] })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search installed plugins..." }) }), _jsxs(Box, { flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", children: plugins.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: searchQuery ? 'No matching plugins' : 'No plugins installed' }), _jsx(Text, { dimColor: true, children: searchQuery
26
26
  ? 'Try a different search term'
27
- : 'Use the Discover tab or /plugin install in Claude Code' })] })) : (_jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12, isFocused: focusZone === 'list' })) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginDetail, { plugin: selectedPlugin }) })] })] }));
27
+ : 'Use the Discover tab or /plugin install in Claude Code' })] })) : (_jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12, isFocused: focusZone === 'list' })) }), _jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", children: _jsx(PluginDetail, { plugin: selectedPlugin }, selectedPlugin?.id ?? 'none') })] })] }));
28
28
  }
@@ -9,6 +9,10 @@ interface MarketplacesTabProps {
9
9
  searchQuery?: string;
10
10
  /** Current focus zone for keyboard navigation */
11
11
  focusZone?: FocusZone;
12
+ /** Whether to show the marketplace action menu */
13
+ showActionMenu?: boolean;
14
+ /** Selected index in the action menu */
15
+ actionMenuSelectedIndex?: number;
12
16
  }
13
17
  /**
14
18
  * Marketplaces tab - manage plugin sources
@@ -16,8 +20,17 @@ interface MarketplacesTabProps {
16
20
  * @param selectedIndex - Currently selected item index
17
21
  * @param searchQuery - Current search query string
18
22
  * @param focusZone - Current focus zone for keyboard navigation
23
+ * @param showActionMenu - Whether to show the action menu
24
+ * @param actionMenuSelectedIndex - Selected index in action menu
19
25
  * @example
20
- * <MarketplacesTab marketplaces={marketplaces} selectedIndex={0} searchQuery="" focusZone="list" />
26
+ * <MarketplacesTab
27
+ * marketplaces={marketplaces}
28
+ * selectedIndex={0}
29
+ * searchQuery=""
30
+ * focusZone="list"
31
+ * showActionMenu={false}
32
+ * actionMenuSelectedIndex={0}
33
+ * />
21
34
  */
22
- export default function MarketplacesTab({ marketplaces, selectedIndex, searchQuery, focusZone, }: MarketplacesTabProps): import("react/jsx-runtime").JSX.Element;
35
+ export default function MarketplacesTab({ marketplaces, selectedIndex, searchQuery, focusZone, showActionMenu, actionMenuSelectedIndex, }: MarketplacesTabProps): import("react/jsx-runtime").JSX.Element;
23
36
  export {};
@@ -13,18 +13,27 @@ import SearchInput from '../components/SearchInput.js';
13
13
  * @param selectedIndex - Currently selected item index
14
14
  * @param searchQuery - Current search query string
15
15
  * @param focusZone - Current focus zone for keyboard navigation
16
+ * @param showActionMenu - Whether to show the action menu
17
+ * @param actionMenuSelectedIndex - Selected index in action menu
16
18
  * @example
17
- * <MarketplacesTab marketplaces={marketplaces} selectedIndex={0} searchQuery="" focusZone="list" />
19
+ * <MarketplacesTab
20
+ * marketplaces={marketplaces}
21
+ * selectedIndex={0}
22
+ * searchQuery=""
23
+ * focusZone="list"
24
+ * showActionMenu={false}
25
+ * actionMenuSelectedIndex={0}
26
+ * />
18
27
  */
19
- export default function MarketplacesTab({ marketplaces, selectedIndex, searchQuery = '', focusZone = 'list', }) {
28
+ export default function MarketplacesTab({ marketplaces, selectedIndex, searchQuery = '', focusZone = 'list', showActionMenu = false, actionMenuSelectedIndex = 0, }) {
20
29
  const selectedMarketplace = marketplaces[selectedIndex] ?? null;
21
30
  // Count total plugins across all marketplaces
22
31
  const totalPlugins = marketplaces.reduce((sum, m) => sum + (m.pluginCount || 0), 0);
23
32
  return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Marketplaces (", marketplaces.length > 0
24
33
  ? `${selectedIndex + 1}/${marketplaces.length}`
25
- : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { color: "gray", children: [totalPlugins, " total plugins"] })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search marketplaces..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: marketplaces.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: searchQuery
34
+ : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { color: "gray", children: [totalPlugins, " total plugins"] })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search marketplaces..." }) }), _jsxs(Box, { flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", children: marketplaces.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: searchQuery
26
35
  ? 'No matching marketplaces'
27
36
  : 'No marketplaces found' }), _jsx(Text, { dimColor: true, children: searchQuery
28
37
  ? 'Try a different search term'
29
- : 'Add marketplaces with /plugin add-marketplace' })] })) : (_jsx(MarketplaceList, { marketplaces: marketplaces, selectedIndex: selectedIndex, isFocused: focusZone === 'list' })) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(MarketplaceDetail, { marketplace: selectedMarketplace }) })] })] }));
38
+ : 'Add marketplaces with /plugin add-marketplace' })] })) : (_jsx(MarketplaceList, { marketplaces: marketplaces, selectedIndex: selectedIndex, isFocused: focusZone === 'list' })) }), _jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", children: _jsx(MarketplaceDetail, { marketplace: selectedMarketplace, showActionMenu: showActionMenu, actionMenuSelectedIndex: actionMenuSelectedIndex }) })] })] }));
30
39
  }