@gloom-sh/terminal 0.18.5 → 0.19.0

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 (436) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +140 -7
  3. package/src/api-client/cot.ts +6 -2
  4. package/src/api-client/credit-documents.ts +6 -0
  5. package/src/api-client/data.ts +17 -6
  6. package/src/api-client/equity-screener.ts +24 -0
  7. package/src/api-client/fund-facts.ts +45 -4
  8. package/src/api-client/index.ts +11 -0
  9. package/src/api-client/market-discovery.ts +10 -0
  10. package/src/api-client/market-movers.ts +7 -0
  11. package/src/api-client/members.ts +1 -1
  12. package/src/api-client/perps.ts +43 -0
  13. package/src/api-client/research-activity.ts +3 -2
  14. package/src/api-client/types.ts +70 -2
  15. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  16. package/src/app/runtime/broker-import.ts +0 -2
  17. package/src/app/runtime/plugin-bindings.ts +73 -56
  18. package/src/architecture/pane-tabs-allowlist.json +3 -0
  19. package/src/architecture/ttl-caches-allowlist.json +19 -0
  20. package/src/brokers/sync-broker-instance.ts +2 -8
  21. package/src/capabilities/chart-series.ts +1 -1
  22. package/src/cli/commands/crypto-hints.ts +40 -24
  23. package/src/cli/commands/market.ts +72 -26
  24. package/src/cli/commands/options-chain.ts +48 -7
  25. package/src/cli/commands/options-leaps.ts +9 -3
  26. package/src/cli/commands/overview.ts +61 -12
  27. package/src/cli/commands/search.ts +21 -1
  28. package/src/cli/commands/themes.ts +11 -2
  29. package/src/cli/commands/ticker.ts +55 -34
  30. package/src/cli/company-name-hint.ts +41 -0
  31. package/src/cli/earnings-calendar.ts +1 -1
  32. package/src/cli/errors.ts +8 -0
  33. package/src/cli/glossary.ts +25 -2
  34. package/src/cli/history-rows.ts +57 -9
  35. package/src/cli/index.ts +2 -0
  36. package/src/cli/listing-arg.ts +180 -52
  37. package/src/cli/output-file.ts +68 -0
  38. package/src/cli/pane-functions/capabilities.ts +3 -0
  39. package/src/cli/pane-functions/catalog.ts +80 -7
  40. package/src/cli/pane-functions/dom.ts +13 -3
  41. package/src/cli/pane-functions/freshness.ts +86 -3
  42. package/src/cli/pane-functions/function-help.ts +13 -10
  43. package/src/cli/pane-functions/headless.ts +17 -7
  44. package/src/cli/pane-functions/index.ts +49 -14
  45. package/src/cli/pane-functions/market-session.ts +66 -4
  46. package/src/cli/pane-functions/options.ts +19 -1
  47. package/src/cli/pane-functions/resolver.ts +19 -8
  48. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  49. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  50. package/src/cli/quote-answer.ts +22 -11
  51. package/src/cli/ticker-resolution.ts +3 -2
  52. package/src/components/chart/composite/composite-chart.tsx +4 -0
  53. package/src/components/chart/composite/interactions.ts +6 -0
  54. package/src/components/chart/composite/legend.tsx +45 -7
  55. package/src/components/chart/composite/panel-gestures.ts +33 -0
  56. package/src/components/chart/composite/panel-surface.tsx +52 -3
  57. package/src/components/chart/composite/rasterizer.ts +24 -13
  58. package/src/components/chart/composite/scene.ts +39 -4
  59. package/src/components/chart/composite/series-cues.ts +14 -0
  60. package/src/components/chart/composite/text-renderer.ts +9 -2
  61. package/src/components/chart/composite/tool-catalog.ts +36 -0
  62. package/src/components/chart/composite/tools.ts +203 -44
  63. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  64. package/src/components/command-bar/assist/inventory.ts +2 -1
  65. package/src/components/command-bar/commands/direct/index.ts +28 -13
  66. package/src/components/command-bar/pane-templates/items.ts +3 -2
  67. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  68. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  69. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  70. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  71. package/src/components/onboarding/wizard-positions.ts +3 -3
  72. package/src/components/ticker/list-table-view.tsx +4 -1
  73. package/src/components/ui/choice-dialog.tsx +23 -18
  74. package/src/components/ui/choice-groups.ts +18 -0
  75. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  76. package/src/components/ui/data-table/opentui/model.ts +26 -1
  77. package/src/components/ui/data-table/types.ts +2 -0
  78. package/src/components/ui/list-view.tsx +8 -4
  79. package/src/components/ui/query-bar-keys.tsx +2 -0
  80. package/src/components/ui/query-bar.tsx +1 -0
  81. package/src/components/ui/select-button.tsx +5 -1
  82. package/src/components/ui/select-field.tsx +1 -0
  83. package/src/components/ui/table-layout.ts +8 -5
  84. package/src/core/app-runtime.ts +2 -0
  85. package/src/core/app-services.ts +2 -2
  86. package/src/data/config/chart-settings.ts +1 -1
  87. package/src/data/json-ticker-repository.ts +17 -8
  88. package/src/data/ticker-repository.ts +16 -9
  89. package/src/data/ticker-writes.ts +81 -0
  90. package/src/data/ttl-cache.ts +165 -0
  91. package/src/docs/snapshot.json +71 -137
  92. package/src/i18n/es.ts +18 -2
  93. package/src/i18n/ja.ts +18 -2
  94. package/src/i18n/ko.ts +18 -2
  95. package/src/i18n/zh-cn.ts +18 -2
  96. package/src/i18n/zh-tw.ts +18 -2
  97. package/src/market-data/depositary-pair.ts +2 -2
  98. package/src/market-data/fund-facts.ts +50 -0
  99. package/src/market-data/market/change-basis.ts +16 -3
  100. package/src/market-data/market/format.ts +25 -0
  101. package/src/market-data/market/freshness.ts +2 -1
  102. package/src/market-data/options-alternatives.ts +81 -4
  103. package/src/news/aggregator.ts +113 -9
  104. package/src/news/cloud-session.ts +21 -0
  105. package/src/news/hooks.ts +1 -0
  106. package/src/news/news-model.ts +1 -0
  107. package/src/news/types.ts +11 -0
  108. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  109. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  110. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  111. package/src/plugins/builtin/alerts/cli.ts +336 -77
  112. package/src/plugins/builtin/alerts/command.ts +25 -1
  113. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  114. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  115. package/src/plugins/builtin/alerts/format.ts +75 -5
  116. package/src/plugins/builtin/alerts/headless.ts +24 -6
  117. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  118. package/src/plugins/builtin/alerts/history.ts +1 -1
  119. package/src/plugins/builtin/alerts/index.tsx +79 -16
  120. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  121. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  122. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  123. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  124. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  125. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  126. package/src/plugins/builtin/alerts/storage.ts +12 -8
  127. package/src/plugins/builtin/alerts/types.ts +4 -0
  128. package/src/plugins/builtin/analytics/index.tsx +2 -1
  129. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  130. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  131. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  132. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  134. package/src/plugins/builtin/attention/index.tsx +2 -1
  135. package/src/plugins/builtin/attention/listing.ts +14 -0
  136. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  137. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  138. package/src/plugins/builtin/buildout/format.ts +38 -0
  139. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  140. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  141. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  142. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  143. package/src/plugins/builtin/changelog/index.tsx +9 -6
  144. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  145. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  146. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  147. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  148. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  149. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  150. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  151. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  152. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  153. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  154. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  155. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  156. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  157. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  158. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  159. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  160. package/src/plugins/builtin/company-attention/client.ts +8 -9
  161. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  162. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  163. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  164. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  165. package/src/plugins/builtin/correlation/headless.ts +13 -5
  166. package/src/plugins/builtin/correlation/history.ts +22 -0
  167. package/src/plugins/builtin/cot/client.ts +22 -18
  168. package/src/plugins/builtin/cot/headless.ts +15 -10
  169. package/src/plugins/builtin/cot/model.ts +14 -1
  170. package/src/plugins/builtin/cot/pane.tsx +22 -14
  171. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  172. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  173. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  174. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  175. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  176. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  177. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  178. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  179. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  180. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  181. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  182. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  183. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  184. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  185. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  186. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  187. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  188. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  189. package/src/plugins/builtin/docs/targets.ts +5 -1
  190. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  191. package/src/plugins/builtin/earnings/board.tsx +62 -71
  192. package/src/plugins/builtin/earnings/headless.ts +40 -4
  193. package/src/plugins/builtin/earnings/scope.ts +31 -43
  194. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  195. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  196. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  197. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  198. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  199. package/src/plugins/builtin/econ/types.ts +2 -0
  200. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  201. package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
  202. package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
  203. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  204. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  205. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  206. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  207. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  208. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  209. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  210. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  211. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  212. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  213. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  214. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  215. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  216. package/src/plugins/builtin/gpu/headless.ts +8 -2
  217. package/src/plugins/builtin/help/function-index.ts +4 -1
  218. package/src/plugins/builtin/help/index.tsx +1 -0
  219. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  220. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  221. package/src/plugins/builtin/holders/headless.ts +18 -1
  222. package/src/plugins/builtin/iv-history/client.ts +52 -11
  223. package/src/plugins/builtin/iv-history/format.ts +36 -23
  224. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  225. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  226. package/src/plugins/builtin/iv-history/model.ts +163 -27
  227. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  228. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  229. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  230. package/src/plugins/builtin/jobs/client.ts +25 -29
  231. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  232. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  233. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  234. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  235. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  236. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  237. package/src/plugins/builtin/market-movers/client.ts +25 -7
  238. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  239. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  240. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  241. package/src/plugins/builtin/market-movers/model.ts +4 -1
  242. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  243. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  244. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  245. package/src/plugins/builtin/market-movers/table.ts +17 -5
  246. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  247. package/src/plugins/builtin/news/index.tsx +1 -0
  248. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  249. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  250. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  251. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  252. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  253. package/src/plugins/builtin/openfda/client.ts +14 -2
  254. package/src/plugins/builtin/openfda/index.tsx +3 -2
  255. package/src/plugins/builtin/openfda/model.ts +9 -0
  256. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  257. package/src/plugins/builtin/options/analytics.ts +29 -5
  258. package/src/plugins/builtin/options/enrichment.ts +17 -5
  259. package/src/plugins/builtin/options/table.ts +5 -2
  260. package/src/plugins/builtin/options/types.ts +2 -0
  261. package/src/plugins/builtin/options/view.tsx +17 -4
  262. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  263. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  264. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  265. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  266. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  267. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  268. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  269. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  270. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  271. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  272. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  273. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  274. package/src/plugins/builtin/pe-band/model.ts +12 -49
  275. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  276. package/src/plugins/builtin/perps/cascade.ts +236 -0
  277. package/src/plugins/builtin/perps/client.ts +56 -2
  278. package/src/plugins/builtin/perps/headless.ts +65 -9
  279. package/src/plugins/builtin/perps/index.tsx +7 -5
  280. package/src/plugins/builtin/perps/model.ts +18 -4
  281. package/src/plugins/builtin/perps/pane.tsx +19 -22
  282. package/src/plugins/builtin/perps/status-line.ts +25 -0
  283. package/src/plugins/builtin/plugin-module.ts +5 -1
  284. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  285. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  286. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  287. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  288. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  289. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  290. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  291. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  292. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  293. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  294. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  295. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  296. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  297. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  298. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  299. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  300. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  301. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  302. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  303. package/src/plugins/builtin/research/event-model.ts +10 -8
  304. package/src/plugins/builtin/research/headless.ts +1 -1
  305. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  306. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  307. package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
  308. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  309. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  310. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  311. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  312. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  313. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  314. package/src/plugins/builtin/scanner/headless.ts +204 -35
  315. package/src/plugins/builtin/scanner/index.ts +16 -5
  316. package/src/plugins/builtin/scanner/live-headless.ts +27 -3
  317. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  318. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  319. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  320. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  321. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  322. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  323. package/src/plugins/builtin/sec/headless.ts +2 -1
  324. package/src/plugins/builtin/series-inventory.ts +2 -0
  325. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  326. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  327. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  328. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  329. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  330. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  331. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  332. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  333. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  334. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  335. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  336. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  337. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  338. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  339. package/src/plugins/builtin/themes/model.ts +16 -7
  340. package/src/plugins/builtin/themes/pane.tsx +5 -5
  341. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  342. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  343. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  344. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  345. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  346. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  347. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  348. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  349. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  350. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  351. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  352. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  353. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  354. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  355. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  357. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  358. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  359. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  360. package/src/plugins/builtin/volatility/index.tsx +2 -2
  361. package/src/plugins/builtin/volatility/pane.tsx +2 -2
  362. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  363. package/src/plugins/catalog-manifest.ts +2 -1
  364. package/src/plugins/marketplace/model.ts +18 -5
  365. package/src/portfolio/futures-positions.ts +6 -3
  366. package/src/portfolio/position-quantity.ts +15 -9
  367. package/src/remote/resources.ts +2 -1
  368. package/src/renderers/dom/data-table/row.tsx +26 -0
  369. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  370. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  371. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  372. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  373. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  374. package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
  375. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  376. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  377. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  378. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  379. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  380. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  381. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  382. package/src/renderers/electrobun/view/main.tsx +24 -1
  383. package/src/renderers/electrobun/view/news-source.ts +3 -1
  384. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  385. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  386. package/src/renderers/opentui/start.tsx +5 -0
  387. package/src/series/registry.ts +1 -0
  388. package/src/series/types.ts +6 -0
  389. package/src/series/validate.ts +1 -0
  390. package/src/sources/gloomberb-cloud/index.ts +1 -0
  391. package/src/sources/gloomberb-cloud/news.ts +47 -7
  392. package/src/sources/provider-router/financials.ts +15 -3
  393. package/src/sources/provider-router/index.ts +8 -5
  394. package/src/state/ticker-cleanup.ts +50 -0
  395. package/src/sync/controller.ts +4 -3
  396. package/src/sync/core-contributors.ts +90 -31
  397. package/src/telemetry/stall-reports-dom.ts +38 -0
  398. package/src/telemetry/stall-reports-node.ts +75 -0
  399. package/src/telemetry/stall-reports.ts +214 -0
  400. package/src/test-support/preload.ts +14 -0
  401. package/src/theme/themes.ts +10 -0
  402. package/src/tickers/company-name.ts +44 -3
  403. package/src/tickers/listing-venues.ts +15 -3
  404. package/src/tickers/terminal-codes.ts +19 -0
  405. package/src/time-series/alignment.ts +1 -1
  406. package/src/time-series/economic.ts +18 -4
  407. package/src/time-series/field-catalog.ts +1 -1
  408. package/src/time-series/filed-trailing-eps.ts +197 -0
  409. package/src/time-series/fundamentals.ts +44 -1
  410. package/src/time-series/ice-credit.ts +3 -0
  411. package/src/time-series/price-comparison.ts +30 -2
  412. package/src/time-series/resolve-context.ts +3 -1
  413. package/src/time-series/resolve.ts +56 -4
  414. package/src/time-series/session-bar.ts +11 -0
  415. package/src/time-series/spec.ts +3 -2
  416. package/src/time-series/studies.ts +8 -2
  417. package/src/time-series/total-return.ts +160 -0
  418. package/src/time-series/transforms.ts +2 -3
  419. package/src/time-series/types.ts +22 -1
  420. package/src/types/financials.ts +32 -1
  421. package/src/types/headless.ts +21 -3
  422. package/src/types/news-source.ts +1 -0
  423. package/src/types/plugin.ts +9 -0
  424. package/src/ui/host.tsx +3 -0
  425. package/src/updater/failure.ts +4 -1
  426. package/src/utils/cli-output.ts +42 -1
  427. package/src/utils/crypto-pair.ts +44 -0
  428. package/src/utils/forward-dividend.ts +2 -0
  429. package/src/utils/fundamentals.ts +24 -3
  430. package/src/utils/fx-market-hours.ts +26 -3
  431. package/src/utils/main-thread-stall.ts +35 -2
  432. package/src/utils/market-capitalization.ts +24 -1
  433. package/src/utils/price-history.ts +7 -1
  434. package/src/utils/shell-word.ts +8 -0
  435. package/src/utils/trailing-dividend.ts +53 -4
  436. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -0,0 +1,74 @@
