@laststance/claude-plugin-dashboard 0.3.0 → 0.3.2

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 (47) hide show
  1. package/README.md +1 -0
  2. package/dist/app.js +346 -211
  3. package/dist/cli.js +3 -1
  4. package/dist/components/ComponentBadges.d.ts +0 -9
  5. package/dist/components/ComponentBadges.js +0 -33
  6. package/dist/components/ComponentDetail.d.ts +32 -0
  7. package/dist/components/ComponentDetail.js +106 -0
  8. package/dist/components/ComponentList.d.ts +36 -2
  9. package/dist/components/ComponentList.js +105 -11
  10. package/dist/components/HelpOverlay.js +1 -0
  11. package/dist/components/KeyHints.d.ts +1 -0
  12. package/dist/components/KeyHints.js +8 -1
  13. package/dist/components/PluginDetail.d.ts +16 -3
  14. package/dist/components/PluginDetail.js +29 -3
  15. package/dist/services/componentService.d.ts +10 -42
  16. package/dist/services/componentService.js +19 -412
  17. package/dist/services/components/hookService.d.ts +17 -0
  18. package/dist/services/components/hookService.js +45 -0
  19. package/dist/services/components/index.d.ts +41 -0
  20. package/dist/services/components/index.js +126 -0
  21. package/dist/services/components/markdownService.d.ts +39 -0
  22. package/dist/services/components/markdownService.js +147 -0
  23. package/dist/services/components/serverService.d.ts +28 -0
  24. package/dist/services/components/serverService.js +69 -0
  25. package/dist/services/components/skillService.d.ts +48 -0
  26. package/dist/services/components/skillService.js +164 -0
  27. package/dist/services/components/utils.d.ts +23 -0
  28. package/dist/services/components/utils.js +42 -0
  29. package/dist/services/pluginActionsService.d.ts +31 -2
  30. package/dist/services/pluginActionsService.js +65 -6
  31. package/dist/store/index.d.ts +46 -0
  32. package/dist/store/index.js +47 -0
  33. package/dist/store/slices/marketplaceSlice.d.ts +344 -0
  34. package/dist/store/slices/marketplaceSlice.js +152 -0
  35. package/dist/store/slices/pluginSlice.d.ts +1544 -0
  36. package/dist/store/slices/pluginSlice.js +191 -0
  37. package/dist/store/slices/uiSlice.d.ts +147 -0
  38. package/dist/store/slices/uiSlice.js +126 -0
  39. package/dist/tabs/DiscoverTab.d.ts +8 -2
  40. package/dist/tabs/DiscoverTab.js +2 -2
  41. package/dist/tabs/EnabledTab.d.ts +8 -2
  42. package/dist/tabs/EnabledTab.js +2 -2
  43. package/dist/tabs/ErrorsTab.js +1 -1
  44. package/dist/tabs/InstalledTab.d.ts +8 -2
  45. package/dist/tabs/InstalledTab.js +2 -2
  46. package/dist/types/index.d.ts +47 -4
  47. package/package.json +7 -2
package/dist/app.js CHANGED
@@ -3,7 +3,9 @@ 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 { useEffect, useReducer } from 'react';
6
+ import { useEffect, useMemo } from 'react';
7
+ import * as os from 'node:os';
8
+ import * as path from 'node:path';
7
9
  import { Box, Text, useInput, useApp } from 'ink';
8
10
  import { match, P } from 'ts-pattern';
9
11
  import TabBar, { getNextTab } from './components/TabBar.js';
@@ -14,12 +16,21 @@ import InstalledTab from './tabs/InstalledTab.js';
14
16
  import MarketplacesTab from './tabs/MarketplacesTab.js';
15
17
  import ErrorsTab from './tabs/ErrorsTab.js';
16
18
  import { loadAllPlugins, loadMarketplaces, searchPlugins, searchMarketplaces, sortPlugins, } from './services/pluginService.js';
19
+ import { getSkillDetailedInfo, getMarkdownComponentDetailedInfo, } from './services/componentService.js';
20
+ import { flattenComponents } from './components/ComponentList.js';
17
21
  import { togglePlugin } from './services/settingsService.js';
18
- import { installPlugin, uninstallPlugin, } from './services/pluginActionsService.js';
22
+ import { installPlugin, uninstallPlugin, updateAllPlugins, } from './services/pluginActionsService.js';
19
23
  import { addMarketplace, removeMarketplace, updateMarketplace, toggleAutoUpdate, } from './services/marketplaceActionsService.js';
20
24
  import AddMarketplaceDialog from './components/AddMarketplaceDialog.js';
21
25
  import ConfirmDialog from './components/ConfirmDialog.js';
22
26
  import HelpOverlay from './components/HelpOverlay.js';
27
+ import { useAppDispatch, useAppSelector,
28
+ // UI actions
29
+ setTab, nextTab, prevTab, setFocusZone, toggleHelp, setSearchQuery, setSort, setMessage,
30
+ // Plugin actions
31
+ setPlugins, setError, setSelectedIndex, moveSelection, togglePluginEnabled, startOperation, endOperation, showConfirmUninstall, hideConfirmUninstall, showConfirmUpdateAll, hideConfirmUpdateAll, setUpdateProgress, clearUpdateProgress, moveComponentSelection, enterComponentMode, exitComponentMode,
32
+ // Marketplace actions
33
+ setMarketplaces, startMarketplaceOperation, endMarketplaceOperation, showConfirmRemoveMarketplace, hideConfirmRemoveMarketplace, showAddMarketplaceDialog, hideAddMarketplaceDialog, setAddMarketplaceError, showMarketplaceActionMenu as showMarketplaceActionMenuAction, hideMarketplaceActionMenu, moveActionMenuSelection, } from './store/index.js';
23
34
  import packageJson from '../package.json' with { type: 'json' };
