gloomberb 0.16.4 → 0.16.5

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 (554) hide show
  1. package/README.md +14 -91
  2. package/package.json +4 -1
  3. package/src/{plugins/builtin/cloud → account}/auth-form.tsx +9 -9
  4. package/src/{plugins/builtin/cloud → account}/auth-model.ts +4 -4
  5. package/src/account/billing.ts +57 -0
  6. package/src/{plugins/builtin/cloud → account}/device-signin-dialog.tsx +18 -12
  7. package/src/{plugins/builtin/cloud → account}/device-signin.ts +2 -2
  8. package/src/{plugins/builtin/cloud → account}/email-auth-attempt.ts +2 -2
  9. package/src/account/session.ts +16 -0
  10. package/src/{plugins/builtin/cloud → account}/upgrade-dialog.tsx +16 -15
  11. package/src/{plugins/builtin/cloud → account}/upgrade-personalization.ts +7 -7
  12. package/src/{plugins/builtin/cloud → account}/upgrade-tickers.ts +6 -6
  13. package/src/{plugins/builtin/cloud → account}/verification-panel.tsx +6 -6
  14. package/src/api-client/data.ts +24 -2
  15. package/src/api-client/earnings.ts +6 -0
  16. package/src/api-client/futures-curve.ts +56 -0
  17. package/src/api-client/index.ts +4 -1
  18. package/src/api-client/market-discovery.ts +10 -1
  19. package/src/api-client/paths.ts +7 -0
  20. package/src/api-client/research-activity.ts +15 -10
  21. package/src/api-client/types.ts +17 -1
  22. package/src/api-client/yield-curves.ts +8 -0
  23. package/src/app/global-shortcuts.ts +15 -1
  24. package/src/app/pane-runtime/ticker-open-runtime.ts +7 -3
  25. package/src/architecture/core-builtin-allowlist.json +4 -13
  26. package/src/brokers/signed-in/sign-in-dialog.tsx +11 -5
  27. package/src/capabilities/chart-series.ts +11 -1
  28. package/src/cli/commands/crypto-hints.ts +7 -0
  29. package/src/cli/commands/fred-series.ts +61 -0
  30. package/src/cli/commands/market.ts +407 -104
  31. package/src/cli/commands/news-hints.ts +62 -0
  32. package/src/cli/commands/news-search.ts +53 -0
  33. package/src/cli/commands/options-leaps.ts +5 -2
  34. package/src/cli/commands/overview.ts +93 -20
  35. package/src/cli/commands/remote.ts +2 -1
  36. package/src/cli/commands/search.ts +82 -9
  37. package/src/cli/commands/system.ts +55 -8
  38. package/src/cli/commands/ticker.ts +88 -11
  39. package/src/cli/context.ts +45 -14
  40. package/src/cli/desktop-pane-shot.ts +59 -7
  41. package/src/cli/earnings-calendar.ts +71 -0
  42. package/src/cli/freshness.ts +18 -7
  43. package/src/cli/glossary.ts +429 -0
  44. package/src/cli/history-rows.ts +48 -7
  45. package/src/cli/index-roots.ts +7 -0
  46. package/src/cli/index.ts +21 -7
  47. package/src/cli/known-gaps.ts +27 -0
  48. package/src/cli/listing-arg.ts +417 -56
  49. package/src/cli/not-a-ticker.ts +12 -5
  50. package/src/cli/pane-functions/access-gate.ts +5 -0
  51. package/src/cli/pane-functions/capabilities.ts +3 -2
  52. package/src/cli/pane-functions/catalog.ts +35 -5
  53. package/src/cli/pane-functions/data.ts +12 -8
  54. package/src/cli/pane-functions/discovery.ts +1 -0
  55. package/src/cli/pane-functions/dom.ts +20 -6
  56. package/src/cli/pane-functions/freshness.ts +363 -43
  57. package/src/cli/pane-functions/function-help.ts +12 -5
  58. package/src/cli/pane-functions/headless.ts +45 -17
  59. package/src/cli/pane-functions/index.ts +63 -10
  60. package/src/cli/pane-functions/market-session.ts +22 -8
  61. package/src/cli/pane-functions/options.ts +63 -22
  62. package/src/cli/pane-functions/report-notices.ts +25 -7
  63. package/src/cli/pane-functions/report.ts +15 -8
  64. package/src/cli/pane-functions/resolver.ts +49 -12
  65. package/src/cli/pane-functions/screenshot.ts +110 -23
  66. package/src/cli/report-tables.ts +3 -0
  67. package/src/cli/result.ts +4 -1
  68. package/src/cli/staged-tickers.ts +52 -0
  69. package/src/cli/symbol-args.ts +9 -0
  70. package/src/cli/symbol-list.ts +172 -0
  71. package/src/cli/ticker-resolution.ts +223 -30
  72. package/src/cli/types.ts +5 -0
  73. package/src/components/chart/composite/axis-overlays.tsx +6 -2
  74. package/src/components/chart/composite/composite-chart.tsx +40 -6
  75. package/src/components/chart/composite/legend.tsx +49 -2
  76. package/src/components/chart/composite/panel-surface.tsx +44 -17
  77. package/src/components/chart/composite/price-axis-labels.tsx +3 -1
  78. package/src/components/command-bar/panel/index.tsx +11 -3
  79. package/src/components/command-bar/panel/runtime.ts +3 -0
  80. package/src/components/command-bar/panel/types.ts +2 -0
  81. package/src/components/command-bar/routes/root/provider-search.ts +4 -3
  82. package/src/components/command-bar/routes/ticker-search/results.ts +12 -11
  83. package/src/components/command-bar/routes/ticker-search/route.ts +4 -3
  84. package/src/components/command-bar/workflow/tickers.ts +23 -4
  85. package/src/components/layout/detached-pane-shell.tsx +9 -0
  86. package/src/components/layout/floating-pane.tsx +3 -0
  87. package/src/components/layout/header.tsx +20 -5
  88. package/src/components/layout/pane/content.tsx +7 -13
  89. package/src/components/layout/pane/footer/index.tsx +6 -2
  90. package/src/components/layout/pane/footer/model.ts +35 -1
  91. package/src/components/layout/pane/header.tsx +24 -1
  92. package/src/components/layout/pane/index.tsx +12 -2
  93. package/src/components/layout/pane/sidebar.tsx +78 -10
  94. package/src/components/layout/shell/active-drag.ts +21 -5
  95. package/src/components/layout/shell/drag/index.ts +39 -9
  96. package/src/components/layout/shell/drag/live.ts +61 -51
  97. package/src/components/layout/shell/drag/runtime.ts +54 -12
  98. package/src/components/layout/shell/index.tsx +25 -7
  99. package/src/components/layout/shell/menu.ts +5 -0
  100. package/src/components/layout/shell/native/pointer-runtime.ts +3 -1
  101. package/src/components/layout/shell/pane/layers.tsx +192 -45
  102. package/src/components/layout/shell/terminal-pointer-runtime.ts +24 -0
  103. package/src/components/markdown-text.tsx +62 -20
  104. package/src/components/metric-treemap/heat-surface.tsx +10 -1
  105. package/src/components/onboarding/account-step/form-panel.tsx +1 -1
  106. package/src/components/onboarding/account-step/index.tsx +1 -1
  107. package/src/components/onboarding/account-step/qr-panel.tsx +1 -1
  108. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  109. package/src/components/onboarding/onboarding-keyboard.ts +1 -1
  110. package/src/components/onboarding/onboarding-wizard.tsx +2 -2
  111. package/src/components/onboarding/use-onboarding-upgrade.ts +3 -3
  112. package/src/components/onboarding/wizard-account.ts +2 -2
  113. package/src/components/pane-template-wizard.tsx +9 -1
  114. package/src/components/price-selector-dialog.tsx +11 -2
  115. package/src/components/sign-in-gate.tsx +3 -3
  116. package/src/components/ticker/list-table-view.tsx +24 -1
  117. package/src/components/ui/action-row.tsx +2 -1
  118. package/src/components/ui/button.tsx +7 -1
  119. package/src/components/ui/choice-dialog.tsx +6 -4
  120. package/src/components/ui/confirm-dialog.tsx +28 -18
  121. package/src/components/ui/data-table/index.tsx +17 -6
  122. package/src/components/ui/data-table/opentui/index.tsx +37 -6
  123. package/src/components/ui/data-table/types.ts +12 -0
  124. package/src/components/ui/dialog-actions.tsx +64 -0
  125. package/src/components/ui/display.tsx +17 -3
  126. package/src/components/ui/frame.tsx +35 -2
  127. package/src/components/ui/icon.tsx +2 -0
  128. package/src/components/ui/text-prompt-dialog.tsx +11 -9
  129. package/src/data/cached-query.ts +2 -0
  130. package/src/data/config/store/normalize.ts +7 -2
  131. package/src/data/ticker-repository.ts +5 -0
  132. package/src/data/ticker-store.ts +15 -0
  133. package/src/docs/content.ts +215 -0
  134. package/src/docs/load.ts +139 -0
  135. package/src/docs/requests.ts +39 -0
  136. package/src/docs/snapshot.chunk.ts +8 -0
  137. package/src/docs/snapshot.json +7416 -0
  138. package/src/docs/topics.ts +216 -0
  139. package/src/i18n/es.ts +61 -4
  140. package/src/i18n/ja.ts +61 -4
  141. package/src/i18n/ko.ts +61 -4
  142. package/src/i18n/zh-cn.ts +61 -4
  143. package/src/i18n/zh-tw.ts +61 -4
  144. package/src/layout/desks.ts +1 -1
  145. package/src/layout/pane-manager/dock-tree.ts +18 -0
  146. package/src/layout/pane-manager/index.ts +1 -0
  147. package/src/market-data/coordinator/chart.ts +10 -4
  148. package/src/market-data/coordinator/index.ts +6 -3
  149. package/src/{plugins/builtin/futures/contracts.ts → market-data/futures-board.ts} +81 -28
  150. package/src/market-data/futures-contract.ts +151 -0
  151. package/src/market-data/hooks.tsx +10 -2
  152. package/src/market-data/market/format.ts +39 -21
  153. package/src/market-data/market/freshness.ts +81 -18
  154. package/src/market-data/market/status.ts +19 -23
  155. package/src/market-data/published-apac-sessions.ts +23 -1
  156. package/src/market-data/quotes/contributions.ts +3 -0
  157. package/src/market-data/quotes/metadata.ts +7 -24
  158. package/src/market-data/quotes/resolution.ts +2 -0
  159. package/src/market-data/quotes/screener-live-quotes.ts +5 -4
  160. package/src/market-data/result-types.ts +2 -0
  161. package/src/plugins/builtin/account-management/model.ts +1 -56
  162. package/src/plugins/builtin/account-management/pane.tsx +6 -4
  163. package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
  164. package/src/plugins/builtin/alerts/cli.ts +313 -47
  165. package/src/plugins/builtin/alerts/events-pane.tsx +45 -7
  166. package/src/plugins/builtin/alerts/events.ts +23 -2
  167. package/src/plugins/builtin/alerts/history-pane.tsx +7 -2
  168. package/src/plugins/builtin/alerts/history.ts +2 -0
  169. package/src/plugins/builtin/alerts/index.tsx +41 -9
  170. package/src/plugins/builtin/alerts/pane.tsx +8 -0
  171. package/src/plugins/builtin/alerts/research-builder.ts +8 -3
  172. package/src/plugins/builtin/alerts/research-rules.ts +18 -2
  173. package/src/plugins/builtin/alerts/scheduled-check.ts +122 -0
  174. package/src/plugins/builtin/alerts/scheduled-sources.ts +25 -0
  175. package/src/plugins/builtin/alerts/scheduled.ts +254 -0
  176. package/src/plugins/builtin/alerts/targets.ts +92 -0
  177. package/src/plugins/builtin/analytics/index.tsx +4 -2
  178. package/src/plugins/builtin/analytics/risk-headless.ts +30 -12
  179. package/src/plugins/builtin/analytics/risk-math.ts +91 -27
  180. package/src/plugins/builtin/analytics/risk-model.ts +35 -5
  181. package/src/plugins/builtin/analytics/risk-pane.tsx +15 -8
  182. package/src/plugins/builtin/attention/index.tsx +2 -0
  183. package/src/plugins/builtin/attention/pane.tsx +3 -1
  184. package/src/plugins/builtin/attention/series.ts +26 -0
  185. package/src/plugins/builtin/awards/index.tsx +2 -0
  186. package/src/plugins/builtin/awards/pane.tsx +3 -1
  187. package/src/plugins/builtin/awards/series.ts +29 -0
  188. package/src/plugins/builtin/bond-calculator/model.ts +2 -1
  189. package/src/plugins/builtin/catalysts/pane.tsx +3 -1
  190. package/src/plugins/builtin/cds/client.ts +70 -13
  191. package/src/plugins/builtin/cds/headless.ts +116 -19
  192. package/src/plugins/builtin/cds/index.tsx +3 -1
  193. package/src/plugins/builtin/cds/model.ts +193 -31
  194. package/src/plugins/builtin/cds/pane.tsx +56 -14
  195. package/src/plugins/builtin/cds/series.ts +19 -0
  196. package/src/plugins/builtin/central-bank-rates/index.tsx +2 -0
  197. package/src/plugins/builtin/central-bank-rates/series.ts +42 -0
  198. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +192 -255
  199. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +84 -46
  200. package/src/plugins/builtin/chart-composer/editor.tsx +5 -4
  201. package/src/plugins/builtin/chart-composer/field-series.ts +40 -0
  202. package/src/plugins/builtin/chart-composer/headless.ts +8 -3
  203. package/src/plugins/builtin/chart-composer/index.tsx +9 -2
  204. package/src/plugins/builtin/chart-composer/series-catalog.ts +58 -50
  205. package/src/plugins/builtin/chart-composer/series-expression.ts +18 -10
  206. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +59 -3
  207. package/src/plugins/builtin/chart-composer/user-series.ts +84 -0
  208. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +10 -2
  209. package/src/plugins/builtin/chat/controller/index.ts +2 -0
  210. package/src/plugins/builtin/cloud/askg/controller.ts +111 -19
  211. package/src/plugins/builtin/cloud/askg/executor.ts +57 -3
  212. package/src/plugins/builtin/cloud/askg/manifest.ts +32 -7
  213. package/src/plugins/builtin/cloud/askg/model.ts +27 -8
  214. package/src/plugins/builtin/cloud/askg/protocol.ts +10 -1
  215. package/src/plugins/builtin/cloud/askg/tool-display.ts +3 -0
  216. package/src/plugins/builtin/cloud/askg/undo.ts +2 -0
  217. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  218. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  219. package/src/plugins/builtin/cloud/auth-dialog.tsx +3 -3
  220. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  221. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +3 -0
  222. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +77 -9
  223. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +6 -1
  224. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +9 -0
  225. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +35 -11
  226. package/src/plugins/builtin/cloud/thesis/exposure.ts +1 -1
  227. package/src/plugins/builtin/cloud/thesis/prompts.tsx +2 -1
  228. package/src/plugins/builtin/company-attention/app-rank.tsx +1 -1
  229. package/src/plugins/builtin/company-attention/headless.ts +19 -5
  230. package/src/plugins/builtin/company-attention/index.tsx +2 -0
  231. package/src/plugins/builtin/company-attention/model.ts +26 -7
  232. package/src/plugins/builtin/company-attention/pane.tsx +12 -9
  233. package/src/plugins/builtin/company-attention/series.ts +52 -0
  234. package/src/plugins/builtin/company-kpis/pane.tsx +4 -2
  235. package/src/plugins/builtin/composite-plugins.ts +19 -13
  236. package/src/plugins/builtin/cot/index.tsx +3 -1
  237. package/src/plugins/builtin/cot/model.ts +40 -11
  238. package/src/plugins/builtin/cot/series.ts +27 -0
  239. package/src/plugins/builtin/credit-boards/index.tsx +2 -0
  240. package/src/plugins/builtin/credit-boards/series.ts +66 -0
  241. package/src/plugins/builtin/credit-conditions/index.tsx +2 -0
  242. package/src/plugins/builtin/credit-conditions/series.ts +26 -0
  243. package/src/plugins/builtin/credit-documents/headless.ts +31 -1
  244. package/src/plugins/builtin/credit-documents/pane.tsx +3 -1
  245. package/src/plugins/builtin/crypto-board/headless.ts +2 -3
  246. package/src/plugins/builtin/crypto-board/index.tsx +2 -0
  247. package/src/plugins/builtin/crypto-board/model.ts +12 -0
  248. package/src/plugins/builtin/crypto-board/pane.tsx +19 -12
  249. package/src/plugins/builtin/crypto-board/series.ts +48 -0
  250. package/src/plugins/builtin/custom-view/live-quotes.ts +2 -2
  251. package/src/plugins/builtin/debt-maturities/index.tsx +2 -0
  252. package/src/plugins/builtin/debt-maturities/series.ts +45 -0
  253. package/src/plugins/builtin/dividend-yield/headless.ts +2 -2
  254. package/src/plugins/builtin/dividend-yield/index.tsx +2 -0
  255. package/src/plugins/builtin/dividend-yield/series.ts +20 -0
  256. package/src/plugins/builtin/docs/blocks.tsx +181 -0
  257. package/src/plugins/builtin/docs/index.tsx +68 -0
  258. package/src/plugins/builtin/docs/pane.tsx +426 -0
  259. package/src/plugins/builtin/docs/targets.ts +72 -0
  260. package/src/plugins/builtin/docs/use-docs.ts +60 -0
  261. package/src/plugins/builtin/doe/index.tsx +2 -0
  262. package/src/plugins/builtin/doe/series.ts +62 -0
  263. package/src/plugins/builtin/earnings/client.ts +3 -2
  264. package/src/plugins/builtin/earnings/format.ts +1 -5
  265. package/src/plugins/builtin/earnings/headless.ts +58 -6
  266. package/src/plugins/builtin/earnings/history-model.ts +15 -1
  267. package/src/plugins/builtin/earnings/history-table.ts +13 -2
  268. package/src/plugins/builtin/earnings/history.tsx +7 -3
  269. package/src/plugins/builtin/econ/fred-catalog.ts +189 -0
  270. package/src/plugins/builtin/econ/fred-series-map.ts +0 -46
  271. package/src/plugins/builtin/econ/index.tsx +2 -0
  272. package/src/plugins/builtin/econ-statistics/defs.ts +5 -0
  273. package/src/plugins/builtin/econ-statistics/headless.ts +19 -4
  274. package/src/plugins/builtin/econ-statistics/stats.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/pane.tsx +4 -11
  276. package/src/plugins/builtin/exposure/holdings.ts +1 -1
  277. package/src/plugins/builtin/exposure/pane.tsx +3 -1
  278. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  279. package/src/plugins/builtin/fear-greed/series.ts +52 -0
  280. package/src/plugins/builtin/futures/index.tsx +43 -3
  281. package/src/plugins/builtin/futures/model.ts +1 -1
  282. package/src/plugins/builtin/futures/returns.ts +1 -1
  283. package/src/plugins/builtin/futures/series.ts +13 -0
  284. package/src/plugins/builtin/futures/table.tsx +1 -1
  285. package/src/plugins/builtin/futures/use-front-returns.ts +1 -1
  286. package/src/plugins/builtin/futures-curve/client.ts +12 -3
  287. package/src/plugins/builtin/futures-curve/headless.ts +70 -9
  288. package/src/plugins/builtin/futures-curve/index.tsx +1 -1
  289. package/src/plugins/builtin/futures-curve/model.ts +66 -11
  290. package/src/plugins/builtin/futures-curve/pane.tsx +52 -18
  291. package/src/plugins/builtin/fx-matrix/index.tsx +1 -1
  292. package/src/plugins/builtin/gpu/board.tsx +13 -10
  293. package/src/plugins/builtin/gpu/changes.tsx +4 -4
  294. package/src/plugins/builtin/gpu/client.ts +19 -1
  295. package/src/plugins/builtin/gpu/equities.tsx +14 -10
  296. package/src/plugins/builtin/gpu/headless.ts +56 -25
  297. package/src/plugins/builtin/gpu/history.tsx +3 -3
  298. package/src/plugins/builtin/gpu/index.tsx +2 -0
  299. package/src/plugins/builtin/gpu/model.ts +101 -16
  300. package/src/plugins/builtin/gpu/pane.tsx +28 -12
  301. package/src/plugins/builtin/gpu/series.ts +38 -0
  302. package/src/plugins/builtin/help/function-card.tsx +25 -6
  303. package/src/plugins/builtin/help/index.tsx +17 -8
  304. package/src/plugins/builtin/holders/beneficial-model.ts +90 -3
  305. package/src/plugins/builtin/holders/beneficial-report.ts +57 -5
  306. package/src/plugins/builtin/holders/beneficial-view.tsx +55 -11
  307. package/src/plugins/builtin/holders/client.ts +22 -3
  308. package/src/plugins/builtin/holders/format.ts +18 -3
  309. package/src/plugins/builtin/holders/headless.ts +36 -133
  310. package/src/plugins/builtin/holders/pane.tsx +11 -4
  311. package/src/plugins/builtin/holders/report-header.ts +40 -5
  312. package/src/plugins/builtin/holders/report-rows.ts +240 -0
  313. package/src/plugins/builtin/insider/client.ts +35 -20
  314. package/src/plugins/builtin/insider/headless.ts +41 -11
  315. package/src/plugins/builtin/insider/index.tsx +26 -9
  316. package/src/plugins/builtin/insider/insider-data.ts +10 -0
  317. package/src/plugins/builtin/insider/issuer.ts +49 -0
  318. package/src/plugins/builtin/insider/model.ts +3 -0
  319. package/src/plugins/builtin/iv-history/client.ts +14 -4
  320. package/src/plugins/builtin/iv-history/headless.ts +7 -4
  321. package/src/plugins/builtin/iv-history/index.tsx +2 -0
  322. package/src/plugins/builtin/iv-history/model.ts +111 -7
  323. package/src/plugins/builtin/iv-history/screen-pane.tsx +5 -9
  324. package/src/plugins/builtin/iv-history/series.ts +29 -0
  325. package/src/plugins/builtin/jobs/headless.ts +28 -3
  326. package/src/plugins/builtin/jobs/index.tsx +3 -1
  327. package/src/plugins/builtin/jobs/series.ts +19 -0
  328. package/src/plugins/builtin/kelly-sizer/pane.tsx +2 -1
  329. package/src/plugins/builtin/kelly-sizer/portfolio.ts +0 -39
  330. package/src/plugins/builtin/macro-day/client.ts +44 -3
  331. package/src/plugins/builtin/macro-day/headless.ts +13 -10
  332. package/src/plugins/builtin/macro-day/model.ts +56 -10
  333. package/src/plugins/builtin/macro-day/pane.tsx +30 -20
  334. package/src/plugins/builtin/market-heatmap/model.ts +19 -11
  335. package/src/plugins/builtin/market-heatmap/portfolio.ts +2 -2
  336. package/src/plugins/builtin/market-movers/client.ts +7 -5
  337. package/src/plugins/builtin/market-movers/index.tsx +3 -2
  338. package/src/plugins/builtin/market-movers/model.ts +30 -0
  339. package/src/plugins/builtin/market-movers/screener.ts +31 -0
  340. package/src/plugins/builtin/market-valuation/chart-series.ts +29 -69
  341. package/src/plugins/builtin/market-valuation/index.tsx +3 -3
  342. package/src/plugins/builtin/market-valuation/indicators.ts +0 -5
  343. package/src/plugins/builtin/members/headless.ts +7 -2
  344. package/src/plugins/builtin/members/model.ts +31 -3
  345. package/src/plugins/builtin/members/pane.tsx +3 -6
  346. package/src/plugins/builtin/mna/headless.ts +15 -3
  347. package/src/plugins/builtin/money-markets/index.tsx +2 -0
  348. package/src/plugins/builtin/money-markets/series.ts +41 -0
  349. package/src/plugins/builtin/notes/cli.ts +12 -7
  350. package/src/plugins/builtin/options/view.tsx +1 -1
  351. package/src/plugins/builtin/options-calculator/inputs.ts +2 -1
  352. package/src/plugins/builtin/options-positioning/client.ts +7 -3
  353. package/src/plugins/builtin/options-positioning/headless.ts +1 -0
  354. package/src/plugins/builtin/options-positioning/pane.tsx +2 -2
  355. package/src/plugins/builtin/options-scenario/client.ts +2 -1
  356. package/src/plugins/builtin/options-scenario/headless.ts +1 -0
  357. package/src/plugins/builtin/options-scenario/hedge.ts +2 -1
  358. package/src/plugins/builtin/perps/client.ts +40 -6
  359. package/src/plugins/builtin/perps/equity-row.tsx +33 -11
  360. package/src/plugins/builtin/perps/freshness.ts +66 -0
  361. package/src/plugins/builtin/perps/headless.ts +45 -30
  362. package/src/plugins/builtin/perps/index.tsx +18 -1
  363. package/src/plugins/builtin/perps/model.ts +20 -5
  364. package/src/plugins/builtin/perps/pane.tsx +76 -20
  365. package/src/plugins/builtin/perps/series.ts +76 -0
  366. package/src/plugins/builtin/plugin-marketplace/pane.tsx +67 -61
  367. package/src/plugins/builtin/plugin-module.ts +3 -0
  368. package/src/plugins/builtin/portfolio-list/account-metrics.ts +1 -1
  369. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +273 -0
  370. package/src/plugins/builtin/portfolio-list/cli/import-13f.ts +109 -0
  371. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +331 -65
  372. package/src/plugins/builtin/portfolio-list/cli/render.ts +200 -31
  373. package/src/plugins/builtin/portfolio-list/cli/shared.ts +24 -0
  374. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -47
  375. package/src/plugins/builtin/portfolio-list/column-values.ts +1 -1
  376. package/src/plugins/builtin/portfolio-list/grid.tsx +1 -1
  377. package/src/plugins/builtin/portfolio-list/headless.ts +34 -4
  378. package/src/plugins/builtin/portfolio-list/imported-from.ts +8 -0
  379. package/src/plugins/builtin/portfolio-list/live-valuation.ts +6 -2
  380. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -1
  381. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +17 -1
  382. package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
  383. package/src/plugins/builtin/portfolio-list/table.tsx +7 -1
  384. package/src/plugins/builtin/power/client.ts +16 -0
  385. package/src/plugins/builtin/power/evidence.ts +6 -2
  386. package/src/plugins/builtin/power/headless.ts +71 -7
  387. package/src/plugins/builtin/power/index.tsx +4 -1
  388. package/src/plugins/builtin/power/model.ts +72 -1
  389. package/src/plugins/builtin/power/pane.tsx +32 -9
  390. package/src/plugins/builtin/rate-path/headless.ts +11 -3
  391. package/src/plugins/builtin/realized-vol/headless.ts +5 -0
  392. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -2
  393. package/src/plugins/builtin/research/event-model.ts +68 -24
  394. package/src/plugins/builtin/research/events-headless.ts +2 -1
  395. package/src/plugins/builtin/research/headless.ts +2 -1
  396. package/src/plugins/builtin/seasonality/client.ts +15 -7
  397. package/src/plugins/builtin/seasonality/headless.ts +36 -12
  398. package/src/plugins/builtin/seasonality/index.tsx +2 -2
  399. package/src/plugins/builtin/seasonality/model.ts +119 -0
  400. package/src/plugins/builtin/seasonality/pane.tsx +108 -37
  401. package/src/plugins/builtin/series-inventory.ts +91 -0
  402. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -1
  403. package/src/plugins/builtin/shared/command-tab.ts +30 -12
  404. package/src/plugins/builtin/shared/earnings-report.ts +55 -0
  405. package/src/plugins/builtin/shared/fred-points.ts +20 -0
  406. package/src/plugins/builtin/shared/fx-rate-freshness.ts +101 -0
  407. package/src/plugins/builtin/shared/open-description.ts +29 -0
  408. package/src/plugins/builtin/shared/report-freshness.ts +9 -0
  409. package/src/plugins/builtin/shared/series-warning.ts +5 -0
  410. package/src/plugins/builtin/short-interest/headless.ts +1 -0
  411. package/src/plugins/builtin/short-interest/index.tsx +2 -0
  412. package/src/plugins/builtin/short-interest/series.ts +36 -0
  413. package/src/plugins/builtin/short-interest/watch-headless.ts +1 -0
  414. package/src/plugins/builtin/short-interest/watch-model.ts +9 -5
  415. package/src/plugins/builtin/short-volume/headless.ts +1 -0
  416. package/src/plugins/builtin/short-volume/index.tsx +2 -0
  417. package/src/plugins/builtin/short-volume/series.ts +23 -0
  418. package/src/plugins/builtin/social-mentions/index.tsx +2 -0
  419. package/src/plugins/builtin/social-mentions/series.ts +23 -0
  420. package/src/plugins/builtin/supply-chain/graph-pane.tsx +3 -2
  421. package/src/plugins/builtin/supply-chain/pane.tsx +3 -1
  422. package/src/plugins/builtin/thirteenf/api.ts +1 -0
  423. package/src/plugins/builtin/thirteenf/data.ts +6 -4
  424. package/src/plugins/builtin/thirteenf/headless.ts +217 -36
  425. package/src/plugins/builtin/thirteenf/model.ts +39 -1
  426. package/src/plugins/builtin/thirteenf/overlap.ts +75 -0
  427. package/src/plugins/builtin/thirteenf/pane.tsx +10 -1
  428. package/src/plugins/builtin/thirteenf/report-notes.ts +103 -1
  429. package/src/plugins/builtin/thirteenf/types.ts +2 -0
  430. package/src/plugins/builtin/ticker-detail/index.tsx +3 -0
  431. package/src/plugins/builtin/ticker-detail/overview/components.tsx +1 -1
  432. package/src/plugins/builtin/ticker-detail/overview/futures-contract.ts +31 -0
  433. package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -3
  434. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +2 -1
  435. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +48 -4
  436. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +13 -3
  437. package/src/plugins/builtin/time-sales/headless.ts +1 -0
  438. package/src/plugins/builtin/treasury-auctions/headless.ts +16 -2
  439. package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
  440. package/src/plugins/builtin/treasury-auctions/model.ts +8 -0
  441. package/src/plugins/builtin/vol-surface/headless.ts +1 -0
  442. package/src/plugins/builtin/volatility/index.tsx +2 -0
  443. package/src/plugins/builtin/volatility/series.ts +84 -0
  444. package/src/plugins/builtin/world-venue-map/geo-series.ts +26 -1
  445. package/src/plugins/builtin/world-venue-map/headless.ts +4 -1
  446. package/src/plugins/builtin/world-venue-map/index.ts +2 -1
  447. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +8 -4
  448. package/src/plugins/builtin/world-venue-map/layers.ts +29 -0
  449. package/src/plugins/builtin/yield-curve/chart.ts +2 -2
  450. package/src/plugins/builtin/yield-curve/curves.ts +21 -8
  451. package/src/plugins/builtin/yield-curve/headless.ts +71 -18
  452. package/src/plugins/builtin/yield-curve/index.tsx +3 -1
  453. package/src/plugins/builtin/yield-curve/series.ts +86 -0
  454. package/src/plugins/catalog-browser.ts +2 -1
  455. package/src/plugins/registry/index.ts +89 -4
  456. package/src/{plugins/builtin/portfolio-list → portfolio}/position-metrics.ts +6 -6
  457. package/src/portfolio/position-value.ts +40 -0
  458. package/src/public/__snapshots__/public-api.test.ts.snap +7 -0
  459. package/src/public/time-series.ts +17 -0
  460. package/src/react/auto-refresh.ts +13 -3
  461. package/src/remote/app-host.tsx +20 -4
  462. package/src/remote/confirm-change.tsx +85 -0
  463. package/src/remote/controller-utils.ts +8 -1
  464. package/src/remote/controller.ts +123 -6
  465. package/src/remote/pane-screenshot.ts +47 -0
  466. package/src/remote/schema.ts +62 -0
  467. package/src/remote/types.ts +30 -0
  468. package/src/remote/watchlist.ts +306 -0
  469. package/src/renderers/browser/main.tsx +3 -2
  470. package/src/renderers/cloudflare/worker.ts +1 -1
  471. package/src/renderers/dom/data-table/frozen-column.ts +3 -1
  472. package/src/renderers/dom/data-table/index.tsx +33 -6
  473. package/src/renderers/dom/data-table/row.tsx +27 -1
  474. package/src/renderers/dom/desktop/control-styles.ts +63 -1
  475. package/src/renderers/dom/desktop/controls.tsx +30 -23
  476. package/src/renderers/dom/desktop/dialog-actions.tsx +170 -0
  477. package/src/renderers/dom/desktop/icons.tsx +7 -0
  478. package/src/renderers/dom/dom-ui-host.tsx +3 -0
  479. package/src/renderers/dom/host/box.tsx +3 -0
  480. package/src/renderers/dom/host/chart-surface.tsx +6 -2
  481. package/src/renderers/dom/host/live-frame.ts +6 -2
  482. package/src/renderers/dom/host/overflow-fade.ts +14 -1
  483. package/src/renderers/dom/host/size-observer.ts +26 -0
  484. package/src/renderers/dom/host/surface-3d.tsx +7 -2
  485. package/src/renderers/dom/host/text.tsx +1 -1
  486. package/src/renderers/dom/styles.css +61 -0
  487. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +17 -2
  488. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +19 -11
  489. package/src/renderers/electrobun/view/cli-pane-shot-health.ts +10 -26
  490. package/src/renderers/electrobun/view/main.tsx +2 -0
  491. package/src/series/capability.ts +75 -0
  492. package/src/series/csv.ts +160 -0
  493. package/src/series/files.ts +107 -0
  494. package/src/series/registry.ts +198 -0
  495. package/src/series/resolve.ts +107 -0
  496. package/src/series/standalone.ts +48 -0
  497. package/src/series/types.ts +145 -0
  498. package/src/series/validate.ts +171 -0
  499. package/src/sources/gloomberb-cloud/index.ts +7 -7
  500. package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
  501. package/src/sources/history-result.ts +1 -0
  502. package/src/sources/listing-symbols.ts +4 -1
  503. package/src/sources/provider-router/batches.ts +4 -1
  504. package/src/sources/provider-router/cache.ts +4 -2
  505. package/src/sources/provider-router/cached-routes.ts +1 -1
  506. package/src/sources/provider-router/financial-routes.ts +1 -0
  507. package/src/sources/provider-router/financials.ts +13 -0
  508. package/src/sources/provider-router/history.ts +43 -25
  509. package/src/sources/provider-router/search.ts +1 -0
  510. package/src/sync/core-contributors.ts +1 -0
  511. package/src/tickers/company-name.ts +75 -0
  512. package/src/tickers/instrument-kind.ts +25 -0
  513. package/src/tickers/open-target.ts +3 -2
  514. package/src/tickers/search/index.ts +45 -7
  515. package/src/tickers/search/ranking.ts +33 -17
  516. package/src/tickers/search/types.ts +7 -1
  517. package/src/tickers/us-listing.ts +34 -0
  518. package/src/time-series/hooks.ts +9 -0
  519. package/src/time-series/resolve.ts +95 -16
  520. package/src/time-series/types.ts +4 -2
  521. package/src/time-series/use-chart-resolution.ts +32 -0
  522. package/src/types/config.ts +6 -0
  523. package/src/types/data-provider.ts +5 -0
  524. package/src/types/financials.ts +52 -1
  525. package/src/types/headless.ts +61 -0
  526. package/src/types/plugin.ts +19 -0
  527. package/src/types/price-history.ts +7 -0
  528. package/src/types/ticker.ts +14 -0
  529. package/src/ui/host.tsx +18 -0
  530. package/src/ui/live-resize.ts +81 -0
  531. package/src/ui/live-stretch.ts +121 -0
  532. package/src/ui/measured-cells.ts +58 -0
  533. package/src/ui/pane-fill.ts +11 -0
  534. package/src/utils/basis-points.ts +17 -0
  535. package/src/utils/cli-output.ts +7 -1
  536. package/src/utils/concurrency.ts +25 -0
  537. package/src/utils/crypto-pair.ts +20 -0
  538. package/src/utils/currency-pair.ts +19 -0
  539. package/src/utils/decimal-comma.ts +19 -0
  540. package/src/utils/dom-screenshot-css.ts +265 -0
  541. package/src/utils/dom-screenshot-rasters.ts +276 -0
  542. package/src/utils/dom-screenshot.ts +437 -52
  543. package/src/utils/exchange-rate-snapshot.ts +6 -2
  544. package/src/utils/exchanges.ts +107 -1
  545. package/src/utils/format.ts +16 -3
  546. package/src/utils/fundamentals.ts +66 -0
  547. package/src/utils/futures-generic.ts +6 -12
  548. package/src/utils/fx-market-hours.ts +15 -5
  549. package/src/utils/price-history.ts +99 -42
  550. package/src/utils/sec-eps-basis.ts +24 -11
  551. package/src/utils/sec.ts +61 -7
  552. package/src/utils/utc-time.ts +51 -4
  553. package/src/plugins/builtin/fx-matrix/freshness.ts +0 -65
  554. /package/src/{plugins/builtin/cloud → tickers}/starter-symbols.ts +0 -0
