gloomberb 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/app.tsx +184 -69
  4. package/src/cli/commands/ticker.ts +45 -15
  5. package/src/cli/helpers.ts +4 -2
  6. package/src/cli/registry.test.ts +42 -35
  7. package/src/cli.test.ts +4 -2
  8. package/src/components/chart/chart-control-hint.tsx +39 -0
  9. package/src/components/chart/chart-controller.test.ts +110 -0
  10. package/src/components/chart/chart-controller.ts +364 -2
  11. package/src/components/chart/chart-data.ts +177 -29
  12. package/src/components/chart/chart-indicator-selector.tsx +53 -0
  13. package/src/components/chart/chart-pane-settings.ts +20 -0
  14. package/src/components/chart/chart-renderer.ts +220 -30
  15. package/src/components/chart/chart-resolution.test.ts +67 -0
  16. package/src/components/chart/chart-resolution.ts +74 -4
  17. package/src/components/chart/chart-scroll.test.ts +69 -0
  18. package/src/components/chart/chart-scroll.ts +104 -0
  19. package/src/components/chart/chart-types.ts +12 -0
  20. package/src/components/chart/chart-viewport.test.ts +5 -1
  21. package/src/components/chart/chart-viewport.ts +1 -1
  22. package/src/components/chart/chart.test.ts +249 -10
  23. package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
  24. package/src/components/chart/comparison-chart-renderer.ts +40 -3
  25. package/src/components/chart/comparison-stock-chart.tsx +218 -123
  26. package/src/components/chart/indicators/bands.test.ts +50 -0
  27. package/src/components/chart/indicators/bands.ts +29 -0
  28. package/src/components/chart/indicators/moving-averages.test.ts +54 -0
  29. package/src/components/chart/indicators/moving-averages.ts +42 -0
  30. package/src/components/chart/indicators/options.test.ts +32 -0
  31. package/src/components/chart/indicators/options.ts +107 -0
  32. package/src/components/chart/indicators/oscillators.test.ts +79 -0
  33. package/src/components/chart/indicators/oscillators.ts +92 -0
  34. package/src/components/chart/indicators/types.ts +36 -0
  35. package/src/components/chart/native/chart-rasterizer.ts +51 -0
  36. package/src/components/chart/native/kitty.test.ts +68 -0
  37. package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
  38. package/src/components/chart/stock-chart.test.ts +239 -0
  39. package/src/components/chart/stock-chart.tsx +2257 -372
  40. package/src/components/command-bar/command-bar.test.tsx +337 -5
  41. package/src/components/command-bar/command-bar.tsx +296 -120
  42. package/src/components/command-bar/helpers.ts +1 -1
  43. package/src/components/command-bar/root-shortcuts.ts +33 -4
  44. package/src/components/command-bar/view-model.test.ts +3 -3
  45. package/src/components/command-bar/view-model.ts +4 -4
  46. package/src/components/command-bar/workflow-ops.ts +10 -0
  47. package/src/components/layout/floating-pane.tsx +3 -1
  48. package/src/components/layout/header.tsx +5 -4
  49. package/src/components/layout/pane-sizing.ts +6 -0
  50. package/src/components/layout/pane.tsx +5 -1
  51. package/src/components/layout/shell.test.tsx +263 -32
  52. package/src/components/layout/shell.tsx +10 -6
  53. package/src/components/layout/status-bar.test.tsx +16 -7
  54. package/src/components/layout/status-bar.tsx +4 -1
  55. package/src/components/markdown-editor.tsx +1 -0
  56. package/src/components/pane-settings-dialog.tsx +13 -152
  57. package/src/components/pane-template-wizard.tsx +1 -0
  58. package/src/components/price-selector-dialog.tsx +12 -7
  59. package/src/components/ticker-badge-text.test.tsx +50 -0
  60. package/src/components/ticker-badge-text.tsx +17 -2
  61. package/src/components/ticker-badge.tsx +2 -2
  62. package/src/components/ticker-list-table.tsx +62 -2
  63. package/src/components/toggle-list.tsx +15 -2
  64. package/src/components/ui/data-table.tsx +82 -5
  65. package/src/components/ui/external-link.tsx +48 -11
  66. package/src/components/ui/fields.tsx +2 -1
  67. package/src/components/ui/index.ts +8 -1
  68. package/src/components/ui/multi-select-chips.tsx +69 -0
  69. package/src/components/ui/multi-select-dialog.tsx +267 -0
  70. package/src/components/ui/multi-select.ts +78 -0
  71. package/src/components/ui/ui.test.tsx +343 -1
  72. package/src/data/plugin-state-store.ts +30 -19
  73. package/src/data/resource-store.ts +133 -103
  74. package/src/data/session-store.ts +19 -12
  75. package/src/data/sqlite-cache.test.ts +37 -2
  76. package/src/data/sqlite-database.ts +8 -2
  77. package/src/data/sqlite-retry.test.ts +63 -0
  78. package/src/data/sqlite-retry.ts +66 -0
  79. package/src/data/ticker-store.ts +29 -18
  80. package/src/index.tsx +6 -1
  81. package/src/market-data/coordinator.test.ts +114 -5
  82. package/src/market-data/coordinator.ts +70 -1
  83. package/src/market-data/hooks.test.tsx +73 -2
  84. package/src/market-data/hooks.tsx +16 -4
  85. package/src/notifications/app-notifier.test.ts +177 -0
  86. package/src/notifications/app-notifier.ts +241 -0
  87. package/src/pane-settings.test.ts +2 -0
  88. package/src/plugins/builtin/ai/index.test.tsx +1 -1
  89. package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
  90. package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
  91. package/src/plugins/builtin/ai/ticker-context.ts +3 -2
  92. package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
  93. package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
  94. package/src/plugins/builtin/alerts/index.test.tsx +357 -0
  95. package/src/plugins/builtin/alerts/index.tsx +519 -0
  96. package/src/plugins/builtin/alerts/types.ts +14 -0
  97. package/src/plugins/builtin/analytics/index.test.tsx +300 -0
  98. package/src/plugins/builtin/analytics/index.tsx +775 -0
  99. package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
  100. package/src/plugins/builtin/analytics/metrics.ts +155 -0
  101. package/src/plugins/builtin/ask-ai.test.tsx +2 -0
  102. package/src/plugins/builtin/chat-controller.test.ts +92 -18
  103. package/src/plugins/builtin/chat-controller.ts +40 -11
  104. package/src/plugins/builtin/chat.test.tsx +715 -4
  105. package/src/plugins/builtin/chat.tsx +420 -119
  106. package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
  107. package/src/plugins/builtin/comparison-chart.tsx +1 -2
  108. package/src/plugins/builtin/correlation/compute.test.ts +147 -0
  109. package/src/plugins/builtin/correlation/compute.ts +115 -0
  110. package/src/plugins/builtin/correlation/index.tsx +385 -0
  111. package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
  112. package/src/plugins/builtin/correlation/settings.test.ts +34 -0
  113. package/src/plugins/builtin/correlation/settings.ts +95 -0
  114. package/src/plugins/builtin/debug.tsx +36 -50
  115. package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
  116. package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
  117. package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
  118. package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
  119. package/src/plugins/builtin/earnings/index.tsx +337 -0
  120. package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
  121. package/src/plugins/builtin/econ/calendar-source.ts +121 -0
  122. package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
  123. package/src/plugins/builtin/econ/fred-client.ts +79 -0
  124. package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
  125. package/src/plugins/builtin/econ/index.tsx +999 -0
  126. package/src/plugins/builtin/econ/types.ts +13 -0
  127. package/src/plugins/builtin/fred-settings.ts +10 -0
  128. package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
  129. package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
  130. package/src/plugins/builtin/help.test.tsx +1 -5
  131. package/src/plugins/builtin/help.tsx +1 -16
  132. package/src/plugins/builtin/insider/index.tsx +417 -0
  133. package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
  134. package/src/plugins/builtin/insider/insider-data.ts +49 -0
  135. package/src/plugins/builtin/layout-manager.tsx +33 -16
  136. package/src/plugins/builtin/market-movers/index.tsx +573 -0
  137. package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
  138. package/src/plugins/builtin/market-movers/screener.ts +105 -0
  139. package/src/plugins/builtin/options.tsx +4 -3
  140. package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
  141. package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
  142. package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
  143. package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
  144. package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
  145. package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
  146. package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
  147. package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
  148. package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
  149. package/src/plugins/builtin/sec.tsx +114 -7
  150. package/src/plugins/builtin/sectors/index.tsx +359 -0
  151. package/src/plugins/builtin/sectors/sector-data.ts +18 -0
  152. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
  153. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
  154. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
  155. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
  156. package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
  157. package/src/plugins/builtin/world-indices/index.tsx +410 -0
  158. package/src/plugins/builtin/world-indices/indices.ts +52 -0
  159. package/src/plugins/builtin/yield-curve/index.tsx +242 -0
  160. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
  161. package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
  162. package/src/plugins/catalog.ts +22 -0
  163. package/src/plugins/gridlock-notification.ts +15 -0
  164. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  165. package/src/plugins/ibkr/gateway-service.test.ts +22 -0
  166. package/src/plugins/ibkr/gateway-service.ts +6 -0
  167. package/src/plugins/ibkr/index.test.tsx +1 -1
  168. package/src/plugins/ibkr/index.tsx +8 -2
  169. package/src/plugins/ibkr/trade-tab.tsx +23 -5
  170. package/src/plugins/ibkr/trade-utils.test.ts +1 -1
  171. package/src/plugins/ibkr/trade-utils.ts +7 -6
  172. package/src/plugins/ibkr/trading-pane.tsx +8 -7
  173. package/src/plugins/plugin-runtime.test.tsx +2 -0
  174. package/src/plugins/plugin-runtime.tsx +10 -0
  175. package/src/plugins/prediction-markets/controller-data.ts +129 -14
  176. package/src/plugins/prediction-markets/controller.ts +1 -1
  177. package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
  178. package/src/plugins/prediction-markets/index.tsx +5 -1
  179. package/src/plugins/prediction-markets/pane.test.tsx +78 -0
  180. package/src/plugins/prediction-markets/pane.tsx +7 -3
  181. package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
  182. package/src/plugins/prediction-markets/services/fetch.ts +17 -7
  183. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  184. package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
  185. package/src/plugins/prediction-markets/test-helpers.tsx +22 -5
  186. package/src/plugins/registry.ts +45 -13
  187. package/src/sources/provider-router.test.ts +3 -2
  188. package/src/sources/sec-edgar.ts +13 -1
  189. package/src/sources/yahoo-finance.ts +75 -1
  190. package/src/state/app-context-selector.test.tsx +17 -1
  191. package/src/state/app-context.test.tsx +24 -0
  192. package/src/state/app-context.tsx +29 -6
  193. package/src/theme/colors.ts +5 -0
  194. package/src/theme/themes.ts +25 -0
  195. package/src/types/config.test.ts +107 -0
  196. package/src/types/config.ts +30 -1
  197. package/src/types/data-provider.ts +13 -0
  198. package/src/types/plugin.ts +35 -1
  199. package/src/updater.test.ts +2 -0
  200. package/src/utils/api-client.ts +12 -0
  201. package/src/utils/format.test.ts +11 -1
  202. package/src/utils/format.ts +86 -8
  203. package/src/utils/inline-content-tokenizer.test.ts +22 -0
  204. package/src/utils/inline-content-tokenizer.ts +76 -0
  205. package/src/utils/link-tokenizer.test.ts +27 -0
  206. package/src/utils/link-tokenizer.ts +93 -0
  207. package/src/utils/market-format.test.ts +109 -0
  208. package/src/utils/market-format.ts +299 -0
  209. package/src/utils/market-status.ts +3 -2
  210. package/src/utils/selection-clipboard.test.ts +43 -0
  211. package/src/utils/selection-clipboard.ts +88 -0
  212. package/src/utils/terminal-input-reset.test.ts +50 -0
  213. package/src/utils/terminal-input-reset.ts +24 -0
  214. package/src/utils/throttled-fetch.test.ts +145 -0
  215. package/src/utils/throttled-fetch.ts +212 -0
  216. package/src/utils/ticker-tokenizer.test.ts +31 -0
  217. package/src/utils/ticker-tokenizer.ts +7 -42
  218. package/src/version.ts +1 -1
