gloomberb 0.12.0 → 0.13.1

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 (135) 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.ts +10 -2
  5. package/src/api-client/research-activity.ts +61 -0
  6. package/src/api-client/types.ts +2 -1
  7. package/src/cli/commands/plugins.ts +4 -3
  8. package/src/cli/desktop-pane-shot.ts +79 -10
  9. package/src/cli/pane-functions/capabilities.ts +76 -23
  10. package/src/cli/pane-functions/dom.test.ts +41 -0
  11. package/src/cli/pane-functions/dom.ts +199 -0
  12. package/src/cli/pane-functions/index.test.ts +13 -2
  13. package/src/cli/pane-functions/index.ts +4 -4
  14. package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
  15. package/src/cli/pane-functions/intraday-shot.ts +334 -0
  16. package/src/cli/pane-functions/report.test.ts +29 -1
  17. package/src/cli/pane-functions/report.ts +68 -7
  18. package/src/cli/pane-functions/screenshot.ts +193 -23
  19. package/src/cli/restore-plugins.ts +10 -3
  20. package/src/components/chart/composite/composite-chart.test.tsx +68 -1
  21. package/src/components/chart/composite/composite-chart.tsx +87 -21
  22. package/src/components/chart/composite/rasterizer.ts +33 -0
  23. package/src/components/chart/composite/renderers.test.ts +51 -4
  24. package/src/components/chart/composite/scene.test.ts +128 -2
  25. package/src/components/chart/composite/scene.ts +92 -16
  26. package/src/components/chart/composite/text-renderer.ts +13 -0
  27. package/src/components/chart/composite/time-scale.ts +30 -10
  28. package/src/components/chart/composite/types.ts +26 -0
  29. package/src/components/command-bar/surface/assist.test.tsx +20 -9
  30. package/src/components/command-bar/surface/test-harness.tsx +23 -4
  31. package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
  32. package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
  33. package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
  34. package/src/components/onboarding/onboarding-wizard.tsx +49 -14
  35. package/src/data/config/store/index.test.ts +23 -0
  36. package/src/data/config/store/node.ts +7 -1
  37. package/src/data/config/store/normalize.ts +4 -0
  38. package/src/plugins/builtin/cds/client.test.ts +20 -0
  39. package/src/plugins/builtin/cds/client.ts +13 -3
  40. package/src/plugins/builtin/cds/headless.test.ts +30 -0
  41. package/src/plugins/builtin/cds/headless.ts +113 -0
  42. package/src/plugins/builtin/cds/index.tsx +4 -0
  43. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
  44. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  45. package/src/plugins/builtin/cloud/askg/protocol.test.ts +189 -0
  46. package/src/plugins/builtin/cloud/askg/protocol.ts +274 -0
  47. package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
  48. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
  49. package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
  50. package/src/plugins/builtin/credit-conditions/client.ts +1 -1
  51. package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
  52. package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
  53. package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
  54. package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
  55. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +5 -2
  56. package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
  57. package/src/plugins/builtin/fear-greed/headless.ts +84 -0
  58. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  59. package/src/plugins/builtin/market-movers/client.ts +76 -0
  60. package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
  61. package/src/plugins/builtin/market-movers/headless.ts +73 -0
  62. package/src/plugins/builtin/market-movers/index.tsx +11 -51
  63. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
  64. package/src/plugins/builtin/research/event-model.ts +245 -0
  65. package/src/plugins/builtin/research/events-headless.test.ts +32 -0
  66. package/src/plugins/builtin/research/events-headless.ts +87 -0
  67. package/src/plugins/builtin/research/index.tsx +4 -0
  68. package/src/plugins/builtin/research-search/pane.tsx +4 -0
  69. package/src/plugins/builtin/scanner/headless.test.ts +59 -0
  70. package/src/plugins/builtin/scanner/headless.ts +125 -0
  71. package/src/plugins/builtin/scanner/index.ts +4 -0
  72. package/src/plugins/builtin/sectors/client.ts +54 -0
  73. package/src/plugins/builtin/sectors/headless.test.ts +28 -0
  74. package/src/plugins/builtin/sectors/headless.ts +80 -0
  75. package/src/plugins/builtin/sectors/index.tsx +6 -51
  76. package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
  77. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
  78. package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
  79. package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
  80. package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
  81. package/src/plugins/builtin/volatility/client.ts +16 -3
  82. package/src/plugins/builtin/volatility/headless.test.ts +35 -0
  83. package/src/plugins/builtin/volatility/headless.ts +75 -0
  84. package/src/plugins/builtin/volatility/index.tsx +4 -0
  85. package/src/plugins/builtin/world-indices/client.ts +43 -0
  86. package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
  87. package/src/plugins/builtin/world-indices/headless.ts +109 -0
  88. package/src/plugins/builtin/world-indices/index.tsx +4 -0
  89. package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
  90. package/src/plugins/builtin/yield-curve/headless.ts +67 -0
  91. package/src/plugins/builtin/yield-curve/index.tsx +6 -8
  92. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
  93. package/src/plugins/bundle.test.ts +58 -1
  94. package/src/plugins/bundle.ts +4 -2
  95. package/src/plugins/catalog-backend.test.ts +26 -0
  96. package/src/plugins/catalog-backend.ts +9 -2
  97. package/src/plugins/catalog-browser.test.ts +0 -13
  98. package/src/plugins/catalog-browser.ts +4 -0
  99. package/src/plugins/catalog-manifest.test.ts +15 -0
  100. package/src/plugins/catalog-manifest.ts +12 -5
  101. package/src/plugins/catalog-ui.ts +0 -6
  102. package/src/plugins/host-contract.ts +9 -0
  103. package/src/plugins/host-modules.ts +12 -0
  104. package/src/plugins/loader.ts +63 -15
  105. package/src/plugins/seed.ts +3 -0
  106. package/src/public/broker.ts +6 -0
  107. package/src/remote/schema.test.ts +65 -0
  108. package/src/remote/schema.ts +322 -39
  109. package/src/remote/types.ts +49 -2
  110. package/src/renderers/browser/app-services.ts +3 -0
  111. package/src/renderers/browser/config-host.ts +24 -8
  112. package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
  113. package/src/renderers/browser/deeplink-bridge.ts +15 -3
  114. package/src/renderers/browser/main.tsx +3 -1
  115. package/src/renderers/browser/research-entry.test.ts +8 -0
  116. package/src/renderers/browser/research-entry.ts +7 -0
  117. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
  118. package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
  119. package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
  120. package/src/renderers/electrobun/view/build-assets.ts +0 -2
  121. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
  122. package/src/sync/core-contributors.test.ts +111 -0
  123. package/src/sync/core-contributors.ts +155 -1
  124. package/src/time-series/resolve.test.ts +118 -1
  125. package/src/time-series/resolve.ts +14 -16
  126. package/src/types/config.ts +2 -0
  127. package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
  128. package/src/plugins/broker-sync/native-loader.ts +0 -10
  129. package/src/plugins/broker-sync/normalize.ts +0 -308
  130. package/src/plugins/broker-sync/public.ts +0 -122
  131. package/src/plugins/broker-sync/robinhood-native.ts +0 -367
  132. package/src/plugins/broker-sync/robinhood.ts +0 -114
  133. package/src/plugins/broker-sync/simplefin-native.ts +0 -201
  134. package/src/plugins/broker-sync/simplefin.ts +0 -70
  135. 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";
