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
@@ -1,7 +1,11 @@
1
1
  import type { GpuObservation } from "../../../api-client/gpu";
2
- import type { HeadlessPaneDefinition } from "../../../types/plugin";
3
- import { fetchGpuBoard, fetchGpuEvents, fetchGpuHistory, loadGpuEquityHistory } from "./client";
4
- import { GPU_TABS, gpuArgument, gpuBasisLabel, gpuChange, gpuEquityRows, gpuEventDate, gpuProvenanceLabel, gpuLabel, gpuRows, gpuSource, gpuTab } from "./model";
2
+ import type { HeadlessPaneDefinition, HeadlessPaneRow } from "../../../types/plugin";
3
+ import { formatUtcTime } from "../../../utils/utc-time";
4
+ import { fetchGpuBoard, fetchGpuEvents, fetchGpuHistory, fetchGpuModelPreviews, gpuPreviewModels, loadGpuEquityHistory } from "./client";
5
+ import {
6
+ GPU_TABS, gpuBasisLabel, gpuChange, gpuChangesLocked, gpuEquityGap, gpuEquityModels, gpuEquityRows, gpuEventDate, gpuFamilies, gpuFamily,
7
+ gpuLabel, gpuNoMatch, gpuPrice, gpuProvenanceLabel, gpuRows, gpuSource, gpuTab,
8
+ } from "./model";
5
9
  import { quoteFreshnessFields } from "../shared/report-freshness";
6
10
  import { getRegularSessionDisplay } from "../../../market-data/market/status";
7
11
 
@@ -9,69 +13,96 @@ const observationRow = (row: GpuObservation) => ({ gpu: gpuLabel(row), source: g
9
13
  price: row.pricePerGpuHr, availability: row.availability, observedAt: row.observedAt, effectiveAt: row.effectiveAt, provenance: row.provenance ?? "live", provenanceLabel: gpuProvenanceLabel(row, true),
10
14
  sourceUrl: row.sourceUrl ?? null, evidenceUrl: row.evidenceUrl ?? null });
11
15
  const percent = (value: unknown) => gpuChange(typeof value === "number" ? value : null);
16
+ /** Prices to the cent and times to the minute, as the pane draws them; exports keep the full value. */
17
+ const price = (value: unknown) => typeof value === "number" ? gpuPrice(value) : "-";
18
+ const minute = (value: unknown) => typeof value === "string" ? formatUtcTime(value) : "-";
19
+ /** A move the free preview leaves to Pro says so, instead of reading as missing. */
20
+ const change = (value: unknown, row: HeadlessPaneRow) => row.changesLocked === true ? "Pro" : percent(value);
12
21
  const columns = [
13
22
  { key: "gpu", header: "GPU" }, { key: "source", header: "Source" }, { key: "basis", header: "Basis" },
14
- { key: "price", header: "$/GPU-hr", align: "right" as const },
15
- { key: "change1d", header: "1D", align: "right" as const, format: percent }, { key: "change7d", header: "7D", align: "right" as const, format: percent },
16
- { key: "change30d", header: "30D", align: "right" as const, format: percent }, { key: "availability", header: "Avail" }, { key: "observedAt", header: "As of" },
23
+ { key: "price", header: "$/GPU-hr", align: "right" as const, format: price },
24
+ { key: "change1d", header: "1D", align: "right" as const, format: change }, { key: "change7d", header: "7D", align: "right" as const, format: change },
25
+ { key: "change30d", header: "30D", align: "right" as const, format: change }, { key: "availability", header: "Avail" },
26
+ { key: "observedAt", header: "As of", format: minute },
17
27
  ];
18
28
 
19
29
  export const gpuHeadless: HeadlessPaneDefinition<"bundle"> = {
20
- shape: "bundle", argument: { kind: "free-text", optional: true, placeholder: "GPU", description: "GPU model, such as H100 or B200." },
30
+ // Observed rental prices; the Equities tab's quotes keep their market line.
31
+ shape: "bundle", freshness: { dataset: true }, argument: { kind: "free-text", optional: true, placeholder: "GPU", description: "GPU model, such as H100, h-100 or B200." },
21
32
  options: [{ key: "tab", type: "enum", values: GPU_TABS.map(({ value }) => ({ value })), defaultValue: "board", description: "Board, sourced history, dated changes or related equities." }],
22
33
  discovery: { aliases: ["GPU"], dataRequirements: ["GPU rental price observations"],
23
34
  limitations: ["List prices, provider-declared spot and asks are distinct bases",
24
35
  "Hyperscaler and Neocloud rows are list indexes: the median of the providers' list prices linked over time, so a provider joining or leaving causes no jump; sample.rawMedian is the raw median of today's providers and can differ slightly",
25
- "1D/7D/30D need sufficient observation history; unavailable changes remain null"] },
36
+ "1D/7D/30D need sufficient observation history; unavailable changes remain null",
37
+ "Free preview: three board rows, or the latest rows of the GPU asked for; Pro unlocks every provider, the moves and the history"] },
26
38
  describe: "GPU rental prices in USD per GPU-hour",
