gloomberb 0.12.0 → 0.13.2

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 (219) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -2
  3. package/src/api-client/auth.ts +2 -1
  4. package/src/api-client/index.test.ts +34 -0
  5. package/src/api-client/index.ts +10 -2
  6. package/src/api-client/request.ts +4 -1
  7. package/src/api-client/research-activity.ts +61 -0
  8. package/src/api-client/types.ts +2 -1
  9. package/src/cli/commands/plugins.ts +4 -3
  10. package/src/cli/desktop-pane-shot.ts +79 -10
  11. package/src/cli/pane-functions/capabilities.ts +76 -23
  12. package/src/cli/pane-functions/cloud-session.test.ts +54 -0
  13. package/src/cli/pane-functions/cloud-session.ts +38 -0
  14. package/src/cli/pane-functions/dom.test.ts +41 -0
  15. package/src/cli/pane-functions/dom.ts +199 -0
  16. package/src/cli/pane-functions/index.test.ts +13 -2
  17. package/src/cli/pane-functions/index.ts +9 -5
  18. package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
  19. package/src/cli/pane-functions/intraday-shot.ts +334 -0
  20. package/src/cli/pane-functions/report.test.ts +29 -1
  21. package/src/cli/pane-functions/report.ts +68 -7
  22. package/src/cli/pane-functions/screenshot.ts +195 -42
  23. package/src/cli/restore-plugins.ts +10 -3
  24. package/src/components/chart/composite/composite-chart.test.tsx +68 -1
  25. package/src/components/chart/composite/composite-chart.tsx +87 -21
  26. package/src/components/chart/composite/rasterizer.ts +33 -0
  27. package/src/components/chart/composite/renderers.test.ts +51 -4
  28. package/src/components/chart/composite/scene.test.ts +128 -2
  29. package/src/components/chart/composite/scene.ts +92 -16
  30. package/src/components/chart/composite/text-renderer.ts +13 -0
  31. package/src/components/chart/composite/time-scale.ts +30 -10
  32. package/src/components/chart/composite/types.ts +26 -0
  33. package/src/components/command-bar/assist/runtime.ts +38 -30
  34. package/src/components/command-bar/surface/assist.test.tsx +63 -9
  35. package/src/components/command-bar/surface/test-harness.tsx +23 -4
  36. package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
  37. package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
  38. package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
  39. package/src/components/onboarding/onboarding-wizard.tsx +49 -14
  40. package/src/data/config/store/index.test.ts +23 -0
  41. package/src/data/config/store/node.ts +7 -1
  42. package/src/data/config/store/normalize.ts +4 -0
  43. package/src/news/types.ts +1 -0
  44. package/src/plugins/builtin/cds/client.test.ts +20 -0
  45. package/src/plugins/builtin/cds/client.ts +13 -3
  46. package/src/plugins/builtin/cds/headless.test.ts +30 -0
  47. package/src/plugins/builtin/cds/headless.ts +113 -0
  48. package/src/plugins/builtin/cds/index.tsx +4 -0
  49. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
  50. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  51. package/src/plugins/builtin/chat/controller/channels.ts +2 -1
  52. package/src/plugins/builtin/chat/controller/index.ts +13 -5
  53. package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
  54. package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
  55. package/src/plugins/builtin/chat/controller/state.ts +1 -0
  56. package/src/plugins/builtin/chat/controller.test.ts +260 -4
  57. package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
  58. package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
  59. package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
  60. package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
  61. package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
  62. package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
  63. package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
  64. package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
  65. package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
  66. package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
  67. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
  68. package/src/plugins/builtin/cloud/plugin.tsx +2 -0
  69. package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
  70. package/src/plugins/builtin/congress-trades/client.ts +11 -0
  71. package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
  72. package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
  73. package/src/plugins/builtin/congress-trades/headless.ts +145 -0
  74. package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
  75. package/src/plugins/builtin/credit-conditions/client.ts +1 -1
  76. package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
  77. package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
  78. package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
  79. package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
  80. package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
  81. package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
  82. package/src/plugins/builtin/dividend-yield/model.ts +4 -22
  83. package/src/plugins/builtin/dividend-yield/view.ts +17 -0
  84. package/src/plugins/builtin/earnings/headless.test.ts +82 -0
  85. package/src/plugins/builtin/earnings/headless.ts +162 -0
  86. package/src/plugins/builtin/earnings/index.tsx +2 -0
  87. package/src/plugins/builtin/earnings-calls/data.ts +29 -8
  88. package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
  89. package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
  90. package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
  91. package/src/plugins/builtin/earnings-calls/model.ts +238 -0
  92. package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
  93. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
  94. package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
  95. package/src/plugins/builtin/fear-greed/headless.ts +84 -0
  96. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  97. package/src/plugins/builtin/holders/client.ts +29 -0
  98. package/src/plugins/builtin/holders/headless.test.ts +58 -0
  99. package/src/plugins/builtin/holders/headless.ts +162 -0
  100. package/src/plugins/builtin/holders/index.tsx +14 -8
  101. package/src/plugins/builtin/holders/pane.tsx +7 -1
  102. package/src/plugins/builtin/insider/client.ts +50 -0
  103. package/src/plugins/builtin/insider/headless.test.ts +122 -0
  104. package/src/plugins/builtin/insider/headless.ts +117 -0
  105. package/src/plugins/builtin/insider/index.tsx +21 -50
  106. package/src/plugins/builtin/insider/insider-data.ts +1 -1
  107. package/src/plugins/builtin/insider/model.ts +72 -0
  108. package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
  109. package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
  110. package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
  111. package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
  112. package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
  113. package/src/plugins/builtin/market-movers/client.ts +76 -0
  114. package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
  115. package/src/plugins/builtin/market-movers/headless.ts +73 -0
  116. package/src/plugins/builtin/market-movers/index.tsx +11 -51
  117. package/src/plugins/builtin/news/headless.test.ts +138 -0
  118. package/src/plugins/builtin/news/headless.ts +190 -0
  119. package/src/plugins/builtin/news/index.tsx +13 -9
  120. package/src/plugins/builtin/news/wire/index.tsx +2 -1
  121. package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
  122. package/src/plugins/builtin/research/analyst-headless.ts +172 -0
  123. package/src/plugins/builtin/research/analyst-model.ts +219 -0
  124. package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
  125. package/src/plugins/builtin/research/client.ts +12 -0
  126. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
  127. package/src/plugins/builtin/research/event-model.ts +245 -0
  128. package/src/plugins/builtin/research/events-headless.test.ts +32 -0
  129. package/src/plugins/builtin/research/events-headless.ts +87 -0
  130. package/src/plugins/builtin/research/headless.test.ts +93 -0
  131. package/src/plugins/builtin/research/headless.ts +161 -0
  132. package/src/plugins/builtin/research/index.tsx +32 -18
  133. package/src/plugins/builtin/research-search/pane.tsx +4 -0
  134. package/src/plugins/builtin/scanner/headless.test.ts +59 -0
  135. package/src/plugins/builtin/scanner/headless.ts +125 -0
  136. package/src/plugins/builtin/scanner/index.ts +4 -0
  137. package/src/plugins/builtin/sec/client.ts +16 -0
  138. package/src/plugins/builtin/sec/headless.test.ts +72 -0
  139. package/src/plugins/builtin/sec/headless.ts +69 -0
  140. package/src/plugins/builtin/sec/index.tsx +20 -75
  141. package/src/plugins/builtin/sec/model.ts +89 -0
  142. package/src/plugins/builtin/sectors/client.ts +54 -0
  143. package/src/plugins/builtin/sectors/headless.test.ts +28 -0
  144. package/src/plugins/builtin/sectors/headless.ts +80 -0
  145. package/src/plugins/builtin/sectors/index.tsx +6 -51
  146. package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
  147. package/src/plugins/builtin/short-interest/client.ts +5 -2
  148. package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
  149. package/src/plugins/builtin/short-interest/headless.ts +111 -0
  150. package/src/plugins/builtin/short-interest/index.tsx +15 -9
  151. package/src/plugins/builtin/thirteenf/data.ts +13 -11
  152. package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
  153. package/src/plugins/builtin/thirteenf/headless.ts +286 -0
  154. package/src/plugins/builtin/thirteenf/index.tsx +4 -0
  155. package/src/plugins/builtin/thirteenf/model.ts +13 -0
  156. package/src/plugins/builtin/thirteenf/table.ts +3 -15
  157. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
  158. package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
  159. package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
  160. package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
  161. package/src/plugins/builtin/volatility/client.ts +16 -3
  162. package/src/plugins/builtin/volatility/headless.test.ts +35 -0
  163. package/src/plugins/builtin/volatility/headless.ts +75 -0
  164. package/src/plugins/builtin/volatility/index.tsx +4 -0
  165. package/src/plugins/builtin/world-indices/client.ts +43 -0
  166. package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
  167. package/src/plugins/builtin/world-indices/headless.ts +109 -0
  168. package/src/plugins/builtin/world-indices/index.tsx +4 -0
  169. package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
  170. package/src/plugins/builtin/yield-curve/headless.ts +67 -0
  171. package/src/plugins/builtin/yield-curve/index.tsx +6 -8
  172. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
  173. package/src/plugins/bundle.test.ts +58 -1
  174. package/src/plugins/bundle.ts +4 -2
  175. package/src/plugins/catalog-backend.test.ts +26 -0
  176. package/src/plugins/catalog-backend.ts +9 -2
  177. package/src/plugins/catalog-browser.test.ts +0 -13
  178. package/src/plugins/catalog-browser.ts +4 -0
  179. package/src/plugins/catalog-manifest.test.ts +15 -0
  180. package/src/plugins/catalog-manifest.ts +12 -5
  181. package/src/plugins/catalog-ui.ts +0 -6
  182. package/src/plugins/host-contract.ts +9 -0
  183. package/src/plugins/host-modules.ts +12 -0
  184. package/src/plugins/loader.ts +63 -15
  185. package/src/plugins/prediction-markets/headless.test.ts +119 -0
  186. package/src/plugins/prediction-markets/headless.ts +255 -0
  187. package/src/plugins/prediction-markets/index.tsx +2 -0
  188. package/src/plugins/seed.ts +3 -0
  189. package/src/public/broker.ts +6 -0
  190. package/src/remote/schema.test.ts +65 -0
  191. package/src/remote/schema.ts +322 -39
  192. package/src/remote/types.ts +49 -2
  193. package/src/renderers/browser/app-services.ts +3 -0
  194. package/src/renderers/browser/config-host.ts +24 -8
  195. package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
  196. package/src/renderers/browser/deeplink-bridge.ts +15 -3
  197. package/src/renderers/browser/main.tsx +3 -1
  198. package/src/renderers/browser/research-entry.test.ts +8 -0
  199. package/src/renderers/browser/research-entry.ts +7 -0
  200. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
  201. package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
  202. package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
  203. package/src/renderers/electrobun/view/build-assets.ts +0 -2
  204. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
  205. package/src/sources/gloomberb-cloud/news.ts +1 -0
  206. package/src/sync/core-contributors.test.ts +111 -0
  207. package/src/sync/core-contributors.ts +155 -1
  208. package/src/time-series/resolve.test.ts +118 -1
  209. package/src/time-series/resolve.ts +14 -16
  210. package/src/types/config.ts +2 -0
  211. package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
  212. package/src/plugins/broker-sync/native-loader.ts +0 -10
  213. package/src/plugins/broker-sync/normalize.ts +0 -308
  214. package/src/plugins/broker-sync/public.ts +0 -122
  215. package/src/plugins/broker-sync/robinhood-native.ts +0 -367
  216. package/src/plugins/broker-sync/robinhood.ts +0 -114
  217. package/src/plugins/broker-sync/simplefin-native.ts +0 -201
  218. package/src/plugins/broker-sync/simplefin.ts +0 -70
  219. package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
