@gloom-sh/terminal 0.19.1 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -0,0 +1,54 @@
1
+ import { setStallAttentionProbe, type StallAttention } from "../../../../telemetry/stall-reports";
2
+
3
+ /** What this needs of a window: its id, its minimized state and its events. */
4
+ interface AttentionWindow {
5
+ id: number;
6
+ isMinimized(): boolean;
7
+ on?(name: string, handler: (event: unknown) => void): void;
8
+ }
9
+
10
+ const openWindows = new Map<number, AttentionWindow>();
11
+ let focusedWindowId: number | null = null;
12
+ let sawFocusChange = false;
13
+
14
+ /**
15
+ * Whether anyone was looking when the desktop app's Bun process froze. The
16
+ * system slows a hidden app's timers, which reads like a freeze, so a stall
17
+ * report from this process says whether one of the app's windows had the
18
+ * keyboard (the app was in front) and whether one was open and not
19
+ * minimized. Read only for a stall about to be reported.
20
+ */
21
+ function desktopAttention(): StallAttention {
22
+ let visible: boolean | null = false;
23
+ try {
24
+ for (const window of openWindows.values()) {
25
+ if (!window.isMinimized()) {
26
+ visible = true;
27
+ break;
28
+ }
29
+ }
30
+ } catch {
31
+ visible = null;
32
+ }
33
+ return { active: sawFocusChange ? focusedWindowId !== null : null, visible };
34
+ }
35
+
36
+ /** Follows a window the app opened until it closes; the first one installs the probe. */
37
+ export function trackWindowAttention(window: AttentionWindow): void {
38
+ if (openWindows.size === 0) setStallAttentionProbe(desktopAttention);
39
+ openWindows.set(window.id, window);
40
+ window.on?.("focus", () => {
41
+ focusedWindowId = window.id;
42
+ sawFocusChange = true;
43
+ });
44
+ window.on?.("blur", () => {
45
+ // Focus moving between two of the app's windows can report the new
46
+ // window's focus before the old one's blur.
47
+ if (focusedWindowId === window.id) focusedWindowId = null;
48
+ sawFocusChange = true;
49
+ });
50
+ window.on?.("close", () => {
51
+ openWindows.delete(window.id);
52
+ if (focusedWindowId === window.id) focusedWindowId = null;
53
+ });
54
+ }
@@ -1,4 +1,6 @@
1
1
  import { BrowserWindow } from "electrobun/bun";