1
+ import type { PaneFooterSegment } from "../../../components";
2
+ import type { QueryBarSelectFilter } from "../../../components/ui/query-bar";
3
+ import { formatCompact } from "../../../utils/format";
4
+
5
+ /** No minimum: every row, with or without a float. */
6
+ export const ANY_FLOAT = "any";
7
+ /** The minimums the pane's menu offers; `--min-float` takes any other. */
8
+ const PRESETS = ["1M", "5M", "10M", "20M", "50M", "100M"] as const;
9
+ const SCALE: Record<string, number> = { k: 1e3, m: 1e6, b: 1e9 };
10
+
11
+ /** Shares in `text` (10M, 500k, 2.5M, 25000000), or null when it is not a share count. */
12
+ function parseShares(text: string): number | null {
13
+ const match = /^((?:\d+(?:\.\d*)?|\.\d+))([kmb])?$/i.exec(text.trim().replace(/[_,]/g, ""));
14
+ if (!match) return null;
15
+ const value = Number(match[1]) * (match[2] ? SCALE[match[2].toLowerCase()]! : 1);
16
+ return Number.isFinite(value) ? value : null;
17
+ }
18
+
19
+ /** The minimum a saved or typed value sets, in shares; null for none (`any`, 0 or a value that is not a count). */
20
+ export function minFloatShares(value: string | null | undefined): number | null {
21
+ if (!value || value.trim().toLowerCase() === ANY_FLOAT) return null;
22
+ const shares = parseShares(value);
23
+ return shares != null && shares > 0 ? shares : null;
24
+ }
25
+
26
+ /** `--min-float` as the pane keeps it: `10M` for 10m or 10000000, `any` for none. Throws on a value that is not a share count. */
27
+ export function normalizeMinFloat(value: string): string {
28
+ if (value.trim().toLowerCase() === ANY_FLOAT) return ANY_FLOAT;
29
+ const shares = parseShares(value);
30
+ if (shares == null) throw new Error(`--min-float takes a share count such as 10M or 500k, or any; got "${value}".`);
31
+ return shares > 0 ? formatCompact(shares) : ANY_FLOAT;
32
+ }
33
+
34
+ /** A row passes with a float at or above the minimum; a row whose float is not known does not. */
35
+ export function meetsMinFloat(floatShares: number | null | undefined, minimum: number | null): boolean {
36
+ return minimum == null || (floatShares != null && floatShares >= minimum);
37
+ }
38
+
39
+ /** The pane's Min float menu: Any and the presets, with a minimum set from the CLI kept as its own choice. */
40
+ export function minFloatFilter(value: string, onChange: (value: string) => void): QueryBarSelectFilter {
41
+ const current = minFloatShares(value) == null ? ANY_FLOAT : value;
42
+ const presets: string[] = [...PRESETS];
43
+ const options = [ANY_FLOAT, ...(presets.includes(current) || current === ANY_FLOAT ? [] : [current]), ...presets]
44
+ .map((option) => ({ value: option, label: option === ANY_FLOAT ? "Any" : option }));
45
+ return {
46
+ id: "min-float",
47
+ label: "Min float",
48
+ title: "Minimum float",
49
+ value: current,
50
+ defaultValue: ANY_FLOAT,
51
+ options,
52
+ onChange,
53
+ };
54
+ }
55
+
56
+ /** What a list says under its Float column while a shown float is partial. */
57
+ export const PARTIAL_FLOAT_NOTE = "Float is partial: the source does not say when it measured it.";
58
+
59
+ /** Whether any row's float is partial, which the list then says once. */
60
+ export function hasPartialFloat(rows: readonly { floatShares?: number | null; floatSharesState?: string }[]): boolean {
61
+ return rows.some((row) => row.floatShares != null && row.floatSharesState === "partial");
62
+ }
63
+
64
+ /** The footer's word for a FLOAT column of partial floats, or none. */
65
+ export function partialFloatFooter(shown: boolean, rows: readonly { floatShares?: number | null; floatSharesState?: string }[]): PaneFooterSegment[] {
66
+ return shown && hasPartialFloat(rows)
67
+ ? [{ id: "float-partial", parts: [{ text: "FLOAT", tone: "label" }, { text: "partial, undated", tone: "muted" }] }]
68
+ : [];
69
+ }
70
+
71
+ /** The empty list's title while a minimum hides every row. */
72
+ export function minFloatEmptyTitle(value: string): string {
73
+ return `No movers with a float of ${value} or more.`;
74
+ }
@@ -50,7 +50,8 @@ type MarketMoverColumnId =
50
50
  | "volume"
