@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
@@ -11,7 +11,7 @@ import { applyChartComposerCapabilityOptions } from "../../plugins/builtin/chart
11
11
  import { parseChartSpec } from "../../plugins/builtin/chart-composer/chart-spec";
12
12
  import { normalizeTickerInput } from "../../tickers/search";
13
13
  import { slugifyName } from "../../utils/slugify";
14
- import { ListingArgError, listingInstrument, listingReadNotes, resolveCliListing, usListingsOnlyMessage } from "../listing-arg";
14
+ import { listCoinNotes, ListingArgError, listingInstrument, listingReadNotes, resolveCliListing, usListingsOnlyMessage } from "../listing-arg";
15
15
  import { uncoveredInstrumentKind, uncoveredInstrumentMessage } from "../../tickers/instrument-coverage";
16
16
  import { uncoveredInstrumentCommand } from "../uncovered-instrument";
17
17
  import { listingAbroadVenue } from "../../utils/sec";
@@ -83,7 +83,7 @@ async function buildPaneInstance(
83
83
  // Free text is a phrase, not a symbol. Treating it as a ticker makes the
84
84
  // pane look for a provider for values such as "HIGH GROWTH SOFTWARE".
85
85
  const headlessArgKind = resolved.headless?.argument.kind;
86
- const fallbackSymbol = !argumentNamesTicker(resolved.template, resolved.headless)
86
+ const fallbackSymbol = !argumentNamesTicker(resolved.template, resolved.headless, resolved.pane)
87
87
  ? null
88
88
  : headlessArgKind === "tickers" || headlessArgKind === "symbol-list"
89
89
  ? normalizeTickerInput(null, cleanTickerInput(target.split(/[,\s]+/)[0] ?? ""))
@@ -254,8 +254,9 @@ export async function applyListingArgument(
254
254
  }
255
255
  notes.push(...answers.map((answer) => `${answer.message} ${answer.details.replaceAll("\n", " ")}`));
256
256
  }