27
39
  async load(args, ctx) {
28
- const argument = Array.isArray(args.argument) ? args.argument.join(" ") : args.argument;
29
- const model = gpuArgument(argument);
40
+ const argument = (Array.isArray(args.argument) ? args.argument.join(" ") : args.argument) ?? "";
30
41
  const tab = gpuTab(args.options.tab);
31
42
  const board = await fetchGpuBoard(ctx.apiClient);
43
+ const families = gpuFamilies(board.rows);
44
+ const model = argument.trim() ? gpuFamily(argument, families) : "";
45
+ if (model === null) throw new Error(gpuNoMatch(argument, families));
32
46
  const metadata = { asOf: board.asOf, stale: board.stale, complete: board.status === "available" && !board.access?.locked, unit: "USD/GPU-hour", access: board.access,
33
47
  methodology: "docs/gpu-rental-prices.md#list-indexes" };
34
48
  // Prices observed on providers' pages, not a feed; each board row says whether its source has gone quiet.
35
49
  const freshness = { source: "GPU cloud providers", status: "not-a-feed" as const, basis: "observed prices" };
36
50
  if (tab === "history") {
37
51
  const history = await fetchGpuHistory({ ...(model ? { gpuModel: model } : {}), limit: 10_000 }, ctx.apiClient);
38
- return { sections: [{ title: "Dated observations", columns: [...columns.filter((column) => !column.key.startsWith("change")), { key: "provenanceLabel", header: "Record" }], rows: history.points.map(observationRow) }], freshness: { ...freshness, oldest: null }, complete: !history.access?.locked, metadata: { ...metadata, access: history.access ?? board.access }, errors: board.gaps };
52
+ return { sections: [{ title: "Dated observations", columns: [...columns.filter((column) => !column.key.startsWith("change")), { key: "provenanceLabel", header: "Record" }], rows: history.points.map(observationRow),
53
+ empty: history.access?.locked ? "Full history is available with Pro." : `No ${model || "GPU"} observations yet.` }],
54
+ freshness: { ...freshness, oldest: null }, complete: !history.access?.locked, metadata: { ...metadata, access: history.access ?? board.access }, errors: board.gaps };
39
55
  }
40
56
  if (tab === "changes") {
41
57
  const payload = await fetchGpuEvents(model || undefined, ctx.apiClient);
42
58
  return { sections: [{ title: "Price, availability and membership changes", columns: [
43
- { key: "date", header: "Date" }, { key: "gpu", header: "GPU" }, { key: "source", header: "Source" }, { key: "basis", header: "Basis" },
44
- { key: "oldPrice", header: "Old $/GPU-hr" }, { key: "newPrice", header: "New $/GPU-hr" }, { key: "changePct", header: "Change", format: percent }, { key: "kind", header: "Event" }, { key: "provenanceLabel", header: "Record" },
59
+ { key: "date", header: "Date", format: minute }, { key: "gpu", header: "GPU" }, { key: "source", header: "Source" }, { key: "basis", header: "Basis" },
60
+ { key: "oldPrice", header: "Old $/GPU-hr", align: "right" as const, format: price }, { key: "newPrice", header: "New $/GPU-hr", align: "right" as const, format: price },
61
+ { key: "changePct", header: "Change", align: "right" as const, format: percent }, { key: "kind", header: "Event" }, { key: "provenanceLabel", header: "Record" },
45
62
  { key: "oldAvailability", header: "Old availability" }, { key: "newAvailability", header: "New availability" },
46
63
  ], rows: payload.events.map((event) => ({ date: gpuEventDate(event), observedAt: event.observedAt,
47
64
  effectiveAt: event.effectiveAt, origin: event.origin ?? "observed", gpu: gpuLabel(event), source: gpuSource(event), basis: gpuBasisLabel(event.basis),
48
65
  oldPrice: event.oldPrice, newPrice: event.newPrice, changePct: event.changePct, kind: event.kind,
49
66
  oldMembers: event.oldMembers, newMembers: event.newMembers, provenance: event.provenance ?? "live",
50
67
  provenanceLabel: gpuProvenanceLabel(event, true), evidenceUrl: event.evidenceUrl ?? null, sourceUrl: event.sourceUrl ?? null,
51
- oldAvailability: event.oldAvailability ?? null, newAvailability: event.newAvailability ?? null })) }], freshness: { ...freshness, oldest: null }, complete: !payload.access?.locked, metadata: { ...metadata, access: payload.access ?? board.access }, errors: board.gaps };
68
+ oldAvailability: event.oldAvailability ?? null, newAvailability: event.newAvailability ?? null })),
69
+ empty: payload.access?.locked ? "Full change history is available with Pro." : "No price or availability changes recorded yet." }],
70
+ freshness: { ...freshness, oldest: null }, complete: !payload.access?.locked, metadata: { ...metadata, access: payload.access ?? board.access }, errors: board.gaps };
52
71
  }