@@ -13,6 +13,7 @@ import {
13
13
  extendedQuoteFields,
14
14
  extendedSessionsOf,
15
15
  findCollection,
16
+ listingLabel,
16
17
  loadCollectionQuotes,
17
18
  valuePortfolioAllocation,
18
19
  valuePortfolioPositions,
@@ -27,9 +28,10 @@ import {
27
28
  formatAllocationWeight,
28
29
  formatTradeUnits,
29
30
  } from "./allocation";
30
- import { currencyMinorDigits, formatMarketPrice, formatMarketQuantity } from "../../../market-data/market/format";
31
+ import { currencyPriceMinDigits, formatMarketPrice, formatMarketQuantity } from "../../../market-data/market/format";
31
32
  import { resolvePortfolioTotalsCurrency } from "./summary/totals";
32
33
  import { quoteFreshnessFields } from "../shared/report-freshness";
34
+ import { importedFromText } from "./imported-from";
33
35
 
34
36
  const DEFAULT_ROW_LIMIT = 50;
35
37
  const MAX_ROW_LIMIT = 200;
@@ -54,7 +56,7 @@ const tradeUnits = (value: unknown, row: HeadlessPaneRow) => (
54
56
  /** Money per unit at the currency's minor digits: $230.00, not 230. */
55
57
  const unitMoney = (currencyKey: string) => (value: unknown, row: HeadlessPaneRow) => (
56
58
  typeof value === "number"
57
- ? formatMarketPrice(value, { assetCategory: textOf(row.assetCategory), minimumFractionDigits: Math.min(2, currencyMinorDigits(textOf(row[currencyKey]))) })
59
+ ? formatMarketPrice(value, { assetCategory: textOf(row.assetCategory), minimumFractionDigits: currencyPriceMinDigits(textOf(row[currencyKey])) })
58
60
  : formatNumber(undefined)
59
61
  );
60
62
 
@@ -113,6 +115,25 @@ const WATCHLIST_COLUMNS: HeadlessPaneColumn[] = [
113
115
  { key: "changePercent", header: "Chg", align: "right", format: percent },
114
116
  ];
115
117
 
118
+ /**
119
+ * `columns` with the text report's Ticker column naming each listing as
120
+ * `portfolio show` does (SAN:EPA, SAN:NYSE), not by the key it is stored under,
121
+ * which leaves two listings of one symbol looking alike. `textFormat` is text
122
+ * only: JSON and CSV keep the stored key in `symbol`, and the rows stay keyed
123
+ * by it.
124
+ */
125
+ function withListingLabels(columns: HeadlessPaneColumn[], tickers: readonly TickerRecord[]): HeadlessPaneColumn[] {
126
+ const labels = new Map(tickers.map((ticker) => [ticker.metadata.ticker, listingLabel(ticker)]));
127
+ return columns.map((column) => column.key !== "symbol" ? column : {
128
+ ...column,
129
+ textFormat: (value, row) => {
130
+ // The cash line is not a listing, even when a ticker is called CASH; its row carries no exchange.
131
+ if (typeof value !== "string" || !("exchange" in row)) return textOf(value) ?? formatNumber(undefined);
132
+ return labels.get(value) ?? value;
133
+ },
134
+ });
135
+ }
136
+
116
137
  function finite(value: unknown): number | null {
117
138
  return typeof value === "number" && Number.isFinite(value) ? value : null;
118
139
  }
@@ -234,7 +255,9 @@ async function portfolioHoldings(
234
255
  const broker = !!(target.portfolio.brokerId || target.portfolio.brokerInstanceId);
235
256
  const showTargets = allocation.targetSum != null;
236
257
  const targetNote = describeTargetSum(allocation.targetSum);
258
+ const imported = importedFromText(target.portfolio);
237
259
  const notices = [
260
+ ...(imported ? [imported] : []),
238
261
  ...(rows.length > limit
239
262
  ? [`${rows.length - limit} more position${rows.length - limit === 1 ? "" : "s"} not shown; totals include every position.`]
240
263
  : []),
@@ -244,7 +267,10 @@ async function portfolioHoldings(
244
267
 
245
268
  return {
246
269
  freshness: { source: broker ? "Your broker account and Gloom Cloud" : "Local portfolio and Gloom Cloud" },
247
- columns: withExtendedColumns(showTargets ? [...POSITION_COLUMNS, ...TARGET_COLUMNS] : POSITION_COLUMNS, extendedSessions),
270
+ columns: withListingLabels(
271
+ withExtendedColumns(showTargets ? [...POSITION_COLUMNS, ...TARGET_COLUMNS] : POSITION_COLUMNS, extendedSessions),
272
+ tickers,
273
+ ),
248
274
  rows: shown,
249
275
  complete: unavailable.length === 0,
250
276
  ...(unavailable.length
@@ -252,6 +278,7 @@ async function portfolioHoldings(
252
278
  : {}),
253
279
  metadata: {
254
280
  collection: { kind: "portfolio", id: target.id, name: target.name, broker },
281
+ ...(target.portfolio.importedFrom ? { importedFrom: target.portfolio.importedFrom } : {}),
255
282
  currency,
256
283
  positions: rows.length,
257
284
  totals: {
@@ -299,7 +326,10 @@ async function watchlistHoldings(
299
326
  const shown = rows.slice(0, limit);
300
327
  return {
301
328
  freshness: { source: "Local watchlist and Gloom Cloud" },
302
- columns: withExtendedColumns(WATCHLIST_COLUMNS, extendedSessionsOf(shown.map((row) => row.extendedSession))),
329
+ columns: withListingLabels(
330
+ withExtendedColumns(WATCHLIST_COLUMNS, extendedSessionsOf(shown.map((row) => row.extendedSession))),
331
+ tickers,
332
+ ),
303
333
  rows: shown,
304
334
  metadata: {
305
335
  collection: { kind: "watchlist", id: target.id, name: target.name },
@@ -0,0 +1,8 @@
1
+ import type { Portfolio } from "../../../types/ticker";
2
+
3
+ /** One line naming the filing an imported portfolio comes from, as `portfolio show`, `fn PF` and the import print it. */
4
+ export function importedFromText(portfolio: Pick<Portfolio, "importedFrom">): string | null {
5
+ const source = portfolio.importedFrom;
6
+ if (!source) return null;
7
+ return `Weights from ${source.name}'s 13F for ${source.period}, filed ${source.filed}; not your holdings.`;
8
+ }
@@ -1,5 +1,6 @@
1
1
  import type { AnalystResearchData, Fundamentals, Quote } from "../../../types/financials";
2
2
  import { hasLikelyQuoteUnitMismatch } from "../../../utils/currency-units";
3
+ import { priceEarningsEps } from "../../../utils/fundamentals";
3
4
  import { selectMarketCapitalization, type MarketCapitalization } from "../../../utils/market-capitalization";
4
5
  import { priceEarningsOnEarnings, type PriceEarnings } from "../../../utils/price-earnings";
5
6
 
@@ -54,9 +55,12 @@ function repriceMultiple(
54
55
  return price / perShare;
55
56
  }
56
57
 
57
- /** Trailing P/E over the current price; not meaningful when the trailing EPS is zero or a loss. */
58
+ /**
59
+ * Trailing P/E over the current price; not meaningful when the trailing EPS is zero or a loss. An EPS in another
60
+ * currency than the price is repriced at the rate the served P/E converted it at.
61
+ */
58
62
  export function liveTrailingPE(quote: Quote | null | undefined, fundamentals: Fundamentals | undefined): PriceEarnings | undefined {
59
- return priceEarningsOnEarnings(repriceMultiple(fundamentals?.trailingPE, fundamentals?.eps, quote, fundamentals), fundamentals?.eps);
63
+ return priceEarningsOnEarnings(repriceMultiple(fundamentals?.trailingPE, priceEarningsEps(fundamentals), quote, fundamentals), fundamentals?.eps);
60
64
  }
61
65
 
62
66
  /** Forward P/E over the current price; not meaningful when the forward EPS behind it is zero or a loss. */
@@ -88,7 +88,7 @@ import { useThrottledTickerOrder } from "../use-throttled-ticker-order";
88
88
  import { useThrottledMemo } from "../use-throttled-memo";
89
89
  import { useColumnClock } from "../use-column-clock";
90
90
  import { liveMarketCapitalization } from "../live-valuation";
91
- import { hasUnknownOptionMultiplier } from "../position-metrics";
91
+ import { hasUnknownOptionMultiplier } from "../../../../portfolio/position-metrics";
92
92
  import {
93
93
  allocationTotal,
94
94
  buildPortfolioAllocation,
@@ -126,6 +126,8 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
126
126
 
127
127
  const [streamWindow, setStreamWindow] = useState({ start: 0, end: 24 });
128
128
  const [quickAddFocused, setQuickAddFocused] = useState(false);
129
+ // Bumped by the empty table's "+ ticker" button to open the add row.
130
+ const [quickAddRequest, setQuickAddRequest] = useState(0);
129
131
 
130
132
  const {
131
133
  cursorSymbol,
@@ -612,6 +614,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
612
614
  return null;
613
615
  }, [activeCollectionId, config, currentPortfolio]);
614
616
  const showQuickAdd = !!(activeCollectionId && activeCollectionEntry && quickAddCollectionKind);
617
+ const requestQuickAdd = useCallback(() => setQuickAddRequest((count) => count + 1), []);
615
618
  const quickAddHeight = showQuickAdd ? 1 : 0;
616
619
  const selectedFinancials = cursorSymbol ? financialsMap.get(cursorSymbol) : undefined;
617
620
  const selectedCap = liveMarketCapitalization(selectedFinancials?.quote, selectedFinancials?.fundamentals);
@@ -636,6 +639,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
636
639
  width={width}
637
640
  onAdded={handleTickerAdded}
638
641
  onFocusChange={setQuickAddFocused}
642
+ focusRequest={quickAddRequest}
639
643
  />
640
644
  ) : null;
641
645
 
@@ -674,6 +678,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
674
678
  visibleRangeBuffer={3}
675
679
  resetScrollKey={activeCollectionId}
676
680
  onRowActivate={handleRowActivate}
681
+ onAddTicker={showQuickAdd ? requestQuickAdd : undefined}
677
682
  rootHeight={contentHeight}
678
683
  />
679
684
  ) : (
@@ -30,6 +30,8 @@ import {
30
30
  } from "./resolution";
31
31
 
32
32
  const QUICK_ADD_DEBOUNCE_MS = 300;
33
+ /** The pane key that opens the add row, as its footer hint shows it. */
34
+ export const QUICK_ADD_KEY = "a";
33
35
 
34
36
  export type { QuickAddCollectionKind } from "./resolution";
35
37
 
@@ -101,6 +103,7 @@ export function QuickAddTickerInput({
101
103
  width,
102
104
  onAdded,
103
105
  onFocusChange,
106
+ focusRequest = 0,
104
107
  }: {
105
108
  collectionId: string;
106
109
  collectionKind: QuickAddCollectionKind;
@@ -109,6 +112,12 @@ export function QuickAddTickerInput({
109
112
  width: number;
110
113
  onAdded: (symbol: string) => void;
111
114
  onFocusChange?: (focused: boolean) => void;
115
+ /**
116
+ * Bumped by a control outside the row, such as the empty table's
117
+ * "+ ticker" button, to open it. A click there focuses the pane in the same
118
+ * press, so the row opens once the pane is focused.
119
+ */
120
+ focusRequest?: number;
112
121
  }) {
113
122
  const dispatch = useAppDispatch();
114
123
  const { notify } = usePluginAppActions();
@@ -132,6 +141,13 @@ export function QuickAddTickerInput({
132
141
  setInputFocused(false);
133
142
  }, []);
134
143
 
144
+ const handledFocusRequestRef = useRef(focusRequest);
145
+ useEffect(() => {
146
+ if (focusRequest === handledFocusRequestRef.current || !focused) return;
147
+ handledFocusRequestRef.current = focusRequest;
148
+ focusInput();
149
+ }, [focusInput, focusRequest, focused]);
150
+
135
151
  const resetInput = useCallback(() => {
136
152
  setInputValue("");
137
153
  setValidation(IDLE_VALIDATION);
@@ -350,7 +366,7 @@ export function QuickAddTickerInput({
350
366
 
351
367
  // The row shows only its placeholder, so the footer says which key reaches it.
352
368
  usePaneFooter("portfolio-list:quick-add", () => ({
353
- hints: inputFocused ? [] : [{ id: "add", key: "a", label: "dd", onPress: focusInput }],
369
+ hints: inputFocused ? [] : [{ id: "add", key: QUICK_ADD_KEY, label: "dd", onPress: focusInput }],
354
370
  }), [focusInput, inputFocused]);
355
371
 
356
372
  return (
@@ -14,7 +14,7 @@ import {
14
14
  resolvePortfolioPositionPnl,
15
15
  portfolioPnlPercent,
16
16
  type PortfolioPositionPnl,
17
- } from "../position-metrics";
17
+ } from "../../../../portfolio/position-metrics";
18
18
 
19
19
  /** A position lot valued from a current quote, in the totals currency. */
20
20
  interface PricedPortfolioLot {
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useMemo, useRef } from "react";
2
+ import { QUICK_ADD_KEY } from "./quick-add";
2
3
  import {
3
4
  TickerListTableView,
4
5
  type DataTableKeyEvent,
@@ -15,7 +16,7 @@ import type { TickerRecord } from "../../../types/ticker";
15
16
  import { objectVersion } from "../../../utils/object-version";
16
17
  import { columnContextVersion } from "./cell-version";
17
18
  import { getColumnValue, type ColumnContext } from "./metrics";
18
- import { portfolioPnlLabel } from "./position-metrics";
19
+ import { portfolioPnlLabel } from "../../../portfolio/position-metrics";
19
20
  import { PORTFOLIO_ROW_MENU_KEYS } from "./row-actions";
20
21
 
21
22
  export type { QuoteFlashDirection };
@@ -98,6 +99,7 @@ export function PortfolioTickerTable({
98
99
  visibleRangeBuffer,
99
100
  resetScrollKey,
100
101
  onRowActivate,
102
+ onAddTicker,
101
103
  rootHeight,
102
104
  }: {
103
105
  columns: ColumnConfig[];
@@ -117,6 +119,8 @@ export function PortfolioTickerTable({
117
119
  visibleRangeBuffer?: number;
118
120
  resetScrollKey?: unknown;
119
121
  onRowActivate?: (ticker: TickerRecord) => void;
122
+ /** Opens the add row; the empty table offers it as a button. */
123
+ onAddTicker?: () => void;
120
124
  rootHeight?: number;
121
125
  }) {
122
126
  const cellCacheRef = useRef(createRowValueCache<string, ReturnType<typeof getColumnValue>>(5000));
@@ -175,6 +179,8 @@ export function PortfolioTickerTable({
175
179
  resetScrollKey={resetScrollKey}
176
180
  onRowActivate={onRowActivate}
177
181
  rowMenuAccelerators={PORTFOLIO_ROW_MENU_KEYS}
182
+ onAddTicker={onAddTicker}
183
+ addTickerShortcut={onAddTicker ? QUICK_ADD_KEY : undefined}
178
184
  rootHeight={rootHeight}
179
185
  />
180
186
  );
@@ -2,6 +2,8 @@ import { apiClient } from "../../../api-client";
2
2
  import type { PowerBoard, PowerDetail, PowerFilter, PowerHistory, PowerProject } from "../../../api-client/power";
3
3
  import { createPluginCache } from "../../../data/plugin-cache";
4
4
  import { loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
5
+ import { powerTickerScopes, type PowerTab, type PowerTickerScope } from "./model";
6
+ import { powerQuery } from "./query";
5
7
 
6
8
  const cacheOptions = { source: "gloom-cloud", schemaVersion: 1, policy: { staleMs: 60 * 60_000, expireMs: 30 * 86_400_000 } };
7
9
  export const powerBoardCache = createPluginCache<PowerBoard>({ ...cacheOptions, kind: "power-board" });
@@ -88,6 +90,20 @@ export async function fetchAllPowerHistory(filter: PowerFilter, client: Pick<Pow
88
90
  }
89
91
  return { ...first, points, hasMore: false, nextOffset: null };
90
92
  }
93
+ /**
94
+ * How many records name a ticker on each other tab, for a tab that has none:
95
+ * one single-row page per scope, read for its total, which a free preview
96
+ * reports in full. A scope that fails to answer has no total rather than
97
+ * failing the tab it explains.
98
+ */
99
+ export async function fetchPowerTickerScopes(symbol: string, tab: PowerTab, context: string,
100
+ client: Pick<PowerApi, "getCloudPowerBoard"> = apiClient, signal?: AbortSignal): Promise<PowerTickerScope[]> {
101
+ return Promise.all(powerTickerScopes(tab, context).map((scope) => {
102
+ const count = () => fetchPowerBoard({ ...powerQuery({ tab: scope.tab, context: scope.context }, symbol), limit: 1 }, client, signal).then((board) => board.total);
103
+ // One retry: a count is cheap, and the first of several at once is the one most often refused.
104
+ return count().catch(() => count()).then((total) => ({ ...scope, total }), () => ({ ...scope, total: null }));
105
+ }));
106
+ }
91
107
  const key = (query: unknown, scope: string) => `${scope}:${JSON.stringify(query)}`;
92
108
  export const loadPowerBoard = (filter: PowerFilter, scope: string, force = false, signal?: AbortSignal) =>
93
109
  loadCloudResource(powerBoardCache, key(filter, scope), () => fetchPowerBoard(filter, apiClient, signal), { force, validate: validatePowerBoard });
@@ -2,7 +2,7 @@ import type { PowerBoard, PowerHistory } from "../../../api-client/power";
2
2
  import type { PaneScreenshotEvidenceHook } from "../../../cli/pane-functions/screenshot-evidence";
3
3
  import { useRemoteUiNode } from "../../../remote/semantic-tree";
4
4
  import { isRecord } from "../../../utils/guards";
5
- import { fetchPowerBoard, fetchAllPowerHistory, validatePowerBoard, validatePowerHistory } from "./client";
5
+ import { fetchPowerBoard, fetchAllPowerHistory, fetchPowerTickerScopes, validatePowerBoard, validatePowerHistory } from "./client";
6
6
  import { otherRows, powerTab, projectRows } from "./model";
7
7
  import { powerQuery } from "./query";
8
8
  interface PowerEvidence {
@@ -29,7 +29,11 @@ export const powerScreenshotEvidence: PaneScreenshotEvidenceHook<PowerEvidence>
29
29
  const filter = powerQuery({ ...settings, ...resolved.options, tab }, symbol);
30
30
  const board = await fetchPowerBoard({ ...filter, limit: 100 });
31
31
  const history = tab === "history" ? await fetchAllPowerHistory(filter) : undefined;
32
- return { settings: { powerSnapshot: { board, ...(history ? { history } : {}) } }, financials: [] };
32
+ // A ticker with nothing on this tab: the counts that say where its records are, captured with the rest.
33
+ const empty = !!symbol && tab !== "coverage" && !(tab === "history" ? history?.points.length : tab === "outcomes" ? board.rates.length
34
+ : tab === "utilities" ? board.exposure.length : board.projects.length);
35
+ const tickerScopes = empty && symbol ? await fetchPowerTickerScopes(symbol, tab, String(settings.context ?? resolved.options.context ?? "capacity")) : undefined;
36
+ return { settings: { powerSnapshot: { board, ...(history ? { history } : {}), ...(tickerScopes ? { tickerScopes } : {}) } }, financials: [] };
33
37
  },
34
38
  read(value) {
35
39
  if (!isRecord(value) || value.kind !== "power" || value.version !== 1 || value.complete !== true || !isRecord(value.board)
@@ -1,11 +1,50 @@
1
- import type { HeadlessPaneDefinition } from "../../../types/plugin";
2
- import { fetchPowerBoard, fetchAllPowerHistory } from "./client";
1
+ import type { DataTableColumn } from "../../../components";
2
+ import type { HeadlessPaneColumn, HeadlessPaneDefinition, HeadlessPaneRow } from "../../../types/plugin";
3
+ import { withoutQuietColumns } from "../shared/research-cells";
4
+ import { fetchPowerBoard, fetchAllPowerHistory, fetchPowerTickerScopes } from "./client";
3
5
  import { powerQuery } from "./query";
4
- import { POWER_TABS, powerTab } from "./model";
6
+ import {
7
+ COVERAGE_COLUMNS, EXPOSURE_COLUMNS, GENERATION_COLUMNS, HISTORY_COLUMNS, POWER_TABS, PROJECT_COLUMNS, RATE_COLUMNS, UTILITY_COLUMNS,
8
+ historyRows, powerCoverageNote, powerLoadedSources, powerNumber, powerPercent, powerRegion, powerScopeOptions, powerTab, powerTickerEmpty, projectRows, tickerLabel, titleCase, UNMAPPED, type PowerTab,
9
+ } from "./model";
10
+
11
+ /** Bars the pane draws beside the numbers they are made of; the report has the numbers. */
12
+ const GRAPHIC_COLUMNS = new Set(["outcome", "pipeline"]);
13
+ /** Columns that only repeat a default on every row, as the pane leaves them out. */
14
+ const QUIET_COLUMNS: Record<string, readonly string[]> = { scope: ["Project"], unknownCapacity: ["0"], unknown: ["0"], fuel: ["Load"], role: ["Direct"], kind: ["Queue"] };
15
+ const text = (value: unknown) => value == null || value === "" ? "-" : String(value);
16
+ const coded = (value: unknown) => typeof value === "string" && value ? titleCase(value) : "-";
17
+
18
+ /**
19
+ * The pane's columns for a tab, printed as the pane prints them: MW and MWh
20
+ * to a tenth with thousands separators, rates as percentages, register codes
21
+ * by name, and each record's listed company or "unmapped". Raw metrics and
22
+ * evidence stay in the exported rows, out of the table.
23
+ */
24
+ function reportColumns(tab: PowerTab, context: unknown): HeadlessPaneColumn[] {
25
+ const columns: DataTableColumn[] = tab === "capacity" && context === "generation" ? GENERATION_COLUMNS : tab === "capacity" && context === "utility" ? UTILITY_COLUMNS
26
+ : ["queue", "loads", "capacity"].includes(tab) ? PROJECT_COLUMNS : tab === "history" ? HISTORY_COLUMNS : tab === "outcomes" ? RATE_COLUMNS : tab === "utilities" ? EXPOSURE_COLUMNS : COVERAGE_COLUMNS;
27
+ // The coverage list names its register by its own id; everything else by the record's source.
28
+ const region = (value: unknown, row: HeadlessPaneRow) => typeof value !== "string" ? "-"
29
+ : tab === "utilities" ? value : powerRegion(value, String((tab === "coverage" ? row.id : row.sourceId) ?? ""));
30
+ const format = (column: DataTableColumn): HeadlessPaneColumn["format"] => {
31
+ if (column.id === "region") return region;
32
+ if (["fuel", "status", "kind", "role"].includes(column.id)) return coded;
33
+ if (column.id === "ticker") return (value) => typeof value === "string" && value ? value : UNMAPPED;
34
+ if (column.id === "developer") return (value, row) => text(value ?? row.utility);
35
+ if (column.id === "observedAt") return (value) => typeof value === "string" ? value.slice(0, 16).replace("T", " ") : "-";
36
+ if (/Rate$/.test(column.id)) return (value) => typeof value === "number" ? powerPercent(value) : "-";
37
+ if (column.align === "right") return (value) => typeof value === "number" ? `${column.id === "changeMw" && value > 0 ? "+" : ""}${powerNumber(value)}` : "-";
38
+ return text;
39
+ };
40
+ // The pane groups outcomes under region headings; the report names the region on each row.
41
+ return [...(tab === "outcomes" ? [{ id: "region", label: "Region", width: 16, align: "left" as const }] : []), ...columns].filter((column) => !GRAPHIC_COLUMNS.has(column.id))
42
+ .map((column) => ({ key: column.id, header: column.label, align: column.align === "right" ? "right" as const : "left" as const, format: format(column) }));
43
+ }
5
44
 
6
45
  export const powerHeadless: HeadlessPaneDefinition<"bundle"> = {
7
46
  shape: "bundle",
8
- freshness: { source: "Grid operator queues and utility filings", status: "not-a-feed", basis: "published records" },
47
+ freshness: { source: "Grid operator queues and utility filings", status: "not-a-feed", basis: "published records", dataset: true },
9
48
  argument: { kind: "free-text", optional: true, placeholder: "ticker", description: "Optional listed developer or utility ticker, with exchange suffix where needed." },
10
49
  options: [
11
50
  { key: "tab", type: "enum", settingKey: "tab", defaultValue: "queue", values: POWER_TABS.map(({ value }) => ({ value })), description: "Queue, history, outcomes, large loads, utility exposure, capacity or coverage." },
@@ -36,10 +75,35 @@ export const powerHeadless: HeadlessPaneDefinition<"bundle"> = {
36
75
  projects.push(...page.projects); offset = page.nextOffset; hasMore = page.hasMore;
37
76
  }
38
77
  const history = tab === "history" ? await fetchAllPowerHistory(filter, ctx.apiClient) : null;
39
- const rows = tab === "history" ? history!.points : tab === "outcomes" ? board.rates : tab === "utilities" ? board.exposure : tab === "coverage" ? board.coverage : projects;
78
+ // Exported rows keep every field the server sent; the pane's derived cells (the ticker map, location, scope) ride beside them.
79
+ const cells = projectRows(projects);
80
+ const rows: HeadlessPaneRow[] = tab === "history" ? historyRows(history!.points).map((point) => ({ ...point, date: point.basis === "published" ? point.period : point.observedAt.slice(0, 16).replace("T", " ") }))
81
+ : tab === "outcomes" ? board.rates.map((rate) => ({ ...rate }))
82
+ : tab === "utilities" ? board.exposure.map((exposure) => ({ ...exposure, ticker: tickerLabel(exposure) }))
83
+ : tab === "coverage" ? board.coverage.map((coverage) => ({ ...coverage }))
84
+ : projects.map((project, index) => {
85
+ const { ticker, location, scope, generationMwh, summerPeakDemandMw, winterPeakDemandMw, salesMwh, revenueUsd } = cells[index]!.cells;
86
+ return { ...project, ticker, location, scope, generationMwh, summerPeakDemandMw, winterPeakDemandMw, salesMwh, revenueUsd };
87
+ });
40
88
  const locked = history?.locked ?? (tab === "outcomes" ? board.locked.rates : tab === "utilities" ? board.locked.exposure : tab === "coverage" ? 0 : board.locked.projects);
41
- return { complete: locked === 0, sections: [{ title: POWER_TABS.find((t) => t.value === tab)!.label, rows: rows.map((r) => ({ ...r })) }],
89
+ const columns = reportColumns(tab, args.options.context);
90
+ const shown = withoutQuietColumns(columns.map((column) => ({ id: column.key, label: column.header, width: 0, align: "left" as const })), rows,
91
+ (row, id) => columns.find((column) => column.key === id)!.format!(row[id], row), QUIET_COLUMNS).map((column) => column.id);
92
+ // A ticker with nothing on this tab says where its records are instead; a narrower filter is the filters' doing.
93
+ const narrowed = ["country", "region", "fuel", "status", "search", "sourceId", "from", "to", "loadClass", "historical"].some((key) => !!args.options[key]);
94
+ const empty = !rows.length && symbol && !narrowed && tab !== "coverage" ? await fetchPowerTickerScopes(symbol, tab, String(args.options.context ?? "capacity"), ctx.apiClient)
95
+ .then((scopes) => {
96
+ const message = powerTickerEmpty(symbol, tab, String(args.options.context ?? "capacity"), scopes);
97
+ const where = message.found.map((scope) => `${powerNumber(scope.total)} ${scope.label} (${powerScopeOptions(scope)})`).join(", ");
98
+ const loaded = powerLoadedSources(tab, board.coverage);
99
+ return [message.title, where ? `${symbol} has ${where}.` : message.hint?.replace(/\.$/, " (--search <name>)."),
100
+ loaded ? `Sources loaded now: ${loaded}.` : null].filter(Boolean).join(" ");
101
+ }) : "No reported records match these filters.";
102
+ // Missing or unsupported sources are coverage, not failures: one plain line, details in --tab coverage.
103
+ const coverageNote = tab === "capacity" ? null : powerCoverageNote(board.coverage);
104
+ return { complete: locked === 0, sections: [{ title: POWER_TABS.find((t) => t.value === tab)!.label, columns: columns.filter((column) => shown.includes(column.key)), rows, empty }],
42
105
  metadata: { generatedAt: board.generatedAt, access: board.access, locked, total: board.total, units: "MW unless specified", filters: filter, aggregates: board.aggregates, coverage: board.coverage },
43
- errors: [...(locked ? ["Additional records and history need Gloom Pro."] : []), ...board.coverage.filter((c) => c.status !== "current").map((c) => `${c.region}: ${c.status}${c.reason ? `: ${c.reason}` : ""}`)] };
106
+ ...(coverageNote ? { notes: [coverageNote] } : {}),
107
+ ...(locked ? { errors: ["Additional records and history need Gloom Pro."] } : {}) };
44
108
  },
45
109
  };
@@ -10,7 +10,10 @@ export const powerModule: PluginModule = {
10
10
  description: "Pro: interconnection queues, historical MW and outcomes, datacenter loads, utility exposure and generation capacity, with primary evidence.",
11
11
  keywords: ["power", "electricity", "interconnection", "grid", "datacenter", "utility", "renewables", "storage", "capacity"],
12
12
  shortcut: { prefix: "POWER", argKind: "text", argPlaceholder: "ticker", argOptional: true }, headless: powerHeadless,
13
- createInstance: (_context, options) => ({ placement: "floating", params: { symbol: options?.symbol ?? options?.arg?.trim().toUpperCase() ?? "" } }) }],
13
+ createInstance: (_context, options) => {
14
+ const symbol = options?.symbol ?? options?.arg?.trim().toUpperCase() ?? "";
15
+ return { placement: "floating", params: { symbol }, ...(symbol ? { title: `POWER ${symbol}` } : {}) };
16
+ } }],
14
17
  setup(ctx) { for (const cache of [powerBoardCache, powerHistoryCache, powerDetailCache]) cache.attach(ctx.persistence); },
15
18
  dispose() { for (const cache of [powerBoardCache, powerHistoryCache, powerDetailCache]) cache.reset(); },
16
19
  };
@@ -8,10 +8,81 @@ export const powerTab = (value: unknown): PowerTab => POWER_TABS.find((tab) => t
8
8
  const REGISTERS: Record<string, string> = { "neso-tec": "GB transmission", "neso-embedded": "GB embedded", "neso-interconnector": "GB interconnectors" };
9
9
  const REGIONS: Record<string, string> = { LBNL: "US project queues", "LBNL-ANNUAL": "US annual queues", EIA860: "US generation capacity", EIA861: "US utilities", EIA923: "US generation output" };
10
10
  export const powerRegion = (value: string, sourceId?: string) => REGISTERS[sourceId ?? ""] ?? REGIONS[value] ?? value;
11
+ /** A source with stored records, even when its latest refresh failed. */
12
+ const loadedSource = (c: PowerCoverage) => (c.status === "current" || c.status === "stale") && c.records > 0;
13
+ const names = (rows: readonly PowerCoverage[]) => [...new Set(rows.map((c) => c.region === "EirGrid" ? "Ireland" : c.region))].join(", ");
14
+ /**
15
+ * Which US grid operators' queues loaded, in plain words. Source errors and
16
+ * unsupported registers stay in the Coverage tab and on the server; a missing
17
+ * queue is a gap in coverage, never zero.
18
+ */
19
+ export function powerCoverageNote(coverage: readonly PowerCoverage[]): string | null {
20
+ const queues = coverage.filter((c) => c.country === "US" && c.role === "direct" && c.kind === "queue");
21
+ const missing = queues.filter((c) => !loadedSource(c));
22
+ if (!missing.length) return null;
23
+ const loaded = queues.filter(loadedSource);
24
+ return `US queues: ${loaded.length ? `${names(loaded)} loaded; ` : ""}${names(missing)} not available`;
25
+ }
26
+ /**
27
+ * The registers carrying records right now, named, for a ticker that has none
28
+ * in them: grid operator and TSO registers, or the EIA filings behind the
29
+ * Capacity tab. Null when nothing is loaded.
30
+ */
31
+ export function powerLoadedSources(tab: PowerTab, coverage: readonly PowerCoverage[]): string | null {
32
+ const loaded = coverage.filter((c) => loadedSource(c) && (tab === "capacity" ? c.role === "context" : c.role === "direct"));
33
+ if (!loaded.length) return null;
34
+ return tab === "capacity" ? "US EIA generation and utility filings" : names([...loaded.filter((c) => c.country === "US"), ...loaded.filter((c) => c.country !== "US")]);
35
+ }
11
36
  export const titleCase = (value: string) => value.replace(/(^|[ _-])\w/g, (match) => match.toUpperCase());
12
37
  export const powerNumber = (n: number | null | undefined) => n == null ? "--" : n.toLocaleString("en-US", { maximumFractionDigits: 1 });
13
38
  export const powerPercent = (n: number | null) => n == null ? "--" : `${(n * 100).toFixed(1)}%`;
14
- const tickerLabel = (row: { entities: PowerProject["entities"] }) => [...new Set(row.entities.flatMap((e) => e.tickers.map((t) => t.ticker)))].join(", ") || "--";
39
+ /** The listed companies a record names, or null when its register names none: the ticker map. */
40
+ export const tickerLabel = (row: { entities: PowerProject["entities"] }) => [...new Set(row.entities.flatMap((e) => e.tickers.map((t) => t.ticker)))].join(", ") || null;
41
+ /** What the ticker column says for a record no listed company is mapped to. */
42
+ export const UNMAPPED = "unmapped";
43
+
44
+ /** Where else a ticker's records can sit: each tab, and the capacity tab's contexts, with what they list. */
45
+ const POWER_TICKER_SCOPES = [
46
+ { tab: "queue", context: "capacity", label: "interconnection queue records", view: "Queue" },
47
+ { tab: "loads", context: "capacity", label: "large-load requests", view: "Loads" },
48
+ { tab: "capacity", context: "capacity", label: "generation capacity records", view: "Capacity" },
49
+ { tab: "capacity", context: "generation", label: "monthly generation records", view: "Generation" },
50
+ { tab: "capacity", context: "utility", label: "utility sales records", view: "Utility sales" },
51
+ ] as const satisfies readonly { tab: PowerTab; context: string; label: string; view: string }[];
52
+ /** `total` is null for a scope that did not answer, which is no evidence either way. */
53
+ export type PowerTickerScope = (typeof POWER_TICKER_SCOPES)[number] & { total: number | null };
54
+ const sameScope = (scope: { tab: string; context: string }, tab: PowerTab, context: string) =>
55
+ scope.tab === tab && (tab !== "capacity" || scope.context === context)
56
+ // Utilities are the large loads summed by utility: a ticker with no loads has no exposure either.
57
+ || tab === "utilities" && scope.tab === "loads";
58
+ /** The scopes to count a ticker's records in when the open tab has none of them. */
59
+ export const powerTickerScopes = (tab: PowerTab, context: string) => POWER_TICKER_SCOPES.filter((scope) => !sameScope(scope, tab, context));
60
+
61
+ /** What the open tab lists, for the sentence that says none of it maps to a ticker. */
62
+ function powerTabRecords(tab: PowerTab, context: string): string {
63
+ if (tab === "capacity") return POWER_TICKER_SCOPES.find((scope) => scope.tab === "capacity" && scope.context === context)?.label ?? "capacity records";
64
+ return { queue: "interconnection queue records", history: "queue history records", outcomes: "queue outcomes", loads: "large-load requests",
65
+ utilities: "large-load requests or utility exposure", coverage: "records" }[tab];
66
+ }
67
+
68
+ /**
69
+ * Why a ticker's tab is empty, and where its records are instead: the title
70
+ * says what does not map to it, the hint where it has records, or, once every
71
+ * other tab has answered with none, that a name search is what is left.
72
+ */
73
+ export function powerTickerEmpty(symbol: string, tab: PowerTab, context: string, scopes: readonly PowerTickerScope[]) {
74
+ const found = scopes.filter((scope): scope is PowerTickerScope & { total: number } => (scope.total ?? 0) > 0);
75
+ const answered = scopes.length > 0 && scopes.every((scope) => scope.total != null);
76
+ return {
77
+ title: `No ${powerTabRecords(tab, context)} map to ${symbol}.`,
78
+ found,
79
+ hint: found.length ? `${symbol} has ${found.map((scope) => `${powerNumber(scope.total)} ${scope.label}`).join(", ")}.`
80
+ : answered ? `No power records map to ${symbol}; search by the company's or a subsidiary's name instead.` : null,
81
+ };
82
+ }
83
+ /** The options that open a scope from the command line. */
84
+ export const powerScopeOptions = (scope: { tab: string; context: string }) =>
85
+ scope.tab === "capacity" && scope.context !== "capacity" ? `--tab capacity --context ${scope.context}` : `--tab ${scope.tab}`;
15
86
  const historyKey = (p: PowerHistoryPoint) => JSON.stringify([p.sourceId, p.country, p.region, p.kind, p.fuel, p.status, p.historical, p.basis]);
16
87
  /** Annual benchmarks use their stated period. Current queues use when we observed them, never a proposed completion date. */
17
88
  export function historyDate(p: PowerHistoryPoint): Date {