gloomberb 0.16.6 → 0.17.0

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 (348) hide show
  1. package/package.json +2 -1
  2. package/src/api-client/chat-pane-link.ts +47 -0
  3. package/src/api-client/chat.ts +9 -1
  4. package/src/api-client/data.ts +37 -1
  5. package/src/api-client/equity-screener.ts +3 -0
  6. package/src/api-client/europe-energy.ts +91 -0
  7. package/src/api-client/index.ts +6 -0
  8. package/src/api-client/letters.ts +41 -0
  9. package/src/api-client/normalizers.ts +36 -6
  10. package/src/api-client/organization-tickers.ts +44 -0
  11. package/src/api-client/request.ts +3 -1
  12. package/src/api-client/types.ts +27 -1
  13. package/src/app/runtime/desktop-deeplink.ts +48 -15
  14. package/src/architecture/core-builtin-allowlist.json +1 -1
  15. package/src/architecture/dom-cell-metrics-allowlist.json +28 -0
  16. package/src/ask-gloom/pane-context.ts +23 -19
  17. package/src/ask-gloom/pane-request.ts +2 -2
  18. package/src/cli/commands/fred-series.ts +77 -11
  19. package/src/cli/commands/market.ts +77 -20
  20. package/src/cli/commands/overview.ts +29 -20
  21. package/src/cli/commands/ticker.ts +64 -11
  22. package/src/cli/desktop-pane-shot-view.ts +114 -0
  23. package/src/cli/desktop-pane-shot.ts +30 -31
  24. package/src/cli/entry.ts +7 -0
  25. package/src/cli/export-source.ts +43 -0
  26. package/src/cli/freshness.ts +18 -3
  27. package/src/cli/glossary.ts +17 -0
  28. package/src/cli/history-rows.ts +174 -13
  29. package/src/cli/known-gaps.ts +14 -3
  30. package/src/cli/listing-arg.ts +20 -2
  31. package/src/cli/listing-suggestion.ts +159 -0
  32. package/src/cli/native-smoke.ts +13 -0
  33. package/src/cli/not-a-ticker.ts +13 -1
  34. package/src/cli/pane-functions/capabilities.ts +16 -8
  35. package/src/cli/pane-functions/dom.ts +28 -13
  36. package/src/cli/pane-functions/function-help.ts +51 -6
  37. package/src/cli/pane-functions/headless.ts +26 -7
  38. package/src/cli/pane-functions/index.ts +46 -3
  39. package/src/cli/pane-functions/report-notices.ts +6 -3
  40. package/src/cli/pane-functions/report.ts +16 -2
  41. package/src/cli/pane-functions/resolver.ts +7 -4
  42. package/src/cli/report-tables.ts +31 -7
  43. package/src/cli/result.ts +54 -11
  44. package/src/cli/symbol-rows.ts +18 -1
  45. package/src/components/chart/composite/axis-overlays.tsx +2 -0
  46. package/src/components/chart/composite/composite-chart.tsx +4 -21
  47. package/src/components/chart/composite/legend.tsx +10 -6
  48. package/src/components/chart/composite/price-axis-labels.tsx +1 -0
  49. package/src/components/command-bar/list/model.ts +2 -0
  50. package/src/components/command-bar/pane-templates/items.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +1 -0
  52. package/src/components/command-bar/routes/ticker-search/results.ts +3 -1
  53. package/src/components/command-bar/workflow/ops.ts +8 -97
  54. package/src/components/command-bar/workflow/pane-reuse.ts +177 -0
  55. package/src/components/data-table/view.tsx +1 -0
  56. package/src/components/feed-data-table/stack-view.tsx +18 -42
  57. package/src/components/layout/missing-pane.tsx +2 -0
  58. package/src/components/layout/shell/index.tsx +1 -0
  59. package/src/components/layout/shell/pane/ask.ts +12 -7
  60. package/src/components/layout/shell/pane/capture.ts +8 -34
  61. package/src/components/markdown-text.tsx +37 -6
  62. package/src/components/reader/article-reader.tsx +91 -266
  63. package/src/components/reader/detail-reader.tsx +210 -0
  64. package/src/components/reader/reader-text.tsx +239 -0
  65. package/src/components/table-view-shared.tsx +8 -4
  66. package/src/components/ticker/badge/list.tsx +15 -4
  67. package/src/components/ui/choice-dialog.tsx +3 -0
  68. package/src/components/ui/data-table/types.ts +7 -0
  69. package/src/components/ui/detail-scroll-body.tsx +31 -4
  70. package/src/components/ui/display.tsx +46 -1
  71. package/src/components/ui/icon.tsx +11 -1
  72. package/src/components/ui/list-view.tsx +3 -1
  73. package/src/components/ui/query-bar-keys.tsx +2 -0
  74. package/src/components/ui/query-bar.tsx +9 -1
  75. package/src/components/ui/select-button.tsx +3 -0
  76. package/src/data/config/store/fold-panes.ts +202 -11
  77. package/src/data/config/store/migrations.ts +13 -1
  78. package/src/docs/snapshot.json +29 -0
  79. package/src/i18n/es.ts +14 -0
  80. package/src/i18n/ja.ts +14 -0
  81. package/src/i18n/ko.ts +14 -0
  82. package/src/i18n/zh-cn.ts +14 -0
  83. package/src/i18n/zh-tw.ts +14 -0
  84. package/src/layout/desk-keys.ts +7 -0
  85. package/src/layout/desks.ts +2 -2
  86. package/src/market-data/market/status.ts +67 -6
  87. package/src/market-data/quotes/contributions.ts +15 -0
  88. package/src/market-data/quotes/regular-session-valuation.ts +131 -0
  89. package/src/market-data/quotes/resolution.ts +4 -0
  90. package/src/market-data/snapshot-provider.ts +6 -3
  91. package/src/notifications/app-badge.ts +30 -0
  92. package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
  93. package/src/plugins/builtin/account-management/form-components.tsx +53 -1
  94. package/src/plugins/builtin/account-management/keyboard.ts +5 -0
  95. package/src/plugins/builtin/account-management/model.ts +27 -0
  96. package/src/plugins/builtin/account-management/pane.tsx +62 -2
  97. package/src/plugins/builtin/alerts/cli.ts +6 -4
  98. package/src/plugins/builtin/analytics/index.tsx +1 -1
  99. package/src/plugins/builtin/awards/pane.tsx +29 -18
  100. package/src/plugins/builtin/backtest/pane.tsx +2 -1
  101. package/src/plugins/builtin/bond-calculator/index.tsx +3 -5
  102. package/src/plugins/builtin/buildout/detail/company.tsx +7 -4
  103. package/src/plugins/builtin/builtin-plugin-meta.ts +5 -4
  104. package/src/plugins/builtin/catalysts/model.ts +92 -10
  105. package/src/plugins/builtin/catalysts/pane.tsx +6 -4
  106. package/src/plugins/builtin/cds/index.tsx +21 -30
  107. package/src/plugins/builtin/changelog/index.tsx +11 -12
  108. package/src/plugins/builtin/chart-composer/headless-schema.ts +2 -0
  109. package/src/plugins/builtin/chart-composer/headless.ts +87 -5
  110. package/src/plugins/builtin/chart-composer/pane.tsx +3 -2
  111. package/src/plugins/builtin/chart-composer/session-history.ts +20 -5
  112. package/src/plugins/builtin/chat/command-bar.ts +6 -8
  113. package/src/plugins/builtin/chat/content/composer-runtime.ts +2 -2
  114. package/src/plugins/builtin/chat/content/composer.tsx +13 -4
  115. package/src/plugins/builtin/chat/content/footer.tsx +15 -5
  116. package/src/plugins/builtin/chat/content/handle-prompt.tsx +326 -0
  117. package/src/plugins/builtin/chat/content/index.tsx +113 -9
  118. package/src/plugins/builtin/chat/content/layout-metrics.ts +4 -0
  119. package/src/plugins/builtin/chat/content/open-targets.ts +20 -4
  120. package/src/plugins/builtin/chat/content/scroll.ts +6 -1
  121. package/src/plugins/builtin/chat/content/transcript.tsx +11 -2
  122. package/src/plugins/builtin/chat/ghost-user.ts +1 -1
  123. package/src/plugins/builtin/chat/handle-prompt.ts +128 -0
  124. package/src/plugins/builtin/chat/index.tsx +44 -21
  125. package/src/plugins/builtin/chat/layout.ts +7 -14
  126. package/src/plugins/builtin/chat/message/desktop.tsx +59 -11
  127. package/src/plugins/builtin/chat/message/header.tsx +69 -21
  128. package/src/plugins/builtin/chat/message/pane-chip.tsx +223 -0
  129. package/src/plugins/builtin/chat/message/person-icons.tsx +53 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +73 -9
  131. package/src/plugins/builtin/chat/message/render-state.ts +15 -2
  132. package/src/plugins/builtin/chat/message/terminal.tsx +33 -8
  133. package/src/plugins/builtin/chat/message/types.ts +3 -0
  134. package/src/plugins/builtin/chat/pane-links/model.ts +149 -0
  135. package/src/plugins/builtin/chat/pane-links/open.ts +29 -0
  136. package/src/plugins/builtin/chat/pane-links/previews.ts +119 -0
  137. package/src/plugins/builtin/chat/pane-state.ts +5 -8
  138. package/src/plugins/builtin/chat/people.ts +202 -0
  139. package/src/plugins/builtin/chat/profile-popover.ts +18 -2
  140. package/src/plugins/builtin/chat/status-widget.tsx +3 -8
  141. package/src/plugins/builtin/chat/unread-badge.ts +65 -0
  142. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +9 -6
  143. package/src/plugins/builtin/clinical-trials/client.ts +26 -1
  144. package/src/plugins/builtin/clinical-trials/index.tsx +10 -2
  145. package/src/plugins/builtin/clinical-trials/model.ts +269 -0
  146. package/src/plugins/builtin/clinical-trials/pane.tsx +247 -62
  147. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +156 -0
  148. package/src/plugins/builtin/cloud/askg/attached-pane.tsx +3 -3
  149. package/src/plugins/builtin/cloud/askg/pane-attachment.ts +3 -3
  150. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -0
  151. package/src/plugins/builtin/comment-letters/pane.tsx +30 -11
  152. package/src/plugins/builtin/commodities-energy/index.tsx +179 -0
  153. package/src/plugins/builtin/commodity-balances/client.ts +208 -0
  154. package/src/plugins/builtin/commodity-balances/headless.ts +66 -0
  155. package/src/plugins/builtin/commodity-balances/index.tsx +32 -0
  156. package/src/plugins/builtin/commodity-balances/model.ts +261 -0
  157. package/src/plugins/builtin/commodity-balances/pane.tsx +227 -0
  158. package/src/plugins/builtin/composite-plugins.ts +26 -1
  159. package/src/plugins/builtin/credit-boards/index.tsx +21 -42
  160. package/src/plugins/builtin/credit-boards/model.ts +0 -2
  161. package/src/plugins/builtin/credit-conditions/index.tsx +10 -19
  162. package/src/plugins/builtin/credit-documents/index.tsx +27 -10
  163. package/src/plugins/builtin/debt-maturities/index.tsx +3 -17
  164. package/src/plugins/builtin/debug/index.tsx +2 -2
  165. package/src/plugins/builtin/dividend-yield/client.ts +3 -1
  166. package/src/plugins/builtin/dividend-yield/headless.ts +3 -1
  167. package/src/plugins/builtin/dividend-yield/pane.tsx +3 -1
  168. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  169. package/src/plugins/builtin/docs/blocks.tsx +4 -3
  170. package/src/plugins/builtin/docs/pane.tsx +16 -8
  171. package/src/plugins/builtin/earnings/index.tsx +2 -0
  172. package/src/plugins/builtin/earnings-calls/pane.tsx +2 -12
  173. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  174. package/src/plugins/builtin/equity-screener/headless.ts +36 -2
  175. package/src/plugins/builtin/equity-screener/model.ts +121 -68
  176. package/src/plugins/builtin/executives/pane.tsx +3 -1
  177. package/src/plugins/builtin/filing-events/index.tsx +20 -0
  178. package/src/plugins/builtin/fixed-income/index.tsx +46 -0
  179. package/src/plugins/builtin/fixed-income/model.ts +27 -0
  180. package/src/plugins/builtin/fixed-income/pane.tsx +162 -0
  181. package/src/plugins/builtin/fixed-income/shortcut.ts +52 -0
  182. package/src/plugins/builtin/futures/index.tsx +24 -7
  183. package/src/plugins/builtin/futures/series.ts +23 -2
  184. package/src/plugins/builtin/futures/use-front-returns.ts +3 -2
  185. package/src/plugins/builtin/futures-curve/pane.tsx +6 -4
  186. package/src/plugins/builtin/help/shortcut-table.tsx +4 -2
  187. package/src/plugins/builtin/insider/detail.tsx +20 -12
  188. package/src/plugins/builtin/insider/index.tsx +2 -2
  189. package/src/plugins/builtin/iv-history/index.tsx +3 -0
  190. package/src/plugins/builtin/iv-history/screen-pane.tsx +13 -5
  191. package/src/plugins/builtin/jodi/client.ts +220 -0
  192. package/src/plugins/builtin/jodi/headless.ts +63 -0
  193. package/src/plugins/builtin/jodi/index.tsx +32 -0
  194. package/src/plugins/builtin/jodi/model.ts +75 -0
  195. package/src/plugins/builtin/jodi/pane.tsx +141 -0
  196. package/src/plugins/builtin/market-heatmap/evidence.ts +13 -4
  197. package/src/plugins/builtin/market-heatmap/headless.ts +3 -3
  198. package/src/plugins/builtin/market-heatmap/index.tsx +41 -23
  199. package/src/plugins/builtin/market-heatmap/live.ts +2 -1
  200. package/src/plugins/builtin/market-heatmap/portfolio.ts +67 -21
  201. package/src/plugins/builtin/market-movers/table.ts +2 -2
  202. package/src/plugins/builtin/market-valuation/align.ts +10 -9
  203. package/src/plugins/builtin/mna/distress/model.ts +52 -1
  204. package/src/plugins/builtin/mna/distress/pane.tsx +17 -12
  205. package/src/plugins/builtin/mna/index.tsx +2 -0
  206. package/src/plugins/builtin/news/wire/index.tsx +2 -2
  207. package/src/plugins/builtin/news/wire/industry-pane.tsx +28 -14
  208. package/src/plugins/builtin/news/wire/letters/client.ts +108 -0
  209. package/src/plugins/builtin/news/wire/letters/model.ts +69 -0
  210. package/src/plugins/builtin/news/wire/letters/pane.tsx +155 -0
  211. package/src/plugins/builtin/news/wire/mutes.ts +2 -1
  212. package/src/plugins/builtin/news/wire/news/query-presets.ts +4 -1
  213. package/src/plugins/builtin/openfda/client.ts +123 -15
  214. package/src/plugins/builtin/openfda/model.ts +163 -0
  215. package/src/plugins/builtin/openfda/pane.tsx +109 -65
  216. package/src/plugins/builtin/openfda/types.ts +25 -0
  217. package/src/plugins/builtin/options/view.tsx +1 -1
  218. package/src/plugins/builtin/pe-band/model.ts +3 -2
  219. package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
  220. package/src/plugins/builtin/plugin-module.ts +3 -1
  221. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +1 -1
  222. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +19 -4
  223. package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -2
  224. package/src/plugins/builtin/portfolio-list/cli/shared.ts +39 -0
  225. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +18 -5
  226. package/src/plugins/builtin/portfolio-list/live-valuation.ts +25 -13
  227. package/src/plugins/builtin/power/evidence.ts +32 -3
  228. package/src/plugins/builtin/power/gas-client.ts +110 -0
  229. package/src/plugins/builtin/power/gas-model.ts +194 -0
  230. package/src/plugins/builtin/power/gas.tsx +145 -0
  231. package/src/plugins/builtin/power/headless.ts +4 -1
  232. package/src/plugins/builtin/power/index.tsx +19 -9
  233. package/src/plugins/builtin/power/pane.tsx +2 -3
  234. package/src/plugins/builtin/power/prices-headless.ts +75 -7
  235. package/src/plugins/builtin/power/prices.tsx +36 -13
  236. package/src/plugins/builtin/primary-dealers/client.ts +108 -0
  237. package/src/plugins/builtin/primary-dealers/headless.ts +53 -0
  238. package/src/plugins/builtin/primary-dealers/index.tsx +17 -0
  239. package/src/plugins/builtin/primary-dealers/model.ts +206 -0
  240. package/src/plugins/builtin/primary-dealers/pane.tsx +149 -0
  241. package/src/plugins/builtin/relative-rotation/index.tsx +3 -0
  242. package/src/plugins/builtin/research/analyst-model.ts +9 -5
  243. package/src/plugins/builtin/research/headless-schema.ts +2 -1
  244. package/src/plugins/builtin/research/relative-valuation-headless.ts +68 -32
  245. package/src/plugins/builtin/research/relative-valuation-model.ts +45 -24
  246. package/src/plugins/builtin/research/relative-valuation-pane.tsx +39 -4
  247. package/src/plugins/builtin/scanner/flow-pane.tsx +8 -1
  248. package/src/plugins/builtin/sec/filing-display.tsx +3 -36
  249. package/src/plugins/builtin/sec/index.tsx +131 -43
  250. package/src/plugins/builtin/sec/model.ts +148 -2
  251. package/src/plugins/builtin/sec/row-summaries.ts +105 -0
  252. package/src/plugins/builtin/sectors/index.tsx +2 -0
  253. package/src/plugins/builtin/sectors/sector-model.ts +2 -2
  254. package/src/plugins/builtin/series-inventory.ts +1 -0
  255. package/src/plugins/builtin/shared/locked-rows.tsx +1 -1
  256. package/src/plugins/builtin/shared/use-quote-board.ts +179 -43
  257. package/src/plugins/builtin/shared/wall-teaser.tsx +2 -2
  258. package/src/plugins/builtin/shared/zip-text.ts +46 -0
  259. package/src/plugins/builtin/short-interest/watch-model.ts +11 -2
  260. package/src/plugins/builtin/themes/model.ts +2 -2
  261. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +11 -0
  262. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +81 -19
  263. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +1 -1
  264. package/src/plugins/builtin/ticker-detail/headless.ts +2 -2
  265. package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -1
  266. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +7 -5
  267. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -6
  268. package/src/plugins/builtin/time-sales/headless.ts +22 -0
  269. package/src/plugins/builtin/trace-bonds/client.ts +196 -0
  270. package/src/plugins/builtin/trace-bonds/headless.ts +69 -0
  271. package/src/plugins/builtin/trace-bonds/index.tsx +33 -0
  272. package/src/plugins/builtin/trace-bonds/model.ts +115 -0
  273. package/src/plugins/builtin/trace-bonds/pane.tsx +191 -0
  274. package/src/plugins/builtin/treasury-auctions/index.tsx +33 -47
  275. package/src/plugins/builtin/treasury-daily/client.ts +96 -0
  276. package/src/plugins/builtin/treasury-daily/headless.ts +69 -0
  277. package/src/plugins/builtin/treasury-daily/index.tsx +17 -0
  278. package/src/plugins/builtin/treasury-daily/model.ts +69 -0
  279. package/src/plugins/builtin/treasury-daily/pane.tsx +140 -0
  280. package/src/plugins/builtin/world-venue-map/entity-detail.tsx +15 -2
  281. package/src/plugins/builtin/world-venue-map/map.tsx +1 -0
  282. package/src/plugins/builtin-editorial.ts +1 -1
  283. package/src/plugins/catalog-browser.ts +25 -2
  284. package/src/plugins/marketplace/model.ts +57 -21
  285. package/src/renderers/dom/build-assets.ts +14 -5
  286. package/src/renderers/dom/data-table/index.tsx +5 -3
  287. package/src/renderers/dom/desktop/controls.tsx +2 -1
  288. package/src/renderers/dom/desktop/icons.tsx +25 -0
  289. package/src/renderers/dom/desktop/list-view.tsx +1 -1
  290. package/src/renderers/dom/desktop/menu.tsx +1 -1
  291. package/src/renderers/dom/desktop/query-bar.tsx +3 -1
  292. package/src/renderers/dom/host/chart-surface.tsx +7 -2
  293. package/src/renderers/dom/host/resize-rows.ts +111 -0
  294. package/src/renderers/dom/host/sticky-scroll.ts +22 -3
  295. package/src/renderers/dom/host/style.ts +14 -0
  296. package/src/renderers/dom/host/surface-3d.tsx +2 -1
  297. package/src/renderers/dom/host/surface-fit.ts +10 -6
  298. package/src/renderers/dom/host/text.tsx +2 -2
  299. package/src/renderers/dom/styles.css +63 -6
  300. package/src/renderers/electrobun/bun/desktop/app-badge.ts +37 -0
  301. package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
  302. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  303. package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +37 -0
  304. package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -40
  305. package/src/renderers/electrobun/bun/desktop/macos-objc.ts +41 -0
  306. package/src/renderers/electrobun/bun/tui-entry.ts +18 -1
  307. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  308. package/src/renderers/electrobun/view/app-badge-host.ts +17 -0
  309. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +8 -3
  310. package/src/renderers/electrobun/view/main.tsx +2 -0
  311. package/src/renderers/electrobun/view/remote/asset-data-client.ts +5 -1
  312. package/src/series/validate.ts +1 -1
  313. package/src/shares/api.ts +5 -1
  314. package/src/shares/location.ts +31 -0
  315. package/src/shares/open.ts +26 -0
  316. package/src/shares/pane-image.ts +18 -6
  317. package/src/shares/personal-pane.ts +42 -54
  318. package/src/sources/gloomberb-cloud/index.ts +7 -2
  319. package/src/sources/gloomberb-cloud/normalizers.ts +5 -0
  320. package/src/sources/history-coverage.ts +22 -4
  321. package/src/sources/history-result.ts +3 -1
  322. package/src/sources/provider-errors.ts +2 -1
  323. package/src/sources/provider-router/batches.ts +18 -2
  324. package/src/sources/provider-router/index.ts +2 -1
  325. package/src/state/app/context/index.tsx +3 -2
  326. package/src/test-support/builtin-panes.ts +31 -0
  327. package/src/tickers/listing-venues.ts +22 -0
  328. package/src/tickers/search/index.ts +7 -2
  329. package/src/tickers/search/ranking.ts +19 -0
  330. package/src/time-series/chart-data.ts +2 -0
  331. package/src/time-series/parsed-history-cache.ts +41 -11
  332. package/src/time-series/resolve.ts +47 -15
  333. package/src/types/config.ts +1 -1
  334. package/src/types/data-provider.ts +13 -1
  335. package/src/types/financials.ts +13 -0
  336. package/src/types/headless.ts +22 -0
  337. package/src/types/plugin.ts +33 -6
  338. package/src/types/price-history.ts +2 -0
  339. package/src/ui/host.tsx +6 -0
  340. package/src/utils/crypto-pair.ts +1 -1
  341. package/src/utils/currency-pair.ts +66 -0
  342. package/src/utils/decimal-comma.ts +57 -0
  343. package/src/utils/guards.ts +25 -0
  344. package/src/utils/local-utc-offset.ts +37 -0
  345. package/src/utils/plugin-proxy-hosts.json +8 -1
  346. package/src/components/chart/composite/crosshair-readout.tsx +0 -150
  347. package/src/plugins/builtin/chat/message/ai-badge.tsx +0 -18
  348. package/src/plugins/builtin/credit-boards/pane.tsx +0 -30
