@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
@@ -2,6 +2,7 @@ import type { ScreenerQuote } from "../../../api-client/market-discovery";
2
2
  export type { ScreenerQuote } from "../../../api-client/market-discovery";
3
3
  import { resolveCurrencyUnit } from "../../../utils/currency-units";
4
4
  import { logFailure } from "../../../utils/log-failure";
5
+ import { savedCopyMark, servedFromSavedCopy } from "../../../data/saved-copies";
5
6
  import type { PluginPersistence } from "../../../types/plugin";
6
7
  import { apiClient } from "../../../api-client";
7
8
  import type {
@@ -68,7 +69,7 @@ function shouldUseCache(api: MarketScreenerApi, options?: FetchCacheOptions): bo
68
69
  return options?.cache === true || (options?.cache !== false && api === screenerApi);
69
70
  }
70
71
 
71
- function readCache<T>(key: string, options?: { allowExpired?: boolean }): { data: T; stale: boolean } | null {
72
+ function readCache<T>(key: string, options?: { allowExpired?: boolean }): { data: T; stale: boolean; fetchedAt: number } | null {
72
73
  const record = marketMoversPersistence?.getResource<CachedResult<T>>(CACHE_KIND, key, {
73
74
  sourceKey: CACHE_SOURCE,
74
75
  schemaVersion: CACHE_SCHEMA_VERSION,
@@ -79,6 +80,7 @@ function readCache<T>(key: string, options?: { allowExpired?: boolean }): { data
79
80
  return {
80
81
  data: record.value.data,
81
82
  stale: record.value.stale || !!record.stale || failedFetches.has(key),
83
+ fetchedAt: record.fetchedAt,
82
84
  };
83
85
  }
84
86
 
@@ -117,10 +119,11 @@ async function loadCached<T>(
117
119
  return result;
118
120
  })
119
121
  .catch((error) => {
120
- // Serving the expired copy is right; hiding that it is expired is not.
122
+ // Serving the expired copy is right; hiding that it is expired is not. A command that shows it
123
+ // ends with when the copy was saved.
121
124
  if (fallback) {
122
125
  failedFetches.add(key);
123
- return { data: fallback.data, stale: true };
126
+ return servedFromSavedCopy({ data: fallback.data, stale: true }, fallback.fetchedAt);
124
127
  }
125
128
  throw error;
126
129
  })
@@ -220,6 +223,17 @@ function parseScreenerSessions(row: Record<string, unknown>): CloudMoverSessionF
220
223
  return sessions;
221
224
  }
222
225
 
226
+ /** A positive float with its state, or nothing: a row without one fails any minimum. */
227
+ function floatOf(source: { floatShares?: unknown; floatSharesState?: unknown; floatSharesReason?: unknown } | null | undefined): Pick<ScreenerQuote, "floatShares" | "floatSharesState" | "floatSharesReason"> {
228
+ const shares = screenerNumber(source?.floatShares);
229
+ if (shares == null || !(shares > 0)) return {};
230
+ return {
231
+ floatShares: shares,
232
+ ...(source?.floatSharesState === "partial" ? { floatSharesState: "partial" as const } : {}),
233
+ ...(source?.floatSharesState === "partial" && typeof source.floatSharesReason === "string" ? { floatSharesReason: source.floatSharesReason } : {}),
234
+ };
235
+ }
236
+
223
237
  /** Validate numeric fields at the API boundary so absent values stay unavailable. */
224
238
  export function parseScreenerResponse(data: unknown): ScreenerQuote[] {
225
239
  if (!data || typeof data !== "object" || !("quotes" in data) || !Array.isArray(data.quotes)) {
@@ -239,6 +253,7 @@ export function parseScreenerResponse(data: unknown): ScreenerQuote[] {
239
253
  previousClose: screenerNumber(q.previousClose) ?? undefined, exchange: typeof q.exchange === "string" ? q.exchange : "",
240
254
  lastUpdated: typeof q.lastUpdated === "number" && Number.isFinite(q.lastUpdated) && q.lastUpdated > 0 ? q.lastUpdated : undefined,
241
255
  sessions: parseScreenerSessions(q),
256
+ ...floatOf(q),
242
257
  }];
243
258
  });
244
259
  }
@@ -325,6 +340,8 @@ function mergeCloudScreenerItem(
325
340
  lastUpdated: item.lastUpdated,
326
341
  previousClose: screenerNumber(item.previousClose) ?? undefined,
327
342
  sessions: parseScreenerSessions(item as unknown as Record<string, unknown>),
343
+ // The live list's own float, the delayed list's when it sends none, each with its own state.
344
+ ...floatOf(item.floatShares != null ? item : metadata),
328
345
  };
