gloomberb 0.7.2 → 0.7.4

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 (167) hide show
  1. package/README.md +79 -132
  2. package/package.json +1 -1
  3. package/src/app.tsx +73 -67
  4. package/src/components/chart/chart-data.ts +7 -2
  5. package/src/components/chart/chart-pane-settings.ts +7 -4
  6. package/src/components/chart/chart-renderer.ts +158 -47
  7. package/src/components/chart/chart-types.ts +20 -2
  8. package/src/components/chart/chart.test.ts +179 -13
  9. package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
  10. package/src/components/chart/comparison-chart-renderer.ts +50 -11
  11. package/src/components/chart/comparison-stock-chart.tsx +129 -129
  12. package/src/components/chart/market-session.ts +187 -0
  13. package/src/components/chart/native/chart-rasterizer.ts +108 -19
  14. package/src/components/chart/price-axis-labels.tsx +25 -9
  15. package/src/components/chart/stock-chart-renderer-switch.test.tsx +97 -1
  16. package/src/components/chart/stock-chart.tsx +95 -49
  17. package/src/components/chart/time-axis-label.tsx +56 -2
  18. package/src/components/command-bar/command-bar.test.tsx +446 -120
  19. package/src/components/command-bar/command-bar.tsx +535 -384
  20. package/src/components/command-bar/command-registry.ts +16 -6
  21. package/src/components/command-bar/list-model.ts +8 -4
  22. package/src/components/command-bar/root-shortcuts.ts +4 -5
  23. package/src/components/command-bar/view-model.test.ts +21 -0
  24. package/src/components/command-bar/view-model.ts +17 -4
  25. package/src/components/command-bar/workflow-ops.ts +11 -32
  26. package/src/components/data-table-view.test.tsx +24 -1
  27. package/src/components/data-table-view.tsx +3 -2
  28. package/src/components/feed-data-table-stack-view.tsx +3 -2
  29. package/src/components/layout/detached-pane-shell.tsx +2 -2
  30. package/src/components/layout/floating-pane.tsx +9 -2
  31. package/src/components/layout/pane-footer.tsx +1 -1
  32. package/src/components/layout/pane-header.tsx +4 -2
  33. package/src/components/layout/pane-sizing.ts +12 -0
  34. package/src/components/layout/pane.tsx +24 -2
  35. package/src/components/layout/shell.tsx +24 -10
  36. package/src/components/layout/status-bar.test.tsx +4 -1
  37. package/src/components/layout/status-bar.tsx +4 -24
  38. package/src/components/markdown-text.tsx +53 -8
  39. package/src/components/onboarding/onboarding-wizard.tsx +7 -6
  40. package/src/components/pane-settings-dialog.tsx +9 -6
  41. package/src/components/pane-template-wizard.tsx +3 -2
  42. package/src/components/price-selector-dialog.tsx +3 -2
  43. package/src/components/price-sparkline-view.tsx +463 -0
  44. package/src/components/price-sparkline.ts +46 -0
  45. package/src/components/table-view-shared.tsx +21 -3
  46. package/src/components/ticker-list-table-view.tsx +3 -2
  47. package/src/components/ticker-list-table.tsx +15 -66
  48. package/src/components/toggle-list.tsx +3 -3
  49. package/src/components/ui/choice-dialog.tsx +3 -2
  50. package/src/components/ui/multi-select-dialog.tsx +3 -2
  51. package/src/components/ui/native-select.tsx +3 -3
  52. package/src/core/state/app-state.ts +210 -80
  53. package/src/core/state/session-persistence.ts +25 -0
  54. package/src/data/config-store-node.ts +113 -25
  55. package/src/data/config-store.test.ts +65 -3
  56. package/src/data/plugin-state-store.ts +39 -13
  57. package/src/data/sqlite-cache.test.ts +13 -0
  58. package/src/market-data/coordinator-subscriptions.test.ts +102 -18
  59. package/src/market-data/coordinator.test.ts +122 -2
  60. package/src/market-data/coordinator.ts +200 -56
  61. package/src/market-data/hooks.test.tsx +31 -0
  62. package/src/market-data/hooks.tsx +1 -0
  63. package/src/news/aggregator.test.ts +41 -0
  64. package/src/news/aggregator.ts +61 -9
  65. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
  66. package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
  67. package/src/plugins/builtin/ai/settings.ts +3 -0
  68. package/src/plugins/builtin/broker-manager/index.tsx +4 -3
  69. package/src/plugins/builtin/chat-controller.test.ts +398 -17
  70. package/src/plugins/builtin/chat-controller.ts +263 -48
  71. package/src/plugins/builtin/chat.test.tsx +312 -6
  72. package/src/plugins/builtin/chat.tsx +245 -66
  73. package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
  74. package/src/plugins/builtin/correlation/colors.test.ts +39 -0
  75. package/src/plugins/builtin/correlation/colors.ts +87 -0
  76. package/src/plugins/builtin/correlation/index.test.tsx +151 -0
  77. package/src/plugins/builtin/correlation/index.tsx +92 -21
  78. package/src/plugins/builtin/debug.tsx +4 -3
  79. package/src/plugins/builtin/econ/index.tsx +3 -2
  80. package/src/plugins/builtin/help.test.tsx +2 -2
  81. package/src/plugins/builtin/holders/index.tsx +25 -13
  82. package/src/plugins/builtin/layout-manager.tsx +1 -7
  83. package/src/plugins/builtin/market-movers/index.tsx +72 -40
  84. package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
  85. package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
  86. package/src/plugins/builtin/news-wire/index.tsx +1 -3
  87. package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
  88. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
  89. package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
  90. package/src/plugins/builtin/options.tsx +9 -17
  91. package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
  92. package/src/plugins/builtin/portfolio-list/metrics.ts +13 -2
  93. package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
  94. package/src/plugins/builtin/portfolio-list/pane.tsx +139 -23
  95. package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
  96. package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
  97. package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
  98. package/src/plugins/builtin/portfolio-list.test.tsx +311 -9
  99. package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
  100. package/src/plugins/builtin/sectors/index.tsx +275 -43
  101. package/src/plugins/builtin/sectors/sector-data.ts +41 -1
  102. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
  103. package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
  104. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
  105. package/src/plugins/builtin/ticker-detail/pane.tsx +18 -7
  106. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +445 -63
  107. package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
  108. package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
  109. package/src/plugins/builtin/world-indices/index.tsx +58 -19
  110. package/src/plugins/ibkr/trading-pane.tsx +3 -2
  111. package/src/plugins/plugin-runtime.tsx +2 -2
  112. package/src/plugins/prediction-markets/settings.ts +4 -0
  113. package/src/plugins/registry.test.ts +55 -0
  114. package/src/plugins/registry.ts +11 -4
  115. package/src/plugins/ticker-navigation.test.ts +35 -5
  116. package/src/plugins/ticker-navigation.ts +16 -22
  117. package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
  118. package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
  119. package/src/renderers/electrobun/bun/application-menu.ts +6 -1
  120. package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
  121. package/src/renderers/electrobun/bun/index.ts +52 -13
  122. package/src/renderers/electrobun/view/app-services.ts +51 -9
  123. package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
  124. package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
  125. package/src/renderers/electrobun/view/main.tsx +0 -16
  126. package/src/renderers/electrobun/view/styles.css +23 -21
  127. package/src/renderers/electrobun/view/ui-host.tsx +6 -4
  128. package/src/renderers/opentui/host.tsx +2 -1
  129. package/src/renderers/opentui/start.tsx +1 -1
  130. package/src/renderers/opentui/test-utils.tsx +6 -5
  131. package/src/sources/gloomberb-cloud.test.ts +55 -2
  132. package/src/sources/gloomberb-cloud.ts +180 -29
  133. package/src/sources/provider-router.test.ts +567 -10
  134. package/src/sources/provider-router.ts +323 -93
  135. package/src/sources/yahoo-finance.test.ts +37 -0
  136. package/src/sources/yahoo-finance.ts +48 -2
  137. package/src/state/app-bootstrap.test.ts +50 -23
  138. package/src/state/app-bootstrap.ts +29 -13
  139. package/src/state/app-context.test.ts +113 -0
  140. package/src/state/app-context.tsx +8 -4
  141. package/src/state/session-persistence.test.ts +5 -3
  142. package/src/state/use-inline-tickers.ts +3 -0
  143. package/src/state/use-quote-streaming.ts +11 -0
  144. package/src/theme/colors.ts +13 -14
  145. package/src/types/config.ts +9 -4
  146. package/src/types/data-provider.ts +5 -1
  147. package/src/types/financials.ts +1 -0
  148. package/src/types/plugin.ts +8 -1
  149. package/src/ui/context-menu.tsx +4 -4
  150. package/src/utils/api-client.test.ts +248 -0
  151. package/src/utils/api-client.ts +178 -26
  152. package/src/utils/exchanges.test.ts +16 -0
  153. package/src/utils/exchanges.ts +33 -0
  154. package/src/utils/fuzzy-search.ts +12 -1
  155. package/src/utils/keyboard.ts +31 -0
  156. package/src/utils/options.ts +1 -1
  157. package/src/utils/price-history.test.ts +28 -1
  158. package/src/utils/price-history.ts +16 -0
  159. package/src/utils/quote-freshness.test.ts +150 -0
  160. package/src/utils/quote-freshness.ts +42 -1
  161. package/src/utils/quote-resolution.ts +2 -0
  162. package/src/utils/quote-time.test.ts +34 -0
  163. package/src/utils/quote-time.ts +18 -5
  164. package/src/utils/ticker-search.test.ts +2 -2
  165. package/src/utils/ticker-search.ts +48 -1
  166. package/src/version.ts +1 -1
  167. package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
@@ -2,8 +2,7 @@ import { Box, Input, ScrollBox, Text, Textarea, useUiCapabilities } from "../../
2
2
  import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import type { RefObject } from "react";
