@gloom-sh/terminal 0.19.1 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -1,9 +1,10 @@
1
1
  import { getSharedRegistry } from "../../plugins/registry";
2
+ import { seriesExpression, seriesNamedBy } from "../../series/registry";
2
3
  import { findCachedPortfolioAccount } from "../../plugins/builtin/portfolio-list/cached-account";
3
- import { parsePublicTickerKey } from "../../utils/exchanges";
4
4
  import { findNamedCollection } from "../../portfolio/collection-name";
5
5
  import { readBareListing } from "../listing-arg";
6
6
  import { apiClient } from "../../api-client";
7
+ import { resolveSavedInstrument } from "../../tickers/saved-instrument";
7
8
  import type { MarketContext } from "../types";
8
9
  import type {
9
10
  HeadlessArgumentExpansion,
@@ -190,7 +191,7 @@ async function loadHeadlessPaneModel(
190
191
  return result;
191
192
  }
192
193
 
193
- type HeadlessMarketContext = Pick<MarketContext, "config" | "store" | "refresh" | "resolveBroker"> & Partial<Pick<MarketContext, "persistence" | "seriesCapabilities" | "findCatalogSeries">> & {
194
+ type HeadlessMarketContext = Pick<MarketContext, "config" | "store" | "refresh" | "resolveBroker"> & Partial<Pick<MarketContext, "persistence" | "seriesCapabilities" | "findCatalogSeries" | "findCatalogSeriesNamed">> & {
194
195
  dataProvider: HeadlessPaneContext["marketData"];
195
196
  };
196
197
 
@@ -210,6 +211,11 @@ export function createHeadlessPaneContext(
210
211
  capabilities: getSharedRegistry() ?? context.seriesCapabilities,
211
212
  catalogSeriesName: (expression) => (getSharedRegistry()?.series.findByExpression(expression)?.definition
212
213
  ?? context.findCatalogSeries?.(expression))?.name,
214
+ catalogSeriesNamed: (word) => {
215
+ const registered = getSharedRegistry()?.series?.list().map(({ definition }) => definition) ?? [];
216
+ const definition = seriesNamedBy(registered, word) ?? context.findCatalogSeriesNamed?.(word);
217
+ return definition ? { expression: seriesExpression(definition), name: definition.name } : undefined;
218
+ },
213
219
  resolveBroker: context.resolveBroker,
214
220
  async resolvePortfolio(id) {
215
221
  const portfolio = findNamedCollection(context.config.portfolios, id);
@@ -222,25 +228,21 @@ export function createHeadlessPaneContext(
222
228
  if (!watchlist) return null;
223
229
  return (await context.store.loadAllTickers()).filter(row => row.metadata.watchlists.includes(watchlist.id));
224
230
  },
225
- async resolveInstrument(key) {
226
- const parsed = parsePublicTickerKey(key);
227
- const ticker = await context.store.loadTicker(key)
228
- ?? (key !== parsed.symbol ? await context.store.loadTicker(parsed.symbol) : null);
229
- return { symbol: parsed.symbol, exchange: parsed.exchange ?? ticker?.metadata.exchange };
230
- },
231
+ resolveInstrument: (key) => resolveSavedInstrument(key, (saved) => context.store.loadTicker(saved)),
231
232
  readDeviceState: <T,>(pluginId: string, key: string) =>
232
233
  context.persistence?.pluginState.get<T>(pluginId, resumeStateKey(key))?.value ?? null,
233
234
  async resolveListing(symbol, argumentWith) {
234
235
  const command = `fn ${resolved.token}`;
235
236
  const read = await readBareListing(symbol, context, {
236
237
  command, noun: "a quote", retry: (key) => `gloom ${command} ${argumentWith(key)}`,
237
- }, { refresh: !!context.refresh });
238
+ }, { refresh: !!context.refresh, successor: true });
238
239
  if (!read) return null;
239
240
  return {
240
241
  symbol: read.symbol,
241
242
  ...read.exchange ? { exchange: read.exchange } : {},
242
243
  ...read.name ? { name: read.name } : {},
243
244
  ...read.coinNote ? { coinNote: read.coinNote } : {},
245
+ ...read.successorNote ? { successorNote: read.successorNote } : {},
244
246
  };
245
247
  },
246
248
  };
@@ -544,12 +546,17 @@ function renderRows(
544
546
  stackPriority: column.stackPriority,
545
547
  stackRows: column.stackRows,
546
548
  minTableWidth: column.minTableWidth,
549
+ optional: column.optional,
550
+ dropPriority: column.dropPriority,
547
551
  })),