@@ -1,5 +1,5 @@
1
1
  import type { DesktopPaneShotRenderedRow } from "../desktop-pane-shot";
2
- import { cliStyles, renderSection, renderTable } from "../../utils/cli-output";
2
+ import { cliStyles, cliTerminalWidth, renderSection, renderTable, wrapText } from "../../utils/cli-output";
3
3
  import { formatUtcTime } from "../../utils/utc-time";
4
4
  import type { MarketContext } from "../types";
5
5
  import type { PaneFunctionReport } from "./report";
@@ -14,6 +14,8 @@ const DOM_REPORT_WIDTH = 1280;
14
14
  const DOM_REPORT_HEIGHT = 720;
15
15
  const DOM_LIMITATION =
16
16
  "Only values visible in the rendered pane are returned; clipped or off-screen data may be omitted.";
17
+ /** The widest a lock note wraps to, as the catalog's text. */
18
+ const LOCK_NOTE_WIDTH = 100;
17
19
  const DOM_EVERY_ROW =
18
20
  "Every row of the pane's table is returned, including rows scrolled out of view.";
19
21
 
@@ -119,7 +121,7 @@ function renderDomTables(rows: DesktopPaneShotRenderedRow[]): string[] {
119
121
  function domReportTables(
120
122
  rows: DesktopPaneShotRenderedRow[],
121
123
  title: string,
122
- freshness: ReportFreshness,
124
+ freshness: ReportFreshness | null,
123
125
  incomplete: boolean | string,
124
126
  notices: readonly string[],
125
127
  ): CliReportTables {
@@ -130,7 +132,7 @@ function domReportTables(
130
132
  table.columns.map((column) => column.header),
131
133
  table.rows.map((cells) => table.columns.map((column) => cells.get(column.key))),
132
134
  )),
133
- footer: reportFooterLines({ freshness, incomplete, notes: notices }),
135
+ footer: reportFooterLines({ ...(freshness ? { freshness } : {}), incomplete, notes: notices }),
134
136
  };
