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
@@ -34,6 +34,7 @@ import { futuresContractLines, futuresSettleText, isFuturesInstrument, loadFutur
34
34
  import type { AppConfig } from "../../types/config";
35
35
  import type { FinancialStatement, Quote, TickerFinancials } from "../../types/financials";
36
36
  import { quoteTradingDay } from "../../market-data/quotes/day-range";
37
+ import { atRegularSessionPrice } from "../../market-data/quotes/regular-session-valuation";
37
38
  import { computeTickerPriceReturns } from "../../market-data/ticker-price-returns";
38
39
  import { instrumentTypeLabel, isCryptoVenuePerpetual, resolveTickerInstrumentKind } from "../../tickers/instrument-kind";
39
40
  import type { SecFilingItem } from "../../types/data-provider";
@@ -324,6 +325,8 @@ interface FundamentalsMetric {
324
325
  text: string;
325
326
  value?: number | string | null;
326
327
  unit?: string;
328
+ /** A figure the text prints inside another line, which CSV gives a row of its own. */
329
+ exportOnly?: boolean;
327
330
  }
328
331
 
329
332
  type MetricFigure = Omit<FundamentalsMetric, "label">;
@@ -395,7 +398,7 @@ function fundamentalsMetrics(
395
398
  }
396
399
 
397
400
  function metricLines(metrics: readonly FundamentalsMetric[]): Array<[string, string]> {
398
- return metrics.map((metric) => [metric.label, metric.text]);
401
+ return metrics.filter((metric) => !metric.exportOnly).map((metric) => [metric.label, metric.text]);
399
402
  }
400
403
 
401
404
  const TRAILING_MONEY_METRICS = new Set(["EPS (TTM)", "Revenue (TTM)", "Net Income (TTM)", "Operating Cash Flow (TTM)", "Free Cash Flow (TTM)"]);
@@ -437,8 +440,53 @@ function fundFactsNote(
437
440
  return !exchange || isUsListingExchange(exchange) ? `${note}; try fn SEC ${symbol}.` : `${note}.`;
438
441
  }
439
442
 
440
- /** The lines `fundamentals` or `valuation` reports, the ones with a value. */
441
- function fundamentalsReportMetrics(financials: FundamentalsReportData, view: "fundamentals" | "valuation"): FundamentalsMetric[] {
443
+ /**
444
+ * The price a valuation is measured at, as `quote` and `ticker` read it: Last
445
+ * and Change are the regular session (after the close, its close and that
446
+ * close's move), and a pre-market, after-hours or overnight print is a line
447
+ * of its own, measured from that close.
448
+ */
449
+ function valuationPriceMetrics(financials: FundamentalsReportData): FundamentalsMetric[] {
450
+ const quote = financials.quote;
451
+ const session = getRegularSessionDisplay(quote);
452
+ if (!quote || !session) return [];
453
+ const options = withCurrencyMinorDigits(quoteFormatOptions(quote, undefined, financials.quoteMetadata?.instrumentType), quote.currency);
454
+ const price = (value: number) => formatMarketPriceWithCurrency(value, quote.currency, options);
455
+ const extended = getExtendedSessionDisplay(quote);
456
+ const extendedLabel = extended ? EXTENDED_SESSION_LABELS[extended.session] : "";
457
+ return [
458
+ { label: "Last", text: price(session.price), value: session.price, unit: quote.currency },
459
+ session.changePercent != null && Number.isFinite(session.changePercent)
460
+ ? {
461
+ label: "Change",
462
+ text: colorBySign(`${formatMarketChangeWithCurrency(session.change, quote.currency, options, session.price)} (${formatPercentRaw(session.changePercent)})`, session.change),
463
+ value: session.changePercent,
464
+ unit: "%",
465
+ }
466
+ : { label: "Change", text: "—" },
467
+ ...extended ? [
468
+ {
469
+ label: extendedLabel,
470
+ // "mid" when the overnight price is the session's bid and ask midpoint, as `quote` marks it.
471
+ text: colorBySign(`${price(extended.price)}${extended.mark ? " mid" : ""}${extended.changePercent != null ? ` (${formatPercentRaw(extended.changePercent)})` : ""}`, extended.change),
472
+ value: extended.price,
473
+ unit: quote.currency,
474
+ },
475
+ extended.changePercent != null
476
+ ? { label: `${extendedLabel} Change`, text: formatPercentRaw(extended.changePercent), value: extended.changePercent, unit: "%", exportOnly: true }
477
+ : { label: `${extendedLabel} Change`, text: "—" },
478
+ ] : [],
479
+ ];
480
+ }
481
+
482
+ /**
483
+ * The lines `fundamentals` or `valuation` reports, the ones with a value.
484
+ * Market cap, the P/E multiples and the dividend yield are measured at the
485
+ * regular session price (atRegularSessionPrice), which `valuation` names in
486
+ * its first lines.
487
+ */
488
+ function fundamentalsReportMetrics(snapshot: FundamentalsReportData, view: "fundamentals" | "valuation"): FundamentalsMetric[] {
489
+ const financials = atRegularSessionPrice(snapshot);
442
490
  const quote = financials.quote;
443
491
  const fundamentals = financials.fundamentals;
444
492
  const capitalization = selectMarketCapitalization(quote, fundamentals);
@@ -446,9 +494,10 @@ function fundamentalsReportMetrics(financials: FundamentalsReportData, view: "fu
446
494
  ? { text: `${formatCompact(capitalization.value)} ${capitalization.currency}`, value: capitalization.value, unit: capitalization.currency }
447
495
  : { text: "—" };
448
496
  const metrics = fundamentalsMetrics(quote, fundamentals, financials.profile, marketCap, computeTickerPriceReturns(financials), fundamentalsCurrency(financials));
449
- return (view === "valuation"
450
- ? metrics.filter(({ label }) => VALUATION_METRICS.has(label) || label.startsWith("Dividend Yield"))
451
- : metrics).filter((metric) => metric.text !== "—");
497
+ if (view === "fundamentals") return metrics.filter((metric) => metric.text !== "—");
498
+ const valuation = metrics.filter(({ label, text }) => (VALUATION_METRICS.has(label) || label.startsWith("Dividend Yield")) && text !== "—");
499
+ // A listing with no valuation (a fund) says so rather than printing its price alone.
500
+ return valuation.length === 0 ? [] : [...valuationPriceMetrics(financials).filter((metric) => metric.text !== "—"), ...valuation];
452
501
  }
453
502
 
454
503
  /** Text for `gloomberb fundamentals` and `gloomberb valuation`: the ticker report's fundamentals without the rest. */
@@ -616,7 +665,9 @@ export async function buildTickerReport({
616
665
  lines.push(cliStyles.muted(membershipParts.join(METADATA_SEPARATOR)));
617
666
  }
618
667
 
619
- const capitalization = selectMarketCapitalization(quote, fundamentals);
668
+ // Market cap and the multiples are measured at the regular session price the Quote section heads with.
669
+ const priced = atRegularSessionPrice(financials);
670
+ const capitalization = selectMarketCapitalization(priced.quote, priced.fundamentals);
620
671
  const convertedMarketCap = capitalization ? await toBase(capitalization.value, capitalization.currency) : Number.NaN;
621
672
  const marketCapText = capitalization
622
673
  ? Number.isFinite(convertedMarketCap)
@@ -624,7 +675,7 @@ export async function buildTickerReport({
624
675
  : `${formatCompact(capitalization.value)} ${capitalization.currency}`
625
676
  : "—";
626
677
  // Shown in the same currency as the market cap, so the two can be compared.
627
- const enterpriseValue = reportedEnterpriseValue(fundamentals);
678
+ const enterpriseValue = reportedEnterpriseValue(priced.fundamentals);
628
679
  const evCurrency = enterpriseValueCurrency(quote, fundamentals);
629
680
  // A minor unit such as GBp stays as reported: the converter would read it as the major currency.
630
681
  const convertedEnterpriseValue = enterpriseValue != null && evCurrency && /^[A-Z]{3}$/.test(evCurrency)
@@ -672,7 +723,7 @@ export async function buildTickerReport({
672
723
  ]);
673
724
  }
674
725
 
675
- const metrics = fundamentalsMetrics(quote, fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials), { text: enterpriseValueText });
726
+ const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials), { text: enterpriseValueText });
676
727
  const beforeFundamentals = lines.length;
677
728
  appendMetricSection(lines, "Fundamentals", metricLines(metrics));
678
729
  for (const note of trailingConversionNotes(fundamentals, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
@@ -759,6 +810,8 @@ function buildTickerStructuredData({
759
810
  const quote = financials.quote;
760
811
  const priceReturns = computeTickerPriceReturns(financials, tickerFile?.metadata.assetCategory);
761
812
  const identity = listingIdentity(listing, quote);
813
+ // Market cap and the multiples at the regular session price, as the text report gives them.
814
+ const priced = atRegularSessionPrice(financials);
762
815
  return {
763
816
  symbol,
764
817
  listing: { symbol: identity.symbol, exchange: identity.exchange || null, name: identity.name },
@@ -770,7 +823,7 @@ function buildTickerStructuredData({
770
823
  ...getQuoteSessionFields(quote),
771
824
  priceBasis: quote.priceBasis ?? null,
772
825
  currency: quote.currency,
773
- marketCap: quote.marketCap ?? null,
826
+ marketCap: priced.quote?.marketCap ?? null,
774
827
  volume: quote.volume ?? null,
775
828
  exchangeName: quote.exchangeName ?? "",
776
829
  fullExchangeName: quote.fullExchangeName ?? "",
@@ -794,7 +847,7 @@ function buildTickerStructuredData({
794
847
  positions: tickerFile.metadata.positions,
795
848
  } : null,
796
849
  fundamentals: financials.fundamentals || priceReturns.return1Y != null || priceReturns.return3Y != null ? exportedFundamentals({
797
- ...financials.fundamentals,
850
+ ...priced.fundamentals,
798
851
  ...priceReturns,
799
852
  }) : undefined,
800
853
  profile: financials.profile,
@@ -0,0 +1,114 @@
1
+ import { existsSync } from "fs";
2
+ import { mkdtemp, rm } from "fs/promises";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { buildViewBundle, electrobunViewPath, readViewStylesheet } from "../renderers/dom/build-assets";
6
+
7
+ /**
8
+ * The page `shot` and rendered `fn` reports draw a pane in: the view bundle,
9
+ * the chunks it loads later (the world map's land, the docs) and the
10
+ * stylesheet, by path under the page's root. What changes per capture, the
11
+ * pane and its data, goes in the page's HTML instead (desktop-pane-shot.ts).
12
+ *
13
+ * A source checkout builds these on first use. The release binary has no
14
+ * sources to build from, so scripts/build.ts builds them once, packs them
15
+ * with `packShotView` and embeds the archive, which its entry hands to
16
+ * `usePrebuiltShotView` before the CLI starts. The desktop app ships the same
17
+ * archive alongside its CLI entry rather than embedding it in an executable.
18
+ */
19
+ export type ShotViewFiles = ReadonlyMap<string, string>;
20
+
21
+ const SHOT_VIEW_ENTRY = "cli-pane-shot-entry.tsx";
22
+ /** The bundle the page loads, named after its entry. */
23
+ export const SHOT_VIEW_SCRIPT = "cli-pane-shot-entry.js";
24
+ export const SHOT_VIEW_STYLESHEET = "styles.css";
25
+ export const SHOT_VIEW_ARCHIVE = "shot-view.json.gz";
26
+
27
+ const MISSING_VIEW_MESSAGE = "This copy of Gloomberb cannot draw panes: it was built without the page they are drawn in. "
28
+ + "Reinstall or update Gloomberb. The standalone gloomberb binary "
29
+ + "(https://github.com/gloom-sh/gloomberb/releases/latest) also includes it.";
30
+
31
+ let prebuiltArchive: string | null = null;
32
+ let loading: Promise<ShotViewFiles> | null = null;
33
+
34
+ /** Builds the page's files into `outdir`, for a capture or for scripts/build.ts to embed. */
35
+ export async function buildShotView(outdir: string, apiUrl: string): Promise<void> {
36
+ await buildViewBundle({
37
+ entrypoint: electrobunViewPath(SHOT_VIEW_ENTRY),
38
+ outdir,
39
+ sourcemap: "none",
40
+ // The page has no `process`, so the cloud endpoint is baked in.
41
+ define: { __GLOOMBERB_API_URL__: JSON.stringify(apiUrl) },
42
+ aliasRules: [
43
+ ["backend-rpc", "native-stubs/backend-rpc.ts"],
44
+ ],
45
+ failureMessage: "Failed to build desktop pane screenshot renderer.",
46
+ missingEntryMessage: "Desktop pane screenshot build did not produce a JavaScript entrypoint.",
47
+ });
48
+ await Bun.write(join(outdir, SHOT_VIEW_STYLESHEET), await readViewStylesheet());
49
+ }
50
+
51
+ async function readShotViewDir(dir: string): Promise<Map<string, string>> {
52
+ const files = new Map<string, string>();
53
+ for await (const path of new Bun.Glob("**/*").scan({ cwd: dir })) {
54
+ files.set(path.replaceAll("\\", "/"), await Bun.file(join(dir, path)).text());
55
+ }
56
+ return files;
57
+ }
58
+
59
+ /** The files `buildShotView` wrote, as one gzipped JSON object of path to text. */
60
+ export async function packShotView(dir: string): Promise<Uint8Array> {
61
+ const json = JSON.stringify(Object.fromEntries(await readShotViewDir(dir)));
62
+ return Bun.gzipSync(new TextEncoder().encode(json), { level: 9 });
63
+ }
64
+
65
+ /**
66
+ * Serve the page from the archive at `archivePath` rather than building it.
67
+ * @knipignore Called by the entry scripts/build.ts writes for the release binary.
68
+ */
69
+ export function usePrebuiltShotView(archivePath: string): void {
70
+ prebuiltArchive = archivePath;
71
+ loading = null;
72
+ }
73
+
74
+ /** The page's files, read or built once per process. */
75
+ export function loadShotView(): Promise<ShotViewFiles> {
76
+ loading ??= (prebuiltArchive ? unpackShotView(prebuiltArchive) : buildShotViewFromSource()).catch((error: unknown) => {
77
+ loading = null;
78
+ throw error;
79
+ });
80
+ return loading;
81
+ }
82
+
83
+ async function unpackShotView(archivePath: string): Promise<ShotViewFiles> {
84
+ const file = Bun.file(archivePath);
85
+ if (!(await file.exists())) throw new Error(MISSING_VIEW_MESSAGE);
86
+ const json = new TextDecoder().decode(Bun.gunzipSync(await file.bytes()));
87
+ return completeShotView(new Map(Object.entries(JSON.parse(json) as Record<string, string>)));
88
+ }
89
+
90
+ async function buildShotViewFromSource(): Promise<ShotViewFiles> {
91
+ // A bundle of the CLI without its sources, and without the page embedded.
92
+ if (!existsSync(electrobunViewPath(SHOT_VIEW_ENTRY))) throw new Error(MISSING_VIEW_MESSAGE);
93
+ const dir = await mkdtemp(join(tmpdir(), "gloom-pane-shot-view-"));
94
+ try {
95
+ await buildShotView(dir, process.env.GLOOMBERB_API_URL ?? "");
96
+ return completeShotView(await readShotViewDir(dir));
97
+ } finally {
98
+ await rm(dir, { recursive: true, force: true });
99
+ }
100
+ }
101
+
102
+ /**
103
+ * The page's files, once every file it needs is there: the bundle, the
104
+ * stylesheet, and each chunk the bundle loads by path (`import("./x.chunk.js")`,
105
+ * see build-assets.ts), which a pane would otherwise only miss when it draws.
106
+ */
107
+ function completeShotView(files: Map<string, string>): ShotViewFiles {
108
+ const script = files.get(SHOT_VIEW_SCRIPT);
109
+ if (script === undefined || !files.has(SHOT_VIEW_STYLESHEET)) throw new Error(MISSING_VIEW_MESSAGE);
110
+ for (const [, chunk] of script.matchAll(/["']\.\/([\w./-]+\.chunk\.js)["']/g)) {
111
+ if (!files.has(chunk!)) throw new Error(MISSING_VIEW_MESSAGE);
112
+ }
113
+ return files;
114
+ }
@@ -1,7 +1,7 @@
1
1
  import type { TapeCapture } from "../plugins/builtin/time-sales/snapshot-client";
2
2
  import { mkdir, mkdtemp, rm, writeFile } from "fs/promises";
3
3
  import { tmpdir } from "os";
4
- import { join, resolve, sep } from "path";
4
+ import { join } from "path";
5
5
  import { encodeRpcValue } from "../renderers/electrobun/shared/rpc-codec";
6
6
  import type { AppConfig } from "../types/config";
7
7
  import type { ChartResolutionResult } from "../time-series/types";
@@ -21,10 +21,8 @@ import { readVisibleKeyValues } from "./visible-key-values";
21
21
  import { scrollTruncationReasons } from "./scroll-truncation";
22
22
  import { SHOT_API_PROXY_PREFIX, SHOT_HTTP_BRIDGE_PATH, SHOT_MARKET_BRIDGE_PATH } from "./desktop-pane-shot-routes";
23
23
  import { SESSION_COOKIE_NAMES } from "../api-client/session-cookie";
24
- import {
25
- electrobunViewPath,
26
- writeElectrobunViewPage,
27
- } from "../renderers/dom/build-assets";
24
+ import { renderElectrobunViewHtml } from "../renderers/dom/build-assets";
25
+ import { loadShotView, SHOT_VIEW_SCRIPT, SHOT_VIEW_STYLESHEET, type ShotViewFiles } from "./desktop-pane-shot-view";
28
26
  import { errorMessage } from "../utils/errors";
29
27
 
30
28
  export interface DesktopPaneShotIntradayHistory {
@@ -208,14 +206,12 @@ export async function renderDesktopPaneScreenshot(
208
206
  apiProxy: DesktopPaneShotApiProxy,
209
207
  options: { captureImage?: boolean; bridge?: DesktopPaneShotBridge; statusLine?: ShotStatusLine } = {},
210
208
  ): Promise<DesktopPaneShotRenderResult> {
209
+ const chrome = await findChromeExecutable();
210
+ const view = await loadShotView();
211
211
  const tempDir = await mkdtemp(join(tmpdir(), "gloom-pane-shot-"));
212
212
  let server: ReturnType<typeof Bun.serve> | null = null;
213
213
  try {
214
- const outdir = join(tempDir, "assets");
215
- await mkdir(outdir, { recursive: true });
216
- await buildShotPage(outdir, payload);
217
- server = serveShotPage(outdir, apiProxy, options.bridge ?? null);
218
- const chrome = await findChromeExecutable();
214
+ server = serveShotPage(view, shotPageHtml(view, payload), apiProxy, options.bridge ?? null);
219
215
  return await capturePageScreenshot({
220
216
  chrome,
221
217
  url: new URL("/", server.url).href,
@@ -234,16 +230,12 @@ export async function renderDesktopPaneScreenshot(
234
230
  }
235
231
  }
236
232
 
237
- async function buildShotPage(outdir: string, payload: DesktopPaneShotPayload): Promise<string> {
233
+ /** The page for one capture: the shared view, with this capture's pane and data in its bootstrap script. */
234
+ function shotPageHtml(view: ShotViewFiles, payload: DesktopPaneShotPayload): string {
238
235
  const payloadJson = JSON.stringify(encodeRpcValue(payload)).replace(/</g, "\\u003c");
239
- return writeElectrobunViewPage({
240
- entrypoint: electrobunViewPath("cli-pane-shot-entry.tsx"),
241
- outdir,
242
- aliasRules: [
243
- ["backend-rpc", "native-stubs/backend-rpc.ts"],
244
- ],
245
- failureMessage: "Failed to build desktop pane screenshot renderer.",
246
- missingEntryMessage: "Desktop pane screenshot build did not produce a JavaScript entrypoint.",
236
+ return renderElectrobunViewHtml({
237
+ stylesheet: view.get(SHOT_VIEW_STYLESHEET) ?? "",
238
+ entrySrc: `./${SHOT_VIEW_SCRIPT}`,
247
239
  title: "Gloomberb Pane Shot",
248
240
  loadingText: "Rendering pane...",
249
241
  bootstrapScript: `
@@ -275,11 +267,11 @@ async function buildShotPage(outdir: string, payload: DesktopPaneShotPayload): P
275
267
  }
276
268
 
277
269
  function serveShotPage(
278
- outdir: string,
270
+ view: ShotViewFiles,
271
+ html: string,
279
272
  apiProxy: DesktopPaneShotApiProxy,
280
273
  bridge: DesktopPaneShotBridge | null,
281
274
  ): ReturnType<typeof Bun.serve> {
282
- const staticRoot = resolve(outdir);
283
275
  return Bun.serve({
284
276
  hostname: "127.0.0.1",
285
277
  port: 0,
@@ -295,7 +287,7 @@ function serveShotPage(
295
287
  if (url.pathname === SHOT_HTTP_BRIDGE_PATH) {
296
288
  return runShotHttpBridge(request, bridge);
297
289
  }
298
- return serveShotAsset(url, staticRoot);
290
+ return serveShotAsset(url, view, html);
299
291
  },
300
292
  });
301
293
  }
@@ -330,21 +322,22 @@ async function runShotHttpBridge(
330
322
  }
331
323
  }
332
324
 
333
- async function serveShotAsset(url: URL, staticRoot: string): Promise<Response> {
325
+ function serveShotAsset(url: URL, view: ShotViewFiles, html: string): Response {
334
326
  let pathname: string;
335
327
  try {
336
328
  pathname = decodeURIComponent(url.pathname);
337
329
  } catch {
338
330
  return new Response("Bad request", { status: 400 });
339
331
  }
340
- const relativePath = pathname === "/" ? "index.html" : pathname.replace(/^\/+/, "");
341
- const filePath = resolve(staticRoot, relativePath);
342
- if (filePath !== staticRoot && !filePath.startsWith(`${staticRoot}${sep}`)) {
343
- return new Response("Not found", { status: 404 });
332
+ if (pathname === "/" || pathname === "/index.html") {
333
+ return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } });
344
334
  }
345
- const file = Bun.file(filePath);
346
- if (!(await file.exists())) return new Response("Not found", { status: 404 });
347
- return new Response(file);
335
+ const path = pathname.replace(/^\/+/, "");
336
+ const body = view.get(path);
337
+ if (body === undefined) return new Response("Not found", { status: 404 });
338
+ // A module script is only run when served as JavaScript.
339
+ const type = path.endsWith(".js") ? "text/javascript" : path.endsWith(".css") ? "text/css" : "text/plain";
340
+ return new Response(body, { headers: { "Content-Type": `${type}; charset=utf-8` } });
348
341
  }
349
342
 
350
343
  async function proxyShotApiRequest(
@@ -427,15 +420,21 @@ async function findChromeExecutable(): Promise<string> {
427
420
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
428
421
  "/Applications/Chromium.app/Contents/MacOS/Chromium",
429
422
  Bun.which("google-chrome"),
423
+ Bun.which("google-chrome-stable"),
430
424
  Bun.which("chromium"),
431
425
  Bun.which("chromium-browser"),
426
+ // Windows installs Chrome outside PATH, per machine or per user.
427
+ ...["PROGRAMFILES", "PROGRAMFILES(X86)", "LOCALAPPDATA"].map((name) => {
428
+ const dir = process.env[name];
429
+ return dir ? join(dir, "Google", "Chrome", "Application", "chrome.exe") : null;
430
+ }),
432
431
  ].filter((value): value is string => !!value);
433
432
 
434
433
  for (const candidate of candidates) {
435
434
  const file = Bun.file(candidate);
436
435
  if (await file.exists()) return candidate;
437
436
  }
438
- throw new Error("Could not find Chrome or Chromium to render the desktop pane screenshot.");
437
+ throw new Error("Drawing a pane needs Google Chrome or Chromium, and neither was found. Install one, or set CHROME_PATH to its executable.");
439
438
  }
440
439
 
441
440
  async function capturePageScreenshot({
package/src/cli/entry.ts CHANGED
@@ -11,9 +11,11 @@ import {
11
11
  OPEN_TUI_NATIVE_SMOKE_COMMAND,
12
12
  OPEN_TUI_RUNTIME_SMOKE_COMMAND,
13
13
  PLUGIN_HOST_SMOKE_COMMAND,
14
+ SHOT_VIEW_SMOKE_COMMAND,
14
15
  smokeOpenTuiNative,
15
16
  smokeOpenTuiRuntime,
16
17
  smokePluginHost,
18
+ smokeShotView,
17
19
  } from "./native-smoke";
18
20
 
19
21
  async function launchOpenTuiApp(cliLaunchRequest: CliLaunchRequest | null = null): Promise<void> {
@@ -51,6 +53,11 @@ export async function runCliEntrypoint(rawArgs = process.argv.slice(2)): Promise
51
53
  process.exit(0);
52
54
  }
53
55
 
56
+ if (command === SHOT_VIEW_SMOKE_COMMAND) {
57
+ await smokeShotView();
58
+ process.exit(0);
59
+ }
60
+
54
61
  if (!command || ((command === "launch-ui" || command === "ui") && !rawArgs.some(isCliHelpFlag))) {
55
62
  await launchOpenTuiApp();
56
63
  return;
@@ -2,6 +2,8 @@
2
2
  * Where exported data comes from, said the same way by every command that
3
3
  * writes CSV or JSON, so an export can go in an audit file as it is: CSV ends
4
4
  * with `#` lines and JSON carries a `source` object with the same fields.
5
+ * CSV and NDJSON rows also end with five fixed columns naming what each row
6
+ * is (`EXPORT_IDENTITY_COLUMNS`).
5
7
  */
6
8
  import { formatFreshnessLine, type ReportFreshness } from "./pane-functions/freshness";
7
9
 
@@ -38,6 +40,47 @@ export function exportSource(freshness: ReportFreshness): ExportSource {
38
40
  };
39
41
  }
40
42
 
43
+ /**
44
+ * The columns every row of an export that ends with these lines carries,
45
+ * last and in this order, so a row says what it is after `head`, a filter or
46
+ * concatenating files: what was asked for, the unit of its values, the bar
47
+ * interval or observation frequency served, and the source and as-of date the
48
+ * closing lines give.
49
+ */
50
+ export const EXPORT_IDENTITY_COLUMNS = ["symbol_or_series", "currency_or_unit", "interval", "source", "as_of"] as const;
51
+
52
+ /** What a command knows about the rows it exports; source and as-of come from its freshness. */
53
+ export interface ExportIdentity {
54
+ /** The symbol or series ID asked for: `USDZAR=X`, `SAN:EPA`, `DEXSFUS`. */
55
+ symbolOrSeries?: string | null;
56
+ /** The currency or unit the values are in: `ZAR`, `USD`, `bp`, `pct`, `index points`. */
57
+ currencyOrUnit?: string | null;
58
+ /** The bar interval or observation frequency actually returned: `1d`, `1mo`, `daily`, `monthly`. */
59
+ interval?: string | null;
60
+ }
61
+
62
+ /**
63
+ * The five cells of `EXPORT_IDENTITY_COLUMNS` for a row: blank where the
64
+ * command cannot say, and `as_of` the observation date of the closing lines,
65
+ * blank when they read `not published`.
66
+ */
67
+ export function exportIdentityCells(freshness: ReportFreshness, identity: ExportIdentity = {}): string[] {
68
+ const source = exportSource(freshness);
69
+ return [
70
+ identity.symbolOrSeries?.trim() ?? "",
71
+ identity.currencyOrUnit?.trim() ?? "",
72
+ identity.interval?.trim() ?? "",
73
+ source.name,
74
+ source.observationDate ?? "",
75
+ ];
76
+ }
77
+
78
+ /** The same five as fields of an NDJSON row, null where blank. */
79
+ export function exportIdentityFields(freshness: ReportFreshness, identity: ExportIdentity = {}): Record<string, string | null> {
80
+ const cells = exportIdentityCells(freshness, identity);
81
+ return Object.fromEntries(EXPORT_IDENTITY_COLUMNS.map((column, index) => [column, cells[index] || null]));
82
+ }
83
+
41
84
  /**
42
85
  * The lines an export ends with: the source line the text report ends with,
43
86
  * then the publisher, observation date (the newest; the oldest follows when
@@ -60,9 +60,9 @@ function latestReportedPeriod(financials: Pick<TickerFinancials, "annualStatemen
60
60
  }
61
61
 
62
62
  /**
63
- * Fundamentals and valuation: reported figures, not a feed. Dated by when the
64
- * statistics block was observed, with the newest statement period they run
65
- * through named in the status; stale only when the block says so.
63
+ * Fundamentals: reported figures, not a feed. Dated by when the statistics
64
+ * block was observed, with the newest statement period they run through
65
+ * named in the status; stale only when the block says so.
66
66
  */
67
67
  export function fundamentalsFreshness(financials: TickerFinancials, now = Date.now()): ReportFreshness {
68
68
  const fundamentals = financials.fundamentals;
@@ -73,3 +73,18 @@ export function fundamentalsFreshness(financials: TickerFinancials, now = Date.n
73
73
  asOf: fundamentals?.fetchedAt ?? null,
74
74
  }, { stale: fundamentals?.stale === true }, now)!;
75
75
  }
76
+
77
+ /**
78
+ * Valuation: market cap, multiples and yields measured at the quote's regular
79
+ * session price, beside the reported figures they divide. Dated by the quote,
80
+ * as `quote` dates it (after the close, its trading day's close), with the
81
+ * statement period the reported figures run through after the status. Stale
82
+ * fundamentals were measured at some older price, so they date it as
83
+ * `fundamentals` does; so does a missing quote.
84
+ */
85
+ export function valuationFreshness(financials: TickerFinancials, now = Date.now()): ReportFreshness {
86
+ const quote = financials.quote;
87
+ if (!quote || financials.fundamentals?.stale === true) return fundamentalsFreshness(financials, now);
88
+ const period = latestReportedPeriod(financials);
89
+ return rowsFreshness([quoteObservation(quote, now)], period ? { note: `reported through ${period}` } : undefined, undefined, now)!;
90
+ }
@@ -336,6 +336,23 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
336
336
  shown: ["Drift"],
337
337
  definition: "The plain difference between two percentages. A 22% weight against a 20% target is 2pp over it, not 10%.",
338
338
  },
339
+ // Financial statements and ratios (FA).
340
+ {
341
+ term: "EBITDA", aliases: ["earnings before interest taxes depreciation and amortization", "ebitda ttm"], functions: ["FA"],
342
+ definition: "Earnings before interest, taxes, depreciation and amortization: operating income with depreciation and amortization "
343
+ + "added back, a rough measure of the cash a business earns before paying for its capital and its debt.",
344
+ },
345
+ {
346
+ term: "Net debt", aliases: ["net debt/ebitda", "net debt to ebitda", "net leverage"], functions: ["FA"], shown: ["Net Debt"],
347
+ definition: "Total debt less cash and short-term investments. Negative means more cash than debt. "
348
+ + "Net debt / EBITDA is how many years of EBITDA would repay it; FA divides by a year of EBITDA, the trailing four quarters in a quarterly column.",
349
+ },
350
+ {
351
+ term: "TTM", aliases: ["trailing twelve months", "trailing 12 months", "trailing four quarters", "ltm", "last twelve months"],
352
+ functions: ["FA"],
353
+ definition: "Trailing twelve months: the four latest quarters added up, a year that ends with the latest quarter rather than the fiscal year. "
354
+ + "FA's quarterly leverage ratios read their flows over it, and show < 4 quarters when fewer are on file.",
355
+ },
339
356
  // Funds (ETF). Named in what ETF says it cannot show yet.
340
357
  {
341
358
  term: "NAV", aliases: ["net asset value", "nav per share", "net asset value per share"], functions: ["ETF"],