@laststance/claude-plugin-dashboard 0.2.3 → 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 (58) hide show
  1. package/README.md +7 -1
  2. package/dist/app.d.ts +7 -1
  3. package/dist/app.js +544 -262
  4. package/dist/cli.js +60 -67
  5. package/dist/components/ComponentBadges.d.ts +0 -9
  6. package/dist/components/ComponentBadges.js +0 -33
  7. package/dist/components/ComponentDetail.d.ts +32 -0
  8. package/dist/components/ComponentDetail.js +106 -0
  9. package/dist/components/ComponentList.d.ts +87 -0
  10. package/dist/components/ComponentList.js +287 -0
  11. package/dist/components/HelpOverlay.js +1 -0
  12. package/dist/components/KeyHints.d.ts +1 -0
  13. package/dist/components/KeyHints.js +33 -29
  14. package/dist/components/MarketplaceActionMenu.d.ts +41 -0
  15. package/dist/components/MarketplaceActionMenu.js +68 -0
  16. package/dist/components/MarketplaceDetail.d.ts +10 -3
  17. package/dist/components/MarketplaceDetail.js +10 -4
  18. package/dist/components/PluginDetail.d.ts +19 -3
  19. package/dist/components/PluginDetail.js +56 -6
  20. package/dist/components/PluginList.js +19 -7
  21. package/dist/services/componentService.d.ts +10 -31
  22. package/dist/services/componentService.js +19 -174
  23. package/dist/services/components/hookService.d.ts +17 -0
  24. package/dist/services/components/hookService.js +45 -0
  25. package/dist/services/components/index.d.ts +41 -0
  26. package/dist/services/components/index.js +126 -0
  27. package/dist/services/components/markdownService.d.ts +39 -0
  28. package/dist/services/components/markdownService.js +147 -0
  29. package/dist/services/components/serverService.d.ts +28 -0
  30. package/dist/services/components/serverService.js +69 -0
  31. package/dist/services/components/skillService.d.ts +48 -0
  32. package/dist/services/components/skillService.js +164 -0
  33. package/dist/services/components/utils.d.ts +23 -0
  34. package/dist/services/components/utils.js +42 -0
  35. package/dist/services/marketplaceActionsService.d.ts +17 -0
  36. package/dist/services/marketplaceActionsService.js +18 -0
  37. package/dist/services/pluginActionsService.d.ts +31 -2
  38. package/dist/services/pluginActionsService.js +65 -6
  39. package/dist/services/pluginService.js +78 -2
  40. package/dist/store/index.d.ts +46 -0
  41. package/dist/store/index.js +47 -0
  42. package/dist/store/slices/marketplaceSlice.d.ts +344 -0
  43. package/dist/store/slices/marketplaceSlice.js +152 -0
  44. package/dist/store/slices/pluginSlice.d.ts +1544 -0
  45. package/dist/store/slices/pluginSlice.js +191 -0
  46. package/dist/store/slices/uiSlice.d.ts +147 -0
  47. package/dist/store/slices/uiSlice.js +126 -0
  48. package/dist/tabs/DiscoverTab.d.ts +8 -2
  49. package/dist/tabs/DiscoverTab.js +2 -2
  50. package/dist/tabs/EnabledTab.d.ts +8 -2
  51. package/dist/tabs/EnabledTab.js +3 -3
  52. package/dist/tabs/ErrorsTab.js +1 -1
  53. package/dist/tabs/InstalledTab.d.ts +8 -2
  54. package/dist/tabs/InstalledTab.js +3 -3
  55. package/dist/tabs/MarketplacesTab.d.ts +15 -2
  56. package/dist/tabs/MarketplacesTab.js +13 -4
  57. package/dist/types/index.d.ts +157 -5
  58. package/package.json +10 -3
package/dist/app.js CHANGED
@@ -3,8 +3,11 @@ 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';
10
+ import { match, P } from 'ts-pattern';
8
11
  import TabBar, { getNextTab } from './components/TabBar.js';
9
12
  import KeyHints from './components/KeyHints.js';
10
13
  import DiscoverTab from './tabs/DiscoverTab.js';
@@ -13,12 +16,21 @@ import InstalledTab from './tabs/InstalledTab.js';
13
16
  import MarketplacesTab from './tabs/MarketplacesTab.js';
14
17
  import ErrorsTab from './tabs/ErrorsTab.js';
15
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';
16
21
  import { togglePlugin } from './services/settingsService.js';
17
- import { installPlugin, uninstallPlugin, } from './services/pluginActionsService.js';
18
- import { addMarketplace, removeMarketplace, updateMarketplace, } from './services/marketplaceActionsService.js';
22
+ import { installPlugin, uninstallPlugin, updateAllPlugins, } from './services/pluginActionsService.js';
23
+ import { addMarketplace, removeMarketplace, updateMarketplace, toggleAutoUpdate, } from './services/marketplaceActionsService.js';
19
24
  import AddMarketplaceDialog from './components/AddMarketplaceDialog.js';
20
25
  import ConfirmDialog from './components/ConfirmDialog.js';
21
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';
22
34
  import packageJson from '../package.json' with { type: 'json' };
