gloomberb 0.16.2 → 0.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/package.json +1 -1
  2. package/src/brokers/account-cache.ts +45 -0
  3. package/src/cli/command-options.ts +74 -0
  4. package/src/cli/commands/command-utils.ts +29 -0
  5. package/src/cli/commands/market.ts +170 -54
  6. package/src/cli/commands/overview.ts +62 -19
  7. package/src/cli/commands/ticker.ts +138 -43
  8. package/src/cli/desktop-pane-shot.ts +3 -0
  9. package/src/cli/help.ts +26 -6
  10. package/src/cli/history-rows.ts +187 -15
  11. package/src/cli/index.ts +12 -7
  12. package/src/cli/options.ts +5 -1
  13. package/src/cli/pane-functions/capabilities.ts +5 -0
  14. package/src/cli/pane-functions/dom.ts +69 -16
  15. package/src/cli/pane-functions/function-help.ts +4 -2
  16. package/src/cli/pane-functions/headless.ts +163 -31
  17. package/src/cli/pane-functions/index.ts +9 -2
  18. package/src/cli/pane-functions/report.ts +8 -1
  19. package/src/cli/pane-functions/resolver.ts +28 -2
  20. package/src/cli/pane-functions/screenshot.ts +4 -0
  21. package/src/cli/registry.ts +6 -3
  22. package/src/cli/report-tables.ts +460 -0
  23. package/src/cli/result.ts +13 -3
  24. package/src/components/data-table/export.ts +6 -51
  25. package/src/components/data-table/view.tsx +3 -1
  26. package/src/components/layout/shell/active-drag.ts +67 -35
  27. package/src/components/layout/shell/cursor-occlusion.ts +6 -5
  28. package/src/components/layout/shell/drag/index.ts +12 -7
  29. package/src/components/layout/shell/drag/live.ts +122 -0
  30. package/src/components/layout/shell/drag/overlays.tsx +179 -35
  31. package/src/components/layout/shell/drag/runtime.ts +62 -50
  32. package/src/components/layout/shell/drag/slide.ts +10 -0
  33. package/src/components/layout/shell/index.tsx +37 -51
  34. package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
  35. package/src/components/layout/shell/native/surfaces.ts +30 -14
  36. package/src/components/layout/shell/pane/layers.tsx +419 -179
  37. package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
  38. package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
  39. package/src/data/json-persistence.ts +17 -3
  40. package/src/data/json-storage.ts +2 -0
  41. package/src/data/memory-resource-store.ts +46 -0
  42. package/src/market-data/market/freshness.ts +45 -0
  43. package/src/market-data/quotes/freshness.ts +5 -0
  44. package/src/plugins/builtin/analytics/pane-model.ts +10 -6
  45. package/src/plugins/builtin/attention/pane.tsx +1 -1
  46. package/src/plugins/builtin/catalysts/detail.tsx +1 -1
  47. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  48. package/src/plugins/builtin/holders/client.ts +3 -6
  49. package/src/plugins/builtin/holders/format.ts +31 -1
  50. package/src/plugins/builtin/holders/headless.ts +26 -5
  51. package/src/plugins/builtin/holders/pane.tsx +22 -13
  52. package/src/plugins/builtin/holders/report-header.ts +46 -5
  53. package/src/plugins/builtin/kelly-sizer/pane.tsx +12 -6
  54. package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
  55. package/src/plugins/builtin/members/pane.tsx +10 -0
  56. package/src/plugins/builtin/perps/pane.tsx +1 -1
  57. package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
  58. package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
  59. package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
  60. package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -0
  61. package/src/plugins/builtin/portfolio-list/headless.ts +2 -0
  62. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
  63. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
  64. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
  65. package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
  66. package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
  67. package/src/plugins/builtin/shared/members-menu.tsx +260 -0
  68. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  69. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  70. package/src/plugins/builtin/themes/pane.tsx +3 -0
  71. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
  72. package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
  73. package/src/plugins/builtin/ticker-detail/headless.ts +20 -4
  74. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
  75. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
  76. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
  77. package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
  78. package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
  79. package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
  80. package/src/plugins/builtin/world-venue-map/index.ts +5 -5
  81. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
  82. package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
  83. package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
  84. package/src/plugins/builtin/world-venue-map/model.ts +16 -0
  85. package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
  86. package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
  87. package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
  88. package/src/renderers/browser/app-services.ts +3 -1
  89. package/src/renderers/dom/data-table/dom.ts +31 -0
  90. package/src/renderers/dom/data-table/index.tsx +5 -0
  91. package/src/renderers/dom/host/box.tsx +113 -17
  92. package/src/renderers/dom/host/drag-shield.ts +24 -0
  93. package/src/renderers/dom/host/live-frame.ts +54 -0
  94. package/src/renderers/dom/host/mouse.ts +8 -1
  95. package/src/renderers/dom/host/scroll-box.tsx +6 -5
  96. package/src/renderers/dom/host/tabs.tsx +3 -2
  97. package/src/renderers/dom/styles.css +13 -5
  98. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
  99. package/src/renderers/electrobun/bun/desktop/backend.ts +2 -0
  100. package/src/renderers/electrobun/bun/desktop/initialization.ts +13 -0
  101. package/src/renderers/electrobun/shared/protocol.ts +10 -1
  102. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +3 -0
  103. package/src/renderers/electrobun/view/host/native.ts +0 -10
  104. package/src/renderers/electrobun/view/host/window-move.ts +28 -0
  105. package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
  106. package/src/renderers/electrobun/view/ui-host.tsx +2 -5
  107. package/src/renderers/opentui/ui-host.tsx +7 -1
  108. package/src/sources/gloomberb-cloud/normalizers.ts +16 -20
  109. package/src/sources/provider-router/batches.ts +3 -3
  110. package/src/sources/provider-router/financial-routes.ts +2 -2
  111. package/src/sources/provider-router/financials.ts +70 -7
  112. package/src/sources/provider-router/primary.ts +2 -2
  113. package/src/state/pane-scroll-registry.tsx +67 -35
  114. package/src/test-support/margin-account.ts +108 -0
  115. package/src/time-series/resolution.ts +15 -0
  116. package/src/types/financials.ts +32 -3
  117. package/src/ui/host.tsx +12 -0
  118. package/src/ui/index.tsx +8 -3
  119. package/src/ui/scroll-box-axis.ts +26 -0
  120. package/src/utils/currency-units.ts +8 -3
  121. package/src/utils/depositary-receipt.ts +54 -10
  122. package/src/utils/display-number.ts +63 -0
  123. package/src/utils/fundamentals.ts +17 -1
  124. package/src/utils/price-history-integrity.ts +19 -6
