gloomberb 0.7.1 → 0.7.3

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 (131) 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 +49 -15
  7. package/src/components/chart/chart-types.ts +2 -2
  8. package/src/components/chart/chart.test.ts +27 -8
  9. package/src/components/chart/comparison-stock-chart.tsx +84 -121
  10. package/src/components/chart/native/chart-rasterizer.ts +43 -17
  11. package/src/components/chart/price-axis-labels.tsx +25 -9
  12. package/src/components/chart/stock-chart-renderer-switch.test.tsx +1 -1
  13. package/src/components/chart/stock-chart.tsx +59 -41
  14. package/src/components/command-bar/command-bar.test.tsx +446 -120
  15. package/src/components/command-bar/command-bar.tsx +528 -383
  16. package/src/components/command-bar/command-registry.ts +16 -6
  17. package/src/components/command-bar/root-shortcuts.ts +4 -5
  18. package/src/components/command-bar/view-model.test.ts +1 -0
  19. package/src/components/command-bar/view-model.ts +2 -1
  20. package/src/components/command-bar/workflow-ops.ts +11 -32
  21. package/src/components/data-table-view.test.tsx +24 -1
  22. package/src/components/data-table-view.tsx +3 -2
  23. package/src/components/feed-data-table-stack-view.tsx +3 -2
  24. package/src/components/layout/detached-pane-shell.tsx +2 -2
  25. package/src/components/layout/floating-pane.tsx +9 -2
  26. package/src/components/layout/pane-footer.tsx +1 -1
  27. package/src/components/layout/pane-header.tsx +4 -2
  28. package/src/components/layout/pane-sizing.ts +12 -0
  29. package/src/components/layout/pane.tsx +24 -2
  30. package/src/components/layout/shell.tsx +24 -10
  31. package/src/components/layout/status-bar.test.tsx +4 -1
  32. package/src/components/layout/status-bar.tsx +4 -24
  33. package/src/components/markdown-text.tsx +53 -8
  34. package/src/components/onboarding/onboarding-wizard.tsx +7 -6
  35. package/src/components/pane-settings-dialog.tsx +9 -6
  36. package/src/components/pane-template-wizard.tsx +3 -2
  37. package/src/components/price-selector-dialog.tsx +3 -2
  38. package/src/components/price-sparkline-view.tsx +463 -0
  39. package/src/components/price-sparkline.ts +46 -0
  40. package/src/components/table-view-shared.tsx +47 -7
  41. package/src/components/ticker-list-table-view.tsx +3 -2
  42. package/src/components/ticker-list-table.tsx +11 -51
  43. package/src/components/toggle-list.tsx +3 -3
  44. package/src/components/ui/choice-dialog.tsx +3 -2
  45. package/src/components/ui/multi-select-dialog.tsx +3 -2
  46. package/src/components/ui/native-select.tsx +3 -3
  47. package/src/core/state/app-state.ts +210 -80
  48. package/src/data/config-store-node.ts +113 -25
  49. package/src/data/config-store.test.ts +65 -3
  50. package/src/data/plugin-state-store.ts +39 -13
  51. package/src/data/sqlite-cache.test.ts +13 -0
  52. package/src/market-data/coordinator.ts +8 -1
  53. package/src/news/aggregator.test.ts +41 -0
  54. package/src/news/aggregator.ts +61 -9
  55. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
  56. package/src/plugins/builtin/ai/settings.ts +3 -0
  57. package/src/plugins/builtin/broker-manager/index.tsx +4 -3
  58. package/src/plugins/builtin/chat-controller.test.ts +398 -17
  59. package/src/plugins/builtin/chat-controller.ts +258 -44
  60. package/src/plugins/builtin/chat.test.tsx +257 -6
  61. package/src/plugins/builtin/chat.tsx +225 -60
  62. package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
  63. package/src/plugins/builtin/correlation/colors.test.ts +39 -0
  64. package/src/plugins/builtin/correlation/colors.ts +87 -0
  65. package/src/plugins/builtin/correlation/index.test.tsx +151 -0
  66. package/src/plugins/builtin/correlation/index.tsx +92 -21
  67. package/src/plugins/builtin/debug.tsx +4 -3
  68. package/src/plugins/builtin/econ/index.tsx +3 -2
  69. package/src/plugins/builtin/help.test.tsx +2 -2
  70. package/src/plugins/builtin/holders/index.tsx +25 -13
  71. package/src/plugins/builtin/layout-manager.tsx +1 -7
  72. package/src/plugins/builtin/news-wire/index.tsx +1 -1
  73. package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
  74. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
  75. package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
  76. package/src/plugins/builtin/options.tsx +9 -17
  77. package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
  78. package/src/plugins/builtin/portfolio-list/metrics.ts +11 -0
  79. package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
  80. package/src/plugins/builtin/portfolio-list/pane.tsx +117 -18
  81. package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
  82. package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
  83. package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
  84. package/src/plugins/builtin/portfolio-list.test.tsx +282 -2
  85. package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
  86. package/src/plugins/builtin/sectors/index.tsx +252 -41
  87. package/src/plugins/builtin/sectors/sector-data.ts +41 -1
  88. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
  89. package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
  90. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
  91. package/src/plugins/builtin/ticker-detail/pane.tsx +0 -6
  92. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +442 -63
  93. package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
  94. package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
  95. package/src/plugins/ibkr/trading-pane.tsx +3 -2
  96. package/src/plugins/plugin-runtime.tsx +2 -2
  97. package/src/plugins/prediction-markets/settings.ts +4 -0
  98. package/src/plugins/registry.test.ts +55 -0
  99. package/src/plugins/registry.ts +11 -4
  100. package/src/plugins/ticker-navigation.test.ts +35 -5
  101. package/src/plugins/ticker-navigation.ts +16 -22
  102. package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
  103. package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
  104. package/src/renderers/electrobun/bun/application-menu.ts +6 -1
  105. package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
  106. package/src/renderers/electrobun/bun/index.ts +52 -13
  107. package/src/renderers/electrobun/view/app-services.ts +51 -9
  108. package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
  109. package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
  110. package/src/renderers/electrobun/view/main.tsx +0 -16
  111. package/src/renderers/electrobun/view/styles.css +23 -21
  112. package/src/renderers/electrobun/view/ui-host.tsx +67 -11
  113. package/src/renderers/opentui/host.tsx +2 -1
  114. package/src/renderers/opentui/start.tsx +1 -1
  115. package/src/renderers/opentui/test-utils.tsx +6 -5
  116. package/src/state/app-bootstrap.ts +3 -3
  117. package/src/state/app-context.test.ts +113 -0
  118. package/src/state/app-context.tsx +8 -4
  119. package/src/theme/colors.ts +13 -14
  120. package/src/types/config.ts +9 -4
  121. package/src/types/plugin.ts +8 -1
  122. package/src/ui/context-menu.tsx +4 -4
  123. package/src/ui/host.tsx +8 -2
  124. package/src/ui/index.tsx +1 -0
  125. package/src/utils/api-client.test.ts +128 -0
  126. package/src/utils/api-client.ts +112 -7
  127. package/src/utils/fuzzy-search.ts +12 -1
  128. package/src/utils/keyboard.ts +31 -0
  129. package/src/utils/ticker-search.test.ts +2 -2
  130. package/src/utils/ticker-search.ts +48 -1
  131. package/src/version.ts +1 -1
