@gloom-sh/terminal 0.19.2 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (472) hide show
  1. package/README.md +7 -1
  2. package/package.json +1 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +20 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app.tsx +23 -1
  20. package/src/architecture/core-builtin-allowlist.json +2 -1
  21. package/src/capabilities/factories.ts +1 -0
  22. package/src/cli/ascii-output.ts +42 -0
  23. package/src/cli/base-converter.ts +10 -2
  24. package/src/cli/command-options.ts +16 -1
  25. package/src/cli/commands/crypto-hints.ts +23 -4
  26. package/src/cli/commands/market.ts +92 -23
  27. package/src/cli/commands/news-search.ts +24 -5
  28. package/src/cli/commands/overview.ts +9 -4
  29. package/src/cli/commands/search.ts +71 -12
  30. package/src/cli/commands/system.ts +22 -30
  31. package/src/cli/commands/ticker.ts +49 -9
  32. package/src/cli/commands/version.ts +33 -0
  33. package/src/cli/context.ts +5 -9
  34. package/src/cli/data-dir.ts +7 -0
  35. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  36. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  37. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  38. package/src/cli/desktop-pane-shot.ts +196 -256
  39. package/src/cli/early-command.ts +42 -0
  40. package/src/cli/earnings-calendar.ts +9 -0
  41. package/src/cli/entry.ts +19 -13
  42. package/src/cli/errors.ts +12 -2
  43. package/src/cli/help.ts +1 -1
  44. package/src/cli/history-export.ts +22 -2
  45. package/src/cli/history-rows.ts +154 -4
  46. package/src/cli/index.ts +18 -1
  47. package/src/cli/known-gaps.ts +40 -5
  48. package/src/cli/listing-arg.ts +46 -9
  49. package/src/cli/options.ts +1 -1
  50. package/src/cli/output-file.ts +60 -13
  51. package/src/cli/pane-functions/catalog.ts +14 -3
  52. package/src/cli/pane-functions/data.ts +1 -1
  53. package/src/cli/pane-functions/dom.ts +45 -0
  54. package/src/cli/pane-functions/freshness.ts +46 -9
  55. package/src/cli/pane-functions/function-help.ts +17 -9
  56. package/src/cli/pane-functions/headless.ts +22 -14
  57. package/src/cli/pane-functions/index.ts +6 -2
  58. package/src/cli/pane-functions/options.ts +6 -2
  59. package/src/cli/pane-functions/resolver.ts +27 -5
  60. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  61. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  62. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  63. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  64. package/src/cli/quote-answer.ts +20 -0
  65. package/src/cli/registry.ts +41 -13
  66. package/src/cli/result.ts +12 -6
  67. package/src/cli/ticker-resolution.ts +2 -1
  68. package/src/cli/types.ts +2 -0
  69. package/src/components/chart/composite/format.ts +3 -0
  70. package/src/components/chart-table/header.tsx +1 -1
  71. package/src/components/chart-table/layout.ts +28 -11
  72. package/src/components/command-bar/action-runtime.ts +4 -0
  73. package/src/components/command-bar/commands/direct/index.ts +18 -33
  74. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  75. package/src/components/command-bar/commands/registry.ts +10 -1
  76. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  77. package/src/components/command-bar/routes/root/runtime.ts +24 -1
  78. package/src/components/command-bar/surface/index.tsx +9 -3
  79. package/src/components/company-logo.tsx +2 -1
  80. package/src/components/layout/header.tsx +8 -3
  81. package/src/components/layout/market-summary.ts +25 -7
  82. package/src/components/layout/pane/footer/index.tsx +7 -2
  83. package/src/components/layout/pane/footer/model.ts +40 -5
  84. package/src/components/market-board/index.tsx +3 -2
  85. package/src/components/onboarding/desks-step.tsx +12 -4
  86. package/src/components/onboarding/first-run-workspace.ts +81 -4
  87. package/src/components/onboarding/onboarding-frame.tsx +2 -1
  88. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  89. package/src/components/onboarding/onboarding-wizard.tsx +54 -23
  90. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  91. package/src/components/onboarding/run-setup-again.ts +24 -0
  92. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  93. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  94. package/src/components/onboarding/wizard-model.ts +57 -3
  95. package/src/components/onboarding/wizard-positions.ts +1 -1
  96. package/src/components/ui/delayed-mark.tsx +56 -0
  97. package/src/components/ui/external-link.tsx +3 -6
  98. package/src/components/ui/icon.tsx +8 -1
  99. package/src/components/ui/logo-mark.tsx +8 -5
  100. package/src/components/ui/stat-grid.tsx +263 -19
  101. package/src/data/config/store/node.ts +11 -5
  102. package/src/data/config/store/normalize.ts +16 -3
  103. package/src/data/profile-permissions.ts +47 -0
  104. package/src/data/resource-store.ts +34 -31
  105. package/src/data/sqlite/access.ts +19 -8
  106. package/src/data/sqlite/checkpointer.ts +95 -8
  107. package/src/data/sqlite/database.ts +40 -7
  108. package/src/docs/snapshot.json +191 -78
  109. package/src/i18n/es.ts +18 -2
  110. package/src/i18n/ja.ts +18 -2
  111. package/src/i18n/ko.ts +18 -2
  112. package/src/i18n/zh-cn.ts +18 -2
  113. package/src/i18n/zh-tw.ts +18 -2
  114. package/src/layout/desk-keys.ts +4 -1
  115. package/src/layout/desks.ts +38 -5
  116. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  117. package/src/market-data/batched-reads.ts +16 -0
  118. package/src/market-data/coordinator/auxiliary.ts +4 -2
  119. package/src/market-data/coordinator/index.ts +11 -2
  120. package/src/market-data/futures-board.ts +1 -1
  121. package/src/market-data/futures-contract.ts +3 -1
  122. package/src/market-data/market/index-level.ts +6 -4
  123. package/src/market-data/published-apac-sessions.ts +6 -0
  124. package/src/news/aggregator.ts +7 -4
  125. package/src/news/news-model.ts +11 -0
  126. package/src/news/story-id.ts +22 -0
  127. package/src/news/ticker-symbols.ts +17 -1
  128. package/src/notifications/app-notifier.ts +35 -14
  129. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  130. package/src/plugins/builtin/account-management/index.tsx +2 -3
  131. package/src/plugins/builtin/account-management/model.ts +1 -1
  132. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  133. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  134. package/src/plugins/builtin/alerts/events.ts +10 -2
  135. package/src/plugins/builtin/alerts/index.tsx +5 -5
  136. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  137. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  138. package/src/plugins/builtin/alerts/targets.ts +4 -1
  139. package/src/plugins/builtin/attention/headless.ts +1 -1
  140. package/src/plugins/builtin/attention/index.tsx +2 -4
  141. package/src/plugins/builtin/attention/listing.ts +1 -1
  142. package/src/plugins/builtin/attention/series.ts +1 -1
  143. package/src/plugins/builtin/awards/index.tsx +1 -2
  144. package/src/plugins/builtin/backtest/client.ts +1 -0
  145. package/src/plugins/builtin/backtest/index.tsx +1 -2
  146. package/src/plugins/builtin/backtest/rules.ts +3 -1
  147. package/src/plugins/builtin/backtest/window.ts +8 -7
  148. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  149. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  150. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  151. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  152. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  153. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  154. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  155. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  156. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  157. package/src/plugins/builtin/buildout/format.ts +9 -0
  158. package/src/plugins/builtin/buildout/index.tsx +51 -5
  159. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  160. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  161. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  162. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  163. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  164. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  165. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  166. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  167. package/src/plugins/builtin/cds/headless.ts +10 -2
  168. package/src/plugins/builtin/cds/model.ts +7 -3
  169. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  170. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  171. package/src/plugins/builtin/changelog/index.tsx +3 -2
  172. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -2
  173. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  174. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  175. package/src/plugins/builtin/chart-composer/pane.tsx +26 -2
  176. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  177. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  178. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  179. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  180. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  181. package/src/plugins/builtin/chat/index.tsx +5 -5
  182. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  183. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  184. package/src/plugins/builtin/cloud/index.tsx +1 -2
  185. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  186. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  187. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  188. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  189. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  190. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  191. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  192. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  193. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  194. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  195. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  196. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  197. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  198. package/src/plugins/builtin/connections/index.ts +2 -3
  199. package/src/plugins/builtin/cot/index.tsx +1 -2
  200. package/src/plugins/builtin/cpi/index.tsx +1 -2
  201. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  202. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  203. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  204. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  205. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  206. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  207. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  208. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  209. package/src/plugins/builtin/crypto-board/series.ts +2 -0
  210. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  211. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  212. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  213. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  214. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  215. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  216. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  217. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  218. package/src/plugins/builtin/debug/index.tsx +1 -1
  219. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  220. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  221. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  222. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  223. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  224. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  225. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  226. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  227. package/src/plugins/builtin/docs/index.tsx +2 -3
  228. package/src/plugins/builtin/doe/headless.ts +16 -3
  229. package/src/plugins/builtin/doe/index.tsx +5 -4
  230. package/src/plugins/builtin/earnings/board.tsx +2 -1
  231. package/src/plugins/builtin/earnings/history.tsx +2 -1
  232. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  233. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  234. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  235. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  236. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  237. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  238. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  239. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  240. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  241. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  242. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  243. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  244. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  245. package/src/plugins/builtin/exposure/index.tsx +1 -2
  246. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  247. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  248. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  249. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  250. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  251. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  252. package/src/plugins/builtin/futures/index.tsx +15 -0
  253. package/src/plugins/builtin/futures/model.ts +14 -0
  254. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  255. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  256. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  257. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  258. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  259. package/src/plugins/builtin/gpu/board.tsx +51 -12
  260. package/src/plugins/builtin/gpu/client.ts +15 -2
  261. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  262. package/src/plugins/builtin/gpu/headless.ts +14 -2
  263. package/src/plugins/builtin/gpu/index.tsx +1 -2
  264. package/src/plugins/builtin/gpu/model.ts +20 -2
  265. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  266. package/src/plugins/builtin/help/index.tsx +1 -1
  267. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  268. package/src/plugins/builtin/holders/index.tsx +2 -3
  269. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  270. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  271. package/src/plugins/builtin/jodi/index.tsx +1 -2
  272. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  273. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  274. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  275. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  276. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  277. package/src/plugins/builtin/market-movers/index.tsx +7 -1
  278. package/src/plugins/builtin/market-movers/session-body.tsx +4 -1
  279. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  280. package/src/plugins/builtin/members/index.tsx +1 -2
  281. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  282. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  283. package/src/plugins/builtin/news/headless.ts +7 -0
  284. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  285. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  286. package/src/plugins/builtin/notes/index.tsx +3 -6
  287. package/src/plugins/builtin/openfda/index.tsx +1 -2
  288. package/src/plugins/builtin/options/view.tsx +6 -5
  289. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  290. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  291. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  292. package/src/plugins/builtin/perps/index.tsx +1 -2
  293. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  294. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  295. package/src/plugins/builtin/plugin-module.ts +16 -2
  296. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  297. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  298. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  299. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  300. package/src/plugins/builtin/power/index.tsx +1 -2
  301. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  302. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  303. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  304. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  305. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  306. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  307. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  308. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  309. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  310. package/src/plugins/builtin/research/index.tsx +5 -8
  311. package/src/plugins/builtin/research-search/index.tsx +1 -2
  312. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  313. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  314. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  315. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  316. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  317. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  318. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  319. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  320. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  321. package/src/plugins/builtin/sec/headless.ts +5 -1
  322. package/src/plugins/builtin/sec/index.tsx +4 -2
  323. package/src/plugins/builtin/sec/model.ts +2 -0
  324. package/src/plugins/builtin/sectors/index.tsx +11 -5
  325. package/src/plugins/builtin/series-inventory.ts +2 -0
  326. package/src/plugins/builtin/shared/cloud-resource.ts +60 -0
  327. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  328. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  329. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  330. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  331. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  332. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  333. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  334. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  335. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  336. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  337. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  338. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  339. package/src/plugins/builtin/thirteenf/api.ts +116 -13
  340. package/src/plugins/builtin/thirteenf/data.ts +132 -30
  341. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  342. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  343. package/src/plugins/builtin/thirteenf/pane.tsx +20 -11
  344. package/src/plugins/builtin/thirteenf/types.ts +12 -0
  345. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +17 -4
  346. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  347. package/src/plugins/builtin/ticker-detail/headless.ts +11 -3
  348. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  349. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  350. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  351. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  352. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  353. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  354. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  355. package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  357. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  358. package/src/plugins/builtin/volatility/index.tsx +1 -2
  359. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  360. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  361. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  362. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  363. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  364. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  365. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  366. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  367. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  368. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  369. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  370. package/src/plugins/bundle.ts +52 -2
  371. package/src/plugins/catalog-browser.ts +2 -2
  372. package/src/plugins/catalog-cli.ts +20 -0
  373. package/src/plugins/host-link.ts +13 -1
  374. package/src/plugins/installer.ts +15 -4
  375. package/src/plugins/loader.ts +43 -11
  376. package/src/plugins/peer-imports.ts +201 -0
  377. package/src/plugins/peer-retry.ts +26 -29
  378. package/src/plugins/plugin-process.ts +128 -0
  379. package/src/plugins/redirection-guard.ts +45 -0
  380. package/src/remote/endpoint.ts +16 -3
  381. package/src/renderers/browser/main.tsx +3 -2
  382. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  383. package/src/renderers/dom/desktop/icons.tsx +2 -0
  384. package/src/renderers/dom/host/image-surface.tsx +46 -21
  385. package/src/renderers/dom/styles.css +7 -0
  386. package/src/renderers/dom/toast-host.tsx +2 -0
  387. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  388. package/src/renderers/electrobun/bun/desktop/initialization.ts +3 -2
  389. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  390. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  391. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  392. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  393. package/src/renderers/electrobun/bun/window/create.ts +4 -2
  394. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  395. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  396. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  397. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  398. package/src/renderers/electrobun/view/main.tsx +7 -2
  399. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  400. package/src/renderers/opentui/browser-open.ts +10 -4
  401. package/src/renderers/opentui/image/surface.tsx +1 -1
  402. package/src/renderers/opentui/start.tsx +12 -6
  403. package/src/renderers/opentui/toast-host.tsx +2 -1
  404. package/src/series/registry.ts +18 -0
  405. package/src/series/standalone.ts +10 -1
  406. package/src/series/types.ts +7 -0
  407. package/src/series/validate.ts +11 -2
  408. package/src/sources/gloomberb-cloud/index.ts +28 -5
  409. package/src/sources/gloomberb-cloud/news.ts +24 -3
  410. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  411. package/src/sources/provider-errors.ts +26 -3
  412. package/src/sources/provider-router/history.ts +5 -0
  413. package/src/sources/provider-router/index.ts +13 -0
  414. package/src/sync/core-contributors.ts +32 -0
  415. package/src/telemetry/attention-counts.ts +64 -6
  416. package/src/telemetry/attention-setting.ts +33 -0
  417. package/src/telemetry/crash-reports-node.ts +3 -1
  418. package/src/telemetry/stall-clocks.ts +93 -0
  419. package/src/telemetry/stall-reports-node.ts +12 -7
  420. package/src/telemetry/stall-reports.ts +34 -5
  421. package/src/test-support/preload.ts +9 -0
  422. package/src/tickers/company-name.ts +19 -0
  423. package/src/tickers/listing-fate.ts +14 -3
  424. package/src/tickers/saved-instrument.ts +17 -0
  425. package/src/tickers/search/home-listing.ts +13 -2
  426. package/src/tickers/search/index.ts +4 -1
  427. package/src/tickers/search/types.ts +6 -1
  428. package/src/time-series/bar-aggregation.ts +7 -1
  429. package/src/time-series/base-series.ts +3 -3
  430. package/src/time-series/chart-bounds.ts +1 -1
  431. package/src/time-series/chart-data.ts +5 -2
  432. package/src/time-series/chart-seed.ts +1 -1
  433. package/src/time-series/coin-session-close.ts +211 -0
  434. package/src/time-series/live-bars.ts +13 -2
  435. package/src/time-series/live-quotes.ts +1 -1
  436. package/src/time-series/official-close.ts +159 -0
  437. package/src/time-series/price-comparison.ts +55 -6
  438. package/src/time-series/resolve.ts +12 -3
  439. package/src/time-series/series-presentation.ts +1 -1
  440. package/src/time-series/studies/formula.ts +205 -0
  441. package/src/time-series/studies/indicators.ts +299 -0
  442. package/src/time-series/studies/leverage.ts +75 -0
  443. package/src/time-series/studies/pair.ts +138 -0
  444. package/src/time-series/studies/resolve.ts +333 -0
  445. package/src/time-series/studies/shared.ts +141 -0
  446. package/src/time-series/studies/units.ts +243 -0
  447. package/src/time-series/studies/vwap.ts +274 -0
  448. package/src/types/config.ts +18 -2
  449. package/src/types/data-provider.ts +23 -0
  450. package/src/types/financials.ts +22 -0
  451. package/src/types/headless.ts +42 -3
  452. package/src/types/plugin.ts +32 -3
  453. package/src/ui/host.tsx +11 -0
  454. package/src/ui/toast.tsx +26 -1
  455. package/src/utils/ascii-text.ts +140 -0
  456. package/src/utils/cli-output.ts +91 -18
  457. package/src/utils/concurrency.ts +24 -0
  458. package/src/utils/exchanges.ts +6 -0
  459. package/src/utils/external-url.ts +20 -7
  460. package/src/utils/futures-generic.ts +4 -4
  461. package/src/utils/http-proxy-upstream.ts +124 -0
  462. package/src/utils/main-thread-stall.ts +75 -3
  463. package/src/utils/perf-marks.ts +36 -2
  464. package/src/utils/private-files.ts +141 -0
  465. package/src/utils/public-address.ts +99 -0
  466. package/src/account/upgrade-personalization.ts +0 -57
  467. package/src/account/upgrade-tickers.ts +0 -124
  468. package/src/api-client/upgrade-experiment.ts +0 -91
  469. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  470. package/src/renderers/electrobun/launcher/preload.ts +0 -13
  471. package/src/renderers/electrobun/launcher/windows-emulation.ts +0 -50
  472. package/src/time-series/studies.ts +0 -1628
