@gloom-sh/terminal 0.19.2 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (472) hide show
  1. package/README.md +7 -1
  2. package/package.json +1 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +20 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app.tsx +23 -1
  20. package/src/architecture/core-builtin-allowlist.json +2 -1
  21. package/src/capabilities/factories.ts +1 -0
  22. package/src/cli/ascii-output.ts +42 -0
  23. package/src/cli/base-converter.ts +10 -2
  24. package/src/cli/command-options.ts +16 -1
  25. package/src/cli/commands/crypto-hints.ts +23 -4
  26. package/src/cli/commands/market.ts +92 -23
  27. package/src/cli/commands/news-search.ts +24 -5
  28. package/src/cli/commands/overview.ts +9 -4
  29. package/src/cli/commands/search.ts +71 -12
  30. package/src/cli/commands/system.ts +22 -30
  31. package/src/cli/commands/ticker.ts +49 -9
  32. package/src/cli/commands/version.ts +33 -0
  33. package/src/cli/context.ts +5 -9
  34. package/src/cli/data-dir.ts +7 -0
  35. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  36. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  37. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  38. package/src/cli/desktop-pane-shot.ts +196 -256
  39. package/src/cli/early-command.ts +42 -0
  40. package/src/cli/earnings-calendar.ts +9 -0
  41. package/src/cli/entry.ts +19 -13
  42. package/src/cli/errors.ts +12 -2
  43. package/src/cli/help.ts +1 -1
  44. package/src/cli/history-export.ts +22 -2
  45. package/src/cli/history-rows.ts +154 -4
  46. package/src/cli/index.ts +18 -1
  47. package/src/cli/known-gaps.ts +40 -5
  48. package/src/cli/listing-arg.ts +46 -9
  49. package/src/cli/options.ts +1 -1
  50. package/src/cli/output-file.ts +60 -13
  51. package/src/cli/pane-functions/catalog.ts +14 -3
  52. package/src/cli/pane-functions/data.ts +1 -1
  53. package/src/cli/pane-functions/dom.ts +45 -0
  54. package/src/cli/pane-functions/freshness.ts +46 -9
  55. package/src/cli/pane-functions/function-help.ts +17 -9
  56. package/src/cli/pane-functions/headless.ts +22 -14
  57. package/src/cli/pane-functions/index.ts +6 -2
  58. package/src/cli/pane-functions/options.ts +6 -2
  59. package/src/cli/pane-functions/resolver.ts +27 -5
  60. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  61. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  62. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  63. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  64. package/src/cli/quote-answer.ts +20 -0
  65. package/src/cli/registry.ts +41 -13
  66. package/src/cli/result.ts +12 -6
  67. package/src/cli/ticker-resolution.ts +2 -1
  68. package/src/cli/types.ts +2 -0
  69. package/src/components/chart/composite/format.ts +3 -0
  70. package/src/components/chart-table/header.tsx +1 -1
  71. package/src/components/chart-table/layout.ts +28 -11
  72. package/src/components/command-bar/action-runtime.ts +4 -0
  73. package/src/components/command-bar/commands/direct/index.ts +18 -33
  74. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  75. package/src/components/command-bar/commands/registry.ts +10 -1
  76. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  77. package/src/components/command-bar/routes/root/runtime.ts +24 -1
  78. package/src/components/command-bar/surface/index.tsx +9 -3
  79. package/src/components/company-logo.tsx +2 -1
  80. package/src/components/layout/header.tsx +8 -3
  81. package/src/components/layout/market-summary.ts +25 -7
  82. package/src/components/layout/pane/footer/index.tsx +7 -2
  83. package/src/components/layout/pane/footer/model.ts +40 -5
  84. package/src/components/market-board/index.tsx +3 -2
  85. package/src/components/onboarding/desks-step.tsx +12 -4
  86. package/src/components/onboarding/first-run-workspace.ts +81 -4
  87. package/src/components/onboarding/onboarding-frame.tsx +2 -1
  88. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  89. package/src/components/onboarding/onboarding-wizard.tsx +54 -23
  90. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  91. package/src/components/onboarding/run-setup-again.ts +24 -0
  92. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  93. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  94. package/src/components/onboarding/wizard-model.ts +57 -3
  95. package/src/components/onboarding/wizard-positions.ts +1 -1
  96. package/src/components/ui/delayed-mark.tsx +56 -0
  97. package/src/components/ui/external-link.tsx +3 -6
  98. package/src/components/ui/icon.tsx +8 -1
  99. package/src/components/ui/logo-mark.tsx +8 -5
  100. package/src/components/ui/stat-grid.tsx +263 -19
  101. package/src/data/config/store/node.ts +11 -5
  102. package/src/data/config/store/normalize.ts +16 -3
  103. package/src/data/profile-permissions.ts +47 -0
  104. package/src/data/resource-store.ts +34 -31
  105. package/src/data/sqlite/access.ts +19 -8
  106. package/src/data/sqlite/checkpointer.ts +95 -8
  107. package/src/data/sqlite/database.ts +40 -7
  108. package/src/docs/snapshot.json +191 -78
  109. package/src/i18n/es.ts +18 -2
  110. package/src/i18n/ja.ts +18 -2
  111. package/src/i18n/ko.ts +18 -2
  112. package/src/i18n/zh-cn.ts +18 -2
  113. package/src/i18n/zh-tw.ts +18 -2
  114. package/src/layout/desk-keys.ts +4 -1
  115. package/src/layout/desks.ts +38 -5
  116. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  117. package/src/market-data/batched-reads.ts +16 -0
  118. package/src/market-data/coordinator/auxiliary.ts +4 -2
  119. package/src/market-data/coordinator/index.ts +11 -2
  120. package/src/market-data/futures-board.ts +1 -1
  121. package/src/market-data/futures-contract.ts +3 -1
  122. package/src/market-data/market/index-level.ts +6 -4
  123. package/src/market-data/published-apac-sessions.ts +6 -0
  124. package/src/news/aggregator.ts +7 -4
  125. package/src/news/news-model.ts +11 -0
  126. package/src/news/story-id.ts +22 -0
  127. package/src/news/ticker-symbols.ts +17 -1
  128. package/src/notifications/app-notifier.ts +35 -14
  129. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  130. package/src/plugins/builtin/account-management/index.tsx +2 -3
  131. package/src/plugins/builtin/account-management/model.ts +1 -1
  132. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  133. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  134. package/src/plugins/builtin/alerts/events.ts +10 -2
  135. package/src/plugins/builtin/alerts/index.tsx +5 -5
  136. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  137. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  138. package/src/plugins/builtin/alerts/targets.ts +4 -1
  139. package/src/plugins/builtin/attention/headless.ts +1 -1
  140. package/src/plugins/builtin/attention/index.tsx +2 -4
  141. package/src/plugins/builtin/attention/listing.ts +1 -1
  142. package/src/plugins/builtin/attention/series.ts +1 -1
  143. package/src/plugins/builtin/awards/index.tsx +1 -2
  144. package/src/plugins/builtin/backtest/client.ts +1 -0
  145. package/src/plugins/builtin/backtest/index.tsx +1 -2
  146. package/src/plugins/builtin/backtest/rules.ts +3 -1
  147. package/src/plugins/builtin/backtest/window.ts +8 -7
  148. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  149. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  150. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  151. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  152. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  153. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  154. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  155. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  156. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  157. package/src/plugins/builtin/buildout/format.ts +9 -0
  158. package/src/plugins/builtin/buildout/index.tsx +51 -5
  159. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  160. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  161. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  162. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  163. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  164. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  165. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  166. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  167. package/src/plugins/builtin/cds/headless.ts +10 -2
  168. package/src/plugins/builtin/cds/model.ts +7 -3
  169. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  170. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  171. package/src/plugins/builtin/changelog/index.tsx +3 -2
  172. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -2
  173. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  174. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  175. package/src/plugins/builtin/chart-composer/pane.tsx +26 -2
  176. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  177. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  178. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  179. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  180. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  181. package/src/plugins/builtin/chat/index.tsx +5 -5
  182. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  183. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  184. package/src/plugins/builtin/cloud/index.tsx +1 -2
  185. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  186. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  187. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  188. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  189. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  190. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  191. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  192. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  193. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  194. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  195. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  196. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  197. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  198. package/src/plugins/builtin/connections/index.ts +2 -3
  199. package/src/plugins/builtin/cot/index.tsx +1 -2
  200. package/src/plugins/builtin/cpi/index.tsx +1 -2
  201. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  202. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  203. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  204. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  205. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  206. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  207. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  208. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  209. package/src/plugins/builtin/crypto-board/series.ts +2 -0
  210. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  211. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  212. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  213. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  214. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  215. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  216. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  217. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  218. package/src/plugins/builtin/debug/index.tsx +1 -1
  219. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  220. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  221. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  222. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  223. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  224. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  225. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  226. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  227. package/src/plugins/builtin/docs/index.tsx +2 -3
  228. package/src/plugins/builtin/doe/headless.ts +16 -3
  229. package/src/plugins/builtin/doe/index.tsx +5 -4
  230. package/src/plugins/builtin/earnings/board.tsx +2 -1
  231. package/src/plugins/builtin/earnings/history.tsx +2 -1
  232. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  233. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  234. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  235. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  236. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  237. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  238. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  239. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  240. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  241. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  242. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  243. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  244. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  245. package/src/plugins/builtin/exposure/index.tsx +1 -2
  246. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  247. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  248. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  249. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  250. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  251. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  252. package/src/plugins/builtin/futures/index.tsx +15 -0
  253. package/src/plugins/builtin/futures/model.ts +14 -0
  254. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  255. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  256. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  257. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  258. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  259. package/src/plugins/builtin/gpu/board.tsx +51 -12
  260. package/src/plugins/builtin/gpu/client.ts +15 -2
  261. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  262. package/src/plugins/builtin/gpu/headless.ts +14 -2
  263. package/src/plugins/builtin/gpu/index.tsx +1 -2
  264. package/src/plugins/builtin/gpu/model.ts +20 -2
  265. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  266. package/src/plugins/builtin/help/index.tsx +1 -1
  267. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  268. package/src/plugins/builtin/holders/index.tsx +2 -3
  269. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  270. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  271. package/src/plugins/builtin/jodi/index.tsx +1 -2
  272. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  273. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  274. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  275. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  276. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  277. package/src/plugins/builtin/market-movers/index.tsx +7 -1
  278. package/src/plugins/builtin/market-movers/session-body.tsx +4 -1
  279. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  280. package/src/plugins/builtin/members/index.tsx +1 -2
  281. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  282. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  283. package/src/plugins/builtin/news/headless.ts +7 -0
  284. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  285. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  286. package/src/plugins/builtin/notes/index.tsx +3 -6
  287. package/src/plugins/builtin/openfda/index.tsx +1 -2
  288. package/src/plugins/builtin/options/view.tsx +6 -5
  289. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  290. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  291. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  292. package/src/plugins/builtin/perps/index.tsx +1 -2
  293. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  294. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  295. package/src/plugins/builtin/plugin-module.ts +16 -2
  296. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  297. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  298. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  299. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  300. package/src/plugins/builtin/power/index.tsx +1 -2
  301. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  302. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  303. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  304. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  305. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  306. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  307. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  308. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  309. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  310. package/src/plugins/builtin/research/index.tsx +5 -8
  311. package/src/plugins/builtin/research-search/index.tsx +1 -2
  312. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  313. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  314. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  315. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  316. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  317. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  318. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  319. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  320. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  321. package/src/plugins/builtin/sec/headless.ts +5 -1
  322. package/src/plugins/builtin/sec/index.tsx +4 -2
  323. package/src/plugins/builtin/sec/model.ts +2 -0
  324. package/src/plugins/builtin/sectors/index.tsx +11 -5
  325. package/src/plugins/builtin/series-inventory.ts +2 -0
  326. package/src/plugins/builtin/shared/cloud-resource.ts +60 -0
  327. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  328. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  329. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  330. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  331. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  332. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  333. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  334. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  335. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  336. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  337. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  338. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  339. package/src/plugins/builtin/thirteenf/api.ts +116 -13
  340. package/src/plugins/builtin/thirteenf/data.ts +132 -30
  341. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  342. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  343. package/src/plugins/builtin/thirteenf/pane.tsx +20 -11
  344. package/src/plugins/builtin/thirteenf/types.ts +12 -0
  345. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +17 -4
  346. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  347. package/src/plugins/builtin/ticker-detail/headless.ts +11 -3
  348. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  349. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  350. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  351. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  352. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  353. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  354. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  355. package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  357. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  358. package/src/plugins/builtin/volatility/index.tsx +1 -2
  359. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  360. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  361. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  362. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  363. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  364. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  365. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  366. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  367. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  368. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  369. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  370. package/src/plugins/bundle.ts +52 -2
  371. package/src/plugins/catalog-browser.ts +2 -2
  372. package/src/plugins/catalog-cli.ts +20 -0
  373. package/src/plugins/host-link.ts +13 -1
  374. package/src/plugins/installer.ts +15 -4
  375. package/src/plugins/loader.ts +43 -11
  376. package/src/plugins/peer-imports.ts +201 -0
  377. package/src/plugins/peer-retry.ts +26 -29
  378. package/src/plugins/plugin-process.ts +128 -0
  379. package/src/plugins/redirection-guard.ts +45 -0
  380. package/src/remote/endpoint.ts +16 -3
  381. package/src/renderers/browser/main.tsx +3 -2
  382. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  383. package/src/renderers/dom/desktop/icons.tsx +2 -0
  384. package/src/renderers/dom/host/image-surface.tsx +46 -21
  385. package/src/renderers/dom/styles.css +7 -0
  386. package/src/renderers/dom/toast-host.tsx +2 -0
  387. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  388. package/src/renderers/electrobun/bun/desktop/initialization.ts +3 -2
  389. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  390. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  391. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  392. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  393. package/src/renderers/electrobun/bun/window/create.ts +4 -2
  394. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  395. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  396. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  397. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  398. package/src/renderers/electrobun/view/main.tsx +7 -2
  399. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  400. package/src/renderers/opentui/browser-open.ts +10 -4
  401. package/src/renderers/opentui/image/surface.tsx +1 -1
  402. package/src/renderers/opentui/start.tsx +12 -6
  403. package/src/renderers/opentui/toast-host.tsx +2 -1
  404. package/src/series/registry.ts +18 -0
  405. package/src/series/standalone.ts +10 -1
  406. package/src/series/types.ts +7 -0
  407. package/src/series/validate.ts +11 -2
  408. package/src/sources/gloomberb-cloud/index.ts +28 -5
  409. package/src/sources/gloomberb-cloud/news.ts +24 -3
  410. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  411. package/src/sources/provider-errors.ts +26 -3
  412. package/src/sources/provider-router/history.ts +5 -0
  413. package/src/sources/provider-router/index.ts +13 -0
  414. package/src/sync/core-contributors.ts +32 -0
  415. package/src/telemetry/attention-counts.ts +64 -6
  416. package/src/telemetry/attention-setting.ts +33 -0
  417. package/src/telemetry/crash-reports-node.ts +3 -1
  418. package/src/telemetry/stall-clocks.ts +93 -0
  419. package/src/telemetry/stall-reports-node.ts +12 -7
  420. package/src/telemetry/stall-reports.ts +34 -5
  421. package/src/test-support/preload.ts +9 -0
  422. package/src/tickers/company-name.ts +19 -0
  423. package/src/tickers/listing-fate.ts +14 -3
  424. package/src/tickers/saved-instrument.ts +17 -0
  425. package/src/tickers/search/home-listing.ts +13 -2
  426. package/src/tickers/search/index.ts +4 -1
  427. package/src/tickers/search/types.ts +6 -1
  428. package/src/time-series/bar-aggregation.ts +7 -1
  429. package/src/time-series/base-series.ts +3 -3
  430. package/src/time-series/chart-bounds.ts +1 -1
  431. package/src/time-series/chart-data.ts +5 -2
  432. package/src/time-series/chart-seed.ts +1 -1
  433. package/src/time-series/coin-session-close.ts +211 -0
  434. package/src/time-series/live-bars.ts +13 -2
  435. package/src/time-series/live-quotes.ts +1 -1
  436. package/src/time-series/official-close.ts +159 -0
  437. package/src/time-series/price-comparison.ts +55 -6
  438. package/src/time-series/resolve.ts +12 -3
  439. package/src/time-series/series-presentation.ts +1 -1
  440. package/src/time-series/studies/formula.ts +205 -0
  441. package/src/time-series/studies/indicators.ts +299 -0
  442. package/src/time-series/studies/leverage.ts +75 -0
  443. package/src/time-series/studies/pair.ts +138 -0
  444. package/src/time-series/studies/resolve.ts +333 -0
  445. package/src/time-series/studies/shared.ts +141 -0
  446. package/src/time-series/studies/units.ts +243 -0
  447. package/src/time-series/studies/vwap.ts +274 -0
  448. package/src/types/config.ts +18 -2
  449. package/src/types/data-provider.ts +23 -0
  450. package/src/types/financials.ts +22 -0
  451. package/src/types/headless.ts +42 -3
  452. package/src/types/plugin.ts +32 -3
  453. package/src/ui/host.tsx +11 -0
  454. package/src/ui/toast.tsx +26 -1
  455. package/src/utils/ascii-text.ts +140 -0
  456. package/src/utils/cli-output.ts +91 -18
  457. package/src/utils/concurrency.ts +24 -0
  458. package/src/utils/exchanges.ts +6 -0
  459. package/src/utils/external-url.ts +20 -7
  460. package/src/utils/futures-generic.ts +4 -4
  461. package/src/utils/http-proxy-upstream.ts +124 -0
  462. package/src/utils/main-thread-stall.ts +75 -3
  463. package/src/utils/perf-marks.ts +36 -2
  464. package/src/utils/private-files.ts +141 -0
  465. package/src/utils/public-address.ts +99 -0
  466. package/src/account/upgrade-personalization.ts +0 -57
  467. package/src/account/upgrade-tickers.ts +0 -124
  468. package/src/api-client/upgrade-experiment.ts +0 -91
  469. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  470. package/src/renderers/electrobun/launcher/preload.ts +0 -13
  471. package/src/renderers/electrobun/launcher/windows-emulation.ts +0 -50
  472. package/src/time-series/studies.ts +0 -1628
