@laststance/claude-plugin-dashboard 0.1.1 → 0.2.1

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 (35) hide show
  1. package/README.md +70 -32
  2. package/dist/app.d.ts +29 -0
  3. package/dist/app.js +496 -69
  4. package/dist/components/AddMarketplaceDialog.d.ts +20 -0
  5. package/dist/components/AddMarketplaceDialog.js +18 -0
  6. package/dist/components/ComponentBadges.d.ts +32 -0
  7. package/dist/components/ComponentBadges.js +82 -0
  8. package/dist/components/HelpOverlay.d.ts +15 -0
  9. package/dist/components/HelpOverlay.js +51 -0
  10. package/dist/components/KeyHints.d.ts +6 -3
  11. package/dist/components/KeyHints.js +39 -10
  12. package/dist/components/MarketplaceList.d.ts +4 -2
  13. package/dist/components/MarketplaceList.js +7 -3
  14. package/dist/components/PluginDetail.js +2 -1
  15. package/dist/components/PluginList.d.ts +29 -2
  16. package/dist/components/PluginList.js +26 -5
  17. package/dist/components/SearchInput.js +1 -1
  18. package/dist/components/TabBar.d.ts +5 -3
  19. package/dist/components/TabBar.js +20 -8
  20. package/dist/services/componentService.d.ts +35 -0
  21. package/dist/services/componentService.js +178 -0
  22. package/dist/services/marketplaceActionsService.d.ts +44 -0
  23. package/dist/services/marketplaceActionsService.js +92 -0
  24. package/dist/services/pluginService.d.ts +10 -0
  25. package/dist/services/pluginService.js +22 -0
  26. package/dist/tabs/DiscoverTab.d.ts +5 -3
  27. package/dist/tabs/DiscoverTab.js +3 -2
  28. package/dist/tabs/EnabledTab.d.ts +24 -0
  29. package/dist/tabs/EnabledTab.js +26 -0
  30. package/dist/tabs/InstalledTab.d.ts +10 -3
  31. package/dist/tabs/InstalledTab.js +14 -10
  32. package/dist/tabs/MarketplacesTab.d.ts +10 -3
  33. package/dist/tabs/MarketplacesTab.js +12 -3
  34. package/dist/types/index.d.ts +71 -1
  35. package/package.json +11 -3
package/dist/app.js CHANGED
@@ -3,23 +3,29 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  * Main App component for Claude Code Plugin Dashboard
4
4
  * Interactive TUI to browse and manage Claude Code plugins
5
5
  */
6
- import { useState, useEffect, useReducer } from 'react';
6
+ import { useEffect, useReducer } from 'react';
7
7
  import { Box, Text, useInput, useApp } from 'ink';
8
8
  import TabBar, { getNextTab } from './components/TabBar.js';
9
9
  import KeyHints from './components/KeyHints.js';
10
10
  import DiscoverTab from './tabs/DiscoverTab.js';
11
+ import EnabledTab from './tabs/EnabledTab.js';
11
12
  import InstalledTab from './tabs/InstalledTab.js';
12
13
  import MarketplacesTab from './tabs/MarketplacesTab.js';
13
14
  import ErrorsTab from './tabs/ErrorsTab.js';
14
- import { loadAllPlugins, loadMarketplaces, searchPlugins, sortPlugins, } from './services/pluginService.js';
15
+ import { loadAllPlugins, loadMarketplaces, searchPlugins, searchMarketplaces, sortPlugins, } from './services/pluginService.js';
15
16
  import { togglePlugin } from './services/settingsService.js';
16
17
  import { installPlugin, uninstallPlugin, } from './services/pluginActionsService.js';
18
+ import { addMarketplace, removeMarketplace, updateMarketplace, } from './services/marketplaceActionsService.js';
19
+ import AddMarketplaceDialog from './components/AddMarketplaceDialog.js';
17
20
  import ConfirmDialog from './components/ConfirmDialog.js';
21
+ import HelpOverlay from './components/HelpOverlay.js';
22
+ import packageJson from '../package.json' with { type: 'json' };
18
23
  /**
19
24
  * Initial application state
20
25
  */