@@ -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,46 @@ 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]);
3995
+ }, [
3996
+ mergePlainRootTickerResults,
3997
+ rootPlainTickerSearchArg,
3998
+ rootProviderResults,
3999
+ rootProviderResultsQuery,
4000
+ rootResultModel.items,
4001
+ rootTickerSearchArg,
4002
+ ]);
3767
4003
  const orderedRootResults = useMemo(() => orderListResults(rootResults), [rootResults]);
4004
+ const activeRootProviderResultsKey = useMemo(() => {
4005
+ if (!rootTickerSearchArg || rootProviderResultsQuery !== rootTickerSearchArg || !rootProviderResults) return null;
4006
+ return [
4007
+ rootTickerSearchArg,
4008
+ ...rootProviderResults.map((item) => `${item.id}:${item.category}:${item.label}:${item.right || ""}`),
4009
+ ].join("\n");
4010
+ }, [rootProviderResults, rootProviderResultsQuery, rootTickerSearchArg]);
4011
+
4012
+ useEffect(() => {
4013
+ if (!activeRootProviderResultsKey) return;
4014
+ setRootSelectedIdx(0);
4015
+ setRootHoveredIdx(null);
4016
+ nativeListScrollRef.current?.scrollTo(0);
4017
+ }, [activeRootProviderResultsKey]);
3768
4018
 