@@ -65,6 +66,10 @@ import {
65
66
  isFinancialAnalysisFunction,
66
67
  withShotPriceHistory,
67
68
  } from "./data";
69
+ import {
70
+ loadShotIntradayWindow,
71
+ resolveShotIntradayRequest,
72
+ } from "./intraday-shot";
68
73
 
69
74
  const DESKTOP_CELL_WIDTH_PX = 8;
70
75
  const DESKTOP_CELL_HEIGHT_PX = 18;
@@ -289,6 +294,8 @@ export interface PaneScreenshotPriceSeriesEvidence {
289
294
  pointCount: number;
290
295
  first: PaneScreenshotChartPointEvidence;
291
296
  last: PaneScreenshotChartPointEvidence;
297
+ resolution?: string;
298
+ sessionDates?: string[];
292
299
  }
293
300
 
294
301
  export interface PaneScreenshotFundamentalSeriesEvidence {
@@ -362,6 +369,7 @@ export interface PaneScreenshotResult {
362
369
  unavailableSymbols: string[];
363
370
  semanticMismatch: boolean;
364
371
  usable: boolean;
372
+ unusableReason: string | null;
365
373
  dataEvidence: PaneScreenshotDataEvidence | null;
366
374
  outputPath: string;
367
375
  render: DesktopPaneShotRenderResult & {
@@ -379,7 +387,7 @@ export function defaultScreenshotPath(resolved: ResolvedPaneFunction, rawArg: st
379
387
  return resolve(process.cwd(), `gloomberb-${suffix}.png`);
380
388
  }
381
389
 
382
- async function buildDesktopShotPayload(
390
+ export async function buildDesktopShotPayload(
383
391
  resolved: ResolvedPaneFunction,
384
392
  context: MarketContext,
385
393
  rawArg: string,
@@ -414,9 +422,10 @@ async function buildDesktopShotPayload(
414
422
  const paneState = stripDesktopShotCredentials<Record<string, PaneRuntimeState>>({
415
423
  [resolved.instance.instanceId]: initialPaneState,
416
424
  });
425
+ let shotInstance = stripDesktopShotCredentials<typeof resolved.instance>(resolved.instance);
417
426
  const layout = {
418
427
  dockRoot: null,
419
- instances: [resolved.instance],
428
+ instances: [shotInstance],
420
429
  floating: [{
421
430
  instanceId: resolved.instance.instanceId,
422
431
  x: 0,
@@ -444,6 +453,7 @@ async function buildDesktopShotPayload(
444
453
 
445
454
  const tickers: TickerRecord[] = [];
446
455
  const financials: Array<[string, TickerFinancials]> = [];
456
+ const intradayHistories: DesktopPaneShotIntradayHistory[] = [];
447
457
  const optionsChains: Array<[string, OptionsChain]> = [];
448
458
  const [fredSeries, capabilitySeries, valuationSeries, statSeries] = await Promise.all([
449
459
  collectShotFredSeries(resolved),
@@ -452,16 +462,64 @@ async function buildDesktopShotPayload(
452
462
  collectShotStatSeries(resolved),
453
463
  ]);
454
464
  const includeOptionsChains = resolved.pane.id === OPTIONS_PANE_ID || resolved.template?.paneId === OPTIONS_PANE_ID;
465
+ const shotNow = new Date();
455
466
  for (const symbol of collectShotSymbols(resolved, rawArg)) {
456
467
  const entry = await fetchTickerFinancials(context, symbol);
457
468
  const requestedRange = shotPriceHistoryRange(resolved);
458
469
  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
- ?? "";
470
+ const exchange = entry.instrument.exchange
471
+ ?? entry.tickerFile?.metadata.exchange
472
+ ?? data.quote?.listingExchangeName
473
+ ?? data.quote?.exchangeName
474
+ ?? "";
475
+ if (resolved.capability.id === "intraday-price-chart") {
476
+ const request = resolveShotIntradayRequest(resolved.options);
477
+ const intraday = await loadShotIntradayWindow({
478
+ provider: context.dataProvider,
479
+ symbol: entry.instrument.symbol,
480
+ exchange,
481
+ request,
482
+ now: shotNow,
483
+ });
484
+ data = { ...data, priceHistory: intraday.points };
485
+ intradayHistories.push({
486
+ symbol: entry.instrument.symbol,
487
+ exchange,
488
+ rangePreset: request.rangePreset,
489
+ resolution: request.resolution,
490
+ requestedSession: request.session,
491
+ sessionDates: intraday.sessionDates,
492
+ points: intraday.points,
493
+ start: intraday.start?.toISOString() ?? null,
494
+ end: intraday.end?.toISOString() ?? null,
495
+ unavailableReason: intraday.unavailableReason,
496
+ });
497
+ const presetStart = subtractTimeRange(shotNow, request.rangePreset);
498
+ const needsExplicitWindow = request.session !== null
499
+ || (intraday.start !== null && intraday.start.getTime() < presetStart.getTime())
500
+ || (intraday.end !== null && intraday.end.getTime() > shotNow.getTime());
501
+ if (needsExplicitWindow && intraday.start && intraday.end) {
502
+ const chartSpec = parseChartSpec(shotInstance.settings?.chartSpec);
503
+ if (chartSpec) {
504
+ shotInstance = {
505
+ ...shotInstance,
506
+ settings: {
507
+ ...shotInstance.settings,
508
+ chartSpec: {
509
+ ...chartSpec,
510
+ viewport: {
511
+ ...chartSpec.viewport,
512
+ dateWindow: {
513
+ start: intraday.start.toISOString(),
514
+ end: intraday.end.toISOString(),
515
+ },
516
+ },
517
+ },
518
+ },
519
+ };
520
+ }
521
+ }
522
+ } else if (requestedRange) {
465
523
  try {
466
524
  const priceHistory = await context.dataProvider.getPriceHistory(entry.instrument.symbol, exchange, requestedRange);
467
525
  data = { ...data, priceHistory: clipPriceHistoryToRange(priceHistory, requestedRange) };
@@ -472,15 +530,13 @@ async function buildDesktopShotPayload(
472
530
  tickers.push(entry.tickerFile ?? createFallbackTicker(symbol, data, context));
473
531
  financials.push([symbol, data]);
474
532
  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
533
  const chain = await context.dataProvider.getOptionsChain(entry.instrument.symbol, exchange);
481
534
  optionsChains.push([symbol, chain]);
482
535
  }
483
536
  }
537
+ layout.instances[0] = shotInstance;
538
+ config.layout.instances[0] = shotInstance;
539
+ if (config.layouts[0]) config.layouts[0].layout.instances[0] = shotInstance;
484
540
 
485
541
  return {
486
542
  config,
@@ -493,6 +549,7 @@ async function buildDesktopShotPayload(
493
549
  watermark,
494
550
  tickers,
495
551
  financials,
552
+ intradayHistories,
496
553
  optionsChains,
497
554
  fredSeries,
498
555
  valuationSeries,
@@ -525,7 +582,7 @@ export function shotPriceHistoryRange(resolved: ResolvedPaneFunction): TimeRange
525
582
  case "return-correlation":
526
583
  return (resolved.options.rangePreset ?? "1Y") as TimeRange;
527
584
  case "intraday-price-chart":
528
- return "1D";
585
+ return (resolved.options.rangePreset ?? "1D") as TimeRange;
529
586
  case "historical-prices":
530
587
  case "security-relationship":
531
588
  return (resolved.options.range ?? "1Y") as TimeRange;
@@ -545,6 +602,7 @@ export async function renderDesktopShot({
545
602
  scale,
546
603
  watermark,
547
604
  options,
605
+ captureImage = true,
548
606
  }: {
549
607
  resolved: ResolvedPaneFunction;
550
608
  context: MarketContext;
@@ -556,8 +614,9 @@ export async function renderDesktopShot({
556
614
  scale?: number;
557
615
  watermark?: string | null;
558
616
  options: Record<string, string | true>;
617
+ captureImage?: boolean;
559
618
  }): Promise<PaneScreenshotResult> {
560
- await mkdir(dirname(outputPath), { recursive: true });
619
+ if (captureImage) await mkdir(dirname(outputPath), { recursive: true });
561
620
  const apiProxy = resolveDesktopShotApiProxy(context);
562
621
  const previousSessionToken = apiClient.getSessionToken();
563
622
  let payload: DesktopPaneShotPayload;
@@ -575,7 +634,7 @@ export async function renderDesktopShot({
575
634
  scale ?? 1,
576
635
  watermark ?? null,
577
636
  );
578
- render = await renderDesktopPaneScreenshot(payload, outputPath, apiProxy);
637
+ render = await renderDesktopPaneScreenshot(payload, outputPath, apiProxy, { captureImage });
579
638
  } finally {
580
639
  apiClient.setSessionToken(previousSessionToken);
581
640
  }
@@ -598,9 +657,10 @@ export async function renderDesktopShot({
598
657
  );
599
658
  const expectedChart = shotExpectedChart(resolved, payload);
600
659
  const dataEvidence = shotDataEvidenceFor(resolved, payload);
601
- const chartEvidenceMismatches = expectedChart
602
- ? chartEvidenceMismatchesFor(render.semanticUi, expectedChart)
603
- : [];
660
+ const chartEvidenceMismatches = [
661
+ ...(expectedChart ? chartEvidenceMismatchesFor(render.semanticUi, expectedChart) : []),
662
+ ...intradayChartEvidenceMismatchesFor(resolved, payload, render.semanticUi),
663
+ ];
604
664
  const semanticMismatch = missingExpectedText.length > 0
605
665
  || missingExpectedSelections.length > 0
606
666
  || chartEvidenceMismatches.length > 0;
@@ -619,6 +679,9 @@ export async function renderDesktopShot({
619
679
  requiresStructuredDataEvidence: requiresStructuredDataEvidence(resolved),
620
680
  hasStructuredDataEvidence: dataEvidence !== null,
621
681
  });
682
+ const unusableReason = usable
683
+ ? null
684
+ : shotUnusableReasonFor(resolved, payload, render, unavailableSymbols, semanticMismatch);
622
685
  return {
623
686
  kind: "pane-screenshot",
624
687
  target: resolved.token,
@@ -637,6 +700,7 @@ export async function renderDesktopShot({
637
700
  unavailableSymbols,
638
701
  semanticMismatch,
639
702
  usable,
703
+ unusableReason,
640
704
  dataEvidence,
641
705
  outputPath,
642
706
  render: {
@@ -652,8 +716,37 @@ export async function renderDesktopShot({
652
716
  }
653
717
 
654
718
  function requiresStructuredDataEvidence(resolved: ResolvedPaneFunction): boolean {
655
- return ["price-chart", "price-comparison", "fundamental-series", "financial-statements"]
656
- .includes(resolved.capability.id);
719
+ return [
720
+ "price-chart",
721
+ "intraday-price-chart",
722
+ "price-comparison",
723
+ "fundamental-series",
724
+ "financial-statements",
725
+ ].includes(resolved.capability.id);
726
+ }
727
+
728
+ export function shotUnusableReasonFor(
729
+ resolved: ResolvedPaneFunction,
730
+ payload: DesktopPaneShotPayload,
731
+ render: Pick<DesktopPaneShotRenderResult, "loadingStateDetected" | "errorStateDetected" | "emptyStateDetected">,
732
+ unavailableSymbols: string[],
733
+ semanticMismatch: boolean,
734
+ ): string {
735
+ if (resolved.capability.id === "intraday-price-chart") {
736
+ const intraday = payload.intradayHistories[0];
737
+ if (intraday?.unavailableReason) return intraday.unavailableReason;
738
+ if (intraday && intraday.points.length > 0) {
739
+ return `Intraday bars were loaded for ${intraday.symbol}, but the chart did not render them.`;
740
+ }
741
+ const symbol = payload.financials[0]?.[0] ?? resolved.createOptions?.symbol ?? "the ticker";
742
+ return `No intraday price history is available for ${symbol} for the requested session window.`;
743
+ }
744
+ if (render.loadingStateDetected) return "The pane was still loading when the screenshot was captured.";
745
+ if (render.errorStateDetected) return "The pane rendered an error state.";
746
+ if (render.emptyStateDetected) return "The pane rendered an empty state.";
747
+ if (unavailableSymbols.length > 0) return `Data is unavailable for ${unavailableSymbols.join(", ")}.`;
748
+ if (semanticMismatch) return "The rendered content did not match the requested capability.";
749
+ return "The pane did not produce verifiable screenshot evidence.";
657
750
  }
658
751
 
659
752
  const STATEMENT_EVIDENCE_KEYS: Record<string, ReadonlySet<string>> = {
@@ -685,6 +778,23 @@ export function shotDataEvidenceFor(
685
778
  resolved: ResolvedPaneFunction,
686
779
  payload: DesktopPaneShotPayload,
687
780
  ): PaneScreenshotDataEvidence | null {
781
+ if (resolved.capability.id === "intraday-price-chart") {
782
+ const intraday = payload.intradayHistories[0];
783
+ if (!intraday || intraday.points.length === 0) return null;
784
+ const evidence = chartSeriesEvidence(intraday.symbol, intraday.points);
785
+ if (!evidence.first || !evidence.last) return null;
786
+ return {
787
+ kind: "price-series",
788
+ symbol: intraday.symbol,
789
+ range: intraday.rangePreset,
790
+ pointCount: evidence.pointCount,
791
+ first: evidence.first,
792
+ last: evidence.last,
793
+ resolution: intraday.resolution,
794
+ sessionDates: intraday.sessionDates,
795
+ };
796
+ }
797
+
688
798
  if (resolved.capability.id === "price-chart") {
689
799
  const range = String(resolved.options.rangePreset ?? "5Y") as TimeRange;
690
800
  const [symbol, financials] = payload.financials[0] ?? [];
@@ -1140,6 +1250,48 @@ export function chartEvidenceMismatchesFor(
1140
1250
  return mismatches;
1141
1251
  }
1142
1252
 
1253
+ export function intradayChartEvidenceMismatchesFor(
1254
+ resolved: ResolvedPaneFunction,
1255
+ payload: DesktopPaneShotPayload,
1256
+ semanticUi: RemoteUiNodeSnapshot[],
1257
+ ): string[] {
1258
+ if (resolved.capability.id !== "intraday-price-chart") return [];
1259
+ const intraday = payload.intradayHistories[0];
1260
+ if (!intraday || intraday.points.length === 0) return [];
1261
+ const metadata = semanticUi.find((node) => (
1262
+ node.role === "chart-data" && node.metadata?.kind === "chart-composer"
1263
+ ))?.metadata;
1264
+ if (!metadata) return ["rendered intraday chart-data semantic evidence is missing"];
1265
+ const baseSeries = Array.isArray(metadata.baseSeries) ? metadata.baseSeries : [];
1266
+ const actual = baseSeries[0];
1267
+ if (!isRecord(actual)) return ["rendered intraday base series is missing"];
1268
+
1269
+ const expected = chartSeriesEvidence(intraday.symbol, intraday.points);
1270
+ const mismatches: string[] = [];
1271
+ if (actual.pointCount !== expected.pointCount) {
1272
+ mismatches.push("rendered intraday point count does not match");
1273
+ }
1274
+ if (metadata.projectedPointCount !== expected.pointCount) {
1275
+ mismatches.push("rendered intraday projection point count does not match");
1276
+ }
1277
+ const pointMatches = (
1278
+ actualPoint: unknown,
1279
+ expectedPoint: PaneScreenshotChartPointEvidence | null,
1280
+ ): boolean => {
1281
+ if (!isRecord(actualPoint) || !expectedPoint) return false;
1282
+ return actualPoint.date === expectedPoint.date
1283
+ && typeof actualPoint.value === "number"
1284
+ && closeEnough(actualPoint.value, expectedPoint.close);
1285
+ };
1286
+ if (!pointMatches(actual.first, expected.first)) {
1287
+ mismatches.push("rendered intraday first bar does not match");
1288
+ }
1289
+ if (!pointMatches(actual.last, expected.last)) {
1290
+ mismatches.push("rendered intraday last bar does not match");
1291
+ }
1292
+ return mismatches;
1293
+ }
1294
+
1143
1295
  export function shotUnavailableSymbols(
1144
1296
  resolved: ResolvedPaneFunction,
1145
1297
  payload: DesktopPaneShotPayload,
@@ -1162,6 +1314,16 @@ export function shotUnavailableSymbols(
1162
1314
  return [];
1163
1315
  });
1164
1316
  }
1317
+ if (resolved.capability.id === "intraday-price-chart") {
1318
+ const symbol = payload.intradayHistories[0]?.symbol ?? payload.financials[0]?.[0];
1319
+ const metadata = semanticUi.find((node) => (
1320
+ node.role === "chart-data" && node.metadata?.kind === "chart-composer"
1321
+ ))?.metadata;
1322
+ const renderedPoints = typeof metadata?.projectedPointCount === "number"
1323
+ ? metadata.projectedPointCount
1324
+ : 0;
1325
+ return symbol && renderedPoints <= 0 ? [symbol] : [];
1326
+ }
1165
1327
  const graphKind = shotGraphKind(resolved);
1166
1328
  if (graphKind) {
1167
1329
  const metric = resolved.options.metric as GraphMetricKey;
@@ -1174,7 +1336,7 @@ export function shotUnavailableSymbols(
1174
1336
  ).length === 0 ? [symbol] : []
1175
1337
  ));
1176
1338
  }
1177
- if (["price-chart", "intraday-price-chart", "historical-prices"].includes(resolved.capability.id)) {
1339
+ if (["price-chart", "historical-prices"].includes(resolved.capability.id)) {
1178
1340
  return payload.financials.flatMap(([symbol, financials]) => financials.priceHistory.length > 0 ? [] : [symbol]);
1179
1341
  }
1180
1342
  if (["price-comparison", "return-correlation", "security-relationship"].includes(resolved.capability.id)) {
@@ -1211,6 +1373,14 @@ export function shotSemanticRowCount(
1211
1373
  count + (isRecord(entry) && typeof entry.pointCount === "number" ? Math.max(0, entry.pointCount) : 0)
1212
1374
  ), 0);
1213
1375
  }
1376
+ if (resolved.capability.id === "intraday-price-chart") {
1377
+ const metadata = semanticUi.find((node) => (
1378
+ node.role === "chart-data" && node.metadata?.kind === "chart-composer"
1379
+ ))?.metadata;
1380
+ return typeof metadata?.projectedPointCount === "number"
1381
+ ? Math.max(0, metadata.projectedPointCount)
1382
+ : 0;
1383
+ }
1214
1384
  const graphKind = shotGraphKind(resolved);
1215
1385
  if (graphKind) {
1216
1386
  const metric = resolved.options.metric as GraphMetricKey;
@@ -1223,7 +1393,7 @@ export function shotSemanticRowCount(
1223
1393
  ).length
1224
1394
  ), 0);
1225
1395
  }
1226
- if (["price-chart", "intraday-price-chart", "historical-prices"].includes(resolved.capability.id)) {
1396
+ if (["price-chart", "historical-prices"].includes(resolved.capability.id)) {
1227
1397
  return payload.financials[0]?.[1].priceHistory.length ?? 0;
1228
1398
  }
1229
1399
  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 () => {