135
137
  }
136
138
 
@@ -190,10 +192,19 @@ export function buildDomPaneReportFromRender(
190
192
  const rendered = renderedReportNotices(result.render.semanticUi ?? []);
191
193
  const notices = [...(resolved.pane.reportNotices?.(resolved.instance.settings ?? {}) ?? []), ...rendered.notices];
192
194
  const textLines = [resolved.label, "", ...notices, ...(notices.length > 0 ? [""] : [])];
195
+ // A wall in place of the whole pane: nothing was read, so the report says
196
+ // what is locked and how to reach it, and cites no source.
197
+ const gate = !hasStructuredRows ? result.render.accessGate ?? null : null;
198
+ const lockNote = gate ? resolved.pane.reportLockNote?.({ gate, symbol: result.symbols[0] ?? null }) ?? null : null;
199
+ const locked = gate === "sign-in" || lockNote ? gate : null;
200
+ // JSON and CSV say why the report is empty in the note's words, on one line.
201
+ const lockReason = lockNote?.replace(/\s*\n\s*/g, " ") ?? null;
193
202
  if (hasStructuredRows) {
194
203
  textLines.push(...renderDomTables(rows));
204
+ } else if (lockNote) {
205
+ textLines.push(...wrapText(lockNote, Math.min(cliTerminalWidth() ?? LOCK_NOTE_WIDTH, LOCK_NOTE_WIDTH)));
195
206
  } else if (result.render.visibleText) {
196
- textLines.push(result.render.accessGate === "sign-in" ? signInWallText(result.render.visibleText) : result.render.visibleText);
207
+ textLines.push(gate === "sign-in" ? signInWallText(result.render.visibleText) : result.render.visibleText);
197
208
  } else {
198
209
  textLines.push(failureReason ?? "No rendered values were available.");
199
210
  }
@@ -226,45 +237,49 @@ export function buildDomPaneReportFromRender(
226
237
  truncationReasons,
227
238
  ...(everyRow ? { everyRow } : {}),
228
239
  limitation: everyRow ? DOM_EVERY_ROW : DOM_LIMITATION,
229
- ...(failureReason ? { reason: failureReason } : {}),
240
+ ...(lockReason ?? failureReason ? { reason: lockReason ?? failureReason } : {}),
230
241
  ...(notices.length > 0 ? { metadata: { ...rendered.facts, notices } } : {}),
231
- freshness,
242
+ ...(locked ? { locked } : { freshness }),
232
243
  },
233
244
  text: textLines.join("\n").trimEnd(),
234
245
  tables: domReportTables(
235
246
  rows,
236
247
  resolved.label,
237
- freshness,
238
- !complete && (failureReason ?? (truncationReasons.length > 0 ? truncationReasons.join("; ") : true)),
248
+ locked ? null : freshness,
249
+ !complete && (lockReason ?? failureReason ?? (truncationReasons.length > 0 ? truncationReasons.join("; ") : true)),
239
250
  notices,
240
251
  ),
241
252
  };