4
4
  import { TextAttributes, type InputRenderable, type ScrollBoxRenderable, type TextareaRenderable } from "../../ui";
5
- import { useNativeRenderer } from "../../ui";
6
- import { useViewport } from "../../react/input";
5
+ import { useShortcut, useViewport } from "../../react/input";
7
6
  import { Button, NumberField, Spinner, TextField } from "../ui";
8
7
  import { NativeSelect, openNativeSelect, type NativeSelectElement } from "../ui/native-select";
9
8
  import { ToggleList } from "../toggle-list";
@@ -19,21 +18,25 @@ import {
19
18
  commandBarSelectedText,
20
19
  commandBarSubtleText,
21
20
  commandBarText,
21
+ blendHex,
22
+ colors,
22
23
  getCurrentThemeId,
23
24
  previewTheme,
24
25
  } from "../../theme/colors";
25
26
  import {
26
27
  getFocusedCollectionId,
28
+ syncConfigActiveLayoutState,
27
29
  useAppDispatch,
28
30
  useAppSelector,
29
31
  useFocusedTicker,
30
32
  type AppState,
31
33
  } from "../../state/app-context";
34
+ import { scheduleConfigSave } from "../../state/config-save-scheduler";
32
35
  import { fuzzyFilter } from "../../utils/fuzzy-search";
33
36
  import { commands, matchPrefix, type Command } from "./command-registry";
34
37
  import { useThemeColors } from "../../theme/theme-context";
35
38
  import { ThemePicker, type ThemePickerHandle } from "./theme-picker";
36
- import { exportConfig, importConfig, resetAllData, saveConfig } from "../../data/config-store";
39
+ import { exportConfig, importConfig, resetAllData } from "../../data/config-store";
37
40
  import type { DataProvider } from "../../types/data-provider";
38
41
  import type { TickerRepository } from "../../data/ticker-repository";
39
42
  import type { PluginRegistry } from "../../plugins/registry";
@@ -185,6 +188,37 @@ const NATIVE_COMMAND_BAR_PADDING_X_PX = 14;
185
188
  const NATIVE_COMMAND_BAR_PADDING_Y_PX = 14;
186
189
  const QUICK_LOOK_TICKER_SEARCH_OPTIONS = { includeOptionContracts: false } as const;
187
190
 