@@ -11,6 +11,7 @@ const DEFAULT_SHOT_DEVICE_SCALE_FACTOR = 2;
11
11
  import {
12
12
  renderDesktopPaneScreenshot,
13
13
  type DesktopPaneShotApiProxy,
14
+ type DesktopPaneShotIntradayHistory,
14
15
  type DesktopPaneShotPayload,
15
16
  type DesktopPaneShotRenderResult,
16
17
  } from "../desktop-pane-shot";
@@ -18,6 +19,7 @@ import { optionPaneState } from "./options";
18
19
  import type { ResolvedPaneFunction } from "./resolver";
19
20
  import type { MarketContext } from "../types";
20
21
  import { capabilityPluginState } from "./capabilities";
22
+ import { resolvePersistedCloudSessionToken } from "./cloud-session";
21
23
  import type { RemoteUiNodeSnapshot } from "../../remote/types";
22
24
  import {
23
25
  graphRowsForFinancials,
@@ -65,14 +67,16 @@ import {
65
67
  isFinancialAnalysisFunction,
66
68
  withShotPriceHistory,
67
69
  } from "./data";
70
+ import {
71
+ loadShotIntradayWindow,
72
+ resolveShotIntradayRequest,
73
+ } from "./intraday-shot";
68
74
 
69
75
  const DESKTOP_CELL_WIDTH_PX = 8;
70
76
  const DESKTOP_CELL_HEIGHT_PX = 18;
71
77
  const OPTIONS_PANE_ID = "options";
72
78
  const MARKET_VALUATION_PANE_ID = "market-valuation";
73
79
  const ECON_STATISTICS_PANE_ID = "econ-statistics";
74
- const CLOUD_PLUGIN_ID = "gloomberb-cloud";
75
- const CLOUD_SESSION_KEYS = ["resume:session", "session"] as const;
76
80
  const CREDENTIAL_FIELD_NAMES = new Set([
77
81
  "accesstoken",
78
82
  "accessurl",
@@ -91,10 +95,6 @@ const CREDENTIAL_FIELD_NAMES = new Set([
91
95
  "token",
92
96
  ]);
93
97
 
94
- interface PersistedShotCloudSession {
95
- sessionToken?: unknown;
96
- }
97
-
98
98
  /**
99
99
  * The proxy receives the desktop session out of band. Keeping this separate
100
100
  * from DesktopPaneShotPayload prevents the token from entering the page,
@@ -133,21 +133,9 @@ export function stripDesktopShotCredentials<T>(value: T): T {
133
133
  export function resolveDesktopShotApiProxy(
134
134
  context: Pick<MarketContext, "persistence">,
135
135
  ): DesktopPaneShotApiProxy {
136
- let sessionToken: string | null = null;
137
- for (const key of CLOUD_SESSION_KEYS) {
138
- const value = context.persistence.pluginState.get<PersistedShotCloudSession>(
139
- CLOUD_PLUGIN_ID,
140
- key,
141
- 1,
142
- )?.value;
143
- if (typeof value?.sessionToken === "string" && value.sessionToken.length > 0) {
144
- sessionToken = value.sessionToken;
145
- break;
146
- }
147
- }
148
136
  return {
149
137
  baseUrl: getCloudApiBaseUrl(),
150
- sessionToken,
138
+ sessionToken: resolvePersistedCloudSessionToken(context),
151
139
  };
152
140
  }
153
141
 
@@ -289,6 +277,8 @@ export interface PaneScreenshotPriceSeriesEvidence {
289
277
  pointCount: number;
290
278
  first: PaneScreenshotChartPointEvidence;
291
279
  last: PaneScreenshotChartPointEvidence;
280
+ resolution?: string;
281
+ sessionDates?: string[];
292
282
  }
293
283
 
294
284
  export interface PaneScreenshotFundamentalSeriesEvidence {
@@ -362,6 +352,7 @@ export interface PaneScreenshotResult {
362
352
  unavailableSymbols: string[];
363
353
  semanticMismatch: boolean;
364
354
  usable: boolean;
355
+ unusableReason: string | null;
365
356
  dataEvidence: PaneScreenshotDataEvidence | null;
366
357
  outputPath: string;
367
358
  render: DesktopPaneShotRenderResult & {
@@ -379,7 +370,7 @@ export function defaultScreenshotPath(resolved: ResolvedPaneFunction, rawArg: st
379
370
  return resolve(process.cwd(), `gloomberb-${suffix}.png`);
380
371
  }
381
372
 
382
- async function buildDesktopShotPayload(
373
+ export async function buildDesktopShotPayload(
383
374
  resolved: ResolvedPaneFunction,
384
375
  context: MarketContext,
385
376
  rawArg: string,
@@ -414,9 +405,10 @@ async function buildDesktopShotPayload(
414
405
  const paneState = stripDesktopShotCredentials<Record<string, PaneRuntimeState>>({
415
406
  [resolved.instance.instanceId]: initialPaneState,
416
407
  });
408
+ let shotInstance = stripDesktopShotCredentials<typeof resolved.instance>(resolved.instance);
417
409
  const layout = {
418
410
  dockRoot: null,
419
- instances: [resolved.instance],
411
+ instances: [shotInstance],
420
412
  floating: [{
421
413
  instanceId: resolved.instance.instanceId,
422
414
  x: 0,
@@ -444,6 +436,7 @@ async function buildDesktopShotPayload(
444
436
 
445
437
  const tickers: TickerRecord[] = [];
446
438
  const financials: Array<[string, TickerFinancials]> = [];
439
+ const intradayHistories: DesktopPaneShotIntradayHistory[] = [];
447
440
  const optionsChains: Array<[string, OptionsChain]> = [];
448
441
  const [fredSeries, capabilitySeries, valuationSeries, statSeries] = await Promise.all([
449
442
  collectShotFredSeries(resolved),
@@ -452,16 +445,64 @@ async function buildDesktopShotPayload(
452
445
  collectShotStatSeries(resolved),
453
446
  ]);
454
447
  const includeOptionsChains = resolved.pane.id === OPTIONS_PANE_ID || resolved.template?.paneId === OPTIONS_PANE_ID;
448
+ const shotNow = new Date();
455
449
  for (const symbol of collectShotSymbols(resolved, rawArg)) {
456
450
  const entry = await fetchTickerFinancials(context, symbol);
457
451
  const requestedRange = shotPriceHistoryRange(resolved);
458
452
  let data = entry.financials;
459
- if (requestedRange) {
460
- const exchange = entry.instrument.exchange
461
- ?? entry.tickerFile?.metadata.exchange
462
- ?? data.quote?.listingExchangeName
463
- ?? data.quote?.exchangeName
464
- ?? "";
453
+ const exchange = entry.instrument.exchange
454
+ ?? entry.tickerFile?.metadata.exchange
455
+ ?? data.quote?.listingExchangeName
456
+ ?? data.quote?.exchangeName
457
+ ?? "";
458
+ if (resolved.capability.id === "intraday-price-chart") {
459
+ const request = resolveShotIntradayRequest(resolved.options);
460
+ const intraday = await loadShotIntradayWindow({
461
+ provider: context.dataProvider,
462
+ symbol: entry.instrument.symbol,
463
+ exchange,
464
+ request,
465
+ now: shotNow,
466
+ });
467
+ data = { ...data, priceHistory: intraday.points };
468
+ intradayHistories.push({
469
+ symbol: entry.instrument.symbol,
470
+ exchange,
471
+ rangePreset: request.rangePreset,
472
+ resolution: request.resolution,
473
+ requestedSession: request.session,
474
+ sessionDates: intraday.sessionDates,
475
+ points: intraday.points,
476
+ start: intraday.start?.toISOString() ?? null,
477
+ end: intraday.end?.toISOString() ?? null,
478
+ unavailableReason: intraday.unavailableReason,
479
+ });
480
+ const presetStart = subtractTimeRange(shotNow, request.rangePreset);
481
+ const needsExplicitWindow = request.session !== null
482
+ || (intraday.start !== null && intraday.start.getTime() < presetStart.getTime())
483
+ || (intraday.end !== null && intraday.end.getTime() > shotNow.getTime());
484
+ if (needsExplicitWindow && intraday.start && intraday.end) {
485
+ const chartSpec = parseChartSpec(shotInstance.settings?.chartSpec);
486
+ if (chartSpec) {
487
+ shotInstance = {
488
+ ...shotInstance,
489
+ settings: {
490
+ ...shotInstance.settings,
491
+ chartSpec: {
492
+ ...chartSpec,
493
+ viewport: {
494
+ ...chartSpec.viewport,
495
+ dateWindow: {
496
+ start: intraday.start.toISOString(),
497
+ end: intraday.end.toISOString(),
498
+ },
499
+ },
500
+ },
501
+ },
502
+ };
503
+ }
504
+ }
505
+ } else if (requestedRange) {
465
506
  try {
466
507
  const priceHistory = await context.dataProvider.getPriceHistory(entry.instrument.symbol, exchange, requestedRange);
467
508
  data = { ...data, priceHistory: clipPriceHistoryToRange(priceHistory, requestedRange) };
@@ -472,15 +513,13 @@ async function buildDesktopShotPayload(
472
513
  tickers.push(entry.tickerFile ?? createFallbackTicker(symbol, data, context));
473
514
  financials.push([symbol, data]);
474
515
  if (includeOptionsChains && context.dataProvider.getOptionsChain) {
475
- const exchange = entry.instrument.exchange
476
- ?? entry.tickerFile?.metadata.exchange
477
- ?? data.quote?.listingExchangeName
478
- ?? data.quote?.exchangeName
479
- ?? "";
480
516
  const chain = await context.dataProvider.getOptionsChain(entry.instrument.symbol, exchange);
481
517
  optionsChains.push([symbol, chain]);
482
518
  }
483
519
  }
520
+ layout.instances[0] = shotInstance;
521
+ config.layout.instances[0] = shotInstance;
522
+ if (config.layouts[0]) config.layouts[0].layout.instances[0] = shotInstance;
484
523
 
485
524
  return {
486
525
  config,
@@ -493,6 +532,7 @@ async function buildDesktopShotPayload(
493
532
  watermark,
494
533
  tickers,
495
534
  financials,
535
+ intradayHistories,
496
536
  optionsChains,
497
537
  fredSeries,
498
538
  valuationSeries,
@@ -525,7 +565,7 @@ export function shotPriceHistoryRange(resolved: ResolvedPaneFunction): TimeRange
525
565
  case "return-correlation":
526
566
  return (resolved.options.rangePreset ?? "1Y") as TimeRange;
527
567
  case "intraday-price-chart":
528
- return "1D";
568
+ return (resolved.options.rangePreset ?? "1D") as TimeRange;
529
569
  case "historical-prices":
530
570
  case "security-relationship":
531
571
  return (resolved.options.range ?? "1Y") as TimeRange;
@@ -545,6 +585,7 @@ export async function renderDesktopShot({
545
585
  scale,
546
586
  watermark,
547
587
  options,
588
+ captureImage = true,
548
589
  }: {
549
590
  resolved: ResolvedPaneFunction;
550
591
  context: MarketContext;
@@ -556,8 +597,9 @@ export async function renderDesktopShot({
556
597
  scale?: number;
557
598
  watermark?: string | null;
558
599
  options: Record<string, string | true>;
600
+ captureImage?: boolean;
559
601
  }): Promise<PaneScreenshotResult> {
560
- await mkdir(dirname(outputPath), { recursive: true });
602
+ if (captureImage) await mkdir(dirname(outputPath), { recursive: true });
561
603
  const apiProxy = resolveDesktopShotApiProxy(context);
562
604
  const previousSessionToken = apiClient.getSessionToken();
563
605
  let payload: DesktopPaneShotPayload;
@@ -575,7 +617,7 @@ export async function renderDesktopShot({
575
617
  scale ?? 1,
576
618
  watermark ?? null,
577
619
  );
578
- render = await renderDesktopPaneScreenshot(payload, outputPath, apiProxy);
620
+ render = await renderDesktopPaneScreenshot(payload, outputPath, apiProxy, { captureImage });
579
621
  } finally {
580
622
  apiClient.setSessionToken(previousSessionToken);
581
623
  }
@@ -598,9 +640,10 @@ export async function renderDesktopShot({
598
640
  );
599
641
  const expectedChart = shotExpectedChart(resolved, payload);
600
642
  const dataEvidence = shotDataEvidenceFor(resolved, payload);
601
- const chartEvidenceMismatches = expectedChart
602
- ? chartEvidenceMismatchesFor(render.semanticUi, expectedChart)
603
- : [];
643
+ const chartEvidenceMismatches = [
644
+ ...(expectedChart ? chartEvidenceMismatchesFor(render.semanticUi, expectedChart) : []),
645
+ ...intradayChartEvidenceMismatchesFor(resolved, payload, render.semanticUi),
646
+ ];
604
647
  const semanticMismatch = missingExpectedText.length > 0
605
648
  || missingExpectedSelections.length > 0
606
649
  || chartEvidenceMismatches.length > 0;
@@ -619,6 +662,9 @@ export async function renderDesktopShot({
619
662
  requiresStructuredDataEvidence: requiresStructuredDataEvidence(resolved),
620
663
  hasStructuredDataEvidence: dataEvidence !== null,
621
664
  });
665
+ const unusableReason = usable
666
+ ? null
667
+ : shotUnusableReasonFor(resolved, payload, render, unavailableSymbols, semanticMismatch);
622
668
  return {
623
669
  kind: "pane-screenshot",
624
670
  target: resolved.token,
@@ -637,6 +683,7 @@ export async function renderDesktopShot({
637
683
  unavailableSymbols,
638
684
  semanticMismatch,
639
685
  usable,
686
+ unusableReason,
640
687
  dataEvidence,
641
688
  outputPath,
642
689
  render: {
@@ -652,8 +699,37 @@ export async function renderDesktopShot({
652
699
  }
653
700
 
654
701
  function requiresStructuredDataEvidence(resolved: ResolvedPaneFunction): boolean {
655
- return ["price-chart", "price-comparison", "fundamental-series", "financial-statements"]
656
- .includes(resolved.capability.id);
702
+ return [
703
+ "price-chart",
704
+ "intraday-price-chart",
705
+ "price-comparison",
706
+ "fundamental-series",
707
+ "financial-statements",
708
+ ].includes(resolved.capability.id);
709
+ }
710
+
711
+ export function shotUnusableReasonFor(
712
+ resolved: ResolvedPaneFunction,
713
+ payload: DesktopPaneShotPayload,
714
+ render: Pick<DesktopPaneShotRenderResult, "loadingStateDetected" | "errorStateDetected" | "emptyStateDetected">,
715
+ unavailableSymbols: string[],
716
+ semanticMismatch: boolean,
717
+ ): string {
718
+ if (resolved.capability.id === "intraday-price-chart") {
719
+ const intraday = payload.intradayHistories[0];
720
+ if (intraday?.unavailableReason) return intraday.unavailableReason;
721
+ if (intraday && intraday.points.length > 0) {
722
+ return `Intraday bars were loaded for ${intraday.symbol}, but the chart did not render them.`;
723
+ }
724
+ const symbol = payload.financials[0]?.[0] ?? resolved.createOptions?.symbol ?? "the ticker";
725
+ return `No intraday price history is available for ${symbol} for the requested session window.`;
726
+ }
727
+ if (render.loadingStateDetected) return "The pane was still loading when the screenshot was captured.";
728
+ if (render.errorStateDetected) return "The pane rendered an error state.";
729
+ if (render.emptyStateDetected) return "The pane rendered an empty state.";
730
+ if (unavailableSymbols.length > 0) return `Data is unavailable for ${unavailableSymbols.join(", ")}.`;
731
+ if (semanticMismatch) return "The rendered content did not match the requested capability.";
732
+ return "The pane did not produce verifiable screenshot evidence.";
657
733
  }
658
734
 
659
735
  const STATEMENT_EVIDENCE_KEYS: Record<string, ReadonlySet<string>> = {
@@ -685,6 +761,23 @@ export function shotDataEvidenceFor(
685
761
  resolved: ResolvedPaneFunction,
686
762
  payload: DesktopPaneShotPayload,
687
763
  ): PaneScreenshotDataEvidence | null {
764
+ if (resolved.capability.id === "intraday-price-chart") {
765
+ const intraday = payload.intradayHistories[0];
766
+ if (!intraday || intraday.points.length === 0) return null;
767
+ const evidence = chartSeriesEvidence(intraday.symbol, intraday.points);
768
+ if (!evidence.first || !evidence.last) return null;
769
+ return {
770
+ kind: "price-series",
771
+ symbol: intraday.symbol,
772
+ range: intraday.rangePreset,
773
+ pointCount: evidence.pointCount,
774
+ first: evidence.first,
775
+ last: evidence.last,
776
+ resolution: intraday.resolution,
777
+ sessionDates: intraday.sessionDates,
778
+ };
779
+ }
780
+
688
781
  if (resolved.capability.id === "price-chart") {
689
782
  const range = String(resolved.options.rangePreset ?? "5Y") as TimeRange;
690
783
  const [symbol, financials] = payload.financials[0] ?? [];
@@ -1140,6 +1233,48 @@ export function chartEvidenceMismatchesFor(
1140
1233
  return mismatches;
1141
1234
  }
1142
1235
 
1236
+ export function intradayChartEvidenceMismatchesFor(
1237
+ resolved: ResolvedPaneFunction,
1238
+ payload: DesktopPaneShotPayload,
1239
+ semanticUi: RemoteUiNodeSnapshot[],
1240
+ ): string[] {
1241
+ if (resolved.capability.id !== "intraday-price-chart") return [];
1242
+ const intraday = payload.intradayHistories[0];
1243
+ if (!intraday || intraday.points.length === 0) return [];
1244
+ const metadata = semanticUi.find((node) => (
1245
+ node.role === "chart-data" && node.metadata?.kind === "chart-composer"
1246
+ ))?.metadata;
1247
+ if (!metadata) return ["rendered intraday chart-data semantic evidence is missing"];
1248
+ const baseSeries = Array.isArray(metadata.baseSeries) ? metadata.baseSeries : [];
1249
+ const actual = baseSeries[0];
1250
+ if (!isRecord(actual)) return ["rendered intraday base series is missing"];
1251
+
1252
+ const expected = chartSeriesEvidence(intraday.symbol, intraday.points);
1253
+ const mismatches: string[] = [];
1254
+ if (actual.pointCount !== expected.pointCount) {
1255
+ mismatches.push("rendered intraday point count does not match");
1256
+ }
1257
+ if (metadata.projectedPointCount !== expected.pointCount) {
1258
+ mismatches.push("rendered intraday projection point count does not match");
1259
+ }
1260
+ const pointMatches = (
1261
+ actualPoint: unknown,
1262
+ expectedPoint: PaneScreenshotChartPointEvidence | null,
1263
+ ): boolean => {
1264
+ if (!isRecord(actualPoint) || !expectedPoint) return false;
1265
+ return actualPoint.date === expectedPoint.date
1266
+ && typeof actualPoint.value === "number"
1267
+ && closeEnough(actualPoint.value, expectedPoint.close);
1268
+ };
1269
+ if (!pointMatches(actual.first, expected.first)) {
1270
+ mismatches.push("rendered intraday first bar does not match");
1271
+ }
1272
+ if (!pointMatches(actual.last, expected.last)) {
1273
+ mismatches.push("rendered intraday last bar does not match");
1274
+ }
1275
+ return mismatches;
1276
+ }
1277
+
1143
1278
  export function shotUnavailableSymbols(
1144
1279
  resolved: ResolvedPaneFunction,
1145
1280
  payload: DesktopPaneShotPayload,
@@ -1162,6 +1297,16 @@ export function shotUnavailableSymbols(
1162
1297
  return [];
1163
1298
  });
1164
1299
  }
1300
+ if (resolved.capability.id === "intraday-price-chart") {
1301
+ const symbol = payload.intradayHistories[0]?.symbol ?? payload.financials[0]?.[0];
1302
+ const metadata = semanticUi.find((node) => (
1303
+ node.role === "chart-data" && node.metadata?.kind === "chart-composer"
1304
+ ))?.metadata;
1305
+ const renderedPoints = typeof metadata?.projectedPointCount === "number"
1306
+ ? metadata.projectedPointCount
1307
+ : 0;
1308
+ return symbol && renderedPoints <= 0 ? [symbol] : [];
1309
+ }
1165
1310
  const graphKind = shotGraphKind(resolved);
1166
1311
  if (graphKind) {
1167
1312
  const metric = resolved.options.metric as GraphMetricKey;
@@ -1174,7 +1319,7 @@ export function shotUnavailableSymbols(
1174
1319
  ).length === 0 ? [symbol] : []
1175
1320
  ));
1176
1321
  }
1177
- if (["price-chart", "intraday-price-chart", "historical-prices"].includes(resolved.capability.id)) {
1322
+ if (["price-chart", "historical-prices"].includes(resolved.capability.id)) {
1178
1323
  return payload.financials.flatMap(([symbol, financials]) => financials.priceHistory.length > 0 ? [] : [symbol]);
1179
1324
  }
1180
1325
  if (["price-comparison", "return-correlation", "security-relationship"].includes(resolved.capability.id)) {
@@ -1211,6 +1356,14 @@ export function shotSemanticRowCount(
1211
1356
  count + (isRecord(entry) && typeof entry.pointCount === "number" ? Math.max(0, entry.pointCount) : 0)
1212
1357
  ), 0);
1213
1358
  }
1359
+ if (resolved.capability.id === "intraday-price-chart") {
1360
+ const metadata = semanticUi.find((node) => (
1361
+ node.role === "chart-data" && node.metadata?.kind === "chart-composer"
1362
+ ))?.metadata;
1363
+ return typeof metadata?.projectedPointCount === "number"
1364
+ ? Math.max(0, metadata.projectedPointCount)
1365
+ : 0;
1366
+ }
1214
1367
  const graphKind = shotGraphKind(resolved);
1215
1368
  if (graphKind) {
1216
1369
  const metric = resolved.options.metric as GraphMetricKey;
@@ -1223,7 +1376,7 @@ export function shotSemanticRowCount(
1223
1376
  ).length
1224
1377
  ), 0);
1225
1378
  }
1226
- if (["price-chart", "intraday-price-chart", "historical-prices"].includes(resolved.capability.id)) {
1379
+ if (["price-chart", "historical-prices"].includes(resolved.capability.id)) {
1227
1380
  return payload.financials[0]?.[1].priceHistory.length ?? 0;
1228
1381
  }
1229
1382
  if (["price-comparison", "return-correlation", "security-relationship"].includes(resolved.capability.id)) {
@@ -11,24 +11,31 @@ const log = debugLog.createLogger("plugin-seed");
11
11
  * Runs before the plugin catalog is read so a restored plugin is available in
12
12
  * the same session, and stays quiet on failure: an offline launch should start
13
13
  * normally and try again next time, not block on a network request.
14
+ *
15
+ * Returns the seeded ids so a caller that already holds the config in memory
16
+ * can adopt them. The desktop loads its config before restoring, and without
17
+ * this it would keep an object whose `seededPlugins` is one launch stale and
18
+ * overwrite the record on the next save.
14
19
  */
15
- export async function restoreExtractedPlugins(): Promise<void> {
20
+ export async function restoreExtractedPlugins(): Promise<string[] | null> {
16
21
  try {
17
22
  const config = await loadCliConfigIfAvailable();
18
23
  // No data directory yet means a first run: there is nothing to restore.
19
- if (!config) return;
24
+ if (!config) return null;
20
25
  const { installPlugin } = await import("./commands/plugins");
21
26
  const result = await seedExtractedPlugins(config, installPlugin);
22
27
 
23
28
  const seeded = [...new Set(result.seeded)].sort();
24
29
  const existing = [...new Set(config.seededPlugins ?? [])].sort();
25
- if (seeded.join() === existing.join()) return;
30
+ if (seeded.join() === existing.join()) return seeded;
26
31
 
27
32
  await saveConfig({ ...config, seededPlugins: seeded });
28
33
  if (result.installed.length > 0) {
29
34
  log.info(`Restored ${result.installed.join(", ")} into ~/.gloomberb/plugins`);
30
35
  }
36
+ return seeded;
31
37
  } catch (error) {
32
38
  log.error(`Plugin restore skipped: ${error}`);
39
+ return null;
33
40
  }
34
41
  }
@@ -31,6 +31,7 @@ import {
31
31
  buildCompositeNavigationFrame,
32
32
  zoomCompositeViewport,
33
33
  } from "./interactions";
34
+ import { COMPOSITE_RIGHT_OFFSET_RATIO } from "./time-scale";
34
35
  import { getThemeColors, syncTheme } from "../../../theme/colors";
35
36
  import { ThemeProvider } from "../../../theme/theme-context";
36
37
  import { DEFAULT_THEME } from "../../../theme/themes";
@@ -304,6 +305,70 @@ describe("CompositeChart", () => {
304
305
  expect(testSetup.captureCharFrame()).toContain("ACME Price $110 +10.65%");
305
306
  });
306
307
 
308
+ test("holds a lower panel's rows while its series has no observations in view", async () => {
309
+ const price: ResolvedSeries = {
310
+ ...series("price", "main", "left", "USD", []),
311
+ points: [
312
+ point("2025-01-01", 100),
313
+ point("2025-01-02", 103),
314
+ point("2025-01-03", 101),
315
+ point("2025-01-04", 104),
316
+ point("2025-01-05", 102),
317
+ ],
318
+ };
319
+ const volume: ResolvedSeries = {
320
+ ...series("volume", "volume", "left", "shares", []),
321
+ label: "Volume",
322
+ unitGroup: "volume",
323
+ style: "columns",
324
+ points: [point("2025-01-01", 4_000), point("2025-01-02", 6_000)],
325
+ };
326
+ const panels = [{ id: "main", height: 3 }, { id: "volume", height: 1 }];
327
+ const renderChart = async (
328
+ volumeSeries: ResolvedSeries,
329
+ viewport?: { start: Date; end: Date },
330
+ ) => {
331
+ testSetup = await testRender(
332
+ <CompositeChart
333
+ width={78}
334
+ height={18}
335
+ series={[price, volumeSeries]}
336
+ panels={panels}
337
+ viewport={viewport}
338
+ />,
339
+ { width: 80, height: 20 },
340
+ );
341
+ await act(async () => {
342
+ await testSetup!.renderOnce();
343
+ await testSetup!.renderOnce();
344
+ });
345
+ const frame = testSetup!.captureCharFrame();
346
+ await act(async () => testSetup!.renderer.destroy());
347
+ testSetup = undefined;
348
+ return frame;
349
+ };
350
+ // The lowest price-axis label sits on the boundary between the two panels,
351
+ // so it moves down the moment the lower panel gives up its rows.
352
+ const priceAxisFloor = (frame: string) => frame
353
+ .split("\n")
354
+ .reduce((row, line, index) => line.includes("$") ? index : row, -1);
355
+
356
+ const loaded = await renderChart(volume);
357
+ // Panned to a window the upper panel covers and the lower panel has no bars in.
358
+ const panned = await renderChart(volume, {
359
+ start: new Date("2025-01-03T00:00:00.000Z"),
360
+ end: new Date("2025-01-05T00:00:00.000Z"),
361
+ });
362
+ // The same chart before the lower panel's data has loaded at all.
363
+ const loading = await renderChart({ ...volume, points: [] });
364
+
365
+ expect(priceAxisFloor(loaded)).toBeGreaterThan(0);
366
+ expect(priceAxisFloor(panned)).toBe(priceAxisFloor(loaded));
367
+ expect(priceAxisFloor(loading)).toBe(priceAxisFloor(loaded));
368
+ // A panel with loaded history keeps its axis while the window holds no bars.
369
+ expect(panned).toMatch(/\dK/);
370
+ });
371
+
307
372
  test("lays out mixed panels with one shared legend and time axis", async () => {
308
373
  testSetup = await testRender(
309
374
  <CompositeChart
@@ -1429,7 +1494,9 @@ describe("CompositeChart", () => {
1429
1494
  frame,
1430
1495
  viewport,
1431
1496
  1 + 4 * 0.04,
1432
- pointerX / Math.max(plotWidth - 1, 1),
1497
+ // The pointer reads in plot space, which is wider than the viewport by
1498
+ // the reserved right offset.
1499
+ pointerX / Math.max(plotWidth - 1, 1) * (1 + COMPOSITE_RIGHT_OFFSET_RATIO),
1433
1500
  );
1434
1501
 
1435
1502
  await act(async () => {