548
552
  rows.map((row) => columns.map((column) => cell(row, column))),
549
553
  {
550
- below: under.length === 0 ? undefined : rows.map((row) => under.flatMap((column) => (
551
- row[column.key] == null || row[column.key] === "" ? [] : cell(row, column).split("\n")
552
- ))),
554
+ // A value the text leaves out (a summary that only repeats the headline) takes no line.
555
+ below: under.length === 0 ? undefined : rows.map((row) => under.flatMap((column) => {
556
+ const text = row[column.key] == null || row[column.key] === "" ? "" : cell(row, column);
557
+ return text === "" ? [] : text.split("\n");
558
+ })),
559
+ ...(under.length > 0 && under.every((column) => column.underRow === "line") ? { cutBelow: true } : {}),
553
560
  },
554
561
  );
555
562
  }
@@ -571,7 +578,7 @@ function renderBundle(
571
578
  return result.sections.filter((section) => !isLockedSection(section)).flatMap((section, index) => [
572
579
  ...(index > 0 ? [""] : []),
573
580
  // A section with no title is its rows, or its empty message, alone.
574
- ...(section.title ? [renderSection(section.title)] : []),
581
+ ...(section.title ? [renderSection(section.textTitle ?? section.title)] : []),
575
582
  "rows" in section && section.rows
576
583
  ? renderRows(section.textRows ?? section.rows, section.textColumns ?? section.columns, definition.columns, section.empty)
577
584
  : renderEntries(section.textEntries ?? section.entries),
@@ -898,7 +905,8 @@ export function renderHeadlessPaneText(
898
905
  ): string {
899
906
  // A long title wraps to a narrow --width like the text under a table; each line stays bold.
900
907
  const lines = [...wrapToTerminal(reportTitle(definition, args, result, fallbackTitle)).split("\n").map(cliStyles.bold), ""];
901
- const notices = textNotices(result);
908
+ // A notice wraps to a narrow --width, as the notes and errors under the tables do.
909
+ const notices = textNotices(result).map((notice) => wrapToTerminal(notice));
902
910
  if (notices.length) lines.push(...notices, "");
903
911
  const gate = reportGate(definition, result);
904
912
  const reason = emptyReportReason(definition, result, args, token, gate);
@@ -46,6 +46,7 @@ import { checkOutputPath, resolveOutputPath, writeOutputFile } from "../output-f
46
46
  import type { CatalogCommandHit } from "../unknown-command";
47
47
  import { CLI_EXIT_CODES } from "../exit-codes";
48
48
  import { shotOptionRefusal } from "./shot-options";
49
+ import { listingChangeLine } from "../../tickers/listing-fate";
49
50
  import {
50
51
  GLOSSARY,
51
52
  glossaryRecord,
@@ -312,7 +313,7 @@ export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
312
313
  let quoted: ReturnType<typeof loadListingQuoteAnswer> | null = null;
313
314
  const quoteAnswer = listing ? () => quoted ??= loadListingQuoteAnswer(context.dataProvider, listing) : null;
314
315
  const fate = listing && quoteAnswer && isTickerLinkPeer(resolved.instance, registry.panes)
315
- ? listingFateLoader(listing, context, quoteAnswer().then((answer) => answer.quote))
316
+ ? listingFateLoader(listing, context, quoteAnswer().then((answer) => answer.quote), { successor: true })
316
317
  : null;
317
318
  const moved = { command: "fn", retry: (key: string) => `gloom fn ${resolved.token} ${key}` };
318
319
  // A report that fails or comes back empty for an exchange the symbol is not listed on says so.
@@ -356,7 +357,10 @@ export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
356
357
  const data = glossary
357
358
  ? { ...report.data, glossary: glossary.map(({ term, short, definition }) => ({ term, ...(short ? { short } : {}), definition })) }
358
359
  : report.data;
359
- const warnings = [...parsed.listingNotes ?? [], ...await choice ?? [], ...report.data.warnings ?? []];
360
+ // What became of the company that had the symbol (GOGL), as `quote` says it, leads.
361
+ const successor = fate ? listingChangeLine(await fate()) : null;
362
+ // A note the listing and the report both make (a coin's code charted as a fund) is said once.
363
+ const warnings = [...new Set([...successor ? [successor] : [], ...parsed.listingNotes ?? [], ...await choice ?? [], ...report.data.warnings ?? []])];
360
364
  // A report with no data is printed as it is, and fails: exit 1, and `ok: false` in JSON with the reason.
361
365
  const failure = reportFailure(report.data, ["fn", resolved.token, parsed.arg.trim()].filter(Boolean).join(" "), {
362
366
  failsWithoutData: resolved.headless?.failsWithoutData, tables: report.tables });
@@ -1,4 +1,4 @@
1
- import type { HeadlessPaneDefinition, PaneTemplateCreateOptions, PaneTemplateDef } from "../../types/plugin";
1
+ import type { HeadlessPaneDefinition, PaneDef, PaneTemplateCreateOptions, PaneTemplateDef } from "../../types/plugin";
2
2
  import { parseTickerListInput } from "../../tickers/list";
3
3
  import { normalizeTickerInput } from "../../tickers/search";
4
4
  import { resolveFinancialPeriodOption } from "../../plugins/builtin/ticker-detail/financials/model";
@@ -85,12 +85,16 @@ export function cleanTickerInput(value: string): string {
85
85
  /**
86
86
  * Whether a function's argument names the ticker its pane shows. Its report
87
87
  * says first: AUCT's `10Y` and IPO's `pending` are free text its own resolver
88
- * reads, never a symbol to look up. Without a report, the shortcut says.
88
+ * reads, never a symbol to look up. Without a report, a rendered pane whose
89
+ * argument is an id it opens (the story pane's story id) says so, then the
90
+ * shortcut.
89
91
  */
90
92
  export function argumentNamesTicker(
91
93
  template: Pick<PaneTemplateDef, "shortcut"> | undefined,
92
94
  headless: Pick<HeadlessPaneDefinition, "argument"> | undefined,
95
+ pane?: Pick<PaneDef, "reportArgument">,
93
96
  ): boolean {
97
+ if (!headless && pane?.reportArgument?.identifier) return false;
94
98
  const kind = headless?.argument.kind;
95
99
  if (kind === "free-text" || kind === "none") return false;
96
100
  return kind === "tickers" || kind === "symbol-list" || template?.shortcut?.argKind !== "text";
@@ -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";
@@ -21,6 +22,8 @@ import {
21
22
  notTradedMessage,
22
23
  oneWordCompanyName,
23
24
  quoteBareListing,
25
+ serviceNamedCompany,
26
+ type ListingArg,
24
27
  requireCliListing,
25
28
  resolveCliListing,
26
29
  tradingBoardNote,
@@ -64,7 +67,9 @@ export interface QuoteAnswer {
64
67
  /**
65
68
  * One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
66
69
  * is not listed on says so. A bare symbol the data service read on its home
67
- * listing abroad names that venue (SXR8 -> XETRA). One no source quotes is
70
+ * listing abroad names that venue (SXR8 -> XETRA). One no source quotes that
71
+ * the service marks as another listing's company name is that company
72
+ * (`serviceNamedCompany`: "siemens" is SIE:XETRA); else it is
68
73
  * quoted on its listings (`quoteBareListing`), else names them, or asks for an
69
74
  * exchange when several companies list it (2222); a bare index root (VIX) also
70
75
  * names its index (^VIX), and a futures board code a security keeps (GC) its
@@ -98,6 +103,13 @@ export async function answerQuote(
98
103
  const notTraded = await notTradedMessage(listing, market);
99
104
  if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
100
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: [] };
109
+ // A word the service marks as a company's name is that company, before the symbol is read on its listings abroad.
110
+ const named = !listing.exchange && listing.typed ? await serviceNamedCompany(listing.typed, market) : null;
111
+ const meant = named ? await answerNamedListing(named, market, request, refresh, datesChangeBasis) : null;
112
+ if (meant) return meant;
101
113
  const other = await quoteBareListing(listing, market, request, { refresh, datesChangeBasis });
102
114
  if (other?.kind === "quoted") {
103
115
  return {
@@ -129,6 +141,22 @@ export async function answerQuote(
129
141
  return { result, listing, failure: { message, details, oneLine }, notes: [] };
130
142
  }
131
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
+
132
160
  /**
133
161
  * A company's name typed as one word (`Apple`) that the source says is not a
134
162
  * ticker, answered as the listing search names for it with the line saying so
@@ -155,17 +183,32 @@ async function answerCompanyName(
155
183
  const named = await oneWordCompanyName(listing.typed, market);
156
184
  // A real ticker a source failed to quote is not a company's name, even when the company carries the word.
157
185
  if (!named || (unsettled && named.symbol.toUpperCase() === listing.symbol.toUpperCase())) return null;
158
- // A listing on a venue the app cannot read is no answer: the word keeps its own "Not a ticker".
186
+ const answer = await answerNamedListing(named, market, request, refresh, datesChangeBasis);
187
+ if (answer) return answer;
188
+ if (!unsettled) return null;
189
+ const sentence = await notATickerWords(listing.typed, market);
190
+ return { result: { ...result, error: new Error(sentence) }, listing, notes: [] };
191
+ }
192
+
193
+ /**
194
+ * The row of the listing a company's name was read as (`named`), saying so;
195
+ * null when the app cannot read its venue or it has no quote.
196
+ */
197
+ async function answerNamedListing(
198
+ named: ListingArg,
199
+ market: MarketContext,
200
+ request: ListingDataRequest,
201
+ refresh: boolean,
202
+ datesChangeBasis: boolean,
203
+ ): Promise<QuoteAnswer | null> {
204
+ // A listing on a venue the app cannot read is no answer: the word keeps its own.
159
205
  const resolved = await resolveCliListing(named.key, null, market).catch((error: unknown) => {
160
206
  if (error instanceof ListingArgError) return null;
161
207
  throw error;
162
208
  });
163
209
  const found = resolved ? { ...resolved, readAs: named.readAs, ...named.meant ? { meant: named.meant } : {}, ...named.readFromName ? { readFromName: named.readFromName } : {} } : null;
164
210
  const [quoted] = found ? await market.dataProvider.getQuotesBatch([found.request], { forceRefresh: refresh, revalidate: true, datesChangeBasis }) : [];
165
- if (found && quoted?.quote) return answerQuote(quoted, found, market, request, refresh, null, { datesChangeBasis });
166
- if (!unsettled) return null;
167
- const sentence = await notATickerWords(listing.typed, market);
168
- return { result: { ...result, error: new Error(sentence) }, listing, notes: [] };
211
+ return found && quoted?.quote ? answerQuote(quoted, found, market, request, refresh, null, { datesChangeBasis }) : null;
169
212
  }
170
213
 
171
214
  /** A listing a command found a quote for, with what `quote` says beside it. */