@@ -20,6 +20,16 @@ import type {
20
20
  } from "../../types/plugin";
21
21
  import { cliStyles, renderSection, renderStats, renderTable } from "../../utils/cli-output";
22
22
  import { humanizeCliKey } from "../result";
23
+ import {
24
+ exportEntriesTable,
25
+ exportNumber,
26
+ exportRowsTable,
27
+ float32Shortest,
28
+ reportFooterLines,
29
+ type CliReportTable,
30
+ type CliReportTables,
31
+ } from "../report-tables";
32
+ import { labelWithUnit } from "../../utils/display-number";
23
33
  import { observationDate } from "../../time-series/price-comparison";
24
34
  import type { ResolvedSeries } from "../../time-series/types";
25
35
  import type { PaneFunctionReport } from "./report";
@@ -316,20 +326,6 @@ function displayNumber(value: number): string {
316
326
  return String(float32Shortest(value) ?? Number(value.toPrecision(12)));
317
327
  }
318
328
 
319
- /**
320
- * Some feeds store prices as float32, so 338.93 arrives as 338.929992676. A value
321
- * that is exactly a float32 prints as the shortest decimal that rounds back to it,
322
- * which leaves genuine doubles (never exactly float32 unless short) untouched.
323
- */
324
- function float32Shortest(value: number): number | null {
325
- if (Math.fround(value) !== value) return null;
326
- for (let digits = 1; digits <= 9; digits += 1) {
327
- const candidate = Number(value.toPrecision(digits));
328
- if (Math.fround(candidate) === value) return candidate;
329
- }
330
- return null;
331
- }
332
-
333
329
  function displayTime(time: number): string {
334
330
  if (!Number.isFinite(time)) return "-";
335
331
  return formatUtcTime(new Date(time));
@@ -337,14 +333,36 @@ function displayTime(time: number): string {
337
333
 
338
334
  const CALENDAR_RESOLUTIONS = new Set(["1d", "1wk", "1mo"]);
339
335
 
340
- /** Session bars print the trading date the chart and comparison notice use, not the bar's open instant. */
341
- function seriesPointDate(series: HeadlessSeries, date: HeadlessSeries["points"][number]["date"]): string {
342
- const time = date instanceof Date ? date.getTime() : typeof date === "number" ? date : Date.parse(date);
343
- if (!Number.isFinite(time)) return typeof date === "string" ? date : "-";
336
+ type SeriesPointDate = HeadlessSeries["points"][number]["date"];
337
+
338
+ function seriesPointTime(date: SeriesPointDate): number {
339
+ return date instanceof Date ? date.getTime() : typeof date === "number" ? date : Date.parse(date);
340
+ }
341
+
342
+ function isDateOnly(date: SeriesPointDate): boolean {
343
+ return typeof date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(date);
344
+ }
345
+
346
+ /** Session bars are dated by the trading date the chart and comparison notice use, not the bar's open instant. */
347
+ function calendarSeries(series: HeadlessSeries): (HeadlessSeries & Partial<Pick<ResolvedSeries, "historyResolution" | "timeBasis">>) | null {
344
348
  const resolved = series as HeadlessSeries & Partial<Pick<ResolvedSeries, "historyResolution" | "timeBasis">>;
345
- return resolved.historyResolution && CALENDAR_RESOLUTIONS.has(resolved.historyResolution)
346
- ? observationDate(time, resolved)
347
- : formatUtcTime({ time, dateOnly: typeof date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(date) });
349
+ return resolved.historyResolution && CALENDAR_RESOLUTIONS.has(resolved.historyResolution) ? resolved : null;
350
+ }
351
+
352
+ function seriesPointDate(series: HeadlessSeries, date: SeriesPointDate): string {
353
+ const time = seriesPointTime(date);
354
+ if (!Number.isFinite(time)) return typeof date === "string" ? date : "-";
355
+ const calendar = calendarSeries(series);
356
+ return calendar ? observationDate(time, calendar) : formatUtcTime({ time, dateOnly: isDateOnly(date) });
357
+ }
358
+
359
+ /** The same date for a spreadsheet: the trading date, the calendar date, or the ISO instant. */
360
+ function seriesPointExportDate(series: HeadlessSeries, date: SeriesPointDate): string {
361
+ const time = seriesPointTime(date);
362
+ if (!Number.isFinite(time)) return typeof date === "string" ? date : "";
363
+ const calendar = calendarSeries(series);
364
+ if (calendar) return observationDate(time, calendar);
365
+ return isDateOnly(date) ? date as string : new Date(time).toISOString().replace(".000Z", "Z");
348
366
  }
349
367
 
350
368
  function seriesValue(series: HeadlessSeries, value: number | null): string {
@@ -377,6 +395,10 @@ function renderEntries(entries: HeadlessPaneEntry[]): string {
377
395
  return renderStats(entries.map((entry) => [entry.label, entry.formatted ?? displayValue(entry.value)]));
378
396
  }
379
397
 
398
+ function statisticsEntries(stats: NonNullable<HeadlessSeriesResult["stats"]>): HeadlessPaneEntry[] {
399
+ return Array.isArray(stats) ? stats : Object.entries(stats).map(([label, value]) => ({ label, value }));
400
+ }
401
+
380
402
  function renderBundle(
381
403
  definition: HeadlessPaneDefinition,
382
404
  result: HeadlessBundleResult,
@@ -412,14 +434,118 @@ function renderSeries(result: HeadlessSeriesResult): string[] {
412
434
  ];
413
435
  const lines = [renderRows(rows, columns, undefined)];
414
436
  if (result.stats) {
415
- const entries = Array.isArray(result.stats)
416
- ? result.stats
417
- : Object.entries(result.stats).map(([label, value]) => ({ label, value }));
418
- lines.push("", renderSection("Statistics"), renderEntries(entries));
437
+ lines.push("", renderSection("Statistics"), renderEntries(statisticsEntries(result.stats)));
419
438
  }
420
439
  return lines;
421
440
  }
422
441
 
442
+ /** The headers the text table prints. */
443
+ function displayedColumns(columns: HeadlessPaneColumn[]): HeadlessPaneColumn[] {
444
+ return columns.map((column) => ({ ...column, header: humanizeCliKey(column.header) }));
445
+ }
446
+
447
+ const SERIES_FIELDS = ["value", "open", "high", "low", "close", "volume"] as const;
448
+
449
+ /** Every point of a series: its date (the trading date for session bars) and its values, in the series' unit. */
450
+ function seriesTable(series: HeadlessSeries): CliReportTable {
451
+ const fields = SERIES_FIELDS.filter((field) => series.points.some((point) => point[field] != null));
452
+ const unit = series.unit?.trim() ?? "";
453
+ const dates = series.points.map((point) => seriesPointExportDate(series, point.date));
454
+ const header = (field: (typeof SERIES_FIELDS)[number]) => {
455
+ const name = `${field[0]!.toUpperCase()}${field.slice(1)}`;
456
+ return field === "volume" ? name : labelWithUnit(name, unit);
457
+ };
458
+ return {
459
+ title: series.label,
460
+ columns: [dates.every((date) => date.length <= 10) ? "Date" : "Time", ...fields.map(header)],
461
+ rows: series.points.map((point, index) => [
462
+ dates[index]!,
463
+ ...fields.map((field) => {
464
+ const value = point[field];
465
+ return typeof value === "number" && Number.isFinite(value) ? exportNumber(value) : "";
466
+ }),
467
+ ]),
468
+ };
469
+ }
470
+
471
+ /** What a report leaves out, for the closing `# incomplete` line. */
472
+ function incompleteDetail(
473
+ result: HeadlessPaneResult,
474
+ status: { complete: boolean; unavailableSymbols: string[] },
475
+ ): boolean | string {
476
+ if (status.complete) return false;
477
+ const requested = result.metadata?.requested;
478
+ const available = result.metadata?.available;
479
+ if (typeof requested === "number" && typeof available === "number" && available < requested) {
480
+ return `${available} of ${requested} available`;
481
+ }
482
+ return status.unavailableSymbols.length > 0 ? `no data for ${status.unavailableSymbols.join(", ")}` : true;
483
+ }
484
+
485
+ function textNotices(result: HeadlessPaneResult): string[] {
486
+ const notices = result.metadata?.notices;
487
+ if (!Array.isArray(notices)) return [];
488
+ return [...new Set(notices.filter((notice): notice is string => typeof notice === "string" && notice.trim().length > 0))];
489
+ }
490
+
491
+ /**
492
+ * The tables of a headless report for `--csv` and `--ndjson`, with the text
493
+ * view's columns: a rows or snapshot report is one table, a bundle one per
494
+ * section (key/value sections as `Metric,Value`), a series report one per
495
+ * series with every point, then its statistics. The footer carries the text
496
+ * view's closing line, notices, notes and errors.
497
+ */
498
+ export function headlessReportTables(
499
+ definition: HeadlessPaneDefinition,
500
+ result: HeadlessPaneResult,
501
+ args: HeadlessPaneLoadArgs,
502
+ fallbackTitle: string,
503
+ freshness: ReportFreshness,
504
+ status: { complete: boolean; unavailableSymbols: string[] },
505
+ ): CliReportTables {
506
+ const title = reportTitle(definition, args, result, fallbackTitle);
507
+ let tables: CliReportTable[];
508
+ switch (definition.shape) {
509
+ case "rows": {
510
+ const rowsResult = result as HeadlessPaneResult & { rows: HeadlessPaneRow[]; columns?: HeadlessPaneColumn[] };
511
+ const columns = displayedColumns(columnsFor(rowsResult.rows, rowsResult.columns, definition.columns));
512
+ tables = [exportRowsTable(title, columns, rowsResult.rows)];
513
+ break;
514
+ }
515
+ case "bundle":
516
+ tables = (result as HeadlessBundleResult).sections.map((section) => (
517
+ "rows" in section && section.rows
518
+ ? exportRowsTable(section.title, displayedColumns(columnsFor(section.rows, section.columns, definition.columns)), section.rows)
519
+ : exportEntriesTable(section.title, section.entries)
520
+ ));
521
+ break;
522
+ case "series": {
523
+ const seriesResult = result as HeadlessSeriesResult;
524
+ tables = seriesResult.series.map(seriesTable);
525
+ if (seriesResult.stats) tables.push(exportEntriesTable("Statistics", statisticsEntries(seriesResult.stats)));
526
+ break;
527
+ }
528
+ case "snapshot": {
529
+ const snapshot = result as HeadlessSnapshotResult;
530
+ tables = [exportRowsTable(title, displayedColumns(columnsFor(snapshot.items, undefined, definition.columns)), snapshot.items)];
531
+ break;
532
+ }
533
+ default: {
534
+ const _exhaustive: never = definition.shape;
535
+ return _exhaustive;
536
+ }
537
+ }
538
+ return {
539
+ tables,
540
+ footer: reportFooterLines({
541
+ freshness,
542
+ incomplete: incompleteDetail(result, status),
543
+ errors: result.errors,
544
+ notes: [...textNotices(result), ...(result.notes ?? [])],
545
+ }),
546
+ };
547
+ }
548
+
423
549
  function reportTitle(
424
550
  definition: HeadlessPaneDefinition,
425
551
  args: HeadlessPaneLoadArgs,
@@ -450,11 +576,8 @@ export function renderHeadlessPaneText(
450
576
  freshness: ReportFreshness = deriveHeadlessFreshness(definition, result),
451
577
  ): string {
452
578
  const lines = [cliStyles.bold(reportTitle(definition, args, result, fallbackTitle)), ""];
453
- const notices = result.metadata?.notices;
454
- if (Array.isArray(notices)) {
455
- const textNotices = [...new Set(notices.filter((notice): notice is string => typeof notice === "string" && notice.trim().length > 0))];
456
- if (textNotices.length) lines.push(...textNotices, "");
457
- }
579
+ const notices = textNotices(result);
580
+ if (notices.length) lines.push(...notices, "");
458
581
  switch (definition.shape) {
459
582
  case "rows": {
460
583
  const rowsResult = result as HeadlessPaneResult & { rows: HeadlessPaneRow[]; columns?: HeadlessPaneColumn[] };
@@ -513,6 +636,7 @@ export async function buildHeadlessFunctionReport(
513
636
  const unavailableSymbols = loaded.result.unavailableSymbols ?? (rowCount === 0 ? symbols : []);
514
637
  const serialized = serializeHeadlessPaneResult(loaded.definition, loaded.result);
515
638
  const freshness = deriveHeadlessFreshness(loaded.definition, loaded.result);
639
+ const complete = loaded.result.complete !== false && unavailableSymbols.length === 0 && !loaded.result.errors?.length;
516
640
  return {
517
641
  data: {
518
642
  kind: loaded.definition.shape,
@@ -522,7 +646,7 @@ export async function buildHeadlessFunctionReport(
522
646
  options: resolved.options,
523
647
  rowCount,
524
648
  empty: rowCount === 0,
525
- complete: loaded.result.complete !== false && unavailableSymbols.length === 0 && !loaded.result.errors?.length,
649
+ complete,
526
650
  unavailableSymbols,
527
651
  ...serialized,
528
652
  freshness,
@@ -534,5 +658,13 @@ export async function buildHeadlessFunctionReport(
534
658
  resolved.label,
535
659
  freshness,
536
660
  ),
661
+ tables: headlessReportTables(
662
+ loaded.definition,
663
+ loaded.result,
664
+ loaded.args,
665
+ resolved.label,
666
+ freshness,
667
+ { complete, unavailableSymbols },
668
+ ),
537
669
  };
538
670
  }
@@ -23,6 +23,7 @@ import {
23
23
  import { accessGateStatus, incompleteReportGateMessage } from "./access-gate";
24
24
  import { withPersistedCloudSession } from "./cloud-session";
25
25
  import { loadForListing } from "../listing-arg";
26
+ import { selectReportTables } from "../report-tables";
26
27
 
27
28
  async function withPaneRuntime<T>(
28
29
  ctx: CliCommandContext,
@@ -33,7 +34,7 @@ async function withPaneRuntime<T>(
33
34
  registry: PaneFunctionCatalog;
34
35
  resolved: ResolvedPaneFunction;
35
36
  }) => Promise<T>,
36
- settings: { strictHeadlessOptions?: boolean } = {},
37
+ settings: { strictHeadlessOptions?: boolean; tableSection?: boolean } = {},
37
38
  ): Promise<T> {
38
39
  return withMarketData(ctx, async (market) => {
39
40
  const context: MarketContext = ctx.cliOptions.refresh ? { ...market, refresh: true } : market;
@@ -59,6 +60,11 @@ export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
59
60
  + `Use "gloomberb catalog ${resolved.token}" to inspect readiness.`,
60
61
  );
61
62
  }
63
+ const tabular = ctx.cliOptions.format === "csv" || ctx.cliOptions.format === "ndjson";
64
+ if (resolved.tableSection !== undefined && !tabular) {
65
+ throw new Error("--section picks one table of --csv or --ndjson output.");
66
+ }
67
+ if (resolved.tableSection === true) throw new Error("--section needs a section title or number.");
62
68
  // A report that fails or comes back empty for an exchange the symbol is not listed on says so.
63
69
  const report = await withPersistedCloudSession(
64
70
  context,
@@ -82,8 +88,9 @@ export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
82
88
  }
83
89
  ctx.printResult({ data: report.data }, {
84
90
  text: () => report.text,
91
+ ...(tabular ? { tables: selectReportTables(report.tables, resolved.tableSection) } : {}),
85
92
  });
86
- }, { strictHeadlessOptions: true });
93
+ }, { strictHeadlessOptions: true, tableSection: true });
87
94
  });
88
95
  }
89
96
 
@@ -2,8 +2,9 @@ import type { MarketContext } from "../types";
2
2
  import type { NormalizedPaneFunctionOptions } from "./capabilities";
3
3
  import type { ResolvedPaneFunction } from "./resolver";
4
4
  import { buildHeadlessFunctionReport } from "./headless";
5
- import { appendDomReportFooter, buildDomFunctionReport } from "./dom";
5
+ import { appendDomReportFooter, buildDomFunctionReport, domReportNote } from "./dom";
6
6
  import type { ReportFreshness } from "./freshness";
7
+ import type { CliReportTables } from "../report-tables";
7
8
 
8
9
  export type PaneFunctionReportSource = "headless" | "dom";
9
10
 
@@ -29,6 +30,8 @@ interface PaneFunctionReportData {
29
30
  export interface PaneFunctionReport {
30
31
  data: PaneFunctionReportData;
31
32
  text: string;
33
+ /** What `--csv` and `--ndjson` write: the text view's tables as flat rows, and the closing `#` lines. */
34
+ tables: CliReportTables;
32
35
  }
33
36
 
34
37
  export function resolvePaneFunctionReportSource(
@@ -61,6 +64,10 @@ export async function buildFunctionReport(
61
64
  report.data.truncated === true,
62
65
  report.data.freshness,
63
66
  );
67
+ report.tables = {
68
+ ...report.tables,
69
+ footer: [...report.tables.footer, `note: ${domReportNote(elapsedMs, report.data.truncated === true)}`],
70
+ };
64
71
  }
65
72
  return report;
66
73
  }
@@ -25,6 +25,7 @@ import {
25
25
  type ParsedPaneFunctionArgs,
26
26
  } from "./options";
27
27
  import {
28
+ capabilityHasOption,
28
29
  capabilityPaneSettings,
29
30
  getHeadlessPaneDefinition,
30
31
  getPaneFunctionCapability,
@@ -46,6 +47,8 @@ export interface ResolvedPaneFunction {
46
47
  optionSettings: Record<string, unknown>;
47
48
  capability: PaneFunctionCapability;
48
49
  options: NormalizedPaneFunctionOptions;
50
+ /** `--section` of a report: the table `--csv` and `--ndjson` write, when the function has no `section` option of its own. */
51
+ tableSection?: string | true;
49
52
  }
50
53
 
51
54
  async function buildPaneInstance(
@@ -126,12 +129,28 @@ export async function applyListingArgument(
126
129
  return { ...args, arg: listing.key, options, listing };
127
130
  }
128
131
 
132
+ /**
133
+ * `--section` picks one table of a report's `--csv` or `--ndjson` output. A
134
+ * function that has a `section` option of its own (CALLS: which part of a
135
+ * transcript) keeps the flag; its report is one table, so there is nothing to
136
+ * pick there.
137
+ */
138
+ function takeTableSection(
139
+ capability: PaneFunctionCapability,
140
+ options: ParsedPaneFunctionArgs["options"],
141
+ ): { options: ParsedPaneFunctionArgs["options"]; tableSection?: string | true } {
142
+ if (!("section" in options) || capabilityHasOption(capability, "section")) return { options };
143
+ const { section, ...rest } = options;
144
+ return { options: rest, tableSection: section };
145
+ }
146
+
129
147
  export async function resolvePaneFunction(
130
148
  registry: PaneFunctionCatalog,
131
149
  context: MarketContext,
132
- args: ParsedPaneFunctionArgs,
133
- resolutionSettings: { strictHeadlessOptions?: boolean } = {},
150
+ parsedArgs: ParsedPaneFunctionArgs,
151
+ resolutionSettings: { strictHeadlessOptions?: boolean; tableSection?: boolean } = {},
134
152
  ): Promise<ResolvedPaneFunction> {
153
+ let args = parsedArgs;
135
154
  if (!args.target) {
136
155
  throw new Error("Usage: gloomberb fn <function-or-pane> [argument] [--key value]");
137
156
  }
@@ -152,6 +171,12 @@ export async function resolvePaneFunction(
152
171
  const createOptions = buildCreateOptions(template, args.arg);
153
172
  const headless = getHeadlessPaneDefinition(template, pane);
154
173
  const capability = getPaneFunctionCapability(template, pane);
174
+ let tableSection: string | true | undefined;
175
+ if (resolutionSettings.tableSection) {
176
+ const taken = takeTableSection(capability, args.options);
177
+ args = { ...args, options: taken.options };
178
+ tableSection = taken.tableSection;
179
+ }
155
180
  const normalizedOptions = normalizeCapabilityOptions(capability, args.options, {
156
181
  strict: args.requireBotSafe || (!!headless && resolutionSettings.strictHeadlessOptions === true),
157
182
  });
@@ -177,6 +202,7 @@ export async function resolvePaneFunction(
177
202
  optionSettings: settings,
178
203
  capability,
179
204
  options: normalizedOptions,
205
+ ...(tableSection !== undefined ? { tableSection } : {}),
180
206
  };
181
207
  resolved.instance = await buildPaneInstance(resolved, context, args.arg);
182
208
  return resolved;
@@ -74,6 +74,7 @@ import type { ResolvedSeries } from "../../time-series/types";
74
74
  import { getQuoteMonitorPaneSettings } from "../../plugins/builtin/ticker-detail/settings";
75
75
  import { accessGateSentence } from "./access-gate";
76
76
  import { findCollection } from "../../plugins/builtin/portfolio-list/cli/render";
77
+ import { peekCachedBrokerAccounts } from "../../plugins/builtin/portfolio-list/cached-account";
77
78
  import {
78
79
  paneEvidenceMismatches,
79
80
  paneScreenshotEvidenceHook,
@@ -625,6 +626,9 @@ export async function buildDesktopShotPayload(
625
626
  deviceScaleFactor,
626
627
  watermark,
627
628
  tickers,
629
+ ...(COLLECTION_PANE_IDS.has(resolved.pane.id)
630
+ ? { brokerAccounts: peekCachedBrokerAccounts(context.config, context.persistence?.resources) }
631
+ : {}),
628
632
  financials,
629
633
  ...(instrumentFinancials?.length ? { instrumentFinancials } : {}),
630
634
  ...(historyVariants?.length ? { historyVariants } : {}),
@@ -27,6 +27,8 @@ interface RegisteredCliCommand {
27
27
  command: CliCommandDef;
28
28
  ownerId: string;
29
29
  source: "core" | "plugin";
30
+ /** Core or a built-in plugin: its declared options are the only ones it takes. External plugins are not held to theirs. */
31
+ builtin: boolean;
30
32
  }
31
33
 
32
34
  export interface CliCommandRegistry {
@@ -80,6 +82,7 @@ export function buildCliCommandRegistry({
80
82
  command: CliCommandDef,
81
83
  ownerId: string,
82
84
  source: "core" | "plugin",
85
+ builtin: boolean,
83
86
  ) => {
84
87
  const tokens = new Set([
85
88
  validateCommandToken(command.name, ownerId, source),
@@ -95,7 +98,7 @@ export function buildCliCommandRegistry({
95
98
  );
96
99
  }
97
100
 
98
- const entry: RegisteredCliCommand = { command, ownerId, source };
101
+ const entry: RegisteredCliCommand = { command, ownerId, source, builtin };
99
102
  allCommands.push(entry);
100
103
  for (const token of tokens) {
101
104
  allTokens.set(token, entry);
@@ -103,7 +106,7 @@ export function buildCliCommandRegistry({
103
106
  };
104
107
 
105
108
  for (const command of coreCommands) {
106
- registerCommand(command, "core", "core");
109
+ registerCommand(command, "core", "core", true);
107
110
  }
108
111
 
109
112
  const loadablePlugins: GloomPlugin[] = [];
@@ -117,7 +120,7 @@ export function buildCliCommandRegistry({
117
120
  }
118
121
  loadablePlugins.push(entry.plugin);
119
122
  for (const command of entry.plugin.cliCommands ?? []) {
120
- registerCommand(command, entry.plugin.id, "plugin");
123
+ registerCommand(command, entry.plugin.id, "plugin", entry.source === "builtin");
121
124
  }
122
125
  }
123
126