3769
4019
  const rootGhostCompletion = !currentRoute && rootShortcutIntent.kind === "inferred-complete"
3770
4020
  ? rootShortcutIntent.completionQuery
@@ -3776,7 +4026,7 @@ export function CommandBar({
3776
4026
  if (currentRoute || rootShortcutIntent.kind === "none") return null;
3777
4027
 
3778
4028
  if (rootShortcutIntent.source === "pane-template") {
3779
- const argKind = rootShortcutIntent.template.shortcut?.argKind ?? rootShortcutIntent.template.shortcut?.argPlaceholder;
4029
+ const argKind = getPaneTemplateArgKind(rootShortcutIntent.template);
3780
4030
  if (argKind === "ticker") {
3781
4031
  const symbol = normalizeTickerInput(activeTickerSymbol, rootShortcutIntent.argText);
3782
4032
  if (symbol) {
@@ -3792,9 +4042,9 @@ export function CommandBar({
3792
4042
  }
3793
4043
  const inferred = normalizeTickerInput(activeTickerSymbol, undefined);
3794
4044
  if (inferred) {
3795
- return `Shortcut: ${rootShortcutIntent.label} · inferred ${inferred} · Tab to accept`;
4045
+ return `Shortcut: ${rootShortcutIntent.label} for ${inferred} · Tab to accept`;
3796
4046
  }
3797
- return `Shortcut: ${rootShortcutIntent.label} · Enter tickers to compare`;
4047
+ return `Shortcut: ${rootShortcutIntent.label} · Enter tickers`;
3798
4048
  }
3799
4049
  return `Shortcut: ${rootShortcutIntent.label}`;
3800
4050
  }
@@ -3809,8 +4059,8 @@ export function CommandBar({
3809
4059
  const symbol = normalizeTickerInput(activeTickerSymbol, rootShortcutIntent.argText);
3810
4060
  if (symbol) {
3811
4061
  return rootShortcutIntent.kind === "inferred-complete"
3812
- ? `Shortcut: DES ${symbol} · Tab to accept`
3813
- : `Shortcut: DES ${symbol}`;
4062
+ ? `Shortcut: ${rootShortcutIntent.prefix} ${symbol} · Tab to accept`
4063
+ : `Shortcut: ${rootShortcutIntent.prefix} ${symbol}`;
3814
4064
  }
3815
4065
  return "Shortcut: Description";
3816
4066
  }
@@ -3865,7 +4115,7 @@ export function CommandBar({
3865
4115
  return true;
3866
4116
  }
3867
4117
  if (intent.source === "pane-template") {
3868
- const argKind = intent.template.shortcut?.argKind ?? intent.template.shortcut?.argPlaceholder;
4118
+ const argKind = getPaneTemplateArgKind(intent.template);
3869
4119
  if (argKind === "ticker") {
3870
4120
  openModeRoute("ticker-search", intent.argText, {
3871
4121
  action: "pane-template",
@@ -3959,7 +4209,7 @@ export function CommandBar({
3959
4209
  }
3960
4210
  return {
3961
4211
  id: `security-description:${match.arg}`,
3962
- label: `DES ${match.arg.toUpperCase()}`,
4212
+ label: `${match.prefix} ${match.arg.toUpperCase()}`,
3963
4213
  detail: "Open security details or resolve the ticker",
3964
4214
  category: "Search",
3965
4215
  kind: "command",
@@ -4162,21 +4412,7 @@ export function CommandBar({
4162
4412
  if (currentRoute.kind === "pane-settings") {
4163
4413
  const descriptor = pluginRegistry.resolvePaneSettings(currentRoute.paneId);
4164
4414
  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;
4415
+ const filtered = buildPaneSettingItems(currentRoute.paneId, currentRoute.query, { keepRouteOpen: true });
4180
4416
  return {
4181
4417
  kind: "pane-settings",
4182
4418
  title: descriptor.settingsDef.title || "Pane Settings",
@@ -4201,6 +4437,7 @@ export function CommandBar({
4201
4437
  activeTickerData,
4202
4438
  activeTickerSymbol,
4203
4439
  buildLayoutItems,
4440
+ buildPaneSettingItems,
4204
4441
  buildPluginItems,
4205
4442
  closeAll,
4206
4443
  createPaneTemplateItem,
@@ -4648,40 +4885,7 @@ export function CommandBar({
4648
4885
  }
4649
4886
 
4650
4887
  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
- }
4888
+ void selected.action();
4685
4889
  return;
4686
4890
  }
4687
4891
 
@@ -4689,10 +4893,7 @@ export function CommandBar({
4689
4893
  }, [
4690
4894
  closeAll,
4691
4895
  currentRoute,
4692
- normalizePaneSettingField,
4693
4896
  openInlineConfirm,
4694
- openPickerRoute,
4695
- openWorkflowRoute,
4696
4897
  persistLayoutChange,
4697
4898
  pluginRegistry,
4698
4899
  resolveImmediateRootSelection,
@@ -4842,343 +5043,277 @@ export function CommandBar({
4842
5043
  }
4843
5044
  }, [currentRoute, pluginRegistry, updateTopRoute, updateWorkflowValue]);
4844
5045
 
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 === "`") {
5046
+ useShortcut((event) => {
5047
+ if (event.name === "escape" || event.name === "`") {
5048
+ event.stopPropagation();
5049
+ event.preventDefault();
5050
+ dismissCommandBar();
5051
+ return;
5052
+ }
5053
+
5054
+ if (currentRoute?.kind === "confirm") {
5055
+ if (isPlainBackspace(event)) {
4880
5056
  event.stopPropagation();
4881
5057
  event.preventDefault();
4882
- dismissCommandBar();
5058
+ popRoute();
4883
5059
  return;
4884
5060
  }
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
- }
5061
+ if (event.name === "return" || event.name === "enter" || event.name === "y") {
5062
+ event.stopPropagation();
5063
+ event.preventDefault();
5064
+ void confirmCurrentRoute();
4904
5065
  return;
4905
5066
  }
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
- ) {
5067
+ if (event.name === "n") {
4915
5068
  event.stopPropagation();
4916
5069
  event.preventDefault();
4917
5070
  popRoute();
4918
- return;
4919
5071
  }
5072
+ return;
5073
+ }
4920
5074
 
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
- }
5075
+ if (
5076
+ currentRoute
5077
+ && (currentRoute.kind === "mode"
5078
+ || currentRoute.kind === "picker"
5079
+ || currentRoute.kind === "pane-settings")
5080
+ && isPlainBackspace(event)
5081
+ && currentRoute.query.length === 0
5082
+ ) {
5083
+ event.stopPropagation();
5084
+ event.preventDefault();
5085
+ popRoute();
5086
+ return;
5087
+ }
4951
5088
 
4952
- if (activeTextarea && (event.name === "up" || event.name === "down" || (event.ctrl && (event.name === "p" || event.name === "n")))) {
4953
- return;
4954
- }
5089
+ if (currentRoute?.kind === "workflow") {
5090
+ const visibleFields = getVisibleWorkflowFields(currentRoute.fields, currentRoute.values);
5091
+ const activeField = visibleFields.find((field) => field.id === currentRoute.activeFieldId) ?? visibleFields[0];
5092
+ const activeTextarea = activeField?.type === "textarea";
4955
5093
 
4956
- if (event.name === "up" || (event.ctrl && event.name === "p")) {
5094
+ if (isPlainBackspace(event)) {
5095
+ const activeValue = activeField
5096
+ ? getWorkflowFieldStringValue(activeField, currentRoute.values[activeField.id])
5097
+ : "";
5098
+ if (!activeField || !isWorkflowTextField(activeField) || activeValue.length === 0) {
4957
5099
  event.stopPropagation();
4958
5100
  event.preventDefault();
4959
- moveWorkflowFocus(-1);
5101
+ popRoute();
4960
5102
  return;
4961
5103
  }
5104
+ }
4962
5105
 
4963
- if (event.name === "down" || (event.ctrl && event.name === "n")) {
4964
- event.stopPropagation();
4965
- event.preventDefault();
4966
- moveWorkflowFocus(1);
4967
- return;
4968
- }
5106
+ if (event.name === "tab") {
5107
+ event.stopPropagation();
5108
+ event.preventDefault();
5109
+ moveWorkflowFocus(event.shift ? -1 : 1);
5110
+ return;
5111
+ }
4969
5112
 
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
- }
5113
+ if (activeTextarea && event.ctrl && event.name === "s") {
5114
+ event.stopPropagation();
5115
+ event.preventDefault();
5116
+ void submitWorkflowRoute(currentRoute);
5117
+ return;
5118
+ }
4976
5119
 
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
- }
5120
+ if (activeTextarea && (event.name === "up" || event.name === "down" || (event.ctrl && (event.name === "p" || event.name === "n")))) {
5121
+ return;
5122
+ }
4994
5123
 
5124
+ if (event.name === "up" || (event.ctrl && event.name === "p")) {
5125
+ event.stopPropagation();
5126
+ event.preventDefault();
5127
+ moveWorkflowFocus(-1);
4995
5128
  return;
4996
5129
  }
4997
5130
 
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
- }
5131
+ if (event.name === "down" || (event.ctrl && event.name === "n")) {
5132
+ event.stopPropagation();
5133
+ event.preventDefault();
5134
+ moveWorkflowFocus(1);
5041
5135
  return;
5042
5136
  }
5043
5137
 
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
- }
5138
+ if (event.name === "space" && activeField?.type === "toggle") {
5139
+ event.stopPropagation();
5140
+ event.preventDefault();
5141
+ updateWorkflowValue(activeField.id, !coerceFieldBoolean(currentRoute.values[activeField.id]));
5062
5142
  return;
5063
5143
  }
5064
5144
 
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") {
5145
+ if (
5146
+ event.name === "return"
5147
+ || event.name === "enter"
5148
+ || (nativePaneChrome && event.name === "space" && activeField?.type === "select")
5149
+ ) {
5150
+ if (!activeField) return;
5151
+ if (activeField.type === "select" || activeField.type === "multi-select" || activeField.type === "ordered-multi-select" || activeField.type === "toggle") {
5079
5152
  event.stopPropagation();
5080
5153
  event.preventDefault();
5081
- themePickerRef.current?.commit();
5154
+ if (nativePaneChrome && activeField.type === "select") {
5155
+ openNativeSelect(workflowNativeSelectRefs.current.get(activeField.id));
5156
+ return;
5157
+ }
5158
+ openWorkflowFieldPicker(currentRoute, activeField);
5082
5159
  return;
5083
5160
  }
5084
5161
  }
5085
5162
 
5086
- const activeListState = visibleListStateRef.current;
5087
- if (!activeListState) return;
5163
+ return;
5164
+ }
5088
5165
 
5089
- if (!currentRoute && event.name === "tab") {
5090
- if (acceptRootShortcutTab()) {
5091
- event.stopPropagation();
5092
- event.preventDefault();
5093
- return;
5094
- }
5166
+ if (currentRoute?.kind === "picker") {
5167
+ if (event.name === "up" || (event.ctrl && event.name === "p")) {
5168
+ event.stopPropagation();
5169
+ event.preventDefault();
5170
+ moveListSelection(-1);
5171
+ return;
5095
5172
  }
5096
-
5097
5173
  if (event.name === "down" || (event.ctrl && event.name === "n")) {
5098
5174
  event.stopPropagation();
5099
5175
  event.preventDefault();
5100
5176
  moveListSelection(1);
5101
5177
  return;
5102
5178
  }
5179
+ if (currentRoute.pickerId === "field-multi-select" && (event.name === "space" || event.sequence === " ")) {
5180
+ event.stopPropagation();
5181
+ event.preventDefault();
5182
+ const listState = visibleListStateRef.current;
5183
+ const selected = listState?.results[listState.selectedIdx];
5184
+ if (!selected) return;
5185
+ handleMultiSelectToggle(selected.id);
5186
+ return;
5187
+ }
5188
+ if (currentRoute.pickerId === "field-multi-select" && event.name === "[") {
5189
+ event.stopPropagation();
5190
+ event.preventDefault();
5191
+ handleMultiSelectMove("up");
5192
+ return;
5193
+ }
5194
+ if (currentRoute.pickerId === "field-multi-select" && event.name === "]") {
5195
+ event.stopPropagation();
5196
+ event.preventDefault();
5197
+ handleMultiSelectMove("down");
5198
+ return;
5199
+ }
5200
+ if (event.name === "return" || event.name === "enter") {
5201
+ event.stopPropagation();
5202
+ event.preventDefault();
5203
+ if (currentRoute.pickerId === "field-multi-select") {
5204
+ commitMultiSelectPicker();
5205
+ return;
5206
+ }
5207
+ activateListSelection();
5208
+ }
5209
+ return;
5210
+ }
5103
5211
 
5212
+ if (currentRoute?.kind === "pane-settings") {
5104
5213
  if (event.name === "up" || (event.ctrl && event.name === "p")) {
5105
5214
  event.stopPropagation();
5106
5215
  event.preventDefault();
5107
5216
  moveListSelection(-1);
5108
5217
  return;
5109
5218
  }
5110
-
5111
- if ((event.meta && (event.name === "backspace" || event.name === "delete")) || (event.ctrl && event.name === "u")) {
5219
+ if (event.name === "down" || (event.ctrl && event.name === "n")) {
5112
5220
  event.stopPropagation();
5113
5221
  event.preventDefault();
5114
- setActiveListQuery("");
5222
+ moveListSelection(1);
5115
5223
  return;
5116
5224
  }
5225
+ if (event.name === "return" || event.name === "enter" || event.name === "space") {
5226
+ event.stopPropagation();
5227
+ event.preventDefault();
5228
+ activateListSelection();
5229
+ }
5230
+ return;
5231
+ }
5117
5232
 
5118
- if ((event.ctrl && event.name === "w") || (event.meta && (event.name === "h" || event.name === "u"))) {
5233
+ if (themePickerActive) {
5234
+ if (event.name === "up" || (event.ctrl && event.name === "p")) {
5119
5235
  event.stopPropagation();
5120
5236
  event.preventDefault();
5121
- const trimmed = activeListState.query.replace(/\s+$/, "");
5122
- const nextQuery = trimmed.replace(/[^\s]+$/, "").replace(/\s+$/, "");
5123
- setActiveListQuery(nextQuery);
5237
+ themePickerRef.current?.move(-1);
5124
5238
  return;
5125
5239
  }
5126
-
5127
- const pluginToggleMode = (currentRoute?.kind === "mode" && currentRoute.screen === "plugins")
5128
- || (!currentRoute && rootModeInfo.kind === "plugins");
5129
- if (pluginToggleMode && event.name === "space") {
5240
+ if (event.name === "down" || (event.ctrl && event.name === "n")) {
5130
5241
  event.stopPropagation();
5131
5242
  event.preventDefault();
5132
- const selected = activeListState.results[activeListState.selectedIdx];
5133
- if (selected?.pluginToggle) {
5134
- void selected.pluginToggle();
5135
- }
5243
+ themePickerRef.current?.move(1);
5136
5244
  return;
5137
5245
  }
5138
-
5139
5246
  if (event.name === "return" || event.name === "enter") {
5140
5247
  event.stopPropagation();
5141
5248
  event.preventDefault();
5142
- if (event.shift) {
5143
- activateListSelection({ secondary: true });
5144
- return;
5145
- }
5146
- activateListSelection();
5249
+ themePickerRef.current?.commit();
5250
+ return;
5147
5251
  }
5148
- };
5252
+ }
5149
5253
 
5150
- if (keyInput.onInternal) {
5151
- keyInput.onInternal("keypress", handleKeyPress);
5152
- } else {
5153
- keyInput.on("keypress", handleKeyPress);
5254
+ const activeListState = visibleListStateRef.current;
5255
+ if (!activeListState) return;
5256
+
5257
+ if (!currentRoute && event.name === "tab") {
5258
+ if (acceptRootShortcutTab()) {
5259
+ event.stopPropagation();
5260
+ event.preventDefault();
5261
+ return;
5262
+ }
5154
5263
  }
5155
- return () => {
5156
- if (keyInput.offInternal) {
5157
- keyInput.offInternal("keypress", handleKeyPress);
5158
- } else {
5159
- keyInput.off("keypress", handleKeyPress);
5264
+
5265
+ if (event.name === "down" || (event.ctrl && event.name === "n")) {
5266
+ event.stopPropagation();
5267
+ event.preventDefault();
5268
+ moveListSelection(1);
5269
+ return;
5270
+ }
5271
+
5272
+ if (event.name === "up" || (event.ctrl && event.name === "p")) {
5273
+ event.stopPropagation();
5274
+ event.preventDefault();
5275
+ moveListSelection(-1);
5276
+ return;
5277
+ }
5278
+
5279
+ if ((event.meta && (event.name === "backspace" || event.name === "delete")) || (event.ctrl && event.name === "u")) {
5280
+ event.stopPropagation();
5281
+ event.preventDefault();
5282
+ setActiveListQuery("");
5283
+ return;
5284
+ }
5285
+
5286
+ if ((event.ctrl && event.name === "w") || (event.meta && (event.name === "h" || event.name === "u"))) {
5287
+ event.stopPropagation();
5288
+ event.preventDefault();
5289
+ const trimmed = activeListState.query.replace(/\s+$/, "");
5290
+ const nextQuery = trimmed.replace(/[^\s]+$/, "").replace(/\s+$/, "");
5291
+ setActiveListQuery(nextQuery);
5292
+ return;
5293
+ }
5294
+
5295
+ const pluginToggleMode = (currentRoute?.kind === "mode" && currentRoute.screen === "plugins")
5296
+ || (!currentRoute && rootModeInfo.kind === "plugins");
5297
+ if (pluginToggleMode && event.name === "space") {
5298
+ event.stopPropagation();
5299
+ event.preventDefault();
5300
+ const selected = activeListState.results[activeListState.selectedIdx];
5301
+ if (selected?.pluginToggle) {
5302
+ void selected.pluginToggle();
5160
5303
  }
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
- ]);
5304
+ return;
5305
+ }
5306
+
5307
+ if (event.name === "return" || event.name === "enter") {
5308
+ event.stopPropagation();
5309
+ event.preventDefault();
5310
+ if (event.shift) {
5311
+ activateListSelection({ secondary: true });
5312
+ return;
5313
+ }
5314
+ activateListSelection();
5315
+ }
5316
+ }, { phase: "before" });
5182
5317
 
5183
5318
  const barWidth = nativePaneChrome
5184
5319
  ? Math.max(46, Math.min(78, termWidth - 10, Math.floor(termWidth * 0.64)))
@@ -5221,8 +5356,18 @@ export function CommandBar({
5221
5356
  Math.max(7, estimateWorkflowBodyRows(currentRoute)),
5222
5357
  )
5223
5358
  : baseBodyHeight;
5224
- const bodyHeight = currentRoute?.kind === "workflow" ? workflowBodyHeight : baseBodyHeight;
5225
- const listBodyHeight = baseBodyHeight;
5359
+ const shouldUseCompactListHeight = nativePaneChrome
5360
+ && visibleListState != null
5361
+ && !themePickerActive
5362
+ && !showCustomMultiSelectPicker;
5363
+ const listBodyHeight = shouldUseCompactListHeight
5364
+ ? Math.min(baseBodyHeight, Math.max(1, nativeListRows.length))
5365
+ : baseBodyHeight;
5366
+ const bodyHeight = currentRoute?.kind === "workflow"
5367
+ ? workflowBodyHeight
5368
+ : shouldUseCompactListHeight
5369
+ ? listBodyHeight
5370
+ : baseBodyHeight;
5226
5371
  const nativePanelPaddingColumns = nativePaneChrome
5227
5372
  ? Math.ceil((NATIVE_COMMAND_BAR_PADDING_X_PX * 2) / Math.max(1, cellWidthPx))
5228
5373
  : 0;
@@ -5233,7 +5378,7 @@ export function CommandBar({
5233
5378
  || currentRoute?.kind === "confirm"
5234
5379
  || showCustomMultiSelectPicker)
5235
5380
  ? 1
5236
- : 3;
5381
+ : currentRoute ? 3 : 2;
5237
5382
  const barHeight = nativePaneChrome
5238
5383
  ? bodyHeight + nativeBodyChromeRows + nativePanelPaddingRows
5239
5384
  : bodyHeight + 7;
@@ -5578,7 +5723,7 @@ export function CommandBar({
5578
5723
  data-gloom-role="command-bar-panel"
5579
5724
  style={nativePaneChrome ? {
5580
5725
  borderRadius: 8,
5581
- boxShadow: "0 14px 32px rgba(0,0,0,0.18)",
5726
+ boxShadow: `0 14px 32px ${blendHex(commandBarBg(), colors.bg, 0.18)}`,
5582
5727
  overflow: "hidden",
5583
5728
  padding: `${NATIVE_COMMAND_BAR_PADDING_Y_PX}px ${NATIVE_COMMAND_BAR_PADDING_X_PX}px`,
5584
5729
  } : undefined}
@@ -5669,15 +5814,15 @@ export function CommandBar({
5669
5814
  paletteText={paletteText}
5670
5815
  panelBg={panelBg}
5671
5816
  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
- }}
5817
+ onCommit={(themeId) => {
5818
+ const nextConfig = {
5819
+ ...stateRef.current.config,
5820
+ theme: themeId,
5821
+ };
5822
+ commitTheme(themeId);
5823
+ persistConfig(nextConfig);
5824
+ closeAll({ revertThemePreview: false });
5825
+ }}
5681
5826
  />
5682
5827
  )}
5683
5828