51
51
  | "volumeRatio"
52
52
  | "range"
53
- | "marketCap";
53
+ | "marketCap"
54
+ | "floatShares";
54
55
  export type MarketMoverColumn = DataTableColumn & { id: MarketMoverColumnId };
55
56
  /**
56
57
  * A list row. Price and change are the regular session's, as `ticker` reads
@@ -123,6 +124,8 @@ function getSortValue(
123
124
  return fiftyTwoWeekPositionPercent(row.price, row.fiftyTwoWeekLow, row.fiftyTwoWeekHigh);
124
125
  case "marketCap":
125
126
  return row.marketCap ?? null;
127
+ case "floatShares":
128
+ return row.floatShares ?? null;
126
129
  }
127
130
  }
128
131
 
@@ -220,6 +220,17 @@ function parseScreenerSessions(row: Record<string, unknown>): CloudMoverSessionF
220
220
  return sessions;
221
221
  }
222
222
 
223
+ /** A positive float with its state, or nothing: a row without one fails any minimum. */
224
+ function floatOf(source: { floatShares?: unknown; floatSharesState?: unknown; floatSharesReason?: unknown } | null | undefined): Pick<ScreenerQuote, "floatShares" | "floatSharesState" | "floatSharesReason"> {
225
+ const shares = screenerNumber(source?.floatShares);
226
+ if (shares == null || !(shares > 0)) return {};
227
+ return {
228
+ floatShares: shares,
229
+ ...(source?.floatSharesState === "partial" ? { floatSharesState: "partial" as const } : {}),
230
+ ...(source?.floatSharesState === "partial" && typeof source.floatSharesReason === "string" ? { floatSharesReason: source.floatSharesReason } : {}),
231
+ };
232
+ }
233
+
223
234
  /** Validate numeric fields at the API boundary so absent values stay unavailable. */