329
346
  }
330
347
 
@@ -375,7 +392,7 @@ export async function fetchPreferredMarketMovers(
375
392
  const eligibility = sources.isCloudEligible();
376
393
  if (!(typeof eligibility === "boolean" ? eligibility : await eligibility)) {
377
394
  const result = await sources.fetchMarket(category, count, options);
378
- return { quotes: rankScreenerQuotes(category, result.data), source: "gloom", stale: result.stale };
395
+ return { ...savedCopyMark(result), quotes: rankScreenerQuotes(category, result.data), source: "gloom", stale: result.stale };
379
396
  }
380
397
 
381
398
  const marketMetadata = sources.fetchMarket(
@@ -411,6 +428,7 @@ export async function fetchPreferredMarketMovers(
411
428
 
412
429
  const fallback = await marketMetadata;
413
430
  return {
431
+ ...savedCopyMark(fallback),
414
432
  quotes: rankScreenerQuotes(category, fallback.data),
415
433
  source: "gloom",
416
434
  stale: fallback.stale,
@@ -20,6 +20,7 @@ import { nextHeaderSort } from "../../../utils/sort-values";
20
20
  import { usePluginPaneState, usePluginTickerActions } from "../../runtime";
21
21
  import { ProWall, SignInWall } from "../cloud/auth-actions";
22
22
  import { loadSessionMovers, SessionMoversAccessError } from "./client";
23
+ import { ANY_FLOAT, minFloatEmptyTitle, minFloatFilter, minFloatShares, partialFloatFooter } from "./min-float";
23
24
  import { summaryFooterSegments } from "./footer";
24
25
  import type { MarketSummaryQuote } from "./screener";
25
26
  import {
@@ -104,7 +105,9 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
104
105
  const { pinTicker } = usePluginTickerActions();
105
106
  const [savedSide, setSavedSide] = usePluginPaneState<CloudSessionMoversSide>("sessionSide", "up");
106
107
  const side = resolveSide(view, savedSide);
107
- const listKey = `${view}:${side}`;
108
+ const [minFloatValue, setMinFloatValue] = usePluginPaneState<string>("minFloat", ANY_FLOAT);
109
+ const minFloat = minFloatShares(minFloatValue);
110
+ const listKey = `${view}:${side}:${minFloat ?? ""}`;
108
111
  const [loaded, setLoaded] = useState<LoadedList | null>(null);
109
112
  // The first frame would otherwise claim an empty list.
110
113
  const [loading, setLoading] = useState(true);
@@ -132,9 +135,9 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
132
135
  setLoadError(null);
133
136
  }
134
137
  try {
135
- const next = await loadSessionMovers(view, side, options);
138
+ const next = await loadSessionMovers(view, side, { ...options, minFloat });
136
139
  if (fetchGenRef.current !== gen) return;
137
- setLoaded({ key: `${view}:${side}`, payload: next });
140
+ setLoaded({ key: `${view}:${side}:${minFloat ?? ""}`, payload: next });
138
141
  setLoadError(null);
139
142
  setLastLoadedAt(Date.now());
140
143
  } catch (error) {
@@ -147,7 +150,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
147
150
  } finally {
148
151
  if (fetchGenRef.current === gen && !options?.background) setLoading(false);
149
152
  }
150
- }, [onDenied, side, view]);
153
+ }, [minFloat, onDenied, side, view]);
151
154
 
152
155
  useEffect(() => {
153
156
  setSelectedId(null);
@@ -167,7 +170,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
167
170
  freshnessScopeKey: `market-movers:${listKey}`,
168
171
  liveStreaming,
169
172
  });
170
- const columns = useMemo(() => buildSessionMoverColumns(view, width), [view, width]);
173
+ const columns = useMemo(() => buildSessionMoverColumns(view, width, { float: minFloat != null }), [minFloat, view, width]);
171
174
  // The sort survives a change of list, but not onto a column this list or width does not show.
172
175
  const sort = columns.some((column) => column.id === sortPreference.columnId) ? sortPreference : DEFAULT_SORT;
173
176
  const rows = useMemo(
@@ -199,8 +202,10 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
199
202
  parts: [{ text: feedStatus, tone: feedStatus === "live" ? "value" as const : "muted" as const }],
200
203
  }] : []),