@@ -35,6 +35,8 @@ import {
35
35
  cloneLayout,
36
36
  createPaneInstance,
37
37
  findPaneInstance,
38
+ findPrimaryPaneInstance,
39
+ resolveFollowBindingInstance,
38
40
  type LayoutConfig,
39
41
  } from "../../types/config";
40
42
  import { resolveBrokerConfigFields, type BrokerConfigField } from "../../types/broker";
@@ -52,6 +54,7 @@ import {
52
54
  removePane,
53
55
  swapPanes,
54
56
  } from "../../plugins/pane-manager";
57
+ import { notifyGridlockComplete } from "../../plugins/gridlock-notification";
55
58
  import { CHART_RENDERER_PREFERENCES } from "../chart/chart-types";
56
59
  import {
57
60
  buildSections,
@@ -85,6 +88,7 @@ import {
85
88
  applyCollectionMembershipChange,
86
89
  getCollectionTargetOptions,
87
90
  resolvePreferredCollectionTarget,
91
+ resolveSoleCollectionTarget,
88
92
  resolveTickerInput,
89
93
  resolveTickerInputOrThrow,
90
94
  resolveTickerListInput,
@@ -260,6 +264,7 @@ export function CommandBar({
260
264
  const rootThemeBaseIdRef = useRef<string | null>(null);
261
265
  const workflowInputRefs = useRef<Record<string, RefObject<InputRenderable | TextareaRenderable | null>>>({});
262
266
  const visibleListStateRef = useRef<ListScreenState | null>(null);
267
+ const processedLaunchSequenceRef = useRef<number | null>(null);
263
268
  const currentRoute = routeStack[routeStack.length - 1] ?? null;
264
269
  const currentRouteRef = useRef<CommandBarRoute | null>(currentRoute);
265
270
  currentRouteRef.current = currentRoute;
@@ -348,16 +353,12 @@ export function CommandBar({
348
353
 
349
354
  const setActiveCollection = useCallback((collectionId: string) => {
350
355
  const currentState = stateRef.current;
351
- const resolvePortfolioPane = (candidate?: string | null): string | null => {
352
- if (!candidate) return null;
353
- const instance = findPaneInstance(currentState.config.layout, candidate);
354
- if (!instance) return null;
355
- if (instance.paneId === "portfolio-list") return instance.instanceId;
356
- if (instance.binding?.kind === "follow") return resolvePortfolioPane(instance.binding.sourceInstanceId);
357
- return null;
358
- };
359
- const targetPaneId = resolvePortfolioPane(currentState.focusedPaneId)
360
- ?? currentState.config.layout.instances.find((instance) => instance.paneId === "portfolio-list")?.instanceId
356
+ const targetPaneId = resolveFollowBindingInstance(
357
+ currentState.config.layout,
358
+ currentState.focusedPaneId,
359
+ (instance) => instance.paneId === "portfolio-list",
360
+ )?.instanceId
361
+ ?? findPrimaryPaneInstance(currentState.config.layout, "portfolio-list")?.instanceId
361
362
  ?? null;
362
363
  if (!targetPaneId) return;
363
364
  dispatch({ type: "UPDATE_PANE_STATE", paneId: targetPaneId, patch: { collectionId } });
@@ -414,6 +415,12 @@ export function CommandBar({
414
415
  pluginRegistry.updateLayoutFn(nextLayout);
415
416
  }, [pluginRegistry]);
416
417
 
418
+ const notifyGridlockRevert = useCallback(() => {
419
+ notifyGridlockComplete(pluginRegistry.notify.bind(pluginRegistry), () => {
420
+ dispatch({ type: "UNDO_LAYOUT" });
421
+ });
422
+ }, [dispatch, pluginRegistry]);
423
+
417
424
  const duplicatePane = useCallback((paneId: string) => {
418
425
  const currentState = stateRef.current;
419
426
  const pane = findPaneInstance(currentState.config.layout, paneId);
@@ -606,6 +613,9 @@ export function CommandBar({
606
613
  next[selectorKey] = brokerId;
607
614
  return next;
608
615
  }, []);
616
+ const notify = useCallback((body: string, options?: { type?: "info" | "success" | "error" }) => {
617
+ pluginRegistry.notify({ body, ...options });
618
+ }, [pluginRegistry]);
609
619
 
610
620
  const openAddToPortfolioWorkflow = useCallback((
611
621
  ticker: TickerRecord,
@@ -618,7 +628,7 @@ export function CommandBar({
618
628
  defaultAvgCost,
619
629
  });
620
630
  if (!workflow) {
621
- pluginRegistry.showToastFn("Create a manual portfolio first.", { type: "info" });
631
+ notify("Create a manual portfolio first.", { type: "info" });
622
632
  return;
623
633
  }
624
634
 
@@ -638,7 +648,7 @@ export function CommandBar({
638
648
  successBehavior: "close",
639
649
  payload: { kind: "builtin", actionId: "add-portfolio" },
640
650
  });
641
- }, [openWorkflowRoute, pluginRegistry]);
651
+ }, [notify, openWorkflowRoute]);
642
652
 
643
653
  const connectBrokerProfile = useCallback(async (
644
654
  brokerId: string,
@@ -670,8 +680,8 @@ export function CommandBar({
670
680
  dispatch({ type: "SET_CONFIG", config: freshConfig });
671
681
  const brokerTab = freshConfig.portfolios.find((portfolio) => portfolio.brokerInstanceId === instance.id);
672
682
  if (brokerTab) setActiveCollection(brokerTab.id);
673
- pluginRegistry.showToastFn("Connected! Positions will sync automatically.", { type: "success" });
674
- }, [dispatch, pluginRegistry, setActiveCollection]);
683
+ notify("Connected! Positions will sync automatically.", { type: "success" });
684
+ }, [dispatch, notify, pluginRegistry, setActiveCollection]);
675
685
 
676
686
  const createManualPortfolio = useCallback(async (name: string) => {
677
687
  const currentState = stateRef.current;
@@ -683,8 +693,8 @@ export function CommandBar({
683
693
  dispatch({ type: "SET_CONFIG", config: nextConfig });
684
694
  setActiveCollection(portfolio.id);
685
695
  await saveConfig(nextConfig);
686
- pluginRegistry.showToastFn(`Created portfolio "${portfolio.name}".`, { type: "success" });
687
- }, [dispatch, pluginRegistry, setActiveCollection]);
696
+ notify(`Created portfolio "${portfolio.name}".`, { type: "success" });
697
+ }, [dispatch, notify, setActiveCollection]);
688
698
 
689
699
  const createWatchlist = useCallback(async (name: string) => {
690
700
  const currentState = stateRef.current;
@@ -702,8 +712,8 @@ export function CommandBar({
702
712
  dispatch({ type: "SET_CONFIG", config: nextConfig });
703
713
  setActiveCollection(id);
704
714
  await saveConfig(nextConfig);
705
- pluginRegistry.showToastFn(`Created watchlist "${trimmedName}".`, { type: "success" });
706
- }, [dispatch, pluginRegistry, setActiveCollection]);
715
+ notify(`Created watchlist "${trimmedName}".`, { type: "success" });
716
+ }, [dispatch, notify, setActiveCollection]);
707
717
 
708
718
  const deleteWatchlist = useCallback(async (watchlistId: string) => {
709
719
  const currentState = stateRef.current;
@@ -722,8 +732,8 @@ export function CommandBar({
722
732
  if (fallback) setActiveCollection(fallback);
723
733
  }
724
734
  await saveConfig(nextConfig);
725
- pluginRegistry.showToastFn(`Deleted "${watchlist.name}".`, { type: "success" });
726
- }, [activeCollectionId, dispatch, pluginRegistry, setActiveCollection]);
735
+ notify(`Deleted "${watchlist.name}".`, { type: "success" });
736
+ }, [activeCollectionId, dispatch, notify, setActiveCollection]);
727
737
 
728
738
  const deletePortfolio = useCallback(async (portfolioId: string) => {
729
739
  const currentState = stateRef.current;
@@ -752,8 +762,8 @@ export function CommandBar({
752
762
  if (fallback) setActiveCollection(fallback);
753
763
  }
754
764
  await saveConfig(nextConfig);
755
- pluginRegistry.showToastFn(`Deleted "${portfolio.name}".`, { type: "success" });
756
- }, [activeCollectionId, dispatch, pluginRegistry, setActiveCollection, tickerRepository]);
765
+ notify(`Deleted "${portfolio.name}".`, { type: "success" });
766
+ }, [activeCollectionId, dispatch, notify, setActiveCollection, tickerRepository]);
757
767
 
758
768
  const setPortfolioPositionFromWorkflow = useCallback(async (values: Record<string, CommandBarFieldValue>) => {
759
769
  const currentState = stateRef.current;
@@ -800,8 +810,8 @@ export function CommandBar({
800
810
  });
801
811
  await tickerRepository.saveTicker(result.ticker);
802
812
  dispatch({ type: "UPDATE_TICKER", ticker: result.ticker });
803
- pluginRegistry.showToastFn(`Set position for ${result.ticker.metadata.ticker} in "${portfolio.name}".`, { type: "success" });
804
- }, [activeCollectionId, activeTickerSymbol, dataProvider, dispatch, pluginRegistry, tickerRepository]);
813
+ notify(`Set position for ${result.ticker.metadata.ticker} in "${portfolio.name}".`, { type: "success" });
814
+ }, [activeCollectionId, activeTickerSymbol, dataProvider, dispatch, notify, pluginRegistry, tickerRepository]);
805
815
 
806
816
  const addTickerMembershipFromWorkflow = useCallback(async (values: Record<string, CommandBarFieldValue>) => {
807
817
  const currentState = stateRef.current;
@@ -828,12 +838,12 @@ export function CommandBar({
828
838
  if (result.changed) {
829
839
  await tickerRepository.saveTicker(result.ticker);
830
840
  dispatch({ type: "UPDATE_TICKER", ticker: result.ticker });
831
- pluginRegistry.showToastFn(`Added ${result.ticker.metadata.ticker} to "${portfolio.name}".`, { type: "success" });
841
+ notify(`Added ${result.ticker.metadata.ticker} to "${portfolio.name}".`, { type: "success" });
832
842
  return;
833
843
  }
834
844
 
835
- pluginRegistry.showToastFn(`${result.ticker.metadata.ticker} is already in "${portfolio.name}".`, { type: "info" });
836
- }, [activeCollectionId, activeTickerSymbol, dataProvider, dispatch, pluginRegistry, tickerRepository]);
845
+ notify(`${result.ticker.metadata.ticker} is already in "${portfolio.name}".`, { type: "info" });
846
+ }, [activeCollectionId, activeTickerSymbol, dataProvider, dispatch, notify, pluginRegistry, tickerRepository]);
837
847
 
838
848
  const disconnectBrokerInstance = useCallback(async (instanceId: string) => {
839
849
  const instance = stateRef.current.config.brokerInstances.find((entry) => entry.id === instanceId);
@@ -843,8 +853,8 @@ export function CommandBar({
843
853
  await pluginRegistry.removeBrokerInstanceFn(instanceId);
844
854
  const freshConfig = pluginRegistry.getConfigFn();
845
855
  dispatch({ type: "SET_CONFIG", config: freshConfig });
846
- pluginRegistry.showToastFn(`Removed ${instance.label}.`, { type: "success" });
847
- }, [dispatch, pluginRegistry]);
856
+ notify(`Removed ${instance.label}.`, { type: "success" });
857
+ }, [dispatch, notify, pluginRegistry]);
848
858
 
849
859
  const openBuiltInWorkflow = useCallback((actionId: string) => {
850
860
  switch (actionId) {
@@ -907,7 +917,7 @@ export function CommandBar({
907
917
  true,
908
918
  );
909
919
  if (!workflow) {
910
- pluginRegistry.showToastFn("No connectable brokers are installed.", { type: "info" });
920
+ notify("No connectable brokers are installed.", { type: "info" });
911
921
  return;
912
922
  }
913
923
  openWorkflowRoute(workflow);
@@ -919,7 +929,7 @@ export function CommandBar({
919
929
  activeTicker: activeTickerData,
920
930
  });
921
931
  if (!workflow) {
922
- pluginRegistry.showToastFn("Create a manual portfolio first.", { type: "info" });
932
+ notify("Create a manual portfolio first.", { type: "info" });
923
933
  return;
924
934
  }
925
935
  openWorkflowRoute({
@@ -949,7 +959,7 @@ export function CommandBar({
949
959
  false,
950
960
  );
951
961
  if (!workflow) {
952
- pluginRegistry.showToastFn("No connectable brokers are installed.", { type: "info" });
962
+ notify("No connectable brokers are installed.", { type: "info" });
953
963
  return;
954
964
  }
955
965
  openWorkflowRoute(workflow);
@@ -957,8 +967,8 @@ export function CommandBar({
957
967
  }
958
968
  default:
959
969
  return;
960
- }
961
- }, [activeCollectionId, activeTickerData, buildBrokerWorkflow, openWorkflowRoute, pluginRegistry, state.config]);
970
+ }
971
+ }, [activeCollectionId, activeTickerData, buildBrokerWorkflow, notify, openWorkflowRoute, pluginRegistry, state.config]);
962
972
 
963
973
  const openPickerRoute = useCallback((
964
974
  route: CommandBarRoute,
@@ -1031,7 +1041,7 @@ export function CommandBar({
1031
1041
  const openPaneSettingsRoute = useCallback((paneId: string) => {
1032
1042
  const descriptor = pluginRegistry.resolvePaneSettings(paneId);
1033
1043
  if (!descriptor) {
1034
- pluginRegistry.showToastFn("The focused pane has no settings.", { type: "info" });
1044
+ notify("The focused pane has no settings.", { type: "info" });
1035
1045
  return;
1036
1046
  }
1037
1047
  pushRoute({
@@ -1043,7 +1053,7 @@ export function CommandBar({
1043
1053
  error: null,
1044
1054
  pendingFieldKey: null,
1045
1055
  });
1046
- }, [pluginRegistry, pushRoute]);
1056
+ }, [notify, pluginRegistry, pushRoute]);
1047
1057
 
1048
1058
  const executeCollectionCommand = useCallback(async (
1049
1059
  commandId: "add-watchlist" | "add-portfolio" | "remove-watchlist" | "remove-portfolio",
@@ -1072,7 +1082,7 @@ export function CommandBar({
1072
1082
  if (kind === "portfolio" && action === "add") {
1073
1083
  const manualPortfolios = stateForCommand.config.portfolios.filter(isManualPortfolio);
1074
1084
  if (manualPortfolios.length === 0) {
1075
- pluginRegistry.showToastFn("Create a manual portfolio first.", { type: "info" });
1085
+ notify("Create a manual portfolio first.", { type: "info" });
1076
1086
  return;
1077
1087
  }
1078
1088
 
@@ -1119,12 +1129,27 @@ export function CommandBar({
1119
1129
  }
1120
1130
 
1121
1131
  const targetId = explicitTargetId
1122
- ?? resolvePreferredCollectionTarget(
1123
- stateForCommand,
1124
- kind,
1125
- activeCollectionId,
1126
- action,
1127
- resolvedTicker.ticker,
1132
+ ?? (
1133
+ kind === "watchlist" && action === "add"
1134
+ ? resolvePreferredCollectionTarget(
1135
+ stateForCommand,
1136
+ kind,
1137
+ activeCollectionId,
1138
+ action,
1139
+ resolvedTicker.ticker,
1140
+ ) ?? resolveSoleCollectionTarget(
1141
+ stateForCommand,
1142
+ kind,
1143
+ action,
1144
+ resolvedTicker.ticker,
1145
+ )
1146
+ : resolvePreferredCollectionTarget(
1147
+ stateForCommand,
1148
+ kind,
1149
+ activeCollectionId,
1150
+ action,
1151
+ resolvedTicker.ticker,
1152
+ )
1128
1153
  );
1129
1154
 
1130
1155
  if (!targetId) {
@@ -1136,7 +1161,7 @@ export function CommandBar({
1136
1161
  description: option.description,
1137
1162
  }));
1138
1163
  if (options.length === 0) {
1139
- pluginRegistry.showToastFn(
1164
+ notify(
1140
1165
  action === "add"
1141
1166
  ? `No ${kind}s are available for ${resolvedTicker.symbol}.`
1142
1167
  : `${resolvedTicker.symbol} is not in any ${kind}.`,
@@ -1174,7 +1199,7 @@ export function CommandBar({
1174
1199
  } else {
1175
1200
  const portfolio = stateForCommand.config.portfolios.find((entry) => entry.id === targetId);
1176
1201
  if (!portfolio || !isManualPortfolio(portfolio)) {
1177
- pluginRegistry.showToastFn("Choose a manual portfolio.", { type: "error" });
1202
+ notify("Choose a manual portfolio.", { type: "error" });
1178
1203
  return;
1179
1204
  }
1180
1205
 
@@ -1192,12 +1217,12 @@ export function CommandBar({
1192
1217
  ? stateForCommand.config.watchlists.find((entry) => entry.id === targetId)?.name
1193
1218
  : stateForCommand.config.portfolios.find((entry) => entry.id === targetId)?.name) || targetId;
1194
1219
  if (changed) {
1195
- pluginRegistry.showToastFn(
1220
+ notify(
1196
1221
  `${action === "add" ? "Added" : "Removed"} ${resolvedTicker.symbol} ${action === "add" ? "to" : "from"} "${targetName}".`,
1197
1222
  { type: "success" },
1198
1223
  );
1199
1224
  } else {
1200
- pluginRegistry.showToastFn(
1225
+ notify(
1201
1226
  action === "add"
1202
1227
  ? `${resolvedTicker.symbol} is already in "${targetName}".`
1203
1228
  : `${resolvedTicker.symbol} is not in "${targetName}".`,
@@ -1210,6 +1235,7 @@ export function CommandBar({
1210
1235
  activeTickerSymbol,
1211
1236
  buildSharedWorkflowDeps,
1212
1237
  closeAll,
1238
+ notify,
1213
1239
  openAddToPortfolioWorkflow,
1214
1240
  openModeRoute,
1215
1241
  openPickerRoute,
@@ -1433,14 +1459,14 @@ export function CommandBar({
1433
1459
  const name = coerceFieldString(route.values.name).trim();
1434
1460
  if (!name) throw new Error("Layout name is required.");
1435
1461
  dispatch({ type: "NEW_LAYOUT", name });
1436
- pluginRegistry.showToastFn(`Created layout "${name}".`, { type: "success" });
1462
+ notify(`Created layout "${name}".`, { type: "success" });
1437
1463
  break;
1438
1464
  }
1439
1465
  case "rename-layout": {
1440
1466
  const name = coerceFieldString(route.values.name).trim();
1441
1467
  if (!name) throw new Error("Layout name is required.");
1442
1468
  dispatch({ type: "RENAME_LAYOUT", index: state.config.activeLayoutIndex, name });
1443
- pluginRegistry.showToastFn(`Renamed layout to "${name}".`, { type: "success" });
1469
+ notify(`Renamed layout to "${name}".`, { type: "success" });
1444
1470
  break;
1445
1471
  }
1446
1472
  case "new-portfolio": {
@@ -1492,7 +1518,7 @@ export function CommandBar({
1492
1518
  }
1493
1519
  await command.execute(values);
1494
1520
  if (route.successLabel) {
1495
- pluginRegistry.showToastFn(route.successLabel, { type: "success" });
1521
+ notify(route.successLabel, { type: "success" });
1496
1522
  }
1497
1523
  break;
1498
1524
  }
@@ -1516,7 +1542,7 @@ export function CommandBar({
1516
1542
  };
1517
1543
  await pluginRegistry.createPaneFromTemplateAsyncFn(template.id, createOptions);
1518
1544
  if (route.successLabel) {
1519
- pluginRegistry.showToastFn(route.successLabel, { type: "success" });
1545
+ notify(route.successLabel, { type: "success" });
1520
1546
  }
1521
1547
  break;
1522
1548
  }
@@ -1566,6 +1592,7 @@ export function CommandBar({
1566
1592
  dispatch,
1567
1593
  extractBrokerWorkflowValues,
1568
1594
  getWorkflowFieldStringValue,
1595
+ notify,
1569
1596
  pluginRegistry,
1570
1597
  addTickerMembershipFromWorkflow,
1571
1598
  setPortfolioPositionFromWorkflow,
@@ -1574,9 +1601,16 @@ export function CommandBar({
1574
1601
  updateTopRoute,
1575
1602
  ]);
1576
1603
 
1577
- const openPluginCommandWorkflow = useCallback((command: CommandDef) => {
1604
+ const openPluginCommandWorkflow = useCallback((
1605
+ command: CommandDef,
1606
+ options?: { values?: Record<string, string> },
1607
+ ) => {
1578
1608
  if (!command.wizard || command.wizard.length === 0) return;
1579
1609
  const normalized = normalizeWizardFields(command.wizard);
1610
+ const values = {
1611
+ ...normalized.initialValues,
1612
+ ...(options?.values ?? {}),
1613
+ };
1580
1614
  openWorkflowRoute({
1581
1615
  kind: "workflow",
1582
1616
  workflowId: `plugin-command:${command.id}`,
@@ -1584,8 +1618,8 @@ export function CommandBar({
1584
1618
  subtitle: command.description,
1585
1619
  description: normalized.description,
1586
1620
  fields: normalized.fields,
1587
- values: normalized.initialValues,
1588
- activeFieldId: getFirstVisibleFieldId(normalized.fields, normalized.initialValues),
1621
+ values,
1622
+ activeFieldId: getFirstVisibleFieldId(normalized.fields, values),
1589
1623
  submitLabel: command.label,
1590
1624
  cancelLabel: "Back",
1591
1625
  pendingLabel: normalized.pendingLabel,
@@ -1600,6 +1634,28 @@ export function CommandBar({
1600
1634
  });
1601
1635
  }, [openWorkflowRoute]);
1602
1636
 
1637
+ useEffect(() => {
1638
+ const launch = state.commandBarLaunchRequest;
1639
+ if (!launch) {
1640
+ processedLaunchSequenceRef.current = null;
1641
+ return;
1642
+ }
1643
+ if (!state.commandBarOpen) return;
1644
+ if (processedLaunchSequenceRef.current === launch.sequence) return;
1645
+ processedLaunchSequenceRef.current = launch.sequence;
1646
+
1647
+ if (launch.kind === "plugin-command") {
1648
+ const command = pluginRegistry.commands.get(launch.commandId);
1649
+ if (!command?.wizard || command.wizard.length === 0) return;
1650
+ openPluginCommandWorkflow(command);
1651
+ }
1652
+ }, [
1653
+ openPluginCommandWorkflow,
1654
+ pluginRegistry,
1655
+ state.commandBarLaunchRequest,
1656
+ state.commandBarOpen,
1657
+ ]);
1658
+
1603
1659
  const shouldOpenTemplateConfig = useCallback((template: PaneTemplateDef, arg?: string): boolean => {
1604
1660
  if (template.wizard && template.wizard.length > 0) {
1605
1661
  if (!arg?.trim()) {
@@ -1668,12 +1724,12 @@ export function CommandBar({
1668
1724
  closeAll({ revertThemePreview: false });
1669
1725
  } catch (error) {
1670
1726
  const displayLabel = getPaneTemplateDisplayLabel(template);
1671
- pluginRegistry.showToastFn(
1727
+ notify(
1672
1728
  error instanceof Error ? error.message : `Could not create ${displayLabel.toLowerCase()}.`,
1673
1729
  { type: "error" },
1674
1730
  );
1675
1731
  }
1676
- }, [closeAll, pluginRegistry]);
1732
+ }, [closeAll, notify, pluginRegistry]);
1677
1733
 
1678
1734
  async function runPaneTemplateShortcut(
1679
1735
  template: PaneTemplateDef,
@@ -1925,12 +1981,24 @@ export function CommandBar({
1925
1981
  });
1926
1982
  }, [getPaneTemplateContext, pluginRegistry, state.config.disabledPlugins]);
1927
1983
 
1984
+ const getAvailablePluginCommands = useCallback((): CommandDef[] => {
1985
+ const disabledPlugins = new Set(state.config.disabledPlugins || []);
1986
+ return [...pluginRegistry.commands.values()]
1987
+ .filter((command) => {
1988
+ if (command.hidden?.()) return false;
1989
+ const pluginId = pluginRegistry.getCommandPluginId(command.id);
1990
+ if (pluginId && disabledPlugins.has(pluginId)) return false;
1991
+ return true;
1992
+ });
1993
+ }, [pluginRegistry, state.config.disabledPlugins]);
1994
+
1928
1995
  const rootShortcutIntent = useMemo(() => parseRootShortcutIntent({
1929
1996
  query: rootQuery,
1930
1997
  commands,
1998
+ pluginCommands: getAvailablePluginCommands(),
1931
1999
  paneTemplates: getAvailablePaneShortcutTemplates(rootQuery),
1932
2000
  activeTicker: activeTickerSymbol,
1933
- }), [activeTickerSymbol, getAvailablePaneShortcutTemplates, rootQuery]);
2001
+ }), [activeTickerSymbol, getAvailablePaneShortcutTemplates, getAvailablePluginCommands, rootQuery]);
1934
2002
 
1935
2003
  const createPaneTemplateItem = useCallback((
1936
2004
  template: PaneTemplateDef,
@@ -2009,69 +2077,108 @@ export function CommandBar({
2009
2077
  : items;
2010
2078
  }, [createPaneTemplateItem, getAvailablePaneTemplates]);
2011
2079
 
2012
- const runPluginCommandDirect = useCallback(async (command: CommandDef) => {
2080
+ const runPluginCommandDirect = useCallback(async (
2081
+ command: CommandDef,
2082
+ values?: Record<string, string>,
2083
+ ) => {
2013
2084
  try {
2014
- await command.execute();
2085
+ await command.execute(values);
2015
2086
  closeAll({ revertThemePreview: false });
2016
2087
  } catch (error) {
2017
- pluginRegistry.showToastFn(
2088
+ notify(
2018
2089
  error instanceof Error ? error.message : `Could not run ${command.label.toLowerCase()}.`,
2019
2090
  { type: "error" },
2020
2091
  );
2021
2092
  }
2022
- }, [closeAll, pluginRegistry]);
2093
+ }, [closeAll, notify, pluginRegistry]);
2094
+
2095
+ const parsePluginCommandShortcutArg = useCallback((
2096
+ command: CommandDef,
2097
+ shortcutArg: string,
2098
+ ): Record<string, string> => {
2099
+ if (!shortcutArg.trim()) return {};
2100
+ return command.shortcutArg?.parse?.(shortcutArg, {
2101
+ activeTicker: activeTickerSymbol,
2102
+ }) ?? { shortcut: shortcutArg };
2103
+ }, [activeTickerSymbol]);
2023
2104
 
2024
- const pluginCommandItems = useCallback((): ResultItem[] => {
2025
- const disabledPlugins = new Set(state.config.disabledPlugins || []);
2026
- return [...pluginRegistry.commands.values()]
2027
- .filter((command) => {
2028
- if (command.hidden?.()) return false;
2029
- const pluginId = pluginRegistry.getCommandPluginId(command.id);
2030
- if (pluginId && disabledPlugins.has(pluginId)) return false;
2031
- return true;
2032
- })
2033
- .map((command) => {
2034
- const pluginId = pluginRegistry.getCommandPluginId(command.id);
2035
- const pluginName = pluginId ? pluginRegistry.allPlugins.get(pluginId)?.name : null;
2036
- return {
2037
- id: command.id,
2038
- label: command.label,
2039
- detail: command.description || "",
2040
- category: pluginName || "Plugin Commands",
2041
- kind: "command" as const,
2042
- action: () => {
2043
- if (command.wizard && command.wizard.length > 0) {
2044
- openPluginCommandWorkflow(command);
2045
- return;
2046
- }
2047
- const confirm = resolvePluginCommandConfirm(command);
2048
- if (confirm) {
2049
- openInlineConfirm({
2050
- confirmId: `plugin-command:${command.id}`,
2051
- title: confirm.title,
2052
- body: confirm.body,
2053
- confirmLabel: confirm.confirmLabel || command.label,
2054
- cancelLabel: confirm.cancelLabel || "Back",
2055
- tone: confirm.tone || "danger",
2056
- onConfirm: async () => {
2057
- await command.execute();
2058
- },
2059
- });
2060
- return;
2061
- }
2062
- void runPluginCommandDirect(command);
2063
- },
2064
- };
2065
- });
2105
+ const createPluginCommandItem = useCallback((
2106
+ command: CommandDef,
2107
+ options?: { shortcutArg?: string },
2108
+ ): ResultItem => {
2109
+ const pluginId = pluginRegistry.getCommandPluginId(command.id);
2110
+ const pluginName = pluginId ? pluginRegistry.allPlugins.get(pluginId)?.name : null;
2111
+ const shortcut = command.shortcut?.trim() || undefined;
2112
+ const shortcutArg = options?.shortcutArg?.trim() || "";
2113
+ return {
2114
+ id: command.id,
2115
+ label: command.label,
2116
+ detail: shortcutArg || command.description || "",
2117
+ category: pluginName || "Plugin Commands",
2118
+ kind: "command",
2119
+ right: shortcut,
2120
+ searchText: `${command.label} ${command.description || ""} ${(command.keywords ?? []).join(" ")} ${shortcut || ""}`,
2121
+ action: () => {
2122
+ if (shortcutArg && command.wizard && command.wizard.length > 0) {
2123
+ try {
2124
+ openPluginCommandWorkflow(command, {
2125
+ values: parsePluginCommandShortcutArg(command, shortcutArg),
2126
+ });
2127
+ } catch (error) {
2128
+ notify(
2129
+ error instanceof Error ? error.message : `Could not parse ${command.label.toLowerCase()}.`,
2130
+ { type: "error" },
2131
+ );
2132
+ }
2133
+ return;
2134
+ }
2135
+ if (shortcutArg) {
2136
+ try {
2137
+ void runPluginCommandDirect(command, parsePluginCommandShortcutArg(command, shortcutArg));
2138
+ } catch (error) {
2139
+ notify(
2140
+ error instanceof Error ? error.message : `Could not parse ${command.label.toLowerCase()}.`,
2141
+ { type: "error" },
2142
+ );
2143
+ }
2144
+ return;
2145
+ }
2146
+ if (command.wizard && command.wizard.length > 0) {
2147
+ openPluginCommandWorkflow(command);
2148
+ return;
2149
+ }
2150
+ const confirm = resolvePluginCommandConfirm(command);
2151
+ if (confirm) {
2152
+ openInlineConfirm({
2153
+ confirmId: `plugin-command:${command.id}`,
2154
+ title: confirm.title,
2155
+ body: confirm.body,
2156
+ confirmLabel: confirm.confirmLabel || command.label,
2157
+ cancelLabel: confirm.cancelLabel || "Back",
2158
+ tone: confirm.tone || "danger",
2159
+ onConfirm: async () => {
2160
+ await command.execute();
2161
+ },
2162
+ });
2163
+ return;
2164
+ }
2165
+ void runPluginCommandDirect(command);
2166
+ },
2167
+ };
2066
2168
  }, [
2067
2169
  openInlineConfirm,
2068
2170
  openPluginCommandWorkflow,
2171
+ parsePluginCommandShortcutArg,
2172
+ notify,
2069
2173
  pluginRegistry,
2070
2174
  resolvePluginCommandConfirm,
2071
2175
  runPluginCommandDirect,
2072
- state.config.disabledPlugins,
2073
2176
  ]);
2074
2177
 
2178
+ const pluginCommandItems = useCallback((): ResultItem[] => {
2179
+ return getAvailablePluginCommands().map((command) => createPluginCommandItem(command));
2180
+ }, [createPluginCommandItem, getAvailablePluginCommands]);
2181
+
2075
2182
  const tickerActionItems = useCallback((): ResultItem[] => {
2076
2183
  const ticker = activeTickerData;
2077
2184
  const financials = activeFinancials;
@@ -2200,11 +2307,11 @@ export function CommandBar({
2200
2307
  const exportPath = `${process.env.HOME || "~"}/gloomberb-config-backup.json`;
2201
2308
  void exportConfig(state.config, exportPath)
2202
2309
  .then(() => {
2203
- pluginRegistry.showToastFn(`Config exported to ${exportPath}`, { type: "success" });
2310
+ notify(`Config exported to ${exportPath}`, { type: "success" });
2204
2311
  closeAll({ revertThemePreview: false });
2205
2312
  })
2206
2313
  .catch((error) => {
2207
- pluginRegistry.showToastFn(error instanceof Error ? error.message : "Export failed.", { type: "error" });
2314
+ notify(error instanceof Error ? error.message : "Export failed.", { type: "error" });
2208
2315
  });
2209
2316
  return;
2210
2317
  }
@@ -2215,11 +2322,11 @@ export function CommandBar({
2215
2322
  dispatch({ type: "SET_CONFIG", config: imported });
2216
2323
  applyTheme(imported.theme);
2217
2324
  dispatch({ type: "SET_THEME", theme: imported.theme });
2218
- pluginRegistry.showToastFn(`Imported config from ${importPath}.`, { type: "success" });
2325
+ notify(`Imported config from ${importPath}.`, { type: "success" });
2219
2326
  closeAll({ revertThemePreview: false });
2220
2327
  })
2221
2328
  .catch((error) => {
2222
- pluginRegistry.showToastFn(error instanceof Error ? error.message : "Import failed.", { type: "error" });
2329
+ notify(error instanceof Error ? error.message : "Import failed.", { type: "error" });
2223
2330
  });
2224
2331
  return;
2225
2332
  }
@@ -2488,6 +2595,10 @@ export function CommandBar({
2488
2595
  return name ? `in "${name}"` : command.description;
2489
2596
  }
2490
2597
  case "check-for-updates":
2598
+ if (state.updateProgress?.phase === "downloading") return `Downloading v${state.updateAvailable?.version}: ${state.updateProgress.percent ?? 0}%`;
2599
+ if (state.updateProgress?.phase === "replacing") return "Installing update";
2600
+ if (state.updateProgress?.phase === "done") return "Update installed - restart to apply";
2601
+ if (state.updateProgress?.phase === "error") return `Update failed: ${state.updateProgress.error}`;
2491
2602
  if (state.updateCheckInProgress) return "Checking GitHub releases now";
2492
2603
  if (state.updateAvailable) return `Latest available: v${state.updateAvailable.version}`;
2493
2604
  if (state.updateNotice) return state.updateNotice;
@@ -2533,6 +2644,12 @@ export function CommandBar({
2533
2644
  });
2534
2645
  }
2535
2646
 
2647
+ if (rootShortcutIntent.source === "plugin-command") {
2648
+ return createPluginCommandItem(rootShortcutIntent.command, {
2649
+ shortcutArg: rootShortcutIntent.argText,
2650
+ });
2651
+ }
2652
+
2536
2653
  const { command } = rootShortcutIntent;
2537
2654
  if (command.id === "search-ticker") {
2538
2655
  const inferredSymbol = normalizeTickerInput(activeTickerSymbol, undefined);
@@ -2563,12 +2680,22 @@ export function CommandBar({
2563
2680
  : manualPortfolios.length === 1
2564
2681
  ? manualPortfolios[0]!.id
2565
2682
  : null)
2566
- : resolvePreferredCollectionTarget(
2567
- state,
2568
- kind,
2569
- activeCollectionId,
2570
- action,
2571
- localTicker,
2683
+ : (
2684
+ resolvePreferredCollectionTarget(
2685
+ state,
2686
+ kind,
2687
+ activeCollectionId,
2688
+ action,
2689
+ localTicker,
2690
+ )
2691
+ ?? (commandId === "add-watchlist"
2692
+ ? resolveSoleCollectionTarget(
2693
+ state,
2694
+ kind,
2695
+ action,
2696
+ localTicker,
2697
+ )
2698
+ : null)
2572
2699
  );
2573
2700
  const preferredTargetName = preferredTargetId
2574
2701
  ? (kind === "watchlist"
@@ -2601,7 +2728,11 @@ export function CommandBar({
2601
2728
  let initialIdx = 0;
2602
2729
  const shortcutItem = buildRootShortcutItem();
2603
2730
 
2604
- if (rootShortcutIntent.kind !== "none" && rootShortcutIntent.source === "pane-template" && shortcutItem) {
2731
+ if (
2732
+ rootShortcutIntent.kind !== "none"
2733
+ && (rootShortcutIntent.source === "pane-template" || rootShortcutIntent.source === "plugin-command")
2734
+ && shortcutItem
2735
+ ) {
2605
2736
  items.push(shortcutItem);
2606
2737
  } else if (match && match.command.id === "plugins") {
2607
2738
  const disabledPlugins = state.config.disabledPlugins || [];
@@ -2838,6 +2969,7 @@ export function CommandBar({
2838
2969
  action: () => {
2839
2970
  const { width, height } = pluginRegistry.getTermSizeFn();
2840
2971
  persistLayoutChange(gridlockAllPanes(currentLayout, { x: 0, y: 0, width, height }));
2972
+ notifyGridlockRevert();
2841
2973
  closeAll({ revertThemePreview: false });
2842
2974
  },
2843
2975
  });
@@ -2987,12 +3119,14 @@ export function CommandBar({
2987
3119
  activeTickerData,
2988
3120
  activeTickerSymbol,
2989
3121
  closeAll,
3122
+ createPluginCommandItem,
2990
3123
  currentRoute,
2991
3124
  dispatch,
2992
3125
  duplicatePane,
2993
3126
  executeCollectionCommand,
2994
3127
  localTickerSearchResultItems,
2995
3128
  nonShortcutPaneTemplateItems,
3129
+ notifyGridlockRevert,
2996
3130
  openBuiltInWorkflow,
2997
3131
  openPickerRoute,
2998
3132
  paneShortcutItems,
@@ -3162,6 +3296,12 @@ export function CommandBar({
3162
3296
  return `Shortcut: ${rootShortcutIntent.label}`;
3163
3297
  }
3164
3298
 
3299
+ if (rootShortcutIntent.source === "plugin-command") {
3300
+ return rootShortcutIntent.argText
3301
+ ? `Shortcut: ${rootShortcutIntent.label} · ${rootShortcutIntent.argText}`
3302
+ : `Shortcut: ${rootShortcutIntent.label}`;
3303
+ }
3304
+
3165
3305
  if (rootShortcutIntent.command.id === "search-ticker") {
3166
3306
  const symbol = normalizeTickerInput(activeTickerSymbol, rootShortcutIntent.argText);
3167
3307
  if (symbol) {
@@ -3184,7 +3324,14 @@ export function CommandBar({
3184
3324
  activeCollectionId,
3185
3325
  action,
3186
3326
  localTicker,
3187
- );
3327
+ ) ?? (commandId === "add-watchlist"
3328
+ ? resolveSoleCollectionTarget(
3329
+ state,
3330
+ kind,
3331
+ action,
3332
+ localTicker,
3333
+ )
3334
+ : null);
3188
3335
  const preferredTargetName = preferredTargetId
3189
3336
  ? (kind === "watchlist"
3190
3337
  ? state.config.watchlists.find((entry) => entry.id === preferredTargetId)?.name
@@ -3205,6 +3352,7 @@ export function CommandBar({
3205
3352
  const intent = parseRootShortcutIntent({
3206
3353
  query: rootQueryRef.current,
3207
3354
  commands,
3355
+ pluginCommands: getAvailablePluginCommands(),
3208
3356
  paneTemplates: getAvailablePaneShortcutTemplates(rootQueryRef.current),
3209
3357
  activeTicker: activeTickerSymbol,
3210
3358
  });
@@ -3229,6 +3377,10 @@ export function CommandBar({
3229
3377
  return false;
3230
3378
  }
3231
3379
 
3380
+ if (intent.source === "plugin-command") {
3381
+ return false;
3382
+ }
3383
+
3232
3384
  if (intent.command.id === "search-ticker") {
3233
3385
  openModeRoute("ticker-search", intent.argText);
3234
3386
  return true;
@@ -3243,6 +3395,7 @@ export function CommandBar({
3243
3395
  return false;
3244
3396
  }, [
3245
3397
  activeTickerSymbol,
3398
+ getAvailablePluginCommands,
3246
3399
  getAvailablePaneShortcutTemplates,
3247
3400
  openModeRoute,
3248
3401
  openPaneTemplateWorkflow,
@@ -3253,6 +3406,7 @@ export function CommandBar({
3253
3406
  const intent = parseRootShortcutIntent({
3254
3407
  query,
3255
3408
  commands,
3409
+ pluginCommands: getAvailablePluginCommands(),
3256
3410
  paneTemplates: getAvailablePaneShortcutTemplates(query),
3257
3411
  activeTicker: activeTickerSymbol,
3258
3412
  });
@@ -3265,6 +3419,12 @@ export function CommandBar({
3265
3419
  });
3266
3420
  }
3267
3421
 
3422
+ if (intent.kind !== "none" && intent.source === "plugin-command") {
3423
+ return createPluginCommandItem(intent.command, {
3424
+ shortcutArg: intent.argText,
3425
+ });
3426
+ }
3427
+
3268
3428
  const match = matchPrefix(query);
3269
3429
  if (!match) {
3270
3430
  return null;
@@ -3372,8 +3532,10 @@ export function CommandBar({
3372
3532
  return null;
3373
3533
  }, [
3374
3534
  createPaneTemplateItem,
3535
+ createPluginCommandItem,
3375
3536
  activeTickerSymbol,
3376
3537
  executeCollectionCommand,
3538
+ getAvailablePluginCommands,
3377
3539
  getAvailablePaneShortcutTemplates,
3378
3540
  openModeRoute,
3379
3541
  paneTemplateItems,
@@ -3675,6 +3837,7 @@ export function CommandBar({
3675
3837
  action: () => {
3676
3838
  const { width, height } = pluginRegistry.getTermSizeFn();
3677
3839
  persistLayoutChange(gridlockAllPanes(currentLayout, { x: 0, y: 0, width, height }));
3840
+ notifyGridlockRevert();
3678
3841
  closeAll({ revertThemePreview: false });
3679
3842
  },
3680
3843
  });
@@ -3863,6 +4026,7 @@ export function CommandBar({
3863
4026
  localTickerSearchResultItems,
3864
4027
  mapTickerSearchCandidateToResultItem,
3865
4028
  nonShortcutPaneTemplateItems,
4029
+ notifyGridlockRevert,
3866
4030
  openBuiltInWorkflow,
3867
4031
  openInlineConfirm,
3868
4032
  openPaneSettingsRoute,
@@ -3995,20 +4159,31 @@ export function CommandBar({
3995
4159
  }, [moveListSelection, setHoveredIndex]);
3996
4160
 
3997
4161
  const updateWorkflowValue = useCallback((fieldId: string, value: CommandBarFieldValue) => {
3998
- updateTopRoute((route) => {
3999
- if (route.kind !== "workflow") return route;
4162
+ setRouteStack((current) => {
4163
+ let workflowIndex = -1;
4164
+ for (let index = current.length - 1; index >= 0; index -= 1) {
4165
+ if (current[index]?.kind === "workflow") {
4166
+ workflowIndex = index;
4167
+ break;
4168
+ }
4169
+ }
4170
+ if (workflowIndex < 0) return current;
4171
+ const next = [...current];
4172
+ const route = next[workflowIndex]!;
4173
+ if (route.kind !== "workflow") return current;
4000
4174
  const nextValues = { ...route.values, [fieldId]: value };
4001
4175
  const nextActiveFieldId = route.activeFieldId && getVisibleWorkflowFields(route.fields, nextValues).some((field) => field.id === route.activeFieldId)
4002
4176
  ? route.activeFieldId
4003
4177
  : getFirstVisibleFieldId(route.fields, nextValues);
4004
- return {
4178
+ next[workflowIndex] = {
4005
4179
  ...route,
4006
4180
  values: nextValues,
4007
4181
  activeFieldId: nextActiveFieldId,
4008
4182
  error: null,
4009
4183
  };
4184
+ return next;
4010
4185
  });
4011
- }, [updateTopRoute]);
4186
+ }, []);
4012
4187
 
4013
4188
  const moveWorkflowFocus = useCallback((delta: number) => {
4014
4189
  if (currentRoute?.kind !== "workflow") return;
@@ -4871,6 +5046,7 @@ export function CommandBar({
4871
5046
  placeholderColor={paletteSubtleText}
4872
5047
  backgroundColor={colors.panel}
4873
5048
  flexGrow={1}
5049
+ wrapText
4874
5050
  />
4875
5051
  ) : (
4876
5052
  <box flexDirection="column" paddingX={1} paddingY={0}>