@@ -7,7 +7,6 @@ import {
7
7
  RESEARCH_SEARCH_PANE_ID,
8
8
  RESEARCH_SEARCH_TEMPLATE_ID,
9
9
  } from "./model";
10
- import { ResearchSearchPane } from "./pane";
11
10
 
12
11
  const description =
13
12
  "Full-text search across earnings call transcripts, news wires, and SEC filings, with saved searches and keyword alerts.";
@@ -40,7 +39,7 @@ export const researchSearchPlugin: GloomPlugin = {
40
39
  reportFreshness: { status: "not-a-feed", basis: "search results" },
41
40
  name: "Research Search",
42
41
  icon: "RS",
43
- component: ResearchSearchPane,
42
+ get component(): typeof import("./pane").ResearchSearchPane { const { ResearchSearchPane } = require("./pane"); return ResearchSearchPane; },
44
43
  defaultPosition: "right",
45
44
  defaultMode: "floating",
46
45
  defaultFloatingSize: { width: 118, height: 32 },
@@ -1,13 +1,25 @@
1
1
  import type { RevenueBreakdownView } from "../../../api-client/revenue-breakdown";
2
2
  import type { HeadlessPaneColumn, HeadlessPaneDefinition } from "../../../types/plugin";
3
3
  import { fetchRevenueBreakdown } from "./client";
4
- import { growthPercent, quarterLabel, reportedSpan, revenueAmount, sharePercent } from "./model";
4
+ import { growthPercent, quarterLabel, reportedSpan, revenueScale, sharePercent } from "./model";
5
5
  import { SEC_FILINGS } from "../shared/report-freshness";
6
6
  import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
7
7
 
8
- /** Amounts compact and shares and growth in percent, as the pane prints them (15.97B, 83.7%, +10.8%); exports keep the whole number. */
8
+ /** Shares and growth in percent, as the pane prints them (83.7%, +10.8%), amounts on the table's scale; exports keep the whole number. */
9
9
  const number = (format: (value: number | null) => string) => (value: unknown) => format(typeof value === "number" ? value : null);
10
- const amountColumn = (key: string, header: string): HeadlessPaneColumn => ({ key, header, align: "right", textFormat: number(revenueAmount) });
10
+
11
+ /**
12
+ * A table's amounts on its one scale (`revenueScale`), named once in the text
13
+ * report's heading ("Total revenue, USD millions"). Exports keep the section
14
+ * name and the whole number.
15
+ */
16
+ function amountScale(values: ReadonlyArray<number | null>, currency: string | null) {
17
+ const scale = revenueScale(values, currency);
18
+ return {
19
+ section: (title: string) => ({ title, ...scale.unit ? { textTitle: `${title}, ${scale.unit}` } : {} }),
20
+ column: (key: string, header: string): HeadlessPaneColumn => ({ key, header, align: "right", textFormat: number(scale.format) }),
21
+ };
22
+ }
11
23
 
12
24
  export const revenueBreakdownHeadless: HeadlessPaneDefinition<"bundle"> = {
13
25
  shape: "bundle",
@@ -46,16 +58,18 @@ export const revenueBreakdownHeadless: HeadlessPaneDefinition<"bundle"> = {
46
58
  ctx.apiClient,
47
59
  await resolveHeadlessIssuerListing(ctx, symbol),
48
60
  ));
61
+ const rowScale = amountScale(data.rows.flatMap((row) => [...row.values, row.ttm]), data.currency);
62
+ const totalScale = amountScale(data.total, data.currency);
49
63
  return {
50
64
  complete: data.access === "full",
51
65
  errors: data.lockedRows > 0 ? [`${data.lockedRows} more rows need Gloom Pro`] : [],
52
66
  sections: [
53
67
  {
54
- title: `Quarterly revenue by ${data.view}`,
68
+ ...rowScale.section(`Quarterly revenue by ${data.view}`),
55
69
  columns: [
56
70
  { key: data.view, header: data.view[0]!.toUpperCase() + data.view.slice(1) },
57
- ...data.periods.map((period) => amountColumn(quarterLabel(period), quarterLabel(period))),
58
- amountColumn("ttm", "TTM"),
71
+ ...data.periods.map((period) => rowScale.column(quarterLabel(period), quarterLabel(period))),
72
+ rowScale.column("ttm", "TTM"),
59
73
  { key: "share", header: "Share", align: "right", textFormat: number(sharePercent) },
60
74
  { key: "yoy", header: "YoY", align: "right", textFormat: number(growthPercent) },
61
75
  ],
@@ -68,8 +82,8 @@ export const revenueBreakdownHeadless: HeadlessPaneDefinition<"bundle"> = {
68
82
  })),
69
83
  },
70
84
  {
71
- title: "Total revenue",
72
- columns: [{ key: "quarter", header: "Quarter" }, { key: "end", header: "End" }, amountColumn("revenue", "Revenue")],
85
+ ...totalScale.section("Total revenue"),
86
+ columns: [{ key: "quarter", header: "Quarter" }, { key: "end", header: "End" }, totalScale.column("revenue", "Revenue")],
73
87
  rows: data.periods.map((period, index) => ({
74
88
  quarter: quarterLabel(period),
75
89
  end: period.end,
@@ -1,4 +1,5 @@
1
1
  import type { DataTableColumn } from "../../../components";
2
+ import { pickUnit } from "../../../utils/format";
2
3
  import type {
3
4
  RevenueBreakdownPayload,
4
5
  RevenueBreakdownPeriod,
@@ -83,6 +84,28 @@ export function revenueAmount(value: number | null): string {
83
84
  return `${value < 0 ? "-" : ""}${scaled.toFixed(digits)}${suffix}`;
84
85
  }
85
86
 
87
+ const SCALE_WORDS: Record<string, string> = { K: "thousands", M: "millions", B: "billions", T: "trillions" };
88
+
89
+ /**
90
+ * One scale for a report table's amounts, its largest one's, so a 0.0 never sits beside a 5.52M: `unit`
91
+ * names it once ("USD millions") and `format` prints each amount in it, with one count of decimals down the
92
+ * table (179.3 and 0.0, or 54.32 and 7.88). An amount too small for that many decimals keeps two significant
93
+ * digits (0.030), so only a true zero reads 0.0. The pane keeps each amount's own suffix (`revenueAmount`).
94
+ */
95
+ export function revenueScale(values: ReadonlyArray<number | null>, currency: string | null): { unit: string; format: (value: number | null) => string } {
96
+ const amounts = values.filter((value): value is number => value !== null && Number.isFinite(value));
97
+ const { suffix, divisor } = pickUnit(amounts);
98
+ const digits = Math.max(0, ...amounts.map((value) => Math.abs(value) / divisor)) >= 100 ? 1 : 2;
99
+ const format = (value: number | null) => {
100
+ if (value === null || !Number.isFinite(value)) return revenueAmount(null);
101
+ const scaled = value / divisor;
102
+ if (scaled === 0 || Number(scaled.toFixed(digits)) !== 0) return scaled.toFixed(digits);
103
+ // Two significant digits: as many decimals as the first one's place, and one more.
104
+ return scaled.toFixed(Math.min(20, 1 - Math.floor(Math.log10(Math.abs(scaled)))));
105
+ };
106
+ return { unit: [currency?.trim(), SCALE_WORDS[suffix]].filter(Boolean).join(" "), format };
107
+ }
108
+
86
109
  export const sharePercent = (value: number | null) =>
87
110
  value === null ? "--" : `${(value * 100).toFixed(1)}%`;
88
111
 
@@ -110,7 +110,9 @@ export function ReverseDcfPane({ width, height, focused }: PaneProps) {
110
110
  }, [model]);
111
111
 
112
112
  const selectedId = String(discountRate);
113
- const contentHeight = Math.max(4, height - 1 - statGridRows(stats, width));
113
+ // A figure wraps only into rows the table's least leaves, so the pane never spills.
114
+ const statMaxLines = height - 1 - 4;
115
+ const contentHeight = Math.max(4, height - 1 - statGridRows(stats, width, undefined, { maxLines: statMaxLines }));
114
116
 
115
117
  return <Box width={width} height={height} flexDirection="column" overflow="hidden">
116
118
  <QueryBar width={width} filters={[
@@ -120,7 +122,7 @@ export function ReverseDcfPane({ width, height, focused }: PaneProps) {
120
122
  {!symbol ? <EmptyState title="Select a ticker." /> : <PaneStatusBody subject="reverse DCF" loading={inputs.loading && !model}
121
123
  error={!model ? inputs.error ?? identityError ?? null : model.error} empty={false}>
122
124
  {model && !model.error ? <>
123
- <StatGrid items={stats} width={width} />
125
+ <StatGrid items={stats} width={width} maxLines={statMaxLines} />
124
126
  <DataTableView<Row> focused={focused} columns={COLUMNS} items={model.sensitivity} rootWidth={width} rootHeight={contentHeight}
125
127
  getItemKey={(row) => String(row.discountRate)} emptyStateTitle="No implied growth." sortColumnId={null} sortDirection="desc"
126
128
  selection={{ kind: "id", selectedId, getId: (row) => String(row.discountRate),
@@ -1,5 +1,4 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
- import { RoadmapPane } from "./pane";
3
2
 
4
3
  export const roadmapModule: PluginModule = {
5
4
  panes: [
@@ -7,8 +6,8 @@ export const roadmapModule: PluginModule = {
7
6
  id: "roadmap",
8
7
  name: "Roadmap",
9
8
  icon: "R",
10
- component: RoadmapPane,
11
- reportFreshness: { status: "not-a-feed", basis: "curated list" },
9
+ get component(): typeof import("./pane").RoadmapPane { const { RoadmapPane } = require("./pane"); return RoadmapPane; },
10
+ reportFreshness: { status: "not-a-feed", basis: "curated list", local: true },
12
11
  defaultPosition: "right",
13
12
  defaultMode: "floating",
14
13
  defaultFloatingSize: { width: 100, height: 30 },
@@ -1,4 +1,4 @@
1
- import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type RefObject } from "react";
1
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from "react";
2
2
  import { apiClient } from "../../../api-client";
3
3
  import { usePlanAccess } from "../../../api-client/plan-access";
4
4
  import type { RoadmapItem, RoadmapVote } from "../../../api-client/roadmap";
@@ -7,7 +7,9 @@ import {
7
7
  DataTableStackView,
8
8
  EMPTY_TABLE_CELL,
9
9
  isSectionedItemRow,
10
+ PaneFooterScope,
10
11
  renderSectionedRowHeader,
12
+ usePaneTabs,
11
13
  usePaneStatusFooter,
12
14
  useRowNotes,
13
15
  type DataTableCell,
@@ -23,6 +25,7 @@ import { useThemeColors } from "../../../theme/theme-context";
23
25
  import type { PaneProps } from "../../../types/plugin";
24
26
  import { Box, TextAttributes, type ScrollBoxRenderable } from "../../../ui";
25
27
  import { formatShortDate } from "../../../utils/datetime-format";
28
+ import { ChangelogPane } from "../changelog";
26
29
  import {
27
30
  isVotable,
28
31
  optimisticVote,
@@ -97,7 +100,7 @@ function RoadmapDetail({ item, failure, width, scrollRef }: {
97
100
  );
98
101
  }
99
102
 
100
- export function RoadmapPane({ focused, width, height }: PaneProps) {
103
+ function RoadmapView({ focused, width, height }: PaneProps) {
101
104
  const colors = useThemeColors();
102
105
  const client = useContext(RoadmapClientContext) ?? apiClient;
103
106
  const access = usePlanAccess();
@@ -242,3 +245,47 @@ export function RoadmapPane({ focused, width, height }: PaneProps) {
242
245
  />
243
246
  );
244
247
  }
248
+
249
+ /**
250
+ * What is coming and what came: the roadmap's features, and the Changelog as
251
+ * CHG shows it. The first tab is not "Roadmap", which the title already says.
252
+ */
253
+ const VIEWS = [
254
+ { value: "roadmap", label: "Features" },
255
+ { value: "changelog", label: "Changelog" },
256
+ ] as const;
257
+ type ViewId = (typeof VIEWS)[number]["value"];
258
+ const VIEW_PANES: Record<ViewId, (props: PaneProps) => ReactNode> = {
259
+ roadmap: RoadmapView,
260
+ changelog: ChangelogPane,
261
+ };
262
+
263
+ export function RoadmapPane(props: PaneProps) {
264
+ const [storedView, setView] = usePluginPaneState<string>("view", "roadmap");
265
+ const view: ViewId = storedView === "changelog" ? "changelog" : "roadmap";
266
+ // A view stays mounted once shown, so coming back to it keeps its place.
267
+ const [mounted, setMounted] = useState<ReadonlySet<ViewId>>(() => new Set([view]));
268
+ useEffect(() => {
269
+ setMounted((current) => (current.has(view) ? current : new Set([...current, view])));
270
+ }, [view]);
271
+ const { strip, rows } = usePaneTabs({ tabs: [...VIEWS], activeValue: view, onSelect: setView, focused: props.focused, dense: true });
272
+ const bodyHeight = Math.max(1, props.height - rows);
273
+
274
+ return (
275
+ <Box width={props.width} height={props.height} flexDirection="column" overflow="hidden">
276
+ {strip}
277
+ {VIEWS.map(({ value }) => {
278
+ if (!mounted.has(value) && value !== view) return null;
279
+ const View = VIEW_PANES[value];
280
+ const active = value === view;
281
+ return (
282
+ <Box key={value} visible={active} height={bodyHeight} flexGrow={1} flexBasis={0} overflow="hidden" data-gloom-role="nested-pane-content">
283
+ <PaneFooterScope active={active}>
284
+ <View {...props} height={bodyHeight} focused={props.focused && active} />
285
+ </PaneFooterScope>
286
+ </Box>
287
+ );
288
+ })}
289
+ </Box>
290
+ );
291
+ }
@@ -1,4 +1,5 @@
1
- import type { ScannerHiloExtreme, ScannerHiloPayload } from "../../../api-client";
1
+ import type { ScannerHiloExtreme, ScannerHiloPayload, ScannerStatus } from "../../../api-client";
2
+ import { venueMarketLine } from "../../../cli/pane-functions/freshness";
2
3
 
3
4
  type HiloWindowKey = keyof ScannerHiloPayload["windows"];
4
5
  export type HiloMinPrice = "off" | "1" | "5";
@@ -94,3 +95,19 @@ export function filterHiloRows(
94
95
  ? [...filtered].sort((left, right) => right.count - left.count || right.at - left.at)
95
96
  : [...filtered].sort((left, right) => right.at - left.at);
96
97
  }
98
+
99
+ /**
100
+ * What an empty table says. The price filter when rows came in and it hid them all, or when the market is open and
101
+ * nothing has printed a new extreme yet. A closed market says so and when it reopens, in the status line's words
102
+ * (`Markets closed until Mon.`), as SCAN says its own close: its session has no new highs or lows to count.
103
+ */
104
+ export function hiloEmptyState(
105
+ { filtered, status, now }: { filtered: boolean; status: ScannerStatus | undefined; now: number },
106
+ ): { title: string; hint?: string } {
107
+ if (filtered || status !== "closed") return { title: "Nothing above the price filter yet." };
108
+ const line = venueMarketLine("NYSE", now);
109
+ return {
110
+ title: line?.state === "closed" ? `${line.text[0]!.toUpperCase()}${line.text.slice(1)}.` : "Market closed.",
111
+ hint: "New highs and lows resume at the open.",
112
+ };
113
+ }
@@ -18,7 +18,7 @@ import { usePluginTickerActions } from "../../runtime";
18
18
  import { ScannerDeniedState } from "./denied";
19
19
  import { useHiloFeed, useScannerStatusFooter } from "./feed";
20
20
  import { HiloBars } from "./hilo-bars";
21
- import { filterHiloRows, formatHiloPrice, type HiloMinPrice, type HiloSort } from "./hilo-model";
21
+ import { filterHiloRows, formatHiloPrice, hiloEmptyState, type HiloMinPrice, type HiloSort } from "./hilo-model";
22
22
 
23
23
  type Side = "lows" | "highs";
24
24
 
@@ -118,6 +118,15 @@ function HiloPane({ focused, width, height }: PaneProps) {
118
118
  );
119
119
 
120
120
  useScannerStatusFooter("hilo", feed, focused);
121
+ // Read when the feed moves, not each render: the clock only matters once a closed session's text is shown.
122
+ const status = feed.payload?.status;
123
+ const emptyStates = useMemo<Record<Side, { title: string; hint?: string }>>(() => {
124
+ const now = Date.now();
125
+ return {
126
+ lows: hiloEmptyState({ filtered: !!feed.payload?.lows.length, status, now }),
127
+ highs: hiloEmptyState({ filtered: !!feed.payload?.highs.length, status, now }),
128
+ };
129
+ }, [feed.payload?.lows.length, feed.payload?.highs.length, status]);
121
130
 
122
131
  const split = width >= SPLIT_MIN_WIDTH;
123
132
  // Zero counts before the first payload would read as a dead market, not a loading one.
@@ -168,7 +177,8 @@ function HiloPane({ focused, width, height }: PaneProps) {
168
177
  renderCell={RENDER_ROW[side]}
169
178
  selectedTextOverridesCellColor
170
179
  emptyContent={feed.payload ? undefined : <PaneStatusBody loading loadingLabel="Waiting for the scanner..." />}
171
- emptyStateTitle="Nothing above the price filter yet."
180
+ emptyStateTitle={emptyStates[side].title}
181
+ emptyStateHint={emptyStates[side].hint}
172
182
  />
173
183
  );
174
184
 
@@ -100,9 +100,11 @@ const DISPLAY_ORDER = ["symbol", "name", "price", "change", "conditions", "rvol"
100
100
  /**
101
101
  * Symbol, name, price and change, then one column per condition with the
102
102
  * figure it matched on, then context as the width allows: a column is added
103
- * only when it fits whole, the row's status first.
103
+ * only when it fits whole, the row's status first, and only when a row has
104
+ * one (after the close none does, and an empty column would take the room of
105
+ * the figures).
104
106
  */
105
- function liveScanColumns(width: number, conditions: readonly LiveScanCondition[]): DataTableColumn[] {
107
+ export function liveScanColumns(width: number, conditions: readonly LiveScanCondition[], withStatus: boolean): DataTableColumn[] {
106
108
  const own = conditions.map((condition, index): DataTableColumn => {
107
109
  const label = conditionColumnLabel(condition);
108
110
  return { id: `c:${index}`, label, width: Math.max(8, label.length), align: "right" };
@@ -121,7 +123,7 @@ function liveScanColumns(width: number, conditions: readonly LiveScanCondition[]
121
123
  ));
122
124
  let columns = order([...base, ...own]);
123
125
  for (const column of OPTIONAL_COLUMNS) {
124
- if (repeats.has(column.id)) continue;
126
+ if (repeats.has(column.id) || (column.id === "status" && !withStatus)) continue;
125
127
  const next = order([...columns, column]);
126
128
  if (getTableWidth(next) > width) break;
127
129
  columns = next;
@@ -349,7 +351,8 @@ export default function LiveScanPane({ focused, width, height }: PaneProps) {
349
351
  [payload, track],
350
352
  );
351
353
  const matches = useMemo(() => sortMatches(payload?.matches ?? [], sort, noteOf), [noteOf, payload?.matches, sort]);
352
- const columns = useMemo(() => liveScanColumns(width, scan.conditions), [scan.conditions, width]);
354
+ const withStatus = useMemo(() => matches.some((match) => noteOf(match) != null), [matches, noteOf]);
355
+ const columns = useMemo(() => liveScanColumns(width, scan.conditions, withStatus), [scan.conditions, width, withStatus]);
353
356
 
354
357
  const apply = (next: LiveScan) => {
355
358
  setStoredScan(next);
@@ -3,6 +3,7 @@ import { useSecFilingContent } from "../../../market-data/hooks";
3
3
  import { getSharedMarketDataCoordinator, resolveEntryValue } from "../../../market-data/coordinator";
4
4
  import type { QueryEntry } from "../../../market-data/result-types";
5
5
  import type { SecFilingDocument, SecFilingItem } from "../../../types/data-provider";
6
+ import { logFailure } from "../../../utils/log-failure";
6
7
  import { documentContentTarget, isInlineExhibitDocument } from "./filing-documents";
7
8
 
8
9
  interface ScopedFilingContentCache {
@@ -41,18 +42,32 @@ export function buildInlineFilingContentTargets(
41
42
  .map((document) => documentContentTarget(filing, document));
42
43
  }
43
44
 
44
- export function nextUncachedFilingContentTarget(
45
+ /** Filings whose content is read at once: the queue's head and the next ones read ahead. */
46
+ const CONTENT_READS_AT_ONCE = 4;
47
+
48
+ /** The first `count` targets not cached yet, once each, in the caller's priority order. */
49
+ export function nextUncachedFilingContentTargets(
45
50
  targets: readonly SecFilingItem[],
46
51
  cache: ReadonlyMap<string, string | null>,
47
- ): SecFilingItem | null {
52
+ count: number,
53
+ ): SecFilingItem[] {
48
54
  const seen = new Set<string>();
55
+ const next: SecFilingItem[] = [];
49
56
  for (const target of targets) {
57
+ if (next.length >= count) break;
50
58
  const key = target.accessionNumber;
51
59
  if (seen.has(key)) continue;
52
60
  seen.add(key);
53
- if (!cache.has(key)) return target;
61
+ if (!cache.has(key)) next.push(target);
54
62
  }
55
- return null;
63
+ return next;
64
+ }
65
+
66
+ export function nextUncachedFilingContentTarget(
67
+ targets: readonly SecFilingItem[],
68
+ cache: ReadonlyMap<string, string | null>,
69
+ ): SecFilingItem | null {
70
+ return nextUncachedFilingContentTargets(targets, cache, 1)[0] ?? null;
56
71
  }
57
72
 
58
73
  export function useSecFilingContentCache({
@@ -81,12 +96,29 @@ export function useSecFilingContentCache({
81
96
  return () => { retryGeneration.current += 1; };
82
97
  }, [scopeKey]);
83
98
 
84
- const nextTarget = useMemo(
85
- () => nextUncachedFilingContentTarget(targets, contentCache),
99
+ const upcoming = useMemo(
100
+ () => nextUncachedFilingContentTargets(targets, contentCache, CONTENT_READS_AT_ONCE),
86
101
  [contentCache, targets],
87
102
  );
103
+ const nextTarget = upcoming[0] ?? null;
88
104
  const contentEntry = useSecFilingContent(nextTarget);
89
105
 
106
+ // The queue takes one filing at a time; the next few are read beside it, and
107
+ // taken from the coordinator once the head settles (below). Each is asked
108
+ // once per scope: the coordinator shares a read in flight and keeps it.
109
+ const readAhead = useRef<{ scopeKey: string; keys: Set<string> }>({ scopeKey, keys: new Set() });
110
+ useEffect(() => {
111
+ const coordinator = getSharedMarketDataCoordinator();
112
+ if (!coordinator) return;
113
+ if (readAhead.current.scopeKey !== scopeKey) readAhead.current = { scopeKey, keys: new Set() };
114
+ const asked = readAhead.current.keys;
115
+ for (const target of upcoming.slice(1)) {
116
+ if (asked.has(target.accessionNumber)) continue;
117
+ asked.add(target.accessionNumber);
118
+ void coordinator.loadSecFilingContent(target).catch(logFailure("sec.filing-content", "Reading filing content ahead failed"));
119
+ }
120
+ }, [scopeKey, upcoming]);
121
+
90
122
  useEffect(() => {
91
123
  if (!nextTarget) return;
92
124
  const first = settledFilingContent(contentEntry);
@@ -39,8 +39,12 @@ const SEC_COLUMNS: HeadlessPaneColumn[] = [
39
39
  /** Some row has 8-K item codes; a column every row leaves `-` takes no room in the text table (exports keep it). */
40
40
  const hasItems = (rows: readonly HeadlessPaneRow[]) => rows.some((row) => typeof row.items === "string" && row.items !== "");
41
41
 
42
+ /** A current report's press release exhibit, printed only when some row has one; exports keep it with the filing's link. */
43
+ const PRESS_RELEASE_COLUMN: HeadlessPaneColumn = { key: "pressReleaseUrl", header: "Press release" };
44
+
42
45
  function secTextColumns(rows: readonly HeadlessPaneRow[]): HeadlessPaneColumn[] {
43
- return hasItems(rows) ? SEC_COLUMNS : SEC_COLUMNS.filter((column) => column.key !== "items");
46
+ const columns = hasItems(rows) ? SEC_COLUMNS : SEC_COLUMNS.filter((column) => column.key !== "items");
47
+ return rows.some((row) => typeof row.pressReleaseUrl === "string" && row.pressReleaseUrl) ? [...columns, PRESS_RELEASE_COLUMN] : columns;
44
48
  }
45
49
 
46
50
  /**
@@ -457,9 +457,11 @@ function SecView({ width, height, focused, paneType }: { width: number; height:
457
457
  usePaneStatusLinkFooter({
458
458
  registrationId: "sec",
459
459
  focused,
460
- url: openFiling ? openFiling.filingUrl : factsUrl ?? abroadPage ?? announcements?.url,
460
+ // A current report links its press release, where its figures are, when its index names one.
461
+ url: openFiling ? openFiling.pressReleaseUrl ?? openFiling.filingUrl : factsUrl ?? abroadPage ?? announcements?.url,
461
462
  source: openFiling ? openFiling.form : facts?.covered ? listNames(fundFactPublishers(facts)) : announcements?.name ?? null,
462
- label: openFiling ? "filing" : factsUrl ? "source" : abroadPage ? "issuer page" : announcements ? "announcements" : "filing",
463
+ label: openFiling ? openFiling.pressReleaseUrl ? "press release" : "filing"
464
+ : factsUrl ? "source" : abroadPage ? "issuer page" : announcements ? "announcements" : "filing",
463
465
  // Rows read in the background for their insiders do not hold up the pane.
464
466
  loading: loading || loadingDocuments || (!!loadingKey && viewedTargets.some((target) => target.accessionNumber === loadingKey)),
465
467
  error: authWall ? null : error ?? detailError,
@@ -227,6 +227,8 @@ export function buildSecFilingRows(filings: readonly SecFilingItem[]) {
227
227
  cik: filing.cik,
228
228
  companyName: filing.companyName ?? null,
229
229
  url: filing.filingUrl,
230
+ // A current report's press release exhibit, where its index names one: the page its figures are on.
231
+ ...(filing.pressReleaseUrl ? { pressReleaseUrl: filing.pressReleaseUrl } : {}),
230
232
  };
231
233
  });
232
234
  }
@@ -16,6 +16,7 @@ import type { ExtendedSession } from "../../../market-data/market/status";
16
16
  import { useAutoRefresh, useUpdatedAgo } from "../../../react/auto-refresh";
17
17
  import { useLiveStreamingSetting } from "../../../state/hooks/live-streaming";
18
18
  import { isStreamCarryingQuote, useVisibleBoardSymbols } from "../shared/use-quote-board";
19
+ import { useQuoteDelayFooter } from "../shared/cloud-upgrade";
19
20
  import { SectorMoveBar } from "./move-bar";
20
21
  import {
21
22
  SECTOR_COLLECTIONS,
@@ -345,16 +346,21 @@ function EtfBoard({ collectionId, focused, width, height, rootBefore }: EtfBoard
345
346
  .map((row) => `${row.etf}: ${sectorRowIssues(row).join(" · ")}`);
346
347
  usePaneNoticeFooter({ registrationId: "sectors:row-issues", notices: rowIssueNotices, focused });
347
348
 
349
+ // Signed out or free, the ETFs' quotes are delayed ones.
350
+ const quoteDelay = useQuoteDelayFooter(rows.map((row) => (row.loading ? null : { dataSource: row.quoteDataSource })), "sectors-delay");
348
351
  usePaneFooter("sectors", () => {
349
352
  const info = loadingErrorFooterInfo(loading, loadError);
350
353
  if (returnAsOfDate) info.push({ id: "return-as-of", parts: [{ text: `returns as of ${returnAsOfDate}`, tone: "muted" }] });
351
354
  if (updatedAgo) info.push({ id: "updated", parts: [{ text: `checked ${updatedAgo}`, tone: "muted" }] });
352
355
  // Keep the leading current failure readable when the pane is narrow; separate
353
356
  // flex children would each shrink it to a few characters beside routine status.
354
- return { info: info.length > 0 ? [{ id: "status", parts: info.flatMap((segment, index) => [
355
- ...(index > 0 ? [{ text: "·", tone: "muted" as const }] : []), ...segment.parts,
356
- ]) }] : [] };
357
- }, [loadError, loading, returnAsOfDate, updatedAgo]);
357
+ return { info: [
358
+ ...(info.length > 0 ? [{ id: "status", parts: info.flatMap((segment, index) => [
359
+ ...(index > 0 ? [{ text: "·", tone: "muted" as const }] : []), ...segment.parts,
360
+ ]) }] : []),
361
+ ...(quoteDelay ? [quoteDelay] : []),
362
+ ] };
363
+ }, [loadError, loading, quoteDelay, returnAsOfDate, updatedAgo]);
358
364
 
359
365
  return (
360
366
  <DataTableView<SectorRow, SectorColumn>
@@ -442,7 +448,7 @@ export const sectorsModule: PluginModule = {
442
448
  paneId: "sectors",
443
449
  label: "Thematic Baskets",
444
450
  description: "Curated themes with equal-weight returns, breadth and member leaders and laggards.",
445
- keywords: ["themes", "thematic", "baskets", "AI compute", "datacenters", "electrification", "power grid", "nuclear", "uranium", "defense", "aerospace", "cybersecurity", "robotics", "automation", "critical minerals", "crypto", "bitcoin", "solar", "clean energy", "gold", "silver", "homebuilders", "memory", "chip equipment", "cloud software"],
451
+ keywords: ["themes", "thematic", "baskets", "AI compute", "datacenters", "electrification", "power grid", "nuclear", "uranium", "defense", "aerospace", "cybersecurity", "robotics", "automation", "critical minerals", "crypto", "bitcoin", "bitcoin miners", "miners", "bitcoin mining", "solar", "clean energy", "gold", "silver", "homebuilders", "memory", "chip equipment", "cloud software"],
446
452
  shortcut: { prefix: "THEM", argKind: "text", argPlaceholder: "theme", argOptional: true },
447
453
  headless: themesHeadless,
448
454
  // The theme param opens the Themes tab, and the named theme's members.
@@ -61,6 +61,7 @@ export const SERIES_INVENTORY: readonly SeriesInventoryEntry[] = [
61
61
  { module: "futures", chart: "Front-month futures board, charted from the board", series: ["fut:*"] },
62
62
  { module: "futures-curve", chart: "Futures curve by contract month", notInCatalog: "not-a-time-series", reason: "Plotted by delivery month; each front month is in the catalog as FUT:code." },
63
63
  { module: "crypto-board", chart: "Price sparklines for the large coins", series: ["crypto:*"] },
64
+ { module: "crypto-board", chart: "Bitcoin network hash rate, difficulty, next retarget estimate and hashprice", series: ["bitcoin:*"] },
64
65
  { module: "perps", chart: "Perpetual funding, open interest, premium and long/short account share", series: ["perps:*"] },
65
66
  { module: "crypto-etf-flows", chart: "The running net flow of the US spot bitcoin or ether ETFs since launch", notInCatalog: "not-yet", reason: "Daily and dated: the running total and each fund's daily flow could be crypto ETF series, and none is registered for the composer yet." },
66
67
  { module: "perps", chart: "Perpetual mark price", notInCatalog: "not-yet", reason: "The pair's price is in the catalog; the perpetual's own mark price could be a crypto series." },
@@ -96,6 +97,7 @@ export const SERIES_INVENTORY: readonly SeriesInventoryEntry[] = [
96
97
  { module: "analytics", chart: "Portfolio value, drawdown and factor exposure", notInCatalog: "account", reason: "Built from the user's own portfolio; the factor legs are tickers in the catalog." },
97
98
  { module: "holders", chart: "Holders treemap", notInCatalog: "not-a-time-series", reason: "A treemap of one filing date." },
98
99
  { module: "market-heatmap", chart: "Market heat map", notInCatalog: "not-a-time-series", reason: "A treemap of today's moves." },
100
+ { module: "buildout", chart: "TheBuildout's theme board", notInCatalog: "not-a-time-series", reason: "A treemap of the themes' market caps, coloured by their median 1Y return today." },
99
101
  { module: "portfolio-list", chart: "Portfolio treemap", notInCatalog: "not-a-time-series", reason: "A treemap of current positions." },
100
102
  { module: "world-venue-map", chart: "Chokepoint transits and the map's place series", series: ["geo:*"] },
101
103
  ];
@@ -1,5 +1,6 @@
1
1
  import { logFailure } from "../../../utils/log-failure";
2
2
  import { ApiRequestError } from "../../../api-client/errors";
3
+ import { createMissingRouteLatch } from "../../../api-client/coalesce";
3
4
  import { savedCopyForTransientFailure, type PluginCacheResult } from "../../../data/plugin-cache";
4
5
 
5
6
  interface CloudCache<T> {
@@ -81,3 +82,62 @@ export function unavailableOnServer(error: unknown, message: string, statuses: r
81
82
  ? new ApiRequestError(message, error.status, error.retryAfterMs, error.code, error.details)
82
83
  : error;
83
84
  }
85
+
86
+ /**
87
+ * Reads ahead many items' payloads in batch requests (`read`, at most `max`
88
+ * ids each, sent side by side) into the cache their own loads read under
89
+ * `key`, skipping an item cached fresh unless `force`. Answers the ids stored, which a forced load can
90
+ * take as fresh; an item the batch failed or left out stays for its own load
91
+ * to ask, which says why. A batch route the server does not serve (404) is
92
+ * left alone until it is asked again (BATCH_ROUTE_REPROBE_MS).
93
+ */
94
+ export function createCloudBatchAhead<T>(
95
+ cache: CloudCache<T>,
96
+ options: {
97
+ max: number;
98
+ validate: (payload: T) => T;
99
+ read: (ids: string[]) => Promise<Array<{ id: string; payload: T }>>;
100
+ },
101
+ ): (items: ReadonlyArray<{ id: string; key: string }>, force?: boolean) => Promise<Set<string>> {
102
+ const route = createMissingRouteLatch();
103
+ return async (items, force = false) => {
104
+ const stored = new Set<string>();
105
+ const keys = new Map<string, string>();
106
+ for (const { id, key } of items) {
107
+ const cached = cache.get(key);
108
+ if (force || !cached || cached.stale) keys.set(id, key);
109
+ }
110
+ const ids = [...keys.keys()];
111
+ if (route.missing() || ids.length < 2) return stored;
112
+ const chunks = Array.from({ length: Math.ceil(ids.length / options.max) }, (_, index) => ids.slice(index * options.max, (index + 1) * options.max));
113
+ await Promise.all(chunks.map(async (chunk) => {
114
+ const read = await options.read(chunk).catch((error: unknown) => {
115
+ route.refused(error);
116
+ if (!(error instanceof ApiRequestError && error.status === 404)) logFailure("shared.cloud-resource", "Read a batch of cloud payloads ahead failed")(error);
117
+ return [];
118
+ });
119
+ await Promise.all(read.map(async ({ id, payload }) => {
120
+ const key = keys.get(id);
121
+ if (!key) return;
122
+ try {
123
+ const valid = options.validate(payload);
124
+ await cache.load(key, async () => valid, { force: true });
125
+ stored.add(id);
126
+ } catch (error) {
127
+ logFailure("shared.cloud-resource", "Store a batched cloud payload failed")(error);
128
+ }
129
+ }));
130
+ }));
131
+ return stored;
132
+ };
133
+ }
134
+
135
+ /**
136
+ * A batch reply's items. A reply without them is a service that read the
137
+ * batch path as something else (an older one's `/:symbol` route, for a symbol
138
+ * named "batch"), which is a service without the route: a 404.
139
+ */
140
+ export function batchReplyItems<T>(reply: { items?: T[] } | null | undefined): T[] {
141
+ if (!Array.isArray(reply?.items)) throw new ApiRequestError("This service does not read these in batches", 404);
142
+ return reply.items;
143
+ }
@@ -11,7 +11,7 @@ import { useRendererHost } from "../../../ui";
11
11
  import type { RendererHost } from "../../../ui";
12
12
  import { getSharedRegistry } from "../../registry";
13
13
  import { requestAccountManagementTab } from "../account-management/navigation";
14
- import { resolvePlanAccess, usePlanAccess, type PlanAccess } from "../../../api-client/plan-access";
14
+ import { freeQuotesDelayMinutes, resolvePlanAccess, usePlanAccess, type PlanAccess, type QuoteDelayFacts } from "../../../api-client/plan-access";
15
15
  import { useViewport } from "../../../react/input";
16
16
  import { type DialogApi, useOptionalDialog } from "../../../ui/dialog";
17
17
  import { promptCloudUpgrade } from "../../../account/upgrade-dialog";
@@ -365,3 +365,26 @@ export function useCloudAccessFooter({
365
365
 
366
366
  return { access, hint, openUpgrade, segment };
367
367
  }
368
+
369
+ /**
370
+ * The footer's clock for a pane whose prices this account sees late: the free
371
+ * tier's delay (`freeQuotesDelayMinutes`) on any of `quotes`, pressed for the
372
+ * upgrade, as the quote footer's clock is. Null with Pro or a trial, and while
373
+ * every price shown is live, so those accounts see nothing new. A pane whose
374
+ * data comes as one board says so with one `{ dataSource }` for the board.
375
+ */
376
+ export function useQuoteDelayFooter(
377
+ quotes: Iterable<QuoteDelayFacts | null | undefined>,
378
+ placement: string,
379
+ ): PaneFooterSegment | null {
380
+ const language = useAppLanguage();
381
+ const minutes = freeQuotesDelayMinutes(usePlanAccess(), quotes);
382
+ const openUpgrade = useCloudUpgradeAction(placement);
383
+ return useMemo(() => (minutes == null ? null : delayedFooterSegment({
384
+ id: "quote-delay",
385
+ detail: tf("{delay} delayed · try Pro live", { delay: `${minutes}m` }),
386
+ delay: `${minutes}m`,
387
+ onPress: openUpgrade,
388
+ quoteStatus: true,
389
+ })), [language, minutes, openUpgrade]);
390
+ }