242
253
  }
243
254
 
255
+ /** The report's closing lines; a locked view, which read nothing, cites no source. */
244
256
  export function appendDomReportFooter(
245
257
  text: string,
246
258
  elapsedMs: number,
247
259
  view: DomReportView,
248
- freshness: ReportFreshness,
260
+ freshness: ReportFreshness | undefined,
249
261
  ): string {
250
262
  return [
251
263
  text,
252
264
  "",
253
265
  domReportNote(elapsedMs, view),
254
- cliStyles.muted(formatFreshnessLine(freshness)),
266
+ ...(freshness ? [cliStyles.muted(formatFreshnessLine(freshness))] : []),
255
267
  ].join("\n");
256
268
  }
257
269
 
258
- /** How much of the pane a rendered-view report holds: cut, every row its tables publish, or what the viewport shows. */
259
- export type DomReportView = "clipped" | "every-row" | "visible";
270
+ /** How much of the pane a rendered-view report holds: none behind a wall, cut, every row its tables publish, or what the viewport shows. */
271
+ export type DomReportView = "locked" | "clipped" | "every-row" | "visible";
260
272
 
261
273
  export function domReportView(data: Readonly<Record<string, unknown>>): DomReportView {
274
+ if (data.locked) return "locked";
262
275
  return data.truncated === true ? "clipped" : data.everyRow === true ? "every-row" : "visible";
263
276
  }
264
277
 
265
278
  /** What a rendered-view report says about where its values come from. */
266
279
  export function domReportNote(elapsedMs: number, view: DomReportView): string {
267
- const source = view === "every-row"
280
+ const source = view === "locked"
281
+ ? "Rendered view: the pane is locked, so no values were read."
282
+ : view === "every-row"
268
283
  ? "Rendered view: every row of the pane's table, including rows scrolled out of view."
269
284
  : `Rendered view: values come from the visible pane. ${view === "clipped" ? "The rendered view is clipped." : "The rendered view may be clipped."}`;
270
285
  return `${source} Render time: ${elapsedMs} ms.`;
@@ -225,7 +225,8 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
225
225
  bloomberg: ["RV"],
226
226
  },
227
227
  EQS: {
228
- summary: "Screen covered stocks on valuation, growth, margins, liquidity, short interest, insider activity and 13F holders, then rank the matches by any metric.",
228
+ summary: "Screen covered stocks on valuation, growth, margins, liquidity, volatility, short interest, insider activity and 13F holders, then rank the matches by any metric. "
229
+ + "ATR% is the 14-session average true range as a percent of the last close. IV rank is a rank, not an IV percentile: 0 is the lowest 30-day implied volatility of the past 52 weeks and 100 the highest.",
229
230
  usage: ["EQS"],
230
231
  keys: [key("a", "dd criterion"), key("m", "etric"), key("s", "ave screen"), key("o", "pen research")],
231
232
  data: same("Daily snapshot, statements as filed"),
@@ -717,7 +718,7 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
717
718
  bloomberg: ["RRG"],
718
719
  },
719
720
  HM: {
720
- summary: "The largest US stocks and ETFs as a treemap sized by the square root of market cap or assets, or by plain market cap in its settings, and colored by the day's move.",
721
+ summary: "The largest US stocks and ETFs as a treemap sized by the day's move, or by market cap or its square root (assets for ETFs) in its settings, and colored by the day's move.",
721
722
  usage: ["HM"],
722
723
  keys: [OPEN],
723
724
  data: QUOTES,
@@ -780,6 +781,22 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
780
781
  data: same("Weekly, on release"),
781
782
  bloomberg: ["DOE"],
782
783
  },