21
- const initialState = {
22
- activeTab: 'discover',
26
+ export const initialState = {
27
+ activeTab: 'enabled',
28
+ focusZone: 'list',
23
29
  plugins: [],
24
30
  marketplaces: [],
25
31
  errors: [],
@@ -33,16 +39,53 @@ const initialState = {
33
39
  operation: 'idle',
34
40
  operationPluginId: null,
35
41
  confirmUninstall: false,
42
+ showHelp: false,
43
+ marketplaceOperation: 'idle',
44
+ operationMarketplaceId: null,
45
+ confirmRemoveMarketplace: false,
46
+ showAddMarketplaceDialog: false,
47
+ addMarketplaceError: null,
36
48
  };
49
+ /**
50
+ * Get available focus zones for the current tab
51
+ * Errors tab has no search zone since it doesn't support filtering
52
+ * @param activeTab - The currently active tab
53
+ * @returns Array of available focus zones in navigation order
54
+ */
55
+ export function getAvailableZones(activeTab) {
56
+ if (activeTab === 'errors') {
57
+ return ['tabbar', 'list'];
58
+ }
59
+ return ['tabbar', 'search', 'list'];
60
+ }
61
+ /**
62
+ * Get display message for marketplace operation status
63
+ * @param operation - The marketplace operation type
64
+ * @param marketplaceId - Optional marketplace identifier
65
+ * @returns Display message for the operation
66
+ */
67
+ function getMarketplaceOperationMessage(operation, marketplaceId) {
68
+ switch (operation) {
69
+ case 'adding':
70
+ return 'Adding marketplace...';
71
+ case 'removing':
72
+ return `Removing ${marketplaceId}...`;
73
+ case 'updating':
74
+ return `Updating ${marketplaceId || 'marketplaces'}...`;
75
+ default:
76
+ return '';
77
+ }
78
+ }
37
79
  /**
38
80
  * State reducer for application state management
39
81
  */
40
- function appReducer(state, action) {
82
+ export function appReducer(state, action) {
41
83
  switch (action.type) {
42
84
  case 'SET_TAB':
43
85
  return {
44
86
  ...state,
45
87
  activeTab: action.payload,
88
+ focusZone: 'list',
46
89
  selectedIndex: 0,
47
90
  searchQuery: '',
48
91
  message: null,
@@ -51,6 +94,7 @@ function appReducer(state, action) {
51
94
  return {
52
95
  ...state,
53
96
  activeTab: getNextTab(state.activeTab, 'next'),
97
+ focusZone: 'list',
54
98
  selectedIndex: 0,
55
99
  searchQuery: '',
56
100
  message: null,
@@ -59,10 +103,16 @@ function appReducer(state, action) {
59
103
  return {
60
104
  ...state,
61
105
  activeTab: getNextTab(state.activeTab, 'prev'),
106
+ focusZone: 'list',
62
107
  selectedIndex: 0,
63
108
  searchQuery: '',
64
109
  message: null,
65
110
  };
111
+ case 'SET_FOCUS_ZONE':
112
+ return {
113
+ ...state,
114
+ focusZone: action.payload,
115
+ };
66
116
  case 'SET_PLUGINS':
67
117
  return {
68
118
  ...state,
@@ -180,19 +230,83 @@ function appReducer(state, action) {
180
230
  confirmUninstall: false,
181
231
  operationPluginId: null,
182
232
  };
233
+ case 'TOGGLE_HELP':
234
+ return {
235
+ ...state,
236
+ showHelp: !state.showHelp,
237
+ };
238
+ case 'SHOW_CONFIRM_REMOVE_MARKETPLACE':
239
+ return {
240
+ ...state,
241
+ confirmRemoveMarketplace: true,
242
+ operationMarketplaceId: action.payload,
243
+ };
244
+ case 'HIDE_CONFIRM_REMOVE_MARKETPLACE':
245
+ return {
246
+ ...state,
247
+ confirmRemoveMarketplace: false,
248
+ operationMarketplaceId: null,
249
+ };
250
+ case 'SHOW_ADD_MARKETPLACE_DIALOG':
251
+ return {
252
+ ...state,
253
+ showAddMarketplaceDialog: true,
254
+ searchQuery: '', // Reuse searchQuery for dialog input
255
+ addMarketplaceError: null, // Clear previous error
256
+ };
257
+ case 'HIDE_ADD_MARKETPLACE_DIALOG':
258
+ return {
259
+ ...state,
260
+ showAddMarketplaceDialog: false,
261
+ searchQuery: '',
262
+ addMarketplaceError: null,
263
+ };
264
+ case 'SET_ADD_MARKETPLACE_ERROR':
265
+ return {
266
+ ...state,
267
+ addMarketplaceError: action.payload,
268
+ };
269
+ case 'START_MARKETPLACE_OPERATION':
270
+ return {
271
+ ...state,
272
+ marketplaceOperation: action.payload.operation,
273
+ operationMarketplaceId: action.payload.marketplaceId ?? null,
274
+ message: getMarketplaceOperationMessage(action.payload.operation, action.payload.marketplaceId),
275
+ };
276
+ case 'END_MARKETPLACE_OPERATION':
277
+ return {
278
+ ...state,
279
+ marketplaceOperation: 'idle',
280
+ operationMarketplaceId: null,
281
+ };
183
282
  default:
184
283
  return state;
185
284
  }
186
285
  }
187
286
  /**
188
- * Get items array for current tab
287
+ * Get items array for current tab with search filter applied
288
+ * @param state - Current app state
289
+ * @returns Filtered array of items for the active tab
290
+ * @example
291
+ * getItemsForTab({ activeTab: 'installed', searchQuery: 'su', plugins: [...] })
292
+ * // => Only installed plugins matching 'su'
189
293
  */
190
- function getItemsForTab(state) {
294
+ export function getItemsForTab(state) {
191
295
  switch (state.activeTab) {
296
+ case 'enabled': {
297
+ const enabledPlugins = state.plugins.filter((p) => p.isInstalled && p.isEnabled);
298
+ return state.searchQuery
299
+ ? searchPlugins(state.searchQuery, enabledPlugins)
300
+ : enabledPlugins;
301
+ }
302
+ case 'installed': {
303
+ const installedPlugins = state.plugins.filter((p) => p.isInstalled);
304
+ return state.searchQuery
305
+ ? searchPlugins(state.searchQuery, installedPlugins)
306
+ : installedPlugins;
307
+ }
192
308
  case 'discover':
193
309
  return getFilteredPlugins(state);
194
- case 'installed':
195
- return state.plugins.filter((p) => p.isInstalled);
196
310
  case 'marketplaces':
197
311
  return state.marketplaces;
198
312
  case 'errors':
@@ -204,7 +318,7 @@ function getItemsForTab(state) {
204
318
  /**
205
319
  * Get filtered and sorted plugins for discover tab
206
320
  */
207
- function getFilteredPlugins(state) {
321
+ export function getFilteredPlugins(state) {
208
322
  let plugins = state.plugins;
209
323
  // Apply search filter
210
324
  if (state.searchQuery) {
@@ -220,7 +334,6 @@ function getFilteredPlugins(state) {
220
334
  export default function App() {
221
335
  const { exit } = useApp();
222
336
  const [state, dispatch] = useReducer(appReducer, initialState);
223
- const [isSearchMode, setIsSearchMode] = useState(false);
224
337
  // Load data on mount
225
338
  useEffect(() => {
226
339
  try {
@@ -282,13 +395,123 @@ export default function App() {
282
395
  });
283
396
  }
284
397
  }
398
+ /**
399
+ * Handle adding a new marketplace
400
+ * @param source - Marketplace source (e.g., "owner/repo", URL, or local path)
401
+ */
402
+ async function handleAddMarketplace(source) {
403
+ dispatch({
404
+ type: 'START_MARKETPLACE_OPERATION',
405
+ payload: { operation: 'adding' },
406
+ });
407
+ const result = await addMarketplace(source);
408
+ dispatch({ type: 'END_MARKETPLACE_OPERATION' });
409
+ if (result.success) {
410
+ dispatch({ type: 'HIDE_ADD_MARKETPLACE_DIALOG' });
411
+ // Reload marketplaces to get fresh state
412
+ const marketplaces = loadMarketplaces();
413
+ dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
414
+ // Also reload plugins as new marketplace may have plugins
415
+ const plugins = loadAllPlugins();
416
+ dispatch({ type: 'SET_PLUGINS', payload: plugins });
417
+ // Reset selection to avoid pointing to a different marketplace after re-sort
418
+ dispatch({ type: 'SET_SELECTED_INDEX', payload: 0 });
419
+ dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
420
+ }
421
+ else {
422
+ // Keep dialog open and show error inline
423
+ dispatch({
424
+ type: 'SET_ADD_MARKETPLACE_ERROR',
425
+ payload: result.error || result.message,
426
+ });
427
+ }
428
+ }
429
+ /**
430
+ * Handle removing a marketplace
431
+ * @param marketplaceId - Marketplace identifier to remove
432
+ */
433
+ async function handleRemoveMarketplace(marketplaceId) {
434
+ dispatch({
435
+ type: 'START_MARKETPLACE_OPERATION',
436
+ payload: { operation: 'removing', marketplaceId },
437
+ });
438
+ const result = await removeMarketplace(marketplaceId);
439
+ dispatch({ type: 'END_MARKETPLACE_OPERATION' });
440
+ if (result.success) {
441
+ // Reload marketplaces to get fresh state
442
+ const marketplaces = loadMarketplaces();
443
+ dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
444
+ // Also reload plugins as removed marketplace's plugins should be gone
445
+ const plugins = loadAllPlugins();
446
+ dispatch({ type: 'SET_PLUGINS', payload: plugins });
447
+ dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
448
+ // Reset selection if needed
449
+ if (state.selectedIndex >= marketplaces.length) {
450
+ dispatch({
451
+ type: 'SET_SELECTED_INDEX',
452
+ payload: Math.max(0, marketplaces.length - 1),
453
+ });
454
+ }
455
+ }
456
+ else {
457
+ dispatch({
458
+ type: 'SET_MESSAGE',
459
+ payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
460
+ });
461
+ }
462
+ }
463
+ /**
464
+ * Handle updating a marketplace (or all marketplaces)
465
+ * @param marketplaceId - Optional marketplace identifier. If omitted, updates all.
466
+ */
467
+ async function handleUpdateMarketplace(marketplaceId) {
468
+ dispatch({
469
+ type: 'START_MARKETPLACE_OPERATION',
470
+ payload: { operation: 'updating', marketplaceId },
471
+ });
472
+ const result = await updateMarketplace(marketplaceId);
473
+ dispatch({ type: 'END_MARKETPLACE_OPERATION' });
474
+ if (result.success) {
475
+ // Reload marketplaces and plugins to get fresh state
476
+ const marketplaces = loadMarketplaces();
477
+ dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
478
+ const plugins = loadAllPlugins();
479
+ dispatch({ type: 'SET_PLUGINS', payload: plugins });
480
+ // Reset selection to avoid pointing to a different marketplace after re-sort
481
+ dispatch({ type: 'SET_SELECTED_INDEX', payload: 0 });
482
+ dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
483
+ }
484
+ else {
485
+ dispatch({
486
+ type: 'SET_MESSAGE',
487
+ payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
488
+ });
489
+ }
490
+ }
285
491
  // Keyboard input handling
286
492
  useInput((input, key) => {
287
- // Block all input during operations
288
- if (state.operation !== 'idle') {
493
+ // Block all input during operations (plugin or marketplace)
494
+ if (state.operation !== 'idle' || state.marketplaceOperation !== 'idle') {
495
+ return;
496
+ }
497
+ // Handle help overlay
498
+ if (state.showHelp) {
499
+ if (input === 'h' || key.escape) {
500
+ dispatch({ type: 'TOGGLE_HELP' });
501
+ }
502
+ return;
503
+ }
504
+ // Exit (q or Ctrl+C) - Global handler, works in all focus zones
505
+ if (input === 'q' || (key.ctrl && input === 'c')) {
506
+ exit();
507
+ return;
508
+ }
509
+ // Toggle help (h key)
510
+ if (input === 'h') {
511
+ dispatch({ type: 'TOGGLE_HELP' });
289
512
  return;
290
513
  }
291
- // Handle confirmation dialog
514
+ // Handle plugin uninstall confirmation dialog
292
515
  if (state.confirmUninstall && state.operationPluginId) {
293
516
  if (input === 'y' || input === 'Y') {
294
517
  dispatch({ type: 'HIDE_CONFIRM_UNINSTALL' });
@@ -302,12 +525,34 @@ export default function App() {
302
525
  }
303
526
  return;
304
527
  }
305
- // Search mode input
306
- if (isSearchMode) {
307
- if (key.escape || key.return) {
308
- setIsSearchMode(false);
528
+ // Handle marketplace remove confirmation dialog
529
+ if (state.confirmRemoveMarketplace && state.operationMarketplaceId) {
530
+ if (input === 'y' || input === 'Y') {
531
+ dispatch({ type: 'HIDE_CONFIRM_REMOVE_MARKETPLACE' });
532
+ handleRemoveMarketplace(state.operationMarketplaceId);
533
+ return;
534
+ }
535
+ if (input === 'n' || input === 'N' || key.escape) {
536
+ dispatch({ type: 'HIDE_CONFIRM_REMOVE_MARKETPLACE' });
537
+ dispatch({ type: 'SET_MESSAGE', payload: 'Remove cancelled' });
538
+ return;
539
+ }
540
+ return;
541
+ }
542
+ // Handle add marketplace dialog input
543
+ if (state.showAddMarketplaceDialog) {
544
+ // Submit on Enter
545
+ if (key.return && state.searchQuery.trim()) {
546
+ handleAddMarketplace(state.searchQuery.trim());
547
+ return;
548
+ }
549
+ // Cancel on Escape
550
+ if (key.escape) {
551
+ dispatch({ type: 'HIDE_ADD_MARKETPLACE_DIALOG' });
552
+ dispatch({ type: 'SET_MESSAGE', payload: 'Add marketplace cancelled' });
309
553
  return;
310
554
  }
555
+ // Backspace
311
556
  if (key.backspace || key.delete) {
312
557
  dispatch({
313
558
  type: 'SET_SEARCH_QUERY',
@@ -315,6 +560,7 @@ export default function App() {
315
560
  });
316
561
  return;
317
562
  }
563
+ // Character input
318
564
  if (input && input.length === 1 && !key.ctrl && !key.meta) {
319
565
  dispatch({
320
566
  type: 'SET_SEARCH_QUERY',
@@ -324,48 +570,147 @@ export default function App() {
324
570
  }
325
571
  return;
326
572
  }
327
- // Emacs-style navigation (Ctrl+P / Ctrl+N)
328
- if (key.ctrl && input === 'p') {
329
- dispatch({ type: 'MOVE_SELECTION', payload: 'up' });
573
+ // Search mode input (when focusZone is 'search')
574
+ if (state.focusZone === 'search') {
575
+ // Up arrow: move focus to tabbar
576
+ if (key.upArrow || (key.ctrl && input === 'p')) {
577
+ dispatch({ type: 'SET_FOCUS_ZONE', payload: 'tabbar' });
578
+ return;
579
+ }
580
+ // Down arrow, Enter, or Escape: move focus to list
581
+ if (key.escape ||
582
+ key.return ||
583
+ key.downArrow ||
584
+ (key.ctrl && input === 'n')) {
585
+ dispatch({ type: 'SET_FOCUS_ZONE', payload: 'list' });
586
+ return;
587
+ }
588
+ if (key.backspace || key.delete) {
589
+ dispatch({
590
+ type: 'SET_SEARCH_QUERY',
591
+ payload: state.searchQuery.slice(0, -1),
592
+ });
593
+ return;
594
+ }
595
+ if (input && input.length === 1 && !key.ctrl && !key.meta) {
596
+ dispatch({
597
+ type: 'SET_SEARCH_QUERY',
598
+ payload: state.searchQuery + input,
599
+ });
600
+ return;
601
+ }
330
602
  return;
331
603
  }
332
- if (key.ctrl && input === 'n') {
333
- dispatch({ type: 'MOVE_SELECTION', payload: 'down' });
604
+ // TabBar focus zone navigation
605
+ if (state.focusZone === 'tabbar') {
606
+ // Down arrow: move to search (or list if no search)
607
+ if (key.downArrow || (key.ctrl && input === 'n')) {
608
+ const zones = getAvailableZones(state.activeTab);
609
+ dispatch({
610
+ type: 'SET_FOCUS_ZONE',
611
+ payload: zones.includes('search') ? 'search' : 'list',
612
+ });
613
+ return;
614
+ }
615
+ // Left/Right arrows and Ctrl+B/F: tab switching (only in tabbar)
616
+ // Keep focus on tabbar after navigation
617
+ if (key.leftArrow || (key.ctrl && input === 'b')) {
618
+ dispatch({ type: 'PREV_TAB' });
619
+ dispatch({ type: 'SET_FOCUS_ZONE', payload: 'tabbar' });
620
+ return;
621
+ }
622
+ if (key.rightArrow || (key.ctrl && input === 'f')) {
623
+ dispatch({ type: 'NEXT_TAB' });
624
+ dispatch({ type: 'SET_FOCUS_ZONE', payload: 'tabbar' });
625
+ return;
626
+ }
627
+ // Tab key: next tab (resets focus to list)
628
+ if (key.tab) {
629
+ dispatch({ type: 'NEXT_TAB' });
630
+ return;
631
+ }
334
632
  return;
335
633
  }
336
- // Tab navigation
337
- if (key.leftArrow) {
338
- dispatch({ type: 'PREV_TAB' });
634
+ // List focus zone navigation (default zone)
635
+ // Up arrow: move up in list or focus search/tabbar at top
636
+ if (key.upArrow || (key.ctrl && input === 'p')) {
637
+ if (state.selectedIndex === 0) {
638
+ // At top of list: move focus to search (or tabbar if no search)
639
+ const zones = getAvailableZones(state.activeTab);
640
+ dispatch({
641
+ type: 'SET_FOCUS_ZONE',
642
+ payload: zones.includes('search') ? 'search' : 'tabbar',
643
+ });
644
+ }
645
+ else {
646
+ dispatch({ type: 'MOVE_SELECTION', payload: 'up' });
647
+ }
339
648
  return;
340
649
  }
341
- if (key.rightArrow) {
342
- dispatch({ type: 'NEXT_TAB' });
650
+ // Down arrow: move down in list
651
+ if (key.downArrow || (key.ctrl && input === 'n')) {
652
+ dispatch({ type: 'MOVE_SELECTION', payload: 'down' });
343
653
  return;
344
654
  }
655
+ // Tab key: next tab (from list zone)
345
656
  if (key.tab) {
346
657
  dispatch({ type: 'NEXT_TAB' });
347
658
  return;
348
659
  }
349
- // List navigation
350
- if (key.upArrow) {
351
- dispatch({ type: 'MOVE_SELECTION', payload: 'up' });
660
+ // Enter search mode (/ key on supported tabs)
661
+ const searchEnabledTabs = [
662
+ 'enabled',
663
+ 'installed',
664
+ 'discover',
665
+ 'marketplaces',
666
+ ];
667
+ if (input === '/' && searchEnabledTabs.includes(state.activeTab)) {
668
+ dispatch({ type: 'SET_FOCUS_ZONE', payload: 'search' });
352
669
  return;
353
670
  }
354
- if (key.downArrow) {
355
- dispatch({ type: 'MOVE_SELECTION', payload: 'down' });
356
- return;
357
- }
358
- // Enter search mode
359
- if (input === '/' && state.activeTab === 'discover') {
360
- setIsSearchMode(true);
671
+ // Enter key: Install (non-installed) or Toggle (installed)
672
+ if (key.return &&
673
+ (state.activeTab === 'enabled' ||
674
+ state.activeTab === 'installed' ||
675
+ state.activeTab === 'discover')) {
676
+ const items = getItemsForTab(state);
677
+ const selectedPlugin = items[state.selectedIndex];
678
+ if (selectedPlugin) {
679
+ if (!selectedPlugin.isInstalled) {
680
+ // Install non-installed plugin
681
+ handleInstall(selectedPlugin.id);
682
+ }
683
+ else {
684
+ // Toggle installed plugin
685
+ try {
686
+ const newState = togglePlugin(selectedPlugin.id);
687
+ dispatch({
688
+ type: 'TOGGLE_PLUGIN_ENABLED',
689
+ payload: selectedPlugin.id,
690
+ });
691
+ dispatch({
692
+ type: 'SET_MESSAGE',
693
+ payload: newState
694
+ ? `✅ ${selectedPlugin.name} enabled`
695
+ : `❌ ${selectedPlugin.name} disabled`,
696
+ });
697
+ }
698
+ catch (error) {
699
+ dispatch({
700
+ type: 'SET_MESSAGE',
701
+ payload: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
702
+ });
703
+ }
704
+ }
705
+ }
361
706
  return;
362
707
  }
363
- // Toggle plugin (Space or Enter)
364
- if ((input === ' ' || key.return) &&
365
- (state.activeTab === 'discover' || state.activeTab === 'installed')) {
366
- const items = state.activeTab === 'installed'
367
- ? state.plugins.filter((p) => p.isInstalled)
368
- : getFilteredPlugins(state);
708
+ // Toggle plugin (Space only)
709
+ if (input === ' ' &&
710
+ (state.activeTab === 'enabled' ||
711
+ state.activeTab === 'installed' ||
712
+ state.activeTab === 'discover')) {
713
+ const items = getItemsForTab(state);
369
714
  const selectedPlugin = items[state.selectedIndex];
370
715
  if (selectedPlugin && selectedPlugin.isInstalled) {
371
716
  try {
@@ -419,12 +764,43 @@ export default function App() {
419
764
  dispatch({ type: 'SET_SEARCH_QUERY', payload: '' });
420
765
  return;
421
766
  }
422
- // Install (i key) - only on discover/installed tabs
767
+ // Marketplace-specific key bindings
768
+ if (state.activeTab === 'marketplaces' && state.focusZone === 'list') {
769
+ // Add marketplace (a key)
770
+ if (input === 'a') {
771
+ dispatch({ type: 'SHOW_ADD_MARKETPLACE_DIALOG' });
772
+ return;
773
+ }
774
+ // Remove marketplace (d key or Backspace)
775
+ if (input === 'd' || key.backspace || key.delete) {
776
+ const selectedMarketplace = filteredMarketplaces[state.selectedIndex];
777
+ if (selectedMarketplace) {
778
+ dispatch({
779
+ type: 'SHOW_CONFIRM_REMOVE_MARKETPLACE',
780
+ payload: selectedMarketplace.id,
781
+ });
782
+ }
783
+ return;
784
+ }
785
+ // Update marketplace (u key)
786
+ if (input === 'u') {
787
+ const selectedMarketplace = filteredMarketplaces[state.selectedIndex];
788
+ if (selectedMarketplace) {
789
+ handleUpdateMarketplace(selectedMarketplace.id);
790
+ }
791
+ else {
792
+ // No marketplace selected, update all
793
+ handleUpdateMarketplace();
794
+ }
795
+ return;
796
+ }
797
+ }
798
+ // Install (i key) - only on enabled/installed/discover tabs
423
799
  if (input === 'i' &&
424
- (state.activeTab === 'discover' || state.activeTab === 'installed')) {
425
- const items = state.activeTab === 'installed'
426
- ? state.plugins.filter((p) => p.isInstalled)
427
- : getFilteredPlugins(state);
800
+ (state.activeTab === 'enabled' ||
801
+ state.activeTab === 'installed' ||
802
+ state.activeTab === 'discover')) {
803
+ const items = getItemsForTab(state);
428
804
  const selectedPlugin = items[state.selectedIndex];
429
805
  if (selectedPlugin && !selectedPlugin.isInstalled) {
430
806
  handleInstall(selectedPlugin.id);
@@ -437,12 +813,12 @@ export default function App() {
437
813
  }
438
814
  return;
439
815
  }
440
- // Uninstall (u key) - only on discover/installed tabs
816
+ // Uninstall (u key) - only on enabled/installed/discover tabs
441
817
  if (input === 'u' &&
442
- (state.activeTab === 'discover' || state.activeTab === 'installed')) {
443
- const items = state.activeTab === 'installed'
444
- ? state.plugins.filter((p) => p.isInstalled)
445
- : getFilteredPlugins(state);
818
+ (state.activeTab === 'enabled' ||
819
+ state.activeTab === 'installed' ||
820
+ state.activeTab === 'discover')) {
821
+ const items = getItemsForTab(state);
446
822
  const selectedPlugin = items[state.selectedIndex];
447
823
  if (selectedPlugin && selectedPlugin.isInstalled) {
448
824
  dispatch({ type: 'SHOW_CONFIRM_UNINSTALL', payload: selectedPlugin.id });
@@ -452,11 +828,6 @@ export default function App() {
452
828
  }
453
829
  return;
454
830
  }
455
- // Exit (q or Ctrl+C)
456
- if (input === 'q' || (key.ctrl && input === 'c')) {
457
- exit();
458
- return;
459
- }
460
831
  });
461
832
  // Loading state
462
833
  if (state.loading) {
@@ -468,14 +839,70 @@ export default function App() {
468
839
  }
469
840
  // Get filtered data for current tab
470
841
  const filteredPlugins = getFilteredPlugins(state);
471
- const installedPlugins = state.plugins.filter((p) => p.isInstalled);
472
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsx(Text, { bold: true, color: "magenta", children: "\u26A1 Claude Code Plugin Dashboard" }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { dimColor: true, children: "v0.1.0" })] }), _jsx(TabBar, { activeTab: state.activeTab }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [state.activeTab === 'discover' && (_jsx(DiscoverTab, { plugins: filteredPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, sortBy: state.sortBy, sortOrder: state.sortOrder, isSearchMode: isSearchMode })), state.activeTab === 'installed' && (_jsx(InstalledTab, { plugins: installedPlugins, selectedIndex: state.selectedIndex })), state.activeTab === 'marketplaces' && (_jsx(MarketplacesTab, { marketplaces: state.marketplaces, selectedIndex: state.selectedIndex })), state.activeTab === 'errors' && (_jsx(ErrorsTab, { errors: state.errors, selectedIndex: state.selectedIndex }))] }), state.confirmUninstall && state.operationPluginId && (_jsx(ConfirmDialog, { message: `Uninstall ${state.operationPluginId}?` })), state.message && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: state.message }) })), _jsx(KeyHints, { extraHints: state.activeTab === 'discover' || state.activeTab === 'installed'
473
- ? [
474
- { key: 'i', action: 'install' },
475
- { key: 'u', action: 'uninstall' },
476
- ...(state.activeTab === 'discover'
477
- ? [{ key: 's', action: 'sort' }]
478
- : []),
479
- ]
480
- : undefined })] }));
842
+ // Apply search filter to enabled plugins
843
+ const enabledPluginsBase = state.plugins.filter((p) => p.isInstalled && p.isEnabled);
844
+ const enabledPlugins = state.searchQuery
845
+ ? searchPlugins(state.searchQuery, enabledPluginsBase)
846
+ : enabledPluginsBase;
847
+ // Apply search filter to installed plugins
848
+ const installedPluginsBase = state.plugins.filter((p) => p.isInstalled);
849
+ const installedPlugins = state.searchQuery
850
+ ? searchPlugins(state.searchQuery, installedPluginsBase)
851
+ : installedPluginsBase;
852
+ // Apply search filter to marketplaces
853
+ const filteredMarketplaces = state.searchQuery
854
+ ? searchMarketplaces(state.searchQuery, state.marketplaces)
855
+ : state.marketplaces;
856
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsx(Text, { bold: true, color: "magenta", children: "\u26A1 Claude Code Plugin Dashboard" }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: ["v", packageJson.version] })] }), _jsx(TabBar, { activeTab: state.activeTab, isFocused: state.focusZone === 'tabbar' }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [state.activeTab === 'enabled' && (_jsx(EnabledTab, { plugins: enabledPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'installed' && (_jsx(InstalledTab, { plugins: installedPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'discover' && (_jsx(DiscoverTab, { plugins: filteredPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, sortBy: state.sortBy, sortOrder: state.sortOrder, focusZone: state.focusZone })), state.activeTab === 'marketplaces' && (_jsx(MarketplacesTab, { marketplaces: filteredMarketplaces, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'errors' && (_jsx(ErrorsTab, { errors: state.errors, selectedIndex: state.selectedIndex }))] }), state.confirmUninstall && state.operationPluginId && (_jsx(ConfirmDialog, { message: `Uninstall ${state.operationPluginId}?` })), state.confirmRemoveMarketplace && state.operationMarketplaceId && (_jsx(ConfirmDialog, { message: `Remove marketplace ${state.operationMarketplaceId}?` })), state.showAddMarketplaceDialog && (_jsx(AddMarketplaceDialog, { value: state.searchQuery, error: state.addMarketplaceError ?? undefined })), _jsx(HelpOverlay, { isVisible: state.showHelp }), state.message && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: state.message }) })), _jsx(KeyHints, { focusZone: state.focusZone, extraHints: (() => {
857
+ // Search mode - no extra hints (base hints cover it)
858
+ if (state.focusZone === 'search') {
859
+ return undefined;
860
+ }
861
+ // TabBar mode - no extra hints
862
+ if (state.focusZone === 'tabbar') {
863
+ return undefined;
864
+ }
865
+ // List mode - add tab-specific hints
866
+ // Plugin tabs hints (enabled, installed, discover)
867
+ if (state.activeTab === 'enabled' ||
868
+ state.activeTab === 'installed' ||
869
+ state.activeTab === 'discover') {
870
+ const hints = [
871
+ { key: '/', action: 'search' },
872
+ { key: 'i', action: 'install' },
873
+ { key: 'u', action: 'uninstall' },
874
+ ];
875
+ // Get selected plugin to determine Enter action
876
+ const items = state.activeTab === 'enabled'
877
+ ? enabledPlugins
878
+ : state.activeTab === 'installed'
879
+ ? installedPlugins
880
+ : filteredPlugins;
881
+ const selectedPlugin = items[state.selectedIndex];
882
+ // Add contextual Enter hint
883
+ if (selectedPlugin) {
884
+ if (!selectedPlugin.isInstalled) {
885
+ hints.push({ key: 'Enter', action: 'install' });
886
+ }
887
+ else {
888
+ hints.push({ key: 'Enter', action: 'toggle' });
889
+ }
890
+ }
891
+ // Add sort hint for discover tab
892
+ if (state.activeTab === 'discover') {
893
+ hints.push({ key: 's', action: 'sort' });
894
+ }
895
+ return hints;
896
+ }
897
+ // Marketplaces tab hints
898
+ if (state.activeTab === 'marketplaces') {
899
+ return [
900
+ { key: '/', action: 'search' },
901
+ { key: 'a', action: 'add' },
902
+ { key: 'd', action: 'remove' },
903
+ { key: 'u', action: 'update' },
904
+ ];
905
+ }
906
+ return undefined;
907
+ })() })] }));
481
908
  }