2
+ import { measurePerf } from "../../../../utils/perf-marks";
3
+ import { trackWindowAttention } from "./attention";
2
4
  import {
3
5
  applyNativeWindowFrame,
4
6
  applyWindowMoveEvent,
@@ -83,7 +85,8 @@ interface AppWindowOptions {
83
85
  * its cached frame current as it moves, and holds it to `minSize` on resize.
84
86
  */
85
87
  export function createAppWindow({ title, frame, rpc, minSize, onFrameChange, preload }: AppWindowOptions): BrowserWindow {
86
- const window = new BrowserWindow({
88
+ // Opening a window waits on the native layer, the webview's creation included.
89
+ const window = measurePerf("native.window.create", () => new BrowserWindow({
87
90
  title,
88
91
  frame,
89
92
  url: APP_VIEW_URL,
@@ -94,7 +97,7 @@ export function createAppWindow({ title, frame, rpc, minSize, onFrameChange, pre
94
97
  titleBarStyle: desktopTitleBarStyle(),
95
98
  navigationRules: JSON.stringify(["views://*"]),
96
99
  sandbox: false,
97
- });
100
+ }));
98
101
  applyWindowsWindowIcon(title);
99
102
  applyWindowsCustomChrome(title);
100
103
  if (process.platform === "win32") {
@@ -105,6 +108,7 @@ export function createAppWindow({ title, frame, rpc, minSize, onFrameChange, pre
105
108
  });
106
109
  }
107
110
  updateWindowFrameCache(window, frame, minSize);
111
+ trackWindowAttention(window);
108
112
  const readsNativeFrame = process.platform === "win32";
109
113
  (window as any).on?.("move", (event: WindowMoveEvent) => {
110
114
  if (readsNativeFrame) {
@@ -0,0 +1,48 @@
1
+ import { BrowserView, BrowserWindow } from "electrobun/bun";
2
+ import { measurePerf } from "../../../../utils/perf-marks";
3
+
4
+ /**
5
+ * Electrobun's window and webview calls that go to the native layer. On
6
+ * Windows each one is answered on the thread that runs the windows, and this
7
+ * process waits for it: while that thread is busy, so is this one.
8
+ */
9
+ const WINDOW_CALLS = [
10
+ "activate", "close", "show", "showInactive", "hide", "minimize", "unminimize", "maximize", "unmaximize",
11
+ "isMinimized", "isMaximized", "isFullScreen", "setFullScreen", "isAlwaysOnTop", "setAlwaysOnTop",
12
+ "setTitle", "setPosition", "setSize", "setFrame", "getFrame",
13
+ ];
14
+ const VIEW_CALLS = [
15
+ "executeJavascript", "loadURL", "loadHTML", "setNavigationRules", "openDevTools", "closeDevTools", "toggleDevTools",
16
+ "setPageZoom",
17
+ ];
18
+
19
+ let measured = false;
20
+
21
+ /** `getFrame` as a stall report keeps it: `get-frame`. */
22
+ function sectionPart(name: string): string {
23
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
24
+ }
25
+
26
+ function measureCalls(prototype: object, prefix: string, names: readonly string[]): void {
27
+ const methods = prototype as Record<string, unknown>;
28
+ for (const name of names) {
29
+ const original = methods[name];
30
+ if (typeof original !== "function") continue;
31
+ const section = `${prefix}.${sectionPart(name)}`;
32
+ methods[name] = function measuredNativeCall(this: unknown, ...args: unknown[]) {
33
+ return measurePerf(section, () => original.apply(this, args));
34
+ };
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Names each native window and webview call (`native.window.get-frame`,
40
+ * `native.view.execute-javascript`), so a stall inside or right after one
41
+ * says so instead of naming whatever was measured last. Once per process.
42
+ */
43
+ export function measureNativeWindowCalls(): void {
44
+ if (measured) return;
45
+ measured = true;
46
+ measureCalls(BrowserWindow.prototype, "native.window", WINDOW_CALLS);
47
+ measureCalls(BrowserView.prototype, "native.view", VIEW_CALLS);
48
+ }
@@ -16,8 +16,12 @@ import { JsonTickerRepository } from "../../../data/json-ticker-repository";
16
16
  import type { DesktopPaneShotPayload } from "../../../cli/desktop-pane-shot";
17
17
  import {
18
18
  SHOT_API_PROXY_PREFIX,
19
+ SHOT_API_SOCKET_PATH,
19
20
  SHOT_HTTP_BRIDGE_PATH,
20
21
  SHOT_MARKET_BRIDGE_PATH,
22
+ shotRouteUrl,
23
+ type ShotApiSocketCall,
24
+ type ShotApiSocketReply,
21
25
  } from "../../../cli/desktop-pane-shot-routes";
22
26
  import { instrumentFromTicker } from "../../../market-data/request-types";
23
27
  import type { RendererHost } from "../../../ui/host";
@@ -95,6 +99,12 @@ const TRACKED_RESPONSE_METHODS = new Set<PropertyKey>([
95
99
  "text",
96
100
  ]);
97
101
 
102
+ /**
103
+ * The page's own address, read once before anything can change it: the
104
+ * server answers only inside the secret folder the page opened at.
105
+ */
106
+ const SHOT_PAGE_URL = window.location.href;
107
+
98
108
  let pendingShotWork = 0;
99
109
  let didInstallShotFetchTracker = false;
100
110
  const rendererHost: RendererHost = {
@@ -142,22 +152,90 @@ function installShotFetchTracker(): void {
142
152
  )) as typeof fetch;
143
153
  }
144
154
 
155
+ /** Sends a read over the socket; null once the socket has closed. */
156
+ type ShotApiSocketSend = (call: Omit<ShotApiSocketCall, "id">, signal: AbortSignal | null | undefined) => Promise<Response> | null;
157
+
158
+ /**
159
+ * Cloud API reads over one socket to the Bun server, answered as each lands.
160
+ * A page's fetches share six connections to its origin, so a board's seventy
161
+ * contract histories and terms waited on each other for seconds. Null when
162
+ * the socket does not open; the page's fetch carries the reads then, and
163
+ * once the socket closes.
164
+ */
165
+ function openShotApiSocket(): Promise<ShotApiSocketSend | null> {
166
+ return new Promise((resolve) => {
167
+ const pending = new Map<number, (reply: ShotApiSocketReply | null) => void>();
168
+ let nextId = 1;
169
+ let open = false;
170
+ const url = shotRouteUrl(SHOT_API_SOCKET_PATH, SHOT_PAGE_URL);
171
+ url.protocol = "ws:";
172
+ const socket = new WebSocket(url);
173
+ socket.addEventListener("message", (event) => {
174
+ let reply: ShotApiSocketReply;
175
+ try {
176
+ reply = JSON.parse(String(event.data)) as ShotApiSocketReply;
177
+ } catch {
178
+ // Intentional ignore: every reply the server sends is JSON; anything else answers no call.
179
+ return;
180
+ }
181
+ pending.get(reply.id)?.(reply);
182
+ pending.delete(reply.id);
183
+ });
184
+ socket.addEventListener("close", () => {
185
+ open = false;
186
+ for (const settle of pending.values()) settle(null);
187
+ pending.clear();
188
+ resolve(null);
189
+ });
190
+ socket.addEventListener("open", () => {
191
+ open = true;
192
+ resolve((call, signal) => !open ? null : new Promise<Response>((answer, fail) => {
193
+ const aborted = () => fail(signal?.reason ?? new DOMException("The operation was aborted.", "AbortError"));
194
+ if (signal?.aborted) return aborted();
195
+ const id = nextId++;
196
+ const onAbort = () => {
197
+ pending.delete(id);
198
+ aborted();
199
+ };
200
+ signal?.addEventListener("abort", onAbort, { once: true });
201
+ pending.set(id, (reply) => {
202
+ signal?.removeEventListener("abort", onAbort);
203
+ // A socket that closed mid-read is a request that never got an answer, as a dropped connection is to fetch.
204
+ if (!reply) return fail(new TypeError("Failed to fetch"));
205
+ if ("error" in reply) return fail(new TypeError(reply.error));
206
+ const bytes = Uint8Array.from(atob(reply.body), (char) => char.charCodeAt(0));
207
+ const empty = bytes.length === 0 || reply.status === 204 || reply.status === 304;
208
+ answer(new Response(empty ? null : bytes, { status: reply.status, statusText: reply.statusText, headers: reply.headers }));
209
+ });
210
+ socket.send(JSON.stringify({ id, ...call } satisfies ShotApiSocketCall));
211
+ }));
212
+ }, { once: true });
213
+ });
214
+ }
215
+
145
216
  function installShotCloudApiTransport(): void {
146
217
  apiClient.setCookieSessionMode(true);
218
+ let socket: Promise<ShotApiSocketSend | null> | null = null;
147
219
  setCloudApiFetchTransport((url, init = {}) => {
148
220
  const upstream = new URL(url);
149
- const proxyUrl = new URL(`${SHOT_API_PROXY_PREFIX}${upstream.pathname}`, window.location.origin);
221
+ const proxyUrl = shotRouteUrl(`${SHOT_API_PROXY_PREFIX}${upstream.pathname}`, SHOT_PAGE_URL);
150
222
  proxyUrl.search = upstream.search;
151
223
  const headers = new Headers(init.headers);
152
224
  // Bun owns the real session cookie. The browser only talks to the local
153
225
  // same-origin proxy and never receives or serializes the credential.
154
226
  headers.delete("Cookie");
155
227
  headers.delete("Origin");
156
- return window.fetch(proxyUrl, {
228
+ const viaFetch = () => window.fetch(proxyUrl, {
157
229
  ...init,
158
230
  headers,
159
231
  credentials: "same-origin",
160
232
  });
233
+ if ((init.method ?? "GET").toUpperCase() !== "GET" || init.body != null) return viaFetch();
234
+ return trackShotWork((async () => {
235
+ socket ??= openShotApiSocket();
236
+ const send = await socket;
237
+ return await send?.({ path: `${upstream.pathname}${upstream.search}`, headers: [...headers] }, init.signal) ?? await viaFetch();
238
+ })());
161
239
  });
162
240
  }
163
241
 
@@ -168,7 +246,7 @@ function installShotCloudApiTransport(): void {
168
246
  */
169
247
  function installShotHttpFetchTransport(): void {
170
248
  setHttpFetchTransport(async (url, init) => {
171
- const response = await window.fetch(SHOT_HTTP_BRIDGE_PATH, {
249
+ const response = await window.fetch(shotRouteUrl(SHOT_HTTP_BRIDGE_PATH, SHOT_PAGE_URL), {
172
250
  method: "POST",
173
251
  headers: { "Content-Type": "application/json" },
174
252
  body: JSON.stringify(await toRequestEnvelope(url, init)),
@@ -190,7 +268,7 @@ async function restoreShotCloudSession(): Promise<void> {
190
268
  * of the corporate action history come from providers the router merges in.
191
269
  */
192
270
  async function requestShotMarketData<T>(operation: string, args: unknown[], signal?: AbortSignal): Promise<T> {
193
- const response = await window.fetch(SHOT_MARKET_BRIDGE_PATH, {
271
+ const response = await window.fetch(shotRouteUrl(SHOT_MARKET_BRIDGE_PATH, SHOT_PAGE_URL), {
194
272
  method: "POST",
195
273
  headers: { "Content-Type": "application/json" },
196
274
  body: JSON.stringify({ operation, args }),
@@ -14,7 +14,9 @@ declare global {
14
14
  * Set by a capture that carries a status line: shows the first of the
15
15
  * variants (longest first) that fits the footer, cut in the middle when
16
16
  * none does, and resolves to the text shown. `dates` says what the line
17
- * dates, so the pane's own text that says the same goes.
17
+ * dates, so the pane's own text that says the same goes. With no variants
18
+ * the pane has nothing to date: the footer row is given back to the body
19
+ * and the text shown is empty.
18
20
  */
19
21
  __GLOOM_CLI_SHOT_FIT_STATUS__?: (variants: string[], options?: { dates?: ShotLineDates }) => Promise<string>;
20
22
  }
@@ -90,22 +92,28 @@ function footerOverflows(): boolean {
90
92
  }
91
93
 
92
94
  interface ShotStatusLineState {
93
- text: string;
95
+ /** Null once the capture has no line to show: the footer row is gone. */
96
+ text: string | null;
94
97
  dates: ShotLineDates;
95
98
  /** No variant fit beside the pane's own `as of`, so it gave way. */
96
99
  crowded: boolean;
97
100
  }
98
101
 
99
102
  function useShotStatusLine(enabled: boolean, hasAsOf: () => boolean): ShotStatusLineState {
100
- const [text, setText] = useState(PENDING_STATUS);
103
+ const [text, setText] = useState<string | null>(PENDING_STATUS);
101
104
  const [dates, setDates] = useState<ShotLineDates>(null);
102
105
  const [crowded, setCrowded] = useState(false);
103
106
  useEffect(() => {
104
107
  if (!enabled) return;
105
108
  // What the line says goes before it is fitted, so the line gets its room.
106
- window.__GLOOM_CLI_SHOT_FIT_STATUS__ = (variants, options) => {
109
+ window.__GLOOM_CLI_SHOT_FIT_STATUS__ = async (variants, options) => {
107
110
  setDates(options?.dates ?? null);
108
111
  setCrowded(false);
112
+ if (variants.length === 0) {
113
+ setText(null);
114
+ await nextFrames(2);
115
+ return "";
116
+ }
109
117
  const show = async (next: string) => {
110
118
  setText(next);
111
119
  await nextFrames(2);
@@ -148,7 +156,7 @@ export function PaneShotFrame({ paneId, title, width, height, preserveStatus = f
148
156
  const { text: statusText, dates, crowded } = useShotStatusLine(statusLine, hasAsOf);
149
157
  const datesData = statusLine && dates === "data";
150
158
  return <PaneFooterProvider>{(registeredFooter) => {
151
- const status: PaneFooterSegment[] = statusLine ? [{ id: "shot-status", parts: [{ text: statusText, tone: "label" }] }] : [];
159
+ const status: PaneFooterSegment[] = statusLine && statusText !== null ? [{ id: "shot-status", parts: [{ text: statusText, tone: "label" }] }] : [];
152
160
  const info = registeredFooter.info.filter((segment) => (preserveStatus || segment.icon === "warning" || segment.capture)
153
161
  && !(datesPaneData(segment) && (datesData || crowded))
154
162
  && !(segment.quoteStatus && statusLine && dates !== null));
@@ -36,6 +36,9 @@ export function detachedSnapshotKey(snapshot: DesktopSharedStateSnapshot, paneId
36
36
  pluginConfig: snapshot.config.pluginConfig,
37
37
  disabledPlugins: snapshot.config.disabledPlugins,
38
38
  chartPreferences: snapshot.config.chartPreferences,
39
+ // A popped-out window collects Attention Counts on its own, so turning
40
+ // them off in the main window has to reach it.
41
+ telemetry: snapshot.config.telemetry ?? null,
39
42
  instances: paneStateIds.map((id) => findPaneInstance(snapshot.config.layout, id) ?? null),
40
43
  paneState: Object.fromEntries(paneStateIds.map((id) => [id, snapshot.paneState[id] ?? {}])),
41
44
  });
@@ -53,7 +53,7 @@ import { crashReportsEnabled, installCrashReporter, reportCrash } from "../../..
53
53
  import { browserDoNotTrack, installWindowCrashListeners } from "../../../telemetry/crash-reports-dom";
54
54
  import { forwardAttendedStalls } from "../../../telemetry/stall-reports-dom";
55
55
  import { currentTelemetryConfig } from "../../../telemetry/live-config";
56
- import { attentionCountsEnabled, installAttentionCounter } from "../../../telemetry/attention-counts";
56
+ import { installAttentionCounter } from "../../../telemetry/attention-counts";
57
57
  import { installUsageCounter, usageCountsEnabled } from "../../../telemetry/usage-counts";
58
58
  import { installWindowUsageFlush } from "../../../telemetry/usage-counts-dom";
59
59
  import { cloudSyncController } from "../../../sync/controller";
@@ -160,7 +160,12 @@ async function boot() {
160
160
  getInstallId: () => init.telemetry.installId,
161
161
  officialPluginIds: loadOfficialPluginIds,
162
162
  });
163
- installAttentionCounter(() => !init.telemetry.optedOut && !browserDoNotTrack() && attentionCountsEnabled(currentTelemetryConfig(init.config)));
163
+ installAttentionCounter({
164
+ allowed: () => !init.telemetry.optedOut && !browserDoNotTrack(),
165
+ launchConfig: () => init.config,
166
+ // A popped-out window does not sync: the main window does, and sends it the settings it pulled.
167
+ ...(init.windowKind === "detached" ? { accountSettingsPulled: () => true } : {}),
168
+ });
164
169
  // Desktop has no Debug Log: this window's freezes go to the Bun process, which keeps the stall log.
165
170
  setStallSink(forwardAttendedStalls(sendElectrobunStall).sink);
166
171
  installElectrobunCapabilityStreamClient();
@@ -31,6 +31,7 @@ type PayloadBuilder = (...args: unknown[]) => Record<string, unknown>;
31
31
  const assetDataPayloads: Record<string, PayloadBuilder> = {
32
32
  canProvide: (ticker, exchange, context) => ({ ticker, exchange, context }),
33
33
  primaryMarketSourceName: () => ({}),
34
+ batchesReads: (method) => ({ method }),
34
35
  getCachedFinancialsForTargets: (targets, options) => ({ targets, options }),
35
36
  // An abort signal does not cross the bridge; the board stops waiting instead.
36
37
  getQuotesBatch: (targets, options) => {
@@ -25,9 +25,16 @@ export const webRendererHost: RendererHost = {
25
25
  void backendRequest("host.exit").catch(() => window.close());
26
26
  });
27
27
  },
28
- startWindowDrag() {
29
- // Windows moves the window itself, as it does from a native title bar.
28
+ startWindowDrag(press) {
29
+ // Windows moves the window itself, as it does from a native title bar, and
30
+ // a double-click there maximizes or restores it.
30
31
  if (detectPlatform() === "win32") {
32
+ if (press?.detail === 2) {
33
+ void backendRequest("host.windowControl", { action: "toggle-maximize" }).catch((error: unknown) => {
34
+ console.error("[desktop] could not maximize the window", error instanceof Error ? error.message : String(error));
35
+ });
36
+ return;
37
+ }
31
38
  void backendRequest("host.startWindowDrag").catch((error: unknown) => {
32
39
  console.error("[desktop] could not start moving the window", error instanceof Error ? error.message : String(error));
33
40
  });
@@ -6,6 +6,8 @@
6
6
  * nothing opened and can show the link instead.
7
7
  */
8
8
 
9
+ import { systemOpenCommand } from "../../utils/external-url";
10
+
9
11
  type Env = Readonly<Record<string, string | undefined>>;
10
12
 
11
13
  function present(value: string | undefined): boolean {
@@ -35,27 +37,31 @@ export function browserOpenCommands(url: string, env: Env, platform: NodeJS.Plat
35
37
  const forwarded = present(env.BROWSER) ? envBrowserCommand(env.BROWSER!, url) : null;
36
38
 
37
39
  if (platform === "darwin") {
38
- if (!remote) return [["open", url]];
40
+ const opener = systemOpenCommand(url, platform);
41
+ if (!remote) return opener ? [opener] : [];
39
42
  return forwarded ? [forwarded] : [];
40
43
  }
41
44
  if (platform === "win32") {
42
- if (!remote) return [["cmd", "/c", "start", "", url]];
45
+ const opener = systemOpenCommand(url, platform);
46
+ if (!remote) return opener ? [opener] : [];
43
47
  return forwarded ? [forwarded] : [];
44
48
  }
45
49
 
46
50
  // Linux and the other unixes. WSL has no Linux browser, but Windows can
47
51
  // open the link: `wslview` when wslu is installed, else Windows' own handler.
48
52
  if (present(env.WSL_DISTRO_NAME) || present(env.WSL_INTEROP)) {
53
+ const windowsOpener = systemOpenCommand(url, "win32");
49
54
  return [
50
55
  ...(forwarded ? [forwarded] : []),
51
56
  ["wslview", url],
52
- ["rundll32.exe", "url.dll,FileProtocolHandler", url],
57
+ ...(windowsOpener ? [windowsOpener] : []),
53
58
  ];
54
59
  }
55
60
  const display = present(env.DISPLAY) || present(env.WAYLAND_DISPLAY);
56
61
  // xdg-open reads $BROWSER itself, so a forwarding helper still applies.
57
62
  if (remote && !display && !forwarded) return [];
58
- return [["xdg-open", url]];
63
+ const opener = systemOpenCommand(url, platform);
64
+ return opener ? [opener] : [];
59
65
  }
60
66
 
61
67
  /** How long an opener may run before it counts as having opened the browser. */
@@ -25,7 +25,7 @@ function resolveInsets(props: Record<string, unknown>): CellInsets {
25
25
  }
26
26
 
27
27
  export const OpenTuiImageSurface = forwardRef<unknown, ImageSurfaceProps>(function OpenTuiImageSurface(
28
- { children, src, alt: _alt, objectFit = "contain", ...props },
28
+ { children, src, alt: _alt, objectFit = "contain", fallbackWhileLoading: _fallbackWhileLoading, imageBackground: _imageBackground, ...props },
29
29
  forwardedRef,
30
30
  ) {
31
31
  const renderer = useNativeRenderer();
@@ -1,6 +1,7 @@
1
1
  import { hostname } from "os";
2
2
  import { getGloomberbDirs } from "../../data/config/home";
3
- import { existsSync, mkdirSync } from "fs";
3
+ import { existsSync } from "fs";
4
+ import { makePrivateDirSync } from "../../utils/private-files";
4
5
  import { App } from "../../app";
5
6
  import { getDataDir, initDataDir, loadConfig, setConfigStoreHost } from "../../data/config/store";
6
7
  import { applyLanguageFromConfig } from "../../i18n";
@@ -28,7 +29,7 @@ import { startRemoteControlServer, type RemoteControlServer } from "../../remote
28
29
  import { createAppServices } from "../../core/app-services";
29
30
  import { flushPendingPersistence } from "../../state/persist-scheduler";
30
31
  import { loadOfficialPluginIds } from "../../plugins/marketplace/feed";
31
- import { flushCrashReports, installCrashReporter } from "../../telemetry/crash-reports";
32
+ import { flushCrashReports, installCrashReporter, telemetryOptedOut } from "../../telemetry/crash-reports";
32
33
  import {
33
34
  CRASH_REPORT_EXIT_FLUSH_MS,
34
35
  createNodeCrashReporterHost,
@@ -38,13 +39,14 @@ import {
38
39
  } from "../../telemetry/crash-reports-node";
39
40
  import { currentTelemetryConfig } from "../../telemetry/live-config";
40
41
  import { installUpdateFailureReporter } from "../../telemetry/update-failures";
42
+ import { createStallClocks } from "../../telemetry/stall-clocks";
41
43
  import { installStallReporter, recordStall } from "../../telemetry/stall-reports";
42
44
  import { createNodeStallReporterHost } from "../../telemetry/stall-reports-node";
43
- import { setStallSink } from "../../utils/main-thread-stall";
45
+ import { setStallClocks, setStallSink } from "../../utils/main-thread-stall";
44
46
  import { createNodeUpdateFailureHost } from "../../telemetry/update-failures-node";
45
47
  import { terminalInstallKind } from "../../updater";
46
48
  import { flushUsageCounts, installUsageCounter, usageCountsEnabled } from "../../telemetry/usage-counts";
47
- import { attentionCountsEnabled, installAttentionCounter } from "../../telemetry/attention-counts";
49
+ import { installAttentionCounter } from "../../telemetry/attention-counts";
48
50
  import { starPromptEnvironmentAllows } from "../../app/star-prompt/model";
49
51
  import { allowStarPrompt } from "../../app/star-prompt/runtime";
50
52
 
@@ -114,7 +116,7 @@ export async function startOpenTuiApp({ externalPlugins, cliLaunchRequest }: Sta
114
116
  const dataDir = await getDataDir() ?? getGloomberbDirs().data;
115
117
 
116
118
  if (!existsSync(dataDir)) {
117
- mkdirSync(dataDir, { recursive: true });
119
+ makePrivateDirSync(dataDir);
118
120
  }
119
121
 
120
122
  const config = await measurePerfAsync("startup.opentui.init-data-dir", () => initDataDir(dataDir));
@@ -129,6 +131,7 @@ export async function startOpenTuiApp({ externalPlugins, cliLaunchRequest }: Sta
129
131
  }));
130
132
  installStallReporter(createNodeStallReporterHost({ getDataDir: () => config.dataDir }));
131
133
  setStallSink((event) => recordStall("terminal", event));
134
+ setStallClocks(createStallClocks());
132
135
  installUsageCounter({
133
136
  surface: "terminal",
134
137
  os: describeNodeOs(),
@@ -136,7 +139,10 @@ export async function startOpenTuiApp({ externalPlugins, cliLaunchRequest }: Sta
136
139
  getInstallId: () => readOrCreateInstallId(config.dataDir),
137
140
  officialPluginIds: loadOfficialPluginIds,
138
141
  });
139
- const stopAttention = installAttentionCounter(() => attentionCountsEnabled(currentTelemetryConfig(config), process.env));
142
+ const stopAttention = installAttentionCounter({
143
+ allowed: () => !telemetryOptedOut(process.env),
144
+ launchConfig: () => config,
145
+ });
140
146
  allowStarPrompt(starPromptEnvironmentAllows({
141
147
  env: process.env,
142
148
  stdinIsTTY: process.stdin.isTTY,
@@ -2,7 +2,7 @@
2
2
  import { useTerminalDimensions } from "@opentui/react";
3
3
  import { useSyncExternalStore } from "react";
4
4
  import { colors } from "../../theme/colors";
5
- import { createToastStore, type ToastHost, type ToastTone } from "../../ui/toast";
5
+ import { createToastStore, useToastsShown, type ToastHost, type ToastTone } from "../../ui/toast";
6
6
  import { useActionShortcut } from "../../ui";
7
7
  import { displayWidth } from "../../utils/format";
8
8
 
@@ -26,6 +26,7 @@ function toneIcon(tone: ToastTone): string {
26
26
  function ToastViewport({ position = "bottom-right" }: { position?: string }) {
27
27
  const dimensions = useTerminalDimensions();
28
28
  const shownToasts = useSyncExternalStore(toastStore.subscribe, toastStore.getSnapshot, toastStore.getSnapshot);
29
+ useToastsShown(shownToasts);
29
30
  const actionShortcut = useActionShortcut("notification-action");
30
31
  const maxWidth = Math.max(1, Math.min(60, dimensions.width - 4));
31
32
  const placement = position.startsWith("top") ? { top: 1 } : { bottom: 1 };
@@ -23,6 +23,24 @@ export function seriesExpression(definition: SeriesDefinition): string {
23
23
  return definition.expression ?? loadedSeriesExpression(definition);
24
24
  }
25
25
 
26
+ /**
27
+ * The one catalog series a bare word is an alias of (`hashrate` is
28
+ * `bitcoin:hashrate`), for a chart typed with the word where a ticker goes.
29
+ * Only the words a series lists as its `aliases`, never any other id or
30
+ * name, and none that two series claim.
31
+ */
32
+ export function seriesNamedBy(definitions: Iterable<SeriesDefinition>, word: string): SeriesDefinition | undefined {
33
+ const wanted = word.trim().toLowerCase();
34
+ if (!wanted) return undefined;
35
+ let found: SeriesDefinition | undefined;
36
+ for (const definition of definitions) {
37
+ if (definition.input || !definition.aliases?.includes(wanted)) continue;
38
+ if (found && found.id !== definition.id) return undefined;
39
+ found = definition;
40
+ }
41
+ return found;
42
+ }
43
+
26
44
  /**
27
45
  * The capability series id a loaded series charts under: its name, one of its
28
46
  * lines after a tilde, and for a series with an input, the symbol after a
@@ -195,7 +195,7 @@ export async function resolveLoadedSeries(
195
195
  ): Promise<ResolvedSeries> {
196
196
  const ticker = request.ticker?.trim().toUpperCase() || undefined;
197
197
  const line = definition.lines?.length ? request.line ?? definition.lines[0] : undefined;
198
- const { points, edge, warning, attribution } = loadResult(await loadSeries(definition, {
198
+ const { points, edge, warning, savedAt, attribution } = loadResult(await loadSeries(definition, {
199
199
  ...seriesLoadWindow(request.viewport, now),
200
200
  ...(ticker ? { ticker } : {}),
201
201
  ...viewportBars(request.viewport, ticker),
@@ -221,6 +221,7 @@ export async function resolveLoadedSeries(
221
221
  panelId: "main",
222
222
  interpolation: style === "step" ? "step-after" : "none",
223
223
  ...(warning ? { warning } : {}),
224
+ ...(savedAt != null && Number.isFinite(savedAt) ? { savedAt } : {}),
224
225
  ...(attribution ? { attribution: { ...attribution } } : {}),
225
226
  points: normalizeSeriesPoints(points, edge),
226
227
  };
@@ -4,7 +4,7 @@ import type { CapabilityInvoker } from "../capabilities/types";
4
4
  import type { GloomPlugin } from "../types/plugin";
5
5
  import { resolveRegisteredSeries } from "./capability";
6
6
  import { readSeriesFolder } from "./files";
7
- import { SeriesRegistry, seriesExpression } from "./registry";
7
+ import { SeriesRegistry, seriesExpression, seriesNamedBy } from "./registry";
8
8
  import type { SeriesDefinition } from "./types";
9
9
 
10
10
  /**
@@ -75,3 +75,12 @@ export function standaloneSeriesCatalog(
75
75
  return byExpression.get(expression);
76
76
  };
77
77
  }
78
+
79
+ /** The catalog series a bare word is an alias of (`hashrate`), where no plugin runtime runs; see `seriesNamedBy`. */
80
+ export function standaloneSeriesNamed(
81
+ plugins: readonly GloomPlugin[],
82
+ disabledPlugins: readonly string[] = [],
83
+ ): (word: string) => SeriesDefinition | undefined {
84
+ const definitions = () => plugins.filter((plugin) => !disabledPlugins.includes(plugin.id)).flatMap((plugin) => plugin.series ?? []);
85
+ return (word) => seriesNamedBy(definitions(), word);
86
+ }
@@ -92,6 +92,11 @@ export interface SeriesLoadResult {
92
92
  points: readonly SeriesPoint[];
93
93
  /** A note the chart shows above the plot, such as "cached data is stale". */
94
94
  warning?: string;
95
+ /**
96
+ * When the copy the points came from was saved (ms), when its refresh failed
97
+ * and that copy answered: the chart's footer says "saved copy from …".
98
+ */
99
+ savedAt?: number;
95
100
  /** What a report of this series cites in place of Gloom Cloud. */
96
101
  attribution?: SeriesAttribution;
97
102
  }
@@ -123,6 +128,13 @@ interface SeriesDefinitionBase {
123
128
  group?: string;
124
129
  /** Extra words the catalog search matches, such as a ticker the series is often read against. */
125
130
  tags?: readonly string[];
131
+ /**
132
+ * Words `fn G` charts the series by where a ticker goes (`hashrate`), each
133
+ * in lower case: only when no listing answers to the word, the quote source
134
+ * says it is not a ticker, and no other series has the word. A series with
135
+ * an `input` takes none.
136
+ */
137
+ aliases?: readonly string[];
126
138
  /** One sentence for the catalog search and for `gloomberb` automation. */
127
139
  description?: string;
128
140
  /**
@@ -23,11 +23,13 @@ const RATE_UNITS = ["%", "bp", "x", "ratio", "index", "score", "points"] as cons
23
23
  const QUANTITY_UNITS = [
24
24
  "number", "count", "units", "shares", "contracts", "days", "people", "jobs", "posts", "views", "hours",
25
25
  "barrels", "barrels/day", "cubic feet", "MW", "MWh", "TWh", "GWh/day", "tons", "ounces",
26
+ // A hash rate: exahashes per second.
27
+ "EH/s",
26
28
  // The instrument's own currency, for a field that applies to any ticker.
27
29
  "currency", "currency/share",
28
30
  ] as const;
29
- /** What a price is quoted per: `USD/barrel`, `USD/GPU-hour`. */
30
- const PER_UNITS = ["share", "barrel", "gallon", "MMBtu", "MWh", "ounce", "ton", "tCO2", "hour", "GPU-hour"] as const;
31
+ /** What a price is quoted per: `USD/barrel`, `USD/GPU-hour`, a miner's `USD/PH/s/day`. */
32
+ const PER_UNITS = ["share", "barrel", "gallon", "MMBtu", "MWh", "ounce", "ton", "tCO2", "hour", "GPU-hour", "PH/s/day"] as const;
31
33
  const SCALES = ["thousand", "million", "billion", "trillion"] as const;
32
34
 
33
35
  const RATE_SET: ReadonlySet<string> = new Set(RATE_UNITS);
@@ -141,6 +143,13 @@ export function validateSeriesDefinition(definition: SeriesDefinition, context:
141
143
  if (!Array.isArray(definition.tags) || definition.tags.length > 16) fail(id, "tags must be a list of at most 16 words.");
142
144
  for (const tag of definition.tags) checkLabel(id, "tag", tag, 40, context, false);
143
145
  }
146
+ if (definition.aliases !== undefined) {
147
+ if (!Array.isArray(definition.aliases) || definition.aliases.length > 4) fail(id, "aliases must be a list of at most 4 words.");
148
+ if (definition.input !== undefined) fail(id, "a series with an input takes no aliases: the word would stand for a ticker.");
149
+ for (const alias of definition.aliases) {
150
+ if (typeof alias !== "string" || !/^[a-z][a-z-]{3,31}$/.test(alias)) fail(id, `alias "${String(alias)}" must be one lower-case word of 4 to 32 letters.`);
151
+ }
152
+ }
144
153
  if (definition.url !== undefined && (typeof definition.url !== "string" || !/^https?:\/\/\S+$/.test(definition.url))) {
145
154
  fail(id, "url must be an http or https address.");
146
155
  }