@gloom-sh/terminal 0.18.5 → 0.19.1

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 (570) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +163 -8
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  8. package/src/api-client/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +19 -7
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/hiring.ts +3 -0
  14. package/src/api-client/index.ts +15 -0
  15. package/src/api-client/ipo.ts +24 -1
  16. package/src/api-client/market-discovery.ts +10 -0
  17. package/src/api-client/market-movers.ts +7 -0
  18. package/src/api-client/members.ts +1 -1
  19. package/src/api-client/mna.ts +3 -0
  20. package/src/api-client/paths.ts +9 -1
  21. package/src/api-client/perps.ts +43 -0
  22. package/src/api-client/research-activity.ts +3 -2
  23. package/src/api-client/roadmap.ts +85 -0
  24. package/src/api-client/types.ts +82 -2
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +0 -2
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +2 -2
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/market.ts +107 -38
  35. package/src/cli/commands/news-search.ts +22 -0
  36. package/src/cli/commands/options-chain.ts +48 -7
  37. package/src/cli/commands/options-leaps.ts +9 -3
  38. package/src/cli/commands/overview.ts +147 -32
  39. package/src/cli/commands/search.ts +21 -1
  40. package/src/cli/commands/themes.ts +11 -2
  41. package/src/cli/commands/ticker.ts +87 -36
  42. package/src/cli/company-name-hint.ts +41 -0
  43. package/src/cli/earnings-calendar.ts +1 -1
  44. package/src/cli/errors.ts +25 -2
  45. package/src/cli/glossary.ts +31 -2
  46. package/src/cli/history-rows.ts +57 -9
  47. package/src/cli/index-roots.ts +0 -7
  48. package/src/cli/index.ts +2 -0
  49. package/src/cli/listing-arg.ts +235 -60
  50. package/src/cli/output-file.ts +68 -0
  51. package/src/cli/pane-functions/capabilities.ts +4 -0
  52. package/src/cli/pane-functions/catalog.ts +80 -7
  53. package/src/cli/pane-functions/discovery.ts +21 -1
  54. package/src/cli/pane-functions/dom.ts +13 -3
  55. package/src/cli/pane-functions/freshness.ts +93 -4
  56. package/src/cli/pane-functions/function-help.ts +20 -10
  57. package/src/cli/pane-functions/headless.ts +17 -7
  58. package/src/cli/pane-functions/index.ts +54 -16
  59. package/src/cli/pane-functions/market-session.ts +66 -4
  60. package/src/cli/pane-functions/options.ts +19 -1
  61. package/src/cli/pane-functions/resolver.ts +19 -8
  62. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  63. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  64. package/src/cli/quote-answer.ts +22 -11
  65. package/src/cli/saved-copy-persistence.ts +88 -0
  66. package/src/cli/ticker-resolution.ts +3 -2
  67. package/src/components/chart/composite/composite-chart.tsx +4 -0
  68. package/src/components/chart/composite/interactions.ts +6 -0
  69. package/src/components/chart/composite/legend.tsx +45 -7
  70. package/src/components/chart/composite/panel-gestures.ts +33 -0
  71. package/src/components/chart/composite/panel-surface.tsx +52 -3
  72. package/src/components/chart/composite/rasterizer.ts +24 -13
  73. package/src/components/chart/composite/scene.ts +39 -4
  74. package/src/components/chart/composite/series-cues.ts +14 -0
  75. package/src/components/chart/composite/text-renderer.ts +9 -2
  76. package/src/components/chart/composite/tool-catalog.ts +36 -0
  77. package/src/components/chart/composite/tools.ts +203 -44
  78. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  79. package/src/components/command-bar/assist/inventory.ts +2 -1
  80. package/src/components/command-bar/commands/direct/index.ts +28 -13
  81. package/src/components/command-bar/list/model.ts +5 -0
  82. package/src/components/command-bar/pane-templates/items.ts +3 -2
  83. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  84. package/src/components/command-bar/selection-runtime.ts +52 -3
  85. package/src/components/command-bar/surface/index.tsx +2 -0
  86. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  87. package/src/components/layout/pane/capture-stamp.ts +2 -2
  88. package/src/components/layout/pane/footer/index.tsx +22 -6
  89. package/src/components/layout/pane/footer/model.ts +83 -15
  90. package/src/components/layout/pane/status-footer.ts +35 -2
  91. package/src/components/layout/status-widget-space.ts +9 -0
  92. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  93. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  94. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  95. package/src/components/onboarding/wizard-positions.ts +3 -3
  96. package/src/components/ticker/list-table-view.tsx +8 -21
  97. package/src/components/ui/choice-dialog.tsx +23 -18
  98. package/src/components/ui/choice-groups.ts +18 -0
  99. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  100. package/src/components/ui/data-table/opentui/model.ts +26 -1
  101. package/src/components/ui/data-table/types.ts +2 -0
  102. package/src/components/ui/display.tsx +18 -3
  103. package/src/components/ui/icon.tsx +4 -1
  104. package/src/components/ui/list-view.tsx +8 -4
  105. package/src/components/ui/query-bar-keys.tsx +2 -0
  106. package/src/components/ui/query-bar.tsx +1 -0
  107. package/src/components/ui/select-button.tsx +5 -1
  108. package/src/components/ui/select-field.tsx +1 -0
  109. package/src/components/ui/table-layout.ts +8 -5
  110. package/src/core/app-runtime.ts +2 -0
  111. package/src/core/app-services.ts +2 -2
  112. package/src/data/config/chart-settings.ts +1 -1
  113. package/src/data/json-ticker-repository.ts +17 -8
  114. package/src/data/plugin-cache.ts +5 -3
  115. package/src/data/saved-copies.ts +8 -0
  116. package/src/data/ticker-repository.ts +16 -9
  117. package/src/data/ticker-writes.ts +81 -0
  118. package/src/data/ttl-cache.ts +165 -0
  119. package/src/docs/snapshot.json +103 -142
  120. package/src/i18n/es.ts +20 -6
  121. package/src/i18n/ja.ts +20 -6
  122. package/src/i18n/ko.ts +20 -6
  123. package/src/i18n/zh-cn.ts +20 -6
  124. package/src/i18n/zh-tw.ts +20 -6
  125. package/src/market-data/depositary-pair.ts +27 -3
  126. package/src/market-data/fund-facts.ts +50 -0
  127. package/src/market-data/market/change-basis.ts +16 -3
  128. package/src/market-data/market/format.ts +25 -0
  129. package/src/market-data/market/freshness.ts +2 -1
  130. package/src/market-data/options-alternatives.ts +81 -4
  131. package/src/market-data/quotes/metadata.ts +7 -0
  132. package/src/news/aggregator.ts +119 -9
  133. package/src/news/cloud-session.ts +21 -0
  134. package/src/news/hooks.ts +1 -0
  135. package/src/news/news-model.ts +1 -0
  136. package/src/news/types.ts +17 -0
  137. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  138. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  139. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  140. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  141. package/src/plugins/builtin/alerts/cli.ts +362 -80
  142. package/src/plugins/builtin/alerts/command.ts +25 -1
  143. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  144. package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
  145. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  146. package/src/plugins/builtin/alerts/format.ts +75 -5
  147. package/src/plugins/builtin/alerts/headless.ts +34 -12
  148. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  149. package/src/plugins/builtin/alerts/history.ts +1 -1
  150. package/src/plugins/builtin/alerts/index.tsx +79 -16
  151. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  152. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  153. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  154. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  155. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  156. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  157. package/src/plugins/builtin/alerts/storage.ts +12 -8
  158. package/src/plugins/builtin/alerts/types.ts +4 -0
  159. package/src/plugins/builtin/analytics/index.tsx +2 -1
  160. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  161. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  162. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  163. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  164. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  165. package/src/plugins/builtin/attention/index.tsx +2 -1
  166. package/src/plugins/builtin/attention/listing.ts +14 -0
  167. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  168. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  169. package/src/plugins/builtin/buildout/format.ts +38 -0
  170. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  171. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  172. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  173. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  174. package/src/plugins/builtin/cds/client.ts +142 -30
  175. package/src/plugins/builtin/cds/headless.ts +35 -8
  176. package/src/plugins/builtin/cds/model.ts +145 -18
  177. package/src/plugins/builtin/cds/pane.tsx +16 -5
  178. package/src/plugins/builtin/changelog/index.tsx +9 -6
  179. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  180. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  181. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  182. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  183. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  184. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  185. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  186. package/src/plugins/builtin/chart-composer/index.tsx +5 -6
  187. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  188. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  189. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  190. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  191. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  192. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  193. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  194. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  195. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  196. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  197. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  198. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  199. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  200. package/src/plugins/builtin/clinical-trials/model.ts +86 -31
  201. package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
  202. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  203. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
  204. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  205. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  206. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  207. package/src/plugins/builtin/company-attention/client.ts +8 -9
  208. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  209. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  210. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  211. package/src/plugins/builtin/company-attention/pane.tsx +12 -2
  212. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  213. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  214. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  215. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  216. package/src/plugins/builtin/composite-plugins.ts +2 -1
  217. package/src/plugins/builtin/correlation/headless.ts +13 -5
  218. package/src/plugins/builtin/correlation/history.ts +22 -0
  219. package/src/plugins/builtin/cot/client.ts +22 -18
  220. package/src/plugins/builtin/cot/headless.ts +15 -10
  221. package/src/plugins/builtin/cot/model.ts +14 -1
  222. package/src/plugins/builtin/cot/pane.tsx +22 -14
  223. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  224. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  225. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  226. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  227. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  228. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  229. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  230. package/src/plugins/builtin/credit-documents/headless.ts +10 -10
  231. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  232. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  233. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  234. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  235. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  236. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  237. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  238. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  239. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  240. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  241. package/src/plugins/builtin/docs/targets.ts +5 -1
  242. package/src/plugins/builtin/doe/headless.ts +3 -2
  243. package/src/plugins/builtin/doe/pane.tsx +13 -3
  244. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  245. package/src/plugins/builtin/earnings/board.tsx +62 -71
  246. package/src/plugins/builtin/earnings/headless.ts +40 -4
  247. package/src/plugins/builtin/earnings/scope.ts +31 -43
  248. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  249. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  250. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  251. package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
  252. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  253. package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
  254. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  255. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  256. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  257. package/src/plugins/builtin/econ/figures.ts +48 -0
  258. package/src/plugins/builtin/econ/index.tsx +14 -5
  259. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  260. package/src/plugins/builtin/econ/types.ts +2 -0
  261. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  262. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  263. package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
  264. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  265. package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
  266. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  267. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  268. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  269. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  270. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  271. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  272. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  273. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  274. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  275. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  276. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  277. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  278. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  279. package/src/plugins/builtin/gpu/headless.ts +8 -2
  280. package/src/plugins/builtin/help/function-index.ts +4 -1
  281. package/src/plugins/builtin/help/index.tsx +62 -59
  282. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  283. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  284. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  285. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  286. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  287. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  288. package/src/plugins/builtin/holders/headless.ts +22 -3
  289. package/src/plugins/builtin/holders/pane.tsx +3 -0
  290. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  291. package/src/plugins/builtin/insider/headless.ts +2 -2
  292. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  293. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  294. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  295. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  296. package/src/plugins/builtin/iv-history/client.ts +52 -11
  297. package/src/plugins/builtin/iv-history/format.ts +36 -23
  298. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  299. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  300. package/src/plugins/builtin/iv-history/model.ts +163 -27
  301. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  302. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  303. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  304. package/src/plugins/builtin/jobs/client.ts +25 -29
  305. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  306. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  307. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  308. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  309. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  310. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  311. package/src/plugins/builtin/macro-day/model.ts +39 -5
  312. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  313. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  314. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  315. package/src/plugins/builtin/market-movers/client.ts +27 -7
  316. package/src/plugins/builtin/market-movers/headless.ts +34 -12
  317. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  318. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  319. package/src/plugins/builtin/market-movers/model.ts +4 -1
  320. package/src/plugins/builtin/market-movers/screener.ts +22 -4
  321. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  322. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  323. package/src/plugins/builtin/market-movers/table.ts +17 -5
  324. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  325. package/src/plugins/builtin/mna/headless.ts +2 -0
  326. package/src/plugins/builtin/mna/pane.tsx +3 -1
  327. package/src/plugins/builtin/news/headless.ts +3 -1
  328. package/src/plugins/builtin/news/index.tsx +4 -0
  329. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  330. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  331. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  332. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  333. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  334. package/src/plugins/builtin/openfda/client.ts +14 -2
  335. package/src/plugins/builtin/openfda/index.tsx +3 -2
  336. package/src/plugins/builtin/openfda/model.ts +9 -0
  337. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  338. package/src/plugins/builtin/options/analytics.ts +29 -5
  339. package/src/plugins/builtin/options/enrichment.ts +17 -5
  340. package/src/plugins/builtin/options/table.ts +5 -2
  341. package/src/plugins/builtin/options/types.ts +2 -0
  342. package/src/plugins/builtin/options/view.tsx +17 -4
  343. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  344. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  345. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  346. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  347. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  348. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  349. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  350. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  351. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  352. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  353. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  354. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  355. package/src/plugins/builtin/pe-band/model.ts +12 -49
  356. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  357. package/src/plugins/builtin/perps/cascade.ts +236 -0
  358. package/src/plugins/builtin/perps/client.ts +56 -2
  359. package/src/plugins/builtin/perps/headless.ts +65 -9
  360. package/src/plugins/builtin/perps/index.tsx +7 -5
  361. package/src/plugins/builtin/perps/model.ts +18 -4
  362. package/src/plugins/builtin/perps/pane.tsx +19 -22
  363. package/src/plugins/builtin/perps/status-line.ts +25 -0
  364. package/src/plugins/builtin/plugin-module.ts +5 -1
  365. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  366. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  367. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  368. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  369. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  370. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
  371. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  372. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  373. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  374. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
  375. package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
  376. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  377. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  378. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  379. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  380. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  381. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  382. package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
  383. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  384. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  385. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  386. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  387. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  388. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  389. package/src/plugins/builtin/research/event-model.ts +40 -12
  390. package/src/plugins/builtin/research/headless.ts +1 -1
  391. package/src/plugins/builtin/research/index.tsx +7 -3
  392. package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
  393. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  394. package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
  395. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  396. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  397. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  398. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  399. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  400. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  401. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  402. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  403. package/src/plugins/builtin/roadmap/model.ts +67 -0
  404. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  405. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  406. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  407. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  408. package/src/plugins/builtin/scanner/headless.ts +204 -35
  409. package/src/plugins/builtin/scanner/index.ts +16 -5
  410. package/src/plugins/builtin/scanner/live-headless.ts +48 -5
  411. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  412. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  413. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  414. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  415. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  416. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  417. package/src/plugins/builtin/sec/headless.ts +4 -2
  418. package/src/plugins/builtin/sec/index.tsx +3 -0
  419. package/src/plugins/builtin/series-inventory.ts +2 -0
  420. package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
  421. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  422. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  423. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  424. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  425. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  426. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  427. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  428. package/src/plugins/builtin/shared/report-freshness.ts +11 -2
  429. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  430. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  431. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  432. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  433. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  434. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  435. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  436. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  437. package/src/plugins/builtin/themes/model.ts +16 -7
  438. package/src/plugins/builtin/themes/pane.tsx +5 -5
  439. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  440. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  441. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  442. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  443. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  444. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  445. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  446. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  447. package/src/plugins/builtin/ticker-detail/overview/model.ts +11 -3
  448. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  449. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
  450. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  451. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  452. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  453. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  454. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  455. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  456. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  457. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  458. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  459. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  460. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  461. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  462. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  463. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  464. package/src/plugins/builtin/volatility/index.tsx +2 -2
  465. package/src/plugins/builtin/volatility/pane.tsx +7 -4
  466. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  467. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  468. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  469. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  470. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  471. package/src/plugins/catalog-browser.ts +2 -1
  472. package/src/plugins/catalog-manifest.ts +2 -1
  473. package/src/plugins/host-link.ts +1 -1
  474. package/src/plugins/marketplace/model.ts +18 -5
  475. package/src/portfolio/column-values.ts +11 -7
  476. package/src/portfolio/futures-positions.ts +6 -3
  477. package/src/portfolio/position-quantity.ts +15 -9
  478. package/src/remote/resources.ts +2 -1
  479. package/src/renderers/dom/data-table/row.tsx +26 -0
  480. package/src/renderers/dom/desktop/icons.tsx +7 -0
  481. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  482. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  483. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  484. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  485. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  486. package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
  487. package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
  488. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  489. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  490. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  491. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  492. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  493. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  494. package/src/renderers/electrobun/bun/index.ts +26 -2
  495. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  496. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  497. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  498. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  499. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  500. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  501. package/src/renderers/electrobun/shared/protocol.ts +12 -0
  502. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  503. package/src/renderers/electrobun/view/main.tsx +24 -1
  504. package/src/renderers/electrobun/view/news-source.ts +5 -1
  505. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  506. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  507. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  508. package/src/renderers/opentui/start.tsx +5 -0
  509. package/src/series/registry.ts +1 -0
  510. package/src/series/types.ts +6 -0
  511. package/src/series/validate.ts +1 -0
  512. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  513. package/src/sources/gloomberb-cloud/index.ts +43 -8
  514. package/src/sources/gloomberb-cloud/news.ts +47 -7
  515. package/src/sources/provider-router/financials.ts +15 -3
  516. package/src/sources/provider-router/index.ts +8 -5
  517. package/src/state/ticker-cleanup.ts +50 -0
  518. package/src/sync/controller.ts +4 -3
  519. package/src/sync/core-contributors.ts +90 -31
  520. package/src/telemetry/stall-reports-dom.ts +38 -0
  521. package/src/telemetry/stall-reports-node.ts +75 -0
  522. package/src/telemetry/stall-reports.ts +214 -0
  523. package/src/test-support/preload.ts +14 -0
  524. package/src/theme/themes.ts +10 -0
  525. package/src/tickers/company-name-listing.ts +53 -13
  526. package/src/tickers/company-name.ts +50 -4
  527. package/src/tickers/index-roots.ts +7 -0
  528. package/src/tickers/listing-fate.ts +2 -2
  529. package/src/tickers/listing-venues.ts +15 -3
  530. package/src/tickers/search/index.ts +6 -2
  531. package/src/tickers/search/ranking.ts +5 -1
  532. package/src/tickers/search/upsert.ts +13 -0
  533. package/src/tickers/terminal-codes.ts +19 -0
  534. package/src/tickers/ticker-changes.ts +8 -4
  535. package/src/time-series/alignment.ts +1 -1
  536. package/src/time-series/economic.ts +18 -4
  537. package/src/time-series/field-catalog.ts +1 -1
  538. package/src/time-series/filed-trailing-eps.ts +197 -0
  539. package/src/time-series/fundamentals.ts +44 -1
  540. package/src/time-series/ice-credit.ts +3 -0
  541. package/src/time-series/price-comparison.ts +30 -2
  542. package/src/time-series/resolve-context.ts +3 -1
  543. package/src/time-series/resolve.ts +56 -4
  544. package/src/time-series/session-bar.ts +11 -0
  545. package/src/time-series/spec.ts +3 -2
  546. package/src/time-series/studies.ts +8 -2
  547. package/src/time-series/total-return.ts +160 -0
  548. package/src/time-series/transforms.ts +2 -3
  549. package/src/time-series/types.ts +22 -1
  550. package/src/types/company-from.ts +40 -0
  551. package/src/types/data-provider.ts +7 -0
  552. package/src/types/financials.ts +59 -1
  553. package/src/types/headless.ts +27 -3
  554. package/src/types/news-source.ts +1 -0
  555. package/src/types/plugin.ts +9 -0
  556. package/src/ui/host.tsx +3 -0
  557. package/src/updater/failure.ts +4 -1
  558. package/src/utils/cli-output.ts +42 -1
  559. package/src/utils/crypto-pair.ts +165 -7
  560. package/src/utils/declared-dividends.ts +1 -1
  561. package/src/utils/forward-dividend.ts +2 -0
  562. package/src/utils/fundamentals.ts +60 -8
  563. package/src/utils/fx-market-hours.ts +26 -3
  564. package/src/utils/main-thread-stall.ts +35 -2
  565. package/src/utils/market-capitalization.ts +24 -1
  566. package/src/utils/price-history.ts +7 -1
  567. package/src/utils/shell-word.ts +8 -0
  568. package/src/utils/trailing-dividend.ts +53 -4
  569. package/src/version.ts +2 -2
  570. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -11,6 +11,7 @@ const STUDY_FIELDS = [
11
11
  "protocolSection.designModule.phases",
12
12
  "protocolSection.designModule.enrollmentInfo",
13
13
  "protocolSection.conditionsModule.conditions",
14
+ "protocolSection.armsInterventionsModule.interventions",
14
15
  "protocolSection.descriptionModule.briefSummary",
15
16
  ].join(",");