24
35
  /**
25
36
  * Initial application state
@@ -40,6 +51,8 @@ export const initialState = {
40
51
  operation: 'idle',
41
52
  operationPluginId: null,
42
53
  confirmUninstall: false,
54
+ confirmUpdateAll: false,
55
+ updateProgress: null,
43
56
  showHelp: false,
44
57
  marketplaceOperation: 'idle',
45
58
  operationMarketplaceId: null,
@@ -48,6 +61,7 @@ export const initialState = {
48
61
  addMarketplaceError: null,
49
62
  showMarketplaceActionMenu: false,
50
63
  actionMenuSelectedIndex: 0,
64
+ selectedComponentIndex: 0,
51
65
  };
52
66
  /**
53
67
  * Get available focus zones for the current tab
@@ -215,7 +229,9 @@ export function appReducer(state, action) {
215
229
  operationPluginId: action.payload.pluginId,
216
230
  message: action.payload.operation === 'installing'
217
231
  ? `Installing ${action.payload.pluginId}...`
218
- : `Uninstalling ${action.payload.pluginId}...`,
232
+ : action.payload.operation === 'uninstalling'
233
+ ? `Uninstalling ${action.payload.pluginId}...`
234
+ : `Updating plugins...`,
219
235
  };
220
236
  case 'END_OPERATION':
221
237
  return {
@@ -311,6 +327,32 @@ export function appReducer(state, action) {
311
327
  actionMenuSelectedIndex: newIndex,
312
328
  };
313
329
  }
330
+ case 'SET_COMPONENT_INDEX':
331
+ return {
332
+ ...state,
333
+ selectedComponentIndex: action.payload,
334
+ };
335
+ case 'MOVE_COMPONENT_SELECTION': {
336
+ const newIndex = action.payload === 'up'
337
+ ? Math.max(0, state.selectedComponentIndex - 1)
338
+ : Math.min(action.maxIndex, state.selectedComponentIndex + 1);
339
+ return {
340
+ ...state,
341
+ selectedComponentIndex: newIndex,
342
+ };
343
+ }
344
+ case 'ENTER_COMPONENT_MODE':
345
+ return {
346
+ ...state,
347
+ focusZone: 'components',
348
+ selectedComponentIndex: 0,
349
+ };
350
+ case 'EXIT_COMPONENT_MODE':
351
+ return {
352
+ ...state,
353
+ focusZone: 'list',
354
+ selectedComponentIndex: 0,
355
+ };
314
356
  default:
315
357
  return state;
316
358
  }
@@ -373,66 +415,110 @@ export function getFilteredMarketplaces(state) {
373
415
  */