784
+ PSD: {
785
+ summary: "World production, use, trade and ending stocks for wheat, corn, soybeans and cotton, by country and latest market year. Season-average price forecasts appear when USDA publishes them. The Crops tab of Commodities & Energy.",
786
+ usage: ["PSD"],
787
+ keys: [TABS],
788
+ data: same("Monthly, as USDA updates the market-year tables"),
789
+ bloomberg: [],
790
+ docs: "https://gloom.sh/docs/functions",
791
+ },
792
+ JODI: {
793
+ summary: "Latest monthly crude oil and natural gas balances for countries: production, imports, exports and stocks, and demand for natural gas. The Oil & Gas tab of Commodities & Energy.",
794
+ usage: ["JODI"],
795
+ keys: [TABS],
796
+ data: same("Monthly, as the latest country reports arrive"),
797
+ bloomberg: [],
798
+ docs: "https://gloom.sh/docs/functions",
799
+ },
783
800
  POWER: {
784
801
  summary: "Pro: global power interconnection queues, recorded MW history, completion and withdrawal cohorts, large-load requests and approvals, utility exposure and generation capacity. Open a project for primary evidence and revisions; mapped companies link to DES, FA, G and SPLC.",
785
802
  usage: ["POWER", "POWER NEE"],
@@ -797,10 +814,10 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
797
814
  bloomberg: [],
798
815
  },
799
816
  EPWR: {
800
- summary: "Day-ahead power prices for 32 European markets in EUR/MWh, and the EU carbon allowance (EUA) auction price in EUR/tCO2. Each market shows its latest delivery day, the change on the day before, 7- and 30-day averages, a year ago and where it sits in its 52-week range, with its year charted and its history from 2015; the EUA shows its latest auction day, how the auction was covered, and its auctions since 2020. The Prices tab of POWER.",
801
- usage: ["EPWR", "EPWR DE", "EPWR UK", "EPWR EUA"],
802
- keys: [key("o", "pen Ember"), key("g", "raph"), key("c", "arbon")],
803
- data: same("Daily; each market as of its latest published delivery day, usually a few days back; the EUA on each auction day"),
817
+ summary: "Day-ahead power prices for 32 European markets in EUR/MWh, the EU carbon allowance (EUA) auction price in EUR/tCO2, and European gas storage. Each market shows its latest delivery day, the change on the day before, 7- and 30-day averages, a year ago and where it sits in its 52-week range, with its year charted and its history from 2015; the EUA shows its latest auction day, how the auction was covered, and its auctions since 2020. EPWR GAS shows the EU and each member state's storage as of its latest gas day: % full, TWh, injection and withdrawal in GWh/day, against a year ago and the five-year range, with the year charted against last year and that range. The Prices tab of POWER.",
818
+ usage: ["EPWR", "EPWR DE", "EPWR UK", "EPWR EUA", "EPWR GAS", "EPWR GAS DE"],
819
+ keys: [key("o", "pen Ember or GIE AGSI+"), key("g", "raph"), key("c", "arbon")],
820
+ data: same("Daily; each market as of its latest published delivery day, usually a few days back; the EUA on each auction day; gas storage by gas day, published each evening, the latest day estimated until confirmed"),
804
821
  bloomberg: [],
805
822
  docs: "POWER",
806
823
  },
@@ -927,6 +944,34 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
927
944
  data: same("Daily, after each auction"),
928
945
  bloomberg: ["AUCT"],
929
946
  },