16
17
  const CLINICAL_TRIALS_API_BASE_URL = "https://clinicaltrials.gov/api/v2";
@@ -45,6 +46,8 @@ export interface ClinicalTrial {
45
46
  /** Lead sponsor class, e.g. "INDUSTRY". */
46
47
  sponsorClass: string;
47
48
  conditions: string[];
49
+ /** What is tested, in the registry's order: a drug, a device, a procedure; a placebo arm is left out. */
50
+ interventions: TrialIntervention[];
48
51
  summary: string;
49
52
  studyType: string;
50
53
  enrollment: number | null;
@@ -62,12 +65,18 @@ export interface ClinicalTrial {
62
65
  firstSubmitDate: Date | null;
63
66
  /** When the registry first posted the study. */
64
67
  firstPostDate: Date | null;
65
- /** When the registry last posted a change to the study; the list is newest first by it. */
68
+ /** When the registry last posted a change to the study; the registry hands studies over newest first by it. */
66
69
  lastUpdatePostDate: Date | null;
67
70
  /** Deep link to the study page for [o]pen. */
68
71
  url: string;
69
72
  }
70
73
 
74
+ interface TrialIntervention {
75
+ name: string;
76
+ /** Raw registry type, e.g. "DRUG" or "DEVICE". */
77
+ type: string;
78
+ }
79
+
71
80
  export type DatePrecision = "year" | "month" | "day";
72
81
  export type TrialDateType = "ACTUAL" | "ESTIMATED";
73
82
 
@@ -107,6 +116,28 @@ function asDate(value: unknown): Date | null {
107
116
  return Number.isNaN(date.getTime()) ? null : date;
108
117
  }
109
118
 
119
+ /** Arms that test nothing: a placebo, a sham or the usual treatment. */
120
+ const CONTROL_INTERVENTION = /^(?:placebo|sham|vehicle|saline|standard (?:of )?care|usual care|no intervention|control)\b/i;
121
+
122
+ function parseInterventions(value: unknown): TrialIntervention[] {
123
+ if (!Array.isArray(value)) return [];
124
+ return value.flatMap((item) => {
125
+ if (!item || typeof item !== "object") return [];
126
+ const record = item as Record<string, unknown>;
127
+ const name = asString(record.name);
128
+ if (!name || CONTROL_INTERVENTION.test(name)) return [];
129
+ return [{ name, type: asString(record.type) ?? "" }];
130
+ });
131
+ }
132
+
133
+ /** Kinds of intervention that are a product being tested, ahead of a device, a procedure or a behaviour. */
134
+ const PRODUCT_TYPES = new Set(["DRUG", "BIOLOGICAL", "COMBINATION_PRODUCT", "GENETIC", "DIETARY_SUPPLEMENT"]);
135
+
136
+ /** The one intervention the list names: the first drug or biologic, else the first thing tested; empty when none. */
137
+ export function trialDrug(trial: Pick<ClinicalTrial, "interventions">): string {
138
+ return (trial.interventions.find((item) => PRODUCT_TYPES.has(item.type)) ?? trial.interventions[0])?.name ?? "";
139
+ }
140
+
110
141
  function moduleOf(record: Record<string, unknown>, key: string): Record<string, unknown> {
111
142
  const value = record[key];
112
143
  return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
@@ -144,6 +175,7 @@ export function parseClinicalTrial(raw: unknown): ClinicalTrial | null {
144
175
  const conditionsModule = moduleOf(protocol, "conditionsModule");
145
176
  const description = moduleOf(protocol, "descriptionModule");
146
177
  const enrollmentInfo = moduleOf(design, "enrollmentInfo");
178
+ const armsInterventions = moduleOf(protocol, "armsInterventionsModule");
147
179
 
148
180
  const start = trialDate(moduleOf(statusModule, "startDateStruct"));
149
181
  const primaryCompletion = trialDate(moduleOf(statusModule, "primaryCompletionDateStruct"));
@@ -161,6 +193,7 @@ export function parseClinicalTrial(raw: unknown): ClinicalTrial | null {
161
193
  sponsor: asString(leadSponsor.name) ?? "Unknown sponsor",
162
194
  sponsorClass: asString(leadSponsor.class) ?? "",
163
195
  conditions: asStringArray(conditionsModule.conditions),
196
+ interventions: parseInterventions(armsInterventions.interventions),
164
197
  summary: asString(description.briefSummary) ?? "",
165
198
  studyType: asString(design.studyType) ?? "",
166
199
  enrollment: asCount(enrollmentInfo.count),
@@ -204,6 +237,8 @@ export interface StudiesQuery {
204
237
  phases?: readonly string[];
205
238
  /** Lead sponsor classes, any of, e.g. `INDUSTRY`. */
206
239
  sponsorClasses?: readonly string[];
240
+ /** Lead sponsor classes to leave out, e.g. `INDUSTRY` for everything but companies. */
241
+ excludedSponsorClasses?: readonly string[];
207
242
  pageSize?: number;
208
243
  /** The previous page's `nextPageToken`; without it, the first page. */
209
244
  pageToken?: string;
@@ -222,10 +257,16 @@ function areaClause(area: string, values: string[]): string | null {
222
257
  return values.length === 1 ? `AREA[${area}]${values[0]}` : `AREA[${area}](${values.join(" OR ")})`;
223
258
  }
224
259
 
260
+ /** `NOT AREA[LeadSponsorClass]INDUSTRY`, or `NOT AREA[LeadSponsorClass](NIH OR FED)` for several. */
261
+ function excludeClause(area: string, values: string[]): string | null {
262
+ const clause = areaClause(area, values);
263
+ return clause ? `NOT ${clause}` : null;
264
+ }
265
+
225
266
  /**
226
267
  * Build the v2 studies URL. Pure: no network, no API key. The most recently
227
- * updated studies come first, the order the pane lists them in, so each page
228
- * continues the one before it. Filters go to the registry with the search, so
268
+ * updated studies come first, so each page continues the one before it; the
269
+ * pane orders what has loaded. Filters go to the registry with the search, so
229
270
  * every page it returns already matches them.
230
271
  */
231
272
  export function buildStudiesUrl(query: StudiesQuery = {}): string {
@@ -240,7 +281,11 @@ export function buildStudiesUrl(query: StudiesQuery = {}): string {
240
281
  if (sponsor) params.set("query.spons", sponsor);
241
282
  const statuses = codes(query.statuses);
242
283
  if (statuses.length > 0) params.set("filter.overallStatus", statuses.join(","));
243
- const advanced = [areaClause("Phase", codes(query.phases)), areaClause("LeadSponsorClass", codes(query.sponsorClasses))]
284
+ const advanced = [
285
+ areaClause("Phase", codes(query.phases)),
286
+ areaClause("LeadSponsorClass", codes(query.sponsorClasses)),
287
+ excludeClause("LeadSponsorClass", codes(query.excludedSponsorClasses)),
288
+ ]
244
289
  .filter((clause): clause is string => clause !== null);
245
290
  if (advanced.length > 0) params.set("filter.advanced", advanced.join(" AND "));
246
291
  if (query.pageToken) params.set("pageToken", query.pageToken);
@@ -1,7 +1,8 @@
1
1
  import type { DataTableColumn } from "../../../components";
2
2
  import { blendHex, type ThemeColors } from "../../../theme/colors";
3
3
  import type { PaneReportOptionDef } from "../../../types/plugin";
4
- import type { StudiesQuery } from "./client";
4
+ import type { ClinicalTrial, StudiesQuery } from "./client";
5
+ import type { SponsorTicker } from "./sponsor-tickers";
5
6
 
6
7
  /**
7
8
  * Where a study stands, as a reader of the list sorts it: open to patients,
@@ -109,6 +110,55 @@ export function trialPhaseLabel(phases: readonly string[], compact = false): str
109
110
  return `${compact ? "Ph" : "Phase"} ${numbers.join("/")}`;
110
111
  }
111
112
 
113
+ /** Whether the study has a phase to show: not an observational study with none, nor a device study the registry calls not applicable. */
114
+ export function hasTrialPhase(phases: readonly string[]): boolean {
115
+ const label = trialPhaseLabel(phases);
116
+ return label !== "--" && label !== "N/A";
117
+ }
118
+
119
+ /**
120
+ * How far along a study's phase is, for ordering: Phase 4 before 3 before 2
121
+ * before 1 before early phase 1, a study of two phases (2/3) by its later
122
+ * one, and a study with no phase to show after them all.
123
+ */
124
+ export function trialPhaseRank(phases: readonly string[]): number {
125
+ let rank = 0;
126
+ for (const phase of phases) {
127
+ const code = phase.trim().toUpperCase();
128
+ const value = code === "EARLY_PHASE1" ? 0.5 : Number(code.replace("PHASE", ""));
129
+ if (Number.isFinite(value) && value > rank) rank = value;
130
+ }
131
+ return rank;
132
+ }
133
+
134
+ /**
135
+ * The three kinds of lead sponsor the list is ordered by: a company whose
136
+ * ticker Gloom Cloud found (0), any other company (1), and everything else:
137
+ * universities, hospitals, agencies, networks and individuals (2).
138
+ */
139
+ type TrialSponsorGroup = 0 | 1 | 2;
140
+
141
+ function trialSponsorGroup(trial: Pick<ClinicalTrial, "sponsorClass">, ticker: SponsorTicker | undefined): TrialSponsorGroup {
142
+ if (ticker?.kind === "listed") return 0;
143
+ return trial.sponsorClass === "INDUSTRY" ? 1 : 2;
144
+ }
145
+
146
+ /**
147
+ * The studies as the pane lists them: listed sponsors first, then other
148
+ * companies, then everything else, the latest phase first within each; studies
149
+ * of one group and phase stay in the registry's order (most recently updated
150
+ * first). A new array, so the loaded rows keep their order.
151
+ */
152
+ export function orderTrials<T extends Pick<ClinicalTrial, "sponsorClass" | "phases" | "sponsor">>(
153
+ trials: readonly T[],
154
+ tickerOf: (trial: T) => SponsorTicker | undefined,
155
+ ): T[] {
156
+ return trials
157
+ .map((trial, index) => ({ trial, index, group: trialSponsorGroup(trial, tickerOf(trial)), phase: trialPhaseRank(trial.phases) }))
158
+ .sort((a, b) => a.group - b.group || b.phase - a.phase || a.index - b.index)
159
+ .map(({ trial }) => trial);
160
+ }
161
+
112
162
  /** The phase as the detail view says it. */
113
163
  export function trialPhaseDetail(phases: readonly string[]): string {
114
164
  const label = trialPhaseLabel(phases);
@@ -228,7 +278,7 @@ export const TRIAL_REPORT_OPTIONS: readonly PaneReportOptionDef[] = [
228
278
  },
229
279
  ];
230
280
 
231
- type TrialColumnId = "updated" | "phase" | "status" | "title" | "sponsor" | "ticker";
281
+ type TrialColumnId = "updated" | "phase" | "status" | "title" | "drug" | "sponsor" | "ticker";
232
282
  export type TrialColumn = DataTableColumn & { id: TrialColumnId };
233
283
 
234
284
  export interface TrialLayout {
@@ -242,37 +292,42 @@ export interface TrialLayout {
242
292
  /** The gap after each column plus the table's padding, as the terminal table draws them. */
243
293
  const TABLE_CHROME = 1 + 2;
244
294
 
295
+ /** The title is what the list is read by: the drug and then the date give way before it has less than this. */
296
+ const TITLE_ROOM_WITH_DRUG = 24;
297
+ const TITLE_ROOM_WITH_DATE = 36;
298
+ /** An ISO day, as every date column reads. */
299
+ const DATE_WIDTH = 10;
300
+
245
301
  /**
246
- * The title takes what is left; status, phase and ticker stay narrow and the
247
- * sponsor keeps a readable width. Status reads in full, and the sponsor and
248
- * ticker widen, only once the title has room of its own. The columns add up
249
- * to the pane, so nothing scrolls sideways.
302
+ * The title takes what is left; phase, status and ticker stay narrow and the
303
+ * sponsor keeps a readable width. Status reads in full, and the sponsor, drug
304
+ * and ticker widen, only once the title has room of its own. The columns add
305
+ * up to the pane, so nothing scrolls sideways. Narrower than a terminal the
306
+ * drug, and before it the update date (the detail has it, and the list's
307
+ * dates run close together), give way to the title.
250
308
  */
251
309
  export function trialLayout(width: number): TrialLayout {
252
- const compact = width < 90;
310
+ const compact = width < 100;
253
311
  const wide = width >= 140;
254
- // Narrower than a terminal, the update date gives way to the title: the list
255
- // is newest first and the detail has the date.
256
- const timed = width >= 76;
257
- const fixed: TrialColumn[] = [
258
- // An ISO day, as every date column reads.
259
- ...(timed ? [{ id: "updated" as const, label: "Updated", width: 10, align: "left" as const }] : []),
260
- { id: "phase", label: "Phase", width: compact ? 7 : 9, align: "left" },
261
- { id: "status", label: "Status", width: wide ? 23 : 11, align: "left" },
262
- { id: "sponsor", label: "Sponsor", width: wide ? 30 : compact ? 15 : 22, align: "left" },
263
- { id: "ticker", label: "Ticker", width: wide ? 11 : 6, align: "left" },
264
- ];
265
- const used = fixed.reduce((sum, column) => sum + column.width + 1, 0) + TABLE_CHROME;
266
- const title: TrialColumn = { id: "title", label: "Title", width: Math.max(12, width - used - 1), align: "left", flexGrow: 1 };
267
- const statusAt = fixed.findIndex((column) => column.id === "status") + 1;
268
- return { compact, showVenue: wide, columns: [...fixed.slice(0, statusAt), title, ...fixed.slice(statusAt)] };
269
- }
270
-
271
- /**
272
- * A search typed as a ticker (`TRIAL VKTX`): one to five letters, with a
273
- * share class. The registry knows sponsors by name, so a symbol no study
274
- * mentions is looked up as its company.
275
- */
276
- export function looksLikeTicker(term: string): boolean {
277
- return /^[A-Za-z]{1,5}(?:[.-][A-Za-z])?$/.test(term.trim());
312
+ const phase: TrialColumn = { id: "phase", label: "Phase", width: compact ? 7 : 9, align: "left" };
313
+ const status: TrialColumn = { id: "status", label: "Status", width: wide ? 23 : 11, align: "left" };
314
+ const drug: TrialColumn = { id: "drug", label: "Drug", width: wide ? 18 : compact ? 12 : 14, align: "left" };
315
+ const sponsor: TrialColumn = { id: "sponsor", label: "Sponsor", width: wide ? 28 : compact ? 15 : 22, align: "left" };
316
+ const ticker: TrialColumn = { id: "ticker", label: "Ticker", width: wide ? 11 : 6, align: "left" };
317
+ const updated: TrialColumn = { id: "updated", label: "Updated", width: DATE_WIDTH, align: "left" };
318
+ const cells = (columns: TrialColumn[]) => columns.reduce((sum, column) => sum + column.width + 1, 0) + TABLE_CHROME;
319
+ // What the title keeps beside the other columns: its own gap, and the pane's last column's.
320
+ const titleWidth = (columns: TrialColumn[]) => width - cells(columns) - 1;
321
+ const base = [phase, status, sponsor, ticker];
322
+ const withDrug = titleWidth([...base, drug]) >= TITLE_ROOM_WITH_DRUG;
323
+ const lead = withDrug ? [...base, drug] : base;
324
+ const timed = titleWidth([...lead, updated]) >= TITLE_ROOM_WITH_DATE;
325
+ const rest = timed ? [...lead, updated] : lead;
326
+ const title: TrialColumn = { id: "title", label: "Title", width: Math.max(12, titleWidth(rest)), align: "left", flexGrow: 1 };
327
+ return {
328
+ compact,
329
+ showVenue: wide,
330
+ // Phase and status, the title, then what is about the study (drug, sponsor, ticker) and when it was updated.
331
+ columns: [phase, status, title, ...(withDrug ? [drug] : []), sponsor, ticker, ...(timed ? [updated] : [])],
332
+ };
278
333
  }
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef } from "react";
2
2
  import {
3
3
  DataTableStackView,
4
4
  PaneLinkMenu,
@@ -12,11 +12,9 @@ import {
12
12
  type DataTableCell,
13
13
  type DataTableKeyEvent,
14
14
  type DataTableRootKeyContext,
15
- type PageRequest,
16
15
  type PaneFooterSegment,
17
16
  type PaneHint,
18
17
  type QueryBarFilter,
19
- type RowPage,
20
18
  } from "../../../components";
21
19
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
22
20
  import { DetailReader, type DetailFact } from "../../../components/reader/detail-reader";
@@ -34,15 +32,17 @@ import { Box, Text, TextAttributes, type ScrollBoxRenderable } from "../../../ui
34
32
  import { isPlainArrowUp, stopSearchFocusNavigation } from "../../../utils/search-focus-navigation";
35
33
  import { useOpenDescription } from "../shared/open-description";
36
34
  import { useResearchCloudSession } from "../shared/research-cloud-session";
37
- import { ClinicalTrialsClient, type ClinicalTrial, type DatePrecision, type TrialDateType } from "./client";
35
+ import { missingCell, NO_VALUE } from "../shared/research-cells";
36
+ import { ClinicalTrialsClient, trialDrug, type ClinicalTrial, type DatePrecision, type TrialDateType } from "./client";
38
37
  import {
39
38
  ALL,
40
39
  PHASE_FILTER_OPTIONS,
41
40
  SPONSOR_FILTER_OPTIONS,
42
41
  STATUS_FILTER_OPTIONS,
42
+ hasTrialPhase,
43
43
  keepsToneWhenSelected,
44
- looksLikeTicker,
45
44
  normalizeTrialFilters,
45
+ orderTrials,
46
46
  sponsorClassLabel,
47
47
  trialFiltersNarrow,
48
48
  trialFiltersQuery,
@@ -56,16 +56,19 @@ import {
56
56
  type TrialFilterOption,
57
57
  type TrialFilters,
58
58
  } from "./model";
59
- import { useSponsorTickers, type SponsorTicker } from "./sponsor-tickers";
60
- import { tickerSponsorName } from "./ticker-sponsor";
59
+ import { sponsorTickerStore, useSponsorTickers, type SponsorTicker } from "./sponsor-tickers";
60
+ import { COMPANIES_FIRST, createTrialPageLoader } from "./trial-pages";
61
+ import { looksLikeTicker, useTickerCompanySearch } from "../shared/ticker-company";
61
62
  import { CLINICAL_TRIALS_PANE_ID } from "./types";
62
63
 
63
64
  const SEARCH_DEBOUNCE_MS = 250;
64
65
  const DEFAULT_PAGE_SIZE = 50;
66
+ /** How long a page waits for its sponsors' tickers, so the list draws already in order; a slower lookup fills in after. */
67
+ const TICKER_WAIT_MS = 2000;
65
68
 
66
69
  /** As precise as the registry gave it, marked when it is the sponsor's estimate. */
67
70
  function formatDate(date: Date | null, precision: DatePrecision = "day", type?: TrialDateType): string {
68
- if (!date || Number.isNaN(date.getTime()) || date.getTime() === 0) return "-";
71
+ if (!date || Number.isNaN(date.getTime()) || date.getTime() === 0) return NO_VALUE;
69
72
  const text = date.toISOString().slice(0, precision === "year" ? 4 : precision === "month" ? 7 : 10);
70
73
  return type === "ESTIMATED" ? `${text} (est.)` : text;
71
74
  }
@@ -75,7 +78,7 @@ function trialPosted(trial: ClinicalTrial): Date | null {
75
78
  return trial.firstPostDate ?? trial.firstSubmitDate;
76
79
  }
77
80
 
78
- /** The ticker cell: the symbol, its venue when there is room; blank when there is none; `?` when it was not looked up. */
81
+ /** The ticker cell: the symbol, its venue when there is room; the blank mark when there is none; `?` when it was not looked up; nothing while it is being looked up. */
79
82
  function tickerCell(ticker: SponsorTicker | undefined, showVenue: boolean, colors: ThemeColors, selected: boolean, open: () => void): DataTableCell {
80
83
  if (ticker?.kind === "listed") {
81
84
  const venue = showVenue && ticker.exchange && !/^(NYSE|NASDAQ|AMEX|ARCA|BATS|CBOE)$/.test(ticker.exchange) ? ticker.exchange : "";
@@ -91,6 +94,7 @@ function tickerCell(ticker: SponsorTicker | undefined, showVenue: boolean, color
91
94
  };
92
95
  }
93
96
  if (ticker?.kind === "failed") return { text: "?", value: null, color: colors.textMuted };
97
+ if (ticker?.kind === "none") return missingCell(colors);
94
98
  return { text: "", value: null };
95
99
  }
96
100
 
@@ -122,7 +126,7 @@ function trialFacts(trial: ClinicalTrial, ticker: SponsorTicker | undefined, col
122
126
  ticker?.kind === "listed"
123
127
  ? { label: "Ticker", value: tickerDetail(ticker), color: colors.textBright, link: { label: tf("Open {ticker}", { ticker: ticker.ticker }), onOpen: openTicker } }
124
128
  : { label: "Ticker", value: tickerDetail(ticker), color: colors.textDim },
125
- { label: "Enrollment", value: trial.enrollment != null ? trial.enrollment.toLocaleString("en-US") : "-" },
129
+ { label: "Enrollment", value: trial.enrollment != null ? trial.enrollment.toLocaleString("en-US") : NO_VALUE },
126
130
  { label: "Registry ID", value: trial.nctId },
127
131
  { label: "Start", value: formatDate(trial.startDate, trial.startDatePrecision, trial.startDateType) },
128
132
  { label: "Posted", value: formatDate(trialPosted(trial)) },
@@ -169,25 +173,27 @@ export function TrialsPane({ width, height, focused }: PaneProps) {
169
173
 
170
174
  // The registry pages by an opaque token, which the next page's offset keys.
171
175
  // A new search or filter makes a new loader and so starts over with no tokens.
176
+ // With no sponsor class chosen, the companies' studies page first and the rest
177
+ // after them, so the list opens on the companies instead of on whatever was
178
+ // updated last; the pane orders what has loaded (`orderTrials`).
172
179
  const term = query.trim();
173
180
  const filtersKey = `${filters.status}|${filters.phase}|${filters.sponsorClass}`;
174
- const loadPage = useMemo(() => {
175
- const tokens = new Map<number, string>();
176
- const narrowing = trialFiltersQuery(filters);
177
- return async ({ offset, signal }: PageRequest): Promise<RowPage<ClinicalTrial>> => {
178
- const pageToken = offset > 0 ? tokens.get(offset) : undefined;
179
- if (offset > 0 && !pageToken) return { rows: [], hasMore: false };
180
- const page = await client.listTrials({ term: term || undefined, ...narrowing, pageSize: DEFAULT_PAGE_SIZE, pageToken }, signal);
181
- const nextOffset = offset + page.trials.length;
182
- if (page.nextPageToken) tokens.set(nextOffset, page.nextPageToken);
183
- return { rows: page.trials, hasMore: !!page.nextPageToken, nextOffset };
184
- };
181
+ const loadPage = useMemo(() => createTrialPageLoader({
182
+ list: (studiesQuery, signal) => client.listTrials(studiesQuery, signal),
183
+ query: { term: term || undefined, ...trialFiltersQuery(filters), pageSize: DEFAULT_PAGE_SIZE },
184
+ stages: filters.sponsorClass === ALL ? COMPANIES_FIRST : [{}],
185
+ // A page's sponsors are asked for before it shows, so it lands in order; a slow lookup fills in after.
186
+ onPage: (pageTrials, signal) => sponsorTickerStore.loadWithin(
187
+ pageTrials.map((trial) => trial.sponsor),
188
+ TICKER_WAIT_MS,
189
+ signal,
190
+ { companies: pageTrials.filter((trial) => trial.sponsorClass === "INDUSTRY").map((trial) => trial.sponsor) },
191
+ ),
185
192
  // `filtersKey` stands for `filters`, which is rebuilt each render.
186
193
  // eslint-disable-next-line react-hooks/exhaustive-deps
187
- }, [client, filtersKey, term]);
194
+ }), [client, filtersKey, term]);
188
195
  const studies = usePagedRows(loadPage, { getId: (trial) => trial.nctId, keepPreviousRows: true });
189
- const trials = studies.rows;
190
- const error = studies.error?.message ?? null;
196
+ const loadedTrials = studies.rows;
191
197
  const loadMore = useTableLoadMore(scrollRef, studies.hasMore && !openItemId, studies.loadMore);
192
198
  // Scrolling asks for the next page, so a page that does not fill a tall
193
199
  // pane asks for it once it is laid out.
@@ -199,12 +205,19 @@ export function TrialsPane({ width, height, focused }: PaneProps) {
199
205
  if (box?.viewport && box.scrollHeight > 0 && box.scrollHeight <= box.viewport.height) loadNextPage();
200
206
  }, 0);
201
207
  return () => clearTimeout(timer);
202
- }, [hasMore, height, loadNextPage, loadingMore, openItemId, trials.length]);
208
+ }, [hasMore, height, loadNextPage, loadingMore, openItemId, loadedTrials.length]);
203
209
 
204
- // Tickers come from Gloom Cloud a page of sponsors at a time; rows never wait for them.
205
- const sponsors = useMemo(() => [...new Set(trials.map((trial) => trial.sponsor))], [trials]);
206
- const sponsorTickers = useSponsorTickers(sponsors, session.requestKey);
210
+ // Tickers come from Gloom Cloud a page of sponsors at a time; a page waits a moment for them (see the loader).
211
+ // A company its ticker lookup has no name for is looked up in its search (Bayer is BAYN on Xetra).
212
+ const sponsors = useMemo(() => [...new Set(loadedTrials.map((trial) => trial.sponsor))], [loadedTrials]);
213
+ const companies = useMemo(
214
+ () => [...new Set(loadedTrials.filter((trial) => trial.sponsorClass === "INDUSTRY").map((trial) => trial.sponsor))],
215
+ [loadedTrials],
216
+ );
217
+ const sponsorTickers = useSponsorTickers(sponsors, session.requestKey, { companies });
207
218
  const tickerOf = useCallback((trial: ClinicalTrial) => sponsorTickers.tickers.get(trial.sponsor), [sponsorTickers.tickers]);
219
+ // Listed sponsors first, then other companies, then the rest, the latest phase first in each.
220
+ const trials = useMemo(() => orderTrials(loadedTrials, tickerOf), [loadedTrials, tickerOf]);
208
221
 
209
222
  const selectedTrial = trials.find((item) => item.nctId === selectedId) ?? trials[0] ?? null;
210
223
  const openTrial = openItemId
@@ -230,28 +243,23 @@ export function TrialsPane({ width, height, focused }: PaneProps) {
230
243
  setOpenItemId(null);
231
244
  }, [filters, setOpenItemId, setSavedFilters, setSelectedId]);
232
245
 
233
- const reload = useCallback(() => {
234
- studies.reload();
235
- sponsorTickers.retry();
236
- }, [sponsorTickers, studies]);
237
246
  // `TRIAL VKTX`: no study names a symbol, so one that finds nothing is
238
247
  // searched as its company's name, quoted so the registry matches the whole
239
248
  // name as a sponsor files it, and the search box then shows it. Each term
240
249
  // is looked up once.
241
- const [looked, setLooked] = useState<ReadonlySet<string>>(() => new Set());
242
- const resolvingTicker = studies.status === "loaded" && trials.length === 0 && !studies.error
243
- && looksLikeTicker(term) && !looked.has(term.toUpperCase());
244
- useEffect(() => {
245
- if (!resolvingTicker) return;
246
- let live = true;
247
- void tickerSponsorName(dataProvider, term).catch(() => null).then((name) => {
248
- if (!live) return;
249
- setLooked((current) => new Set([...current, term.toUpperCase()]));
250
- if (name) updateQuery(`"${name}"`);
251
- });
252
- return () => { live = false; };
253
- }, [dataProvider, resolvingTicker, term, updateQuery]);
254
- const loading = (studies.loading && trials.length === 0) || resolvingTicker;
250
+ const companySearch = useTickerCompanySearch(dataProvider, term, {
251
+ eligible: studies.status === "loaded" && trials.length === 0 && !studies.error && looksLikeTicker(term),
252
+ key: term.toUpperCase(),
253
+ onCompany: (name) => updateQuery(`"${name}"`),
254
+ });
255
+ const retryCompanySearch = companySearch.retry;
256
+ const error = studies.error?.message ?? companySearch.error;
257
+ const reload = useCallback(() => {
258
+ retryCompanySearch();
259
+ studies.reload();
260
+ sponsorTickers.retry();
261
+ }, [retryCompanySearch, sponsorTickers, studies]);
262
+ const loading = (studies.loading && trials.length === 0) || companySearch.resolving;
255
263
  usePaneRefreshKey(reload, { focused, enabled: !searchFocused && !openItemId });
256
264
 
257
265
  const detailTicker = detailTrial ? tickerOf(detailTrial) : undefined;
@@ -314,11 +322,11 @@ export function TrialsPane({ width, height, focused }: PaneProps) {
314
322
  switch (column.id) {
315
323
  case "updated": {
316
324
  const updated = formatDate(trial.lastUpdatePostDate);
317
- return trial.lastUpdatePostDate ? { text: updated, value: updated, color: colors.textDim } : { text: "--", value: null, color: colors.textMuted };
325
+ return trial.lastUpdatePostDate ? { text: updated, value: updated, color: colors.textDim } : missingCell(colors);
318
326
  }
319
327
  case "phase": {
320
- const text = trialPhaseLabel(trial.phases, layout.compact);
321
- return { text, color: text === "--" || text === "N/A" ? colors.textMuted : colors.text };
328
+ if (!hasTrialPhase(trial.phases)) return missingCell(colors);
329
+ return { text: trialPhaseLabel(trial.phases, layout.compact), color: colors.text };
322
330
  }
323
331
  case "status": {
324
332
  const status = trialStatus(trial.status);
@@ -331,6 +339,10 @@ export function TrialsPane({ width, height, focused }: PaneProps) {
331
339
  }
332
340
  case "title":
333
341
  return { text: trial.title, color: colors.text, attributes: rowState.selected ? TextAttributes.BOLD : undefined };
342
+ case "drug": {
343
+ const drug = trialDrug(trial);
344
+ return drug ? { text: drug, color: colors.text } : missingCell(colors);
345
+ }
334
346
  case "sponsor":
335
347
  return { text: trial.sponsor, color: trial.sponsorClass === "INDUSTRY" ? colors.textBright : colors.textDim };
336
348
  case "ticker": {
@@ -0,0 +1,90 @@
1
+ import { apiClient } from "../../../api-client";
2
+ import { companyNameExtraWords, shortCompanyName } from "../../../tickers/company-name";
3
+ import { classifyInstrumentKind } from "../../../tickers/search/ranking";
4
+ import type { InstrumentSearchResult } from "../../../types/instrument";
5
+ import { canonicalExchange } from "../../../utils/exchanges";
6
+
7
+ /** A company's listing as Gloom Cloud's search marks its primary one. */
8
+ export interface FoundListing {
9
+ ticker: string;
10
+ exchange: string | null;
11
+ entityName: string | null;
12
+ }
13
+
14
+ /** The listing of each name asked, null for a name with none that qualifies; rejects when the search could not answer. */
15
+ export type ListingFinder = (names: readonly string[], signal?: AbortSignal) => Promise<Map<string, FoundListing | null>>;
16
+
17
+ /** Rows asked of the search for each name: enough to see every company's primary listing that carries it. */
18
+ const SEARCH_LIMIT = 30;
19
+
20
+ /**
21
+ * Legal forms that say which country's company a name is. The search drops
22
+ * them when it compares names, so "Merck KGaA" reads as "Merck & Co.": a
23
+ * sponsor filed with one of them is only matched to a listing that has it.
24
+ */
25
+ const COUNTRY_FORMS = new Set([
26
+ "ag", "kgaa", "gmbh", "kg", "se", "sa", "sas", "sarl", "srl", "spa", "nv", "bv", "ab", "as", "asa", "aps", "oy", "oyj", "plc", "pcl", "bhd", "berhad",
27
+ ]);
28
+
29
+ /** Legal forms and titles written out in a listing's name ("Bayer Aktiengesellschaft", "Koninklijke Philips"), which the search reads as words of the name. */
30
+ const WRITTEN_FORMS = new Set([
31
+ "aktiengesellschaft", "aktieselskab", "aktiebolag", "naamloze", "vennootschap", "societe", "anonyme", "sociedad", "anonima",
32
+ "kabushiki", "kaisha", "spolka", "akcyjna", "osakeyhtio", "koninklijke",
33
+ ]);
34
+
35
+ function wordsOf(name: string): string[] {
36
+ return name.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
37
+ }
38
+
39
+ function listedExchange(result: InstrumentSearchResult): string {
40
+ return canonicalExchange(result.exchange === "SMART" ? result.primaryExchange : result.exchange || result.primaryExchange);
41
+ }
42
+
43
+ /** What a sponsor's name is searched as: the name without its legal form, when that is one word; null for any longer name. */
44
+ export function sponsorSearchName(sponsor: string): string | null {
45
+ const short = shortCompanyName(sponsor.trim());
46
+ return short && !/\s/.test(short) ? short : null;
47
+ }
48
+
49
+ /**
50
+ * The listing of the company a sponsor's name stands for, from the rows the
51
+ * search gave for `sponsorSearchName`, for a sponsor Gloom Cloud has no
52
+ * reviewed name for (`Bayer` is Bayer AG, BAYN on Xetra). Careful by design,
53
+ * since a wrong ticker beside a sponsor is worse than none:
54
+ * - only a name that is one word once its legal form is dropped is asked;
55
+ * - the listing is a primary listing whose name is that word and nothing
56
+ * more, apart from legal forms (Bayer AG, but not Evolv Technologies
57
+ * Holdings for Evolv);
58
+ * - when more than one company's primary listing fits (Siemens), none does;
59
+ * - a sponsor filed under a legal form of its own country (Merck KGaA) never
60
+ * takes a listing without it.
61
+ * A longer name (a university, a hospital, a start-up with its subsidiary
62
+ * line) is never matched.
63
+ */
64
+ export function pickSponsorListing(sponsor: string, rows: readonly InstrumentSearchResult[]): FoundListing | null {
65
+ const short = sponsorSearchName(sponsor);
66
+ if (!short) return null;
67
+ const sponsorForms = wordsOf(sponsor).filter((word) => COUNTRY_FORMS.has(word));
68
+ const fits = rows.filter((row) => {
69
+ if (row.homeListing !== true || classifyInstrumentKind(row.type) !== "equity" || !listedExchange(row)) return false;
70
+ const words = wordsOf(row.name);
71
+ if (sponsorForms.some((form) => !words.includes(form))) return false;
72
+ return companyNameExtraWords(short, row.name) === words.filter((word) => WRITTEN_FORMS.has(word)).length;
73
+ });
74
+ if (fits.length !== 1) return null;
75
+ const [row] = fits;
76
+ return { ticker: row!.symbol.toUpperCase(), exchange: listedExchange(row!), entityName: row!.name };
77
+ }
78
+
79
+ /** One batched search of Gloom Cloud for every sponsor name that can be matched; the rest answer null without a request. */
80
+ export const findSponsorListings: ListingFinder = async (sponsors, signal) => {
81
+ const found = new Map<string, FoundListing | null>(sponsors.map((sponsor) => [sponsor, null]));
82
+ const searched = new Map(sponsors.flatMap((sponsor) => {
83
+ const name = sponsorSearchName(sponsor);
84
+ return name ? [[sponsor, name] as const] : [];
85
+ }));
86
+ if (searched.size === 0) return found;
87
+ const answers = await apiClient.searchInstrumentsBatch([...new Set(searched.values())], SEARCH_LIMIT, signal ? { signal } : {});
88
+ for (const [sponsor, name] of searched) found.set(sponsor, pickSponsorListing(sponsor, answers.get(name) ?? []));
89
+ return found;
90
+ };