gloomberb 0.10.2 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +3 -3
  2. package/README.zh-CN.md +2 -2
  3. package/package.json +2 -1
  4. package/src/api-client/auth.ts +8 -0
  5. package/src/api-client/index.ts +70 -0
  6. package/src/api-client/types.ts +49 -0
  7. package/src/app/runtime/desktop-deeplink.test.ts +8 -0
  8. package/src/app/runtime/desktop-deeplink.ts +14 -1
  9. package/src/components/chart/composite/composite-chart.test.tsx +340 -17
  10. package/src/components/chart/composite/composite-chart.tsx +859 -68
  11. package/src/components/chart/composite/interactions.test.ts +78 -22
  12. package/src/components/chart/composite/interactions.ts +45 -17
  13. package/src/components/chart/composite/rasterizer.ts +8 -2
  14. package/src/components/chart/composite/renderers.test.ts +61 -1
  15. package/src/components/chart/composite/scene.ts +14 -4
  16. package/src/components/chart/composite/tools.test.ts +371 -0
  17. package/src/components/chart/composite/tools.ts +508 -0
  18. package/src/components/chart/composite/types.ts +4 -0
  19. package/src/components/chart/core/pointer.ts +2 -0
  20. package/src/components/chart/native/raster/primitives.ts +18 -0
  21. package/src/components/chart/price-axis-labels.tsx +42 -2
  22. package/src/components/chart/static/chart/axis-overlays.test.tsx +2 -0
  23. package/src/components/chart/static/chart/axis-overlays.tsx +46 -0
  24. package/src/components/command-bar/action-runtime.ts +3 -0
  25. package/src/components/command-bar/assist/inventory.test.ts +93 -0
  26. package/src/components/command-bar/assist/inventory.ts +92 -0
  27. package/src/components/command-bar/assist/model.test.ts +104 -0
  28. package/src/components/command-bar/assist/model.ts +169 -0
  29. package/src/components/command-bar/assist/runtime.ts +185 -0
  30. package/src/components/command-bar/commands/direct/index.ts +1 -1
  31. package/src/components/command-bar/keyboard-shortcuts.ts +5 -0
  32. package/src/components/command-bar/list/model.ts +15 -2
  33. package/src/components/command-bar/list/navigation.ts +6 -0
  34. package/src/components/command-bar/list/view.tsx +16 -3
  35. package/src/components/command-bar/pane-templates/workflow.ts +1 -0
  36. package/src/components/command-bar/panel/body.tsx +2 -0
  37. package/src/components/command-bar/panel/index.tsx +3 -0
  38. package/src/components/command-bar/panel/runtime.ts +6 -0
  39. package/src/components/command-bar/panel/types.ts +1 -0
  40. package/src/components/command-bar/routes/root/results.ts +28 -2
  41. package/src/components/command-bar/routes/root/runtime.ts +66 -36
  42. package/src/components/command-bar/routes/root/shortcuts.ts +8 -4
  43. package/src/components/command-bar/routing/navigation-state.ts +17 -0
  44. package/src/components/command-bar/selection-runtime.ts +26 -0
  45. package/src/components/command-bar/surface/assist.test.tsx +276 -0
  46. package/src/components/command-bar/surface/index.tsx +93 -2
  47. package/src/components/command-bar/surface/portfolio.test.tsx +2 -1
  48. package/src/components/command-bar/view-model.ts +7 -1
  49. package/src/components/command-bar/workflow/broker.test.ts +22 -0
  50. package/src/components/command-bar/workflow/broker.ts +1 -0
  51. package/src/components/index.ts +1 -1
  52. package/src/components/layout/detached-pane-shell.tsx +40 -2
  53. package/src/components/layout/floating-pane.tsx +4 -1
  54. package/src/components/layout/pane/header.tsx +64 -6
  55. package/src/components/layout/pane/index.tsx +4 -1
  56. package/src/components/layout/shell/index.tsx +23 -0
  57. package/src/components/layout/shell/pane/layers.tsx +5 -0
  58. package/src/components/onboarding/account-step/chooser-panel.tsx +74 -0
  59. package/src/components/onboarding/account-step/form-panel.tsx +157 -0
  60. package/src/components/onboarding/account-step/index.tsx +58 -0
  61. package/src/components/onboarding/account-step/model.test.ts +67 -0
  62. package/src/components/onboarding/account-step/model.ts +126 -0
  63. package/src/components/onboarding/account-step/signed-in-panel.tsx +31 -0
  64. package/src/components/onboarding/onboarding-steps.tsx +91 -55
  65. package/src/components/onboarding/onboarding-wizard.test.tsx +73 -6
  66. package/src/components/onboarding/onboarding-wizard.tsx +93 -9
  67. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +3 -3
  68. package/src/components/onboarding/wizard-account.ts +235 -0
  69. package/src/components/onboarding/wizard-keyboard.ts +67 -1
  70. package/src/components/onboarding/wizard-model.ts +2 -2
  71. package/src/components/ui/data-table/index.tsx +1 -0
  72. package/src/components/ui/data-table/opentui/index.tsx +44 -2
  73. package/src/components/ui/data-table/types.ts +10 -0
  74. package/src/components/ui/data-table/visible-range.test.ts +29 -0
  75. package/src/components/ui/data-table/visible-range.ts +21 -0
  76. package/src/components/ui/ui.test.tsx +23 -1
  77. package/src/i18n/es.ts +1093 -0
  78. package/src/i18n/index.test.ts +5 -2
  79. package/src/i18n/index.ts +3 -0
  80. package/src/i18n/ja.ts +66 -3
  81. package/src/i18n/ko.ts +66 -3
  82. package/src/i18n/languages.test.ts +4 -1
  83. package/src/i18n/languages.ts +12 -1
  84. package/src/i18n/zh-cn.ts +66 -3
  85. package/src/i18n/zh-tw.ts +66 -3
  86. package/src/market-data/coordinator/index.ts +1 -1
  87. package/src/market-data/coordinator/quotes.ts +64 -1
  88. package/src/market-data/coordinator/subscriptions.test.ts +72 -0
  89. package/src/market-data/quotes/cadence.ts +2 -0
  90. package/src/plugins/builtin/account-management/footer.ts +9 -3
  91. package/src/plugins/builtin/account-management/model.ts +53 -1
  92. package/src/plugins/builtin/account-management/pane.tsx +107 -24
  93. package/src/plugins/builtin/ai/index.tsx +7 -2
  94. package/src/plugins/builtin/ai/screener/market-runtime.ts +4 -2
  95. package/src/plugins/builtin/ai/screener/pane.tsx +3 -0
  96. package/src/plugins/builtin/chart-composer/editor.tsx +0 -1
  97. package/src/plugins/builtin/chart-composer/index.tsx +10 -2
  98. package/src/plugins/builtin/chart-composer/pane.tsx +111 -44
  99. package/src/plugins/builtin/chart-composer/quick-add.tsx +27 -3
  100. package/src/plugins/builtin/chart-composer/semantic.ts +21 -0
  101. package/src/plugins/builtin/chat/controller/persistence.ts +5 -0
  102. package/src/plugins/builtin/cloud/plugin.tsx +9 -1
  103. package/src/plugins/builtin/cloud/upgrade-command.ts +28 -0
  104. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +54 -0
  105. package/src/plugins/builtin/help/index.tsx +38 -0
  106. package/src/plugins/builtin/news/wire/news/footer.ts +18 -38
  107. package/src/plugins/builtin/options/footer.test.ts +20 -112
  108. package/src/plugins/builtin/options/footer.ts +35 -78
  109. package/src/plugins/builtin/options/index.tsx +6 -0
  110. package/src/plugins/builtin/options/live-quotes.test.ts +117 -14
  111. package/src/plugins/builtin/options/live-quotes.ts +93 -39
  112. package/src/plugins/builtin/options/view.test.tsx +5 -2
  113. package/src/plugins/builtin/options/view.tsx +49 -9
  114. package/src/plugins/builtin/portfolio-list/index.tsx +12 -4
  115. package/src/plugins/builtin/portfolio-list/pane/index.tsx +22 -1
  116. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +17 -13
  117. package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.test.tsx +76 -0
  118. package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.ts +78 -0
  119. package/src/plugins/builtin/shared/cloud-upgrade.ts +131 -0
  120. package/src/plugins/builtin/shared/live-streaming.ts +60 -0
  121. package/src/plugins/builtin/shared/plan-access.test.ts +69 -0
  122. package/src/plugins/builtin/shared/plan-access.ts +109 -0
  123. package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +1 -1
  124. package/src/plugins/builtin/ticker-detail/index.tsx +11 -2
  125. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -3
  126. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +4 -2
  127. package/src/plugins/registry/index.test.ts +51 -0
  128. package/src/plugins/registry/index.ts +12 -0
  129. package/src/plugins/registry/pane-settings.ts +29 -0
  130. package/src/renderers/electrobun/view/data-table/index.tsx +44 -3
  131. package/src/renderers/electrobun/view/host/box.test.tsx +97 -0
  132. package/src/renderers/electrobun/view/host/box.tsx +3 -0
  133. package/src/renderers/electrobun/view/host/chart-surface.tsx +85 -1
  134. package/src/renderers/electrobun/view/host/focus-scope.test.ts +37 -0
  135. package/src/renderers/electrobun/view/host/focus-scope.ts +56 -0
  136. package/src/renderers/electrobun/view/host/input.tsx +16 -1
  137. package/src/renderers/electrobun/view/host/mouse.ts +1 -0
  138. package/src/renderers/electrobun/view/host/style.ts +1 -1
  139. package/src/renderers/electrobun/view/host/tabs.tsx +4 -2
  140. package/src/renderers/electrobun/view/main.tsx +2 -0
  141. package/src/renderers/opentui/chart-surface.tsx +2 -0
  142. package/src/renderers/opentui/host.tsx +3 -0
  143. package/src/sources/gloomberb-cloud/index.test.ts +8 -0
  144. package/src/sources/gloomberb-cloud/index.ts +4 -4
  145. package/src/sources/provider-router/history.test.ts +3 -3
  146. package/src/sources/provider-router/history.ts +15 -4
  147. package/src/sources/yahoo-finance/history.test.ts +47 -0
  148. package/src/sources/yahoo-finance/history.ts +5 -1
  149. package/src/state/hooks/quote-streaming.test.tsx +181 -1
  150. package/src/state/hooks/quote-streaming.ts +93 -7
  151. package/src/theme/colors.ts +20 -1
  152. package/src/theme/themes.test.ts +3 -0
  153. package/src/{sources/gloomberb-cloud → time-series}/history-quality.ts +27 -3
  154. package/src/time-series/resolution.test.ts +51 -0
  155. package/src/time-series/resolution.ts +41 -0
  156. package/src/time-series/resolve.test.ts +207 -1
  157. package/src/time-series/resolve.ts +196 -29
  158. package/src/time-series/spec-studies.test.ts +4 -0
  159. package/src/time-series/spec.ts +15 -1
  160. package/src/time-series/types.ts +7 -0
  161. package/src/time-series/use-chart-resolution.test.tsx +37 -0
  162. package/src/time-series/use-chart-resolution.ts +17 -1
  163. package/src/types/plugin.ts +9 -0
  164. package/src/ui/host.tsx +17 -0
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { Box, Text } from "../../../ui";
2
+ import { Box, Text, useUiHost } from "../../../ui";
3
3
  import {
4
4
  ChoiceDialog,
5
5
  Tabs,
@@ -13,7 +13,8 @@ import {
13
13
  import { CompositeChart } from "../../../components/chart/composite";
14
14
  import type { PaneProps, TickerResearchTabProps } from "../../../types/plugin";
15
15
  import type { ChartResolution, TimeRange } from "../../../components/chart/core/types";
16
- import type { ChartSpec, SeriesStyle } from "../../../time-series/types";
16
+ import type { ChartSpec, ResolvedSeries, SeriesStyle } from "../../../time-series/types";
17
+ import { getSupportedChartResolutionsForViewport } from "../../../time-series/resolution";
17
18
  import { useResolvedChartSpec } from "../../../time-series/hooks";
18
19
  import { useShortcut } from "../../../react/input";
19
20
  import { useDialog, useDialogState, type PromptContext } from "../../../ui/dialog";
@@ -60,13 +61,9 @@ import {
60
61
  } from "./settings";
61
62
  import { resolveChartComposerShortcut } from "./shortcuts";
62
63
  import { ChartSeriesQuickAdd } from "./quick-add";
64
+ import { useLiveStreamingSetting } from "../shared/live-streaming";
63
65
 
64
66
  const RANGE_TABS = RANGES.map((range, index) => ({ label: `${index + 1}:${range}`, value: range }));
65
- const RESOLUTION_TABS = RESOLUTIONS.map((value) => ({ label: value.toUpperCase(), value }));
66
- const RESOLUTION_TABS_WIDTH = RESOLUTION_TABS.reduce(
67
- (width, tab) => width + [...tab.label].length + 1,
68
- 0,
69
- );
70
67
  const AUTO_VIEWPORT_DEBOUNCE_MS = 350;
71
68
 
72
69
  interface RuntimeChartViewport {
@@ -92,6 +89,8 @@ interface ChartComposerSurfaceProps {
92
89
  onCapture?: (capturing: boolean) => void;
93
90
  }
94
91
 
92
+ const QUICK_ADD_CAPTURE = "quick-add";
93
+
95
94
  function isPriceStudyTarget(spec: ChartSpec): boolean {
96
95
  return spec.series.some((series) => (
97
96
  series.source.kind === "security"
@@ -110,7 +109,9 @@ function ChartComposerSurface({
110
109
  }: ChartComposerSurfaceProps) {
111
110
  const dialog = useDialog();
112
111
  const dispatch = useAppDispatch();
112
+ const isDesktopWeb = useUiHost().kind === "desktop-web";
113
113
  const paneId = usePaneInstanceId();
114
+ const liveStreaming = useLiveStreamingSetting();
114
115
  const dialogOpen = useDialogState((state) => state.isOpen);
115
116
  const authoredViewportKey = useMemo(() => JSON.stringify({
116
117
  range: spec.viewport.range,
@@ -149,19 +150,73 @@ function ChartComposerSurface({
149
150
  : null,
150
151
  requestViewport: activeRuntimeViewport?.requestViewport,
151
152
  targetPointCount,
153
+ liveStreaming,
152
154
  });
155
+ const availableResolutions = useMemo<ChartResolution[]>(() => {
156
+ if (!resolution.resolutionSupport) {
157
+ if (!resolution.loading) return RESOLUTIONS;
158
+ return spec.viewport.resolution === "auto"
159
+ ? ["auto"]
160
+ : ["auto", spec.viewport.resolution];
161
+ }
162
+ const supported = new Set(getSupportedChartResolutionsForViewport(
163
+ spec.viewport.range,
164
+ resolution.resolutionSupport,
165
+ spec.viewport.dateWindow,
166
+ ));
167
+ return RESOLUTIONS.filter((value) => value === "auto" || supported.has(value));
168
+ }, [
169
+ resolution.loading,
170
+ resolution.resolutionSupport,
171
+ spec.viewport.dateWindow,
172
+ spec.viewport.range,
173
+ spec.viewport.resolution,
174
+ ]);
175
+ const resolutionTabs = useMemo(
176
+ () => availableResolutions.map((value) => ({ label: value.toUpperCase(), value })),
177
+ [availableResolutions],
178
+ );
179
+ const resolutionTabsWidth = useMemo(
180
+ () => resolutionTabs.reduce((total, tab) => total + [...tab.label].length + 1, 0),
181
+ [resolutionTabs],
182
+ );
153
183
  const selectedStudies = getSelectedBuiltinStudies(spec);
154
184
  const selectedPairStudies = getSelectedPairStudies(spec);
155
185
  const inlineStyleTarget = useMemo(() => getChartInlineStyleTarget(spec), [spec]);
156
186
  const styles = useMemo(() => getChartInlineStyles(spec), [spec]);
157
- const styleTabs = useMemo(
158
- () => styles.map((value) => ({ label: value.toUpperCase(), value })),
159
- [styles],
160
- );
161
187
  const inlineStyle = inlineStyleTarget?.style ?? "line";
162
188
  const inlineStyleLabel = inlineStyleTarget ? chartSeriesLabel(inlineStyleTarget) : "";
163
189
  const viewport = resolution.viewport;
164
190
  const baseSeriesIds = useMemo(() => new Set(spec.series.map((series) => series.id)), [spec.series]);
191
+ // Hidden series are never loaded, so the resolver has nothing to report for
192
+ // them. Without a placeholder they vanish from the legend entirely and the
193
+ // only way back is the series dialog.
194
+ const legendSeries = useMemo(() => {
195
+ const resolved = resolution.legendSeries ?? [];
196
+ const resolvedIds = new Set(resolved.map((entry) => entry.id));
197
+ const missing = spec.series.filter((entry) => !resolvedIds.has(entry.id));
198
+ if (missing.length === 0) return resolution.legendSeries;
199
+ const bySpecOrder = new Map(spec.series.map((entry, index) => [entry.id, index] as const));
200
+ return [
201
+ ...resolved,
202
+ ...missing.map((entry): ResolvedSeries => ({
203
+ id: entry.id,
204
+ label: chartSeriesLabel(entry),
205
+ color: entry.color ?? colors.textDim,
206
+ unit: "",
207
+ unitGroup: "unknown",
208
+ nativeFrequency: "daily",
209
+ dataShape: "scalar",
210
+ style: entry.style,
211
+ transform: entry.transform,
212
+ axis: entry.axis === "right" ? "right" : "left",
213
+ panelId: entry.panelId,
214
+ interpolation: entry.interpolation,
215
+ hidden: true,
216
+ points: [],
217
+ })),
218
+ ].sort((a, b) => (bySpecOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (bySpecOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER));
219
+ }, [resolution.legendSeries, spec.series]);
165
220
  const plottedSeries = useMemo(
166
221
  () => projectVisibleChartSeries(
167
222
  spec,
@@ -171,6 +226,8 @@ function ChartComposerSurface({
171
226
  [resolution.bufferedSeries, resolution.legendSeries, resolution.series, spec],
172
227
  );
173
228
  const [interactionCaptured, setInteractionCapturedState] = useState(false);
229
+ // Typing in quick-add must not freeze the plot: it only takes the keyboard.
230
+ const [modalCaptured, setModalCaptured] = useState(false);
174
231
  const [quickAddWidth, setQuickAddWidth] = useState(14);
175
232
  const interactionCaptureRef = useRef(false);
176
233
  const interactionCaptureSourcesRef = useRef(new Set<string>());
@@ -183,6 +240,7 @@ function ChartComposerSurface({
183
240
  if (captured) sources.add(source);
184
241
  else sources.delete(source);
185
242
  const next = sources.size > 0;
243
+ setModalCaptured([...sources].some((entry) => entry !== QUICK_ADD_CAPTURE));
186
244
  if (interactionCaptureRef.current === next) return;
187
245
  interactionCaptureRef.current = next;
188
246
  setInteractionCapturedState(next);
@@ -197,6 +255,8 @@ function ChartComposerSurface({
197
255
  [setInteractionCaptured],
198
256
  );
199
257
  const surfaceInteractive = !dialogOpen && !interactionCaptured;
258
+ /** The plot keeps its pointer unless something modal is actually covering it. */
259
+ const surfacePointerInteractive = !dialogOpen && !modalCaptured;
200
260
  const shortcutActive = focused && surfaceInteractive;
201
261
  const activatePane = useCallback(() => {
202
262
  if (!focused) dispatch({ type: "FOCUS_PANE", paneId });
@@ -257,7 +317,11 @@ function ChartComposerSurface({
257
317
  useRemoteUiNode({
258
318
  role: "chart-data",
259
319
  label: "Rendered chart composer data",
260
- metadata: chartComposerSemanticMetadata(spec, resolution),
320
+ metadata: chartComposerSemanticMetadata(
321
+ spec,
322
+ resolution,
323
+ activeRuntimeViewport?.requestViewport,
324
+ ),
261
325
  });
262
326
 
263
327
  const openSeriesEditor = useCallback(async () => {
@@ -310,6 +374,15 @@ function ChartComposerSurface({
310
374
  const setResolution = useCallback((next: ChartResolution) => {
311
375
  setSpec({ ...spec, viewport: { ...spec.viewport, resolution: next } });
312
376
  }, [setSpec, spec]);
377
+ useEffect(() => {
378
+ if (
379
+ spec.viewport.resolution === "auto"
380
+ || availableResolutions.includes(spec.viewport.resolution)
381
+ ) {
382
+ return;
383
+ }
384
+ setResolution("auto");
385
+ }, [availableResolutions, setResolution, spec.viewport.resolution]);
313
386
  const setInlineStyle = useCallback((style: SeriesStyle) => {
314
387
  if (!inlineStyleTarget || !styles.includes(style)) return;
315
388
  setSpec({
@@ -352,7 +425,7 @@ function ChartComposerSurface({
352
425
  {...context}
353
426
  title="Chart Resolution"
354
427
  selectedChoiceId={spec.viewport.resolution}
355
- choices={RESOLUTIONS.map((value) => ({
428
+ choices={availableResolutions.map((value) => ({
356
429
  id: value,
357
430
  label: value.toUpperCase(),
358
431
  description: value === "auto"
@@ -362,11 +435,17 @@ function ChartComposerSurface({
362
435
  />
363
436
  ),
364
437
  }).catch(() => "");
365
- if (RESOLUTIONS.includes(next as ChartResolution)) setResolution(next as ChartResolution);
438
+ if (availableResolutions.includes(next as ChartResolution)) setResolution(next as ChartResolution);
366
439
  } finally {
367
440
  setInteractionCaptured("prompt", false);
368
441
  }
369
- }, [dialog, setInteractionCaptured, setResolution, spec.viewport.resolution]);
442
+ }, [
443
+ availableResolutions,
444
+ dialog,
445
+ setInteractionCaptured,
446
+ setResolution,
447
+ spec.viewport.resolution,
448
+ ]);
370
449
  const openModePicker = useCallback(async () => {
371
450
  if (styles.length === 0) return;
372
451
  setInteractionCaptured("prompt", true);
@@ -400,7 +479,7 @@ function ChartComposerSurface({
400
479
  [baseSeriesIds, spec],
401
480
  );
402
481
  const handleQuickAddActiveChange = useCallback(
403
- (active: boolean) => setInteractionCaptured("quick-add", active),
482
+ (active: boolean) => setInteractionCaptured(QUICK_ADD_CAPTURE, active),
404
483
  [setInteractionCaptured],
405
484
  );
406
485
  const openIndicators = useCallback((event?: PaneFooterPressEvent) => {
@@ -505,7 +584,15 @@ function ChartComposerSurface({
505
584
  return (
506
585
  <Box flexDirection="column" width={width} height={height} backgroundColor={colors.panel}>
507
586
  <Box flexDirection="row" height={1} paddingX={1} gap={0} overflow="hidden">
508
- <Box flexShrink={0} height={1} maxWidth={52} overflow="hidden">
587
+ <Box
588
+ flexShrink={0}
589
+ height={1}
590
+ maxWidth={52}
591
+ overflow="hidden"
592
+ // Desktop tabs are laid out in pixels, so a cell budget clips them
593
+ // while the row still has room to spare.
594
+ style={isDesktopWeb ? { maxWidth: "none", width: "auto" } : undefined}
595
+ >
509
596
  <Tabs
510
597
  tabs={RANGE_TABS}
511
598
  activeValue={spec.viewport.dateWindow ? null : spec.viewport.range}
@@ -522,13 +609,14 @@ function ChartComposerSurface({
522
609
  <Box
523
610
  flexShrink={1}
524
611
  minWidth={0}
525
- width={RESOLUTION_TABS_WIDTH}
612
+ width={resolutionTabsWidth}
526
613
  height={1}
527
614
  overflow="hidden"
615
+ style={isDesktopWeb ? { width: "auto", flexShrink: 0 } : undefined}
528
616
  data-gloom-role="chart-resolution-control"
529
617
  >
530
618
  <Tabs
531
- tabs={RESOLUTION_TABS}
619
+ tabs={resolutionTabs}
532
620
  activeValue={spec.viewport.resolution}
533
621
  onSelect={(value) => setResolution(value as ChartResolution)}
534
622
  compact
@@ -538,29 +626,6 @@ function ChartComposerSurface({
538
626
  keyboardNavigation={false}
539
627
  />
540
628
  </Box>
541
- {width >= 132 && inlineStyleTarget && (
542
- <Box
543
- flexDirection="row"
544
- flexShrink={0}
545
- maxWidth={64}
546
- height={1}
547
- overflow="hidden"
548
- data-gloom-role="chart-inline-style"
549
- data-gloom-label={`${inlineStyleLabel} style`}
550
- >
551
- <Text fg={colors.textDim}>{`${inlineStyleLabel}: `}</Text>
552
- <Tabs
553
- tabs={styleTabs}
554
- activeValue={inlineStyle}
555
- onSelect={(value) => setInlineStyle(value as SeriesStyle)}
556
- compact
557
- dense
558
- variant="bare"
559
- focused={focused}
560
- keyboardNavigation={false}
561
- />
562
- </Box>
563
- )}
564
629
  </Box>
565
630
  <MultiSelectDialogButton
566
631
  ref={indicatorsDialogRef}
@@ -594,7 +659,7 @@ function ChartComposerSurface({
594
659
  <Box flexGrow={1} minHeight={4}>
595
660
  <CompositeChart
596
661
  series={plottedSeries}
597
- legendSeries={resolution.legendSeries}
662
+ legendSeries={legendSeries}
598
663
  timelineSeries={resolution.timelineSeries}
599
664
  panels={spec.panels}
600
665
  viewport={viewport}
@@ -602,7 +667,9 @@ function ChartComposerSurface({
602
667
  width={Math.max(1, width)}
603
668
  height={Math.max(4, height - 1)}
604
669
  focused={focused}
605
- interactive={surfaceInteractive}
670
+ interactive={surfacePointerInteractive}
671
+ allowHistoricalBackfill
672
+ showLatestChangePercent={!spec.viewport.dateWindow && spec.viewport.range === "1D"}
606
673
  onViewportChange={handleChartViewportChange}
607
674
  onActivate={activatePane}
608
675
  onToggleSeries={toggleSeries}
@@ -34,6 +34,19 @@ interface ClosestElementLike {
34
34
  } | null;
35
35
  }
36
36
 
37
+ /**
38
+ * Blurs the field itself rather than going through the imperative handle, which
39
+ * only reaches the element it still owns. A no-op unless this widget truly holds
40
+ * the document focus, so a click inside a dialog leaves that dialog alone.
41
+ */
42
+ export function releaseQuickAddFocus(quickAddId: string): void {
43
+ const active = (globalThis as {
44
+ document?: { activeElement?: { blur?: () => void } | null };
45
+ }).document?.activeElement;
46
+ if (!active || !isChartQuickAddMouseTarget(active, quickAddId)) return;
47
+ active.blur?.();
48
+ }
49
+
37
50
  export function isChartQuickAddMouseTarget(target: unknown, quickAddId: string): boolean {
38
51
  if (!target || typeof target !== "object") return false;
39
52
  const candidate = target as Partial<ClosestElementLike> & {
@@ -197,16 +210,18 @@ export function ChartSeriesQuickAdd({
197
210
  }, [active, focused]);
198
211
 
199
212
  useEffect(() => {
200
- if (ui.kind !== "desktop-web" || !inputFocused) return;
213
+ if (ui.kind !== "desktop-web") return;
201
214
 
215
+ // Watch regardless of what state believes: the field can hold the document
216
+ // focus after the state that tracks it has already moved on.
202
217
  const handleOutsideMouseDown = (event: globalThis.MouseEvent) => {
203
218
  if (isChartQuickAddMouseTarget(event.target, quickAddId)) return;
204
- inputRef.current?.blur?.();
219
+ releaseQuickAddFocus(quickAddId);
205
220
  };
206
221
 
207
222
  document.addEventListener("mousedown", handleOutsideMouseDown, true);
208
223
  return () => document.removeEventListener("mousedown", handleOutsideMouseDown, true);
209
- }, [inputFocused, quickAddId, ui.kind]);
224
+ }, [quickAddId, ui.kind]);
210
225
 
211
226
  const cancelPendingBlur = useCallback(() => {
212
227
  if (blurTimerRef.current === null) return;
@@ -216,10 +231,17 @@ export function ChartSeriesQuickAdd({
216
231
 
217
232
  useEffect(() => cancelPendingBlur, [cancelPendingBlur]);
218
233
 
234
+ // Writing the field programmatically comes back through onChange, which is
235
+ // otherwise indistinguishable from a keystroke and re-arms what just closed.
236
+ const clearingRef = useRef(false);
219
237
  const clearInput = useCallback(() => {
238
+ clearingRef.current = true;
220
239
  setQuery("");
221
240
  inputRef.current?.editBuffer.setText?.("");
222
241
  inputRef.current?.setCursorOffset?.(0);
242
+ queueMicrotask(() => {
243
+ clearingRef.current = false;
244
+ });
223
245
  }, []);
224
246
 
225
247
  const focusInput = useCallback(() => {
@@ -320,6 +342,7 @@ export function ChartSeriesQuickAdd({
320
342
  zIndex={30}
321
343
  data-gloom-role="chart-series-quick-add"
322
344
  data-gloom-chart-quick-add={quickAddId}
345
+ data-gloom-focus-scope="chart-series-quick-add"
323
346
  >
324
347
  <InlineQuickAddRow
325
348
  value={query}
@@ -335,6 +358,7 @@ export function ChartSeriesQuickAdd({
335
358
  onChange={(value) => {
336
359
  setQuery(value);
337
360
  setError(null);
361
+ if (clearingRef.current) return;
338
362
  if (!active) setActive(true);
339
363
  if (!inputFocused) setInputFocused(true);
340
364
  }}
@@ -15,6 +15,15 @@ function pointEvidence(point: TimeSeriesPoint | undefined) {
15
15
  : null;
16
16
  }
17
17
 
18
+ function viewportEvidence(viewport: UseChartResolutionResult["viewport"]) {
19
+ if (!viewport) return null;
20
+ return {
21
+ start: viewport.start.toISOString(),
22
+ end: viewport.end.toISOString(),
23
+ spanMs: viewport.end.getTime() - viewport.start.getTime(),
24
+ };
25
+ }
26
+
18
27
  function sourceEvidence(series: ChartSeriesSpec) {
19
28
  return series.source.kind === "security"
20
29
  ? {
@@ -36,6 +45,7 @@ function sourceEvidence(series: ChartSeriesSpec) {
36
45
  export function chartComposerSemanticMetadata(
37
46
  spec: ChartSpec,
38
47
  resolution: UseChartResolutionResult,
48
+ runtimeViewport?: { start: Date; end: Date } | null,
39
49
  ): Record<string, unknown> {
40
50
  const resolvedById = new Map(resolution.series.map((series) => [series.id, series] as const));
41
51
  const symbols = [...new Set(spec.series.flatMap((series) => (
@@ -54,6 +64,7 @@ export function chartComposerSemanticMetadata(
54
64
  panelId: series.panelId,
55
65
  visible: series.visible !== false,
56
66
  pointCount: resolved?.points.length ?? 0,
67
+ latestChangePercent: resolved?.latestChangePercent ?? null,
57
68
  first: pointEvidence(resolved?.points[0]),
58
69
  last: pointEvidence(resolved?.points.at(-1)),
59
70
  };
@@ -69,7 +80,16 @@ export function chartComposerSemanticMetadata(
69
80
  loading: resolution.loading,
70
81
  errors: resolution.errors,
71
82
  warnings: resolution.warnings,
83
+ resolutionSupport: resolution.resolutionSupport ?? null,
84
+ viewport: viewportEvidence(runtimeViewport ?? resolution.viewport),
85
+ authoredViewport: viewportEvidence(resolution.viewport),
72
86
  baseSeries,
87
+ bufferedSeries: (resolution.bufferedSeries ?? []).map((series) => ({
88
+ id: series.id,
89
+ pointCount: series.points.length,
90
+ first: pointEvidence(series.points[0]),
91
+ last: pointEvidence(series.points.at(-1)),
92
+ })),
73
93
  resolvedSeries: resolution.series.map((series) => ({
74
94
  id: series.id,
75
95
  label: series.label,
@@ -77,6 +97,7 @@ export function chartComposerSemanticMetadata(
77
97
  transform: series.transform,
78
98
  axis: series.axis,
79
99
  panelId: series.panelId,
100
+ timeBasis: series.timeBasis ?? null,
80
101
  pointCount: series.points.length,
81
102
  first: pointEvidence(series.points[0]),
82
103
  last: pointEvidence(series.points.at(-1)),
@@ -22,6 +22,9 @@ export interface ChatSessionUser {
22
22
  username: string;
23
23
  emailVerified: boolean;
24
24
  plan?: "free" | "pro";
25
+ /** Cached so a cold start renders the right entitlement before /auth/get-session lands. */
26
+ trialEndsAt?: string | null;
27
+ effectivePlan?: "free" | "pro";
25
28
  }
26
29
 
27
30
  export function readChatSessionState(
@@ -42,6 +45,8 @@ export function normalizeSessionUser(user: PersistedAuthUser | null | undefined)
42
45
  username: user.username ?? user.name ?? "account",
43
46
  emailVerified: user.emailVerified === true,
44
47
  plan: user.plan,
48
+ trialEndsAt: user.trialEndsAt ?? undefined,
49
+ effectivePlan: user.effectivePlan,
45
50
  }
46
51
  : null;
47
52
  }
@@ -21,6 +21,8 @@ import {
21
21
  import { registerTwitterFeedFeature } from "../cloud-tweets/registration";
22
22
  import { composeBuiltinPlugin, type PluginModule } from "../plugin-module";
23
23
  import { registerCloudAuthCommands } from "./auth-commands";
24
+ import { registerCloudUpgradeCommand } from "./upgrade-command";
25
+ import { CloudUpgradeStatusWidget } from "./upgrade-status-widget";
24
26
 
25
27
  interface GloomberbCloudPluginComponents {
26
28
  ChatPane: (props: PaneProps) => ReactNode;
@@ -137,7 +139,13 @@ const accountModule: PluginModule = {
137
139
  shortcut: { prefix: "ACM" },
138
140
  createInstance: () => ({ placement: "floating" }),
139
141
  }],
140
- setup: registerCloudAuthCommands,
142
+ slots: {
143
+ "status:widget": () => <CloudUpgradeStatusWidget />,
144
+ },
145
+ setup: (ctx) => {
146
+ registerCloudAuthCommands(ctx);
147
+ registerCloudUpgradeCommand(ctx);
148
+ },
141
149
  };
142
150
 
143
151
  const buildoutModule: PluginModule = {
@@ -0,0 +1,28 @@
1
+ import { apiClient } from "../../../api-client";
2
+ import type { GloomPluginContext } from "../../../types/plugin";
3
+ import { requestAccountManagementTab } from "../account-management/navigation";
4
+ import { openCloudUpgradeUrl } from "../shared/cloud-upgrade";
5
+ import { resolvePlanAccess } from "../shared/plan-access";
6
+
7
+ export function registerCloudUpgradeCommand(ctx: GloomPluginContext): void {
8
+ ctx.registerCommand({
9
+ id: "cloud-upgrade",
10
+ label: "Upgrade to Pro",
11
+ description: "Real-time market data, real-time news, and X data",
12
+ keywords: ["upgrade", "pro", "pricing", "plan", "billing", "subscribe", "trial", "real-time"],
13
+ category: "config",
14
+ shortcut: "UPGRADE",
15
+ // Paying subscribers have nothing to upgrade to; trial accounts still do.
16
+ hidden: () => resolvePlanAccess(apiClient.getCurrentUser()).isPayingPro,
17
+ execute: () => {
18
+ if (!apiClient.getSessionToken()) {
19
+ ctx.openCommandBar("Sign Up");
20
+ return;
21
+ }
22
+ if (openCloudUpgradeUrl()) return;
23
+ // No renderer-backed opener yet: fall back to the in-app billing surface.
24
+ requestAccountManagementTab("pro");
25
+ ctx.showPane("account-management");
26
+ },
27
+ });
28
+ }
@@ -0,0 +1,54 @@
1
+ import { useEffect, useState } from "react";
2
+ import { apiClient } from "../../../api-client";
3
+ import { t, tf } from "../../../i18n";
4
+ import { useAppLanguage } from "../../../i18n/react";
5
+ import { useAppSelector } from "../../../state/app/context";
6
+ import { colors, hoverBg } from "../../../theme/colors";
7
+ import { Box, Text } from "../../../ui";
8
+ import { chatController, type ChatController } from "../chat/controller";
9
+ import { useCloudPlanAction, useCloudUpgradeAction } from "../shared/cloud-upgrade";
10
+ import { resolvePlanAccess } from "../shared/plan-access";
11
+
12
+ interface CloudUpgradeStatusWidgetProps {
13
+ controller?: Pick<ChatController, "getSnapshot" | "subscribe">;
14
+ }
15
+
16
+ /**
17
+ * Global entitlement status: the trial countdown while Pro is on loan, or the
18
+ * delay free accounts get on cloud data. Signed-out users already have sign-in
19
+ * affordances next to it, and paying subscribers have nothing to report.
20
+ */
21
+ export function CloudUpgradeStatusWidget({ controller = chatController }: CloudUpgradeStatusWidgetProps) {
22
+ useAppLanguage();
23
+ const cloudPluginDisabled = useAppSelector((state) => state.config.disabledPlugins).includes("gloomberb-cloud");
24
+ const openUpgrade = useCloudUpgradeAction();
25
+ const openPlan = useCloudPlanAction();
26
+ const [access, setAccess] = useState(() => resolvePlanAccess(apiClient.getCurrentUser()));
27
+ const [hovered, setHovered] = useState(false);
28
+
29
+ useEffect(
30
+ // The chat widget already drives session refreshes; this only follows them.
31
+ () => controller.subscribe(() => setAccess(resolvePlanAccess(apiClient.getCurrentUser()))),
32
+ [controller],
33
+ );
34
+
35
+ if (cloudPluginDisabled || !access.signedIn || access.isPayingPro) return null;
36
+
37
+ const trial = access.isTrialActive;
38
+ const label = trial
39
+ ? tf("Pro trial {days}d", { days: access.trialDaysLeft })
40
+ : t("delayed data · upgrade");
41
+
42
+ return (
43
+ <Box
44
+ flexDirection="row"
45
+ paddingRight={1}
46
+ backgroundColor={hovered ? hoverBg() : undefined}
47
+ onMouseOver={() => setHovered((current) => (current ? current : true))}
48
+ onMouseOut={() => setHovered((current) => (current ? false : current))}
49
+ onMouseDown={trial ? openPlan : openUpgrade}
50
+ >
51
+ <Text fg={trial ? colors.positive : colors.warning}>{label}</Text>
52
+ </Box>
53
+ );
54
+ }
@@ -153,6 +153,40 @@ function HelpPane({ focused, width, height }: PaneProps) {
153
153
  />
154
154
  </HelpSection>
155
155
 
156
+ <HelpSection title="Charts">
157
+ <ShortcutRow
158
+ badges={["Drag", "Scroll"]}
159
+ description="Pan the chart through time."
160
+ />
161
+ <ShortcutRow
162
+ badges={["Shift+Z", "Shift+Drag"]}
163
+ description="Pick the zoom tool, then drag a time range to zoom into."
164
+ />
165
+ <ShortcutRow
166
+ badges={["Shift+M", "Alt+Drag"]}
167
+ description="Pick the ruler, then drag to measure change, percent, bars, and elapsed time."
168
+ />
169
+ <ShortcutRow
170
+ badges={["Shift+D", "Shift+P"]}
171
+ description="Draw a trend line or a freehand shape. Grab a line's end to reshape it, drag its middle to move it."
172
+ />
173
+ <ShortcutRow
174
+ badges={["c", "Backspace"]}
175
+ description="Cycle the drawing colour, or delete the selected drawing."
176
+ />
177
+ <ShortcutRow
178
+ badges={["Ctrl+Scroll"]}
179
+ description="Zoom around the pointer."
180
+ />
181
+ <ShortcutRow
182
+ badges={["+/-", "0"]}
183
+ description="Zoom the focused chart in or out, or reset it."
184
+ />
185
+ <Text fg={colors.textDim} wrapText>
186
+ {t("The tool icons sit over the top-left corner of the chart. Most terminals keep shift-drag and option-drag for their own text selection, so pick the tool there instead.")}
187
+ </Text>
188
+ </HelpSection>
189
+
156
190
  <HelpSection title="Global Keys">
157
191
  <ShortcutRow
158
192
  badges={["Tab", "Shift+Tab"]}
@@ -327,6 +361,10 @@ function HelpPane({ focused, width, height }: PaneProps) {
327
361
  badges={["DES", "<ticker>"]}
328
362
  description="Open security details for a specific ticker."
329
363
  />
364
+ <ShortcutRow
365
+ badges={["UPGRADE"]}
366
+ description="Go Pro for real-time data at gloom.sh/cloud, free for 7 days."
367
+ />
330
368
  <ShortcutRow
331
369
  badges={["Up/Down", "Ctrl+P/N"]}
332
370
  description="Move through command bar results."