23
35
  /**
24
36
  * Initial application state
@@ -39,12 +51,17 @@ export const initialState = {
39
51
  operation: 'idle',
40
52
  operationPluginId: null,
41
53
  confirmUninstall: false,
54
+ confirmUpdateAll: false,
55
+ updateProgress: null,
42
56
  showHelp: false,
43
57
  marketplaceOperation: 'idle',
44
58
  operationMarketplaceId: null,
45
59
  confirmRemoveMarketplace: false,
46
60
  showAddMarketplaceDialog: false,
47
61
  addMarketplaceError: null,
62
+ showMarketplaceActionMenu: false,
63
+ actionMenuSelectedIndex: 0,
64
+ selectedComponentIndex: 0,
48
65
  };
49
66
  /**
50
67
  * Get available focus zones for the current tab
@@ -65,16 +82,12 @@ export function getAvailableZones(activeTab) {
65
82
  * @returns Display message for the operation
66
83
  */
67
84
  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
- }
85
+ return match(operation)
86
+ .with('adding', () => 'Adding marketplace...')
87
+ .with('removing', () => `Removing ${marketplaceId}...`)
88
+ .with('updating', () => `Updating ${marketplaceId || 'marketplaces'}...`)
89
+ .with('idle', () => '')
90
+ .exhaustive();
78
91
  }
79
92
  /**
80
93
  * State reducer for application state management
@@ -89,6 +102,8 @@ export function appReducer(state, action) {
89
102
  selectedIndex: 0,
90
103
  searchQuery: '',
91
104
  message: null,
105
+ showMarketplaceActionMenu: false,
106
+ actionMenuSelectedIndex: 0,
92
107
  };
93
108
  case 'NEXT_TAB':
94
109
  return {
@@ -98,6 +113,8 @@ export function appReducer(state, action) {
98
113
  selectedIndex: 0,
99
114
  searchQuery: '',
100
115
  message: null,
116
+ showMarketplaceActionMenu: false,
117
+ actionMenuSelectedIndex: 0,
101
118
  };
102
119
  case 'PREV_TAB':
103
120
  return {
@@ -107,6 +124,8 @@ export function appReducer(state, action) {
107
124
  selectedIndex: 0,
108
125
  searchQuery: '',
109
126
  message: null,
127
+ showMarketplaceActionMenu: false,
128
+ actionMenuSelectedIndex: 0,
110
129
  };
111
130
  case 'SET_FOCUS_ZONE':
112
131
  return {
@@ -210,7 +229,9 @@ export function appReducer(state, action) {
210
229
  operationPluginId: action.payload.pluginId,
211
230
  message: action.payload.operation === 'installing'
212
231
  ? `Installing ${action.payload.pluginId}...`
213
- : `Uninstalling ${action.payload.pluginId}...`,
232
+ : action.payload.operation === 'uninstalling'
233
+ ? `Uninstalling ${action.payload.pluginId}...`
234
+ : `Updating plugins...`,
214
235
  };
215
236
  case 'END_OPERATION':
216
237
  return {
@@ -279,6 +300,59 @@ export function appReducer(state, action) {
279
300
  marketplaceOperation: 'idle',
280
301
  operationMarketplaceId: null,
281
302
  };
303
+ case 'SHOW_MARKETPLACE_ACTION_MENU':
304
+ return {
305
+ ...state,
306
+ showMarketplaceActionMenu: true,
307
+ actionMenuSelectedIndex: 0,
308
+ };
309
+ case 'HIDE_MARKETPLACE_ACTION_MENU':
310
+ return {
311
+ ...state,
312
+ showMarketplaceActionMenu: false,
313
+ actionMenuSelectedIndex: 0,
314
+ };
315
+ case 'SET_ACTION_MENU_INDEX':
316
+ return {
317
+ ...state,
318
+ actionMenuSelectedIndex: action.payload,
319
+ };
320
+ case 'MOVE_ACTION_MENU_SELECTION': {
321
+ const maxIndex = 3; // 4 actions: browse, update, autoUpdate, remove
322
+ const newIndex = action.payload === 'up'
323
+ ? Math.max(0, state.actionMenuSelectedIndex - 1)
324
+ : Math.min(maxIndex, state.actionMenuSelectedIndex + 1);
325
+ return {
326
+ ...state,
327
+ actionMenuSelectedIndex: newIndex,
328
+ };
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
+ };
282
356
  default:
283
357
  return state;
284
358
  }
@@ -292,28 +366,23 @@ export function appReducer(state, action) {
292
366
  * // => Only installed plugins matching 'su'
293
367
  */
294
368
  export function getItemsForTab(state) {
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
- }
308
- case 'discover':
309
- return getFilteredPlugins(state);
310
- case 'marketplaces':
311
- return state.marketplaces;
312
- case 'errors':
313
- return state.errors;
314
- default:
315
- return [];
316
- }
369
+ return match(state.activeTab)
370
+ .with('enabled', () => {
371
+ const enabledPlugins = state.plugins.filter((p) => p.isInstalled && p.isEnabled);
372
+ return state.searchQuery
373
+ ? searchPlugins(state.searchQuery, enabledPlugins)
374
+ : enabledPlugins;
375
+ })
376
+ .with('installed', () => {
377
+ const installedPlugins = state.plugins.filter((p) => p.isInstalled);
378
+ return state.searchQuery
379
+ ? searchPlugins(state.searchQuery, installedPlugins)
380
+ : installedPlugins;
381
+ })
382
+ .with('discover', () => getFilteredPlugins(state))
383
+ .with('marketplaces', () => state.marketplaces)
384
+ .with('errors', () => state.errors)
385
+ .otherwise(() => []);
317
386
  }