374
416
  export default function App() {
375
417
  const { exit } = useApp();
376
- const [state, dispatch] = useReducer(appReducer, initialState);
418
+ const dispatch = useAppDispatch();
419
+ // Select state from Redux slices
420
+ const ui = useAppSelector((s) => s.ui);
421
+ const pluginState = useAppSelector((s) => s.plugins);
422
+ const marketplaceState = useAppSelector((s) => s.marketplaces);
423
+ // Combine into AppState-compatible object for existing helper functions
424
+ const state = {
425
+ activeTab: ui.activeTab,
426
+ focusZone: ui.focusZone,
427
+ showHelp: ui.showHelp,
428
+ searchQuery: ui.searchQuery,
429
+ sortBy: ui.sortBy,
430
+ sortOrder: ui.sortOrder,
431
+ message: ui.message,
432
+ plugins: pluginState.plugins,
433
+ errors: pluginState.errors,
434
+ loading: pluginState.loading,
435
+ error: pluginState.error,
436
+ selectedIndex: pluginState.selectedIndex,
437
+ operation: pluginState.operation,
438
+ operationPluginId: pluginState.operationPluginId,
439
+ confirmUninstall: pluginState.confirmUninstall,
440
+ confirmUpdateAll: pluginState.confirmUpdateAll,
441
+ updateProgress: pluginState.updateProgress,
442
+ selectedComponentIndex: pluginState.selectedComponentIndex,
443
+ marketplaces: marketplaceState.marketplaces,
444
+ marketplaceOperation: marketplaceState.marketplaceOperation,
445
+ operationMarketplaceId: marketplaceState.operationMarketplaceId,
446
+ confirmRemoveMarketplace: marketplaceState.confirmRemoveMarketplace,
447
+ showAddMarketplaceDialog: marketplaceState.showAddMarketplaceDialog,
448
+ addMarketplaceError: marketplaceState.addMarketplaceError,
449
+ showMarketplaceActionMenu: marketplaceState.showMarketplaceActionMenu,
450
+ actionMenuSelectedIndex: marketplaceState.actionMenuSelectedIndex,
451
+ };
377
452
  // Load data on mount
378
453
  useEffect(() => {
379
454
  try {
380
455
  const plugins = loadAllPlugins();
381
456
  const marketplaces = loadMarketplaces();
382
- dispatch({ type: 'SET_PLUGINS', payload: plugins });
383
- dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
457
+ dispatch(setPlugins(plugins));
458
+ dispatch(setMarketplaces(marketplaces));
384
459
  }
385
460
  catch (error) {
386
- dispatch({
387
- type: 'SET_ERROR',
388
- payload: error instanceof Error ? error.message : 'Failed to load data',
389
- });
461
+ dispatch(setError(error instanceof Error ? error.message : 'Failed to load data'));
390
462
  }
391
463
  }, []);
392
464
  /**
393
465
  * Handle plugin installation
394
466
  */
395
467
  async function handleInstall(pluginId) {
396
- dispatch({
397
- type: 'START_OPERATION',
398
- payload: { operation: 'installing', pluginId },
399
- });
468
+ dispatch(startOperation({ operation: 'installing', pluginId }));
400
469
  const result = await installPlugin(pluginId);
401
- dispatch({ type: 'END_OPERATION' });
470
+ dispatch(endOperation());
402
471
  if (result.success) {
403
472
  // Reload plugins to get fresh state
404
473
  const plugins = loadAllPlugins();
405
- dispatch({ type: 'SET_PLUGINS', payload: plugins });
406
- dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
474
+ dispatch(setPlugins(plugins));
475
+ dispatch(setMessage(`✅ ${result.message}`));
407
476
  }
408
477
  else {
409
- dispatch({
410
- type: 'SET_MESSAGE',
411
- payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
412
- });
478
+ dispatch(setMessage(`❌ ${result.message}${result.error ? `: ${result.error}` : ''}`));
413
479
  }
414
480
  }
415
481
  /**
416
482
  * Handle plugin uninstallation
417
483
  */
418
484
  async function handleUninstall(pluginId) {
419
- dispatch({
420
- type: 'START_OPERATION',
421
- payload: { operation: 'uninstalling', pluginId },
422
- });
485
+ dispatch(startOperation({ operation: 'uninstalling', pluginId }));
423
486
  const result = await uninstallPlugin(pluginId);
424
- dispatch({ type: 'END_OPERATION' });
487
+ dispatch(endOperation());
425
488
  if (result.success) {
426
489
  // Reload plugins to get fresh state
427
490
  const plugins = loadAllPlugins();
428
- dispatch({ type: 'SET_PLUGINS', payload: plugins });
429
- dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
491
+ dispatch(setPlugins(plugins));
492
+ dispatch(setMessage(`✅ ${result.message}`));
430
493
  }
431
494
  else {
432
- dispatch({
433
- type: 'SET_MESSAGE',
434
- payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
435
- });
495
+ dispatch(setMessage(`❌ ${result.message}${result.error ? `: ${result.error}` : ''}`));
496
+ }
497
+ }
498
+ /**
499
+ * Handle updating all installed plugins sequentially
500
+ */
501
+ async function handleUpdateAllPlugins() {
502
+ const installed = state.plugins.filter((p) => p.isInstalled);
503
+ if (installed.length === 0) {
504
+ dispatch(setMessage('⚠️ No installed plugins to update'));
505
+ return;
506
+ }
507
+ const pluginIds = installed.map((p) => p.id);
508
+ dispatch(startOperation({ operation: 'updating', pluginId: pluginIds[0] }));
509
+ const result = await updateAllPlugins(pluginIds, (current, total, pluginId) => {
510
+ dispatch(setUpdateProgress({ current, total, pluginId }));
511
+ });
512
+ dispatch(clearUpdateProgress());
513
+ dispatch(endOperation());
514
+ // Reload plugins to get fresh state
515
+ const plugins = loadAllPlugins();
516
+ dispatch(setPlugins(plugins));
517
+ if (result.failed === 0) {
518
+ dispatch(setMessage(`✅ Updated ${result.succeeded}/${result.total} plugins`));
519
+ }
520
+ else {
521
+ dispatch(setMessage(`⚠️ Updated ${result.succeeded}/${result.total} (${result.failed} failed)`));
436
522
  }
437
523
  }
438
524
  /**
@@ -440,30 +526,24 @@ export default function App() {
440
526
  * @param source - Marketplace source (e.g., "owner/repo", URL, or local path)
441
527
  */
442
528
  async function handleAddMarketplace(source) {
443
- dispatch({
444
- type: 'START_MARKETPLACE_OPERATION',
445
- payload: { operation: 'adding' },
446
- });
529
+ dispatch(startMarketplaceOperation({ operation: 'adding' }));
447
530
  const result = await addMarketplace(source);
448
- dispatch({ type: 'END_MARKETPLACE_OPERATION' });
531
+ dispatch(endMarketplaceOperation());
449
532
  if (result.success) {
450
- dispatch({ type: 'HIDE_ADD_MARKETPLACE_DIALOG' });
533
+ dispatch(hideAddMarketplaceDialog());
451
534
  // Reload marketplaces to get fresh state
452
535
  const marketplaces = loadMarketplaces();
453
- dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
536
+ dispatch(setMarketplaces(marketplaces));
454
537
  // Also reload plugins as new marketplace may have plugins
455
538
  const plugins = loadAllPlugins();
456
- dispatch({ type: 'SET_PLUGINS', payload: plugins });
539
+ dispatch(setPlugins(plugins));
457
540
  // Reset selection to avoid pointing to a different marketplace after re-sort
458
- dispatch({ type: 'SET_SELECTED_INDEX', payload: 0 });
459
- dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
541
+ dispatch(setSelectedIndex(0));
542
+ dispatch(setMessage(`✅ ${result.message}`));
460
543
  }
461
544
  else {
462
545
  // Keep dialog open and show error inline
463
- dispatch({
464
- type: 'SET_ADD_MARKETPLACE_ERROR',
465
- payload: result.error || result.message,
466
- });
546
+ dispatch(setAddMarketplaceError(result.error || result.message));
467
547
  }
468
548
  }
469
549
  /**
@@ -471,33 +551,24 @@ export default function App() {
471
551
  * @param marketplaceId - Marketplace identifier to remove
472
552
  */
473
553
  async function handleRemoveMarketplace(marketplaceId) {
474
- dispatch({
475
- type: 'START_MARKETPLACE_OPERATION',
476
- payload: { operation: 'removing', marketplaceId },
477
- });
554
+ dispatch(startMarketplaceOperation({ operation: 'removing', marketplaceId }));
478
555
  const result = await removeMarketplace(marketplaceId);
479
- dispatch({ type: 'END_MARKETPLACE_OPERATION' });
556
+ dispatch(endMarketplaceOperation());
480
557
  if (result.success) {
481
558
  // Reload marketplaces to get fresh state
482
559
  const marketplaces = loadMarketplaces();
483
- dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
560
+ dispatch(setMarketplaces(marketplaces));
484
561
  // Also reload plugins as removed marketplace's plugins should be gone
485
562
  const plugins = loadAllPlugins();
486
- dispatch({ type: 'SET_PLUGINS', payload: plugins });
487
- dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
563
+ dispatch(setPlugins(plugins));
564
+ dispatch(setMessage(`✅ ${result.message}`));
488
565
  // Reset selection if needed
489
566
  if (state.selectedIndex >= marketplaces.length) {
490
- dispatch({
491
- type: 'SET_SELECTED_INDEX',
492
- payload: Math.max(0, marketplaces.length - 1),
493
- });
567
+ dispatch(setSelectedIndex(Math.max(0, marketplaces.length - 1)));
494
568
  }
495
569
  }
496
570
  else {
497
- dispatch({
498
- type: 'SET_MESSAGE',
499
- payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
500
- });
571
+ dispatch(setMessage(`❌ ${result.message}${result.error ? `: ${result.error}` : ''}`));
501
572
  }
502
573
  }
503
574
  /**
@@ -505,27 +576,21 @@ export default function App() {
505
576
  * @param marketplaceId - Optional marketplace identifier. If omitted, updates all.
506
577
  */
507
578
  async function handleUpdateMarketplace(marketplaceId) {
508
- dispatch({
509
- type: 'START_MARKETPLACE_OPERATION',
510
- payload: { operation: 'updating', marketplaceId },
511
- });
579
+ dispatch(startMarketplaceOperation({ operation: 'updating', marketplaceId }));
512
580
  const result = await updateMarketplace(marketplaceId);
513
- dispatch({ type: 'END_MARKETPLACE_OPERATION' });
581
+ dispatch(endMarketplaceOperation());
514
582
  if (result.success) {
515
583
  // Reload marketplaces and plugins to get fresh state
516
584
  const marketplaces = loadMarketplaces();
517
- dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
585
+ dispatch(setMarketplaces(marketplaces));
518
586
  const plugins = loadAllPlugins();
519
- dispatch({ type: 'SET_PLUGINS', payload: plugins });
587
+ dispatch(setPlugins(plugins));
520
588
  // Reset selection to avoid pointing to a different marketplace after re-sort
521
- dispatch({ type: 'SET_SELECTED_INDEX', payload: 0 });
522
- dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
589
+ dispatch(setSelectedIndex(0));
590
+ dispatch(setMessage(`✅ ${result.message}`));
523
591
  }
524
592
  else {
525
- dispatch({
526
- type: 'SET_MESSAGE',
527
- payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
528
- });
593
+ dispatch(setMessage(`❌ ${result.message}${result.error ? `: ${result.error}` : ''}`));
529
594
  }
530
595
  }
531
596
  /**
@@ -534,23 +599,17 @@ export default function App() {
534
599
  * @param currentValue - Current auto-update state
535
600
  */
536
601
  async function handleToggleAutoUpdate(marketplaceId, currentValue) {
537
- dispatch({
538
- type: 'START_MARKETPLACE_OPERATION',
539
- payload: { operation: 'updating', marketplaceId },
540
- });
602
+ dispatch(startMarketplaceOperation({ operation: 'updating', marketplaceId }));
541
603
  const result = await toggleAutoUpdate(marketplaceId, currentValue);
542
- dispatch({ type: 'END_MARKETPLACE_OPERATION' });
604
+ dispatch(endMarketplaceOperation());
543
605
  if (result.success) {
544
606
  // Reload marketplaces to get fresh state
545
607
  const marketplaces = loadMarketplaces();
546
- dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
547
- dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
608
+ dispatch(setMarketplaces(marketplaces));
609
+ dispatch(setMessage(`✅ ${result.message}`));
548
610
  }
549
611
  else {
550
- dispatch({
551
- type: 'SET_MESSAGE',
552
- payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
553
- });
612
+ dispatch(setMessage(`❌ ${result.message}${result.error ? `: ${result.error}` : ''}`));
554
613
  }
555
614
  }
556
615
  /**
@@ -560,13 +619,10 @@ export default function App() {
560
619
  */
561
620
  function handleBrowseMarketplacePlugins(marketplaceId) {
562
621
  // Switch to Discover tab
563
- dispatch({ type: 'SET_TAB', payload: 'discover' });
622
+ dispatch(setTab('discover'));
564
623
  // Set search query to filter by marketplace
565
- dispatch({ type: 'SET_SEARCH_QUERY', payload: marketplaceId });
566
- dispatch({
567
- type: 'SET_MESSAGE',
568
- payload: `Browsing plugins from ${marketplaceId}`,
569
- });
624
+ dispatch(setSearchQuery(marketplaceId));
625
+ dispatch(setMessage(`Browsing plugins from ${marketplaceId}`));
570
626
  }
571
627
  // Keyboard input handling
572
628
  useInput((input, key) => {
@@ -577,7 +633,7 @@ export default function App() {
577
633
  // Handle help overlay
578
634
  if (state.showHelp) {
579
635
  if (input === 'h' || key.escape) {
580
- dispatch({ type: 'TOGGLE_HELP' });
636
+ dispatch(toggleHelp());
581
637
  }
582
638
  return;
583
639
  }
@@ -588,19 +644,33 @@ export default function App() {
588
644
  }
589
645
  // Toggle help (h key)
590
646
  if (input === 'h') {
591
- dispatch({ type: 'TOGGLE_HELP' });
647
+ dispatch(toggleHelp());
592
648
  return;
593
649
  }
594
650
  // Handle plugin uninstall confirmation dialog
595
651
  if (state.confirmUninstall && state.operationPluginId) {
596
652
  if (input === 'y' || input === 'Y' || key.return) {
597
- dispatch({ type: 'HIDE_CONFIRM_UNINSTALL' });
653
+ dispatch(hideConfirmUninstall());
598
654
  handleUninstall(state.operationPluginId);
599
655
  return;
600
656
  }
601
657
  if (input === 'n' || input === 'N' || key.escape) {
602
- dispatch({ type: 'HIDE_CONFIRM_UNINSTALL' });
603
- dispatch({ type: 'SET_MESSAGE', payload: 'Uninstall cancelled' });
658
+ dispatch(hideConfirmUninstall());
659
+ dispatch(setMessage('Uninstall cancelled'));
660
+ return;
661
+ }
662
+ return;
663
+ }
664
+ // Handle update all confirmation dialog
665
+ if (state.confirmUpdateAll) {
666
+ if (input === 'y' || input === 'Y' || key.return) {
667
+ dispatch(hideConfirmUpdateAll());
668
+ handleUpdateAllPlugins();
669
+ return;
670
+ }
671
+ if (input === 'n' || input === 'N' || key.escape) {
672
+ dispatch(hideConfirmUpdateAll());
673
+ dispatch(setMessage('Update cancelled'));
604
674
  return;
605
675
  }
606
676
  return;
@@ -608,13 +678,13 @@ export default function App() {
608
678
  // Handle marketplace remove confirmation dialog
609
679
  if (state.confirmRemoveMarketplace && state.operationMarketplaceId) {
610
680
  if (input === 'y' || input === 'Y' || key.return) {
611
- dispatch({ type: 'HIDE_CONFIRM_REMOVE_MARKETPLACE' });
681
+ dispatch(hideConfirmRemoveMarketplace());
612
682
  handleRemoveMarketplace(state.operationMarketplaceId);
613
683
  return;
614
684
  }
615
685
  if (input === 'n' || input === 'N' || key.escape) {
616
- dispatch({ type: 'HIDE_CONFIRM_REMOVE_MARKETPLACE' });
617
- dispatch({ type: 'SET_MESSAGE', payload: 'Remove cancelled' });
686
+ dispatch(hideConfirmRemoveMarketplace());
687
+ dispatch(setMessage('Remove cancelled'));
618
688
  return;
619
689
  }
620
690
  return;
@@ -628,24 +698,18 @@ export default function App() {
628
698
  }
629
699
  // Cancel on Escape
630
700
  if (key.escape) {
631
- dispatch({ type: 'HIDE_ADD_MARKETPLACE_DIALOG' });
632
- dispatch({ type: 'SET_MESSAGE', payload: 'Add marketplace cancelled' });
701
+ dispatch(hideAddMarketplaceDialog());
702
+ dispatch(setMessage('Add marketplace cancelled'));
633
703
  return;
634
704
  }
635
705
  // Backspace
636
706
  if (key.backspace || key.delete) {
637
- dispatch({
638
- type: 'SET_SEARCH_QUERY',
639
- payload: state.searchQuery.slice(0, -1),
640
- });
707
+ dispatch(setSearchQuery(state.searchQuery.slice(0, -1)));
641
708
  return;
642
709
  }
643
710
  // Character input
644
711
  if (input && input.length === 1 && !key.ctrl && !key.meta) {
645
- dispatch({
646
- type: 'SET_SEARCH_QUERY',
647
- payload: state.searchQuery + input,
648
- });
712
+ dispatch(setSearchQuery(state.searchQuery + input));
649
713
  return;
650
714
  }
651
715
  return;
@@ -654,21 +718,21 @@ export default function App() {
654
718
  if (state.showMarketplaceActionMenu) {
655
719
  const selectedMarketplace = getFilteredMarketplaces(state)[state.selectedIndex];
656
720
  if (!selectedMarketplace) {
657
- dispatch({ type: 'HIDE_MARKETPLACE_ACTION_MENU' });
721
+ dispatch(hideMarketplaceActionMenu());
658
722
  return;
659
723
  }
660
724
  // Up/Down arrow navigation
661
725
  if (key.upArrow || (key.ctrl && input === 'p')) {
662
- dispatch({ type: 'MOVE_ACTION_MENU_SELECTION', payload: 'up' });
726
+ dispatch(moveActionMenuSelection('up'));
663
727
  return;
664
728
  }
665
729
  if (key.downArrow || (key.ctrl && input === 'n')) {
666
- dispatch({ type: 'MOVE_ACTION_MENU_SELECTION', payload: 'down' });
730
+ dispatch(moveActionMenuSelection('down'));
667
731
  return;
668
732
  }
669
733
  // Execute action on Enter
670
734
  if (key.return) {
671
- dispatch({ type: 'HIDE_MARKETPLACE_ACTION_MENU' });
735
+ dispatch(hideMarketplaceActionMenu());
672
736
  const actionIndex = state.actionMenuSelectedIndex;
673
737
  if (actionIndex === 0) {
674
738
  // Browse plugins
@@ -684,16 +748,13 @@ export default function App() {
684
748
  }
685
749
  else if (actionIndex === 3) {
686
750
  // Remove marketplace (show confirmation)
687
- dispatch({
688
- type: 'SHOW_CONFIRM_REMOVE_MARKETPLACE',
689
- payload: selectedMarketplace.id,
690
- });
751
+ dispatch(showConfirmRemoveMarketplace(selectedMarketplace.id));
691
752
  }
692
753
  return;
693
754
  }
694
755
  // Close menu on Escape
695
756
  if (key.escape) {
696
- dispatch({ type: 'HIDE_MARKETPLACE_ACTION_MENU' });
757
+ dispatch(hideMarketplaceActionMenu());
697
758
  return;
698
759
  }
699
760
  return;
@@ -702,7 +763,7 @@ export default function App() {
702
763
  if (state.focusZone === 'search') {
703
764
  // Up arrow: move focus to tabbar
704
765
  if (key.upArrow || (key.ctrl && input === 'p')) {
705
- dispatch({ type: 'SET_FOCUS_ZONE', payload: 'tabbar' });
766
+ dispatch(setFocusZone('tabbar'));
706
767
  return;
707
768
  }
708
769
  // Down arrow, Enter, or Escape: move focus to list
@@ -710,21 +771,15 @@ export default function App() {
710
771
  key.return ||
711
772
  key.downArrow ||
712
773
  (key.ctrl && input === 'n')) {
713
- dispatch({ type: 'SET_FOCUS_ZONE', payload: 'list' });
774
+ dispatch(setFocusZone('list'));
714
775
  return;
715
776
  }
716
777
  if (key.backspace || key.delete) {
717
- dispatch({
718
- type: 'SET_SEARCH_QUERY',
719
- payload: state.searchQuery.slice(0, -1),
720
- });
778
+ dispatch(setSearchQuery(state.searchQuery.slice(0, -1)));
721
779
  return;
722
780
  }
723
781
  if (input && input.length === 1 && !key.ctrl && !key.meta) {
724
- dispatch({
725
- type: 'SET_SEARCH_QUERY',
726
- payload: state.searchQuery + input,
727
- });
782
+ dispatch(setSearchQuery(state.searchQuery + input));
728
783
  return;
729
784
  }
730
785
  return;
@@ -734,27 +789,54 @@ export default function App() {
734
789
  // Down arrow: move to search (or list if no search)
735
790
  if (key.downArrow || (key.ctrl && input === 'n')) {
736
791
  const zones = getAvailableZones(state.activeTab);
737
- dispatch({
738
- type: 'SET_FOCUS_ZONE',
739
- payload: zones.includes('search') ? 'search' : 'list',
740
- });
792
+ dispatch(setFocusZone(zones.includes('search') ? 'search' : 'list'));
741
793
  return;
742
794
  }
743
795
  // Left/Right arrows and Ctrl+B/F: tab switching (only in tabbar)
744
796
  // Keep focus on tabbar after navigation
745
797
  if (key.leftArrow || (key.ctrl && input === 'b')) {
746
- dispatch({ type: 'PREV_TAB' });
747
- dispatch({ type: 'SET_FOCUS_ZONE', payload: 'tabbar' });
798
+ dispatch(prevTab());
799
+ dispatch(setFocusZone('tabbar'));
748
800
  return;
749
801
  }
750
802
  if (key.rightArrow || (key.ctrl && input === 'f')) {
751
- dispatch({ type: 'NEXT_TAB' });
752
- dispatch({ type: 'SET_FOCUS_ZONE', payload: 'tabbar' });
803
+ dispatch(nextTab());
804
+ dispatch(setFocusZone('tabbar'));
753
805
  return;
754
806
  }
755
807
  // Tab key: next tab (resets focus to list)
756
808
  if (key.tab) {
757
- dispatch({ type: 'NEXT_TAB' });
809
+ dispatch(nextTab());
810
+ return;
811
+ }
812
+ return;
813
+ }
814
+ // Component focus zone navigation
815
+ if (state.focusZone === 'components') {
816
+ // Get current plugin and its components
817
+ const items = getItemsForTab(state);
818
+ const selectedPlugin = items[state.selectedIndex];
819
+ const components = selectedPlugin
820
+ ? flattenComponents(selectedPlugin.componentsDetailed)
821
+ : [];
822
+ // Left arrow: exit component mode, go back to list
823
+ if (key.leftArrow || (key.ctrl && input === 'b') || key.escape) {
824
+ dispatch(exitComponentMode());
825
+ return;
826
+ }
827
+ // Up/Down: navigate components
828
+ if (key.upArrow || (key.ctrl && input === 'p')) {
829
+ dispatch(moveComponentSelection({
830
+ direction: 'up',
831
+ maxIndex: Math.max(0, components.length - 1),
832
+ }));
833
+ return;
834
+ }
835
+ if (key.downArrow || (key.ctrl && input === 'n')) {
836
+ dispatch(moveComponentSelection({
837
+ direction: 'down',
838
+ maxIndex: Math.max(0, components.length - 1),
839
+ }));
758
840
  return;
759
841
  }
760
842
  return;
@@ -765,24 +847,43 @@ export default function App() {
765
847
  if (state.selectedIndex === 0) {
766
848
  // At top of list: move focus to search (or tabbar if no search)
767
849
  const zones = getAvailableZones(state.activeTab);
768
- dispatch({
769
- type: 'SET_FOCUS_ZONE',
770
- payload: zones.includes('search') ? 'search' : 'tabbar',
771
- });
850
+ dispatch(setFocusZone(zones.includes('search') ? 'search' : 'tabbar'));
772
851
  }
773
852
  else {
774
- dispatch({ type: 'MOVE_SELECTION', payload: 'up' });
853
+ dispatch(moveSelection({
854
+ direction: 'up',
855
+ maxIndex: Math.max(0, getItemsForTab(state).length - 1),
856
+ }));
775
857
  }
776
858
  return;
777
859
  }
778
860
  // Down arrow: move down in list
779
861
  if (key.downArrow || (key.ctrl && input === 'n')) {
780
- dispatch({ type: 'MOVE_SELECTION', payload: 'down' });
862
+ dispatch(moveSelection({
863
+ direction: 'down',
864
+ maxIndex: Math.max(0, getItemsForTab(state).length - 1),
865
+ }));
781
866
  return;
782
867
  }
868
+ // Right arrow: enter component mode (on plugin tabs)
869
+ if ((key.rightArrow || (key.ctrl && input === 'f')) &&
870
+ state.focusZone === 'list' &&
871
+ (state.activeTab === 'enabled' ||
872
+ state.activeTab === 'installed' ||
873
+ state.activeTab === 'discover')) {
874
+ const items = getItemsForTab(state);
875
+ const selectedPlugin = items[state.selectedIndex];
876
+ if (selectedPlugin?.componentsDetailed) {
877
+ const components = flattenComponents(selectedPlugin.componentsDetailed);
878
+ if (components.length > 0) {
879
+ dispatch(enterComponentMode());
880
+ return;
881
+ }
882
+ }
883
+ }
783
884
  // Tab key: next tab (from list zone)
784
885
  if (key.tab) {
785
- dispatch({ type: 'NEXT_TAB' });
886
+ dispatch(nextTab());
786
887
  return;
787
888
  }
788
889
  // Enter search mode (/ key on supported tabs)
@@ -793,7 +894,7 @@ export default function App() {
793
894
  'marketplaces',
794
895
  ];
795
896
  if (input === '/' && searchEnabledTabs.includes(state.activeTab)) {
796
- dispatch({ type: 'SET_FOCUS_ZONE', payload: 'search' });
897
+ dispatch(setFocusZone('search'));
797
898
  return;
798
899
  }
799
900
  // Enter key: Install (non-installed) or Toggle (installed)
@@ -812,22 +913,13 @@ export default function App() {
812
913
  // Toggle installed plugin
813
914
  try {
814
915
  const newState = togglePlugin(selectedPlugin.id);
815
- dispatch({
816
- type: 'TOGGLE_PLUGIN_ENABLED',
817
- payload: selectedPlugin.id,
818
- });
819
- dispatch({
820
- type: 'SET_MESSAGE',
821
- payload: newState
822
- ? `✅ ${selectedPlugin.name} enabled`
823
- : `❌ ${selectedPlugin.name} disabled`,
824
- });
916
+ dispatch(togglePluginEnabled(selectedPlugin.id));
917
+ dispatch(setMessage(newState
918
+ ? `✅ ${selectedPlugin.name} enabled`
919
+ : `❌ ${selectedPlugin.name} disabled`));
825
920
  }
826
921
  catch (error) {
827
- dispatch({
828
- type: 'SET_MESSAGE',
829
- payload: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
830
- });
922
+ dispatch(setMessage(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
831
923
  }
832
924
  }
833
925
  }
@@ -843,22 +935,13 @@ export default function App() {
843
935
  if (selectedPlugin && selectedPlugin.isInstalled) {
844
936
  try {
845
937
  const newState = togglePlugin(selectedPlugin.id);
846
- dispatch({
847
- type: 'TOGGLE_PLUGIN_ENABLED',
848
- payload: selectedPlugin.id,
849
- });
850
- dispatch({
851
- type: 'SET_MESSAGE',
852
- payload: newState
853
- ? `✅ ${selectedPlugin.name} enabled`
854
- : `❌ ${selectedPlugin.name} disabled`,
855
- });
938
+ dispatch(togglePluginEnabled(selectedPlugin.id));
939
+ dispatch(setMessage(newState
940
+ ? `✅ ${selectedPlugin.name} enabled`
941
+ : `❌ ${selectedPlugin.name} disabled`));
856
942
  }
857
943
  catch (error) {
858
- dispatch({
859
- type: 'SET_MESSAGE',
860
- payload: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
861
- });
944
+ dispatch(setMessage(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
862
945
  }
863
946
  }
864
947
  return;
@@ -870,26 +953,20 @@ export default function App() {
870
953
  name: 'date',
871
954
  date: 'installs',
872
955
  };
873
- dispatch({
874
- type: 'SET_SORT',
875
- payload: { by: nextSort[state.sortBy], order: state.sortOrder },
876
- });
956
+ dispatch(setSort({ by: nextSort[state.sortBy], order: state.sortOrder }));
877
957
  return;
878
958
  }
879
959
  // Toggle sort order (S key)
880
960
  if (input === 'S' && state.activeTab === 'discover') {
881
- dispatch({
882
- type: 'SET_SORT',
883
- payload: {
884
- by: state.sortBy,
885
- order: state.sortOrder === 'asc' ? 'desc' : 'asc',
886
- },
887
- });
961
+ dispatch(setSort({
962
+ by: state.sortBy,
963
+ order: state.sortOrder === 'asc' ? 'desc' : 'asc',
964
+ }));
888
965
  return;
889
966
  }
890
967
  // Clear search (Escape)
891
968
  if (key.escape && state.searchQuery) {
892
- dispatch({ type: 'SET_SEARCH_QUERY', payload: '' });
969
+ dispatch(setSearchQuery(''));
893
970
  return;
894
971
  }
895
972
  // Marketplace-specific key bindings
@@ -898,23 +975,20 @@ export default function App() {
898
975
  if (key.return || input === 'm') {
899
976
  const selectedMarketplace = filteredMarketplaces[state.selectedIndex];
900
977
  if (selectedMarketplace) {
901
- dispatch({ type: 'SHOW_MARKETPLACE_ACTION_MENU' });
978
+ dispatch(showMarketplaceActionMenuAction());
902
979
  }
903
980
  return;
904
981
  }
905
982
  // Add marketplace (a key)
906
983
  if (input === 'a') {
907
- dispatch({ type: 'SHOW_ADD_MARKETPLACE_DIALOG' });
984
+ dispatch(showAddMarketplaceDialog());
908
985
  return;
909
986
  }
910
987
  // Remove marketplace (d key or Backspace)
911
988
  if (input === 'd' || key.backspace || key.delete) {
912
989
  const selectedMarketplace = filteredMarketplaces[state.selectedIndex];
913
990
  if (selectedMarketplace) {
914
- dispatch({
915
- type: 'SHOW_CONFIRM_REMOVE_MARKETPLACE',
916
- payload: selectedMarketplace.id,
917
- });
991
+ dispatch(showConfirmRemoveMarketplace(selectedMarketplace.id));
918
992
  }
919
993
  return;
920
994
  }
@@ -942,10 +1016,19 @@ export default function App() {
942
1016
  handleInstall(selectedPlugin.id);
943
1017
  }
944
1018
  else if (selectedPlugin?.isInstalled) {
945
- dispatch({
946
- type: 'SET_MESSAGE',
947
- payload: '⚠️ Plugin is already installed',
948
- });
1019
+ dispatch(setMessage('⚠️ Plugin is already installed'));
1020
+ }
1021
+ return;
1022
+ }
1023
+ // Update all plugins (U key = shift+u) - only on enabled/installed tabs
1024
+ if (input === 'U' &&
1025
+ (state.activeTab === 'enabled' || state.activeTab === 'installed')) {
1026
+ const installed = state.plugins.filter((p) => p.isInstalled);
1027
+ if (installed.length === 0) {
1028
+ dispatch(setMessage('⚠️ No installed plugins to update'));
1029
+ }
1030
+ else {
1031
+ dispatch(showConfirmUpdateAll());
949
1032
  }
950
1033
  return;
951
1034
  }
@@ -957,23 +1040,15 @@ export default function App() {
957
1040
  const items = getItemsForTab(state);
958
1041
  const selectedPlugin = items[state.selectedIndex];
959
1042
  if (selectedPlugin && selectedPlugin.isInstalled) {
960
- dispatch({ type: 'SHOW_CONFIRM_UNINSTALL', payload: selectedPlugin.id });
1043
+ dispatch(showConfirmUninstall(selectedPlugin.id));
961
1044
  }
962
1045
  else if (selectedPlugin && !selectedPlugin.isInstalled) {
963
- dispatch({ type: 'SET_MESSAGE', payload: '⚠️ Plugin is not installed' });
1046
+ dispatch(setMessage('⚠️ Plugin is not installed'));
964
1047
  }
965
1048
  return;
966
1049
  }
967
1050
  });
968
- // Loading state
969
- if (state.loading) {
970
- return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(Text, { children: "Loading plugins..." }) }));
971
- }
972
- // Error state
973
- if (state.error) {
974
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", children: ["Error: ", state.error] }), _jsx(Text, { dimColor: true, children: "Press q to exit" })] }));
975
- }
976
- // Get filtered data for current tab
1051
+ // Get filtered data for current tab (must be before early returns for useMemo dependency)
977
1052
  const filteredPlugins = getFilteredPlugins(state);
978
1053
  // Apply search filter to enabled plugins
979
1054
  const enabledPluginsBase = state.plugins.filter((p) => p.isInstalled && p.isEnabled);
@@ -989,13 +1064,71 @@ export default function App() {
989
1064
  const filteredMarketplaces = state.searchQuery
990
1065
  ? searchMarketplaces(state.searchQuery, state.marketplaces)
991
1066
  : state.marketplaces;
1067
+ // Get selected component detail for component focus mode (memoized)
1068
+ // Must be called before conditional returns to maintain hooks order
1069
+ const selectedComponentDetail = useMemo(() => {
1070
+ if (state.focusZone !== 'components')
1071
+ return null;
1072
+ // Get the appropriate plugins for current tab
1073
+ const currentPlugins = match(state.activeTab)
1074
+ .with('enabled', () => enabledPlugins)
1075
+ .with('installed', () => installedPlugins)
1076
+ .with('discover', () => filteredPlugins)
1077
+ .otherwise(() => []);
1078
+ const selectedPlugin = currentPlugins[state.selectedIndex];
1079
+ if (!selectedPlugin?.componentsDetailed)
1080
+ return null;
1081
+ const components = flattenComponents(selectedPlugin.componentsDetailed);
1082
+ const selectedComponent = components[state.selectedComponentIndex];
1083
+ if (!selectedComponent)
1084
+ return null;
1085
+ // Get install path for the plugin (cross-platform)
1086
+ const installPath = selectedPlugin.isInstalled
1087
+ ? path.join(os.homedir(), '.claude', 'plugins', 'cache', selectedPlugin.marketplace ?? '', selectedPlugin.name, selectedPlugin.version ?? '')
1088
+ : null;
1089
+ if (!installPath)
1090
+ return null;
1091
+ // Fetch detailed info based on component type
1092
+ if (selectedComponent.info.type === 'skill') {
1093
+ return (getSkillDetailedInfo(installPath, selectedComponent.info.name) ?? null);
1094
+ }
1095
+ else if (selectedComponent.info.type === 'command' ||
1096
+ selectedComponent.info.type === 'agent') {
1097
+ return (getMarkdownComponentDetailedInfo(installPath, selectedComponent.info.name, selectedComponent.info.type) ?? null);
1098
+ }
1099
+ // For other types (mcp, lsp, hook), return basic info
1100
+ return {
1101
+ name: selectedComponent.info.name,
1102
+ type: selectedComponent.info.type,
1103
+ description: selectedComponent.info.description,
1104
+ };
1105
+ }, [
1106
+ state.focusZone,
1107
+ state.activeTab,
1108
+ state.selectedIndex,
1109
+ state.selectedComponentIndex,
1110
+ enabledPlugins,
1111
+ installedPlugins,
1112
+ filteredPlugins,
1113
+ ]);
1114
+ const componentFocusMode = state.focusZone === 'components';
1115
+ // Loading state
1116
+ if (state.loading) {
1117
+ return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(Text, { children: "Loading plugins..." }) }));
1118
+ }
1119
+ // Error state
1120
+ if (state.error) {
1121
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", children: ["Error: ", state.error] }), _jsx(Text, { dimColor: true, children: "Press q to exit" })] }));
1122
+ }
992
1123
  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' }), _jsx(Box, { flexGrow: 1, flexDirection: "column", children: match(state.activeTab)
993
- .with('enabled', () => (_jsx(EnabledTab, { plugins: enabledPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone }, "enabled")))
994
- .with('installed', () => (_jsx(InstalledTab, { plugins: installedPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone }, "installed")))
995
- .with('discover', () => (_jsx(DiscoverTab, { plugins: filteredPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, sortBy: state.sortBy, sortOrder: state.sortOrder, focusZone: state.focusZone }, "discover")))
1124
+ .with('enabled', () => (_jsx(EnabledTab, { plugins: enabledPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone, componentFocusMode: componentFocusMode, selectedComponentIndex: state.selectedComponentIndex, selectedComponentDetail: selectedComponentDetail }, "enabled")))
1125
+ .with('installed', () => (_jsx(InstalledTab, { plugins: installedPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone, componentFocusMode: componentFocusMode, selectedComponentIndex: state.selectedComponentIndex, selectedComponentDetail: selectedComponentDetail }, "installed")))
1126
+ .with('discover', () => (_jsx(DiscoverTab, { plugins: filteredPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, sortBy: state.sortBy, sortOrder: state.sortOrder, focusZone: state.focusZone, componentFocusMode: componentFocusMode, selectedComponentIndex: state.selectedComponentIndex, selectedComponentDetail: selectedComponentDetail }, "discover")))
996
1127
  .with('marketplaces', () => (_jsx(MarketplacesTab, { marketplaces: filteredMarketplaces, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone, showActionMenu: state.showMarketplaceActionMenu, actionMenuSelectedIndex: state.actionMenuSelectedIndex }, "marketplaces")))
997
1128
  .with('errors', () => (_jsx(ErrorsTab, { errors: state.errors, selectedIndex: state.selectedIndex }, "errors")))
998
- .exhaustive() }), 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: match([state.focusZone, state.activeTab])
1129
+ .exhaustive() }), state.confirmUninstall && state.operationPluginId && (_jsx(ConfirmDialog, { message: `Uninstall ${state.operationPluginId}?` })), state.confirmUpdateAll && (_jsx(ConfirmDialog, { message: `Update all ${state.plugins.filter((p) => p.isInstalled).length} plugins?` })), 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 || state.updateProgress) && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: state.updateProgress
1130
+ ? `Updating (${state.updateProgress.current}/${state.updateProgress.total}): ${state.updateProgress.pluginId}...`
1131
+ : state.message }) })), _jsx(KeyHints, { focusZone: state.focusZone, extraHints: match([state.focusZone, state.activeTab])
999
1132
  .with(['search', P._], () => undefined)
1000
1133
  .with(['tabbar', P._], () => undefined)
1001
1134
  .with(['list', 'enabled'], () => {
@@ -1003,6 +1136,7 @@ export default function App() {
1003
1136
  { key: '/', action: 'search' },
1004
1137
  { key: 'i', action: 'install' },
1005
1138
  { key: 'u', action: 'uninstall' },
1139
+ { key: 'U', action: 'update all' },
1006
1140
  ];
1007
1141
  const selectedPlugin = enabledPlugins[state.selectedIndex];
1008
1142
  if (selectedPlugin) {
@@ -1018,6 +1152,7 @@ export default function App() {
1018
1152
  { key: '/', action: 'search' },
1019
1153
  { key: 'i', action: 'install' },
1020
1154
  { key: 'u', action: 'uninstall' },
1155
+ { key: 'U', action: 'update all' },
1021
1156
  ];
1022
1157
  const selectedPlugin = installedPlugins[state.selectedIndex];
1023
1158
  if (selectedPlugin) {