191
+ function normalizeCommandTickerSearchText(value: string): string {
192
+ return value.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "");
193
+ }
194
+
195
+ function isLikelyPlainTickerSearch(query: string): boolean {
196
+ const trimmed = query.trim();
197
+ if (!trimmed || /\s/.test(trimmed)) return false;
198
+ if (!/^[A-Za-z0-9.^=\-/]+$/.test(trimmed)) return false;
199
+
200
+ const compact = normalizeCommandTickerSearchText(trimmed);
201
+ if (compact.length < 2) return false;
202
+ if (compact.length <= 6) return true;
203
+ return /[.^=\-/]/.test(trimmed) && compact.length <= 10;
204
+ }
205
+
206
+ function isExactTickerResultMatch(item: ResultItem, query: string): boolean {
207
+ if (item.kind !== "ticker" && item.kind !== "search") return false;
208
+ const normalizedQuery = normalizeCommandTickerSearchText(query);
209
+ if (!normalizedQuery) return false;
210
+ return normalizeCommandTickerSearchText(item.label) === normalizedQuery;
211
+ }
212
+
213
+ function getPaneTemplateArgKind(template: PaneTemplateDef): string | undefined {
214
+ return template.shortcut?.argKind ?? template.shortcut?.argPlaceholder;
215
+ }
216
+
217
+ function canPromptForPaneTemplateArg(template: PaneTemplateDef): boolean {
218
+ const argKind = getPaneTemplateArgKind(template);
219
+ return argKind === "ticker" || argKind === "ticker-list" || argKind === "tickers";
220
+ }
221
+
188
222
  function getDefaultConfigBackupPath(): string {
189
223
  const home = typeof process !== "undefined" ? process.env.HOME : undefined;
190
224
  return `${home || "~"}/gloomberb-config-backup.json`;
@@ -563,9 +597,10 @@ const CommandBarListBody = memo(function CommandBarListBody({
563
597
 
564
598
  const isSelected = row.globalIdx === visibleListState.selectedIdx;
565
599
  const isHovered = row.globalIdx === visibleListState.hoveredIdx && !isSelected;
600
+ const itemRowKey = `item:${row.globalIdx}:${row.item.id}:${row.item.category}:${row.item.label}:${row.item.right || ""}`;
566
601
  return (
567
602
  <CommandBarListItemRow
568
- key={row.item.id}
603
+ key={itemRowKey}
569
604
  item={row.item}
570
605
  globalIdx={row.globalIdx}
571
606
  isSelected={isSelected}
@@ -618,6 +653,7 @@ export function CommandBar({
618
653
  const tickers = useAppSelector((state) => state.tickers);
619
654
  const financials = useAppSelector((state) => state.financials);
620
655
  const focusedPaneId = useAppSelector((state) => state.focusedPaneId);
656
+ const activePanel = useAppSelector((state) => state.activePanel);
621
657
  const layoutHistory = useAppSelector((state) => state.layoutHistory);
622
658
  const recentTickers = useAppSelector((state) => state.recentTickers);
623
659
  const commandBarOpen = useAppSelector((state) => state.commandBarOpen);
@@ -633,6 +669,7 @@ export function CommandBar({
633
669
  tickers,
634
670
  financials,
635
671
  focusedPaneId,
672
+ activePanel,
636
673
  layoutHistory,
637
674
  recentTickers,
638
675
  commandBarOpen,
@@ -643,6 +680,7 @@ export function CommandBar({
643
680
  updateCheckInProgress,
644
681
  updateNotice,
645
682
  }) as AppState, [
683
+ activePanel,
646
684
  commandBarLaunchRequest,
647
685
  commandBarOpen,
648
686
  commandBarQuery,
@@ -660,8 +698,16 @@ export function CommandBar({
660
698
  ]);
661
699
  const stateRef = useRef(state);
662
700
  stateRef.current = state;
701
+ const persistConfig = useCallback((nextConfig: AppState["config"]) => {
702
+ const currentState = stateRef.current;
703
+ scheduleConfigSave(syncConfigActiveLayoutState(
704
+ nextConfig,
705
+ currentState.paneState,
706
+ currentState.focusedPaneId,
707
+ currentState.activePanel,
708
+ ));
709
+ }, []);
663
710
  const { symbol: activeTickerSymbol, ticker: activeTickerData, financials: activeFinancials } = useFocusedTicker();
664
- const renderer = useNativeRenderer();
665
711
  const { width: termWidth, height: termHeight } = useViewport();
666
712
  const { nativePaneChrome, cellWidthPx = 8, cellHeightPx = 18 } = useUiCapabilities();
667
713
  const availableCommands = useMemo(
@@ -833,20 +879,16 @@ export function CommandBar({
833
879
  : instance
834
880
  )),
835
881
  };
836
- const nextConfig = {
837
- ...currentState.config,
838
- layout: nextLayout,
839
- layouts: currentState.config.layouts.map((savedLayout, index) => (
840
- index === currentState.config.activeLayoutIndex ? { ...savedLayout, layout: nextLayout } : savedLayout
841
- )),
842
- };
843
882
  dispatch({ type: "UPDATE_LAYOUT", layout: nextLayout });
844
- void saveConfig(nextConfig);
845
883
  dispatch({ type: "FOCUS_PANE", paneId: targetPane.instanceId });
846
884
  }, [dispatch]);
847
885
 
848
- const openFixedTickerPane = useCallback((symbol: string) => {
849
- pluginRegistry.pinTicker(symbol, { floating: true, paneType: "ticker-detail" });
886
+ const openFixedTickerPane = useCallback((symbol: string, options?: { forceNewPane?: boolean }) => {
887
+ pluginRegistry.pinTicker(symbol, {
888
+ floating: true,
889
+ paneType: "ticker-detail",
890
+ forceNewPane: options?.forceNewPane,
891
+ });
850
892
  }, [pluginRegistry]);
851
893
 
852
894
  const commitTheme = useCallback((themeId: string) => {
@@ -872,7 +914,7 @@ export function CommandBar({
872
914
  ? findPaneInstance(currentState.config.layout, currentState.focusedPaneId)
873
915
  : null;
874
916
  if (options?.forceNewPane) {
875
- openFixedTickerPane(symbol);
917
+ openFixedTickerPane(symbol, { forceNewPane: true });
876
918
  return;
877
919
  }
878
920
 
@@ -1165,9 +1207,9 @@ export function CommandBar({
1165
1207
  );
1166
1208
  dispatch({ type: "SET_CONFIG", config: nextConfig });
1167
1209
  setActiveCollection(portfolio.id);
1168
- await saveConfig(nextConfig);
1210
+ persistConfig(nextConfig);
1169
1211
  notify(`Created portfolio "${portfolio.name}".`, { type: "success" });
1170
- }, [dispatch, notify, setActiveCollection]);
1212
+ }, [dispatch, notify, persistConfig, setActiveCollection]);
1171
1213
 
1172
1214
  const createWatchlist = useCallback(async (name: string) => {
1173
1215
  const currentState = stateRef.current;
@@ -1184,9 +1226,9 @@ export function CommandBar({
1184
1226
  };
1185
1227
  dispatch({ type: "SET_CONFIG", config: nextConfig });
1186
1228
  setActiveCollection(id);
1187
- await saveConfig(nextConfig);
1229
+ persistConfig(nextConfig);
1188
1230
  notify(`Created watchlist "${trimmedName}".`, { type: "success" });
1189
- }, [dispatch, notify, setActiveCollection]);
1231
+ }, [dispatch, notify, persistConfig, setActiveCollection]);
1190
1232
 
1191
1233
  const deleteWatchlist = useCallback(async (watchlistId: string) => {
1192
1234
  const currentState = stateRef.current;
@@ -1204,9 +1246,9 @@ export function CommandBar({
1204
1246
  const fallback = nextConfig.portfolios[0]?.id || nextConfig.watchlists[0]?.id || "";
1205
1247
  if (fallback) setActiveCollection(fallback);
1206
1248
  }
1207
- await saveConfig(nextConfig);
1249
+ persistConfig(nextConfig);
1208
1250
  notify(`Deleted "${watchlist.name}".`, { type: "success" });
1209
- }, [activeCollectionId, dispatch, notify, setActiveCollection]);
1251
+ }, [activeCollectionId, dispatch, notify, persistConfig, setActiveCollection]);
1210
1252
 
1211
1253
  const deletePortfolio = useCallback(async (portfolioId: string) => {
1212
1254
  const currentState = stateRef.current;
@@ -1234,9 +1276,9 @@ export function CommandBar({
1234
1276
  const fallback = nextConfig.portfolios[0]?.id || nextConfig.watchlists[0]?.id || "";
1235
1277
  if (fallback) setActiveCollection(fallback);
1236
1278
  }
1237
- await saveConfig(nextConfig);
1279
+ persistConfig(nextConfig);
1238
1280
  notify(`Deleted "${portfolio.name}".`, { type: "success" });
1239
- }, [activeCollectionId, dispatch, notify, setActiveCollection, tickerRepository]);
1281
+ }, [activeCollectionId, dispatch, notify, persistConfig, setActiveCollection, tickerRepository]);
1240
1282
 
1241
1283
  const setPortfolioPositionFromWorkflow = useCallback(async (values: Record<string, CommandBarFieldValue>) => {
1242
1284
  const currentState = stateRef.current;
@@ -1499,6 +1541,7 @@ export function CommandBar({
1499
1541
  const enabled = !disabledPlugins.includes(plugin.id);
1500
1542
  const toggleAction = () => {
1501
1543
  dispatch({ type: "TOGGLE_PLUGIN", pluginId: plugin.id });
1544
+ const currentState = stateRef.current;
1502
1545
  const nextDisabled = enabled
1503
1546
  ? [...disabledPlugins, plugin.id]
1504
1547
  : disabledPlugins.filter((entry) => entry !== plugin.id);
@@ -1507,7 +1550,7 @@ export function CommandBar({
1507
1550
  pluginRegistry.hidePane(paneId);
1508
1551
  }
1509
1552
  }
1510
- void saveConfig({ ...state.config, disabledPlugins: nextDisabled });
1553
+ persistConfig({ ...currentState.config, disabledPlugins: nextDisabled });
1511
1554
  };
1512
1555
  return {
1513
1556
  id: `plugin:${plugin.id}`,
@@ -1520,7 +1563,7 @@ export function CommandBar({
1520
1563
  action: toggleAction,
1521
1564
  };
1522
1565
  });
1523
- }, [dispatch, pluginRegistry, state.config]);
1566
+ }, [dispatch, persistConfig, pluginRegistry, state.config]);
1524
1567
 
1525
1568
  const buildLayoutItems = useCallback((
1526
1569
  query: string,
@@ -2164,6 +2207,94 @@ export function CommandBar({
2164
2207
  }
2165
2208
  }, []);
2166
2209
 
2210
+ const activatePaneSettingField = useCallback((
2211
+ paneId: string,
2212
+ field: PaneSettingField,
2213
+ currentValue: unknown,
2214
+ options?: { keepRouteOpen?: boolean },
2215
+ ) => {
2216
+ const normalized = normalizePaneSettingField(paneId, field, currentValue);
2217
+ if (normalized.mode === "toggle") {
2218
+ if (options?.keepRouteOpen) {
2219
+ updateTopRoute((route) => route.kind === "pane-settings"
2220
+ ? { ...route, pendingFieldKey: field.key, error: null }
2221
+ : route);
2222
+ }
2223
+ void pluginRegistry.applyPaneSettingValueFn(paneId, field, !normalized.value)
2224
+ .then(() => {
2225
+ if (options?.keepRouteOpen) {
2226
+ updateTopRoute((route) => route.kind === "pane-settings"
2227
+ ? { ...route, pendingFieldKey: null, error: null }
2228
+ : route);
2229
+ } else {
2230
+ closeAll({ revertThemePreview: false });
2231
+ }
2232
+ })
2233
+ .catch((error) => {
2234
+ if (options?.keepRouteOpen) {
2235
+ updateTopRoute((route) => route.kind === "pane-settings"
2236
+ ? {
2237
+ ...route,
2238
+ pendingFieldKey: null,
2239
+ error: error instanceof Error ? error.message : "Could not apply that setting.",
2240
+ }
2241
+ : route);
2242
+ return;
2243
+ }
2244
+ notify(error instanceof Error ? error.message : "Could not apply that setting.", { type: "error" });
2245
+ });
2246
+ return;
2247
+ }
2248
+ if (normalized.mode === "workflow" && normalized.route) {
2249
+ openWorkflowRoute(normalized.route);
2250
+ return;
2251
+ }
2252
+ if (normalized.mode === "picker") {
2253
+ openPickerRoute(normalized.route);
2254
+ }
2255
+ }, [
2256
+ closeAll,
2257
+ normalizePaneSettingField,
2258
+ notify,
2259
+ openPickerRoute,
2260
+ openWorkflowRoute,
2261
+ pluginRegistry,
2262
+ updateTopRoute,
2263
+ ]);
2264
+
2265
+ const buildPaneSettingItems = useCallback((
2266
+ paneId: string | null,
2267
+ query: string,
2268
+ options?: { keepRouteOpen?: boolean },
2269
+ ): ResultItem[] => {
2270
+ if (!paneId) return [];
2271
+ const descriptor = pluginRegistry.resolvePaneSettings(paneId);
2272
+ if (!descriptor) return [];
2273
+
2274
+ const category = descriptor.settingsDef.title || "Pane Settings";
2275
+ const paneLabel = descriptor.pane.title || descriptor.paneDef.name || descriptor.pane.paneId;
2276
+ const items = descriptor.settingsDef.fields.map((field): ResultItem => {
2277
+ const currentValue = descriptor.context.settings[field.key];
2278
+ return {
2279
+ id: `pane-setting:${field.key}`,
2280
+ label: field.label,
2281
+ detail: summarizePaneSettingValue(field, currentValue),
2282
+ category,
2283
+ kind: "action",
2284
+ right: field.type,
2285
+ searchText: `${category} ${paneLabel} ${field.label} ${field.description || ""} ${field.type}`,
2286
+ action: () => activatePaneSettingField(descriptor.paneId, field, currentValue, options),
2287
+ };
2288
+ });
2289
+
2290
+ return query
2291
+ ? fuzzyFilter(items, query, (item) => `${item.label} ${item.detail} ${item.right || ""} ${item.searchText || ""}`)
2292
+ : items;
2293
+ }, [
2294
+ activatePaneSettingField,
2295
+ pluginRegistry,
2296
+ ]);
2297
+
2167
2298
  function readWorkflowTextareaValue(fieldId: string, fallback = ""): string {
2168
2299
  const ref = getInputRef(workflowInputRefs.current, fieldId).current;
2169
2300
  const nextValue = (ref as TextareaRenderable | null)?.editBuffer?.getText?.();
@@ -2446,7 +2577,7 @@ export function CommandBar({
2446
2577
  const argPlaceholder = template.shortcut?.argPlaceholder;
2447
2578
  return template.wizard.some((step) => step.type === "textarea" || step.key !== argPlaceholder);
2448
2579
  }
2449
- if (template.shortcut?.argPlaceholder === "ticker" || template.shortcut?.argPlaceholder === "tickers") {
2580
+ if (canPromptForPaneTemplateArg(template)) {
2450
2581
  return !arg?.trim();
2451
2582
  }
2452
2583
  return false;
@@ -2632,6 +2763,57 @@ export function CommandBar({
2632
2763
  }]
2633
2764
  ), [mapTickerSearchCandidateToResultItem]);
2634
2765
 
2766
+ const mergeTickerSearchResultItems = useCallback((
2767
+ query: string,
2768
+ preferredItems: ResultItem[],
2769
+ fallbackItems: ResultItem[],
2770
+ ): ResultItem[] => {
2771
+ const merged: ResultItem[] = [];
2772
+ const seen = new Set<string>();
2773
+ const addItem = (item: ResultItem) => {
2774
+ if (item.kind === "info") return;
2775
+ const key = `${item.label.trim().toUpperCase()}:${(item.right || "").trim().toUpperCase()}`;
2776
+ if (seen.has(key)) return;
2777
+ seen.add(key);
2778
+ merged.push(item);
2779
+ };
2780
+ preferredItems.forEach(addItem);
2781
+ fallbackItems.forEach(addItem);
2782
+ if (merged.length === 0) return fallbackItems;
2783
+
2784
+ return rankTickerSearchItems(merged, query)
2785
+ .map((item) => isExactTickerResultMatch(item, query) && item.category !== "Saved"
2786
+ ? { ...item, category: "Exact Match" }
2787
+ : item);
2788
+ }, []);
2789
+
2790
+ const mergePlainRootTickerResults = useCallback((
2791
+ query: string,
2792
+ providerItems: ResultItem[],
2793
+ rootItems: ResultItem[],
2794
+ ): ResultItem[] => {
2795
+ const merged: ResultItem[] = [];
2796
+ const seen = new Set<string>();
2797
+ const addItem = (item: ResultItem, options?: { skipInfo?: boolean }) => {
2798
+ if (options?.skipInfo && item.kind === "info") return;
2799
+ const key = (item.kind === "ticker" || item.kind === "search")
2800
+ ? `${item.kind}:${item.label.trim().toUpperCase()}:${(item.right || "").trim().toUpperCase()}`
2801
+ : `${item.kind}:${item.id}`;
2802
+ if (seen.has(key)) return;
2803
+ seen.add(key);
2804
+ merged.push(item);
2805
+ };
2806
+
2807
+ providerItems
2808
+ .filter((item) => item.category === "Exact Match" || isExactTickerResultMatch(item, query))
2809
+ .forEach((item) => addItem(item, { skipInfo: true }));
2810
+ rootItems.forEach((item) => addItem(item));
2811
+ providerItems
2812
+ .filter((item) => item.category !== "Exact Match" && !isExactTickerResultMatch(item, query))
2813
+ .forEach((item) => addItem(item, { skipInfo: true }));
2814
+ return merged.length > 0 ? merged : rootItems;
2815
+ }, []);
2816
+
2635
2817
  const buildTickerSearchCacheKey = useCallback((
2636
2818
  query: string,
2637
2819
  brokerId?: string | null,
@@ -2718,7 +2900,10 @@ export function CommandBar({
2718
2900
  activeCollectionId,
2719
2901
  }), [activeCollectionId, activeTickerSymbol, state.config, state.focusedPaneId]);
2720
2902
 
2721
- const getAvailablePaneTemplates = useCallback((options?: PaneTemplateCreateOptions): PaneTemplateDef[] => {
2903
+ const getAvailablePaneTemplates = useCallback((
2904
+ options?: PaneTemplateCreateOptions,
2905
+ availability?: { includePromptableTickerTemplates?: boolean },
2906
+ ): PaneTemplateDef[] => {
2722
2907
  const disabledPlugins = new Set(state.config.disabledPlugins || []);
2723
2908
  const context = getPaneTemplateContext();
2724
2909
  return [...pluginRegistry.paneTemplates.values()]
@@ -2727,7 +2912,12 @@ export function CommandBar({
2727
2912
  if (pluginId && disabledPlugins.has(pluginId)) return false;
2728
2913
  if (!template.canCreate) return true;
2729
2914
  try {
2730
- return template.canCreate(context, options);
2915
+ const canCreate = template.canCreate(context, options);
2916
+ if (canCreate) return true;
2917
+ return !!availability?.includePromptableTickerTemplates
2918
+ && !options?.arg
2919
+ && !context.activeTicker
2920
+ && canPromptForPaneTemplateArg(template);
2731
2921
  } catch (error) {
2732
2922
  commandBarLog.error("Pane template canCreate failed", {
2733
2923
  templateId: template.id,
@@ -2751,13 +2941,13 @@ export function CommandBar({
2751
2941
  const prefix = template.shortcut?.prefix?.toUpperCase();
2752
2942
  if (!prefix) return false;
2753
2943
  const arg = trimmed.slice(prefix.length).trim();
2754
- const argKind = template.shortcut?.argKind ?? template.shortcut?.argPlaceholder;
2944
+ const argKind = getPaneTemplateArgKind(template);
2755
2945
  if (upper !== prefix && (!argKind || !upper.startsWith(`${prefix} `))) return false;
2756
2946
  if (!template.canCreate) return true;
2757
2947
  try {
2758
2948
  const canCreate = template.canCreate(context, arg ? { arg } : undefined);
2759
2949
  if (canCreate) return true;
2760
- if (!arg && !context.activeTicker && (argKind === "ticker" || argKind === "ticker-list")) {
2950
+ if (!arg && !context.activeTicker && canPromptForPaneTemplateArg(template)) {
2761
2951
  return true;
2762
2952
  }
2763
2953
  return false;
@@ -2837,8 +3027,11 @@ export function CommandBar({
2837
3027
  const paneShortcutItems = useCallback((options?: {
2838
3028
  filterQuery?: string;
2839
3029
  createOptions?: PaneTemplateCreateOptions;
3030
+ includePromptableTickerTemplates?: boolean;
2840
3031
  }): ResultItem[] => {
2841
- const items = getAvailablePaneTemplates(options?.createOptions)
3032
+ const items = getAvailablePaneTemplates(options?.createOptions, {
3033
+ includePromptableTickerTemplates: options?.includePromptableTickerTemplates,
3034
+ })
2842
3035
  .filter((template) => template.shortcut)
2843
3036
  .map((template) => createPaneTemplateItem(template, {
2844
3037
  category: "Panes",
@@ -3128,7 +3321,7 @@ export function CommandBar({
3128
3321
  },
3129
3322
  };
3130
3323
  dispatch({ type: "SET_CONFIG", config: nextConfig });
3131
- void saveConfig(nextConfig);
3324
+ persistConfig(nextConfig);
3132
3325
  closeAll({ revertThemePreview: false });
3133
3326
  return;
3134
3327
  }
@@ -3217,6 +3410,7 @@ export function CommandBar({
3217
3410
 
3218
3411
  useEffect(() => {
3219
3412
  if (tickerSearchRouteQuery == null) {
3413
+ searchRequestIdRef.current += 1;
3220
3414
  setTickerSearchPending(false);
3221
3415
  setTickerSearchResults([]);
3222
3416
  if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
@@ -3225,6 +3419,7 @@ export function CommandBar({
3225
3419
 
3226
3420
  const searchQuery = tickerSearchRouteQuery.trim();
3227
3421
  if (!searchQuery) {
3422
+ searchRequestIdRef.current += 1;
3228
3423
  setTickerSearchPending(false);
3229
3424
  setTickerSearchResults([]);
3230
3425
  if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
@@ -3239,7 +3434,7 @@ export function CommandBar({
3239
3434
  activePortfolio?.brokerInstanceId,
3240
3435
  );
3241
3436
  setTickerSearchResults(cachedCandidates
3242
- ? buildTickerSearchResultItems(cachedCandidates, searchQuery)
3437
+ ? mergeTickerSearchResultItems(searchQuery, localItems, buildTickerSearchResultItems(cachedCandidates, searchQuery))
3243
3438
  : localItems);
3244
3439
  const requestId = ++searchRequestIdRef.current;
3245
3440
  const searchDelay = skipTickerSearchDebounceRef.current ? 0 : 200;
@@ -3266,7 +3461,11 @@ export function CommandBar({
3266
3461
  activePortfolio?.brokerId,
3267
3462
  activePortfolio?.brokerInstanceId,
3268
3463
  );
3269
- setTickerSearchResults(buildTickerSearchResultItems(combined, searchQuery));
3464
+ setTickerSearchResults(mergeTickerSearchResultItems(
3465
+ searchQuery,
3466
+ localTickerSearchResultItems(searchQuery, { limit: 6 }),
3467
+ buildTickerSearchResultItems(combined, searchQuery),
3468
+ ));
3270
3469
  } catch {
3271
3470
  if (requestId !== searchRequestIdRef.current) return;
3272
3471
  const nextItems: ResultItem[] = [{
@@ -3295,6 +3494,7 @@ export function CommandBar({
3295
3494
  buildTickerSearchResultItems,
3296
3495
  localTickerSearchResultItems,
3297
3496
  mapTickerSearchCandidateToResultItem,
3497
+ mergeTickerSearchResultItems,
3298
3498
  readTickerSearchCache,
3299
3499
  tickerSearchRouteQuery,
3300
3500
  writeTickerSearchCache,
@@ -3559,8 +3759,8 @@ export function CommandBar({
3559
3759
  label: "Type a ticker symbol",
3560
3760
  detail: "Open security details after resolving a ticker",
3561
3761
  category: "Search",
3562
- kind: "info",
3563
- action: () => {},
3762
+ kind: "command",
3763
+ action: () => openModeRoute("ticker-search", ""),
3564
3764
  });
3565
3765
  } else if (match.arg) {
3566
3766
  items.push(...localTickerSearchResultItems(match.arg, { limit: 6 }));
@@ -3610,7 +3810,9 @@ export function CommandBar({
3610
3810
  const allItems = [
3611
3811
  ...tickerItems,
3612
3812
  ...commandItems,
3613
- ...paneShortcutItems(),
3813
+ ...buildLayoutItems("", { confirmDangerousActions: true }),
3814
+ ...buildPaneSettingItems(state.focusedPaneId, rootQuery),
3815
+ ...paneShortcutItems({ includePromptableTickerTemplates: true }),
3614
3816
  ...nonShortcutPaneTemplateItems(),
3615
3817
  ...tickerActionItems(),
3616
3818
  ...pluginCommandItems(),
@@ -3625,6 +3827,7 @@ export function CommandBar({
3625
3827
  activeTickerSymbol,
3626
3828
  availableCommands,
3627
3829
  buildLayoutItems,
3830
+ buildPaneSettingItems,
3628
3831
  buildPluginItems,
3629
3832
  createQuickLookLocalTickerCandidates,
3630
3833
  createPluginCommandItem,
@@ -3644,6 +3847,18 @@ export function CommandBar({
3644
3847
  tickerActionItems,
3645
3848
  ]);
3646
3849
 
3850
+ const rootPlainTickerSearchArg = useMemo(() => {
3851
+ if (activeMatch || rootShortcutIntent.kind !== "none" || !isLikelyPlainTickerSearch(rootQuery)) return null;
3852
+ const normalizedQuery = normalizeCommandTickerSearchText(rootQuery);
3853
+ const hasExactNonTickerResult = rootResultModel.items.some((item) => (
3854
+ item.kind !== "ticker"
3855
+ && item.kind !== "search"
3856
+ && normalizeCommandTickerSearchText(item.label) === normalizedQuery
3857
+ ));
3858
+ return hasExactNonTickerResult ? null : rootQuery.trim();
3859
+ }, [activeMatch, rootQuery, rootResultModel.items, rootShortcutIntent.kind]);
3860
+ const rootTickerSearchArg = rootSecurityDescriptionArg ?? rootPlainTickerSearchArg;
3861
+
3647
3862
  useEffect(() => {
3648
3863
  if (currentRoute) return;
3649
3864
 
@@ -3668,6 +3883,7 @@ export function CommandBar({
3668
3883
 
3669
3884
  useEffect(() => {
3670
3885
  if (currentRoute) {
3886
+ rootSearchRequestIdRef.current += 1;
3671
3887
  setRootSearching(false);
3672
3888
  setRootProviderResults(null);
3673
3889
  setRootProviderResultsQuery(null);
@@ -3676,7 +3892,8 @@ export function CommandBar({
3676
3892
  return;
3677
3893
  }
3678
3894
 
3679
- if (!rootSecurityDescriptionArg) {
3895
+ if (!rootTickerSearchArg) {
3896
+ rootSearchRequestIdRef.current += 1;
3680
3897
  setRootSearching(false);
3681
3898
  setRootProviderResults(null);
3682
3899
  setRootProviderResultsQuery(null);
@@ -3685,7 +3902,7 @@ export function CommandBar({
3685
3902
  return;
3686
3903
  }
3687
3904
 
3688
- const searchQuery = rootSecurityDescriptionArg;
3905
+ const searchQuery = rootTickerSearchArg;
3689
3906
  if (rootSearchTimerRef.current) clearTimeout(rootSearchTimerRef.current);
3690
3907
  if (rootLastSearchedQueryRef.current === searchQuery) {
3691
3908
  return;
@@ -3699,7 +3916,10 @@ export function CommandBar({
3699
3916
  activeSearchPortfolio?.brokerId,
3700
3917
  activeSearchPortfolio?.brokerInstanceId,
3701
3918
  );
3702
- setRootProviderResults(cachedCandidates ? buildTickerSearchResultItems(cachedCandidates, searchQuery) : null);
3919
+ const localItems = localTickerSearchResultItems(searchQuery, { limit: 6 });
3920
+ setRootProviderResults(cachedCandidates
3921
+ ? mergeTickerSearchResultItems(searchQuery, localItems, buildTickerSearchResultItems(cachedCandidates, searchQuery))
3922
+ : null);
3703
3923
  setRootProviderResultsQuery(cachedCandidates ? searchQuery : null);
3704
3924
 
3705
3925
  const requestId = ++rootSearchRequestIdRef.current;
@@ -3723,7 +3943,11 @@ export function CommandBar({
3723
3943
  activeSearchPortfolio?.brokerId,
3724
3944
  activeSearchPortfolio?.brokerInstanceId,
3725
3945
  );
3726
- setRootProviderResults(buildTickerSearchResultItems(combined, searchQuery));
3946
+ setRootProviderResults(mergeTickerSearchResultItems(
3947
+ searchQuery,
3948
+ localTickerSearchResultItems(searchQuery, { limit: 6 }),
3949
+ buildTickerSearchResultItems(combined, searchQuery),
3950
+ ));
3727
3951
  setRootProviderResultsQuery(searchQuery);
3728
3952
  } catch {
3729
3953
  if (requestId !== rootSearchRequestIdRef.current) return;
@@ -3751,20 +3975,50 @@ export function CommandBar({
3751
3975
  buildTickerSearchResultItems,
3752
3976
  currentRoute,
3753
3977
  dataProvider,
3978
+ localTickerSearchResultItems,
3979
+ mergeTickerSearchResultItems,
3754
3980
  readTickerSearchCache,
3755
- rootSecurityDescriptionArg,
3981
+ rootTickerSearchArg,
3756
3982
  state.config.portfolios,
3757
3983
  state.tickers,
3758
3984
  writeTickerSearchCache,
3759
3985
  ]);
3760
3986
 
3761
3987
  const rootResults = useMemo(() => {
3762
- if (rootSecurityDescriptionArg && rootProviderResultsQuery === rootSecurityDescriptionArg && rootProviderResults) {
3988
+ if (rootTickerSearchArg && rootProviderResultsQuery === rootTickerSearchArg && rootProviderResults) {
3989
+ if (rootPlainTickerSearchArg) {
3990
+ return mergePlainRootTickerResults(rootPlainTickerSearchArg, rootProviderResults, rootResultModel.items);
3991
+ }
3763
3992
  return rootProviderResults;
3764
3993
  }
3765
3994
  return rootResultModel.items;
3766
- }, [rootProviderResults, rootProviderResultsQuery, rootResultModel.items, rootSecurityDescriptionArg]);
3767
- const orderedRootResults = useMemo(() => orderListResults(rootResults), [rootResults]);
3995
+ }, [
3996
+ mergePlainRootTickerResults,
3997
+ rootPlainTickerSearchArg,
3998
+ rootProviderResults,
3999
+ rootProviderResultsQuery,
4000
+ rootResultModel.items,
4001
+ rootTickerSearchArg,
4002
+ ]);
4003
+ const rootSectionOrder = rootPlainTickerSearchArg ? "app-first" : "default";
4004
+ const orderedRootResults = useMemo(
4005
+ () => orderListResults(rootResults, { sectionOrder: rootSectionOrder }),
4006
+ [rootResults, rootSectionOrder],
4007
+ );
4008
+ const activeRootProviderResultsKey = useMemo(() => {
4009
+ if (!rootTickerSearchArg || rootProviderResultsQuery !== rootTickerSearchArg || !rootProviderResults) return null;
4010
+ return [
4011
+ rootTickerSearchArg,
4012
+ ...rootProviderResults.map((item) => `${item.id}:${item.category}:${item.label}:${item.right || ""}`),
4013
+ ].join("\n");
4014
+ }, [rootProviderResults, rootProviderResultsQuery, rootTickerSearchArg]);
4015
+
4016
+ useEffect(() => {
4017
+ if (!activeRootProviderResultsKey) return;
4018
+ setRootSelectedIdx(0);
4019
+ setRootHoveredIdx(null);
4020
+ nativeListScrollRef.current?.scrollTo(0);
4021
+ }, [activeRootProviderResultsKey]);
3768
4022
 
3769
4023
  const rootGhostCompletion = !currentRoute && rootShortcutIntent.kind === "inferred-complete"
3770
4024
  ? rootShortcutIntent.completionQuery
@@ -3776,7 +4030,7 @@ export function CommandBar({
3776
4030
  if (currentRoute || rootShortcutIntent.kind === "none") return null;
3777
4031
 
3778
4032
  if (rootShortcutIntent.source === "pane-template") {
3779
- const argKind = rootShortcutIntent.template.shortcut?.argKind ?? rootShortcutIntent.template.shortcut?.argPlaceholder;
4033
+ const argKind = getPaneTemplateArgKind(rootShortcutIntent.template);
3780
4034
  if (argKind === "ticker") {
3781
4035
  const symbol = normalizeTickerInput(activeTickerSymbol, rootShortcutIntent.argText);
3782
4036
  if (symbol) {
@@ -3792,9 +4046,9 @@ export function CommandBar({
3792
4046
  }
3793
4047
  const inferred = normalizeTickerInput(activeTickerSymbol, undefined);
3794
4048
  if (inferred) {
3795
- return `Shortcut: ${rootShortcutIntent.label} · inferred ${inferred} · Tab to accept`;
4049
+ return `Shortcut: ${rootShortcutIntent.label} for ${inferred} · Tab to accept`;
3796
4050
  }
3797
- return `Shortcut: ${rootShortcutIntent.label} · Enter tickers to compare`;
4051
+ return `Shortcut: ${rootShortcutIntent.label} · Enter tickers`;
3798
4052
  }
3799
4053
  return `Shortcut: ${rootShortcutIntent.label}`;
3800
4054
  }
@@ -3809,8 +4063,8 @@ export function CommandBar({
3809
4063
  const symbol = normalizeTickerInput(activeTickerSymbol, rootShortcutIntent.argText);
3810
4064
  if (symbol) {
3811
4065
  return rootShortcutIntent.kind === "inferred-complete"
3812
- ? `Shortcut: DES ${symbol} · Tab to accept`
3813
- : `Shortcut: DES ${symbol}`;
4066
+ ? `Shortcut: ${rootShortcutIntent.prefix} ${symbol} · Tab to accept`
4067
+ : `Shortcut: ${rootShortcutIntent.prefix} ${symbol}`;
3814
4068
  }
3815
4069
  return "Shortcut: Description";
3816
4070
  }
@@ -3865,7 +4119,7 @@ export function CommandBar({
3865
4119
  return true;
3866
4120
  }
3867
4121
  if (intent.source === "pane-template") {
3868
- const argKind = intent.template.shortcut?.argKind ?? intent.template.shortcut?.argPlaceholder;
4122
+ const argKind = getPaneTemplateArgKind(intent.template);
3869
4123
  if (argKind === "ticker") {
3870
4124
  openModeRoute("ticker-search", intent.argText, {
3871
4125
  action: "pane-template",
@@ -3959,7 +4213,7 @@ export function CommandBar({
3959
4213
  }
3960
4214
  return {
3961
4215
  id: `security-description:${match.arg}`,
3962
- label: `DES ${match.arg.toUpperCase()}`,
4216
+ label: `${match.prefix} ${match.arg.toUpperCase()}`,
3963
4217
  detail: "Open security details or resolve the ticker",
3964
4218
  category: "Search",
3965
4219
  kind: "command",
@@ -4066,6 +4320,7 @@ export function CommandBar({
4066
4320
  emptyDetail: emptyState.detail,
4067
4321
  footerLeft: getScreenFooterLeft(null),
4068
4322
  footerRight: getScreenFooterRight(null),
4323
+ sectionOrder: rootSectionOrder,
4069
4324
  };
4070
4325
  }
4071
4326
 
@@ -4162,21 +4417,7 @@ export function CommandBar({
4162
4417
  if (currentRoute.kind === "pane-settings") {
4163
4418
  const descriptor = pluginRegistry.resolvePaneSettings(currentRoute.paneId);
4164
4419
  if (!descriptor) return null;
4165
- const items = descriptor.settingsDef.fields.map((field) => {
4166
- const currentValue = descriptor.context.settings[field.key];
4167
- return {
4168
- id: `pane-setting:${field.key}`,
4169
- label: field.label,
4170
- detail: summarizePaneSettingValue(field, currentValue),
4171
- category: descriptor.settingsDef.title || "Pane Settings",
4172
- kind: "action" as const,
4173
- right: field.type,
4174
- action: () => {},
4175
- };
4176
- });
4177
- const filtered = currentRoute.query
4178
- ? fuzzyFilter(items, currentRoute.query, (item) => `${item.label} ${item.detail} ${item.right || ""}`)
4179
- : items;
4420
+ const filtered = buildPaneSettingItems(currentRoute.paneId, currentRoute.query, { keepRouteOpen: true });
4180
4421
  return {
4181
4422
  kind: "pane-settings",
4182
4423
  title: descriptor.settingsDef.title || "Pane Settings",
@@ -4201,6 +4442,7 @@ export function CommandBar({
4201
4442
  activeTickerData,
4202
4443
  activeTickerSymbol,
4203
4444
  buildLayoutItems,
4445
+ buildPaneSettingItems,
4204
4446
  buildPluginItems,
4205
4447
  closeAll,
4206
4448
  createPaneTemplateItem,
@@ -4226,6 +4468,7 @@ export function CommandBar({
4226
4468
  rootModeInfo.kind,
4227
4469
  rootQuery,
4228
4470
  orderedRootResults,
4471
+ rootSectionOrder,
4229
4472
  rootSearching,
4230
4473
  rootSelectedIdx,
4231
4474
  rootShortcutIntent,
@@ -4648,40 +4891,7 @@ export function CommandBar({
4648
4891
  }
4649
4892
 
4650
4893
  if (currentRoute?.kind === "pane-settings") {
4651
- const descriptor = pluginRegistry.resolvePaneSettings(currentRoute.paneId);
4652
- if (!descriptor) return;
4653
- const selectedField = descriptor.settingsDef.fields.find((field) => `pane-setting:${field.key}` === selected.id);
4654
- if (!selectedField) return;
4655
- const currentValue = descriptor.context.settings[selectedField.key];
4656
- const normalized = normalizePaneSettingField(currentRoute.paneId, selectedField, currentValue);
4657
- if (normalized.mode === "toggle") {
4658
- updateTopRoute((route) => route.kind === "pane-settings"
4659
- ? { ...route, pendingFieldKey: selectedField.key, error: null }
4660
- : route);
4661
- void pluginRegistry.applyPaneSettingValueFn(currentRoute.paneId, selectedField, !normalized.value)
4662
- .then(() => {
4663
- updateTopRoute((route) => route.kind === "pane-settings"
4664
- ? { ...route, pendingFieldKey: null, error: null }
4665
- : route);
4666
- })
4667
- .catch((error) => {
4668
- updateTopRoute((route) => route.kind === "pane-settings"
4669
- ? {
4670
- ...route,
4671
- pendingFieldKey: null,
4672
- error: error instanceof Error ? error.message : "Could not apply that setting.",
4673
- }
4674
- : route);
4675
- });
4676
- return;
4677
- }
4678
- if (normalized.mode === "workflow" && normalized.route) {
4679
- openWorkflowRoute(normalized.route);
4680
- return;
4681
- }
4682
- if (normalized.mode === "picker") {
4683
- openPickerRoute(normalized.route);
4684
- }
4894
+ void selected.action();
4685
4895
  return;
4686
4896
  }
4687
4897
 
@@ -4689,10 +4899,7 @@ export function CommandBar({
4689
4899
  }, [
4690
4900
  closeAll,
4691
4901
  currentRoute,
4692
- normalizePaneSettingField,
4693
4902
  openInlineConfirm,
4694
- openPickerRoute,
4695
- openWorkflowRoute,
4696
4903
  persistLayoutChange,
4697
4904
  pluginRegistry,
4698
4905
  resolveImmediateRootSelection,
@@ -4842,343 +5049,277 @@ export function CommandBar({
4842
5049
  }
4843
5050
  }, [currentRoute, pluginRegistry, updateTopRoute, updateWorkflowValue]);
4844
5051
 
4845
- useEffect(() => {
4846
- const keyInput = renderer.keyInput as typeof renderer.keyInput & {
4847
- onInternal?: (event: "keypress", handler: (event: {
4848
- name: string;
4849
- sequence?: string;
4850
- ctrl?: boolean;
4851
- option?: boolean;
4852
- meta?: boolean;
4853
- shift?: boolean;
4854
- stopPropagation: () => void;
4855
- preventDefault: () => void;
4856
- }) => void) => void;
4857
- offInternal?: (event: "keypress", handler: (event: {
4858
- name: string;
4859
- sequence?: string;
4860
- ctrl?: boolean;
4861
- option?: boolean;
4862
- meta?: boolean;
4863
- shift?: boolean;
4864
- stopPropagation: () => void;
4865
- preventDefault: () => void;
4866
- }) => void) => void;
4867
- };
4868
- if (!keyInput) return undefined;
4869
- const handleKeyPress = (event: {
4870
- name: string;
4871
- sequence?: string;
4872
- ctrl?: boolean;
4873
- option?: boolean;
4874
- meta?: boolean;
4875
- shift?: boolean;
4876
- stopPropagation: () => void;
4877
- preventDefault: () => void;
4878
- }) => {
4879
- if (event.name === "escape" || event.name === "`") {
5052
+ useShortcut((event) => {
5053
+ if (event.name === "escape" || event.name === "`") {
5054
+ event.stopPropagation();
5055
+ event.preventDefault();
5056
+ dismissCommandBar();
5057
+ return;
5058
+ }
5059
+
5060
+ if (currentRoute?.kind === "confirm") {
5061
+ if (isPlainBackspace(event)) {
4880
5062
  event.stopPropagation();
4881
5063
  event.preventDefault();
4882
- dismissCommandBar();
5064
+ popRoute();
4883
5065
  return;
4884
5066
  }
4885
-
4886
- if (currentRoute?.kind === "confirm") {
4887
- if (isPlainBackspace(event)) {
4888
- event.stopPropagation();
4889
- event.preventDefault();
4890
- popRoute();
4891
- return;
4892
- }
4893
- if (event.name === "return" || event.name === "enter" || event.name === "y") {
4894
- event.stopPropagation();
4895
- event.preventDefault();
4896
- void confirmCurrentRoute();
4897
- return;
4898
- }
4899
- if (event.name === "n") {
4900
- event.stopPropagation();
4901
- event.preventDefault();
4902
- popRoute();
4903
- }
5067
+ if (event.name === "return" || event.name === "enter" || event.name === "y") {
5068
+ event.stopPropagation();
5069
+ event.preventDefault();
5070
+ void confirmCurrentRoute();
4904
5071
  return;
4905
5072
  }
4906
-
4907
- if (
4908
- currentRoute
4909
- && (currentRoute.kind === "mode"
4910
- || currentRoute.kind === "picker"
4911
- || currentRoute.kind === "pane-settings")
4912
- && isPlainBackspace(event)
4913
- && currentRoute.query.length === 0
4914
- ) {
5073
+ if (event.name === "n") {
4915
5074
  event.stopPropagation();
4916
5075
  event.preventDefault();
4917
5076
  popRoute();
4918
- return;
4919
5077
  }
5078
+ return;
5079
+ }
4920
5080
 
4921
- if (currentRoute?.kind === "workflow") {
4922
- const visibleFields = getVisibleWorkflowFields(currentRoute.fields, currentRoute.values);
4923
- const activeField = visibleFields.find((field) => field.id === currentRoute.activeFieldId) ?? visibleFields[0];
4924
- const activeTextarea = activeField?.type === "textarea";
4925
-
4926
- if (isPlainBackspace(event)) {
4927
- const activeValue = activeField
4928
- ? getWorkflowFieldStringValue(activeField, currentRoute.values[activeField.id])
4929
- : "";
4930
- if (!activeField || !isWorkflowTextField(activeField) || activeValue.length === 0) {
4931
- event.stopPropagation();
4932
- event.preventDefault();
4933
- popRoute();
4934
- return;
4935
- }
4936
- }
4937
-
4938
- if (event.name === "tab") {
4939
- event.stopPropagation();
4940
- event.preventDefault();
4941
- moveWorkflowFocus(event.shift ? -1 : 1);
4942
- return;
4943
- }
4944
-
4945
- if (activeTextarea && event.ctrl && event.name === "s") {
4946
- event.stopPropagation();
4947
- event.preventDefault();
4948
- void submitWorkflowRoute(currentRoute);
4949
- return;
4950
- }
5081
+ if (
5082
+ currentRoute
5083
+ && (currentRoute.kind === "mode"
5084
+ || currentRoute.kind === "picker"
5085
+ || currentRoute.kind === "pane-settings")
5086
+ && isPlainBackspace(event)
5087
+ && currentRoute.query.length === 0
5088
+ ) {
5089
+ event.stopPropagation();
5090
+ event.preventDefault();
5091
+ popRoute();
5092
+ return;
5093
+ }
4951
5094
 
4952
- if (activeTextarea && (event.name === "up" || event.name === "down" || (event.ctrl && (event.name === "p" || event.name === "n")))) {
4953
- return;
4954
- }
5095
+ if (currentRoute?.kind === "workflow") {
5096
+ const visibleFields = getVisibleWorkflowFields(currentRoute.fields, currentRoute.values);
5097
+ const activeField = visibleFields.find((field) => field.id === currentRoute.activeFieldId) ?? visibleFields[0];
5098
+ const activeTextarea = activeField?.type === "textarea";
4955
5099
 
4956
- if (event.name === "up" || (event.ctrl && event.name === "p")) {
5100
+ if (isPlainBackspace(event)) {
5101
+ const activeValue = activeField
5102
+ ? getWorkflowFieldStringValue(activeField, currentRoute.values[activeField.id])
5103
+ : "";
5104
+ if (!activeField || !isWorkflowTextField(activeField) || activeValue.length === 0) {
4957
5105
  event.stopPropagation();
4958
5106
  event.preventDefault();
4959
- moveWorkflowFocus(-1);
5107
+ popRoute();
4960
5108
  return;
4961
5109
  }
5110
+ }
4962
5111
 
4963
- if (event.name === "down" || (event.ctrl && event.name === "n")) {
4964
- event.stopPropagation();
4965
- event.preventDefault();
4966
- moveWorkflowFocus(1);
4967
- return;
4968
- }
5112
+ if (event.name === "tab") {
5113
+ event.stopPropagation();
5114
+ event.preventDefault();
5115
+ moveWorkflowFocus(event.shift ? -1 : 1);
5116
+ return;
5117
+ }
4969
5118
 
4970
- if (event.name === "space" && activeField?.type === "toggle") {
4971
- event.stopPropagation();
4972
- event.preventDefault();
4973
- updateWorkflowValue(activeField.id, !coerceFieldBoolean(currentRoute.values[activeField.id]));
4974
- return;
4975
- }
5119
+ if (activeTextarea && event.ctrl && event.name === "s") {
5120
+ event.stopPropagation();
5121
+ event.preventDefault();
5122
+ void submitWorkflowRoute(currentRoute);
5123
+ return;
5124
+ }
4976
5125
 
4977
- if (
4978
- event.name === "return"
4979
- || event.name === "enter"
4980
- || (nativePaneChrome && event.name === "space" && activeField?.type === "select")
4981
- ) {
4982
- if (!activeField) return;
4983
- if (activeField.type === "select" || activeField.type === "multi-select" || activeField.type === "ordered-multi-select" || activeField.type === "toggle") {
4984
- event.stopPropagation();
4985
- event.preventDefault();
4986
- if (nativePaneChrome && activeField.type === "select") {
4987
- openNativeSelect(workflowNativeSelectRefs.current.get(activeField.id));
4988
- return;
4989
- }
4990
- openWorkflowFieldPicker(currentRoute, activeField);
4991
- return;
4992
- }
4993
- }
5126
+ if (activeTextarea && (event.name === "up" || event.name === "down" || (event.ctrl && (event.name === "p" || event.name === "n")))) {
5127
+ return;
5128
+ }
4994
5129
 
5130
+ if (event.name === "up" || (event.ctrl && event.name === "p")) {
5131
+ event.stopPropagation();
5132
+ event.preventDefault();
5133
+ moveWorkflowFocus(-1);
4995
5134
  return;
4996
5135
  }
4997
5136
 
4998
- if (currentRoute?.kind === "picker") {
4999
- if (event.name === "up" || (event.ctrl && event.name === "p")) {
5000
- event.stopPropagation();
5001
- event.preventDefault();
5002
- moveListSelection(-1);
5003
- return;
5004
- }
5005
- if (event.name === "down" || (event.ctrl && event.name === "n")) {
5006
- event.stopPropagation();
5007
- event.preventDefault();
5008
- moveListSelection(1);
5009
- return;
5010
- }
5011
- if (currentRoute.pickerId === "field-multi-select" && (event.name === "space" || event.sequence === " ")) {
5012
- event.stopPropagation();
5013
- event.preventDefault();
5014
- const listState = visibleListStateRef.current;
5015
- const selected = listState?.results[listState.selectedIdx];
5016
- if (!selected) return;
5017
- handleMultiSelectToggle(selected.id);
5018
- return;
5019
- }
5020
- if (currentRoute.pickerId === "field-multi-select" && event.name === "[") {
5021
- event.stopPropagation();
5022
- event.preventDefault();
5023
- handleMultiSelectMove("up");
5024
- return;
5025
- }
5026
- if (currentRoute.pickerId === "field-multi-select" && event.name === "]") {
5027
- event.stopPropagation();
5028
- event.preventDefault();
5029
- handleMultiSelectMove("down");
5030
- return;
5031
- }
5032
- if (event.name === "return" || event.name === "enter") {
5033
- event.stopPropagation();
5034
- event.preventDefault();
5035
- if (currentRoute.pickerId === "field-multi-select") {
5036
- commitMultiSelectPicker();
5037
- return;
5038
- }
5039
- activateListSelection();
5040
- }
5137
+ if (event.name === "down" || (event.ctrl && event.name === "n")) {
5138
+ event.stopPropagation();
5139
+ event.preventDefault();
5140
+ moveWorkflowFocus(1);
5041
5141
  return;
5042
5142
  }
5043
5143
 
5044
- if (currentRoute?.kind === "pane-settings") {
5045
- if (event.name === "up" || (event.ctrl && event.name === "p")) {
5046
- event.stopPropagation();
5047
- event.preventDefault();
5048
- moveListSelection(-1);
5049
- return;
5050
- }
5051
- if (event.name === "down" || (event.ctrl && event.name === "n")) {
5052
- event.stopPropagation();
5053
- event.preventDefault();
5054
- moveListSelection(1);
5055
- return;
5056
- }
5057
- if (event.name === "return" || event.name === "enter" || event.name === "space") {
5058
- event.stopPropagation();
5059
- event.preventDefault();
5060
- activateListSelection();
5061
- }
5144
+ if (event.name === "space" && activeField?.type === "toggle") {
5145
+ event.stopPropagation();
5146
+ event.preventDefault();
5147
+ updateWorkflowValue(activeField.id, !coerceFieldBoolean(currentRoute.values[activeField.id]));
5062
5148
  return;
5063
5149
  }
5064
5150
 
5065
- if (themePickerActive) {
5066
- if (event.name === "up" || (event.ctrl && event.name === "p")) {
5067
- event.stopPropagation();
5068
- event.preventDefault();
5069
- themePickerRef.current?.move(-1);
5070
- return;
5071
- }
5072
- if (event.name === "down" || (event.ctrl && event.name === "n")) {
5073
- event.stopPropagation();
5074
- event.preventDefault();
5075
- themePickerRef.current?.move(1);
5076
- return;
5077
- }
5078
- if (event.name === "return" || event.name === "enter") {
5151
+ if (
5152
+ event.name === "return"
5153
+ || event.name === "enter"
5154
+ || (nativePaneChrome && event.name === "space" && activeField?.type === "select")
5155
+ ) {
5156
+ if (!activeField) return;
5157
+ if (activeField.type === "select" || activeField.type === "multi-select" || activeField.type === "ordered-multi-select" || activeField.type === "toggle") {
5079
5158
  event.stopPropagation();
5080
5159
  event.preventDefault();
5081
- themePickerRef.current?.commit();
5160
+ if (nativePaneChrome && activeField.type === "select") {
5161
+ openNativeSelect(workflowNativeSelectRefs.current.get(activeField.id));
5162
+ return;
5163
+ }
5164
+ openWorkflowFieldPicker(currentRoute, activeField);
5082
5165
  return;
5083
5166
  }
5084
5167
  }
5085
5168
 
5086
- const activeListState = visibleListStateRef.current;
5087
- if (!activeListState) return;
5169
+ return;
5170
+ }
5088
5171
 
5089
- if (!currentRoute && event.name === "tab") {
5090
- if (acceptRootShortcutTab()) {
5091
- event.stopPropagation();
5092
- event.preventDefault();
5093
- return;
5094
- }
5172
+ if (currentRoute?.kind === "picker") {
5173
+ if (event.name === "up" || (event.ctrl && event.name === "p")) {
5174
+ event.stopPropagation();
5175
+ event.preventDefault();
5176
+ moveListSelection(-1);
5177
+ return;
5095
5178
  }
5096
-
5097
5179
  if (event.name === "down" || (event.ctrl && event.name === "n")) {
5098
5180
  event.stopPropagation();
5099
5181
  event.preventDefault();
5100
5182
  moveListSelection(1);
5101
5183
  return;
5102
5184
  }
5185
+ if (currentRoute.pickerId === "field-multi-select" && (event.name === "space" || event.sequence === " ")) {
5186
+ event.stopPropagation();
5187
+ event.preventDefault();
5188
+ const listState = visibleListStateRef.current;
5189
+ const selected = listState?.results[listState.selectedIdx];
5190
+ if (!selected) return;
5191
+ handleMultiSelectToggle(selected.id);
5192
+ return;
5193
+ }
5194
+ if (currentRoute.pickerId === "field-multi-select" && event.name === "[") {
5195
+ event.stopPropagation();
5196
+ event.preventDefault();
5197
+ handleMultiSelectMove("up");
5198
+ return;
5199
+ }
5200
+ if (currentRoute.pickerId === "field-multi-select" && event.name === "]") {
5201
+ event.stopPropagation();
5202
+ event.preventDefault();
5203
+ handleMultiSelectMove("down");
5204
+ return;
5205
+ }
5206
+ if (event.name === "return" || event.name === "enter") {
5207
+ event.stopPropagation();
5208
+ event.preventDefault();
5209
+ if (currentRoute.pickerId === "field-multi-select") {
5210
+ commitMultiSelectPicker();
5211
+ return;
5212
+ }
5213
+ activateListSelection();
5214
+ }
5215
+ return;
5216
+ }
5103
5217
 
5218
+ if (currentRoute?.kind === "pane-settings") {
5104
5219
  if (event.name === "up" || (event.ctrl && event.name === "p")) {
5105
5220
  event.stopPropagation();
5106
5221
  event.preventDefault();
5107
5222
  moveListSelection(-1);
5108
5223
  return;
5109
5224
  }
5110
-
5111
- if ((event.meta && (event.name === "backspace" || event.name === "delete")) || (event.ctrl && event.name === "u")) {
5225
+ if (event.name === "down" || (event.ctrl && event.name === "n")) {
5112
5226
  event.stopPropagation();
5113
5227
  event.preventDefault();
5114
- setActiveListQuery("");
5228
+ moveListSelection(1);
5115
5229
  return;
5116
5230
  }
5231
+ if (event.name === "return" || event.name === "enter" || event.name === "space") {
5232
+ event.stopPropagation();
5233
+ event.preventDefault();
5234
+ activateListSelection();
5235
+ }
5236
+ return;
5237
+ }
5117
5238
 
5118
- if ((event.ctrl && event.name === "w") || (event.meta && (event.name === "h" || event.name === "u"))) {
5239
+ if (themePickerActive) {
5240
+ if (event.name === "up" || (event.ctrl && event.name === "p")) {
5119
5241
  event.stopPropagation();
5120
5242
  event.preventDefault();
5121
- const trimmed = activeListState.query.replace(/\s+$/, "");
5122
- const nextQuery = trimmed.replace(/[^\s]+$/, "").replace(/\s+$/, "");
5123
- setActiveListQuery(nextQuery);
5243
+ themePickerRef.current?.move(-1);
5124
5244
  return;
5125
5245
  }
5126
-
5127
- const pluginToggleMode = (currentRoute?.kind === "mode" && currentRoute.screen === "plugins")
5128
- || (!currentRoute && rootModeInfo.kind === "plugins");
5129
- if (pluginToggleMode && event.name === "space") {
5246
+ if (event.name === "down" || (event.ctrl && event.name === "n")) {
5130
5247
  event.stopPropagation();
5131
5248
  event.preventDefault();
5132
- const selected = activeListState.results[activeListState.selectedIdx];
5133
- if (selected?.pluginToggle) {
5134
- void selected.pluginToggle();
5135
- }
5249
+ themePickerRef.current?.move(1);
5136
5250
  return;
5137
5251
  }
5138
-
5139
5252
  if (event.name === "return" || event.name === "enter") {
5140
5253
  event.stopPropagation();
5141
5254
  event.preventDefault();
5142
- if (event.shift) {
5143
- activateListSelection({ secondary: true });
5144
- return;
5145
- }
5146
- activateListSelection();
5255
+ themePickerRef.current?.commit();
5256
+ return;
5147
5257
  }
5148
- };
5258
+ }
5149
5259
 
5150
- if (keyInput.onInternal) {
5151
- keyInput.onInternal("keypress", handleKeyPress);
5152
- } else {
5153
- keyInput.on("keypress", handleKeyPress);
5260
+ const activeListState = visibleListStateRef.current;
5261
+ if (!activeListState) return;
5262
+
5263
+ if (!currentRoute && event.name === "tab") {
5264
+ if (acceptRootShortcutTab()) {
5265
+ event.stopPropagation();
5266
+ event.preventDefault();
5267
+ return;
5268
+ }
5154
5269
  }
5155
- return () => {
5156
- if (keyInput.offInternal) {
5157
- keyInput.offInternal("keypress", handleKeyPress);
5158
- } else {
5159
- keyInput.off("keypress", handleKeyPress);
5270
+
5271
+ if (event.name === "down" || (event.ctrl && event.name === "n")) {
5272
+ event.stopPropagation();
5273
+ event.preventDefault();
5274
+ moveListSelection(1);
5275
+ return;
5276
+ }
5277
+
5278
+ if (event.name === "up" || (event.ctrl && event.name === "p")) {
5279
+ event.stopPropagation();
5280
+ event.preventDefault();
5281
+ moveListSelection(-1);
5282
+ return;
5283
+ }
5284
+
5285
+ if ((event.meta && (event.name === "backspace" || event.name === "delete")) || (event.ctrl && event.name === "u")) {
5286
+ event.stopPropagation();
5287
+ event.preventDefault();
5288
+ setActiveListQuery("");
5289
+ return;
5290
+ }
5291
+
5292
+ if ((event.ctrl && event.name === "w") || (event.meta && (event.name === "h" || event.name === "u"))) {
5293
+ event.stopPropagation();
5294
+ event.preventDefault();
5295
+ const trimmed = activeListState.query.replace(/\s+$/, "");
5296
+ const nextQuery = trimmed.replace(/[^\s]+$/, "").replace(/\s+$/, "");
5297
+ setActiveListQuery(nextQuery);
5298
+ return;
5299
+ }
5300
+
5301
+ const pluginToggleMode = (currentRoute?.kind === "mode" && currentRoute.screen === "plugins")
5302
+ || (!currentRoute && rootModeInfo.kind === "plugins");
5303
+ if (pluginToggleMode && event.name === "space") {
5304
+ event.stopPropagation();
5305
+ event.preventDefault();
5306
+ const selected = activeListState.results[activeListState.selectedIdx];
5307
+ if (selected?.pluginToggle) {
5308
+ void selected.pluginToggle();
5160
5309
  }
5161
- };
5162
- }, [
5163
- activateListSelection,
5164
- acceptRootShortcutTab,
5165
- commitMultiSelectPicker,
5166
- confirmCurrentRoute,
5167
- currentRoute,
5168
- dismissCommandBar,
5169
- handleMultiSelectMove,
5170
- handleMultiSelectToggle,
5171
- moveListSelection,
5172
- moveWorkflowFocus,
5173
- nativePaneChrome,
5174
- openWorkflowFieldPicker,
5175
- popRoute,
5176
- renderer,
5177
- rootModeInfo.kind,
5178
- setActiveListQuery,
5179
- themePickerActive,
5180
- updateWorkflowValue,
5181
- ]);
5310
+ return;
5311
+ }
5312
+
5313
+ if (event.name === "return" || event.name === "enter") {
5314
+ event.stopPropagation();
5315
+ event.preventDefault();
5316
+ if (event.shift) {
5317
+ activateListSelection({ secondary: true });
5318
+ return;
5319
+ }
5320
+ activateListSelection();
5321
+ }
5322
+ }, { phase: "before" });
5182
5323
 
5183
5324
  const barWidth = nativePaneChrome
5184
5325
  ? Math.max(46, Math.min(78, termWidth - 10, Math.floor(termWidth * 0.64)))
@@ -5221,8 +5362,18 @@ export function CommandBar({
5221
5362
  Math.max(7, estimateWorkflowBodyRows(currentRoute)),
5222
5363
  )
5223
5364
  : baseBodyHeight;
5224
- const bodyHeight = currentRoute?.kind === "workflow" ? workflowBodyHeight : baseBodyHeight;
5225
- const listBodyHeight = baseBodyHeight;
5365
+ const shouldUseCompactListHeight = nativePaneChrome
5366
+ && visibleListState != null
5367
+ && !themePickerActive
5368
+ && !showCustomMultiSelectPicker;
5369
+ const listBodyHeight = shouldUseCompactListHeight
5370
+ ? Math.min(baseBodyHeight, Math.max(1, nativeListRows.length))
5371
+ : baseBodyHeight;
5372
+ const bodyHeight = currentRoute?.kind === "workflow"
5373
+ ? workflowBodyHeight
5374
+ : shouldUseCompactListHeight
5375
+ ? listBodyHeight
5376
+ : baseBodyHeight;
5226
5377
  const nativePanelPaddingColumns = nativePaneChrome
5227
5378
  ? Math.ceil((NATIVE_COMMAND_BAR_PADDING_X_PX * 2) / Math.max(1, cellWidthPx))
5228
5379
  : 0;
@@ -5233,7 +5384,7 @@ export function CommandBar({
5233
5384
  || currentRoute?.kind === "confirm"
5234
5385
  || showCustomMultiSelectPicker)
5235
5386
  ? 1
5236
- : 3;
5387
+ : currentRoute ? 3 : 2;
5237
5388
  const barHeight = nativePaneChrome
5238
5389
  ? bodyHeight + nativeBodyChromeRows + nativePanelPaddingRows
5239
5390
  : bodyHeight + 7;
@@ -5578,7 +5729,7 @@ export function CommandBar({
5578
5729
  data-gloom-role="command-bar-panel"
5579
5730
  style={nativePaneChrome ? {
5580
5731
  borderRadius: 8,
5581
- boxShadow: "0 14px 32px rgba(0,0,0,0.18)",
5732
+ boxShadow: `0 14px 32px ${blendHex(commandBarBg(), colors.bg, 0.18)}`,
5582
5733
  overflow: "hidden",
5583
5734
  padding: `${NATIVE_COMMAND_BAR_PADDING_Y_PX}px ${NATIVE_COMMAND_BAR_PADDING_X_PX}px`,
5584
5735
  } : undefined}
@@ -5669,15 +5820,15 @@ export function CommandBar({
5669
5820
  paletteText={paletteText}
5670
5821
  panelBg={panelBg}
5671
5822
  onPreview={applyThemePreview}
5672
- onCommit={(themeId) => {
5673
- const nextConfig = {
5674
- ...stateRef.current.config,
5675
- theme: themeId,
5676
- };
5677
- commitTheme(themeId);
5678
- void saveConfig(nextConfig);
5679
- closeAll({ revertThemePreview: false });
5680
- }}
5823
+ onCommit={(themeId) => {
5824
+ const nextConfig = {
5825
+ ...stateRef.current.config,
5826
+ theme: themeId,
5827
+ };
5828
+ commitTheme(themeId);
5829
+ persistConfig(nextConfig);
5830
+ closeAll({ revertThemePreview: false });
5831
+ }}
5681
5832
  />
5682
5833
  )}
5683
5834