318
387
  /**
319
388
  * Get filtered and sorted plugins for discover tab
@@ -328,71 +397,128 @@ export function getFilteredPlugins(state) {
328
397
  plugins = sortPlugins(plugins, state.sortBy, state.sortOrder);
329
398
  return plugins;
330
399
  }
400
+ /**
401
+ * Get filtered marketplaces based on search query
402
+ * @param state - Current app state
403
+ * @returns Filtered array of marketplaces
404
+ */
405
+ export function getFilteredMarketplaces(state) {
406
+ let marketplaces = state.marketplaces;
407
+ // Apply search filter
408
+ if (state.searchQuery) {
409
+ marketplaces = searchMarketplaces(state.searchQuery, marketplaces);
410
+ }
411
+ return marketplaces;
412
+ }
331
413
  /**
332
414
  * Main App component
333
415
  */
334
416
  export default function App() {
335
417
  const { exit } = useApp();
336
- 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
+ };
337
452
  // Load data on mount
338
453
  useEffect(() => {
339
454
  try {
340
455
  const plugins = loadAllPlugins();
341
456
  const marketplaces = loadMarketplaces();
342
- dispatch({ type: 'SET_PLUGINS', payload: plugins });
343
- dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
457
+ dispatch(setPlugins(plugins));
458
+ dispatch(setMarketplaces(marketplaces));
344
459
  }
345
460
  catch (error) {
346
- dispatch({
347
- type: 'SET_ERROR',
348
- payload: error instanceof Error ? error.message : 'Failed to load data',
349
- });
461
+ dispatch(setError(error instanceof Error ? error.message : 'Failed to load data'));
350
462
  }
351
463
  }, []);
352
464
  /**
353
465
  * Handle plugin installation
354
466
  */
355
467
  async function handleInstall(pluginId) {
356
- dispatch({
357
- type: 'START_OPERATION',
358
- payload: { operation: 'installing', pluginId },
359
- });
468
+ dispatch(startOperation({ operation: 'installing', pluginId }));
360
469
  const result = await installPlugin(pluginId);
361
- dispatch({ type: 'END_OPERATION' });
470
+ dispatch(endOperation());
362
471
  if (result.success) {
363
472
  // Reload plugins to get fresh state
364
473
  const plugins = loadAllPlugins();
365
- dispatch({ type: 'SET_PLUGINS', payload: plugins });
366
- dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
474
+ dispatch(setPlugins(plugins));
475
+ dispatch(setMessage(`✅ ${result.message}`));
367
476
  }
368
477
  else {
369
- dispatch({
370
- type: 'SET_MESSAGE',
371
- payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
372
- });
478
+ dispatch(setMessage(`❌ ${result.message}${result.error ? `: ${result.error}` : ''}`));
373
479
  }
374
480
  }
375
481
  /**
376
482
  * Handle plugin uninstallation
377
483
  */
378
484
  async function handleUninstall(pluginId) {
379
- dispatch({
380
- type: 'START_OPERATION',
381
- payload: { operation: 'uninstalling', pluginId },
382
- });
485
+ dispatch(startOperation({ operation: 'uninstalling', pluginId }));
383
486
  const result = await uninstallPlugin(pluginId);
384
- dispatch({ type: 'END_OPERATION' });
487
+ dispatch(endOperation());
385
488
  if (result.success) {
386
489
  // Reload plugins to get fresh state
387
490
  const plugins = loadAllPlugins();
388
- dispatch({ type: 'SET_PLUGINS', payload: plugins });
389
- dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
491
+ dispatch(setPlugins(plugins));
492
+ dispatch(setMessage(`✅ ${result.message}`));
390
493
  }
391
494
  else {
392
- dispatch({
393
- type: 'SET_MESSAGE',
394
- payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
395
- });
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)`));
396
522
  }
397
523
  }
398
524
  /**
@@ -400,30 +526,24 @@ export default function App() {
400
526
  * @param source - Marketplace source (e.g., "owner/repo", URL, or local path)
401
527
  */
402
528
  async function handleAddMarketplace(source) {
403
- dispatch({
404
- type: 'START_MARKETPLACE_OPERATION',
405
- payload: { operation: 'adding' },
406
- });
529
+ dispatch(startMarketplaceOperation({ operation: 'adding' }));
407
530
  const result = await addMarketplace(source);
408
- dispatch({ type: 'END_MARKETPLACE_OPERATION' });
531
+ dispatch(endMarketplaceOperation());
409
532
  if (result.success) {
410
- dispatch({ type: 'HIDE_ADD_MARKETPLACE_DIALOG' });
533
+ dispatch(hideAddMarketplaceDialog());
411
534
  // Reload marketplaces to get fresh state
412
535
  const marketplaces = loadMarketplaces();
413
- dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
536
+ dispatch(setMarketplaces(marketplaces));
414
537
  // Also reload plugins as new marketplace may have plugins
415
538
  const plugins = loadAllPlugins();
416
- dispatch({ type: 'SET_PLUGINS', payload: plugins });
539
+ dispatch(setPlugins(plugins));
417
540
  // 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}` });