257
- // One listing's notes come with its choice note; a list's are said here.
258
- if (listings.length > 1) notes.unshift(...listings.flatMap(listingReadNotes));
257
+ // One listing's notes come with its choice note; a list's are said here, with a coin's code read as a listing
258
+ // (CORR RIOT,BTC: BTC is the Grayscale fund) as `quote BTC` says it.
259
+ if (listings.length > 1) notes.unshift(...listings.flatMap(listingReadNotes), ...await listCoinNotes(listings, context));
259
260
  return {
260
261
  ...args,
261
262
  arg: listings.map((listing) => listing.key).join(", "),
@@ -349,8 +350,8 @@ export async function resolvePaneFunction(
349
350
  if (!pane) {
350
351
  throw new Error(`Template "${template?.id}" points at missing pane "${template?.paneId}".`);
351
352
  }
352
- const createOptions = buildCreateOptions(template, args.arg);
353
353
  const headless = getHeadlessPaneDefinition(template, pane);
354
+ const createOptions = withTickerList(template, buildCreateOptions(template, args.arg), headless, args.arg, paneFunctionCode(template, pane));
354
355
  const capability = getPaneFunctionCapability(template, pane);
355
356
  let tableSection: string | true | undefined;
356
357
  if (resolutionSettings.tableSection) {
@@ -400,6 +401,27 @@ export async function resolvePaneFunction(
400
401
  return resolved;
401
402
  }
402
403
 
404
+ /**
405
+ * A one-ticker template whose pane also opens on a list (`opensTickerList`:
406
+ * DVD's ranking): the list is passed as `symbols`, so `shot` draws the same
407
+ * tickers `fn` reads, in one pane, and fetches each; past the most the
408
+ * function's report takes, it is refused as `fn` refuses it. One ticker, and
409
+ * any other template, stays as it was.
410
+ */
411
+ export function withTickerList(
412
+ template: PaneTemplateDef | undefined,
413
+ createOptions: PaneTemplateCreateOptions | undefined,
414
+ headless: HeadlessPaneDefinition | undefined,
415
+ arg: string,
416
+ token = template?.shortcut?.prefix ?? "",
417
+ ): PaneTemplateCreateOptions | undefined {
418
+ if (!template?.opensTickerList || !createOptions || createOptions.symbols?.length) return createOptions;
419
+ const symbols = [...new Set(arg.split(/[,\s]+/).map((word) => cleanTickerInput(word)).filter(Boolean))];
420
+ if (symbols.length < 2) return createOptions;
421
+ if (headless) validateSymbolCount(headless, token, symbols, "shot");
422
+ return { ...createOptions, symbol: symbols[0]!, arg: symbols.join(","), symbols };
423
+ }
424
+
403
425
  function validateTickerCardinality(
404
426
  capability: PaneFunctionCapability,
405
427
  createOptions: PaneTemplateCreateOptions | undefined,
@@ -208,6 +208,39 @@ export function filledKeyValueCount(rows: DesktopPaneShotRenderResult["visibleKe
208
208
  }).length;
209
209
  }
210
210
 
211
+ /**
212
+ * Whether a capture shows nothing that was loaded: a wall that needs an
213
+ * account, or an empty, loading or failed state, with no rows, labeled values
214
+ * or plotted chart beside it. Its footer carries no status line then, as it
215
+ * has no data to date: the quotes the capture loaded for its symbol are not on
216
+ * screen. A pane that only locks or fails part of what it shows keeps it.
217
+ */
218
+ export function shotLoadedNothing(
219
+ render: Pick<DesktopPaneShotRenderResult, "accessGate" | "emptyStateDetected" | "loadingStateDetected" | "errorStateDetected" | "rows" | "visibleKeyValues" | "semanticUi">,
220
+ ): boolean {
221
+ const stated = !!render.accessGate || render.emptyStateDetected || render.loadingStateDetected || render.errorStateDetected;
222
+ return stated
223
+ && render.rows.length === 0
224
+ && filledKeyValueCount(render.visibleKeyValues) === 0
225
+ && !render.semanticUi.some(plotsData);
226
+ }
227
+
228
+ /**
229
+ * A pane's chart evidence node that says it plotted something: a count of
230
+ * plotted values, or series with points. A node that is neither (a shape this
231
+ * does not know) counts as plotted, so its pane keeps its line.
232
+ */
233
+ function plotsData(node: RemoteUiNodeSnapshot): boolean {
234
+ if (node.role !== "chart-data") return false;
235
+ const metadata = node.metadata;
236
+ if (!isRecord(metadata)) return true;
237
+ const counts = [metadata.plottedValueCount, metadata.projectedPointCount].filter((count): count is number => typeof count === "number");
238
+ if (counts.length > 0) return counts.some((count) => count > 0);
239
+ const lists = [metadata.series, metadata.baseSeries, metadata.resolvedSeries].filter(Array.isArray);
240
+ if (lists.length === 0) return true;
241
+ return lists.some((list) => list.some((series) => isRecord(series) && typeof series.pointCount === "number" && series.pointCount > 0));
242
+ }
243
+
211
244
  /**
212
245
  * A 3D surface that is not the real drawing: the frame has no mesh, or the 2D
213
246
  * grid stands in for it. Both are fine for a person using the pane and wrong
@@ -1,14 +1,20 @@
1
+ import { lookup } from "dns/promises";
2
+ import { isIP } from "net";
1
3
  import { fetchTape } from "../../plugins/builtin/time-sales/client";
2
4
  import {
5
+ encodeResponseBody,
3
6
  readRequestInit,
4
- toResponseEnvelope,
7
+ toResponseHead,
5
8
  type HttpProxyRequestEnvelope,
6
9
  type HttpProxyResponseEnvelope,
7
10
  } from "../../utils/http-proxy-response";
11
+ import { fetchCheckingRedirects, readBytesWithin } from "../../utils/http-proxy-upstream";
12
+ import { isPublicAddress } from "../../utils/public-address";
8
13
  import type { DesktopPaneShotApiProxy, DesktopPaneShotBridge } from "../desktop-pane-shot";
9
14
  import type { MarketContext } from "../types";
10
15
  import { resolvePersistedCloudSessionToken } from "./cloud-session";
11
16
  import { getCloudApiBaseUrl } from "../../api-client/request";
17
+ import { fetchShotHttpAddress } from "./screenshot-http";
12
18
 
13
19
  const CREDENTIAL_FIELD_NAMES = new Set([
14
20
  "accesstoken",
@@ -98,10 +104,27 @@ const SHOT_BRIDGE_MARKET_OPERATIONS = new Set([
98
104
  ]);
99
105
 
100
106
  const SHOT_BRIDGE_HTTP_TIMEOUT_MS = 20_000;
107
+ /** Feeds and API answers are far smaller; the answer goes to the page whole, as JSON. */
108
+ const SHOT_BRIDGE_HTTP_MAX_BYTES = 25 * 1024 * 1024;
109
+ /** What panes send through `httpFetch` while they draw: reads, and the POST a few APIs take a query as. */
110
+ const SHOT_BRIDGE_HTTP_METHODS = new Set(["GET", "HEAD", "POST"]);
111
+
112
+ /** The addresses a host name resolves to. */
113
+ type ResolveHost = (hostname: string) => Promise<string[]>;
114
+
115
+ const resolveHost: ResolveHost = async (hostname) => (
116
+ await lookup(hostname, { all: true, verbatim: true })
117
+ ).map((entry) => entry.address);
118
+
119
+ interface ShotHttpOptions {
120
+ resolveHost?: ResolveHost;
121
+ fetch?: typeof fetch;
122
+ }
101
123
 
102
124
  export function createDesktopShotBridge(
103
125
  context: Pick<MarketContext, "dataProvider">,
104
126
  tapeBootstrap: typeof fetchTape = fetchTape,
127
+ httpOptions: ShotHttpOptions = {},
105
128
  ): DesktopPaneShotBridge {
106
129
  return {
107
130
  async marketData(operation, args) {
@@ -123,30 +146,88 @@ export function createDesktopShotBridge(
123
146
  args,
124
147
  );
125
148
  },
126
- httpFetch: (request) => runDesktopShotHttpFetch(request),
149
+ httpFetch: (request) => runDesktopShotHttpFetch(request, httpOptions),
127
150
  };
128
151
  }
129
152
 
153
+ /**
154
+ * Whether the page may reach `raw`: http or https on a host whose every
155
+ * address is on the public internet. Panes read public APIs and news feeds;
156
+ * nothing they draw lives on this machine or its network, and a URL that
157
+ * names one (or a host that resolves to one) is refused.
158
+ */
159
+ async function checkShotHttpTarget(
160
+ raw: string,
161
+ resolve: ResolveHost,
162
+ ): Promise<{ url: URL; address: string } | { error: string; status: number }> {
163
+ let url: URL;
164
+ try {
165
+ url = new URL(raw);
166
+ } catch {
167
+ // Intentional ignore: a URL that does not parse is answered as an invalid target, never fetched.
168
+ return { error: "The target URL is not valid.", status: 400 };
169
+ }
170
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
171
+ return { error: `Screenshot HTTP bridge refuses ${url.protocol} requests.`, status: 400 };
172
+ }
173
+ if (url.username || url.password) return { error: "Credentials in the URL are not allowed.", status: 400 };
174
+ const host = url.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
175
+ const refused = { error: `Screenshot HTTP bridge refuses ${url.hostname}: it is not on the public internet.`, status: 403 };
176
+ if (host === "localhost" || host.endsWith(".localhost")) return refused;
177
+ let addresses: string[];
178
+ try {
179
+ addresses = isIP(host) ? [host] : await resolve(host);
180
+ } catch {
181
+ return { error: `Could not resolve ${url.hostname}.`, status: 502 };
182
+ }
183
+ if (addresses.length === 0) return { error: `Could not resolve ${url.hostname}.`, status: 502 };
184
+ if (!addresses.every(isPublicAddress)) return refused;
185
+ const address = addresses.find((candidate) => isIP(candidate) === 4) ?? addresses[0]!;
186
+ return { url, address };
187
+ }
188
+
130
189
  /**
131
190
  * Panes such as DVD fetch a third-party API directly. A browser cannot: the
132
191
  * request is cross-origin and dies in CORS, so the pane reported that the
133
192
  * ticker pays no dividend. The desktop renderer already proxies these through
134
- * its native half; the screenshot page gets the same treatment.
193
+ * its native half; the screenshot page gets the same treatment, limited to
194
+ * public hosts, each redirect checked like the first URL.
135
195
  */
136
196
  async function runDesktopShotHttpFetch(
137
197
  request: HttpProxyRequestEnvelope,
198
+ options: ShotHttpOptions,
138
199
  ): Promise<HttpProxyResponseEnvelope> {
139
- const target = new URL(request.url);
140
- if (target.protocol !== "http:" && target.protocol !== "https:") {
141
- throw new Error(`Screenshot HTTP bridge refuses ${target.protocol} requests.`);
200
+ const resolve = options.resolveHost ?? resolveHost;
201
+ const addresses = new Map<string, string>();
202
+ const checkTarget = async (raw: string) => {
203
+ const checked = await checkShotHttpTarget(raw, resolve);
204
+ if ("url" in checked) addresses.set(checked.url.href, checked.address);
205
+ return checked;
206
+ };
207
+ const target = await checkTarget(typeof request?.url === "string" ? request.url : "");
208
+ if ("error" in target) throw new Error(target.error);
209
+ const init = readRequestInit(request.init);
210
+ if (!SHOT_BRIDGE_HTTP_METHODS.has(init.method)) {
211
+ throw new Error(`Screenshot HTTP bridge refuses ${init.method} requests.`);
142
212
  }
143
- const { method, headers, body, redirect } = readRequestInit(request.init);
144
- const response = await fetch(target, {
145
- method,
146
- headers,
147
- body,
148
- redirect,
213
+ const result = await fetchCheckingRedirects(target.url, init, {
214
+ fetch: options.fetch ?? (async (input, init) => {
215
+ const url = new URL(String(input));
216
+ const address = addresses.get(url.href);
217
+ if (!address) throw new Error("The screenshot target has not been checked.");
218
+ return fetchShotHttpAddress(url, init ?? {}, address);
219
+ }) as typeof fetch,
220
+ check: checkTarget,
149
221
  signal: AbortSignal.timeout(SHOT_BRIDGE_HTTP_TIMEOUT_MS),
150
222
  });
151
- return toResponseEnvelope(response);
223
+ if ("error" in result) throw new Error(result.error);
224
+ const { response } = result;
225
+ const tooLarge = `The response from ${target.url.hostname} is larger than ${SHOT_BRIDGE_HTTP_MAX_BYTES / (1024 * 1024)} MB.`;
226
+ if (init.method !== "HEAD" && Number(response.headers.get("content-length") ?? 0) > SHOT_BRIDGE_HTTP_MAX_BYTES) {
227
+ await response.body?.cancel().catch(() => { /* Intentional ignore: an answer over the cap is dropped unread. */ });
228
+ throw new Error(tooLarge);
229
+ }
230
+ const body = await readBytesWithin(response.body, SHOT_BRIDGE_HTTP_MAX_BYTES);
231
+ if (body === null) throw new Error(tooLarge);
232
+ return { ...toResponseHead(response), ...encodeResponseBody(body) };
152
233
  }
@@ -0,0 +1,70 @@
1
+ import { request as httpRequest } from "node:http";
2
+ import { request as httpsRequest } from "node:https";
3
+ import { isIP } from "node:net";
4
+ import { Readable } from "node:stream";
5
+ import { checkServerIdentity } from "node:tls";
6
+ import { createBrotliDecompress, createGunzip, createInflate } from "node:zlib";
7
+
8
+ /**
9
+ * A single HTTP hop to an address already checked by the screenshot bridge.
10
+ * The socket uses that address directly, while Host and TLS keep the URL's
11
+ * name. A fresh connection keeps another request's DNS result out of the hop.
12
+ */
13
+ export function fetchShotHttpAddress(url: URL, init: RequestInit, address: string): Promise<Response> {
14
+ if (!isIP(address)) return Promise.reject(new Error("The screenshot address must be an IP address."));
15
+ const headers = new Headers(init.headers);
16
+ headers.set("host", url.host);
17
+ headers.set("accept-encoding", "gzip, deflate, br");
18
+ if (!headers.has("accept")) headers.set("accept", "*/*");
19
+ if (!headers.has("user-agent")) headers.set("user-agent", `Bun/${Bun.version}`);
20
+ if (typeof init.body === "string" && !headers.has("content-type")) headers.set("content-type", "text/plain;charset=UTF-8");
21
+ const requestHeaders: Record<string, string> = {};
22
+ headers.forEach((value, name) => { requestHeaders[name] = value; });
23
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
24
+ return new Promise((resolve, reject) => {
25
+ const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
26
+ hostname: address,
27
+ method: init.method,
28
+ headers: requestHeaders,
29
+ agent: false,
30
+ signal: init.signal ?? undefined,
31
+ ...(url.protocol === "https:" ? {
32
+ servername: isIP(hostname) ? "" : hostname,
33
+ checkServerIdentity: (_name: string, certificate: Parameters<typeof checkServerIdentity>[1]) => checkServerIdentity(hostname, certificate),
34
+ } : {}),
35
+ }, (incoming) => {
36
+ try {
37
+ const responseHeaders = new Headers();
38
+ for (let index = 0; index < incoming.rawHeaders.length; index += 2) {
39
+ responseHeaders.append(incoming.rawHeaders[index]!, incoming.rawHeaders[index + 1]!);
40
+ }
41
+ const status = incoming.statusCode ?? 502;
42
+ if (init.method === "HEAD" || status === 204 || status === 205 || status === 304) {
43
+ incoming.resume();
44
+ resolve(new Response(null, { status, statusText: incoming.statusMessage, headers: responseHeaders }));
45
+ return;
46
+ }
47
+ const encoding = responseHeaders.get("content-encoding")?.toLowerCase();
48
+ const decoder = encoding === "gzip" ? createGunzip()
49
+ : encoding === "deflate" ? createInflate()
50
+ : encoding === "br" ? createBrotliDecompress() : null;
51
+ if (decoder) {
52
+ responseHeaders.delete("content-encoding");
53
+ responseHeaders.delete("content-length");
54
+ incoming.on("error", (error) => decoder.destroy(error));
55
+ decoder.once("close", () => incoming.destroy());
56
+ }
57
+ const stream = decoder ? incoming.pipe(decoder) : incoming;
58
+ resolve(new Response(Readable.toWeb(stream) as unknown as ReadableStream<Uint8Array>, {
59
+ status, statusText: incoming.statusMessage, headers: responseHeaders,
60
+ }));
61
+ } catch (error) {
62
+ incoming.destroy();
63
+ reject(error);
64
+ }
65
+ });
66
+ request.once("error", reject);
67
+ if (init.body !== undefined && init.body !== null) request.write(init.body);
68
+ request.end();
69
+ });
70
+ }
@@ -1,8 +1,7 @@
1
1
  import { logFailure } from "../../utils/log-failure";
2
2
  import { apiClient } from "../../api-client";
3
3
  import { loadResolvedHeadlessPaneModel, reportCoverage, reportGaps, type LoadedHeadlessPaneModel } from "./headless";
4
- import { dirname } from "path";
5
- import { mkdir } from "fs/promises";
4
+ import { createOutputFolder } from "../output-file";
6
5
  import type { Quote } from "../../types/financials";
7
6
  import { isRecord } from "../../utils/guards";
8
7
  import {
@@ -28,7 +27,18 @@ import { quotesFreshness } from "../freshness";
28
27
  import { paneFitsRows, shotMaxHeightCells, buildDesktopShotPayload } from "./screenshot-payload";
29
28
  import { resolveDesktopShotApiProxy, createDesktopShotBridge } from "./screenshot-bridge";
30
29
  import type { PaneScreenshotResult } from "./screenshot-types";
31
- import { assessPaneScreenshot } from "./screenshot-assessment";
30
+ import { assessPaneScreenshot, shotLoadedNothing } from "./screenshot-assessment";
31
+
32
+ /**
33
+ * Whether a capture's footer keeps a row for the dated status line. A pane of
34
+ * the user's own state (notes, connections, documentation) declares `local`:
35
+ * it has no data to date, and "not a live feed" would be said of its own
36
+ * state. A capture that loaded nothing gives the row back later
37
+ * (`shotLoadedNothing`).
38
+ */
39
+ export function shotHasStatusLine(resolved: Pick<ResolvedPaneFunction, "pane" | "template">): boolean {
40
+ return !declaredReportFreshness(resolved).local;
41
+ }
32
42
 
33
43
  /** The bridge, keeping every quote it serves the page: what a pane that loads its own quotes (WEI) shows. */
34
44
  function recordBridgedQuotes(bridge: DesktopPaneShotBridge): { bridge: DesktopPaneShotBridge; quotes: Quote[] } {
@@ -241,7 +251,7 @@ export async function renderDesktopShot({
241
251
  */
242
252
  onStalledReport?: () => void;
243
253
  }): Promise<PaneScreenshotResult> {
244
- if (captureImage) await mkdir(dirname(outputPath), { recursive: true });
254
+ if (captureImage) await createOutputFolder(outputPath);
245
255
  const apiProxy = resolveDesktopShotApiProxy(context);
246
256
  const previousSessionToken = apiClient.getSessionToken();
247
257
  let payload: DesktopPaneShotPayload;
@@ -277,7 +287,7 @@ export async function renderDesktopShot({
277
287
  watermark ?? null,
278
288
  loadModel,
279
289
  );
280
- if (statusLine && captureImage) payload.statusLine = true;
290
+ if (statusLine && captureImage && shotHasStatusLine(resolved)) payload.statusLine = true;
281
291
  const fitRoles = [
282
292
  ...(fitRows && paneFitsRows(resolved) ? ["data-table-body-scroll"] : []),
283
293
  ...(fitReader && resolved.pane.shotFitsReader ? ["detail-scroll-body"] : []),
@@ -296,6 +306,10 @@ export async function renderDesktopShot({
296
306
  captureImage,
297
307
  bridge: recorded.bridge,
298
308
  statusLine: async (rendered) => {
309
+ if (shotLoadedNothing(rendered)) {
310
+ abandonReports();
311
+ return null;
312
+ }
299
313
  const tab = report && shownReportTab(resolved, rendered.semanticUi);
300
314
  const shown = tab ? reportShotFreshness(resolved, () => loadReport({ ...resolved, options: { ...resolved.options, tab } })) : report;
301
315
  const line = shotFreshness(resolved, shotPayload, rendered, recorded.quotes, shown && await settledWithin(shown, SHOT_REPORT_WAIT_MS));
@@ -2,6 +2,7 @@ import type { QuoteBatchResult } from "../types/data-provider";
2
2
  import type { Quote } from "../types/financials";
3
3
  import { bareListingNote } from "../tickers/listing-fate";
4
4
  import { notATickerSymbol } from "../sources/provider-errors";
5
+ import { listingSuffixCandidate } from "../sources/listing-symbols";
5
6
  import { looksLikeCurrencyPair } from "../utils/currency-pair";
6
7
  import { canonicalExchange } from "../utils/exchanges";
7
8
  import type { CliCommandDef } from "../types/plugin";
@@ -102,6 +103,9 @@ export async function answerQuote(
102
103
  const notTraded = await notTradedMessage(listing, market);
103
104
  if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
104
105
  if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
106
+ // HCI.L is HCI on LSE written with a vendor's suffix: one that does not trade there says so as HCI:LSE does.
107
+ const suffixed = await notTradedOnSuffixVenue(listing, market);
108
+ if (suffixed) return { result, listing, failure: { message: suffixed, oneLine: suffixed }, notes: [] };
105
109
  // A word the service marks as a company's name is that company, before the symbol is read on its listings abroad.
106
110
  const named = !listing.exchange && listing.typed ? await serviceNamedCompany(listing.typed, market) : null;
107
111
  const meant = named ? await answerNamedListing(named, market, request, refresh, datesChangeBasis) : null;
@@ -137,6 +141,22 @@ export async function answerQuote(
137
141
  return { result, listing, failure: { message, details, oneLine }, notes: [] };
138
142
  }
139
143
 
144
+ /**
145
+ * "HCI does not trade on LSE. HCI trades on: NYSE, IEX, JSE." for a symbol
146
+ * written with a venue's suffix (HCI.L) that no source quotes, when its base
147
+ * symbol trades elsewhere and not on the venue the suffix names: the answer
148
+ * `HCI:LSE` gets. Only for London's `.L` and suffixes of two letters or more
149
+ * (`.IS`, `.JO`): another one-letter suffix is as likely a US share class
150
+ * (MKC.V, BRK.B) and keeps its own answer. Null otherwise.
151
+ */
152
+ async function notTradedOnSuffixVenue(listing: CliListing, market: MarketContext): Promise<string | null> {
153
+ if (listing.exchange) return null;
154
+ const suffix = listing.symbol.slice(listing.symbol.lastIndexOf(".") + 1);
155
+ if (!listing.symbol.includes(".") || (suffix.length < 2 && suffix.toUpperCase() !== "L")) return null;
156
+ const venue = listingSuffixCandidate(listing.symbol);
157
+ return venue ? notTradedMessage(venue, market) : null;
158
+ }
159
+
140
160
  /**
141
161
  * A company's name typed as one word (`Apple`) that the source says is not a
142
162
  * ticker, answered as the listing search names for it with the line saying so
@@ -6,7 +6,7 @@ import type {
6
6
  GloomPlugin,
7
7
  } from "../types/plugin";
8
8
  import type { LoadedExternalPlugin } from "../plugins/loader";
9
- import { getPluginCatalog } from "../plugins/catalog";
9
+ import { getBuiltinCliCommands } from "../plugins/catalog-cli";
10
10
  import { initCliServices, initConfigData, initMarketData } from "./context";
11
11
  import { closeAndFail, fail } from "./errors";
12
12
  import { DEFAULT_CLI_OPTIONS, type CliGlobalOptions } from "./options";
@@ -38,11 +38,14 @@ export interface CliCommandRegistry {
38
38
  config: AppConfig | null;
39
39
  plugins: GloomPlugin[];
40
40
  externalPlugins: LoadedExternalPlugin[];
41
+ /** Loads installed contributions when dispatch or application services need them. */
42
+ ensureExternalPlugins?: () => Promise<void>;
41
43
  }
42
44
 
43
45
  interface BuildCliCommandRegistryOptions {
44
46
  coreCommands: CliCommandDef[];
45
47
  externalPlugins?: LoadedExternalPlugin[];
48
+ loadExternalPlugins?: () => Promise<LoadedExternalPlugin[]>;
46
49
  config?: AppConfig | null;
47
50
  }
48
51
 
@@ -72,11 +75,11 @@ export function normalizeCliDispatchResult(result: void | CliDispatchResult): Cl
72
75
  export function buildCliCommandRegistry({
73
76
  coreCommands,
74
77
  externalPlugins = [],
78
+ loadExternalPlugins,
75
79
  config = null,
76
80
  }: BuildCliCommandRegistryOptions): CliCommandRegistry {
77
81
  const allCommands: RegisteredCliCommand[] = [];
78
82
  const allTokens = new Map<string, RegisteredCliCommand>();
79
- const catalog = getPluginCatalog(externalPlugins);
80
83
  const disabledPlugins = new Set(config?.disabledPlugins ?? []);
81
84
 
82
85
  const registerCommand = (
@@ -110,8 +113,10 @@ export function buildCliCommandRegistry({
110
113
  registerCommand(command, "core", "core", true);
111
114
  }
112
115
 
113
- const loadablePlugins: GloomPlugin[] = [];
114
- for (const entry of catalog) {
116
+ for (const { pluginId, commands } of getBuiltinCliCommands()) {
117
+ for (const command of commands) registerCommand(command, pluginId, "plugin", true);
118
+ }
119
+ for (const entry of externalPlugins) {
115
120
  if (entry.error) {
116
121
  registryLog.warn(`Skipping external plugin "${entry.plugin.id}" for CLI registration.`, {
117
122
  path: entry.path,
@@ -119,9 +124,8 @@ export function buildCliCommandRegistry({
119
124
  });
120
125
  continue;
121
126
  }
122
- loadablePlugins.push(entry.plugin);
123
127
  for (const command of entry.plugin.cliCommands ?? []) {
124
- registerCommand(command, entry.plugin.id, "plugin", entry.source === "builtin");
128
+ registerCommand(command, entry.plugin.id, "plugin", false);
125
129
  }
126
130
  }
127
131
 
@@ -137,30 +141,54 @@ export function buildCliCommandRegistry({
137
141
  }
138
142
  }
139
143
 
140
- return {
144
+ let plugins: GloomPlugin[] | undefined;
145
+ let loading: Promise<void> | undefined;
146
+ const registry: CliCommandRegistry = {
141
147
  commands,
142
148
  lookup,
143
149
  config,
144
- plugins: loadablePlugins.filter((plugin) => !disabledPlugins.has(plugin.id)),
150
+ get plugins() {
151
+ const { getPluginCatalog } = require("../plugins/catalog") as typeof import("../plugins/catalog");
152
+ return plugins ??= getPluginCatalog(registry.externalPlugins)
153
+ .filter((entry) => !entry.error && !disabledPlugins.has(entry.plugin.id))
154
+ .map((entry) => entry.plugin);
155
+ },
145
156
  externalPlugins,
157
+ ...(loadExternalPlugins ? { ensureExternalPlugins: () => loading ??= (async () => {
158
+ const loaded = await loadExternalPlugins();
159
+ const expanded = buildCliCommandRegistry({ coreCommands, externalPlugins: loaded, config });
160
+ registry.commands = expanded.commands;
161
+ registry.lookup = expanded.lookup;
162
+ registry.externalPlugins = loaded;
163
+ // A command can retain ctx.plugins before opening market data (search does).
164
+ // Keep that reference current without materializing panes for command-only work.
165
+ if (plugins) plugins.splice(0, plugins.length, ...expanded.plugins);
166
+ })() } : {}),
146
167
  };
168
+ return registry;
147
169
  }
148
170
 
149
171
  export function createCliCommandContext(
150
172
  ownerId: string,
151
- registryOrPlugins: Pick<CliCommandRegistry, "plugins" | "externalPlugins"> | GloomPlugin[],
173
+ registryOrPlugins: Pick<CliCommandRegistry, "plugins" | "externalPlugins" | "ensureExternalPlugins"> | GloomPlugin[],
152
174
  cliOptions: CliGlobalOptions = DEFAULT_CLI_OPTIONS,
153
175
  args: readonly string[] = [],
154
176
  ): CliCommandContext {
155
- const registry = Array.isArray(registryOrPlugins)
177
+ const registry: Pick<CliCommandRegistry, "plugins" | "externalPlugins" | "ensureExternalPlugins"> = Array.isArray(registryOrPlugins)
156
178
  ? { plugins: registryOrPlugins, externalPlugins: [] }
157
179
  : registryOrPlugins;
158
180
  return {
159
181
  initConfigData,
160
- initMarketData: () => initMarketData({ plugins: registry.plugins }),
161
- initServices: () => initCliServices({ externalPlugins: registry.externalPlugins }),
182
+ initMarketData: async () => {
183
+ await registry.ensureExternalPlugins?.();
184
+ return initMarketData({ plugins: registry.plugins });
185
+ },
186
+ initServices: async () => {
187
+ await registry.ensureExternalPlugins?.();
188
+ return initCliServices({ externalPlugins: registry.externalPlugins });
189
+ },
162
190
  cliOptions,
163
- plugins: registry.plugins,
191
+ get plugins() { return registry.plugins; },
164
192
  fail,
165
193
  closeAndFail,
166
194
  output: {
package/src/cli/result.ts CHANGED
@@ -93,6 +93,8 @@ export interface CliResultRenderOptions<T = unknown, Row = Record<string, unknow
93
93
  heading?: string;
94
94
  /** Text-mode line printed under the rows, such as where a listing stops. Not printed with no rows: say it in `empty`. */
95
95
  footnote?: string;
96
+ /** Text-mode line under each table row, indented and muted, cut to the width: a story's summary under its headline. */
97
+ rowNote?: (row: Row) => string | null | undefined;
96
98
  /** Text-mode block printed under the rows and footnote, for figures about a row that are not columns. */
97
99
  details?: (data: T) => string;
98
100
  /**
@@ -446,6 +448,7 @@ function formatTextCell<Row extends Record<string, unknown>>(
446
448
  function renderTextTable<Row extends Record<string, unknown>>(
447
449
  rows: Row[],
448
450
  columns?: CliResultColumn<Row>[],
451
+ rowNote?: (row: Row) => string | null | undefined,
449
452
  ): string {
450
453
  const resolvedColumns = textColumns(rows, columns);
451
454
  const cells = rows.map((row) => resolvedColumns.map((column) => formatTextCell(column, row, "table")));
@@ -469,6 +472,7 @@ function renderTextTable<Row extends Record<string, unknown>>(
469
472
  stackRows: column.stackRows,
470
473
  })),
471
474
  cells.map((row) => shown.map(({ index }) => row[index]!)),
475
+ rowNote ? { below: rows.map((row) => { const note = rowNote(row); return note ? [note] : []; }), cutBelow: true } : {},
472
476
  );
473
477
  }
474
478
 
@@ -581,15 +585,16 @@ function serializeCliRows<T, Row extends Record<string, unknown>>(
581
585
  if (renderOptions.text) {
582
586
  return renderOptions.text(result.data);
583
587
  }
584
- const summary = renderOptions.summary?.(result.data) ?? "";
585
- const withHeading = (text: string) => renderOptions.heading ? `${renderOptions.heading}\n${text}` : text;
588
+ // The heading's lines, the summary and the notes are prose: each wraps to a narrow --width, as the source line does.
589
+ const summary = wrapToTerminal(renderOptions.summary?.(result.data) ?? "");
590
+ const withHeading = (text: string) => renderOptions.heading ? `${wrapToTerminal(renderOptions.heading)}\n${text}` : text;
586
591
  if (rows.length === 0) {
587
- return withHeading(summary || cliStyles.muted(renderOptions.empty ?? "No results."));
592
+ return withHeading(summary || cliStyles.muted(wrapToTerminal(renderOptions.empty ?? "No results.")));
588
593
  }
589
594
  const table = renderTextRows(rows as Row[], result.data, renderOptions);
590
595
  const notes = [window.note, renderOptions.footnote].filter((note): note is string => !!note);
591
596
  const details = renderOptions.details?.(result.data) ?? "";
592
- const body = [table, ...notes.map((note) => cliStyles.muted(note)), ...details ? ["", details] : []].join("\n");
597
+ const body = [table, ...notes.map((note) => cliStyles.muted(wrapToTerminal(note))), ...details ? ["", details] : []].join("\n");
593
598
  return withHeading(summary ? `${summary}\n\n${body}` : body);
594
599
  }
595
600
 
@@ -607,7 +612,7 @@ function renderTextRows<T, Row extends Record<string, unknown>>(
607
612
  }
608
613
  return layout === "record"
609
614
  ? renderTextRecords(rows as Row[], columns)
610
- : renderTextTable(rows as Row[], columns);
615
+ : renderTextTable(rows as Row[], columns, renderOptions.rowNote);
611
616
  }
612
617
 
613
618
  /** Settles once the result `printCliResult` last wrote has left the process: a pipe can hold it back. */
@@ -640,8 +645,9 @@ export function printCliResult<T, Row extends Record<string, unknown> = Record<s
640
645
  // JSON carries warnings in its envelope. Text, CSV and NDJSON are only the
641
646
  // rows, so a warning goes to stderr, out of a file the rows are piped into.
642
647
  if (options.format !== "json" && !options.quiet) {
648
+ // Each wraps to a narrow --width, as a note does.
643
649
  for (const warning of result.warnings ?? []) {
644
- console.error(`${cliStyles.warning("warning:")} ${hinted(warning)}`);
650
+ console.error(wrapToTerminal(`${cliStyles.warning("warning:")} ${hinted(warning)}`));
645
651
  }
646
652
  }
647
653
  // CSV says what its rows do not in closing `# note:` lines; NDJSON has no place for one among its rows,
@@ -139,7 +139,8 @@ export interface CliTickerOptions {
139
139
  */
140
140
  export class CoinListingError extends ListingArgError {
141
141
  constructor(message: string, details: string, readonly hint: string) {
142
- super(message, `${details}\n${hint}`);
142
+ // A listing message that already ends in the coin note (`describeOtherListings`) keeps it once.
143
+ super(message, details.endsWith(hint) ? details : `${details}\n${hint}`);
143
144
  this.name = "CoinListingError";
144
145
  }
145
146
  }
package/src/cli/types.ts CHANGED
@@ -21,6 +21,8 @@ export type MarketContext = ConfigContext & {
21
21
  seriesCapabilities?: CapabilityInvoker;
22
22
  /** The catalog series charted as an expression (`FRED:DFII10`), in a report run without the plugin runtime. */
23
23
  findCatalogSeries?: (expression: string) => import("../series/types").SeriesDefinition | undefined;
24
+ /** The catalog series a bare word is an alias of (`hashrate`), in a report run without the plugin runtime. */
25
+ findCatalogSeriesNamed?: (word: string) => import("../series/types").SeriesDefinition | undefined;
24
26
  /** `--refresh`: prefetched data bypasses the local cache. */
25
27
  refresh?: boolean;
26
28
  /** Filings behind the statements and a bank's figures, read from Gloom Cloud; absent where the data service is a stand-in. */
@@ -49,6 +49,7 @@ function formatFullCurrencyValue(
49
49
  options: MarketFormatOptions = {},
50
50
  ): string | null {
51
51
  if (unit.trim() === INDEX_POINTS_UNIT) return formatMarketPrice(value, { ...options, assetCategory, priceBasis: "points" });
52
+ if (unit.trim() === "32nds") return formatMarketPrice(value, { ...options, assetCategory, priceBasis: "thirty-seconds" });
52
53
  const currency = unitCurrencyCode(unit);
53
54
  return currency ? formatMarketPriceWithCurrency(value, currency, { ...options, assetCategory }) : null;
54
55
  }
@@ -362,6 +363,8 @@ export function formatCompositeAxisValue(value: number, domain: CompositeAxisDom
362
363
  // Derived dimensions are included in the legend value. Axis labels stay
363
364
  // numeric so a narrow gutter cannot truncate USD/JPY into a false USD label.
364
365
  if (group.startsWith("derived-unit:")) return compact;
366
+ // Treasury prices keep their whole points and 32nds at every zoom level.
367
+ if (domain.unit.trim() === "32nds") return formatAxisPriceValue(value, domain);
365
368
  // Compact labels suit a wide view, but a zoomed one can hold several ticks
366
369
  // inside a single rounding step and print one repeated price down the gutter.
367
370
  // Where that happens the axis spends the digits needed to tell them apart,