gloomberb 0.9.1 → 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 (136) 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/helpers.ts +0 -32
  29. package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
  30. package/src/components/chart/comparison-stock-chart/layout-metrics.ts +6 -6
  31. package/src/components/chart/comparison-stock-chart/legend.tsx +45 -44
  32. package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
  33. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
  34. package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
  35. package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
  36. package/src/components/chart/comparison-stock-chart/view.tsx +56 -6
  37. package/src/components/chart/core/pane-settings.test.ts +27 -0
  38. package/src/components/chart/core/pane-settings.ts +60 -1
  39. package/src/components/chart/core/pointer.ts +2 -0
  40. package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
  41. package/src/components/chart/stock-chart/settings.ts +7 -3
  42. package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
  43. package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
  44. package/src/components/command-bar/surface/pane.test.tsx +76 -0
  45. package/src/components/data-table/view.tsx +13 -2
  46. package/src/components/feed-data-table/stack-view.tsx +9 -1
  47. package/src/components/index.ts +1 -1
  48. package/src/components/input-search-bar.test.tsx +103 -0
  49. package/src/components/input-search-bar.tsx +16 -0
  50. package/src/components/layout/detached-pane-shell.tsx +5 -2
  51. package/src/components/layout/header.tsx +13 -5
  52. package/src/components/layout/shell/active-drag.ts +5 -1
  53. package/src/components/layout/shell/drag/runtime.ts +21 -3
  54. package/src/components/layout/shell/fullscreen.ts +10 -0
  55. package/src/components/layout/shell/index.test.tsx +395 -2
  56. package/src/components/layout/shell/index.tsx +128 -2
  57. package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
  58. package/src/components/layout/shell/pane/layers.tsx +26 -14
  59. package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
  60. package/src/components/layout/shell/shortcuts.ts +4 -0
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
  62. package/src/components/layout/shell/window-mode/index.ts +11 -3
  63. package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
  64. package/src/components/layout/status-bar.test.tsx +105 -0
  65. package/src/components/layout/status-bar.tsx +32 -6
  66. package/src/components/layout/transient-layout.tsx +57 -0
  67. package/src/components/layout/window-controls.tsx +19 -10
  68. package/src/components/layout/window-edit/mode.test.ts +76 -5
  69. package/src/components/layout/window-edit/mode.ts +47 -2
  70. package/src/components/layout/window-edit/presentation.ts +2 -1
  71. package/src/components/price-performance.tsx +183 -0
  72. package/src/core/app-services.ts +7 -4
  73. package/src/data/config/store/index.ts +1 -2
  74. package/src/market-data/performance.test.ts +54 -0
  75. package/src/market-data/performance.ts +109 -0
  76. package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
  77. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
  78. package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
  79. package/src/plugins/builtin/comparison-chart/index.test.tsx +218 -15
  80. package/src/plugins/builtin/congress-trades/detail.tsx +204 -68
  81. package/src/plugins/builtin/congress-trades/footer.ts +14 -8
  82. package/src/plugins/builtin/congress-trades/keyboard.ts +4 -23
  83. package/src/plugins/builtin/congress-trades/model.ts +29 -0
  84. package/src/plugins/builtin/congress-trades/pane.tsx +8 -13
  85. package/src/plugins/builtin/congress-trades/table.tsx +10 -1
  86. package/src/plugins/builtin/news/wire/read-state.ts +9 -32
  87. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
  88. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
  89. package/src/plugins/builtin/portfolio-list/pane/index.tsx +1 -1
  90. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
  91. package/src/plugins/builtin/shared/read-state.ts +43 -0
  92. package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
  93. package/src/plugins/builtin/substack/article-detail.tsx +39 -15
  94. package/src/plugins/builtin/substack/article-stack.tsx +6 -2
  95. package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
  96. package/src/plugins/builtin/substack/pane.tsx +8 -3
  97. package/src/plugins/builtin/substack/read-state.ts +55 -0
  98. package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
  99. package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
  100. package/src/plugins/builtin/ticker-detail/overview/model.ts +0 -14
  101. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -0
  102. package/src/plugins/pane-manager/docking.ts +9 -2
  103. package/src/plugins/pane-manager/queries.ts +8 -2
  104. package/src/plugins/prediction-markets/controller/index.ts +2 -0
  105. package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
  106. package/src/plugins/prediction-markets/pane.test.tsx +32 -0
  107. package/src/plugins/prediction-markets/pane.tsx +21 -1
  108. package/src/plugins/prediction-markets/test-helpers.tsx +1 -0
  109. package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
  110. package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
  111. package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
  112. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  113. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +9 -2
  114. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  115. package/src/renderers/electrobun/bun/desktop/window-controls.ts +11 -2
  116. package/src/renderers/electrobun/bun/desktop/window-style.ts +27 -1
  117. package/src/renderers/electrobun/bun/desktop/windows-custom-chrome.ts +58 -0
  118. package/src/renderers/electrobun/bun/desktop/windows-icons.ts +11 -64
  119. package/src/renderers/electrobun/bun/desktop/windows-native.ts +75 -0
  120. package/src/renderers/electrobun/bun/index.ts +22 -2
  121. package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
  122. package/src/renderers/electrobun/bun/window/focus.ts +5 -0
  123. package/src/renderers/electrobun/shared/protocol.ts +1 -0
  124. package/src/renderers/electrobun/view/app-services.ts +6 -4
  125. package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
  126. package/src/renderers/electrobun/view/host/text.test.ts +22 -0
  127. package/src/renderers/electrobun/view/host/text.tsx +24 -20
  128. package/src/renderers/electrobun/view/main.tsx +2 -1
  129. package/src/renderers/electrobun/view/styles.css +9 -8
  130. package/src/renderers/electrobun/view/ui-host.tsx +102 -77
  131. package/src/renderers/opentui/start.tsx +13 -5
  132. package/src/state/app/input-capture.ts +12 -0
  133. package/src/types/plugin.ts +28 -0
  134. package/src/utils/cli-output.ts +6 -0
  135. package/src/utils/search-focus-navigation.ts +24 -0
  136. package/src/version.ts +1 -1