541
+ dispatch(setSelectedIndex(0));
542
+ dispatch(setMessage(`✅ ${result.message}`));
420
543
  }
421
544
  else {
422
545
  // Keep dialog open and show error inline
423
- dispatch({
424
- type: 'SET_ADD_MARKETPLACE_ERROR',
425
- payload: result.error || result.message,
426
- });
546
+ dispatch(setAddMarketplaceError(result.error || result.message));
427
547
  }
428
548
  }
429
549
  /**
@@ -431,33 +551,24 @@ export default function App() {
431
551
  * @param marketplaceId - Marketplace identifier to remove
432
552
  */
433
553
  async function handleRemoveMarketplace(marketplaceId) {
434
- dispatch({
435
- type: 'START_MARKETPLACE_OPERATION',
436
- payload: { operation: 'removing', marketplaceId },
437
- });
554
+ dispatch(startMarketplaceOperation({ operation: 'removing', marketplaceId }));
438
555
  const result = await removeMarketplace(marketplaceId);
439
- dispatch({ type: 'END_MARKETPLACE_OPERATION' });
556
+ dispatch(endMarketplaceOperation());
440
557
  if (result.success) {
441
558
  // Reload marketplaces to get fresh state
442
559
  const marketplaces = loadMarketplaces();
443
- dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
560
+ dispatch(setMarketplaces(marketplaces));
444
561
  // Also reload plugins as removed marketplace's plugins should be gone
445
562
  const plugins = loadAllPlugins();
446
- dispatch({ type: 'SET_PLUGINS', payload: plugins });
447
- dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
563
+ dispatch(setPlugins(plugins));
564
+ dispatch(setMessage(`✅ ${result.message}`));
448
565
  // Reset selection if needed
449
566
  if (state.selectedIndex >= marketplaces.length) {
450
- dispatch({
451
- type: 'SET_SELECTED_INDEX',
452
- payload: Math.max(0, marketplaces.length - 1),
453
- });
567
+ dispatch(setSelectedIndex(Math.max(0, marketplaces.length - 1)));
454
568
  }
455
569
  }
456
570
  else {
457
- dispatch({
458
- type: 'SET_MESSAGE',
459
- payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
460
- });
571
+ dispatch(setMessage(`❌ ${result.message}${result.error ? `: ${result.error}` : ''}`));
461
572
  }
462
573
  }
463
574
  /**
@@ -465,29 +576,54 @@ export default function App() {
465
576
  * @param marketplaceId - Optional marketplace identifier. If omitted, updates all.
466
577
  */
467
578
  async function handleUpdateMarketplace(marketplaceId) {
468
- dispatch({
469
- type: 'START_MARKETPLACE_OPERATION',
470
- payload: { operation: 'updating', marketplaceId },
471
- });
579
+ dispatch(startMarketplaceOperation({ operation: 'updating', marketplaceId }));
472
580
  const result = await updateMarketplace(marketplaceId);
473
- dispatch({ type: 'END_MARKETPLACE_OPERATION' });
581
+ dispatch(endMarketplaceOperation());
474
582
  if (result.success) {
475
583
  // Reload marketplaces and plugins to get fresh state
476
584
  const marketplaces = loadMarketplaces();
477
- dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
585
+ dispatch(setMarketplaces(marketplaces));
478
586
  const plugins = loadAllPlugins();
479
- dispatch({ type: 'SET_PLUGINS', payload: plugins });
587
+ dispatch(setPlugins(plugins));
480
588
  // 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}` });
589
+ dispatch(setSelectedIndex(0));
590
+ dispatch(setMessage(`✅ ${result.message}`));
483
591
  }
484
592
  else {
485
- dispatch({
486
- type: 'SET_MESSAGE',
487
- payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
488
- });
593
+ dispatch(setMessage(`❌ ${result.message}${result.error ? `: ${result.error}` : ''}`));
489
594
  }
490
595
  }
596
+ /**
597
+ * Handle toggling auto-update for a marketplace
598
+ * @param marketplaceId - ID of the marketplace
599
+ * @param currentValue - Current auto-update state
600
+ */
601
+ async function handleToggleAutoUpdate(marketplaceId, currentValue) {
602
+ dispatch(startMarketplaceOperation({ operation: 'updating', marketplaceId }));
603
+ const result = await toggleAutoUpdate(marketplaceId, currentValue);
604
+ dispatch(endMarketplaceOperation());
605
+ if (result.success) {
606
+ // Reload marketplaces to get fresh state
607
+ const marketplaces = loadMarketplaces();
608
+ dispatch(setMarketplaces(marketplaces));
609
+ dispatch(setMessage(`✅ ${result.message}`));
610
+ }
611
+ else {
612
+ dispatch(setMessage(`❌ ${result.message}${result.error ? `: ${result.error}` : ''}`));
613
+ }
614
+ }
615
+ /**
616
+ * Handle browsing plugins for a specific marketplace
617
+ * Switches to Discover tab with marketplace filter applied
618
+ * @param marketplaceId - ID of the marketplace to browse
619
+ */
620
+ function handleBrowseMarketplacePlugins(marketplaceId) {
621
+ // Switch to Discover tab
622
+ dispatch(setTab('discover'));
623
+ // Set search query to filter by marketplace
624
+ dispatch(setSearchQuery(marketplaceId));
625
+ dispatch(setMessage(`Browsing plugins from ${marketplaceId}`));
626
+ }
491
627
  // Keyboard input handling