201
204
  ...(payload?.stale ? [{ id: "stale", parts: [{ text: "stale", tone: "muted" as const }] }] : []),
205
+ // A float the source does not date says so once, while the FLOAT column shows.
206
+ ...partialFloatFooter(columns.some((column) => column.id === "floatShares"), rows),
202
207
  ],
203
- }), [asOfLabel, feedStatus, loadError, loading, payload?.stale, sessionLabel, summaryQuotes]);
208
+ }), [asOfLabel, columns, feedStatus, loadError, loading, payload?.stale, rows, sessionLabel, summaryQuotes]);
204
209
 
205
210
  const openRow = useCallback((row: SessionMoverRow) => {
206
211
  pinTicker(rowKey(row), { floating: true, paneType: TICKER_RESEARCH_PANE_ID, instrument: null });
@@ -217,7 +222,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
217
222
  const sides = sessionSides(view);
218
223
  // Until this list's first answer the table is loading, including the frame
219
224
  // between a change of list and its request going out.
220
- const emptyTitle = payload ? "No movers yet." : loadError ?? "Loading movers...";
225
+ const emptyTitle = payload ? (minFloat != null ? minFloatEmptyTitle(minFloatValue) : "No movers yet.") : loadError ?? "Loading movers...";
221
226
  return (
222
227
  <DataTableView<SessionMoverRow, SessionMoverColumn>
223
228
  focused={focused}
@@ -231,7 +236,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
231
236
  value: side,
232
237
  options: sides,
233
238
  onChange: (value: CloudSessionMoversSide) => setSavedSide(value),
234
- }]}
239
+ }, minFloatFilter(minFloatValue, setMinFloatValue)]}
235
240
  />
236
241
  )}
237
242
  selection={{ kind: "id", selectedId, getId: rowKey, onChange: setSelectedId }}