72
+ const selected = tab === "equities" ? model || "H100" : model;
73
+ const previews = await fetchGpuModelPreviews(gpuPreviewModels(board, tab === "equities" ? gpuEquityModels(selected) : [selected]), ctx.apiClient);
74
+ const rows = [...board.rows, ...previews];
75
+ const previewed = [...new Set(previews.map((row) => row.gpuModel))];
76
+ const notes = previewed.length ? [`${previewed.join(" and ")} ${previewed.length > 1 ? "are" : "is"} not on the free board: ${previewed.length > 1 ? "their" : "its"} latest free rows stand in. Pro adds every provider and the 1D, 7D and 30D moves.`]
77
+ : board.access?.locked && !model && tab === "board" ? ["The free board shows three rows; name a GPU (GPU H100) for its latest free rows, or upgrade to Pro for every GPU."] : [];
53
78
  if (tab === "equities") {
54
- const related = gpuEquityRows(board.rows, model || "H100");
79
+ const related = gpuEquityRows(rows, selected, !!board.access?.locked);
55
80
  const histories = await loadGpuEquityHistory(ctx.marketData);
56
- const rows = await Promise.all(related.map(async (row) => {
81
+ const equities = await Promise.all(related.map(async (row) => {
57
82
  const quote = await ctx.marketData.getQuote(row.symbol, row.exchange).catch(() => null);
58
83
  const history = histories.find((entry) => entry.symbol === row.symbol);
59
84
  // The regular session's last and move, as the pane's board shows them.
60
85
  const headline = getRegularSessionDisplay(quote);
86
+ const reference = row.reference;
61
87
  return { symbol: row.symbol, role: row.role, price: headline?.price ?? null, change1d: headline?.changePercent ?? null,
62
88
  change5d: history?.value ?? null, fiveDayAsOf: history?.asOf ?? null, quoteAsOf: quote?.lastUpdated ? new Date(quote.lastUpdated).toISOString() : null,
63
89
  ...quoteFreshnessFields(quote),
64
- gpu: row.reference ? gpuLabel(row.reference) : row.gpuModel, gpuSource: row.reference ? gpuSource(row.reference) : null, gpuChange7d: row.reference?.change7d ?? null };
90
+ gpu: reference ? gpuLabel(reference) : row.gpuModel, gpuSource: reference ? gpuSource(reference) : null, gpuPrice: reference?.pricePerGpuHr ?? null,
91
+ gpuChange7d: reference?.change7d ?? null, gpuLocked: row.gap === "pro" || (!!reference && gpuChangesLocked(reference)), gpuGap: gpuEquityGap(row) };
65
92
  }));
93
+ const locked = (value: unknown, row: HeadlessPaneRow, show: (value: unknown) => string) => value == null && row.gpuLocked === true ? "Pro" : show(value);
66
94
  return { sections: [{ title: "Related equities", columns: [
67
- { key: "symbol", header: "Ticker" }, { key: "role", header: "Related through" }, { key: "price", header: "Last $" },
68
- { key: "change1d", header: "1D", format: percent }, { key: "change5d", header: "5D", format: percent },
69
- { key: "gpu", header: "GPU" }, { key: "gpuSource", header: "GPU list series" }, { key: "gpuChange7d", header: "GPU 7D", format: percent },
70
- ], rows }], complete: !board.access?.locked, metadata, errors: [...board.gaps, ...histories.flatMap((entry) => entry.error ? [entry.error] : [])],
71
- unavailableSymbols: rows.filter((row) => row.price == null || row.change5d == null).map((row) => row.symbol) };
95
+ { key: "symbol", header: "Ticker" }, { key: "role", header: "Related through" }, { key: "price", header: "Last $", align: "right" as const, format: price },
96
+ { key: "change1d", header: "1D", align: "right" as const, format: percent }, { key: "change5d", header: "5D", align: "right" as const, format: percent },
97
+ { key: "gpu", header: "GPU" }, { key: "gpuSource", header: "GPU list series", format: (value: unknown, row: HeadlessPaneRow) => typeof value === "string" ? value : String(row.gpuGap ?? "-") },
98
+ { key: "gpuPrice", header: "$/GPU-hr", align: "right" as const, format: (value: unknown, row: HeadlessPaneRow) => locked(value, row, price) },
99
+ { key: "gpuChange7d", header: "GPU 7D", align: "right" as const, format: (value: unknown, row: HeadlessPaneRow) => locked(value, row, percent) },
100
+ ], rows: equities }], complete: !board.access?.locked, metadata, notes, errors: [...board.gaps, ...histories.flatMap((entry) => entry.error ? [entry.error] : [])],
101
+ unavailableSymbols: equities.filter((row) => row.price == null || row.change5d == null).map((row) => row.symbol) };
72
102
  }
73
- const rows = gpuRows(board.rows, model).map((row) => ({ ...observationRow(row), change1d: row.change1d, change7d: row.change7d,
74
- change30d: row.change30d, stale: row.stale, sample: row.stats ?? null }));
75
- return { sections: [{ title: "GPU rental prices", columns, rows }], freshness, complete: !board.access?.locked, metadata, errors: board.gaps };
103
+ const boardRows = gpuRows(rows, model).map((row) => ({ ...observationRow(row), change1d: row.change1d, change7d: row.change7d,
104
+ change30d: row.change30d, stale: row.stale, sample: row.stats ?? null, ...(gpuChangesLocked(row) ? { changesLocked: true } : {}) }));
105
+ return { sections: [{ title: "GPU rental prices", columns, rows: boardRows, empty: `No ${model ? `${model} ` : ""}rental prices have been observed yet.` }],
106
+ freshness, complete: !board.access?.locked, metadata, notes, errors: board.gaps };
76
107
  },
77
108
  };