947
+ FI: {
948
+ summary: "A single workspace for credit markets, issuer debt, corporate bonds, Treasury auctions and debt, and primary dealer positions.",
949
+ usage: ["FI"],
950
+ keys: [TABS],
951
+ data: same("As each market publishes"),
952
+ bloomberg: [],
953
+ },
954
+ TRACE: {
955
+ summary: "Recent corporate and agency bond sales with issuer, coupon, price, yield, and grade. Add an issuer to narrow the tape.",
956
+ usage: ["TRACE", "TRACE Ford"],
957
+ keys: [SEARCH],
958
+ data: same("As trades are published"),
959
+ bloomberg: [],
960
+ },
961
+ DTS: {
962
+ summary: "US Treasury daily operating cash and debt held by the public, with recent business days for debt.",
963
+ usage: ["DTS"],
964
+ keys: [TABS],
965
+ data: same("Daily, as Treasury publishes"),
966
+ bloomberg: [],
967
+ },
968
+ PDLR: {
969
+ summary: "Weekly primary dealer net positions and settlement fails across Treasuries and mortgage-backed securities.",
970
+ usage: ["PDLR"],
971
+ keys: [TABS],
972
+ data: same("Weekly, as published"),
973
+ bloomberg: [],
974
+ },
930
975
  BTMM: {
931
976
  summary: "Funding rates (SOFR, EFFR, IORB), Treasury bill yields and their curve, and Fed liquidity from reserves to the reverse repo, each with its one-year percentile.",
932
977
  usage: ["BTMM"],
@@ -31,7 +31,7 @@ import {
31
31
  type CliReportTable,
32
32
  type CliReportTables,
33
33
  } from "../report-tables";
34
- import { labelWithUnit } from "../../utils/display-number";
34
+ import type { ExportIdentity } from "../export-source";
35
35
  import { observationDate } from "../../time-series/price-comparison";
36
36
  import type { ResolvedSeries } from "../../time-series/types";
37
37
  import type { PaneFunctionReport } from "./report";
@@ -268,6 +268,7 @@ export function serializeHeadlessPaneResult(
268
268
  const common = {
269
269
  ...(result.errors ? { errors: result.errors } : {}),
270
270
  ...(result.notes?.length ? { notes: result.notes } : {}),
271
+ ...(result.warnings?.length ? { warnings: result.warnings } : {}),
271
272
  ...(result.metadata ? { metadata: result.metadata } : {}),
272
273
  };
273
274
  switch (definition.shape) {
@@ -478,17 +479,22 @@ function displayedColumns(columns: HeadlessPaneColumn[]): HeadlessPaneColumn[] {
478
479
 
479
480
  const SERIES_FIELDS = ["value", "open", "high", "low", "close", "volume"] as const;
480
481
 
481
- /** Every point of a series: its date (the trading date for session bars) and its values, in the series' unit. */
482
+ /**
483
+ * Every point of a series: its date (the trading date for session bars) and
484
+ * its values, in the series' unit, which the export's `currency_or_unit`
485
+ * column names beside the bare `Value`, `Open`... headers.
486
+ */
482
487
  function seriesTable(series: HeadlessSeries): CliReportTable {
483
488
  const fields = SERIES_FIELDS.filter((field) => series.points.some((point) => point[field] != null));
484
- const unit = series.unit?.trim() ?? "";
485
489
  const dates = series.points.map((point) => seriesPointExportDate(series, point.date));
486
- const header = (field: (typeof SERIES_FIELDS)[number]) => {
487
- const name = `${field[0]!.toUpperCase()}${field.slice(1)}`;
488
- return field === "volume" ? name : labelWithUnit(name, unit);
489
- };
490
+ const header = (field: (typeof SERIES_FIELDS)[number]) => `${field[0]!.toUpperCase()}${field.slice(1)}`;
490
491
  return {
491
492
  title: series.label,
493
+ identity: {
494
+ ...(series.symbol?.trim() ? { symbolOrSeries: series.symbol.trim() } : {}),
495
+ ...(series.unit?.trim() ? { currencyOrUnit: series.unit.trim() } : {}),
496
+ ...(series.interval?.trim() ? { interval: series.interval.trim() } : {}),
497
+ },
492
498
  columns: [dates.every((date) => date.length <= 10) ? "Date" : "Time", ...fields.map(header)],
493
499
  rows: series.points.map((point, index) => [
494
500
  dates[index]!,
@@ -576,6 +582,7 @@ export function headlessReportTables(
576
582
  const { locked, failed } = splitErrors(result);
577
583
  return {
578
584
  tables,
585
+ identity: reportIdentity(definition, result, args),
579
586
  footer: reportFooterLines({
580
587
  freshness,
581
588
  incomplete: incompleteDetail(result, status),
@@ -586,6 +593,18 @@ export function headlessReportTables(
586
593
  };
587
594
  }
588
595
 
596
+ /**
597
+ * What a report's exported rows are of when it is about one symbol or series:
598
+ * the one a chart's series were asked as, else the one symbol asked for or
599
+ * resolved. A series table names its own symbol, unit and interval.
600
+ */
601
+ function reportIdentity(definition: HeadlessPaneDefinition, result: HeadlessPaneResult, args: HeadlessPaneLoadArgs): ExportIdentity {
602
+ const series = definition.shape === "series" ? (result as HeadlessSeriesResult).series : [];
603
+ const charted = [...new Set(series.flatMap((entry) => entry.symbol?.trim() ? [entry.symbol.trim()] : []))];
604
+ const symbols = charted.length > 0 ? charted : args.symbols.length > 0 ? args.symbols : result.symbols ?? [];
605
+ return symbols.length === 1 ? { symbolOrSeries: symbols[0]! } : {};
606
+ }
607
+
589
608
  function reportTitle(
590
609
  definition: HeadlessPaneDefinition,
591
610
  args: HeadlessPaneLoadArgs,
@@ -10,7 +10,7 @@ import {
10
10
  type PaneFunctionCatalog,
11
11
  } from "./catalog";
12
12
  import { createPaneCatalog } from "./discovery";
13
- import { findCatalogSeries } from "../series-search";
13
+ import { catalogFredSeriesIds, findCatalogSeries } from "../series-search";
14
14
  import {
15
15
  parsePaneCatalogArgs,
16
16
  parsePaneFunctionArgs,
@@ -106,12 +106,17 @@ export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
106
106
  `${resolved.token} did not produce a complete bot-safe report.${unavailable}`,
107
107
  );
108
108
  }
109
+ // A chart or other series exported to CSV or NDJSON with no rows fails, rather than writing a header alone.
110
+ if (tabular && report.data.kind === "series" && report.data.empty) {
111
+ const empty = emptySeriesExport(resolved.token, parsed.arg, report.data, catalogFredSeriesIds(ctx.plugins), `--${ctx.cliOptions.format}`);
112
+ ctx.fail(empty.message, empty.details);
113
+ }
109
114
  // --explain follows the report with the terms it shows; JSON lists them under `glossary`.
110
115
  const glossary = parsed.explain ? reportGlossary(resolved.token, report.text) : null;
111
116
  const data = glossary
112
117
  ? { ...report.data, glossary: glossary.map(({ term, short, definition }) => ({ term, ...(short ? { short } : {}), definition })) }
113
118
  : report.data;
114
- const warnings = [...parsed.listingNotes ?? [], ...await choice ?? []];
119
+ const warnings = [...parsed.listingNotes ?? [], ...await choice ?? [], ...report.data.warnings ?? []];
115
120
  // The report states its own freshness in its data and text; JSON cites it as `source` too.
116
121
  ctx.printResult({ data, exportFreshness: report.data.freshness, ...(warnings.length > 0 ? { warnings } : {}) }, {
117
122
  text: () => glossary ? `${report.text}\n\n${renderReportGlossary(resolved.token, glossary)}` : report.text,
@@ -121,6 +126,41 @@ export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
121
126
  });
122
127
  }
123
128
 
129
+ /**
130
+ * What a series export that came back with no rows says: what was asked, why
131
+ * when the report knows, and what to run instead. A FRED series ID given where
132
+ * a ticker goes (`fn G DEXSFUS`) is charted with the `fred:` prefix.
133
+ */
134
+ export function emptySeriesExport(
135
+ token: string,
136
+ arg: string,
137
+ data: { unavailableSymbols: readonly string[]; errors?: readonly string[] },
138
+ fredSeriesIds: ReadonlySet<string>,
139
+ formatFlag = "--csv",
140
+ ): { message: string; details: string } {
141
+ const asked = ["fn", token, arg.trim()].filter(Boolean).join(" ");
142
+ const fred = data.unavailableSymbols
143
+ .map((symbol) => symbol.split(":")[0]!.trim().toUpperCase())
144
+ .filter((id) => /^[A-Z0-9]+$/.test(id) && fredSeriesIds.has(id));
145
+ if (fred.length > 0) {
146
+ const ids = [...new Set(fred)];
147
+ return {
148
+ message: `${asked} has no data to export: ${ids.join(" and ")} ${ids.length === 1 ? "is a FRED series" : "are FRED series"}, not ${ids.length === 1 ? "a ticker" : "tickers"}.`,
149
+ details: `Chart it with the fred: prefix: gloomberb fn G ${ids.map((id) => `fred:${id}`).join(",")} ${formatFlag}`
150
+ + `${ids.length === 1 ? ` (or gloomberb fred ${ids[0]} ${formatFlag})` : ""}.`,
151
+ };
152
+ }
153
+ const missing = data.unavailableSymbols.length > 0 ? ` for ${data.unavailableSymbols.join(", ")}` : "";
154
+ const reasons = (data.errors ?? []).map((error) => error.replace(/^[a-z0-9-]+-\d+: /, ""));
155
+ return {
156
+ message: `${asked} has no data to export${missing}.`,
157
+ details: [
158
+ ...reasons,
159
+ `Check the symbol with gloomberb search, or run gloomberb ${asked} without ${formatFlag} to see the report.`,
160
+ ].join("\n"),
161
+ };
162
+ }
163
+
124
164
  /** How long a capture's process may stay up after its output, for a report it stopped waiting for. */
125
165
  const STALLED_REPORT_EXIT_MS = 1_000;
126
166
 
@@ -263,7 +303,10 @@ async function runPaneCliCommand(ctx: CliCommandContext, run: () => Promise<void
263
303
  // A listing it cannot use says what to type instead; a name that opens nothing, what is close to it.
264
304
  if (error instanceof ListingArgError || error instanceof UnknownPaneFunctionError) ctx.fail(error.message, error.details);
265
305
  const message = error instanceof Error ? error.message : String(error);
266
- ctx.fail(message);
306
+ // A failure the command already worded keeps its advice line.
307
+ const stated = error instanceof Error ? (error as Error & { details?: unknown }).details : undefined;
308
+ const details = typeof stated === "string" ? stated : undefined;
309
+ ctx.fail(message, details);
267
310
  }
268
311
  }
269
312
 
@@ -41,7 +41,9 @@ function publishedSources(value: unknown): NonNullable<HeadlessPaneFreshness["so
41
41
  /**
42
42
  * How current a rendered pane says its data is, from the `report-freshness`
43
43
  * node it publishes: `{ asOf?, status?, delayMinutes?, source?, sources?,
44
- * maxAgeMinutes?, oldestAge?, note? }`. A capture keeps no footer status and
44
+ * maxAgeMinutes?, oldestAge?, note?, basis? }`; `basis` names what a pane
45
+ * that is not a feed lists, when one pane lists different things (NI's
46
+ * stories, and its LTR code's letters). A capture keeps no footer status and
45
47
  * few cells carry a time, so a pane that knows its feed (OMON's chain) dates
46
48
  * the report here, as the plain command over the same data does, and a pane
47
49
  * over several venues (PERP) dates each one as its report does. A pane whose
@@ -50,10 +52,10 @@ function publishedSources(value: unknown): NonNullable<HeadlessPaneFreshness["so
50
52
  */
51
53
  export function renderedReportFreshness(
52
54
  semanticUi: readonly RemoteUiNodeSnapshot[],
53
- ): Pick<HeadlessPaneFreshness, "asOf" | "status" | "delayMinutes" | "source" | "sources" | "maxAgeMinutes" | "oldestAge" | "note"> {
55
+ ): Pick<HeadlessPaneFreshness, "asOf" | "status" | "delayMinutes" | "source" | "sources" | "maxAgeMinutes" | "oldestAge" | "note" | "basis"> {
54
56
  const metadata = semanticUi.find((node) => node.role === "report-freshness")?.metadata;
55
57
  if (!metadata) return {};
56
- const { asOf, status, delayMinutes, source, maxAgeMinutes, oldestAge, note } = metadata as Record<string, unknown>;
58
+ const { asOf, status, delayMinutes, source, maxAgeMinutes, oldestAge, note, basis } = metadata as Record<string, unknown>;
57
59
  const sources = publishedSources(metadata.sources);
58
60
  return {
59
61
  ...(typeof asOf === "string" || isFiniteNumber(asOf) ? { asOf } : {}),
@@ -64,6 +66,7 @@ export function renderedReportFreshness(
64
66
  ...(isFiniteNumber(maxAgeMinutes) && maxAgeMinutes > 0 ? { maxAgeMinutes } : {}),
65
67
  ...(oldestAge === true ? { oldestAge } : {}),
66
68
  ...(typeof note === "string" && note.trim() ? { note } : {}),
69
+ ...(typeof basis === "string" && basis.trim() ? { basis } : {}),
67
70
  };
68
71
  }
69
72
 
@@ -22,8 +22,18 @@ interface PaneFunctionReportData {
22
22
  empty: boolean;
23
23
  complete: boolean;
24
24
  unavailableSymbols: string[];
25
- /** Source, as-of in UTC and live/delayed/stale/not-a-feed status; the last line of the text report. */
26
- freshness: ReportFreshness;
25
+ /** Report errors: a source that did not answer, a symbol that is not one. */
26
+ errors?: string[];
27
+ /** What the report could not do as asked and did instead; `fn` prints them on stderr and in JSON's `warnings`. */
28
+ warnings?: string[];
29
+ /**
30
+ * Source, as-of in UTC and live/delayed/stale/not-a-feed status; the last
31
+ * line of the text report. Absent when a wall covered the pane (`locked`),
32
+ * since nothing was read.
33
+ */
34
+ freshness?: ReportFreshness;
35
+ /** The wall that covered the whole pane, so the report read nothing. */
36
+ locked?: "sign-in" | "pro";
27
37
  [key: string]: unknown;
28
38
  }
29
39
 
@@ -72,6 +82,10 @@ export async function buildFunctionReport(
72
82
  : await buildDomFunctionReport(resolved, context, rawArg);
73
83
  const elapsedMs = Math.max(0, Math.round(performance.now() - startedAt));
74
84
  report.data = { ...report.data, source, elapsedMs };
85
+ // A report about one symbol names it on every exported row; a chart's series name their own.
86
+ if (!report.tables.identity && report.data.symbols.length === 1) {
87
+ report.tables = { ...report.tables, identity: { symbolOrSeries: report.data.symbols[0]! } };
88
+ }
75
89
  if (source === "dom") {
76
90
  const view = domReportView(report.data);
77
91
  report.text = appendDomReportFooter(report.text, elapsedMs, view, report.data.freshness);
@@ -143,14 +143,16 @@ export async function applyListingArgument(
143
143
  const abroad = usOnly ? listings.filter((listing) => listingAbroadVenue(listing.key) != null) : [];
144
144
  if (usOnly && abroad.length > 0) {
145
145
  const token = found.template?.shortcut?.prefix ?? args.target.toUpperCase();
146
- const answers = await Promise.all(abroad.map((listing) => usListingsOnlyMessage(
147
- usOnly, listing, context, (key) => `gloomberb ${command} ${token} ${key}`,
148
- )));
146
+ const answers = await Promise.all(abroad.map(async (listing) => {
147
+ const answer = await usListingsOnlyMessage(usOnly, listing, context, (key) => `gloomberb ${command} ${token} ${key}`);
148
+ const hint = headless?.usListingsOnlyHint?.(listing.key);
149
+ return hint ? { ...answer, details: `${answer.details}\n${hint}` } : answer;
150
+ }));
149
151
  listings = listings.filter((listing) => !abroad.includes(listing));
150
152
  if (listings.length === 0) {
151
153
  throw new ListingArgError(answers.map((answer) => answer.message).join("\n"), answers.map((answer) => answer.details).join("\n"));
152
154
  }
153
- notes.push(...answers.map((answer) => `${answer.message} ${answer.details}`));
155
+ notes.push(...answers.map((answer) => `${answer.message} ${answer.details.replaceAll("\n", " ")}`));
154
156
  }
155
157
  // One listing's notes come with its choice note; a list's are said here.
156
158
  if (listings.length > 1) notes.unshift(...listings.flatMap(listingReadNotes));
@@ -233,6 +235,7 @@ export async function resolvePaneFunction(
233
235
  const normalizedOptions = normalizeCapabilityOptions(capability, args.options, {
234
236
  strict: args.requireBotSafe || (!!headless && resolutionSettings.strictHeadlessOptions === true),
235
237
  code: token,
238
+ argument: args.arg || null,
236
239
  });
237
240
  const settings = {
238
241
  ...optionSettings(args.options),
@@ -17,7 +17,12 @@ import {
17
17
  } from "../utils/display-number";
18
18
  import { isEpochMilliseconds, parseReportTime } from "../utils/utc-time";
19
19
  import type { ReportFreshness } from "./pane-functions/freshness";
20
- import { exportSourceLines } from "./export-source";
20
+ import {
21
+ EXPORT_IDENTITY_COLUMNS,
22
+ exportIdentityCells,
23
+ exportSourceLines,
24
+ type ExportIdentity,
25
+ } from "./export-source";
21
26
 
22
27
  type CliReportValue = string | number;
23
28
 
@@ -29,12 +34,16 @@ export interface CliReportTable {
29
34
  columns: string[];
30
35
  /** Numbers, ISO times and text; an empty string for a missing value. */
31
36
  rows: CliReportValue[][];
37
+ /** What this table's rows are of, where it differs from the report's: one series of a chart. */
38
+ identity?: ExportIdentity;
32
39
  }
33
40
 
34
41
  export interface CliReportTables {
35
42
  tables: CliReportTable[];
36
43
  /** What CSV writes after the data as `#` lines: source and as-of, completeness, errors, notes. */
37
44
  footer: string[];
45
+ /** What the rows are of, for the fixed columns every row ends with (`EXPORT_IDENTITY_COLUMNS`). */
46
+ identity?: ExportIdentity;
38
47
  }
39
48
 
40
49
  /** A column as a report declares it: the text view draws `format(value, row)`, or the value itself. */
@@ -443,14 +452,25 @@ function csvLine(row: readonly unknown[]): string {
443
452
  return `"${first}"${line.slice(first.length)}`;
444
453
  }
445
454
 
455
+ /**
456
+ * A table with the fixed columns after its own, every row naming what it is
457
+ * and the source and as-of the footer cites. A table without columns stays empty.
458
+ */
459
+ function identifiedTable(table: CliReportTable, report: CliReportTables, freshness: ReportFreshness | undefined): CliReportTable {
460
+ if (!freshness || table.columns.length === 0) return table;
461
+ const cells = exportIdentityCells(freshness, { ...report.identity, ...table.identity });
462
+ return { ...table, columns: [...table.columns, ...EXPORT_IDENTITY_COLUMNS], rows: table.rows.map((row) => [...row, ...cells]) };
463
+ }
464
+
446
465
  /**
447
466
  * One table writes as a plain CSV; several write as blocks, each under a
448
467
  * `# section: <title>` line, with a blank line between. The footer follows
449
- * the data as `#` lines after a blank line.
468
+ * the data as `#` lines after a blank line. With the `freshness` the footer
469
+ * cites, every row ends with the fixed columns of `EXPORT_IDENTITY_COLUMNS`.
450
470
  */
451
- export function renderReportCsv(report: CliReportTables): string {
471
+ export function renderReportCsv(report: CliReportTables, freshness?: ReportFreshness): string {
452
472
  const marked = report.tables.length > 1;
453
- const blocks = report.tables.map((table) => [
473
+ const blocks = report.tables.map((table) => identifiedTable(table, report, freshness)).map((table) => [
454
474
  ...(marked ? [commentLine(`section: ${table.title}`)] : []),
455
475
  ...(table.columns.length > 0 ? [[table.columns, ...table.rows].map(csvLine).join("\n")] : []),
456
476
  ].join("\n"));
@@ -462,10 +482,14 @@ export function withCsvFooter(csv: string, footer: readonly string[]): string {
462
482
  return [csv, footer.map(commentLine).join("\n")].filter(Boolean).join("\n\n");
463
483
  }
464
484
 
465
- /** One JSON object per row, keyed by column; rows of a report with several tables name their section. */
466
- export function renderReportNdjson(report: CliReportTables): string {
485
+ /**
486
+ * One JSON object per row, keyed by column; rows of a report with several
487
+ * tables name their section. With the report's `freshness`, every row ends
488
+ * with the fixed fields CSV ends it with.
489
+ */
490
+ export function renderReportNdjson(report: CliReportTables, freshness?: ReportFreshness): string {
467
491
  const tagged = report.tables.length > 1;
468
- return report.tables.flatMap((table) => {
492
+ return report.tables.map((table) => identifiedTable(table, report, freshness)).flatMap((table) => {
469
493
  const sectionKey = table.columns.some((column) => column.toLowerCase() === "section") ? "_section" : "section";
470
494
  return table.rows.map((row) => {
471
495
  const object: Record<string, CliReportValue | null> = tagged ? { [sectionKey]: table.title } : {};
package/src/cli/result.ts CHANGED
@@ -16,7 +16,15 @@ import { formatUtcTime, isEpochMilliseconds, isZonedIsoDateTime } from "../utils
16
16
  import { formatFreshnessLine, type ReportFreshness } from "./pane-functions/freshness";
17
17
  import { withSearchHints, type FredSeriesCheck } from "./not-a-ticker";
18
18
  import { renderReportCsv, renderReportNdjson, withCsvFooter, type CliReportTables } from "./report-tables";
19
- import { exportSource, exportSourceLines, type ExportSource } from "./export-source";
19
+ import {
20
+ EXPORT_IDENTITY_COLUMNS,
21
+ exportIdentityCells,
22
+ exportIdentityFields,
23
+ exportSource,
24
+ exportSourceLines,
25
+ type ExportIdentity,
26
+ type ExportSource,
27
+ } from "./export-source";
20
28
  import { windowRows, type RowWindow, type RowWindowOptions } from "./row-window";
21
29
 
22
30
  export interface CliResult<T = unknown> {
@@ -28,7 +36,8 @@ export interface CliResult<T = unknown> {
28
36
  * (docs/usage.md#how-current-a-report-is): the last line of the text output
29
37
  * and `metadata.freshness` in JSON. Exports cite it too (`exportSource`):
30
38
  * CSV ends with it as `#` lines, or `tables` footer does, and JSON carries
31
- * it as `source`. NDJSON stays rows only.
39
+ * it as `source`. NDJSON stays rows only. CSV and NDJSON rows end with the
40
+ * fixed columns of `EXPORT_IDENTITY_COLUMNS`, its source and as-of among them.
32
41
  */
33
42
  freshness?: ReportFreshness;
34
43
  /**
@@ -88,6 +97,15 @@ export interface CliResultRenderOptions<T = unknown, Row = Record<string, unknow
88
97
  * text view shows them, CSV with its `# section:` and closing `#` lines.
89
98
  */
90
99
  tables?: CliReportTables;
100
+ /**
101
+ * What the rows are of, for the fixed columns (`EXPORT_IDENTITY_COLUMNS`)
102
+ * CSV and NDJSON end every row with when the result cites a source: one
103
+ * identity for every row, or each row's own. Without it a row's own
104
+ * `symbol` and `currency` fields say it, as far as they go.
105
+ */
106
+ exportIdentity?: ExportIdentity | ((row: Row) => ExportIdentity);
107
+ /** What CSV says after the source lines as `# note:` lines: what the rows do not show, such as where a series begins. */
108
+ exportNotes?: readonly string[];
91
109
  }
92
110
 
93
111
  interface CliResultJsonEnvelope<T> extends Omit<CliResult<T>, "freshness" | "exportFreshness"> {
@@ -267,18 +285,34 @@ function serializeColumns<Row>(columns?: CliResultColumn<Row>[]) {
267
285
  function renderCsv<Row extends Record<string, unknown>>(
268
286
  rows: Row[],
269
287
  columns?: CliResultColumn<Row>[],
288
+ identity?: (row: Row) => string[],
270
289
  ): string {
271
290
  const resolvedColumns = columns && columns.length > 0
272
291
  ? columns
273
292
  : inferColumns(rows as Record<string, unknown>[]) as CliResultColumn<Row>[];
274
293
  return serializeCsv([
275
- resolvedColumns.map((column) => column.header),
276
- ...rows.map((row) => resolvedColumns.map((column) => (
277
- column.value ? column.value(row) : row[column.key]
278
- ))),
294
+ [...resolvedColumns.map((column) => column.header), ...identity ? EXPORT_IDENTITY_COLUMNS : []],
295
+ ...rows.map((row) => [
296
+ ...resolvedColumns.map((column) => (column.value ? column.value(row) : row[column.key])),
297
+ ...identity?.(row) ?? [],
298
+ ]),
279
299
  ]);
280
300
  }
281
301
 
302
+ function stringField(row: Record<string, unknown>, key: string): string | null {
303
+ const value = row?.[key];
304
+ return typeof value === "string" && value.trim() ? value : null;
305
+ }
306
+
307
+ /** What a row is of: as the command says, else its own `symbol` and `currency` fields. */
308
+ function rowIdentity<Row extends Record<string, unknown>>(
309
+ row: Row,
310
+ declared: CliResultRenderOptions<unknown, Row>["exportIdentity"],
311
+ ): ExportIdentity {
312
+ if (typeof declared === "function") return declared(row);
313
+ return declared ?? { symbolOrSeries: stringField(row, "symbol"), currencyOrUnit: stringField(row, "currency") };
314
+ }
315
+
282
316
  function textColumns<Row extends Record<string, unknown>>(
283
317
  rows: Row[],
284
318
  columns?: CliResultColumn<Row>[],
@@ -389,16 +423,25 @@ function serializeCliRows<T, Row extends Record<string, unknown>>(
389
423
  renderOptions: CliResultRenderOptions<T, Row>,
390
424
  cited?: ReportFreshness,
391
425
  ): string {
392
- if (renderOptions.tables && options.format === "csv") return renderReportCsv(renderOptions.tables);
393
- if (renderOptions.tables && options.format === "ndjson") return renderReportNdjson(renderOptions.tables);
426
+ if (renderOptions.tables && (options.format === "csv" || options.format === "ndjson")) {
427
+ // A report's tables name what they are of, else the command's one identity for every row does.
428
+ const declared = renderOptions.exportIdentity;
429
+ const tables = renderOptions.tables.identity || !declared || typeof declared === "function"
430
+ ? renderOptions.tables
431
+ : { ...renderOptions.tables, identity: declared };
432
+ return options.format === "csv" ? renderReportCsv(tables, cited) : renderReportNdjson(tables, cited);
433
+ }
394
434
  const window = windowed(asRows(result.data, renderOptions.rows), options, renderOptions);
395
435
  const rows = window.rows;
396
436
  if (options.format === "ndjson") {
397
- return rows.map((row) => JSON.stringify(row)).join("\n");
437
+ return rows.map((row) => JSON.stringify(cited && isPlainObject(row)
438
+ ? { ...row, ...exportIdentityFields(cited, rowIdentity(row as Row, renderOptions.exportIdentity)) }
439
+ : row)).join("\n");
398
440
  }
399
441
  if (options.format === "csv") {
400
- const csv = renderCsv(rows as Row[], renderOptions.columns);
401
- return cited ? withCsvFooter(csv, exportSourceLines(cited)) : csv;
442
+ if (!cited) return renderCsv(rows as Row[], renderOptions.columns);
443
+ const csv = renderCsv(rows as Row[], renderOptions.columns, (row) => exportIdentityCells(cited, rowIdentity(row, renderOptions.exportIdentity)));
444
+ return withCsvFooter(csv, [...exportSourceLines(cited), ...(renderOptions.exportNotes ?? []).map((note) => `note: ${note}`)]);
402
445
  }
403
446
  if (renderOptions.text) {
404
447
  return renderOptions.text(result.data);