@@ -21,9 +21,10 @@ const VOLUME_LABEL: Record<CloudSessionMoversCategory, string> = {
21
21
  * Pre-market and after hours: last, change from the reference close, session
22
22
  * volume, relative volume, float and the catalyst. Gaps add the gap and the
23
23
  * distance from VWAP. The name gives way first, then the columns a narrow pane
24
- * can do without (the day's volume, then the float).
24
+ * can do without (the day's volume, then the float, which stays while a
25
+ * minimum float is set: `float`).
25
26
  */
26
- export function buildSessionMoverColumns(view: CloudSessionMoversCategory, width: number): SessionMoverColumn[] {
27
+ export function buildSessionMoverColumns(view: CloudSessionMoversCategory, width: number, options: { float?: boolean } = {}): SessionMoverColumn[] {
27
28
  const columns: SessionMoverColumn[] = [
28
29
  { id: "rank", label: "#", width: 3, align: "left" },
29
30
  { id: "symbol", label: "TICKER", width: 8, align: "left" },
@@ -37,7 +38,7 @@ export function buildSessionMoverColumns(view: CloudSessionMoversCategory, width
37
38
  { id: "floatShares", label: "FLOAT", width: 7, align: "right" },
38
39
  { id: "catalyst", label: "EVENT", width: 6, align: "left" },
39
40
  ];
40
- const optional: SessionMoverColumnId[] = view === "gaps" ? ["volume", "floatShares"] : ["floatShares"];
41
+ const optional: SessionMoverColumnId[] = [...(view === "gaps" ? ["volume" as const] : []), ...(options.float ? [] : ["floatShares" as const])];
41
42
  // Measured the way the table draws it: a header widens its column, and a
42
43
  // left-aligned column after a right-aligned one gets a lead gap. The name
43
44
  // sits between left-aligned columns, so it adds only its width and a gutter.
@@ -26,15 +26,20 @@ function fiftyTwoWeekPosition(price: number | null, low: number | undefined, hig
26
26
  }
27
27
 
28
28
  const EXTENDED_COLUMN_WIDTH = 9;
29
- /** With an extended column, a narrow pane drops MCAP, then 52W%, before the name gets narrower than this. */
29
+ /** With an extended or float column, a narrow pane drops MCAP, then 52W%, before the name gets narrower than this. */
30
30
  const EXTENDED_MIN_NAME_WIDTH = 12;
31
31
 
32
32
  /**
33
33
  * The list's columns at `width`. A pre-market or after-hours column follows
34
- * CHG% for each session in `extendedSessions`; to make room for it a narrow
35
- * pane gives up MCAP, then 52W%, before it cuts the names short.
34
+ * CHG% for each session in `extendedSessions`, and FLOAT ends the row while a
35
+ * minimum float is set (`float`); to make room for them a narrow pane gives up
36
+ * MCAP, then 52W%, before it cuts the names short.
36
37
  */
37
- export function buildMarketMoverColumns(width: number, extendedSessions: readonly ExtendedSession[] = []): MarketMoverColumn[] {
38
+ export function buildMarketMoverColumns(
39
+ width: number,
40
+ extendedSessions: readonly ExtendedSession[] = [],
41
+ options: { float?: boolean } = {},
42
+ ): MarketMoverColumn[] {
38
43
  const lead: MarketMoverColumn[] = [
39
44
  { id: "rank", label: "#", width: 3, align: "left" },
40
45
  { id: "symbol", label: "TICKER", width: 8, align: "left" },
@@ -55,12 +60,13 @@ export function buildMarketMoverColumns(width: number, extendedSessions: readonl
55
60
  { id: "volumeRatio", label: "V/AVG", width: 6, align: "right" },
56
61
  { id: "range", label: "52W%", width: 6, align: "right" },
57
62
  { id: "marketCap", label: "MCAP", width: 8, align: "right" },
63
+ ...(options.float ? [{ id: "floatShares" as const, label: "FLOAT", width: 7, align: "right" as const }] : []),
58
64
  ];
59
65
  // Measured the way the table draws them (a header and its sort mark widen a
60
66
  // column, gaps between them); the name adds its own width and a gap.
61
67
  const nameRoom = () => width - getTableWidth([...lead, ...quote, ...trailing]) - 1;
62
68
  for (const id of ["marketCap", "range"] as const) {
63
- if (extendedSessions.length === 0 || nameRoom() >= EXTENDED_MIN_NAME_WIDTH) break;
69
+ if ((extendedSessions.length === 0 && !options.float) || nameRoom() >= EXTENDED_MIN_NAME_WIDTH) break;
64
70
  trailing.splice(trailing.findIndex((column) => column.id === id), 1);
65
71
  }
66
72
  return [...lead, { id: "name", label: "NAME", width: Math.max(6, nameRoom()), align: "left" }, ...quote, ...trailing];
@@ -121,5 +127,11 @@ export function renderMarketMoverCell(
121
127
  value: row.marketCap,
122
128
  color: colors.textDim,
123
129
  };
130
+ case "floatShares":
131
+ return {
132
+ text: row.floatShares != null ? formatCompact(row.floatShares, { fixedDecimals: true }) : "—",
133
+ value: row.floatShares ?? null,
134
+ color: colors.textDim,
135
+ };
124
136
  }
125
137
  }
@@ -40,7 +40,8 @@ export function Breadth({ fund, width, height, focused, active }: Pick<PaneProps
40
40
  color: colors.warning, calendarSpaced: true,
41
41
  })], [history, metric, colors.warning]);
42
42
  const link = useChartTableSelection({ rows, getId: keyOf, getDate: dateOf, selectedId: selected ?? latest?.date ?? null, onSelect: setSelected, focused });
43
- const sessionOpen = useLiveSessionRefresh(() => resource.load(true), data?.access === "realtime" ? 15_000 : 60_000, active && visible, resource.updatedAt);
43
+ // The resource keeps a failed load as its error (stale, and a warning below), so this refresh never rejects.
44
+ const sessionOpen = useLiveSessionRefresh(() => resource.load(true), data?.access === "realtime" ? 15_000 : 60_000, active && visible, resource.updatedAt).live;
44
45
  useAutoRefresh(resource.updatedAt, () => { if (active && visible && !sessionOpen) void resource.load(data?.basis === "intraday"); });
45
46
  usePaneRefreshKey(() => void resource.reload(), { focused, enabled: active });
46
47
  usePaneStatusFooter({ registrationId: "members:breadth", loading: !!data && resource.loading,
@@ -1,6 +1,7 @@
1
1
  import type { MnaDealsParams, MnaRegionFilter, MnaStatusFilter, MnaTargetFilter } from "../../../api-client/mna";
2
2
  import type { HeadlessPaneColumn, HeadlessPaneDefinition } from "../../../types/plugin";
3
3
  import { fetchMnaDeals } from "./client";
4
+ import { companyDataNotes } from "../../../types/company-from";
4
5
  import { dealsSymbol, formatDealValue, formatExpectedClose, stageLabel, termsLabel } from "./model";
5
6
  import { REPORT_ROWS } from "../shared/headless-options";
6
7
 
@@ -92,6 +93,7 @@ export const mnaHeadless: HeadlessPaneDefinition<"rows"> = {
92
93
  headline: deal.headline,
93
94
  })),
94
95
  errors: payload.lockedDeals > 0 ? [`${payload.lockedDeals} deals from the last ${payload.delayDays} days need Gloom Pro`] : [],
96
+ ...(payload.companyFrom ? { notes: companyDataNotes(payload.companyFrom) } : {}),
95
97
  metadata: {
96
98
  asOf: payload.asOf, access: payload.access, delayDays: payload.delayDays, hasMore: payload.hasMore,
97
99
  },
@@ -12,7 +12,7 @@ import {
12
12
  type PaneHint,
13
13
  type SelectControl,
14
14
  } from "../../../components";
15
- import { usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
15
+ import { useCompanyDataFooter, usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
16
16
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
17
17
  import { isAccessDenied } from "../../../api-client/errors";
18
18
  import type {
@@ -337,6 +337,8 @@ function MnaDealsView({ focused, width, height, symbol }: {
337
337
  hints,
338
338
  });
339
339
  usePaneFooter("mna:access", () => (access.segment && !openId ? { order: 5, info: [access.segment] } : null), [access.segment, openId]);
340
+ // A receipt's or foreign line's deals include its company's, read from the home line.
341
+ useCompanyDataFooter(data?.companyFrom, !openId);
340
342
 
341
343
  const placeholder = blendHex(colors.bg, colors.textMuted, 0.35);
342
344
  const renderCell = useCallback((item: Item, column: DataTableColumn, _index: number, state: { selected: boolean }): DataTableCell => {
@@ -12,6 +12,7 @@ import {
12
12
  type CloudNewsRead,
13
13
  } from "../../../sources/gloomberb-cloud/news";
14
14
  import { parsePublicTickerKey } from "../../../utils/exchanges";
15
+ import { companyDataNotes, readCompanyFrom } from "../../../types/company-from";
15
16
  import { formatUtcSeconds } from "../../../utils/utc-time";
16
17
  import { cloudNewsFreshness, cloudRealtimeAccess } from "../shared/report-freshness";
17
18
  import { REPORT_FILTER, REPORT_ROWS } from "../shared/headless-options";
@@ -95,7 +96,8 @@ function projectNewsHeadless(
95
96
  // A coin pair no story is filed under shows the stories that mention the coin, and says so.
96
97
  ...(response.searchedFor && args.symbols[0]
97
98
  ? { notes: [coinNewsFallbackNotice(parsePublicTickerKey(args.symbols[0]).symbol, response.searchedFor)] }
98
- : {}),
99
+ // A receipt's or foreign line's stories are its company's, read from the home line.
100
+ : response.companyFrom ? { notes: companyDataNotes(readCompanyFrom(response.companyFrom)) } : {}),
99
101
  metadata: {
100
102
  nextCursor: response.nextCursor,
101
103
  sentiment,
@@ -6,6 +6,7 @@ import { instrumentFromTicker } from "../../../market-data/request-types";
6
6
  import { useCallback } from "react";
7
7
  import { useDebouncedPluginPaneState, usePluginPaneState } from "../../runtime";
8
8
  import { EmptyState, PaneStatusBody, QueryBar, usePaneNoticeFooter } from "../../../components";
9
+ import { useCompanyDataFooter } from "../../../components/layout/pane/status-footer";
9
10
  import { coinNewsFallbackNotice } from "../../../sources/gloomberb-cloud/news";
10
11
  import { newsMuteSettingsDef, newsMutesApplyToFeed, useNewsMuteFilter } from "./wire/mutes";
11
12
  import { newsListEmptyCopy } from "./wire/filter-articles";
@@ -112,7 +113,10 @@ function TickerNewsView({ width, height, focused }: { width: number; height: num
112
113
  ? [{ id: "summary", parts: [{ text: "summary loading", tone: "muted" as const }] }]
113
114
  : undefined,
114
115
  onPopOut: readableArticle ? popOutReadable : undefined,
116
+ feedAccess: newsState.access,
115
117
  });
118
+ // A receipt's or foreign line's stories are its company's, read from the home line.
119
+ useCompanyDataFooter(newsState.companyFrom, !detailOpen);
116
120
 
117
121
  if (!ticker) {
118
122
  return (
@@ -29,10 +29,13 @@ const CATEGORY_LABELS: Record<string, string> = {
29
29
  us: "US",
30
30
  };
31
31
 
32
+ function categoryKey(value: string | null | undefined): string {
33
+ return value?.trim().toLowerCase().replace(/[\s-]+/g, "_") ?? "";
34
+ }
35
+
32
36
  export function formatNewsCategory(value: string | null | undefined): string {
33
- const raw = value?.trim();
34
- if (!raw) return "";
35
- const key = raw.toLowerCase().replace(/[\s-]+/g, "_");
37
+ const key = categoryKey(value);
38
+ if (!key) return "";
36
39
  const mapped = CATEGORY_LABELS[key];
37
40
  if (mapped) return mapped;
38
41
  const words = key.split("_").filter(Boolean);
@@ -46,6 +49,19 @@ export function formatNewsCategory(value: string | null | undefined): string {
46
49
  .join(" ");
47
50
  }
48
51
 
52
+ /**
53
+ * Labels for the news tables' category column, which is ten cells wide, for the
54
+ * ids whose label above would be cut in it. Anywhere with room (a story's
55
+ * detail) keeps the full label.
56
+ */
57
+ const COLUMN_CATEGORY_LABELS: Record<string, string> = {
58
+ product_approval: "Approval",
59
+ };
60
+
61
+ export function formatNewsCategoryForColumn(value: string | null | undefined): string {
62
+ return COLUMN_CATEGORY_LABELS[categoryKey(value)] ?? formatNewsCategory(value);
63
+ }
64
+
49
65
  const KNOWN_TICKERS = new Set([
50
66
  "AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA", "JPM", "JNJ", "UNH",
51
67
  "PG", "XOM", "CVX", "HD", "BAC", "V", "MA", "PFE", "KO", "PEP", "ABBV", "MRK",
@@ -125,6 +125,7 @@ function IndustryNewsBody({ focused, width, height, entry, code, codeFilter }: P
125
125
  defaultSort: DEFAULT_SORT,
126
126
  refreshing: loaded.loading && loaded.articles.length > 0,
127
127
  error: loaded.error,
128
+ feedAccess: loaded.query ? loaded.newsState.access : null,
128
129
  });
129
130
  const { setSelectedArticleId } = stack;
130
131
 
@@ -1,5 +1,7 @@
1
1
  import { useMemo } from "react";
2
2
  import type { PaneFooterSegment } from "../../../../../components";
3
+ import { delayedFooterSegment } from "../../../../../components/layout/pane/footer/model";
4
+ import type { NewsAccess } from "../../../../../news/types";
3
5
  import { t, tf } from "../../../../../i18n";
4
6
  import { useAppLanguage } from "../../../../../i18n/react";
5
7
  import { useCloudAccessFooter } from "../../../shared/cloud-upgrade";
@@ -22,6 +24,18 @@ interface UseNewsArticleFooterOptions {
22
24
  loading?: boolean;
23
25
  error?: string | null;
24
26
  onPopOut?: () => void;
27
+ /** Which news the list on screen is, from the service's answer; null or absent when it did not say. */
28
+ feedAccess?: NewsAccess | null;
29
+ }
30
+
31
+ /**
32
+ * Whether the stories are the live wire: what the service's answer said when
33
+ * it said; otherwise held back for an account without real-time news, and
34
+ * not said at all for one with it. The plan never claims the live wire.
35
+ */
36
+ function newsFeedRealtime(feedAccess: NewsAccess | null | undefined, hasProAccess: boolean): boolean | null {
37
+ if (feedAccess) return feedAccess === "realtime";
38
+ return hasProAccess ? null : false;
25
39
  }
26
40
 
27
41
  export function useNewsArticleFooter({
@@ -32,32 +46,44 @@ export function useNewsArticleFooter({
32
46
  loading = false,
33
47
  error,
34
48
  onPopOut,
49
+ feedAccess = null,
35
50
  }: UseNewsArticleFooterOptions) {
36
51
  const language = useAppLanguage();
52
+ const delayLabel = tf("{count}h", { count: CLOUD_NEWS_DELAY_HOURS });
37
53
  const { access, hint: upgradeHint, segment } = useCloudAccessFooter({
38
- delayLabel: tf("{count}h", { count: CLOUD_NEWS_DELAY_HOURS }),
54
+ delayLabel,
39
55
  focused,
56
+ // A feed the service answered live has nothing to upgrade.
57
+ degraded: feedAccess !== "realtime",
40
58
  segmentId: "news-access",
41
59
  placement: "news-footer",
42
60
  shortcutScope: `${registrationId}:news-upgrade`,
43
61
  });
62
+ const realtime = newsFeedRealtime(feedAccess, access.hasProAccess);
44
63
 
45
64
  // A report or capture of the pane says how far its stories are held back, as the footer does, and dates the
46
65
  // list by its newest story, named so it is not read as the time now.
47
66
  useRemoteUiNode({
48
67
  role: "report-freshness",
49
68
  label: "News freshness",
50
- getMetadata: () => (access.hasProAccess
69
+ getMetadata: () => (realtime === true
51
70
  ? { status: "live", asOfLabel: NEWEST_STORY }
52
- : { status: "delayed", delayMinutes: CLOUD_NEWS_DELAY_HOURS * 60, asOfLabel: NEWEST_STORY }),
71
+ : realtime === false
72
+ ? { status: "delayed", delayMinutes: CLOUD_NEWS_DELAY_HOURS * 60, asOfLabel: NEWEST_STORY }
73
+ : { status: "not-a-feed", basis: "published stories", asOfLabel: NEWEST_STORY }),
53
74
  });
54
75
 
55
76
  const accessInfo = useMemo<PaneFooterSegment[]>(() => {
56
- if (access.isPayingPro) {
77
+ // A trial keeps its days-left segment, which manages the plan.
78
+ if (realtime === true && !access.isTrialActive) {
57
79
  return [{ id: "news-access", parts: [{ text: t("real-time news"), tone: "positive" }] }];
58
80
  }
81
+ // Held back although the account has Pro (a read that went out before its session): say so, without a pitch.
82
+ if (feedAccess === "delayed" && access.hasProAccess) {
83
+ return [delayedFooterSegment({ id: "news-access", detail: tf("{delay} delayed", { delay: delayLabel }), delay: delayLabel })];
84
+ }
59
85
  return segment ? [segment] : [];
60
- }, [access.isPayingPro, language, segment]);
86
+ }, [access.hasProAccess, access.isTrialActive, delayLabel, feedAccess, language, realtime, segment]);
61
87
  const footerInfo = useMemo(() => [...accessInfo, ...(info ?? [])], [accessInfo, info]);
62
88
 
63
89
  // The story's header names its source, so the footer keeps to the link.
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useMemo } from "react";
2
2
  import { Box } from "../../../../../ui";
3
- import type { NewsArticle, NewsQuery } from "../../../../../news/types";
3
+ import type { NewsAccess, NewsArticle, NewsQuery } from "../../../../../news/types";
4
4
  import { useLoadNewsStory, useNewsArticles, useNewsTableLoadMore } from "../../../../../news/hooks";
5
5
  import type { PaneProps } from "../../../../../types/plugin";
6
6
  import { useDebouncedPluginPaneState, usePluginConfigState, usePluginPaneState } from "../../../../runtime";
@@ -31,6 +31,8 @@ interface NewsArticleStackOptions {
31
31
  /** Rows stay on screen while a refresh runs, so the footer says it is loading. */
32
32
  refreshing: boolean;
33
33
  error: string | null;
34
+ /** Which news the rows are, from the service's answer, for the footer's real-time or delayed label. */
35
+ feedAccess?: NewsAccess | null;
34
36
  }
35
37
 
36
38
  /**
@@ -46,6 +48,7 @@ export function useNewsArticleStack({
46
48
  defaultSort,
47
49
  refreshing,
48
50
  error,
51
+ feedAccess,
49
52
  }: NewsArticleStackOptions) {
50
53
  const [selectedArticleId, setSelectedArticleId] = useDebouncedPluginPaneState<string | null>(
51
54
  `${paneKey}:selectedArticleId`,
@@ -95,6 +98,7 @@ export function useNewsArticleStack({
95
98
  loading: detailLoading || refreshing,
96
99
  error: [error, detailError].filter(Boolean).join(" ") || null,
97
100
  onPopOut: readableArticle ? popOutReadable : undefined,
101
+ feedAccess,
98
102
  });
99
103
 
100
104
  return {
@@ -172,6 +176,7 @@ export function NewsPresetPane({
172
176
  defaultSort,
173
177
  refreshing: loading && persisted.length > 0,
174
178
  error,
179
+ feedAccess: newsState.access,
175
180
  });
176
181
  const emptyCopy = newsListEmptyCopy({
177
182
  query: searchQuery,
@@ -17,7 +17,7 @@ import { useLoadNewsStory } from "../../../../../news/hooks";
17
17
  import { formatFeedTime } from "../../../../../utils/datetime-format";
18
18
  import { logFailure } from "../../../../../utils/log-failure";
19
19
  import { nextHeaderSort } from "../../../../../utils/sort-values";
20
- import { formatNewsCategory } from "../categories";
20
+ import { formatNewsCategoryForColumn } from "../categories";
21
21
  import { useOpenTickerChoice } from "../../../shared/ticker-choice";
22
22
  import { usePinnedMoment } from "../../../../../components/use-pinned-moment";
23
23
 
@@ -359,7 +359,7 @@ export function NewsArticleStackView({
359
359
  }
360
360
  case "categories":
361
361
  return {
362
- text: formatNewsCategory(item.categories[0]) || "-",
362
+ text: formatNewsCategoryForColumn(item.categories[0]) || "-",
363
363
  color: tone(colors.textDim),
364
364
  };
365
365
  case "sentiment": {
@@ -68,6 +68,17 @@ export function parseOpenFdaDate(value: unknown): Date | null {
68
68
 
69
69
  const normalizeWords = (value: string): string => value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
70
70
 
71
+ /** Each term once, in the order first coded, whatever its case: `Nausea` and `NAUSEA` are one. */
72
+ function uniqueTerms(terms: readonly string[]): string[] {
73
+ const seen = new Set<string>();
74
+ return terms.filter((term) => {
75
+ const key = normalizeWords(term);
76
+ if (seen.has(key)) return false;
77
+ seen.add(key);
78
+ return true;
79
+ });
80
+ }
81
+
71
82
  /** The search's words appear together in one of the fields, as openFDA matches a phrase. */
72
83
  function fieldsMatch(fields: Array<string | undefined>, query: string): boolean {
73
84
  const words = normalizeWords(query);
@@ -230,9 +241,10 @@ export function parseDrugReport(raw: unknown, query = "", role: DrugRole | null
230
241
  product: shown?.name ?? "Unnamed drug",
231
242
  role: shown?.role ?? null,
232
243
  drugs: drugs.map(({ name, role }) => ({ name, role })),
233
- reactions: (Array.isArray(patient.reaction) ? patient.reaction : [])
244
+ // A report can code one reaction more than once (once per drug, or in another case): each is listed once.
245
+ reactions: uniqueTerms((Array.isArray(patient.reaction) ? patient.reaction : [])
234
246
  .map((entry) => asString(asRecord(entry).reactionmeddrapt))
235
- .filter((entry): entry is string => !!entry),
247
+ .filter((entry): entry is string => !!entry)),
236
248
  outcome,
237
249
  outcomes,
238
250
  manufacturer: shown && shown.manufacturers.length === 1 ? shown.manufacturers[0]! : null,
@@ -3,8 +3,9 @@ import { OpenFdaPane } from "./pane";
3
3
  import { OPENFDA_PANE_ID, OPENFDA_PLUGIN_ID } from "./types";
4
4
 
5
5
  const createOpenFdaPaneInstance = (options?: PaneTemplateCreateOptions) => {
6
- // A search of product and firm names, never a ticker: FDA records name the
7
- // firm as it filed, not a symbol, so a ticker would match unrelated text.
6
+ // A search of product and firm names: FDA records name the firm as it filed,
7
+ // not a symbol. A ticker that matches nothing is searched as its company's
8
+ // name, which the pane looks up (`FDA AZN` is AstraZeneca's reports).
8
9
  const query = (options?.arg ?? options?.values?.query ?? "").trim();
9
10
  const encoded = encodeURIComponent(query).replace(/%/g, "~");
10
11
  return {
@@ -65,6 +65,15 @@ const TONES: Partial<Record<ColumnId, Record<string, StateTone>>> = {
65
65
  status: { Ongoing: "warning", Completed: "text", Terminated: "muted" },
66
66
  };
67
67
 
68
+ /** The most terms a row's list cell names before counting the rest; the record names them all. */
69
+ const MAX_CELL_TERMS = 3;
70
+
71
+ /** A report's coded terms as a row shows them: the first few, then how many more (`Nausea, Rash, Fatigue +9 more`). */
72
+ export function termsCellText(terms: readonly string[]): string {
73
+ const more = terms.length - MAX_CELL_TERMS;
74
+ return `${terms.slice(0, MAX_CELL_TERMS).join(", ")}${more > 0 ? ` +${more} more` : ""}`;
75
+ }
76
+
68
77
  export function recordTone(column: ColumnId, text: string): StateTone {
69
78
  return TONES[column]?.[text] ?? "text";
70
79
  }