@@ -92,8 +92,8 @@ function HistoryDetail({ row, peers, width, height, refreshVersion }: {
92
92
  </PaneStatusBody>;
93
93
  }
94
94
 
95
- export function GpuHistory({ rows, model, setModel, selectedId, onSelect, reloadBoard, width, height, focused }: {
96
- rows: GpuBoardRow[]; model: string; setModel: (value: string) => void; selectedId: string; onSelect: (id: string) => void;
95
+ export function GpuHistory({ rows, models: boardModels, model, setModel, selectedId, onSelect, reloadBoard, width, height, focused }: {
96
+ rows: GpuBoardRow[]; models: readonly string[]; model: string; setModel: (value: string) => void; selectedId: string; onSelect: (id: string) => void;
97
97
  reloadBoard: () => void; width: number; height: number; focused: boolean;
98
98
  }) {
99
99
  const colors = useThemeColors();
@@ -106,7 +106,7 @@ export function GpuHistory({ rows, model, setModel, selectedId, onSelect, reload
106
106
  notices: [references.error, references.data?.refreshError].filter((value): value is string => !!value) });
107
107
  const allRows = useMemo(() => [...rows, ...gpuReferenceRows(references.data?.payload.points ?? [])], [rows, references.data]);
108
108
  // History always reads one model: the one picked on the board, else the selected series\' model.
109
- const models = useMemo(() => [...new Set(allRows.map((row) => row.gpuModel))].sort(), [allRows]);
109
+ const models = useMemo(() => [...new Set([...boardModels, ...allRows.map((row) => row.gpuModel)])].sort(), [boardModels, allRows]);
110
110
  const activeModel = model || allRows.find((row) => row.id === selectedId)?.gpuModel || (models.includes("H100") ? "H100" : models[0] ?? GPU_MODELS[0]!);
111
111
  const sections = useMemo(() => gpuBoardSections(allRows, activeModel).filter((section) => basis === "all" || section.basis === basis), [allRows, activeModel, basis]);
112
112
  const ordered = useMemo(() => sections.flatMap((section) => section.rows), [sections]);
@@ -3,6 +3,7 @@ import { gpuBoardCache, gpuEventsCache, gpuHistoryCache } from "./client";
3
3
  import { gpuHeadless } from "./headless";
4
4
  import { GPU_MODELS, gpuArgument } from "./model";
5
5
  import { GpuPane } from "./pane";
6
+ import { GPU_SERIES } from "./series";
6
7
 
7
8
  export const gpuModule: PluginModule = {
8
9
  panes: [{ id: "gpu", name: "GPU Rental Prices", icon: "G", component: GpuPane, defaultPosition: "right", defaultMode: "floating",
@@ -13,6 +14,7 @@ export const gpuModule: PluginModule = {
13
14
  shortcut: { prefix: "GPU", argKind: "text", argPlaceholder: "GPU", argOptional: true,
14
15
  argOptions: () => GPU_MODELS.map((value) => ({ value, label: value })) }, headless: gpuHeadless,
15
16
  createInstance: (_context, options) => ({ placement: "floating", params: { gpuModel: gpuArgument(options?.arg) } }) }],
17
+ series: GPU_SERIES,
16
18
  setup(ctx) { for (const cache of [gpuBoardCache, gpuHistoryCache, gpuEventsCache]) cache.attach(ctx.persistence); },
17
19
  dispose() { for (const cache of [gpuBoardCache, gpuHistoryCache, gpuEventsCache]) cache.reset(); },
18
20
  };
@@ -10,6 +10,43 @@ export type GpuTab = typeof GPU_TABS[number]["value"];
10
10
  export const gpuTab = (value: unknown): GpuTab => GPU_TABS.find((tab) => tab.value === value)?.value ?? "board";
11
11
  export const GPU_MODELS = ["H100", "H200", "B200", "B300", "GB200", "GB300", "A100", "MI300X", "MI355X", "L40S", "RTX PRO 6000"];
12
12
  export const gpuArgument = (value: string | null | undefined) => (value ?? "").trim().toUpperCase().replace(/\s+/g, " ");
13
+ const gpuModelRank = (name: string) => GPU_MODELS.indexOf(name) < 0 ? 99 : GPU_MODELS.indexOf(name);
14
+ /** Board order: the GPUs people rent most first, the rest by name. */
15
+ const gpuModelOrder = (a: string, b: string) => gpuModelRank(a) - gpuModelRank(b) || a.localeCompare(b);
16
+
17
+ /** Every GPU name a reader can ask for: the ones on this board and the ones the full board carries. */
18
+ export const gpuFamilies = (rows: readonly Pick<GpuObservation, "gpuModel">[]) =>
19
+ [...new Set([...GPU_MODELS, ...rows.map((row) => row.gpuModel)])].sort(gpuModelOrder);
20
+
21
+ /** A GPU name as typed, for comparison: case, spaces, dashes and underscores do not matter. */
22
+ const gpuKey = (value: string) => value.toLowerCase().replace(/[\s_-]+/g, "");
23
+
24
+ /**
25
+ * The GPU a typed name means, so "h100", "H-100" and "H100 80GB SXM" all
26
+ * read H100. A whole name wins over a longer one it begins ("A10" is not
27
+ * A100), and the most leading words that name a GPU win over fewer ("RTX PRO
28
+ * 6000"). Otherwise the first GPU, in board order, whose name begins with
29
+ * what was typed ("B2" is B200), or that begins it ("H100SXM"). Null when
30
+ * nothing matches.
31
+ */
32
+ export function gpuFamily(query: string, families: readonly string[]): string | null {
33
+ const words = query.trim().split(/[\s_-]+/).filter(Boolean);
34
+ if (!words.length) return null;
35
+ const ordered = [...new Set(families)].sort(gpuModelOrder);
36
+ for (let count = words.length; count > 0; count -= 1) {
37
+ const key = gpuKey(words.slice(0, count).join(""));
38
+ const exact = ordered.find((family) => gpuKey(family) === key);
39
+ if (exact) return exact;
40
+ }
41
+ const key = gpuKey(query);
42
+ return ordered.find((family) => gpuKey(family).startsWith(key))
43
+ ?? [...ordered].sort((a, b) => gpuKey(b).length - gpuKey(a).length).find((family) => key.startsWith(gpuKey(family)))
44
+ ?? null;
45
+ }
46
+
47
+ /** The sentence for a name that matches no GPU, naming the ones that do. */
48
+ export const gpuNoMatch = (query: string, families: readonly string[]) =>
49
+ `No GPU matches "${query.trim()}". GPUs: ${families.join(", ")}.`;
13
50
  export const gpuBasisLabel = (basis: GpuBasis, compact = false): string => compact
14
51
  ? ({ list: "List", spot: "Spot", ask: "Ask", reserved: "Rsvd", index: "Locked", reference: "Reference" })[basis]
15
52
  : ({ list: "List price", spot: "Provider-declared spot", ask: "Ask", reserved: "Reserved", index: "Index (licensed)", reference: "Reference" })[basis];
@@ -60,13 +97,31 @@ export function gpuTime(value: string | null | undefined, compact = false): stri
60
97
  return compact ? `${day} ${time}` : `${day}, ${date.getUTCFullYear()} ${time} UTC`;
61
98
  }
62
99
 
63
- export function gpuRows(rows: readonly GpuBoardRow[], model = "", basis = "all"): GpuBoardRow[] {
100
+ export function gpuRows<T extends GpuBoardRow>(rows: readonly T[], model = "", basis = "all"): T[] {
64
101
  const rank = (row: GpuBoardRow) => row.providerClass === "aggregate" ? 0 : row.basis === "list" ? 1 : row.basis === "spot" ? 2 : row.basis === "reserved" ? 3 : 4;
65
- return rows.filter((row) => (!model || row.gpuModel === model) && (basis === "all" || row.basis === basis)).sort((a, b) => {
66
- const modelRank = (name: string) => GPU_MODELS.indexOf(name) < 0 ? 99 : GPU_MODELS.indexOf(name);
67
- return modelRank(a.gpuModel) - modelRank(b.gpuModel) || a.gpuModel.localeCompare(b.gpuModel) || rank(a) - rank(b)
68
- || gpuSource(a).localeCompare(gpuSource(b)) || (b.memoryGb ?? 0) - (a.memoryGb ?? 0) || (a.formFactor ?? "").localeCompare(b.formFactor ?? "");
69
- });
102
+ return rows.filter((row) => (!model || row.gpuModel === model) && (basis === "all" || row.basis === basis)).sort((a, b) =>
103
+ gpuModelOrder(a.gpuModel, b.gpuModel) || rank(a) - rank(b)
104
+ || gpuSource(a).localeCompare(gpuSource(b)) || (b.memoryGb ?? 0) - (a.memoryGb ?? 0) || (a.formFactor ?? "").localeCompare(b.formFactor ?? ""));
105
+ }
106
+
107
+ /** A row the free board leaves out, read from the free preview of its GPU: the latest price, with its moves left to Pro. */
108
+ export type GpuPreviewRow = GpuBoardRow & { changesLocked: true };
109
+ export const gpuChangesLocked = (row: GpuBoardRow): row is GpuPreviewRow => (row as Partial<GpuPreviewRow>).changesLocked === true;
110
+
111
+ /**
112
+ * The free board carries three rows of one GPU. For another GPU, the latest
113
+ * rows its free history preview serves stand in, one per series; reference
114
+ * indexes stay out, as they do on the board.
115
+ */
116
+ export function gpuPreviewRows(points: readonly GpuObservation[]): GpuPreviewRow[] {
117
+ const latest = new Map<string, GpuObservation>();
118
+ for (const point of points) {
119
+ if (point.basis === "reference" || point.provenance === "reference" || point.basis === "index") continue;
120
+ const id = `${point.source}:${point.skuKey}`;
121
+ if (!latest.has(id) || point.observedAt > latest.get(id)!.observedAt) latest.set(id, point);
122
+ }
123
+ return [...latest].map(([id, point]) => ({ ...point, id, label: gpuLabel(point), sourceLabel: gpuSource(point),
124
+ change1d: null, change7d: null, change30d: null, stale: false, lastError: null, changesLocked: true }));
70
125
  }
71
126
 
72
127
  /** The bases in board order. The licensed index never reaches the board. */
@@ -105,9 +160,8 @@ export interface GpuBoardSection { id: string; label: string; basis: GpuBasis; g
105
160
  * sections by basis alone; every model names the model in each heading.
106
161
  */
107
162
  export function gpuBoardSections(rows: readonly GpuBoardRow[], model = ""): GpuBoardSection[] {
108
- const modelRank = (name: string) => GPU_MODELS.indexOf(name) < 0 ? 99 : GPU_MODELS.indexOf(name);
109
163
  const ordered = rows.filter((row) => (!model || row.gpuModel === model) && row.basis !== "index").sort((a, b) =>
110
- modelRank(a.gpuModel) - modelRank(b.gpuModel) || a.gpuModel.localeCompare(b.gpuModel) || basisRank(a.basis) - basisRank(b.basis)
164
+ gpuModelOrder(a.gpuModel, b.gpuModel) || basisRank(a.basis) - basisRank(b.basis)
111
165
  || Number(gpuHeadline(b)) - Number(gpuHeadline(a)) || gpuSource(a).localeCompare(gpuSource(b))
112
166
  || (b.memoryGb ?? 0) - (a.memoryGb ?? 0) || (a.formFactor ?? "").localeCompare(b.formFactor ?? ""));
113
167
  const sections: GpuBoardSection[] = [];
@@ -321,18 +375,49 @@ export const GPU_EQUITY_GROUPS = [
321
375
  { label: "Neoclouds", roles: ["Neocloud"] }, { label: "Hosts and builders", roles: ["Host", "Server builder"] },
322
376
  ] as const;
323
377
 
324
- export function gpuEquityRows(rows: readonly GpuBoardRow[], selectedModel: string) {
378
+ const equityModel = (symbol: string, selectedModel: string) => symbol === "AMD" && !selectedModel.startsWith("MI") ? "MI300X"
379
+ : symbol === "NVDA" && selectedModel.startsWith("MI") ? "H100" : selectedModel;
380
+
381
+ /** The GPUs the Equities tab reads for one selected GPU: AMD reads its own chip, NVIDIA reads H100 beside an AMD chip. */
382
+ export const gpuEquityModels = (selectedModel: string) => [...new Set(GPU_EQUITIES.map(({ symbol }) => equityModel(symbol, selectedModel)))];
383
+
384
+ /**
385
+ * Each share beside the list series that ties it to GPU rents. A share with
386
+ * no series says why: `pro` when the free board leaves its series out,
387
+ * `unlisted` when the board has no such series at all.
388
+ */
389
+ export function gpuEquityRows(rows: readonly GpuBoardRow[], selectedModel: string, locked = false) {
325
390
  return GPU_EQUITIES.map((equity) => {
326
- const model = equity.symbol === "AMD" && !selectedModel.startsWith("MI") ? "MI300X"
327
- : equity.symbol === "NVDA" && selectedModel.startsWith("MI") ? "H100" : selectedModel;
391
+ const model = equityModel(equity.symbol, selectedModel);
328
392
  const candidates = gpuRows(rows, model, "list");
329
- const aggregates = candidates.filter((row) => row.providerClass === "aggregate").sort((a, b) =>
330
- Number(b.formFactor === "SXM") - Number(a.formFactor === "SXM") || (b.stats?.n ?? 0) - (a.stats?.n ?? 0) || (b.memoryGb ?? 0) - (a.memoryGb ?? 0));
331
- const reference = "provider" in equity ? candidates.find((row) => gpuSource(row) === equity.provider)
332
- : aggregates.find((row) => row.provider.startsWith("Neocloud")) ?? aggregates[0];
333
- return { ...equity, gpuModel: model, reference: reference ?? null };
393
+ const reference = ("provider" in equity ? candidates.find((row) => gpuSource(row) === equity.provider) : gpuListIndex(candidates, model)) ?? null;
394
+ const gap: "pro" | "unlisted" | null = reference ? null : locked ? "pro" : "unlisted";
395
+ return { ...equity, gpuModel: model, reference, gap };
334
396
  });
335
397
  }
398
+ export type GpuEquityRow = ReturnType<typeof gpuEquityRows>[number];
399
+
400
+ /**
401
+ * A GPU's list index: of its main variant (SXM first, then the most
402
+ * providers, then the most memory), the neocloud index, else the hyperscaler
403
+ * one. The series a share reads in Equities, and the catalog's GPU list indexes.
404
+ */
405
+ export function gpuListIndex<T extends GpuBoardRow>(rows: readonly T[], model: string): T | null {
406
+ const indexes = gpuRows(rows, model, "list").filter((row) => row.providerClass === "aggregate").sort((a, b) =>
407
+ Number(b.formFactor === "SXM") - Number(a.formFactor === "SXM") || (b.stats?.n ?? 0) - (a.stats?.n ?? 0) || (b.memoryGb ?? 0) - (a.memoryGb ?? 0));
408
+ return indexes.find((row) => row.provider.startsWith("Neocloud")) ?? indexes[0] ?? null;
409
+ }
410
+
411
+ /** The series a share would read, named when it has none: "AWS H100 list", "H100 list index". */
412
+ const gpuEquitySeries = (row: Pick<GpuEquityRow, "gpuModel"> & { provider?: string }) =>
413
+ row.provider ? `${row.provider} ${row.gpuModel} list` : `${row.gpuModel} list index`;
414
+
415
+ /**
416
+ * The series column of a share with no series: the one Pro unlocks (its price
417
+ * and move cells read Pro), or the one the board does not list.
418
+ */
419
+ export const gpuEquityGap = (row: GpuEquityRow) => row.gap === "pro" ? gpuEquitySeries(row)
420
+ : row.gap === "unlisted" ? `No ${gpuEquitySeries(row)}` : null;
336
421
 
337
422
  export function equityFiveDayReturn(history: readonly PricePoint[]): { value: number | null; asOf: string | null } {
338
423
  const dates = new Map<string, { time: number; close: number }>();
@@ -3,7 +3,7 @@ import { isAccessDenied } from "../../../api-client/errors";
3
3
  import { SignInWall } from "../cloud/auth-actions";
4
4
  import { CLOUD_PLAN_KEY, useCloudUpgradeAction } from "../shared/cloud-upgrade";
5
5
  import { isCloudSessionRequired, useResearchCloudSession } from "../shared/research-cloud-session";
6
- import { useCallback } from "react";
6
+ import { useCallback, useMemo } from "react";
7
7
  import { PaneStatusBody, usePaneFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs } from "../../../components";
8
8
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
9
9
  import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "../../../public/react";
@@ -11,11 +11,11 @@ import { usePaneInstance } from "../../../state/app/context";
11
11
  import type { PaneProps } from "../../../types/plugin";
12
12
  import { Box } from "../../../ui";
13
13
  import { GpuBoard } from "./board";
14
- import { getCachedGpuBoard, gpuCacheScope, GPU_NOT_AVAILABLE, loadGpuBoard } from "./client";
14
+ import { getCachedGpuBoard, gpuCacheScope, GPU_NOT_AVAILABLE, gpuPreviewModels, loadGpuBoard, loadGpuModelPreviews } from "./client";
15
15
  import { GpuChanges } from "./changes";
16
16
  import { GpuEquities } from "./equities";
17
17
  import { GpuHistory } from "./history";
18
- import { GPU_TABS, gpuTab, gpuTime } from "./model";
18
+ import { GPU_TABS, gpuEquityModels, gpuFamilies, gpuFamily, gpuNoMatch, gpuTab, gpuTime } from "./model";
19
19
 
20
20
  export function GpuPane(props: PaneProps) {
21
21
  usePlanAccess();
@@ -34,12 +34,28 @@ function GpuView({ width, height, focused }: PaneProps) {
34
34
  const resource = useAsyncResource(loadGpuBoard, { initialData: getCachedGpuBoard, clearOnError: isAccessDenied });
35
35
  const data = resource.data?.payload;
36
36
  const { createPaneFromTemplate } = usePluginAppActions();
37
+ const locked = !!data?.access?.locked;
38
+ const families = useMemo(() => gpuFamilies(data?.rows ?? []), [data]);
39
+ // What was typed ("h-100", "H100 80GB SXM") reads as the GPU it names; a name that matches none stays as typed and says so.
40
+ const activeModel = model ? gpuFamily(model, families) ?? model : "";
41
+ const unknownModel = !!model && !families.includes(activeModel);
42
+ // The free board is one GPU's three rows; another GPU's free preview stands in for the rows it leaves out.
43
+ const previewKey = useMemo(() => data && !unknownModel ? gpuPreviewModels(data, tab === "equities" ? gpuEquityModels(activeModel || "H100")
44
+ : tab === "changes" ? [] : tab === "history" ? [activeModel || "H100"] : [activeModel]).join("\n") : "", [data, unknownModel, tab, activeModel]);
45
+ const previewLoader = useCallback((force: boolean) => loadGpuModelPreviews(previewKey.split("\n"), force), [previewKey]);
46
+ const preview = useAsyncResource(previewKey ? previewLoader : null);
47
+ const rows = useMemo(() => data ? [...data.rows, ...(previewKey ? preview.data ?? [] : [])] : [], [data, previewKey, preview.data]);
48
+ const models = useMemo(() => locked ? [...families].sort() : [...new Set(rows.map((row) => row.gpuModel))].sort(), [locked, families, rows]);
49
+ // A failed preview is the footer's error; the body does not repeat it.
50
+ const emptyTitle = unknownModel ? gpuNoMatch(model, families)
51
+ : preview.error ? `No ${activeModel} rental prices to show.` : `No ${activeModel} rental prices have been observed yet.`;
37
52
  useAutoRefresh(resource.updatedAt, resource.load);
38
- usePaneRefreshKey(() => void resource.reload(), { focused: focused && (tab === "board" || !data) });
53
+ useAutoRefresh(preview.updatedAt, preview.load);
54
+ usePaneRefreshKey(() => { void resource.reload(); void preview.reload(); }, { focused: focused && (tab === "board" || !data) });
39
55
  const signIn = !data && (session.needsVerification || isCloudSessionRequired(resource.error));
40
56
  const notAvailable = !data && resource.error === GPU_NOT_AVAILABLE;
41
- usePaneStatusFooter({ registrationId: "gpu", loading: resource.loading, error: notAvailable || signIn ? null : resource.error,
42
- stale: !!data && (data.stale || resource.data?.stale), info: data?.asOf ? [{ id: "asof", parts: [{ text: `as of ${gpuTime(data.asOf)}`, tone: "muted" }] }] : [] });
57
+ usePaneStatusFooter({ registrationId: "gpu", loading: resource.loading || preview.loading, error: notAvailable || signIn ? null : resource.error ?? preview.error,
58
+ stale: !!data && (data.stale || resource.data?.stale), info: data?.asOf ? [{ id: "asof", asOf: true, parts: [{ text: `as of ${gpuTime(data.asOf)}`, tone: "muted" }] }] : [] });
43
59
  usePaneNoticeFooter({ registrationId: "gpu:notices", focused,
44
60
  notices: [...(data?.gaps ?? []), ...(resource.data?.refreshError ? [resource.data.refreshError] : [])] });
45
61
  usePaneFooter("gpu:actions", () => ({ info: data?.access?.preview ? [{ id: "preview", parts: [{ text: "Free preview", tone: "muted" }] }] : [],
@@ -47,17 +63,17 @@ function GpuView({ width, height, focused }: PaneProps) {
47
63
  onPress: () => createPaneFromTemplate("buildout-pane") }] }), [createPaneFromTemplate, data?.access, upgrade]);
48
64
  const { strip, rows: tabRows } = usePaneTabs(data ? { tabs: [...GPU_TABS], activeValue: tab, onSelect: setTab, focused, dense: true } : null);
49
65
  const body = { width, height: Math.max(3, height - tabRows), focused };
50
- const reloadBoard = useCallback(() => { void resource.reload(); }, [resource.reload]);
66
+ const reloadBoard = useCallback(() => { void resource.reload(); void preview.reload(); }, [resource.reload, preview.reload]);
51
67
  if (signIn) return <SignInWall placement="gpu" width={width} height={height} action="browse GPU rental prices" needsVerification={session.needsVerification} />;
52
68
  return <Box width={width} height={height} flexDirection="column">
53
69
  {strip}
54
- <PaneStatusBody loading={resource.loading && !data} error={!data && !notAvailable ? resource.error : null}
70
+ <PaneStatusBody loading={(resource.loading && !data) || (!!previewKey && preview.loading && !preview.data)} error={!data && !notAvailable ? resource.error : null}
55
71
  empty={notAvailable} emptyTitle={GPU_NOT_AVAILABLE} subject="GPU rental prices">
56
- {data ? tab === "board" ? <GpuBoard rows={data.rows} asOf={data.asOf} model={model} setModel={setModel} selectedId={selectedId}
72
+ {data ? tab === "board" ? <GpuBoard rows={rows} models={models} asOf={data.asOf} model={activeModel} setModel={setModel} selectedId={selectedId} emptyTitle={emptyTitle}
57
73
  select={(row, open) => { setSelectedId(row.id); if (open) setTab("history"); }} {...body} />
58
- : tab === "history" ? <GpuHistory rows={data.rows} model={model} setModel={setModel} selectedId={selectedId} onSelect={setSelectedId} reloadBoard={reloadBoard} {...body} />
59
- : tab === "changes" ? <GpuChanges board={data.rows} model={model} setModel={setModel} reloadBoard={reloadBoard} {...body} />
60
- : <GpuEquities board={data.rows} model={model} setModel={setModel} reloadBoard={reloadBoard} {...body} /> : null}
74
+ : tab === "history" ? <GpuHistory rows={rows} models={models} model={unknownModel ? "" : activeModel} setModel={setModel} selectedId={selectedId} onSelect={setSelectedId} reloadBoard={reloadBoard} {...body} />
75
+ : tab === "changes" ? <GpuChanges models={models} model={unknownModel ? "" : activeModel} setModel={setModel} reloadBoard={reloadBoard} {...body} />
76
+ : <GpuEquities board={rows} models={models} locked={locked} model={unknownModel ? "" : activeModel} setModel={setModel} reloadBoard={reloadBoard} {...body} /> : null}
61
77
  </PaneStatusBody>
62
78
  </Box>;
63
79
  }
@@ -0,0 +1,38 @@
1
+ import type { SeriesDefinition } from "../../../series/types";
2
+ import { cachedSeriesWarning } from "../shared/series-warning";
3
+ import { loadGpuBoard, loadGpuHistory } from "./client";
4
+ import { GPU_MODELS, gpuListIndex } from "./model";
5
+
6
+ /** What the GPU pane says when the free preview leaves history out. */
7
+ const GPU_PRO_ONLY = "Full history is available with Pro.";
8
+
9
+ /**
10
+ * Each GPU's list index, the one the GPU pane's Equities tab reads shares
11
+ * against. The per-provider listings stay on the board and its History tab:
12
+ * the server lists them, and they come and go.
13
+ */
14
+ export const GPU_SERIES: readonly SeriesDefinition[] = GPU_MODELS.map((model): SeriesDefinition => ({
15
+ id: `gpu:${model.toLowerCase().replaceAll(" ", "-")}-list-index`,
16
+ name: `${model} rental list index`,
17
+ unit: "USD/GPU-hour",
18
+ frequency: "intraday",
19
+ group: "Alternative data",
20
+ tags: ["GPU", "rental", "compute", "cloud", "list price", model],
21
+ description: `Median list price to rent one ${model} from neocloud providers (hyperscalers where none list it), chained so a change of providers causes no jump, as GPU charts it. Needs Pro.`,
22
+ style: "step",
23
+ load: async () => {
24
+ const board = await loadGpuBoard();
25
+ if (board.payload.access?.locked) throw new Error(GPU_PRO_ONLY);
26
+ const index = gpuListIndex(board.payload.rows, model);
27
+ if (!index) throw new Error(`No ${model} list index is listed yet.`);
28
+ const history = await loadGpuHistory(index.id);
29
+ if (history.payload.access?.locked) throw new Error(GPU_PRO_ONLY);
30
+ const warning = cachedSeriesWarning(history);
31
+ return {
32
+ points: history.payload.points
33
+ .filter((point) => `${point.source}:${point.skuKey}` === index.id)
34
+ .map((point) => ({ date: point.observedAt, value: point.pricePerGpuHr })),
35
+ ...(warning ? { warning } : {}),
36
+ };
37
+ },
38
+ }));
@@ -2,8 +2,10 @@
2
2
  * The help card for one function, opened by `HELP <fn>` in the command bar
3
3
  * and F1 on a focused pane: what it answers, how to type it, the keys that
4
4
  * matter in its pane, how fresh its data is on each plan, its Bloomberg
5
- * equivalent and its docs entry. Enter or the link opens the docs; Esc, the
6
- * close button or a click outside closes it.
5
+ * equivalent and its docs entry. Enter reads the entry in DOCS, inside the
6
+ * app; the link beside it is the same entry on gloom.sh, in the browser. A
7
+ * popped-out window cannot open a pane, so there Enter opens the link. Esc,
8
+ * the close button or a click outside closes it.
7
9
  *
8
10
  * `useFunctionHelpHost` is mounted with the app's global shortcuts for the
9
11
  * life of the window and owns the dialog; `requestFunctionHelp` lets the
@@ -13,7 +15,9 @@ import { useEffect, useRef, type ReactNode } from "react";
13
15
  import { usePlanAccess } from "../../../api-client/plan-access";
14
16
  import { functionDocsUrl, type FunctionFreshness, type FunctionHelpKey } from "../../../cli/pane-functions/function-help";
15
17
  import { requestFeedbackDialog } from "../../../components/feedback-dialog";
18
+ import { Button } from "../../../components/ui/button";
16
19
  import { ExternalLink } from "../../../components/ui/external-link";
20
+ import { DOCS_PANE_TEMPLATE_ID } from "../../../docs/content";
17
21
  import { DialogFrame } from "../../../components/ui/frame";
18
22
  import { ShortcutHint } from "../../../components/ui/shortcut-hint";
19
23
  import { t } from "../../../i18n";
@@ -21,10 +25,12 @@ import { useViewport } from "../../../react/input";
21
25
  import { useThemeColors } from "../../../theme/theme-context";
22
26
  import { Box, Text, TextAttributes, useRendererHost } from "../../../ui";
23
27
  import { useDialog, useDialogKeyboard, type AlertContext } from "../../../ui/dialog";
28
+ import { getSharedRegistry } from "../../registry";
24
29
  import type { HelpFunction } from "./function-index";
25
30
 
26
31
  export type FunctionHelpRequest =
27
- | { kind: "function"; fn: HelpFunction }
32
+ /** `inAppDocs: false` where DOCS cannot open (a popped-out window). */
33
+ | { kind: "function"; fn: HelpFunction; inAppDocs?: boolean }
28
34
  /** `HELP HELP`: the feedback dialog, which is how people reach us. */
29
35
  | { kind: "support" };
30
36
 
@@ -103,9 +109,10 @@ function FreshnessRows({ data, pro }: { data: FunctionFreshness; pro: boolean })
103
109
  );
104
110
  }
105
111
 
106
- function FunctionHelpCard({ fn, dismiss, dialogId, width, compact = false }: AlertContext & {
112
+ function FunctionHelpCard({ fn, dismiss, dialogId, width, compact = false, inAppDocs = true }: AlertContext & {
107
113
  fn: HelpFunction;
108
114
  width: number;
115
+ inAppDocs?: boolean;
109
116
  /** A short terminal: no blank row between the summary and the rows. */
110
117
  compact?: boolean;
111
118
  }) {
@@ -117,6 +124,10 @@ function FunctionHelpCard({ fn, dismiss, dialogId, width, compact = false }: Ale
117
124
  void rendererHost.openExternal(url);
118
125
  dismiss();
119
126
  };
127
+ const readInApp = () => {
128
+ getSharedRegistry()?.createPaneFromTemplate(DOCS_PANE_TEMPLATE_ID, { arg: fn.code });
129
+ dismiss();
130
+ };
120
131
 
121
132
  useDialogKeyboard((event) => {
122
133
  if (event.ctrl || event.alt || event.meta || event.super) return;
@@ -124,7 +135,8 @@ function FunctionHelpCard({ fn, dismiss, dialogId, width, compact = false }: Ale
124
135
  if (name !== "enter" && name !== "return") return;
125
136
  event.preventDefault();
126
137
  event.stopPropagation();
127
- openDocs();
138
+ if (inAppDocs) readInApp();
139
+ else openDocs();
128
140
  }, { scope: dialogId });
129
141
 
130
142
  const help = fn.help;
@@ -153,6 +165,11 @@ function FunctionHelpCard({ fn, dismiss, dialogId, width, compact = false }: Ale
153
165
  </CardRow>
154
166
  )}
155
167
  <CardRow label="Docs">
168
+ {inAppDocs && (
169
+ <Box flexDirection="row">
170
+ <Button label="Read in Docs" shortcut="Enter" compact onPress={readInApp} stopPropagation />
171
+ </Box>
172
+ )}
156
173
  <ExternalLink url={docsUrl} label={docsUrl.replace(/^https:\/\//, "")} onOpen={openDocs} />
157
174
  </CardRow>
158
175
  </Box>
@@ -189,7 +206,9 @@ export function useFunctionHelpHost({ closeCommandBar }: { closeCommandBar: () =
189
206
  closeOnClickOutside: true,
190
207
  // The terminal host defaults to 60 columns; the desktop sizes to the content.
191
208
  style: { width: width + TERMINAL_DIALOG_CHROME, ...(compact ? { paddingY: 0 } : {}) },
192
- content: (context: AlertContext) => <FunctionHelpCard {...context} fn={request.fn} width={width} compact={compact} />,
209
+ content: (context: AlertContext) => (
210
+ <FunctionHelpCard {...context} fn={request.fn} width={width} compact={compact} inAppDocs={request.inAppDocs !== false} />
211
+ ),
193
212
  }).catch(() => {}).finally(() => {
194
213
  openRef.current = false;
195
214
  });