@@ -0,0 +1,73 @@
1
+ import { ApiRequestError } from "../../../api-client/errors";
2
+ import type { FundBondFacts } from "../../../api-client/fund-facts";
3
+ import { createConcurrencyLimiter } from "../../../utils/concurrency";
4
+ import type { DividendData } from "./client";
5
+ import type { DividendRankingEntry } from "./ranking";
6
+
7
+ /** Tickers one ranking reads at most: `fn DVD`, `shot DVD` and the DVD pane alike. */
8
+ export const MAX_RANKED_SYMBOLS = 25;
9
+ /** Tickers read at once across every ranking in the process, so a ranking never floods Gloom Cloud. */
10
+ const RANKING_CONCURRENCY = 4;
11
+ /** A rate-limited read waits what the server asks, a second when it names nothing, and never longer than this. */
12
+ const RATE_LIMIT_WAIT_MS = 1_000;
13
+ const RATE_LIMIT_WAIT_MAX_MS = 5_000;
14
+
15
+ /** The one gate every ranking's reads pass through. */
16
+ const rankingReads = createConcurrencyLimiter(RANKING_CONCURRENCY);
17
+
18
+ /** How a ranking reads one ticker. */
19
+ export interface DividendRankingReads {
20
+ loadData(symbol: string): Promise<DividendData>;
21
+ /** A fund's bond facts, null for any other listing. It never rejects: without them the fund ranks as before. */
22
+ loadBond?(symbol: string): Promise<FundBondFacts | null>;
23
+ }
24
+
25
+ interface RankingReadOptions {
26
+ /** How the one retry of a rate-limited read waits; tests pass their own. */
27
+ sleep?: (ms: number) => Promise<void>;
28
+ }
29
+
30
+ const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
31
+
32
+ /** The 429 a read was refused with, wrapped or not; null for any other failure. */
33
+ function rateLimited(error: unknown): ApiRequestError | null {
34
+ for (let current: unknown = error, depth = 0; current && typeof current === "object" && depth < 4; depth += 1) {
35
+ if (current instanceof ApiRequestError) return current.status === 429 ? current : null;
36
+ current = (current as { cause?: unknown }).cause;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /** A read Gloom Cloud refused with 429, asked once more after the wait it names; a second refusal is the row's error. */
42
+ async function retryRateLimitedOnce<T>(read: () => Promise<T>, sleep: (ms: number) => Promise<void>): Promise<T> {
43
+ try {
44
+ return await read();
45
+ } catch (error) {
46
+ const limited = rateLimited(error);
47
+ if (!limited) throw error;
48
+ await sleep(Math.min(Math.max(0, limited.retryAfterMs ?? RATE_LIMIT_WAIT_MS), RATE_LIMIT_WAIT_MAX_MS));
49
+ return read();
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Each ticker of a ranking, read into its entry in the order given: the first
55
+ * 25, at most four at once through the gate every ranking shares, so `fn`,
56
+ * `shot` and any open DVD pane together never ask Gloom Cloud for more. A
57
+ * ticker that fails keeps its row with its error, after one more try when it
58
+ * was rate limited.
59
+ */
60
+ export function readDividendRanking(
61
+ symbols: readonly string[],
62
+ reads: DividendRankingReads,
63
+ { sleep = wait }: RankingReadOptions = {},
64
+ ): Promise<DividendRankingEntry[]> {
65
+ return Promise.all(symbols.slice(0, MAX_RANKED_SYMBOLS).map((symbol) => rankingReads(async (): Promise<DividendRankingEntry> => {
66
+ const bond = reads.loadBond?.(symbol) ?? null;
67
+ try {
68
+ return { symbol, data: await retryRateLimitedOnce(() => reads.loadData(symbol), sleep), bond: await bond };
69
+ } catch (error) {
70
+ return { symbol, error: error instanceof Error ? error.message : String(error) };
71
+ }
72
+ })));
73
+ }
@@ -0,0 +1,163 @@
1
+ import { useCallback, useMemo, useRef } from "react";
2
+ import { apiClient } from "../../../api-client";
3
+ import {
4
+ DataTableView,
5
+ PaneStatusBody,
6
+ usePaneFooter,
7
+ type DataTableCell,
8
+ type DataTableColumn,
9
+ } from "../../../components";
10
+ import { loadingErrorFooterInfo, usePaneRefreshKey } from "../../../components/data-table/table-pane";
11
+ import { useAsyncResource } from "../../../react/async-resource";
12
+ import { useAssetData, usePluginAppActions, usePluginPaneState } from "../../../public/react";
13
+ import { useAppSelector } from "../../../state/app/context";
14
+ import { resolveSavedInstrument } from "../../../tickers/saved-instrument";
15
+ import { colors } from "../../../theme/colors";
16
+ import type { HeadlessPaneColumn } from "../../../types/headless";
17
+ import { TextAttributes } from "../../../ui";
18
+ import { compareSortValues, nextHeaderSort, type SortComparableValue, type SortPreference } from "../../../utils/sort-values";
19
+ import { SignInWall } from "../cloud/auth-actions";
20
+ import { isCloudSessionRequired, useResearchCloudSession } from "../shared/research-cloud-session";
21
+ import { fetchDividendData } from "./client";
22
+ import { dividendRankingReads, type DividendReadContext } from "./headless";
23
+ import { projectDividendRanking, type DividendRankingEntry } from "./ranking";
24
+ import { readDividendRanking, type DividendRankingReads } from "./ranking-reads";
25
+
26
+ /** The pane setting a ranking of several tickers is opened with (`shot DVD KO JNJ`). */
27
+ export const RANK_SYMBOLS_SETTING = "rankSymbols";
28
+
29
+ /** The tickers a pane ranks, from its settings: two or more, or none for a pane of one ticker. */
30
+ export function rankSymbolsOf(settings: Readonly<Record<string, unknown>> | undefined): string[] {
31
+ const value = settings?.[RANK_SYMBOLS_SETTING];
32
+ const symbols = Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string" && entry.trim() !== "") : [];
33
+ return symbols.length > 1 ? symbols : [];
34
+ }
35
+
36
+ /** How wide each of the ranking's columns starts, in characters; a column not named fits its header. */
37
+ const COLUMN_WIDTHS: Record<string, number> = { symbol: 10, lastExDate: 10, payDate: 10, note: 24, yieldBasis: 24 };
38
+ /** Growth figures read by their sign, as the single-ticker pane colors them. */
39
+ const SIGNED_COLUMNS = new Set(["growth1Y", "growth3Y"]);
40
+ const DATE_COLUMNS = new Set(["lastExDate", "payDate", "secYieldAsOf"]);
41
+
42
+ type RankingRow = Record<string, unknown> & { symbol: string };
43
+ type RankingColumn = DataTableColumn & { source: HeadlessPaneColumn };
44
+ /**
45
+ * The order `fn DVD` ranks in: trailing yield, highest first. A stable sort
46
+ * of the ranked rows keeps a ticker not read last among those with no yield.
47
+ */
48
+ const RANKED_ORDER: SortPreference<string> = { columnId: "trailingYield", direction: "desc" };
49
+
50
+ function sortRankingRows(rows: RankingRow[], sort: SortPreference<string>): RankingRow[] {
51
+ const { columnId, direction } = sort;
52
+ if (!columnId) return rows;
53
+ return [...rows].sort((left, right) => compareSortValues(left[columnId] as SortComparableValue, right[columnId] as SortComparableValue, direction));
54
+ }
55
+
56
+ /** Signed out, every read is refused for the account: the pane shows the wall the single-ticker DVD shows. */
57
+ function needsSignIn(entries: readonly DividendRankingEntry[] | null): boolean {
58
+ return !!entries?.length && entries.every((entry) => !("data" in entry))
59
+ && entries.some((entry) => "error" in entry && isCloudSessionRequired(entry.error));
60
+ }
61
+
62
+ function cellText(column: HeadlessPaneColumn, row: RankingRow): string {
63
+ const value = row[column.key];
64
+ const format = column.textFormat ?? column.format;
65
+ return format ? format(value, row) : value == null ? "" : String(value);
66
+ }
67
+
68
+ /**
69
+ * Several tickers ranked by trailing yield in one table, with the columns and
70
+ * figures `fn DVD A B C` prints; Enter opens a ticker's own dividend pane.
71
+ */
72
+ export function DividendRankingView({ symbols, focused, width, height, loadData }: {
73
+ symbols: readonly string[];
74
+ focused: boolean;
75
+ width: number;
76
+ height: number;
77
+ /** One listing's history alone, without the quote `fn DVD` prices it at; for tests. */
78
+ loadData?: typeof fetchDividendData;
79
+ }) {
80
+ const key = symbols.join(",");
81
+ const marketData = useAssetData();
82
+ const cloudSession = useResearchCloudSession();
83
+ // A key reads the listing `fn DVD` reads for it: its own venue, else the one its saved ticker is on.
84
+ // Through a ref, so a quote that updates a saved ticker does not read the ranking again.
85
+ const tickers = useAppSelector((state) => state.tickers);
86
+ const tickersRef = useRef(tickers);
87
+ tickersRef.current = tickers;
88
+ const reads = useMemo<DividendRankingReads>(() => {
89
+ const resolveInstrument = (typed: string) => resolveSavedInstrument(typed, (saved) => tickersRef.current.get(saved));
90
+ // `fn DVD`'s own reads: the listing's quote for the price, the stated forward rate, a fund's bond facts.
91
+ if (marketData && !loadData) return dividendRankingReads({ marketData, apiClient, resolveInstrument } satisfies DividendReadContext);
92
+ const history = loadData ?? fetchDividendData;
93
+ return { loadData: async (typed) => {
94
+ const listing = await resolveInstrument(typed);
95
+ return history(listing.symbol, null, listing.exchange ?? "");
96
+ } };
97
+ }, [loadData, marketData]);
98
+ // A sign-in or a verified email reads the ranking again.
99
+ const request = useCallback(() => readDividendRanking(key.split(","), reads), [key, reads, cloudSession.requestKey]);
100
+ const resource = useAsyncResource(request, { keepPreviousData: true });
101
+ const authWall = needsSignIn(resource.data);
102
+ const { createPaneFromTemplate } = usePluginAppActions();
103
+ const section = useMemo(() => resource.data ? projectDividendRanking(resource.data).sections[0] ?? null : null, [resource.data]);
104
+ const [sort, setSort] = usePluginPaneState<SortPreference<string>>("dividend-ranking:sort", RANKED_ORDER);
105
+ const rows = useMemo(() => sortRankingRows((section?.rows ?? []) as RankingRow[], sort), [section, sort]);
106
+ const columns = useMemo<RankingColumn[]>(() => (section?.columns ?? []).map((column) => ({
107
+ id: column.key,
108
+ label: column.header,
109
+ width: Math.max(column.header.length, COLUMN_WIDTHS[column.key] ?? 0),
110
+ align: column.align === "right" ? "right" : "left",
111
+ source: column,
112
+ })), [section]);
113
+ const [selected, setSelected] = usePluginPaneState<string | null>("dividend-ranking:selected", null);
114
+
115
+ usePaneFooter("dividend-ranking", () => ({
116
+ info: loadingErrorFooterInfo(resource.loading && !!resource.data, resource.data ? null : resource.error),
117
+ }), [resource.loading, resource.data, resource.error]);
118
+ usePaneRefreshKey(resource.reload, { focused, enabled: !authWall });
119
+
120
+ const renderCell = useCallback((row: RankingRow, column: RankingColumn): DataTableCell => {
121
+ const text = cellText(column.source, row);
122
+ const value = row[column.id];
123
+ if (column.id === "symbol") return { text, color: colors.textBright, attributes: TextAttributes.BOLD };
124
+ if (column.id === "note") return { text, color: colors.warning };
125
+ if (DATE_COLUMNS.has(column.id) || column.id === "yieldBasis") return { text, color: colors.textDim };
126
+ if (SIGNED_COLUMNS.has(column.id) && typeof value === "number") {
127
+ return { text, color: value > 0 ? colors.positive : value < 0 ? colors.negative : colors.text };
128
+ }
129
+ return { text };
130
+ }, []);
131
+
132
+ if (authWall) return <SignInWall placement="dividend-yield-signin" action="view dividend history" needsVerification={cloudSession.needsVerification} />;
133
+
134
+ return (
135
+ <PaneStatusBody
136
+ subject="dividends"
137
+ loading={!resource.data && resource.loading}
138
+ error={!resource.data ? resource.error : null}
139
+ empty={false}
140
+ >
141
+ <DataTableView<RankingRow, RankingColumn>
142
+ focused={focused}
143
+ columns={columns}
144
+ items={rows}
145
+ rootWidth={width}
146
+ rootHeight={height}
147
+ freezeFirstColumn
148
+ sortColumnId={sort.columnId}
149
+ sortDirection={sort.direction}
150
+ // A third click on a column returns to the ranking.
151
+ onHeaderClick={(id) => setSort((current) => nextHeaderSort(current, id, {
152
+ firstDirection: id === "symbol" || id === "note" || id === "yieldBasis" ? "asc" : "desc", resetTo: RANKED_ORDER }))}
153
+ getItemKey={(row) => row.symbol}
154
+ selection={{ kind: "id", selectedId: rows.some((row) => row.symbol === selected) ? selected! : rows[0]?.symbol ?? "",
155
+ getId: (row) => row.symbol, onChange: (id) => setSelected(id) }}
156
+ onActivate={(row) => createPaneFromTemplate("dividend-yield-pane", { symbol: row.symbol })}
157
+ renderCell={renderCell}
158
+ selectedTextOverridesCellColor
159
+ emptyStateTitle="No tickers"
160
+ />
161
+ </PaneStatusBody>
162
+ );
163
+ }
@@ -146,7 +146,7 @@ const RANKING_COLUMNS: HeadlessPaneColumn[] = [
146
146
  { key: "growth3Y", header: "3Y CAGR", align: "right", format: growth("growth3Y"),
147
147
  description: "Compound annual growth of trailing twelve-month cash per share over three years; n/a when either year holds fewer payments than the cadence pays." },
148
148
  { key: "lastExDate", header: "Last ex", format: day, shrink: false },
149
- { key: "payDate", header: "Pay date", format: day, shrink: false, stackPriority: 2,
149
+ { key: "payDate", header: "Next pay", format: day, shrink: false, stackPriority: 2,
150
150
  description: "Pay date of the next declared distribution; past distributions carry ex-dates only." },
151
151
  ];
152
152
 
@@ -1,4 +1,7 @@
1
1
  import type { DividendPayment, GrowthPayments } from "./types";
2
+ import { formatCurrency } from "../../../utils/format";
3
+ import { currencyMinorDigits } from "../../../market-data/market/format";
4
+ import { resolveCurrencyUnit } from "../../../utils/currency-units";
2
5
  import type { ProjectedChartPoint } from "../../../components/chart/core/data";
3
6
  import { calendarMonthsBefore } from "../../../utils/calendar-date";
4
7
  import { NOT_MEANINGFUL, formatPriceEarnings } from "../../../utils/price-earnings";
@@ -6,6 +9,24 @@ import { LATE_GRACE_DAYS, trailingCashAt, type TrailingPayments } from "../../..
6
9
 
7
10
  const DAY = 24 * 60 * 60 * 1000;
8
11
 
12
+ /**
13
+ * A cash amount the listing pays as a conversion of the currency its company
14
+ * declares in (`DividendData.convertedFrom`), to two decimals of the unit its
15
+ * source states it in: pence stay pence (BP's 6.4059p is `6.41p`, never
16
+ * `£0.06`), and a major unit keeps its currency's decimals (`ZAR 0.87`, not
17
+ * `ZAR 0.866087`), as the conversion gives it no more.
18
+ */
19
+ export function formatConvertedAmount(value: number | null | undefined, currency: string, subUnit?: string): string {
20
+ if (value == null || !Number.isFinite(value)) return "—";
21
+ const unit = subUnit ? resolveCurrencyUnit(subUnit) : null;
22
+ return unit && unit.divisor !== 1 && unit.currency === currency
23
+ ? formatCurrency(value * unit.divisor, subUnit!, 2)
24
+ : formatCurrency(value, currency, currencyMinorDigits(currency));
25
+ }
26
+
27
+ /** What a converted amount says beside it: `converted from USD`. */
28
+ export const convertedFromText = (currency: string) => `converted from ${currency}`;
29
+
9
30
  export function formatDividendYield(value: number | null): string {
10
31
  return value != null && Number.isFinite(value) ? `${(value * 100).toFixed(2)}%` : "—";
11
32
  }
@@ -6,7 +6,6 @@ import type { PaneTemplateContext, PaneTemplateCreateOptions } from "../../../ty
6
6
  import { getSharedRegistry } from "../../registry";
7
7
  import { buildRegistryHelpIndex } from "../help/function-index";
8
8
  import type { PluginModule } from "../plugin-module";
9
- import { DocsPane } from "./pane";
10
9
  import { docsTopicForArgument, docsTopicForPane } from "./targets";
11
10
 
12
11
  /** One reader: every request lands in the same pane, on top of its history. */
@@ -38,8 +37,8 @@ export const docsModule: PluginModule = {
38
37
  id: "docs",
39
38
  name: "Docs",
40
39
  icon: "?",
41
- component: DocsPane,
42
- reportFreshness: { status: "not-a-feed", basis: "documentation" },
40
+ get component(): typeof import("./pane").DocsPane { const { DocsPane } = require("./pane"); return DocsPane; },
41
+ reportFreshness: { status: "not-a-feed", basis: "documentation", local: true },
43
42
  // A page reads whole in a report, not cut at the first screen.
44
43
  shotFitsReader: true,
45
44
  defaultPosition: "right",
@@ -49,6 +49,14 @@ function seasonalRows(row: DoeSeriesRow) {
49
49
  fiveYearLow: level(min), fiveYearAverage: level(average), fiveYearHigh: level(max) }));
50
50
  }
51
51
 
52
+ /** Words for natural gas storage, which `NGS` reads, rather than a series of the petroleum tables. */
53
+ const GAS_WORDS = new Set(["ngs", "ng", "gas", "natgas", "naturalgas", "natgasstorage", "naturalgasstorage", "gasstorage", "storage", "workinggas"]);
54
+
55
+ /** Whether `DOE` was given a word for gas storage (`NGS`, `natural gas`) rather than a series. */
56
+ export function doeGasWord(argument: string): boolean {
57
+ return GAS_WORDS.has(argument.toLowerCase().replace(/[^a-z]/g, ""));
58
+ }
59
+
52
60
  /** `DOE` reads every tab unless one is named; `NGS` starts on gas storage; a series argument adds its year. */
53
61
  export function doeHeadless(defaultTab: DoeTab | "all"): HeadlessPaneDefinition<"bundle"> {
54
62
  return {
@@ -77,10 +85,13 @@ export function doeHeadless(defaultTab: DoeTab | "all"): HeadlessPaneDefinition<
77
85
  nextExpectedAt: oldestReportTime(reports.map((report) => report.nextReleaseAt)) };
78
86
  };
79
87
  const argument = Array.isArray(args.argument) ? args.argument.join(" ") : args.argument;
80
- if (argument) {
88
+ // `DOE NGS` or `DOE natural gas` is the gas storage tables `NGS` opens on, and says so; `NGS gas` is them already.
89
+ const gasWord = argument ? doeGasWord(argument) : false;
90
+ const ngs = defaultTab === "gas";
91
+ if (argument && !gasWord) {
81
92
  const option = doeSeriesOption(argument);
82
93
  const row = option ? data.series.find((entry) => entry.id === option.value) : null;
83
- if (!row) throw new Error("Use a series such as cushing, gasoline, distillate or east");
94
+ if (!row) throw new Error(`Use a series such as cushing, gasoline, distillate or east${ngs ? "" : "; for gas storage, gloom fn NGS"}`);
84
95
  return {
85
96
  sections: [
86
97
  { title: doeHeaderLine(doeReport(data, row.tab)) ?? row.label, columns: SUMMARY_COLUMNS, rows: [reportRow(row)] },
@@ -95,8 +106,10 @@ export function doeHeadless(defaultTab: DoeTab | "all"): HeadlessPaneDefinition<
95
106
  metadata: { ...data, series: [row], complete: data.status === "available" },
96
107
  };
97
108
  }
98
- const tabs = args.options.tab === "all" || args.options.tab == null ? DOE_TABS.map(({ value }) => value) : [doeTab(args.options.tab)];
109
+ const tabs = gasWord ? ["gas" as const]
110
+ : args.options.tab === "all" || args.options.tab == null ? DOE_TABS.map(({ value }) => value) : [doeTab(args.options.tab)];
99
111
  return {
112
+ ...(gasWord && !ngs ? { warnings: [`Gas storage is its own function: gloom fn NGS (one region: gloom fn NGS east)`] } : {}),
100
113
  sections: tabs.map((tab) => ({
101
114
  title: `${DOE_TABS.find((entry) => entry.value === tab)!.label}${doeHeaderLine(doeReport(data, tab)) ? `: ${doeHeaderLine(doeReport(data, tab))}` : ""}`,
102
115
  columns: SUMMARY_COLUMNS,
@@ -4,15 +4,16 @@ import type { PluginModule } from "../plugin-module";
4
4
  import { DOE_CATALOG_SERIES } from "./series";
5
5
  import { doeBoardCache } from "./client";
6
6
  import { doePriceCache } from "./prices";
7
- import { doeHeadless } from "./headless";
7
+ import { doeGasWord, doeHeadless } from "./headless";
8
8
  import { DOE_SERIES_OPTIONS, doeSeriesOption } from "./model";
9
- import { DoePane } from "./pane";
10
9
 
11
10
  /** Opens on a tab, or on the series typed after the mnemonic and its tab. */
12
11
  function doeInstance(tab: DoeTab, options?: PaneTemplateCreateOptions): PaneTemplateInstanceConfig {
13
12
  const input = options?.arg?.trim();
13
+ // `DOE NGS` or `DOE natural gas` opens on gas storage, as `NGS` does.
14
+ if (input && doeGasWord(input)) return { placement: "floating", settings: { tab: "gas" } };
14
15
  const series = doeSeriesOption(input);
15
- if (input && !series) throw new Error("Use a series such as cushing, gasoline, distillate or east");
16
+ if (input && !series) throw new Error(`Use a series such as cushing, gasoline, distillate or east${tab === "gas" ? "" : "; for gas storage, gloom fn NGS"}`);
16
17
  return { placement: "floating", settings: { tab: series?.tab ?? tab }, ...(series ? { params: { series: series.value } } : {}) };
17
18
  }
18
19
 
@@ -23,7 +24,7 @@ const shortcut = (prefix: string, tab: DoeTab | null) => ({
23
24
 
24
25
  export const doeModule: PluginModule = {
25
26
  series: DOE_CATALOG_SERIES,
26
- panes: [{ id: "doe", name: "Oil and Gas Inventories", icon: "D", component: DoePane,
27
+ panes: [{ id: "doe", name: "Oil and Gas Inventories", icon: "D", get component(): typeof import("./pane").DoePane { const { DoePane } = require("./pane"); return DoePane; },
27
28
  defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 96, height: 34 },
28
29
  tableExport: true, headless: doeHeadless("all") }],
29
30
  paneTemplates: [{
@@ -10,6 +10,7 @@ import {
10
10
  type QueryBarFilter,
11
11
  type QueryBarView,
12
12
  } from "../../../components";
13
+ import { ageFooterSegment } from "../../../components/layout/pane/footer";
13
14
  import { handleRefreshKey, usePaneRefreshKey } from "../../../components/data-table/table-pane";
14
15
  import { openFormModal } from "../../../components/form-modal";
15
16
  import { FILL_REST } from "../../../components/ui/fill";
@@ -305,7 +306,7 @@ export function EarningsBoard({ focused, width, height, scopedSymbols, highlight
305
306
  loading: resource.loading,
306
307
  error: resource.error ?? data?.refreshError ?? null,
307
308
  stale: !!data?.stale,
308
- info: data && updatedAgo ? [{ id: "updated", parts: [{ text: updatedAgo, tone: "muted" as const }] }] : [],
309
+ info: data && updatedAgo ? [ageFooterSegment(updatedAgo)] : [],
309
310
  });
310
311
  const symbol = open?.symbol ?? selectedReport?.symbol ?? null;
311
312
  // The desktop and the web only: the terminal's footer stays as it was.
@@ -8,6 +8,7 @@ import {
8
8
  usePaneStatusFooter,
9
9
  type ChartTableChart,
10
10
  } from "../../../components";
11
+ import { ageFooterSegment } from "../../../components/layout/pane/footer";
11
12
  import { staticSeries } from "../../../components/chart/static/series";
12
13
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
13
14
  import { useAsyncResource, useAutoRefresh, usePluginPaneState, useUpdatedAgo } from "../../../public/react";
@@ -107,7 +108,7 @@ export function EarningsHistoryView({ symbol, width, height, focused, registrati
107
108
  error: historyError,
108
109
  stale: !!resource.data?.stale,
109
110
  info: abroad ? [{ id: "us-listings", parts: [{ text: "past reports: US listings only", tone: "muted" as const }] }]
110
- : payload && updatedAgo ? [{ id: "updated", parts: [{ text: updatedAgo, tone: "muted" as const }] }] : [],
111
+ : payload && updatedAgo ? [ageFooterSegment(updatedAgo)] : [],
111
112
  });
112
113
 
113
114
  return (
@@ -4,8 +4,8 @@ import { DEFAULT_GRAPH_OPTIONS, type GraphOptions, type GraphPayload } from "../
4
4
  import { errorMessage } from "../../../utils/errors";
5
5
  import { addDays, newYorkToday } from "../earnings/board-model";
6
6
  import { loadEarningsBoard } from "../earnings/client";
7
- import { loadSupplyChain } from "../supply-chain/client";
8
- import { cachedGraph, loadGraph } from "../supply-chain/graph-client";
7
+ import { loadSupplyChain, loadSupplyChainsAhead } from "../supply-chain/client";
8
+ import { cachedGraph, loadGraph, loadGraphsAhead } from "../supply-chain/graph-client";
9
9
  import { projectRipple, projectSecondHop, rippleCompanyTickers, RIPPLE_DAYS, secondHopLinks, type RippleRow, type SecondHopRow } from "./model";
10
10
 
11
11
  export interface RippleSources {
@@ -13,6 +13,10 @@ export interface RippleSources {
13
13
  calendar(query: EarningsCalendarQuery): Promise<EarningsCalendarPayload>;
14
14
  /** SPLC's graph, which returns both hops around a holding in one call. Absent when the account sees one hop only. */
15
15
  graph?(symbol: string, options: GraphOptions): Promise<GraphPayload>;
16
+ /** Reads every holding's disclosures ahead in batch requests, for `supplyChain` to find. */
17
+ supplyChainsAhead?(symbols: readonly string[]): Promise<void>;
18
+ /** Reads these holdings' graphs ahead in batch requests, for `graph` to find. */
19
+ graphsAhead?(symbols: readonly string[], options: GraphOptions): Promise<void>;
16
20
  }
17
21
 
18
22
  /**
@@ -60,14 +64,29 @@ export interface RippleSnapshot {
60
64
  */
61
65
  export function cachedRippleSources(accessKey: string, force = false, graph: "off" | "load" | "cached" = "off"): RippleSources & { staleFlags: boolean[] } {
62
66
  const staleFlags: boolean[] = [];
67
+ // A forced read takes what this refresh read ahead as fresh.
68
+ const chainsAhead = new Set<string>();
69
+ const graphsAhead = new Set<string>();
63
70
  return {
64
71
  staleFlags,
65
- supplyChain: async (symbol) => { const result = await loadSupplyChain(symbol, accessKey, force); staleFlags.push(result.stale); return result.payload; },
72
+ supplyChainsAhead: async (symbols) => {
73
+ for (const symbol of await loadSupplyChainsAhead(symbols, accessKey, force)) chainsAhead.add(symbol);
74
+ },
75
+ supplyChain: async (symbol) => {
76
+ const result = await loadSupplyChain(symbol, accessKey, force && !chainsAhead.has(symbol.toUpperCase()));
77
+ staleFlags.push(result.stale);
78
+ return result.payload;
79
+ },
66
80
  calendar: async (query) => { const result = await loadEarningsBoard(query, force); staleFlags.push(result.stale); return result.payload; },
67
81
  // Cached by the full query, apart from SPLC Graph's default one.
68
82
  ...(graph !== "off" ? { graph: async (symbol: string, options: GraphOptions) => {
69
83
  const cached = graph === "cached" ? cachedGraph(symbol, "", options, accessKey) : null;
70
- const result = cached ?? await loadGraph(symbol, "", options, accessKey, force); staleFlags.push(result.stale); return result.payload;
84
+ const result = cached ?? await loadGraph(symbol, "", options, accessKey, force && !graphsAhead.has(symbol.toUpperCase()));
85
+ staleFlags.push(result.stale);
86
+ return result.payload;
87
+ } } : {}),
88
+ ...(graph === "load" ? { graphsAhead: async (symbols: readonly string[], options: GraphOptions) => {
89
+ for (const symbol of await loadGraphsAhead(symbols, options, accessKey, force)) graphsAhead.add(symbol);
71
90
  } } : {}),
72
91
  };
73
92
  }
@@ -84,8 +103,12 @@ async function eachLimited(symbols: readonly string[], load: (symbol: string) =>
84
103
  const locked = () => ({ graphs: new Map<string, GraphPayload>(), failures: [] as Failure[], locked: true });
85
104
  const isPreview = (graphs: ReadonlyMap<string, GraphPayload>) => [...graphs.values()].some((graph) => graph.access === "preview");
86
105
 
87
- /** One graph call per holding. The first answer says whether the account sees two hops; a one-hop preview skips the rest. */
88
- async function loadGraphs(holdings: readonly string[], graph: NonNullable<RippleSources["graph"]>) {
106
+ /**
107
+ * One graph per holding. The first answer says whether the account sees two
108
+ * hops; a one-hop preview skips the rest, which are otherwise read ahead in
109
+ * batch requests.
110
+ */
111
+ async function loadGraphs(holdings: readonly string[], graph: NonNullable<RippleSources["graph"]>, ahead?: RippleSources["graphsAhead"]) {
89
112
  const graphs = new Map<string, GraphPayload>();
90
113
  const failures: Failure[] = [];
91
114
  const load = async (symbol: string) => {
@@ -95,6 +118,7 @@ async function loadGraphs(holdings: readonly string[], graph: NonNullable<Ripple
95
118
  const [first, ...rest] = holdings;
96
119
  if (first) await load(first);
97
120
  if (isPreview(graphs)) return locked();
121
+ await ahead?.(rest, RIPPLE_GRAPH_OPTIONS);
98
122
  await eachLimited(rest, load);
99
123
  // Only when the first holding failed can a preview get this far.
100
124
  if (isPreview(graphs)) return locked();
@@ -107,11 +131,12 @@ export async function loadRipple(holdings: readonly string[], sources: RippleSou
107
131
  const symbols = [...new Set(holdings.map((symbol) => symbol.toUpperCase()))];
108
132
  const chains = new Map<string, SupplyChainPayload>();
109
133
  const failures: Failure[] = [];
134
+ await sources.supplyChainsAhead?.(symbols);
110
135
  await eachLimited(symbols, async (symbol) => {
111
136
  try { chains.set(symbol, await sources.supplyChain(symbol)); }
112
137
  catch (error) { failures.push({ symbol, error: errorMessage(error) }); }
113
138
  });
114
- const second = !secondHop ? null : sources.graph ? await loadGraphs(symbols, sources.graph) : locked();
139
+ const second = !secondHop ? null : sources.graph ? await loadGraphs(symbols, sources.graph, sources.graphsAhead) : locked();
115
140
  const graphs = second?.graphs ?? new Map<string, GraphPayload>();
116
141
  const companies = [...rippleCompanyTickers(chains), ...secondHopLinks(chains, graphs).map((link) => link.company.ticker!.toUpperCase())];
117
142
  const reports: EarningsCalendarPayload["reports"] = [];
@@ -1,7 +1,6 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
2
  import { formatTickerListInput, parseTickerListInput } from "../../../tickers/list";
3
3
  import { earningsRippleHeadless } from "./headless";
4
- import { EarningsRipplePane } from "./pane";
5
4
 
6
5
  function namedSymbols(arg: string | undefined): string[] {
7
6
  try { return arg?.trim() ? parseTickerListInput(arg) : []; } catch { return []; }
@@ -9,7 +8,7 @@ function namedSymbols(arg: string | undefined): string[] {
9
8
 
10
9
  export const earningsRippleModule: PluginModule = {
11
10
  panes: [{
12
- id: "earnings-ripple", name: "Earnings Ripple", icon: "E", component: EarningsRipplePane,
11
+ id: "earnings-ripple", name: "Earnings Ripple", icon: "E", get component(): typeof import("./pane").EarningsRipplePane { const { EarningsRipplePane } = require("./pane"); return EarningsRipplePane; },
13
12
  defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 96, height: 20 },
14
13
  tableExport: true,
15
14
  // Without named tickers it lists what is in the person's portfolios.
@@ -256,17 +256,40 @@ export function econEventName(event: Pick<EconEvent, "country" | "event" | "actu
256
256
  /** Letters and digits only, lower case: "Non-Farm" and "nonfarm" read alike. */
257
257
  const compactName = (text: string) => text.toLowerCase().replace(/[^a-z0-9]+/g, "");
258
258
 
259
+ /** Words an initialism leaves out: the U.S. Department of Agriculture is USDA. */
260
+ const INITIALISM_SKIPS = new Set(["of", "the", "and", "for", "on", "in"]);
261
+
262
+ /**
263
+ * The ways a row's publisher is written, compacted: its name, its initials
264
+ * ("usda" for the U.S. Department of Agriculture, "bls" for the Bureau of
265
+ * Labor Statistics) and the name of its site (usda.gov).
266
+ */
267
+ function publisherNames(source: EconEvent["source"]): string[] {
268
+ if (!source?.name?.trim()) return [];
269
+ const initials = source.name.split(/[\s.,&/()-]+/).filter((word) => word && !INITIALISM_SKIPS.has(word.toLowerCase()))
270
+ .map((word) => word[0]!).join("");
271
+ let site = "";
272
+ try {
273
+ site = source.url ? new URL(source.url).hostname.replace(/^www\./i, "").split(".")[0] ?? "" : "";
274
+ } catch {
275
+ // Intentional ignore: a publisher's address that does not parse leaves its name and initials.
276
+ }
277
+ return [source.name, initials, site].map(compactName).filter(Boolean);
278
+ }
279
+
259
280
  /**
260
- * Whether a row's name carries every word of `query`, case, spaces and
261
- * punctuation aside: `nonfarm` finds Non Farm Payrolls and Non-Farm
262
- * Employment Change, `fed funds` the Federal Funds Rate. The name shown and the
263
- * feed's own are both read.
281
+ * Whether a row carries every word of `query` in its name or its publisher's,
282
+ * case, spaces and punctuation aside: `nonfarm` finds Non Farm Payrolls and
283
+ * Non-Farm Employment Change, `fed funds` the Federal Funds Rate, and `usda`
284
+ * every row the U.S. Department of Agriculture publishes. The name shown and
285
+ * the feed's own are both read.
264
286
  */
265
- export function matchesEventSearch(event: Pick<EconEvent, "country" | "event" | "actual" | "forecast" | "prior">, query: string): boolean {
287
+ export function matchesEventSearch(event: Pick<EconEvent, "country" | "event" | "actual" | "forecast" | "prior" | "source">, query: string): boolean {
266
288
  const words = query.split(/\s+/).map(compactName).filter(Boolean);
267
289
  if (words.length === 0) return true;
268
290
  const names = [event.event, econEventName(event)].map(compactName);
269
- return names.some((name) => words.every((word) => name.includes(word)));
291
+ const publisher = publisherNames(event.source);
292
+ return names.some((name) => words.every((word) => name.includes(word) || publisher.some((text) => text.includes(word))));
270
293
  }
271
294
 
272
295
  /**
@@ -2,7 +2,6 @@ import type { PluginModule } from "../plugin-module";
2
2
  import { statsCache } from "./cache";
3
3
  import { STAT_CATEGORIES } from "./defs";
4
4
  import { econStatisticsHeadless } from "./headless";
5
- import { EconStatisticsPane } from "./pane";
6
5
  import { resolveStatArg, STATS, DEFAULT_STAT_ID } from "./stats";
7
6
 
8
7
 
@@ -13,7 +12,7 @@ export const econStatisticsModule: PluginModule = {
13
12
  id: ECON_STATISTICS_PANE_ID,
14
13
  name: "Economic Statistics",
15
14
  icon: "E",
16
- component: EconStatisticsPane,
15
+ get component(): typeof import("./pane").EconStatisticsPane { const { EconStatisticsPane } = require("./pane"); return EconStatisticsPane; },
17
16
  defaultPosition: "right",
18
17
  defaultMode: "floating",
19
18
  defaultFloatingSize: { width: 120, height: 36 },
@@ -21,7 +21,7 @@ const savedPath = (id: string) => `saved/${encodeURIComponent(id)}`;
21
21
  export const screenerApi = {
22
22
  query: (query: ScreenQuery, signal?: AbortSignal, client: ScreenerClient = apiClient) =>
23
23
  client.equityScreener<ScreenPayload>("query", json("POST", query, { signal })),
24
- fields: (signal?: AbortSignal) => apiClient.equityScreener<ScreenFieldsResponse>("fields?include=social,research,attention,hiring-apps,kpis,volatility,dividends,technical,option-volatility,ownership", { signal }),
24
+ fields: (signal?: AbortSignal) => apiClient.equityScreener<ScreenFieldsResponse>("fields?include=social,research,attention,hiring-apps,kpis,volatility,dividends,technical,option-volatility,ownership,quality", { signal }),
25
25
  export: (definition: ScreenDefinition, snapshotId: string) =>
26
26
  apiClient.equityScreener<ScreenExportResponse>("export", json("POST", { ...definition, snapshotId }, { headers: { Accept: "application/json" } })),
27
27
  saved: async (signal?: AbortSignal, client: ScreenerClient = apiClient) =>
@@ -251,6 +251,7 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
251
251
  "IV percentile is the share of the past 52 weeks' options closes with IV30 below the latest; IV30 changes are in volatility points (25% to 27% is +2). IV fields with no close in five days are unavailable. HV20 is 20-session realized volatility, annualized, and HV rank places it between its 52-week low and high.",
252
252
  "Technical fields (RSI, moving averages, MACD and golden crosses, new highs and lows, Bollinger width) read US listings' completed daily sessions: they describe the last close, not the session in progress. A days-since field counts trading days, 0 at the last session.",
253
253
  "Shares outstanding, float, short % of float and institutional ownership cover US filers: the 10-K or 10-Q cover count, the stored statistics float, the FINRA settlement, the 13F quarter. The source does not say when it measured the float, so float and short % of float are partial, dated by when the statistics were retrieved (within 30 days) and never above the dated shares outstanding. A stale or inconsistent input leaves a field unavailable with its reason. Institutional ownership is over the balance-sheet count at the 13F quarter end; over 100% is partial with its reason.",
254
+ "ROIC, FCF yield, net debt / EBITDA and debt / equity read FA's latest annual column: the trailing four quarters, or the fiscal year when those quarters are it. Revenue and EPS CAGRs run from the latest fiscal year back 3 or 5 years, both years from the stored statements or both from the SEC filings (US filers), never one of each. Forward growth is the consensus EE shows for the fiscal year in progress. N/M where FA prints it: EBITDA, equity, invested capital or a base year at or below zero.",
254
255
  ],
255
256
  },
256
257
  async load(args, ctx) {