224
235
  export function parseScreenerResponse(data: unknown): ScreenerQuote[] {
225
236
  if (!data || typeof data !== "object" || !("quotes" in data) || !Array.isArray(data.quotes)) {
@@ -239,6 +250,7 @@ export function parseScreenerResponse(data: unknown): ScreenerQuote[] {
239
250
  previousClose: screenerNumber(q.previousClose) ?? undefined, exchange: typeof q.exchange === "string" ? q.exchange : "",
240
251
  lastUpdated: typeof q.lastUpdated === "number" && Number.isFinite(q.lastUpdated) && q.lastUpdated > 0 ? q.lastUpdated : undefined,
241
252
  sessions: parseScreenerSessions(q),
253
+ ...floatOf(q),
242
254
  }];
243
255
  });
244
256
  }
@@ -325,6 +337,8 @@ function mergeCloudScreenerItem(
325
337
  lastUpdated: item.lastUpdated,
326
338
  previousClose: screenerNumber(item.previousClose) ?? undefined,
327
339
  sessions: parseScreenerSessions(item as unknown as Record<string, unknown>),
340
+ // The live list's own float, the delayed list's when it sends none, each with its own state.
341
+ ...floatOf(item.floatShares != null ? item : metadata),
328
342
  };
329
343
  }
330
344
 
@@ -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,
@@ -112,6 +112,7 @@ function TickerNewsView({ width, height, focused }: { width: number; height: num
112
112
  ? [{ id: "summary", parts: [{ text: "summary loading", tone: "muted" as const }] }]
113
113
  : undefined,
114
114
  onPopOut: readableArticle ? popOutReadable : undefined,
115
+ feedAccess: newsState.access,
115
116
  });
116
117
 
117
118
  if (!ticker) {
@@ -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
  }