gloomberb 0.9.2 → 0.9.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 (111) hide show
  1. package/README.md +23 -2
  2. package/bin/gloomberb +1 -1
  3. package/package.json +1 -1
  4. package/src/app.tsx +10 -7
  5. package/src/capabilities/registry.ts +1 -0
  6. package/src/capabilities/types.ts +23 -0
  7. package/src/cli/commands/api.ts +119 -0
  8. package/src/cli/commands/automation.ts +173 -0
  9. package/src/cli/commands/command-utils.ts +53 -0
  10. package/src/cli/commands/market.ts +527 -0
  11. package/src/cli/commands/overview.ts +222 -0
  12. package/src/cli/commands/search.ts +39 -0
  13. package/src/cli/commands/system.ts +553 -0
  14. package/src/cli/commands/ticker.ts +104 -12
  15. package/src/cli/context.ts +35 -2
  16. package/src/cli/entry.ts +51 -0
  17. package/src/cli/errors.ts +56 -5
  18. package/src/cli/index.ts +73 -11
  19. package/src/cli/options.test.ts +38 -0
  20. package/src/cli/options.ts +95 -0
  21. package/src/cli/pane-functions/index.ts +10 -1
  22. package/src/cli/registry.test.ts +40 -0
  23. package/src/cli/registry.ts +46 -5
  24. package/src/cli/result.test.ts +82 -0
  25. package/src/cli/result.ts +149 -0
  26. package/src/cli/types.ts +11 -0
  27. package/src/cli.test.ts +11 -11
  28. package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
  29. package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
  30. package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
  31. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
  32. package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
  33. package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
  34. package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
  35. package/src/components/chart/core/pane-settings.test.ts +27 -0
  36. package/src/components/chart/core/pane-settings.ts +60 -1
  37. package/src/components/chart/core/pointer.ts +2 -0
  38. package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
  39. package/src/components/chart/stock-chart/settings.ts +7 -3
  40. package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
  41. package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
  42. package/src/components/command-bar/surface/pane.test.tsx +76 -0
  43. package/src/components/data-table/view.tsx +13 -2
  44. package/src/components/feed-data-table/stack-view.tsx +9 -1
  45. package/src/components/index.ts +1 -1
  46. package/src/components/input-search-bar.test.tsx +103 -0
  47. package/src/components/input-search-bar.tsx +16 -0
  48. package/src/components/layout/shell/active-drag.ts +5 -1
  49. package/src/components/layout/shell/drag/runtime.ts +21 -3
  50. package/src/components/layout/shell/fullscreen.ts +10 -0
  51. package/src/components/layout/shell/index.test.tsx +395 -2
  52. package/src/components/layout/shell/index.tsx +128 -2
  53. package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
  54. package/src/components/layout/shell/pane/layers.tsx +26 -14
  55. package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
  56. package/src/components/layout/shell/shortcuts.ts +4 -0
  57. package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
  58. package/src/components/layout/shell/window-mode/index.ts +11 -3
  59. package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
  60. package/src/components/layout/status-bar.test.tsx +105 -0
  61. package/src/components/layout/status-bar.tsx +32 -6
  62. package/src/components/layout/transient-layout.tsx +57 -0
  63. package/src/components/layout/window-edit/mode.test.ts +76 -5
  64. package/src/components/layout/window-edit/mode.ts +47 -2
  65. package/src/components/layout/window-edit/presentation.ts +2 -1
  66. package/src/components/price-performance.tsx +9 -1
  67. package/src/core/app-services.ts +7 -4
  68. package/src/data/config/store/index.ts +1 -2
  69. package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
  70. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
  71. package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
  72. package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
  73. package/src/plugins/builtin/news/wire/read-state.ts +9 -32
  74. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
  75. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
  76. package/src/plugins/builtin/portfolio-list/pane/index.tsx +1 -1
  77. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
  78. package/src/plugins/builtin/shared/read-state.ts +43 -0
  79. package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
  80. package/src/plugins/builtin/substack/article-detail.tsx +39 -15
  81. package/src/plugins/builtin/substack/article-stack.tsx +6 -2
  82. package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
  83. package/src/plugins/builtin/substack/pane.tsx +8 -3
  84. package/src/plugins/builtin/substack/read-state.ts +55 -0
  85. package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
  86. package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
  87. package/src/plugins/pane-manager/docking.ts +9 -2
  88. package/src/plugins/pane-manager/queries.ts +8 -2
  89. package/src/plugins/prediction-markets/controller/index.ts +2 -0
  90. package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
  91. package/src/plugins/prediction-markets/pane.test.tsx +32 -0
  92. package/src/plugins/prediction-markets/pane.tsx +21 -1
  93. package/src/plugins/prediction-markets/test-helpers.tsx +1 -0
  94. package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
  95. package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
  96. package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
  97. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  98. package/src/renderers/electrobun/bun/index.ts +13 -0
  99. package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
  100. package/src/renderers/electrobun/bun/window/focus.ts +5 -0
  101. package/src/renderers/electrobun/view/app-services.ts +6 -4
  102. package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
  103. package/src/renderers/electrobun/view/host/text.test.ts +22 -0
  104. package/src/renderers/electrobun/view/host/text.tsx +24 -20
  105. package/src/renderers/electrobun/view/ui-host.tsx +1 -1
  106. package/src/renderers/opentui/start.tsx +13 -5
  107. package/src/state/app/input-capture.ts +12 -0
  108. package/src/types/plugin.ts +28 -0
  109. package/src/utils/cli-output.ts +6 -0
  110. package/src/utils/search-focus-navigation.ts +24 -0
  111. package/src/version.ts +1 -1