492
628
  useInput((input, key) => {
493
629
  // Block all input during operations (plugin or marketplace)
@@ -497,7 +633,7 @@ export default function App() {
497
633
  // Handle help overlay
498
634
  if (state.showHelp) {
499
635
  if (input === 'h' || key.escape) {
500
- dispatch({ type: 'TOGGLE_HELP' });
636
+ dispatch(toggleHelp());
501
637
  }
502
638
  return;
503
639
  }
@@ -508,19 +644,33 @@ export default function App() {
508
644
  }
509
645
  // Toggle help (h key)
510
646
  if (input === 'h') {
511
- dispatch({ type: 'TOGGLE_HELP' });
647
+ dispatch(toggleHelp());
512
648
  return;
513
649
  }
514
650
  // Handle plugin uninstall confirmation dialog
515
651
  if (state.confirmUninstall && state.operationPluginId) {
516
652
  if (input === 'y' || input === 'Y' || key.return) {
517
- dispatch({ type: 'HIDE_CONFIRM_UNINSTALL' });
653
+ dispatch(hideConfirmUninstall());
518
654
  handleUninstall(state.operationPluginId);
519
655
  return;
520
656
  }
521
657
  if (input === 'n' || input === 'N' || key.escape) {
522
- dispatch({ type: 'HIDE_CONFIRM_UNINSTALL' });
523
- 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'));
524
674
  return;
525
675
  }
526
676
  return;
@@ -528,13 +678,13 @@ export default function App() {
528
678
  // Handle marketplace remove confirmation dialog
529
679
  if (state.confirmRemoveMarketplace && state.operationMarketplaceId) {
530
680
  if (input === 'y' || input === 'Y' || key.return) {
531
- dispatch({ type: 'HIDE_CONFIRM_REMOVE_MARKETPLACE' });
681
+ dispatch(hideConfirmRemoveMarketplace());
532
682
  handleRemoveMarketplace(state.operationMarketplaceId);
533
683
  return;
534
684
  }
535
685
  if (input === 'n' || input === 'N' || key.escape) {
536
- dispatch({ type: 'HIDE_CONFIRM_REMOVE_MARKETPLACE' });
537
- dispatch({ type: 'SET_MESSAGE', payload: 'Remove cancelled' });
686
+ dispatch(hideConfirmRemoveMarketplace());
687
+ dispatch(setMessage('Remove cancelled'));
538
688
  return;
539
689
  }
540
690
  return;
@@ -548,24 +698,63 @@ export default function App() {
548
698
  }
549
699
  // Cancel on Escape
550
700
  if (key.escape) {
551
- dispatch({ type: 'HIDE_ADD_MARKETPLACE_DIALOG' });
552
- dispatch({ type: 'SET_MESSAGE', payload: 'Add marketplace cancelled' });
701
+ dispatch(hideAddMarketplaceDialog());
702
+ dispatch(setMessage('Add marketplace cancelled'));
553
703
  return;
554
704
  }
555
705
  // Backspace
556
706
  if (key.backspace || key.delete) {
557
- dispatch({
558
- type: 'SET_SEARCH_QUERY',
559
- payload: state.searchQuery.slice(0, -1),
560
- });
707
+ dispatch(setSearchQuery(state.searchQuery.slice(0, -1)));
561
708
  return;
562
709
  }
563
710
  // Character input
564
711
  if (input && input.length === 1 && !key.ctrl && !key.meta) {
565
- dispatch({
566
- type: 'SET_SEARCH_QUERY',
567
- payload: state.searchQuery + input,
568
- });
712
+ dispatch(setSearchQuery(state.searchQuery + input));
713
+ return;
714
+ }
715
+ return;
716
+ }
717
+ // Handle marketplace action menu
718
+ if (state.showMarketplaceActionMenu) {
719
+ const selectedMarketplace = getFilteredMarketplaces(state)[state.selectedIndex];
720
+ if (!selectedMarketplace) {
721
+ dispatch(hideMarketplaceActionMenu());
722
+ return;
723
+ }
724
+ // Up/Down arrow navigation
725
+ if (key.upArrow || (key.ctrl && input === 'p')) {
726
+ dispatch(moveActionMenuSelection('up'));
727
+ return;
728
+ }
729
+ if (key.downArrow || (key.ctrl && input === 'n')) {
730
+ dispatch(moveActionMenuSelection('down'));
731
+ return;
732
+ }
733
+ // Execute action on Enter
734
+ if (key.return) {
735
+ dispatch(hideMarketplaceActionMenu());
736
+ const actionIndex = state.actionMenuSelectedIndex;
737
+ if (actionIndex === 0) {
738
+ // Browse plugins
739
+ handleBrowseMarketplacePlugins(selectedMarketplace.id);
740
+ }
741
+ else if (actionIndex === 1) {
742
+ // Update marketplace
743
+ handleUpdateMarketplace(selectedMarketplace.id);
744
+ }
745
+ else if (actionIndex === 2) {
746
+ // Toggle auto-update
747
+ handleToggleAutoUpdate(selectedMarketplace.id, selectedMarketplace.autoUpdate ?? false);
748
+ }
749
+ else if (actionIndex === 3) {
750
+ // Remove marketplace (show confirmation)
751
+ dispatch(showConfirmRemoveMarketplace(selectedMarketplace.id));
752
+ }
753
+ return;
754
+ }
755
+ // Close menu on Escape
756
+ if (key.escape) {
757
+ dispatch(hideMarketplaceActionMenu());
569
758
  return;
570
759
  }
571
760
  return;
@@ -574,7 +763,7 @@ export default function App() {
574
763
  if (state.focusZone === 'search') {
575
764
  // Up arrow: move focus to tabbar
576
765
  if (key.upArrow || (key.ctrl && input === 'p')) {
577
- dispatch({ type: 'SET_FOCUS_ZONE', payload: 'tabbar' });
766
+ dispatch(setFocusZone('tabbar'));
578
767
  return;
579
768
  }
580
769
  // Down arrow, Enter, or Escape: move focus to list
@@ -582,21 +771,15 @@ export default function App() {
582
771
  key.return ||
583
772
  key.downArrow ||
584
773
  (key.ctrl && input === 'n')) {
585
- dispatch({ type: 'SET_FOCUS_ZONE', payload: 'list' });
774
+ dispatch(setFocusZone('list'));
586
775
  return;
587
776
  }
588
777
  if (key.backspace || key.delete) {
589
- dispatch({
590
- type: 'SET_SEARCH_QUERY',
591
- payload: state.searchQuery.slice(0, -1),
592
- });
778
+ dispatch(setSearchQuery(state.searchQuery.slice(0, -1)));
593
779
  return;
594
780
  }
595
781
  if (input && input.length === 1 && !key.ctrl && !key.meta) {
596
- dispatch({
597
- type: 'SET_SEARCH_QUERY',
598
- payload: state.searchQuery + input,
599
- });
782
+ dispatch(setSearchQuery(state.searchQuery + input));
600
783
  return;
601
784
  }
602
785
  return;
@@ -606,27 +789,54 @@ export default function App() {
606
789
  // Down arrow: move to search (or list if no search)
607
790
  if (key.downArrow || (key.ctrl && input === 'n')) {
608
791
  const zones = getAvailableZones(state.activeTab);
609
- dispatch({
610
- type: 'SET_FOCUS_ZONE',
611
- payload: zones.includes('search') ? 'search' : 'list',
612
- });
792
+ dispatch(setFocusZone(zones.includes('search') ? 'search' : 'list'));
613
793
  return;
614
794
  }
615
795
  // Left/Right arrows and Ctrl+B/F: tab switching (only in tabbar)
616
796
  // Keep focus on tabbar after navigation
617
797
  if (key.leftArrow || (key.ctrl && input === 'b')) {
618
- dispatch({ type: 'PREV_TAB' });
619
- dispatch({ type: 'SET_FOCUS_ZONE', payload: 'tabbar' });
798
+ dispatch(prevTab());
799
+ dispatch(setFocusZone('tabbar'));
620
800
  return;
621
801
  }
622
802
  if (key.rightArrow || (key.ctrl && input === 'f')) {
623
- dispatch({ type: 'NEXT_TAB' });
624
- dispatch({ type: 'SET_FOCUS_ZONE', payload: 'tabbar' });
803
+ dispatch(nextTab());
804
+ dispatch(setFocusZone('tabbar'));
625
805
  return;
626
806
  }
627
807
  // Tab key: next tab (resets focus to list)
628
808
  if (key.tab) {
629
- 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
+ }));
630
840
  return;
631
841
  }
632
842
  return;
@@ -637,24 +847,43 @@ export default function App() {
637
847
  if (state.selectedIndex === 0) {
638
848
  // At top of list: move focus to search (or tabbar if no search)
639
849
  const zones = getAvailableZones(state.activeTab);
640
- dispatch({
641
- type: 'SET_FOCUS_ZONE',
642
- payload: zones.includes('search') ? 'search' : 'tabbar',
643
- });
850
+ dispatch(setFocusZone(zones.includes('search') ? 'search' : 'tabbar'));
644
851
  }
645
852
  else {
646
- dispatch({ type: 'MOVE_SELECTION', payload: 'up' });
853
+ dispatch(moveSelection({
854
+ direction: 'up',
855
+ maxIndex: Math.max(0, getItemsForTab(state).length - 1),
856
+ }));
647
857
  }
648
858
  return;
649
859
  }
650
860
  // Down arrow: move down in list
651
861
  if (key.downArrow || (key.ctrl && input === 'n')) {
652
- dispatch({ type: 'MOVE_SELECTION', payload: 'down' });
862
+ dispatch(moveSelection({
863
+ direction: 'down',
864
+ maxIndex: Math.max(0, getItemsForTab(state).length - 1),
865
+ }));
653
866
  return;
654
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
+ }
655
884
  // Tab key: next tab (from list zone)
656
885
  if (key.tab) {
657
- dispatch({ type: 'NEXT_TAB' });
886
+ dispatch(nextTab());
658
887
  return;
659
888
  }
660
889
  // Enter search mode (/ key on supported tabs)
@@ -665,7 +894,7 @@ export default function App() {
665
894
  'marketplaces',
666
895
  ];
667
896
  if (input === '/' && searchEnabledTabs.includes(state.activeTab)) {
668
- dispatch({ type: 'SET_FOCUS_ZONE', payload: 'search' });
897
+ dispatch(setFocusZone('search'));
669
898
  return;
670
899
  }
671
900
  // Enter key: Install (non-installed) or Toggle (installed)
@@ -684,22 +913,13 @@ export default function App() {
684
913
  // Toggle installed plugin
685
914
  try {
686
915
  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
- });
916
+ dispatch(togglePluginEnabled(selectedPlugin.id));
917
+ dispatch(setMessage(newState
918
+ ? `✅ ${selectedPlugin.name} enabled`
919
+ : `❌ ${selectedPlugin.name} disabled`));
697
920
  }
698
921
  catch (error) {
699
- dispatch({
700
- type: 'SET_MESSAGE',
701
- payload: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
702
- });
922
+ dispatch(setMessage(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
703
923
  }
704
924
  }
705
925
  }
@@ -715,22 +935,13 @@ export default function App() {
715
935
  if (selectedPlugin && selectedPlugin.isInstalled) {
716
936
  try {
717
937
  const newState = togglePlugin(selectedPlugin.id);
718
- dispatch({
719
- type: 'TOGGLE_PLUGIN_ENABLED',
720
- payload: selectedPlugin.id,
721
- });
722
- dispatch({
723
- type: 'SET_MESSAGE',
724
- payload: newState
725
- ? `✅ ${selectedPlugin.name} enabled`
726
- : `❌ ${selectedPlugin.name} disabled`,
727
- });
938
+ dispatch(togglePluginEnabled(selectedPlugin.id));
939
+ dispatch(setMessage(newState
940
+ ? `✅ ${selectedPlugin.name} enabled`
941
+ : `❌ ${selectedPlugin.name} disabled`));
728
942
  }
729
943
  catch (error) {
730
- dispatch({
731
- type: 'SET_MESSAGE',
732
- payload: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
733
- });
944
+ dispatch(setMessage(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
734
945
  }
735
946
  }
736
947
  return;
@@ -742,43 +953,42 @@ export default function App() {
742
953
  name: 'date',
743
954
  date: 'installs',
744
955
  };
745
- dispatch({
746
- type: 'SET_SORT',
747
- payload: { by: nextSort[state.sortBy], order: state.sortOrder },
748
- });
956
+ dispatch(setSort({ by: nextSort[state.sortBy], order: state.sortOrder }));
749
957
  return;
750
958
  }
751
959
  // Toggle sort order (S key)
752
960
  if (input === 'S' && state.activeTab === 'discover') {
753
- dispatch({
754
- type: 'SET_SORT',
755
- payload: {
756
- by: state.sortBy,
757
- order: state.sortOrder === 'asc' ? 'desc' : 'asc',
758
- },
759
- });
961
+ dispatch(setSort({
962
+ by: state.sortBy,
963
+ order: state.sortOrder === 'asc' ? 'desc' : 'asc',
964
+ }));
760
965
  return;
761
966
  }
762
967
  // Clear search (Escape)
763
968
  if (key.escape && state.searchQuery) {
764
- dispatch({ type: 'SET_SEARCH_QUERY', payload: '' });
969
+ dispatch(setSearchQuery(''));
765
970
  return;
766
971
  }
767
972
  // Marketplace-specific key bindings
768
973
  if (state.activeTab === 'marketplaces' && state.focusZone === 'list') {
974
+ // Open action menu (Enter or m key)
975
+ if (key.return || input === 'm') {
976
+ const selectedMarketplace = filteredMarketplaces[state.selectedIndex];
977
+ if (selectedMarketplace) {
978
+ dispatch(showMarketplaceActionMenuAction());
979
+ }
980
+ return;
981
+ }
769
982
  // Add marketplace (a key)
770
983
  if (input === 'a') {
771
- dispatch({ type: 'SHOW_ADD_MARKETPLACE_DIALOG' });
984
+ dispatch(showAddMarketplaceDialog());
772
985
  return;
773
986
  }
774
987
  // Remove marketplace (d key or Backspace)
775
988
  if (input === 'd' || key.backspace || key.delete) {
776
989
  const selectedMarketplace = filteredMarketplaces[state.selectedIndex];
777
990
  if (selectedMarketplace) {
778
- dispatch({
779
- type: 'SHOW_CONFIRM_REMOVE_MARKETPLACE',
780
- payload: selectedMarketplace.id,
781
- });
991
+ dispatch(showConfirmRemoveMarketplace(selectedMarketplace.id));
782
992
  }
783
993
  return;
784
994
  }
@@ -806,10 +1016,19 @@ export default function App() {
806
1016
  handleInstall(selectedPlugin.id);
807
1017
  }
808
1018
  else if (selectedPlugin?.isInstalled) {
809
- dispatch({
810
- type: 'SET_MESSAGE',
811
- payload: '⚠️ Plugin is already installed',
812
- });
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());
813
1032
  }
814
1033
  return;
815
1034
  }
@@ -821,23 +1040,15 @@ export default function App() {
821
1040
  const items = getItemsForTab(state);
822
1041
  const selectedPlugin = items[state.selectedIndex];
823
1042
  if (selectedPlugin && selectedPlugin.isInstalled) {
824
- dispatch({ type: 'SHOW_CONFIRM_UNINSTALL', payload: selectedPlugin.id });
1043
+ dispatch(showConfirmUninstall(selectedPlugin.id));
825
1044
  }
826
1045
  else if (selectedPlugin && !selectedPlugin.isInstalled) {
827
- dispatch({ type: 'SET_MESSAGE', payload: '⚠️ Plugin is not installed' });
1046
+ dispatch(setMessage('⚠️ Plugin is not installed'));
828
1047
  }
829
1048
  return;
830
1049
  }
831
1050
  });
832
- // Loading state
833
- if (state.loading) {
834
- return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(Text, { children: "Loading plugins..." }) }));
835
- }
836
- // Error state
837
- if (state.error) {
838
- 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" })] }));
839
- }
840
- // Get filtered data for current tab
1051
+ // Get filtered data for current tab (must be before early returns for useMemo dependency)
841
1052
  const filteredPlugins = getFilteredPlugins(state);
842
1053
  // Apply search filter to enabled plugins
843
1054
  const enabledPluginsBase = state.plugins.filter((p) => p.isInstalled && p.isEnabled);
@@ -853,56 +1064,127 @@ export default function App() {
853
1064
  const filteredMarketplaces = state.searchQuery
854
1065
  ? searchMarketplaces(state.searchQuery, state.marketplaces)
855
1066
  : 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;
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
+ }
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)
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")))
1127
+ .with('marketplaces', () => (_jsx(MarketplacesTab, { marketplaces: filteredMarketplaces, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone, showActionMenu: state.showMarketplaceActionMenu, actionMenuSelectedIndex: state.actionMenuSelectedIndex }, "marketplaces")))
1128
+ .with('errors', () => (_jsx(ErrorsTab, { errors: state.errors, selectedIndex: state.selectedIndex }, "errors")))
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])
1132
+ .with(['search', P._], () => undefined)
1133
+ .with(['tabbar', P._], () => undefined)
1134
+ .with(['list', 'enabled'], () => {
1135
+ const hints = [
1136
+ { key: '/', action: 'search' },
1137
+ { key: 'i', action: 'install' },
1138
+ { key: 'u', action: 'uninstall' },
1139
+ { key: 'U', action: 'update all' },
1140
+ ];
1141
+ const selectedPlugin = enabledPlugins[state.selectedIndex];
1142
+ if (selectedPlugin) {
1143
+ hints.push({
1144
+ key: 'Enter',
1145
+ action: selectedPlugin.isInstalled ? 'toggle' : 'install',
1146
+ });
864
1147
  }
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;
1148
+ return hints;
1149
+ })
1150
+ .with(['list', 'installed'], () => {
1151
+ const hints = [
1152
+ { key: '/', action: 'search' },
1153
+ { key: 'i', action: 'install' },
1154
+ { key: 'u', action: 'uninstall' },
1155
+ { key: 'U', action: 'update all' },
1156
+ ];
1157
+ const selectedPlugin = installedPlugins[state.selectedIndex];
1158
+ if (selectedPlugin) {
1159
+ hints.push({
1160
+ key: 'Enter',
1161
+ action: selectedPlugin.isInstalled ? 'toggle' : 'install',
1162
+ });
896
1163
  }
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
- ];
1164
+ return hints;
1165
+ })
1166
+ .with(['list', 'discover'], () => {
1167
+ const hints = [
1168
+ { key: '/', action: 'search' },
1169
+ { key: 'i', action: 'install' },
1170
+ { key: 'u', action: 'uninstall' },
1171
+ ];
1172
+ const selectedPlugin = filteredPlugins[state.selectedIndex];
1173
+ if (selectedPlugin) {
1174
+ hints.push({
1175
+ key: 'Enter',
1176
+ action: selectedPlugin.isInstalled ? 'toggle' : 'install',
1177
+ });
905
1178
  }
906
- return undefined;
907
- })() })] }));
1179
+ hints.push({ key: 's', action: 'sort' });
1180
+ return hints;
1181
+ })
1182
+ .with(['list', 'marketplaces'], () => [
1183
+ { key: '/', action: 'search' },
1184
+ { key: 'a', action: 'add' },
1185
+ { key: 'd', action: 'remove' },
1186
+ { key: 'u', action: 'update' },
1187
+ ])
1188
+ .with(['list', 'errors'], () => undefined)
1189
+ .otherwise(() => undefined) })] }));
908
1190
  }