@@ -3,7 +3,8 @@ import {
3
3
  DEFAULT_LIVE_CHART_REFRESH_INTERVAL_MS,
4
4
  useChartQueries,
5
5
  } from "../../../market-data/hooks";
6
- import { buildChartKey } from "../../../market-data/selectors";
6
+ import type { ChartRequest, InstrumentRef } from "../../../market-data/request-types";
7
+ import { buildChartKey, resolveEntryData } from "../../../market-data/selectors";
7
8
  import { getSharedMarketData } from "../../../plugins/registry";
8
9
  import { blendHex, colors, getComparisonSeriesColor } from "../../../theme/colors";
9
10
  import type { BrokerContractRef } from "../../../types/instrument";
@@ -24,6 +25,8 @@ import type {
24
25
  ComparisonChartViewState,
25
26
  } from "../core/types";
26
27
 
28
+ const COMPARISON_PERFORMANCE_RANGE = "5Y" as const;
29
+
27
30
  export interface ComparisonChartSymbolSource {
28
31
  symbol: string;
29
32
  currency: string | undefined;
@@ -40,6 +43,16 @@ interface UseComparisonChartRenderDataOptions {
40
43
  viewState: ComparisonChartViewState;
41
44
  }
42
45
 
46
+ function instrumentFromSymbolSource(source: ComparisonChartSymbolSource): InstrumentRef {
47
+ return {
48
+ symbol: source.symbol,
49
+ exchange: source.exchange,
50
+ brokerId: source.brokerId,
51
+ brokerInstanceId: source.brokerInstanceId,
52
+ instrument: source.instrument,
53
+ };
54
+ }
55
+
43
56
  export function useComparisonChartRenderData({
44
57
  symbolSources,
45
58
  viewState,
@@ -143,21 +156,25 @@ export function useComparisonChartRenderData({
143
156
  );
144
157
  const chartRequests = useMemo(() => (
145
158
  symbolSources.map((source) => ({
146
- instrument: {
147
- symbol: source.symbol,
148
- exchange: source.exchange,
149
- brokerId: source.brokerId,
150
- brokerInstanceId: source.brokerInstanceId,
151
- instrument: source.instrument,
152
- },
159
+ instrument: instrumentFromSymbolSource(source),
153
160
  bufferRange: viewState.bufferRange,
154
161
  granularity: effectiveResolution === "auto" ? "range" as const : "resolution" as const,
155
162
  resolution: effectiveResolution === "auto" ? undefined : effectiveResolution,
156
163
  }))
157
164
  ), [effectiveResolution, symbolSources, viewState.bufferRange]);
165
+ const performanceRequests = useMemo<ChartRequest[]>(() => (
166
+ symbolSources.map((source) => ({
167
+ instrument: instrumentFromSymbolSource(source),
168
+ bufferRange: COMPARISON_PERFORMANCE_RANGE,
169
+ granularity: "range",
170
+ }))
171
+ ), [symbolSources]);
158
172
  const chartEntries = useChartQueries(chartRequests, {
159
173
  refreshIntervalMs: DEFAULT_LIVE_CHART_REFRESH_INTERVAL_MS,
160
174
  });
175
+ const performanceEntries = useChartQueries(performanceRequests, {
176
+ refreshIntervalMs: DEFAULT_LIVE_CHART_REFRESH_INTERVAL_MS,
177
+ });
161
178
  const entryStates = useMemo(() => chartRequests.map((request) => (
162
179
  resolveChartBodyState(chartEntries.get(buildChartKey(request)), (value) => Array.isArray(value) && value.length > 0, "No chart data yet.")
163
180
  )), [chartEntries, chartRequests]);
@@ -182,6 +199,17 @@ export function useComparisonChartRenderData({
182
199
  points,
183
200
  };
184
201
  }), [chartEntries, chartRequests, symbolSources]);
202
+ const performanceHistoryBySymbol = useMemo(() => {
203
+ const bySymbol = new Map<string, PricePoint[]>();
204
+ symbolSources.forEach((source, index) => {
205
+ const request = performanceRequests[index];
206
+ const history = request ? resolveEntryData(performanceEntries.get(buildChartKey(request))) : null;
207
+ if (history?.length) {
208
+ bySymbol.set(source.symbol, history);
209
+ }
210
+ });
211
+ return bySymbol;
212
+ }, [performanceEntries, performanceRequests, symbolSources]);
185
213
 
186
214
  return {
187
215
  availableManualResolutions,
@@ -192,6 +220,7 @@ export function useComparisonChartRenderData({
192
220
  isUpdating,
193
221
  resolutionChips,
194
222
  selectionSupportMap,
223
+ performanceHistoryBySymbol,
195
224
  series,
196
225
  supportMap,
197
226
  };
@@ -1,6 +1,7 @@
1
- import { useEffect, useRef, type Dispatch, type MutableRefObject, type SetStateAction } from "react";
1
+ import { useCallback, useEffect, useRef, type Dispatch, type MutableRefObject, type SetStateAction } from "react";
2
2
  import type { BoxRenderable, NativeRendererHost } from "../../../ui";
3
- import { buildDisplayCursorState } from "../core/pointer";
3
+ import { useAppDispatch, usePaneInstanceId } from "../../../state/app/context";
4
+ import { buildDisplayCursorState, type MouseInteractionEvent } from "../core/pointer";
4
5
  import type { ComparisonChartViewState } from "../core/types";
5
6
  import { useChartDisplayCursor, useChartDisplayCursorLayoutRemap } from "../display-cursor";
6
7
  import { clamp } from "./helpers";
@@ -10,6 +11,7 @@ interface UseComparisonChartSelectionRuntimeOptions {
10
11
  chartWidth: number;
11
12
  cursorX: number | null;
12
13
  cursorY: number | null;
14
+ focused: boolean;
13
15
  renderer: NativeRendererHost;
14
16
  setViewState: Dispatch<SetStateAction<ComparisonChartViewState>>;
15
17
  symbols: string[];
@@ -20,6 +22,7 @@ export function useComparisonChartSelectionRuntime({
20
22
  chartWidth,
21
23
  cursorX: viewCursorX,
22
24
  cursorY: viewCursorY,
25
+ focused,
23
26
  renderer,
24
27
  setViewState,
25
28
  symbols,
@@ -28,6 +31,7 @@ export function useComparisonChartSelectionRuntime({
28
31
  displayCursor: ReturnType<typeof useChartDisplayCursor>["displayCursor"];
29
32
  displayCursorX: number | null;
30
33
  displayCursorY: number | null;
34
+ focusPaneForMouseInteraction: (event: MouseInteractionEvent | null | undefined) => void;
31
35
  mouseCrosshairDisabledRef: MutableRefObject<boolean>;
32
36
  plotRef: MutableRefObject<BoxRenderable | null>;
33
37
  scrollPanCellRemainderRef: MutableRefObject<number>;
@@ -35,6 +39,8 @@ export function useComparisonChartSelectionRuntime({
35
39
  setSelectedSymbol: (symbol: string) => void;
36
40
  updateDisplayCursorTarget: ReturnType<typeof useChartDisplayCursor>["updateDisplayCursorTarget"];
37
41
  } {
42
+ const dispatch = useAppDispatch();
43
+ const paneId = usePaneInstanceId();
38
44
  const plotRef = useRef<BoxRenderable | null>(null);
39
45
  const mouseCrosshairDisabledRef = useRef(false);
40
46
  const scrollPanCellRemainderRef = useRef(0);
@@ -52,6 +58,16 @@ export function useComparisonChartSelectionRuntime({
52
58
  const displayCursorX = displayCursor.cellX !== null ? clamp(displayCursor.cellX, 0, chartWidth - 1) : null;
53
59
  const displayCursorY = displayCursor.cellY !== null ? clamp(displayCursor.cellY, 0, chartHeight - 1) : null;
54
60
 
61
+ const focusPaneForMouseInteraction = useCallback((
62
+ event: MouseInteractionEvent | null | undefined,
63
+ ) => {
64
+ event?.stopPropagation?.();
65
+ event?.preventDefault?.();
66
+ if (!focused) {
67
+ dispatch({ type: "FOCUS_PANE", paneId });
68
+ }
69
+ }, [dispatch, focused, paneId]);
70
+
55
71
  const setSelectedSymbol = (symbol: string) => {
56
72
  setViewState((current) => (
57
73
  current.selectedSymbol === symbol
@@ -101,6 +117,7 @@ export function useComparisonChartSelectionRuntime({
101
117
  displayCursor,
102
118
  displayCursorX,
103
119
  displayCursorY,
120
+ focusPaneForMouseInteraction,
104
121
  mouseCrosshairDisabledRef,
105
122
  plotRef,
106
123
  scrollPanCellRemainderRef,
@@ -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>
@@ -5,6 +5,11 @@ import { blendHex, colors } from "../../../theme/colors";
5
5
  import { useThemeColors } from "../../../theme/theme-context";
6
6
  import { usePaneSettingValue } from "../../../state/app/context";
7
7
  import { useQuoteStreaming } from "../../../state/hooks/quote-streaming";
8
+ import {
9
+ appendQuoteToPriceReturnHistory,
10
+ buildPriceReturnFields,
11
+ type PriceReturnField,
12
+ } from "../../../market-data/performance";
8
13
  import type { QuoteSubscriptionTarget } from "../../../types/data-provider";
9
14
  import {
10
15
  type ChartRendererPreference,
@@ -170,6 +175,27 @@ function useComparisonChartQuoteStreaming({
170
175
  useQuoteStreaming(streamingTargets);
171
176
  }
172
177
 
178
+ function getOneYearReturn(fields: readonly PriceReturnField[]): number | null {
179
+ return fields.find((field) => field.id === "1Y")?.value ?? null;
180
+ }
181
+
182
+ function sortSymbolsByOneYearReturn(
183
+ symbols: readonly string[],
184
+ performanceBySymbol: ReadonlyMap<string, PriceReturnField[]>,
185
+ ): string[] {
186
+ const originalIndex = new Map(symbols.map((symbol, index) => [symbol, index]));
187
+ return [...symbols].sort((left, right) => {
188
+ const leftReturn = getOneYearReturn(performanceBySymbol.get(left) ?? []);
189
+ const rightReturn = getOneYearReturn(performanceBySymbol.get(right) ?? []);
190
+ if (leftReturn != null && rightReturn != null && leftReturn !== rightReturn) {
191
+ return rightReturn - leftReturn;
192
+ }
193
+ if (leftReturn != null && rightReturn == null) return -1;
194
+ if (leftReturn == null && rightReturn != null) return 1;
195
+ return (originalIndex.get(left) ?? 0) - (originalIndex.get(right) ?? 0);
196
+ });
197
+ }
198
+
173
199
  function ComparisonStockChartView({
174
200
  paneId,
175
201
  width,
@@ -244,12 +270,33 @@ function ComparisonStockChartView({
244
270
  isUpdating,
245
271
  resolutionChips,
246
272
  selectionSupportMap,
273
+ performanceHistoryBySymbol,
247
274
  series,
248
275
  supportMap,
249
276
  } = useComparisonChartRenderData({
250
277
  symbolSources,
251
278
  viewState,
252
279
  });
280
+ const performanceBySymbol = useMemo(() => {
281
+ const bySymbol = new Map<string, PriceReturnField[]>();
282
+ for (const source of symbolSources) {
283
+ const baselineHistory = performanceHistoryBySymbol.get(source.symbol) ?? [];
284
+ const sourceHistory = appendQuoteToPriceReturnHistory(
285
+ baselineHistory.length >= 2 ? baselineHistory : source.priceHistory,
286
+ source.quote,
287
+ );
288
+ const fallbackHistory = series.find((entry) => entry.symbol === source.symbol)?.points ?? [];
289
+ bySymbol.set(
290
+ source.symbol,
291
+ buildPriceReturnFields(sourceHistory.length >= 2 ? sourceHistory : fallbackHistory),
292
+ );
293
+ }
294
+ return bySymbol;
295
+ }, [performanceHistoryBySymbol, series, symbolSources]);
296
+ const summarySymbols = useMemo(
297
+ () => sortSymbolsByOneYearReturn(symbols, performanceBySymbol),
298
+ [performanceBySymbol, symbols],
299
+ );
253
300
 
254
301
  useEffect(() => {
255
302
  if (symbols.includes(viewState.selectedSymbol ?? "")) return;
@@ -298,6 +345,7 @@ function ComparisonStockChartView({
298
345
  displayCursor,
299
346
  displayCursorX,
300
347
  displayCursorY,
348
+ focusPaneForMouseInteraction,
301
349
  mouseCrosshairDisabledRef,
302
350
  plotRef,
303
351
  scrollPanCellRemainderRef,
@@ -309,9 +357,10 @@ function ComparisonStockChartView({
309
357
  chartWidth,
310
358
  cursorX: viewState.cursorX,
311
359
  cursorY: viewState.cursorY,
360
+ focused,
312
361
  renderer,
313
362
  setViewState,
314
- symbols,
363
+ symbols: summarySymbols,
315
364
  });
316
365
  const {
317
366
  activePreset,
@@ -352,7 +401,6 @@ function ComparisonStockChartView({
352
401
  result,
353
402
  staticScene,
354
403
  timeAxisLabel,
355
- visiblePriceRange,
356
404
  } = useComparisonChartRenderOutput({
357
405
  axisMode,
358
406
  chartColors,
@@ -386,7 +434,7 @@ function ComparisonStockChartView({
386
434
  setResolution,
387
435
  setViewState,
388
436
  supportMap,
389
- symbols,
437
+ symbols: summarySymbols,
390
438
  updateDisplayCursorTarget,
391
439
  viewState,
392
440
  visibleDateWindow,
@@ -403,6 +451,7 @@ function ComparisonStockChartView({
403
451
  chartWidth,
404
452
  commitSelectionCursor,
405
453
  expandBufferRange,
454
+ focusPaneForMouseInteraction,
406
455
  mouseCrosshairDisabledRef,
407
456
  plotRef,
408
457
  pointCount: projection.dates.length,
@@ -479,15 +528,15 @@ function ComparisonStockChartView({
479
528
  legend={(
480
529
  <ComparisonChartLegend
481
530
  legendActiveIndex={legendActiveIndex}
482
- legendColumns={legendColumns}
483
531
  legendItemWidth={legendItemWidth}
484
532
  legendRows={legendRows}
533
+ onFocusInteraction={focusPaneForMouseInteraction}
485
534
  onOpenSymbol={onOpenSymbol}
486
535
  onSelectSymbol={setSelectedSymbol}
536
+ performanceBySymbol={performanceBySymbol}
487
537
  projection={projection}
488
538
  selectedSymbol={viewState.selectedSymbol}
489
- symbols={symbols}
490
- visiblePriceRange={visiblePriceRange}
539
+ symbols={summarySymbols}
491
540
  />
492
541
  )}
493
542
  plotBitmaps={plotBitmaps}
@@ -502,6 +551,7 @@ function ComparisonStockChartView({
502
551
  activePreset={activePreset}
503
552
  availableManualResolutions={availableManualResolutions}
504
553
  effectiveResolution={effectiveResolution}
554
+ focusPaneForMouseInteraction={focusPaneForMouseInteraction}
505
555
  isUpdating={isUpdating}
506
556
  onRangeSelect={setRangePreset}
507
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;