@@ -24,17 +24,21 @@ export function useComparisonChartSymbolSources(symbols: string[]): {
24
24
  const marketFinancials = useTickerFinancialsMap(stableTickers);
25
25
  const symbolSources = useMemo<ComparisonChartSymbolSource[]>(() => stableSymbols.map((symbol) => {
26
26
  const ticker = tickers.get(symbol) ?? null;
27
- const financial = marketFinancials.get(symbol) ?? financials.get(symbol) ?? null;
27
+ const marketFinancial = marketFinancials.get(symbol) ?? null;
28
+ const storedFinancial = financials.get(symbol) ?? null;
28
29
  const instrument = ticker?.metadata.broker_contracts?.[0] ?? null;
30
+ const quote = marketFinancial?.quote ?? storedFinancial?.quote;
29
31
  return {
30
32
  symbol,
31
- currency: financial?.quote?.currency ?? ticker?.metadata.currency,
32
- quote: financial?.quote,
33
+ currency: quote?.currency ?? ticker?.metadata.currency,
34
+ quote,
33
35
  exchange: ticker?.metadata.exchange ?? "",
34
36
  brokerId: instrument?.brokerId,
35
37
  brokerInstanceId: instrument?.brokerInstanceId,
36
38
  instrument,
37
- priceHistory: financial?.priceHistory ?? [],
39
+ priceHistory: marketFinancial?.priceHistory?.length
40
+ ? marketFinancial.priceHistory
41
+ : storedFinancial?.priceHistory ?? [],
38
42
  };
39
43
  }), [financials, marketFinancials, stableSymbols, tickers]);
40
44
 
@@ -12,6 +12,7 @@ import {
12
12
  type ComparisonChartRenderMode,
13
13
  type TimeRange,
14
14
  } from "../core/types";
15
+ import type { MouseInteractionEvent } from "../core/pointer";
15
16
 
16
17
  const MODE_CHIPS: Record<ComparisonChartRenderMode, string> = {
17
18
  area: "A",
@@ -22,6 +23,7 @@ interface ComparisonChartToolbarProps {
22
23
  activePreset: TimeRange | null;
23
24
  availableManualResolutions: ManualChartResolution[];
24
25
  effectiveResolution: ChartResolution;
26
+ focusPaneForMouseInteraction: (event: MouseInteractionEvent | null | undefined) => void;
25
27
  isUpdating: boolean;
26
28
  onRangeSelect: (range: TimeRange) => void;
27
29
  onRenderModeSelect: (mode: ComparisonChartRenderMode) => void;
@@ -36,6 +38,7 @@ export function ComparisonChartToolbar({
36
38
  activePreset,
37
39
  availableManualResolutions,
38
40
  effectiveResolution,
41
+ focusPaneForMouseInteraction,
39
42
  isUpdating,
40
43
  onRangeSelect,
41
44
  onRenderModeSelect,
@@ -54,7 +57,8 @@ export function ComparisonChartToolbar({
54
57
  key={range}
55
58
  fg={activePreset === range ? colors.textBright : (isRangePresetSupported(range, availableManualResolutions) ? colors.textDim : colors.textMuted)}
56
59
  attributes={activePreset === range ? TextAttributes.BOLD : 0}
57
- onMouseDown={() => {
60
+ onMouseDown={(event: any) => {
61
+ focusPaneForMouseInteraction(event);
58
62
  if (isRangePresetSupported(range, availableManualResolutions)) {
59
63
  onRangeSelect(range);
60
64
  }
@@ -73,7 +77,10 @@ export function ComparisonChartToolbar({
73
77
  key={resolution}
74
78
  fg={effectiveResolution === resolution ? colors.textBright : colors.textDim}
75
79
  attributes={effectiveResolution === resolution ? TextAttributes.BOLD : 0}
76
- onMouseDown={() => onResolutionSelect(resolution)}
80
+ onMouseDown={(event: any) => {
81
+ focusPaneForMouseInteraction(event);
82
+ onResolutionSelect(resolution);
83
+ }}
77
84
  >
78
85
  {getChartResolutionLabel(resolution)}
79
86
  </Text>
@@ -89,7 +96,10 @@ export function ComparisonChartToolbar({
89
96
  key={mode}
90
97
  fg={renderMode === mode ? colors.textBright : colors.textDim}
91
98
  attributes={renderMode === mode ? TextAttributes.BOLD : 0}
92
- onMouseDown={() => onRenderModeSelect(mode)}
99
+ onMouseDown={(event: any) => {
100
+ focusPaneForMouseInteraction(event);
101
+ onRenderModeSelect(mode);
102
+ }}
93
103
  >
94
104
  {MODE_CHIPS[mode]}
95
105
  </Text>
@@ -270,6 +270,7 @@ function ComparisonStockChartView({
270
270
  isUpdating,
271
271
  resolutionChips,
272
272
  selectionSupportMap,
273
+ performanceHistoryBySymbol,
273
274
  series,
274
275
  supportMap,
275
276
  } = useComparisonChartRenderData({
@@ -279,7 +280,11 @@ function ComparisonStockChartView({
279
280
  const performanceBySymbol = useMemo(() => {
280
281
  const bySymbol = new Map<string, PriceReturnField[]>();
281
282
  for (const source of symbolSources) {
282
- const sourceHistory = appendQuoteToPriceReturnHistory(source.priceHistory, source.quote);
283
+ const baselineHistory = performanceHistoryBySymbol.get(source.symbol) ?? [];
284
+ const sourceHistory = appendQuoteToPriceReturnHistory(
285
+ baselineHistory.length >= 2 ? baselineHistory : source.priceHistory,
286
+ source.quote,
287
+ );
283
288
  const fallbackHistory = series.find((entry) => entry.symbol === source.symbol)?.points ?? [];
284
289
  bySymbol.set(
285
290
  source.symbol,
@@ -287,7 +292,7 @@ function ComparisonStockChartView({
287
292
  );
288
293
  }
289
294
  return bySymbol;
290
- }, [series, symbolSources]);
295
+ }, [performanceHistoryBySymbol, series, symbolSources]);
291
296
  const summarySymbols = useMemo(
292
297
  () => sortSymbolsByOneYearReturn(symbols, performanceBySymbol),
293
298
  [performanceBySymbol, symbols],
@@ -340,6 +345,7 @@ function ComparisonStockChartView({
340
345
  displayCursor,
341
346
  displayCursorX,
342
347
  displayCursorY,
348
+ focusPaneForMouseInteraction,
343
349
  mouseCrosshairDisabledRef,
344
350
  plotRef,
345
351
  scrollPanCellRemainderRef,
@@ -351,6 +357,7 @@ function ComparisonStockChartView({
351
357
  chartWidth,
352
358
  cursorX: viewState.cursorX,
353
359
  cursorY: viewState.cursorY,
360
+ focused,
354
361
  renderer,
355
362
  setViewState,
356
363
  symbols: summarySymbols,
@@ -444,6 +451,7 @@ function ComparisonStockChartView({
444
451
  chartWidth,
445
452
  commitSelectionCursor,
446
453
  expandBufferRange,
454
+ focusPaneForMouseInteraction,
447
455
  mouseCrosshairDisabledRef,
448
456
  plotRef,
449
457
  pointCount: projection.dates.length,
@@ -522,6 +530,7 @@ function ComparisonStockChartView({
522
530
  legendActiveIndex={legendActiveIndex}
523
531
  legendItemWidth={legendItemWidth}
524
532
  legendRows={legendRows}
533
+ onFocusInteraction={focusPaneForMouseInteraction}
525
534
  onOpenSymbol={onOpenSymbol}
526
535
  onSelectSymbol={setSelectedSymbol}
527
536
  performanceBySymbol={performanceBySymbol}
@@ -542,6 +551,7 @@ function ComparisonStockChartView({
542
551
  activePreset={activePreset}
543
552
  availableManualResolutions={availableManualResolutions}
544
553
  effectiveResolution={effectiveResolution}
554
+ focusPaneForMouseInteraction={focusPaneForMouseInteraction}
545
555
  isUpdating={isUpdating}
546
556
  onRangeSelect={setRangePreset}
547
557
  onRenderModeSelect={(mode) => setViewState((current) => ({ ...current, renderMode: mode }))}
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import {
3
+ createDefaultConfig,
4
+ findPaneInstance,
5
+ } from "../../../types/config";
2
6
  import {
3
7
  consumeStoredChartSelectionChange,
8
+ createChartRenderModeConfig,
4
9
  createStoredChartSelectionSyncState,
5
10
  markStoredChartSelectionLocallyApplied,
6
11
  } from "./pane-settings";
@@ -22,4 +27,26 @@ describe("chart pane setting sync", () => {
22
27
  expect(consumeStoredChartSelectionChange(state, "5Y", "1wk")).toBe(true);
23
28
  expect(state.lastAppliedKey).toBe("5Y:1wk");
24
29
  });
30
+
31
+ test("persists chart render mode as both pane state and global default", () => {
32
+ const config = createDefaultConfig("/tmp/gloomberb-chart-mode-test");
33
+
34
+ const nextConfig = createChartRenderModeConfig({
35
+ activePanel: "right",
36
+ config,
37
+ focusedPaneId: "ticker-detail:main",
38
+ paneState: {},
39
+ }, "ticker-detail:main", "candles");
40
+
41
+ expect(nextConfig.chartPreferences.defaultRenderMode).toBe("candles");
42
+ expect(findPaneInstance(nextConfig.layout, "ticker-detail:main")?.settings).toMatchObject({
43
+ chartRangePreset: "5Y",
44
+ chartRenderMode: "candles",
45
+ chartResolution: "auto",
46
+ });
47
+ expect(findPaneInstance(
48
+ nextConfig.layouts[nextConfig.activeLayoutIndex]!.layout,
49
+ "ticker-detail:main",
50
+ )?.settings?.chartRenderMode).toBe("candles");
51
+ });
25
52
  });
@@ -7,7 +7,18 @@ import {
7
7
  syncConfigActiveLayoutState,
8
8
  } from "../../../state/app/context";
9
9
  import { scheduleConfigSave } from "../../../state/config-save-scheduler";
10
- import type { ChartResolution, TimeRange } from "./types";
10
+ import {
11
+ findPaneInstance,
12
+ type AppConfig,
13
+ } from "../../../types/config";
14
+ import type { ChartRenderMode, ChartResolution, TimeRange } from "./types";
15
+
16
+ interface ChartRenderModePersistenceState {
17
+ activePanel: "left" | "right";
18
+ config: AppConfig;
19
+ focusedPaneId: string | null;
20
+ paneState: Record<string, Record<string, unknown>>;
21
+ }
11
22
 
12
23
  export interface StoredChartSelectionSyncState {
13
24
  lastAppliedKey: string;
@@ -80,3 +91,51 @@ export function usePersistChartControlSelection(rangePresetKey: string): (
80
91
  scheduleConfigSave(nextConfig);
81
92
  };
82
93
  }
94
+
95
+ export function createChartRenderModeConfig(
96
+ currentState: ChartRenderModePersistenceState,
97
+ paneId: string,
98
+ renderMode: ChartRenderMode,
99
+ ): AppConfig {
100
+ const currentPane = findPaneInstance(currentState.config.layout, paneId);
101
+ const layout = currentPane
102
+ ? setPaneSettings(currentState.config.layout, paneId, {
103
+ ...(currentPane.settings ?? {}),
104
+ chartRenderMode: renderMode,
105
+ })
106
+ : currentState.config.layout;
107
+ return syncConfigActiveLayoutState(
108
+ {
109
+ ...currentState.config,
110
+ chartPreferences: {
111
+ ...currentState.config.chartPreferences,
112
+ defaultRenderMode: renderMode,
113
+ },
114
+ layout,
115
+ },
116
+ currentState.paneState,
117
+ currentState.focusedPaneId,
118
+ currentState.activePanel,
119
+ );
120
+ }
121
+
122
+ export function usePersistChartRenderMode(): (renderMode: ChartRenderMode) => void {
123
+ const dispatch = useAppDispatch();
124
+ const stateRef = useAppStateRef();
125
+ const paneId = usePaneInstanceId();
126
+
127
+ return (renderMode) => {
128
+ const currentState = stateRef.current;
129
+ const currentPane = findPaneInstance(currentState.config.layout, paneId);
130
+ if (
131
+ currentState.config.chartPreferences.defaultRenderMode === renderMode
132
+ && currentPane?.settings?.chartRenderMode === renderMode
133
+ ) {
134
+ return;
135
+ }
136
+
137
+ const nextConfig = createChartRenderModeConfig(currentState, paneId, renderMode);
138
+ dispatch({ type: "SET_CONFIG", config: nextConfig });
139
+ scheduleConfigSave(nextConfig);
140
+ };
141
+ }
@@ -31,6 +31,8 @@ export interface ChartMouseEvent {
31
31
  };
32
32
  }
33
33
 
34
+ export type MouseInteractionEvent = Pick<ChartMouseEvent, "stopPropagation" | "preventDefault">;
35
+
34
36
  export function consumeChartMouseEvent(event: Pick<ChartMouseEvent, "stopPropagation" | "preventDefault">): void {
35
37
  event.stopPropagation?.();
36
38
  event.preventDefault?.();
@@ -221,12 +221,12 @@ export function useResolvedStockChartRuntime({
221
221
  pendingCanonicalResetRef: interactionRuntime.pendingCanonicalResetRef,
222
222
  pendingExpansionRef: interactionRuntime.pendingExpansionRef,
223
223
  persistChartControls: settings.persistChartControls,
224
+ persistChartRenderMode: settings.persistChartRenderMode,
224
225
  renderedResolution: dataRuntime.renderedResolution,
225
226
  selectionSupportMap: resolutionRuntime.selectionSupportMap,
226
227
  setPendingAutoWindowOverride: dataRuntime.setPendingAutoWindowOverride,
227
228
  setRenderedAutoView: dataRuntime.setRenderedAutoView,
228
229
  setRequestedResolution: settings.setRequestedResolution,
229
- setStoredRenderMode: settings.setStoredRenderMode,
230
230
  setViewState: settings.setViewState,
231
231
  storedRangePreset: settings.storedRangePreset,
232
232
  storedRenderMode: settings.storedRenderMode,
@@ -3,7 +3,10 @@ import {
3
3
  usePaneInstance,
4
4
  usePaneSettingValue,
5
5
  } from "../../../state/app/context";
6
- import { usePersistChartControlSelection } from "../core/pane-settings";
6
+ import {
7
+ usePersistChartControlSelection,
8
+ usePersistChartRenderMode,
9
+ } from "../core/pane-settings";
7
10
  import type { IndicatorConfig } from "../indicators/types";
8
11
  import {
9
12
  DEFAULT_TICKER_CHART_RANGE_PRESET,
@@ -30,8 +33,9 @@ export function useStockChartSettings({
30
33
  const pane = usePaneInstance();
31
34
  const [storedRangePreset] = usePaneSettingValue("chartRangePreset", DEFAULT_TICKER_CHART_RANGE_PRESET);
32
35
  const [storedResolution] = usePaneSettingValue<ChartResolution>("chartResolution", DEFAULT_TICKER_CHART_RESOLUTION);
33
- const [storedRenderMode, setStoredRenderMode] = usePaneSettingValue<ChartRenderMode>("chartRenderMode", defaultRenderMode);
36
+ const [storedRenderMode] = usePaneSettingValue<ChartRenderMode>("chartRenderMode", defaultRenderMode);
34
37
  const persistChartControls = usePersistChartControlSelection("chartRangePreset");
38
+ const persistChartRenderMode = usePersistChartRenderMode();
35
39
  const [viewState, setViewState] = useState<StockChartViewportState>({
36
40
  presetRange: compact ? "1Y" : storedRangePreset,
37
41
  bufferRange: compact ? "1Y" : storedRangePreset,
@@ -67,7 +71,7 @@ export function useStockChartSettings({
67
71
  persistChartControls,
68
72
  requestedResolution,
69
73
  setRequestedResolution,
70
- setStoredRenderMode,
74
+ persistChartRenderMode,
71
75
  setViewState,
72
76
  storedRangePreset,
73
77
  storedRenderMode,
@@ -67,10 +67,10 @@ interface UseStockChartViewportRuntimeOptions {
67
67
  persistChartControls: (range: TimeRange, resolution: ChartResolution) => void;
68
68
  renderedResolution: ChartResolution;
69
69
  selectionSupportMap: ReadonlyMap<ManualChartResolution, TimeRange>;
70
+ persistChartRenderMode: (mode: ChartRenderMode) => void;
70
71
  setPendingAutoWindowOverride: Dispatch<SetStateAction<DateWindowRange | null>>;
71
72
  setRenderedAutoView: Dispatch<SetStateAction<AutoRenderedView | null>>;
72
73
  setRequestedResolution: Dispatch<SetStateAction<ChartResolution>>;
73
- setStoredRenderMode: Dispatch<SetStateAction<ChartRenderMode>>;
74
74
  setViewState: Dispatch<SetStateAction<StockChartViewportState>>;
75
75
  storedRangePreset: TimeRange;
76
76
  storedRenderMode: ChartRenderMode;
@@ -101,12 +101,12 @@ export function useStockChartViewportRuntime({
101
101
  pendingCanonicalResetRef,
102
102
  pendingExpansionRef,
103
103
  persistChartControls,
104
+ persistChartRenderMode,
104
105
  renderedResolution,
105
106
  selectionSupportMap,
106
107
  setPendingAutoWindowOverride,
107
108
  setRenderedAutoView,
108
109
  setRequestedResolution,
109
- setStoredRenderMode,
110
110
  setViewState,
111
111
  storedRangePreset,
112
112
  storedRenderMode,
@@ -172,10 +172,10 @@ export function useStockChartViewportRuntime({
172
172
  });
173
173
 
174
174
  const persistRenderMode = useCallback((nextMode: ChartRenderMode) => {
175
- if (!compact && nextMode !== storedRenderMode) {
176
- setStoredRenderMode(nextMode);
175
+ if (!compact) {
176
+ persistChartRenderMode(nextMode);
177
177
  }
178
- }, [compact, setStoredRenderMode, storedRenderMode]);
178
+ }, [compact, persistChartRenderMode]);
179
179
 
180
180
  const expandBufferRange = useCallback((action: PendingExpansionAction): boolean => {
181
181
  if (compact) return false;
@@ -16,7 +16,17 @@ import {
16
16
  getPaneTemplateDisplayLabel,
17
17
  } from "./items";
18
18
 
19
+ function paneTemplateHasConfigFields(template: PaneTemplateDef): boolean {
20
+ if (template.wizard?.some((step) => step.type !== "info")) {
21
+ return true;
22
+ }
23
+ return !!buildGeneratedTemplateField(template, null).field;
24
+ }
25
+
19
26
  export function shouldOpenPaneTemplateConfig(template: PaneTemplateDef, arg?: string): boolean {
27
+ if (!paneTemplateHasConfigFields(template)) {
28
+ return false;
29
+ }
20
30
  if (template.wizard && template.wizard.length > 0) {
21
31
  if (!arg?.trim()) {
22
32
  return true;
@@ -117,6 +117,42 @@ function registerOptionalTextPane(pluginRegistry: MutablePaneRegistry): void {
117
117
  });
118
118
  }
119
119
 
120
+ function registerQueryOnlyPane(pluginRegistry: MutablePaneRegistry): void {
121
+ mutableRegistryMap(pluginRegistry.panes).set("prediction-markets", {
122
+ id: "prediction-markets",
123
+ name: "Prediction Markets",
124
+ component: () => null,
125
+ defaultPosition: "left",
126
+ defaultMode: "floating",
127
+ });
128
+ mutableRegistryMap(pluginRegistry.paneTemplates).set("new-prediction-markets-pane", {
129
+ id: "new-prediction-markets-pane",
130
+ paneId: "prediction-markets",
131
+ label: "Prediction Markets",
132
+ description: "Open a new prediction markets browser pane",
133
+ keywords: ["prediction", "markets", "polymarket", "kalshi", "events"],
134
+ shortcut: { prefix: "PM", argPlaceholder: "query", argKind: "text" },
135
+ });
136
+ }
137
+
138
+ function registerShortcutOnlyPane(pluginRegistry: MutablePaneRegistry): void {
139
+ mutableRegistryMap(pluginRegistry.panes).set("top-news", {
140
+ id: "top-news",
141
+ name: "Top News",
142
+ component: () => null,
143
+ defaultPosition: "right",
144
+ defaultMode: "floating",
145
+ });
146
+ mutableRegistryMap(pluginRegistry.paneTemplates).set("top-news-pane", {
147
+ id: "top-news-pane",
148
+ paneId: "top-news",
149
+ label: "Top News",
150
+ description: "Curated top market stories ranked by importance",
151
+ keywords: ["top", "news", "headlines", "stories"],
152
+ shortcut: { prefix: "TOP" },
153
+ });
154
+ }
155
+
120
156
  describe("CommandBar pane and layout routes", () => {
121
157
  const layoutModeConfig = (config: AppConfig): AppConfig => {
122
158
  const research = cloneLayout(config.layout);
@@ -271,6 +307,46 @@ describe("CommandBar pane and layout routes", () => {
271
307
  expect(testSetup.captureCharFrame()).not.toContain("Create Pane");
272
308
  });
273
309
 
310
+ for (const scenario of [
311
+ {
312
+ query: "prediction markets",
313
+ register: registerQueryOnlyPane,
314
+ templateId: "new-prediction-markets-pane",
315
+ },
316
+ {
317
+ query: "top news",
318
+ register: registerShortcutOnlyPane,
319
+ templateId: "top-news-pane",
320
+ },
321
+ ] as const) {
322
+ test(`creates ${scenario.templateId} directly when it has no config fields`, async () => {
323
+ const created: CreatedPaneCall[] = [];
324
+
325
+ testSetup = await testRender(<CommandBarHarness
326
+ query={scenario.query}
327
+ live
328
+ configurePluginRegistry={(pluginRegistry) => {
329
+ scenario.register(pluginRegistry);
330
+ recordPaneCreations(pluginRegistry, created);
331
+ }}
332
+ />, {
333
+ width: 100,
334
+ height: 18,
335
+ });
336
+
337
+ await testSetup.renderOnce();
338
+
339
+ await act(async () => {
340
+ testSetup!.mockInput.pressEnter();
341
+ await Bun.sleep(0);
342
+ await testSetup!.renderOnce();
343
+ });
344
+
345
+ expect(created).toEqual([{ templateId: scenario.templateId, options: undefined }]);
346
+ expect(testSetup.captureCharFrame()).not.toContain("Create Pane");
347
+ });
348
+ }
349
+
274
350
  test("shows pane templates that share the same shortcut", async () => {
275
351
  testSetup = await testRender(<CommandBarHarness
276
352
  query="DUP"
@@ -26,6 +26,11 @@ export type DataTableKeyEvent = TableViewKeyEvent;
26
26
 
27
27
  const DATA_TABLE_SELECTION_COMMIT_DELAY_MS = 150;
28
28
 
29
+ export interface DataTableRootKeyContext {
30
+ selectedIndex: number;
31
+ itemCount: number;
32
+ }
33
+
29
34
  export type DataTableSelectionChangeReason =
30
35
  | "keyboard"
31
36
  | "pointer"
@@ -95,7 +100,10 @@ export interface DataTableViewProps<
95
100
  hoveredIdx?: number | null;
96
101
  setHoveredIdx?: (index: number | null) => void;
97
102
  keyboardNavigation?: boolean;
98
- onRootKeyDown?: (event: DataTableKeyEvent) => boolean | void;
103
+ onRootKeyDown?: (
104
+ event: DataTableKeyEvent,
105
+ context: DataTableRootKeyContext,
106
+ ) => boolean | void;
99
107
  resetScrollKey?: unknown;
100
108
  }
101
109
 
@@ -486,7 +494,10 @@ export function DataTableView<
486
494
  if (event.defaultPrevented || event.propagationStopped) return;
487
495
  if (!focused || !keyboardNavigation) return;
488
496
 
489
- if (onRootKeyDown?.(event)) return;
497
+ if (onRootKeyDown?.(event, {
498
+ selectedIndex: effectiveSelectedIndexRef.current,
499
+ itemCount: tableProps.items.length,
500
+ })) return;
490
501
  if (tableProps.items.length === 0) return;
491
502
 
492
503
  if (isNextTableRowKey(event)) {
@@ -6,6 +6,7 @@ import { formatTimeAgo } from "../../utils/format";
6
6
  import { isPlainKey } from "../../utils/keyboard";
7
7
  import { toTimestampMillis } from "../../utils/timestamp";
8
8
  import { DataTableStackView } from "../data-table/stack-view";
9
+ import type { DataTableRootKeyContext } from "../data-table/view";
9
10
  import {
10
11
  activeStackIndex,
11
12
  sortIndexedStackRows,
@@ -45,9 +46,16 @@ interface FeedDataTableStackViewProps {
45
46
  rootAfter?: ReactNode;
46
47
  onRootKeyDown?: (event: {
47
48
  name?: string;
49
+ sequence?: string;
50
+ ctrl?: boolean;
51
+ meta?: boolean;
52
+ super?: boolean;
53
+ alt?: boolean;
54
+ option?: boolean;
55
+ shift?: boolean;
48
56
  preventDefault?: () => void;
49
57
  stopPropagation?: () => void;
50
- }) => boolean | void;
58
+ }, context: DataTableRootKeyContext) => boolean | void;
51
59
  sourceLabel?: string;
52
60
  titleLabel?: string;
53
61
  emptyStateTitle?: string;
@@ -8,7 +8,7 @@ export type { TickerListVisibleRange } from "./ticker/list-table-view";
8
8
  export { TickerBadgeList } from "./ticker/badge/list";
9
9
  export { InputSearchBar } from "./input-search-bar";
10
10
  export { DataTableView } from "./data-table/view";
11
- export type { DataTableKeyEvent } from "./data-table/view";
11
+ export type { DataTableKeyEvent, DataTableRootKeyContext } from "./data-table/view";
12
12
  export { DataTableStackView } from "./data-table/stack-view";
13
13
  export { FeedDataTableStackView } from "./feed-data-table/stack-view";
14
14
  export type { FeedDataTableItem } from "./feed-data-table/stack-view";
@@ -0,0 +1,103 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act, useCallback, useRef, useState, type Dispatch, type SetStateAction } from "react";
3
+ import { testRender } from "../renderers/opentui/test-utils";
4
+ import { AppContext, createInitialState, type AppAction } from "../state/app/context";
5
+ import { createDefaultConfig } from "../types/config";
6
+ import type { InputRenderable } from "../ui";
7
+ import { InputSearchBar } from "./input-search-bar";
8
+
9
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
10
+ let setSearchActive: Dispatch<SetStateAction<boolean>> | null = null;
11
+
12
+ afterEach(async () => {
13
+ setSearchActive = null;
14
+ if (!testSetup) return;
15
+ await act(async () => {
16
+ testSetup!.renderer.destroy();
17
+ });
18
+ testSetup = undefined;
19
+ });
20
+
21
+ function Harness({
22
+ actions,
23
+ onNavigateDown,
24
+ }: {
25
+ actions: AppAction[];
26
+ onNavigateDown?: () => void;
27
+ }) {
28
+ const state = createInitialState(createDefaultConfig("/tmp/gloomberb-input-search-bar"));
29
+ const inputRef = useRef<InputRenderable | null>(null);
30
+ const [active, setActive] = useState(true);
31
+ setSearchActive = setActive;
32
+ const dispatch = useCallback((action: AppAction) => {
33
+ actions.push(action);
34
+ }, [actions]);
35
+
36
+ return (
37
+ <AppContext value={{ state, dispatch }}>
38
+ <InputSearchBar
39
+ value=""
40
+ focused
41
+ active={active}
42
+ width={30}
43
+ focusToken={0}
44
+ inputRef={inputRef}
45
+ placeholder="search"
46
+ debounceMs={100}
47
+ onNavigateDown={onNavigateDown}
48
+ onFocus={() => {}}
49
+ onBlur={() => {}}
50
+ onQueryChange={() => {}}
51
+ />
52
+ </AppContext>
53
+ );
54
+ }
55
+
56
+ describe("InputSearchBar", () => {
57
+ test("captures app input while the search input is active", async () => {
58
+ const actions: AppAction[] = [];
59
+
60
+ testSetup = await testRender(<Harness actions={actions} />, { width: 40, height: 4 });
61
+ await act(async () => {
62
+ await testSetup!.renderOnce();
63
+ });
64
+
65
+ expect(actions).toEqual([{ type: "SET_INPUT_CAPTURED", captured: true }]);
66
+ if (!setSearchActive) throw new Error("search setter was not registered");
67
+
68
+ await act(async () => {
69
+ setSearchActive(false);
70
+ await testSetup!.renderOnce();
71
+ });
72
+
73
+ expect(actions).toEqual([
74
+ { type: "SET_INPUT_CAPTURED", captured: true },
75
+ { type: "SET_INPUT_CAPTURED", captured: false },
76
+ ]);
77
+ });
78
+
79
+ test("moves from the active search input to the table with Down", async () => {
80
+ const actions: AppAction[] = [];
81
+ let navigatedDown = 0;
82
+
83
+ testSetup = await testRender(
84
+ <Harness
85
+ actions={actions}
86
+ onNavigateDown={() => {
87
+ navigatedDown += 1;
88
+ }}
89
+ />,
90
+ { width: 40, height: 4 },
91
+ );
92
+ await act(async () => {
93
+ await testSetup!.renderOnce();
94
+ });
95
+
96
+ await act(async () => {
97
+ testSetup!.mockInput.pressArrow("down");
98
+ await testSetup!.renderOnce();
99
+ });
100
+
101
+ expect(navigatedDown).toBe(1);
102
+ });
103
+ });