@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
@@ -1,6 +1,8 @@
1
1
  import { unavailableText } from "../../../components/ui/status";
2
2
  import { atRegularSessionPrice } from "../../../market-data/quotes/regular-session-valuation";
3
3
  import type { Fundamentals, TickerFinancials } from "../../../types/financials";
4
+ import { fiscalPeriodLabel, fiscalYearEndMonth } from "../../../utils/fiscal-period";
5
+ import { formatCompactCurrency } from "../../../utils/format";
4
6
  import { enterpriseValueCheck, enterpriseValueLabel, enterpriseValueNotMeaningful, enterpriseValueReconciliationNote,
5
7
  enterpriseValueWithdrawn, reportedEnterpriseValue } from "../../../utils/fundamentals";
6
8
 
@@ -50,8 +52,10 @@ export interface ReverseDcfModel {
50
52
  fcfMarginBasis: FcfMarginBasis;
51
53
  /** Year-ten revenue at `fcfMargin`: trailing free cash flow grown at the implied rate for ten years, over the margin. */
52
54
  impliedRevenue: number | null;
53
- /** Annual FCF growth between the latest annual statement and one up to five years earlier, when both are positive. */
54
- pastGrowth: { rate: number; years: number } | null;
55
+ /** Annual FCF growth between the latest annual statement and one up to five years earlier, when both are positive. `period` names the two years: FY2021–FY2025. */
56
+ pastGrowth: { rate: number; years: number; period: string } | null;
57
+ /** The latest annual statement's free cash flow, which the past growth ends at, and the year it is of (FY2025). */
58
+ fiscalYearFcf: { value: number; label: string } | null;
55
59
  /** Implied growth for each discount rate (rows) at each terminal growth (columns). */
56
60
  sensitivity: { discountRate: number; implied: (ImpliedGrowth | null)[] }[];
57
61
  error: string | null;
@@ -82,15 +86,65 @@ export function solveImpliedGrowth(enterpriseValue: number, fcf: number, discoun
82
86
  return { kind: "rate", value: (low + high) / 2 };
83
87
  }
84
88
 
85
- function pastFcfGrowth(financials: TickerFinancials): ReverseDcfModel["pastGrowth"] {
86
- const annual = financials.annualStatements
89
+ /** The latest annual statements with a free cash flow, oldest first, as many as the past growth looks back over. */
90
+ function annualFreeCashFlows(financials: TickerFinancials) {
91
+ return financials.annualStatements
87
92
  .filter((row) => typeof row.freeCashFlow === "number" && Number.isFinite(row.freeCashFlow))
88
93
  .sort((a, b) => a.date.localeCompare(b.date))
89
94
  .slice(-6);
95
+ }
96
+
97
+ /** The two years a past growth spans, as issuers name them: `FY2021–FY2025`; period end dates where a year is off the fiscal calendar. */
98
+ function fiscalSpan(financials: TickerFinancials, from: string, to: string): string {
99
+ const month = fiscalYearEndMonth(financials.annualStatements);
100
+ const [start, end] = [from, to].map((date) => fiscalPeriodLabel(date, "annual", month)) as [string, string];
101
+ return start.startsWith("FY") && end.startsWith("FY") ? `${start}–${end}` : `${from.slice(0, 10)} to ${to.slice(0, 10)}`;
102
+ }
103
+
104
+ function pastFcfGrowth(financials: TickerFinancials): ReverseDcfModel["pastGrowth"] {
105
+ const annual = annualFreeCashFlows(financials);
90
106
  const first = annual[0], last = annual.at(-1);
91
107
  if (!first || !last || first === last || !(first.freeCashFlow! > 0) || !(last.freeCashFlow! > 0)) return null;
92
108
  const years = Math.round((Date.parse(last.date) - Date.parse(first.date)) / (365.25 * 86_400_000));
93
- return years > 0 ? { rate: (last.freeCashFlow! / first.freeCashFlow!) ** (1 / years) - 1, years } : null;
109
+ return years > 0
110
+ ? { rate: (last.freeCashFlow! / first.freeCashFlow!) ** (1 / years) - 1, years, period: fiscalSpan(financials, first.date, last.date) }
111
+ : null;
112
+ }
113
+
114
+ function latestFiscalYearFcf(financials: TickerFinancials): ReverseDcfModel["fiscalYearFcf"] {
115
+ const last = annualFreeCashFlows(financials).at(-1);
116
+ return last ? { value: last.freeCashFlow!, label: fiscalPeriodLabel(last.date, "annual", fiscalYearEndMonth(financials.annualStatements)) } : null;
117
+ }
118
+
119
+ /** How far above the latest fiscal year's free cash flow the trailing twelve months' must be to say the two bases differ. */
120
+ const TTM_BASE_RATIO = 1.5;
121
+
122
+ /**
123
+ * How a trailing free cash flow more than 1.5 times the latest fiscal year's
124
+ * differs from it (`2.7x` FY2025's `5.3B USD`): the growth priced in starts
125
+ * from the trailing figure, the past growth ends at the year's. Null when they
126
+ * are near each other, the year's is not positive, or the model has no growth
127
+ * to say.
128
+ */
129
+ export function ttmBaseGap(model: Pick<ReverseDcfModel, "freeCashFlow" | "fiscalYearFcf" | "currency" | "error">): { multiple: string; year: string; fcf: string } | null {
130
+ const { freeCashFlow, fiscalYearFcf } = model;
131
+ if (model.error || freeCashFlow == null || !fiscalYearFcf || !(fiscalYearFcf.value > 0) || !(freeCashFlow > fiscalYearFcf.value * TTM_BASE_RATIO)) return null;
132
+ return {
133
+ multiple: `${(freeCashFlow / fiscalYearFcf.value).toFixed(1)}x`,
134
+ year: fiscalYearFcf.label,
135
+ fcf: formatCompactCurrency(fiscalYearFcf.value, model.currency ?? undefined),
136
+ };
137
+ }
138
+
139
+ /** "The TTM base differs from FY2025: TTM FCF is 2.7x that year's 5.3B USD.", the one line a report adds when `ttmBaseGap` finds a gap. */
140
+ export function ttmBaseNote(model: Parameters<typeof ttmBaseGap>[0]): string | null {
141
+ const gap = ttmBaseGap(model);
142
+ return gap ? `The TTM base differs from ${gap.year}: TTM FCF is ${gap.multiple} that year's ${gap.fcf}.` : null;
143
+ }
144
+
145
+ /** What the past growth rate is over, after the rate: `a year, FY2021–FY2025`. */
146
+ export function pastGrowthBasis(growth: NonNullable<ReverseDcfModel["pastGrowth"]>): string {
147
+ return `a year, ${growth.period}`;
94
148
  }
95
149
 
96
150
  /**
@@ -145,7 +199,8 @@ export function projectReverseDcf(
145
199
  const empty: ReverseDcfModel = { symbol: options.symbol, currency, discountRate: options.discountRate, terminalGrowth: TERMINAL_GROWTH,
146
200
  enterpriseValue, computedEnterpriseValue: check?.computed ?? null, enterpriseValueNote: check ? enterpriseValueReconciliationNote(fundamentals) ?? null : null,
147
201
  freeCashFlow, fcfYield: null, implied: null, fcfMargin, fcfMarginBasis, impliedRevenue: null,
148
- pastGrowth: financials ? pastFcfGrowth(financials) : null, sensitivity: [], error: null };
202
+ pastGrowth: financials ? pastFcfGrowth(financials) : null, fiscalYearFcf: financials ? latestFiscalYearFcf(financials) : null,
203
+ sensitivity: [], error: null };
149
204
  const valueCurrency = fundamentals?.marketCapCurrency;
150
205
  // ADRs and cross-listings can report cash flows in one currency and trade in another.
151
206
  if (currency && valueCurrency && currency !== valueCurrency) {
@@ -10,8 +10,8 @@ import type { PaneProps } from "../../../types/plugin";
10
10
  import { Box } from "../../../ui";
11
11
  import { formatCompact, formatCompactCurrency } from "../../../utils/format";
12
12
  import { loadReverseDcfInputs } from "./client";
13
- import { describeFcfMargin, DISCOUNT_RATES, FORECAST_YEARS, formatFcfMargin, projectReverseDcf, TERMINAL_GROWTH, TERMINAL_GROWTHS,
14
- type ImpliedGrowth, type ReverseDcfModel } from "./model";
13
+ import { describeFcfMargin, DISCOUNT_RATES, FORECAST_YEARS, formatFcfMargin, pastGrowthBasis, projectReverseDcf, TERMINAL_GROWTH, TERMINAL_GROWTHS,
14
+ ttmBaseGap, type ImpliedGrowth, type ReverseDcfModel } from "./model";
15
15
 
16
16
  export const DISCOUNT_OPTIONS = DISCOUNT_RATES.map((rate) => ({ value: String(Math.round(rate * 100)), label: `${Math.round(rate * 100)}%` }));
17
17
  /** The margin setting that follows the company's own trailing margin, the pane's default. */
@@ -91,15 +91,18 @@ export function ReverseDcfPane({ width, height, focused }: PaneProps) {
91
91
  const currency = model.currency ?? undefined;
92
92
  const implied = model.implied?.kind === "rate" ? model.implied.value : null;
93
93
  const past = model.pastGrowth;
94
+ const gap = ttmBaseGap(model);
94
95
  return [
95
96
  { id: "implied", label: "Priced-in growth", value: impliedText(model.implied),
96
97
  tone: implied == null || !past ? undefined : implied > past.rate ? "negative" : "positive",
97
98
  detail: `a year for ${FORECAST_YEARS}y, then ${pct(TERMINAL_GROWTH, false)}` },
98
99
  { id: "revenue", label: `Revenue, yr ${FORECAST_YEARS}`, value: formatCompactCurrency(model.impliedRevenue ?? undefined, currency),
99
100
  detail: `at ${describeFcfMargin(model.fcfMargin, model.fcfMarginBasis)}` },
100
- { id: "past", label: "Past FCF growth", value: past ? pct(past.rate) : "--", detail: past ? `per year over ${past.years}y` : undefined },
101
+ { id: "past", label: "Past FCF growth", value: past ? pct(past.rate) : "--", detail: past ? pastGrowthBasis(past) : undefined },
101
102
  { id: "yield", label: "FCF yield", value: pct(model.fcfYield, false) },
102
- { id: "fcf", label: "FCF", value: formatCompactCurrency(model.freeCashFlow ?? undefined, currency) },
103
+ // A trailing figure far above the fiscal year the past growth ends at says so beside itself.
104
+ { id: "fcf", label: "FCF", value: formatCompactCurrency(model.freeCashFlow ?? undefined, currency),
105
+ detail: gap ? `TTM base, ${gap.multiple} ${gap.year}'s ${gap.fcf}` : undefined },
103
106
  { id: "ev", label: "EV", value: formatCompactCurrency(model.enterpriseValue ?? undefined, currency),
104
107
  // More than 5% from market cap + debt - cash: the provider's, with the computed figure where it fits.
105
108
  detail: model.computedEnterpriseValue != null ? `provider vs ${formatCompact(model.computedEnterpriseValue)}` : undefined },
@@ -5,6 +5,7 @@ import {
5
5
  } from "../../../api-client";
6
6
  import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
7
7
  import { listingAbroad, type IssuerListingParams } from "../../../api-client/paths";
8
+ import { servedFromSavedCopy } from "../../../data/saved-copies";
8
9
  import type { HeadlessPaneApiClient, PluginPersistence } from "../../../types/plugin";
9
10
  import { secListingKey } from "../../../utils/sec";
10
11
 
@@ -78,13 +79,14 @@ function loadCached<T extends object>(
78
79
  }
79
80
  const fallback = store?.getResource<T>(kind, key, { ...options, allowExpired: true });
80
81
  if (!fallback) throw error;
81
- return {
82
+ // A command that shows it ends with when the copy was saved, not with the failure.
83
+ return servedFromSavedCopy({
82
84
  ...fallback.value,
83
85
  fetchedAt: fallback.fetchedAt,
84
86
  stale: true,
85
87
  refreshError: (error instanceof Error ? error.message : String(error)).trim() || "Request failed",
86
88
  errorStatus: error instanceof ApiRequestError ? error.status : undefined,
87
- };
89
+ }, fallback.fetchedAt);
88
90
  }).finally(() => { if (active.get(key) === request) active.delete(key); });
89
91
  active.set(key, request);
90
92
  return request.promise;
@@ -1,4 +1,5 @@
1
1
  import type { HeadlessBundleSection, HeadlessPaneDefinition } from "../../../types/plugin";
2
+ import { savedCopyMark, unansweredRefreshError } from "../../../data/saved-copies";
2
3
  import { loadRiskReportWithClient, loadRiskReportsWithClient, type RiskReportsResult } from "./data";
3
4
  import { SEC_FILINGS } from "../shared/report-freshness";
4
5
  import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
@@ -39,7 +40,8 @@ export const riskFactorsHeadless: HeadlessPaneDefinition<"bundle"> = {
39
40
  }
40
41
  const report = await loadRiskReportWithClient(context.apiClient, ticker, year, { force, listing });
41
42
  const sections: HeadlessBundleSection[] = [
42
- { title: "Filing", entries: [
43
+ // A report served from a saved copy dates the report by when it was saved (the list does in the metadata).
44
+ { title: "Filing", ...savedCopyMark(report), entries: [
43
45
  { label: "Ticker", value: report.ticker },
44
46
  { label: "Filing year", value: report.reportYear },
45
47
  { label: "Filed", value: report.filedAt },
@@ -62,12 +64,13 @@ export const riskFactorsHeadless: HeadlessPaneDefinition<"bundle"> = {
62
64
  { key: "group", header: "Group" }, { key: "heading", header: "Heading" },
63
65
  { key: "excerpt", header: "Prior source excerpt" }, { key: "note", header: "Analysis" },
64
66
  ], rows: report.diff.removed.map((risk, index) => ({ ...risk, note: report.notes.removed.find((note) => note.index === index)?.text ?? null })) });
65
- const errors = [list?.refreshError, report.refreshError].filter((error): error is string => !!error);
67
+ const errors = [unansweredRefreshError(list), unansweredRefreshError(report)].filter((error): error is string => !!error);
66
68
  return {
67
69
  sections,
68
70
  errors: errors.length ? errors : undefined,
69
71
  ...(list?.stale || report.stale ? { complete: false } : {}),
70
72
  metadata: {
73
+ ...savedCopyMark(list),
71
74
  ticker: report.ticker, requestedYear: selection, reportYear: report.reportYear,
72
75
  filedAt: report.filedAt, updatedAt: report.updatedAt, docUrl: report.docUrl,
73
76
  reportFetchedAt: report.fetchedAt, reportStale: report.stale,
@@ -0,0 +1,28 @@
1
+ import type { PluginModule } from "../plugin-module";
2
+ import { RoadmapPane } from "./pane";
3
+
4
+ export const roadmapModule: PluginModule = {
5
+ panes: [
6
+ {
7
+ id: "roadmap",
8
+ name: "Roadmap",
9
+ icon: "R",
10
+ component: RoadmapPane,
11
+ reportFreshness: { status: "not-a-feed", basis: "curated list" },
12
+ defaultPosition: "right",
13
+ defaultMode: "floating",
14
+ defaultFloatingSize: { width: 100, height: 30 },
15
+ },
16
+ ],
17
+
18
+ paneTemplates: [
19
+ {
20
+ id: "roadmap-pane",
21
+ paneId: "roadmap",
22
+ label: "Roadmap",
23
+ description: "What is being built, what is planned and what shipped. Pro members vote on what comes next.",
24
+ keywords: ["roadmap", "planned", "building", "shipped", "upcoming", "features", "vote", "votes", "request"],
25
+ shortcut: { prefix: "ROADMAP", aliases: ["RMAP"] },
26
+ },
27
+ ],
28
+ };
@@ -0,0 +1,67 @@
1
+ import type { RoadmapItem, RoadmapPayload, RoadmapStatus, RoadmapVote } from "../../../api-client/roadmap";
2
+ import type { TableSection } from "../../../components";
3
+
4
+ export const ROADMAP_STATUS_LABELS: Record<RoadmapStatus, string> = {
5
+ building: "Building",
6
+ planned: "Planned",
7
+ shipped: "Shipped",
8
+ };
9
+
10
+ const SECTION_ORDER: readonly RoadmapStatus[] = ["building", "planned", "shipped"];
11
+
12
+ /** Building, then Planned, then Shipped, each in the order the service sent; an empty group is left out. */
13
+ export function roadmapSections(items: readonly RoadmapItem[]): TableSection<RoadmapItem>[] {
14
+ return SECTION_ORDER.map((status) => ({
15
+ label: ROADMAP_STATUS_LABELS[status],
16
+ key: status,
17
+ items: items.filter((item) => item.status === status),
18
+ }));
19
+ }
20
+
21
+ /** Votes are taken on what is not shipped yet; a shipped item's count is final. */
22
+ export function isVotable(item: RoadmapItem): boolean {
23
+ return item.status !== "shipped";
24
+ }
25
+
26
+ /**
27
+ * The items with this session's own votes on top: the count and mark a vote
28
+ * changed, held until the next read of the list replaces them.
29
+ */
30
+ export function withLocalVotes(
31
+ payload: RoadmapPayload | null,
32
+ local: Readonly<Record<string, RoadmapVote>>,
33
+ ): RoadmapItem[] {
34
+ if (!payload) return [];
35
+ return payload.items.map((item) => {
36
+ const vote = local[item.id];
37
+ return vote ? { ...item, votes: vote.votes, voted: vote.voted } : item;
38
+ });
39
+ }
40
+
41
+ /** What the row shows while a vote is on its way: the mark flipped and the count moved by one. */
42
+ export function optimisticVote(item: RoadmapItem, voted: boolean): RoadmapVote {
43
+ const step = voted === item.voted ? 0 : voted ? 1 : -1;
44
+ return { id: item.id, votes: Math.max(0, item.votes + step), voted };
45
+ }
46
+
47
+ /** The votes cell: the count, with a check before it when this account voted. */
48
+ export function votesText(item: RoadmapItem): string {
49
+ return item.voted ? `✓ ${item.votes}` : String(item.votes);
50
+ }
51
+
52
+ /** What one vote that failed is called on its row, and the reason the detail gives. */
53
+ export interface VoteFailure {
54
+ status: string;
55
+ reason: string;
56
+ }
57
+
58
+ export function voteFailure(error: unknown, voted: boolean): VoteFailure {
59
+ const status = typeof error === "object" && error && "status" in error ? (error as { status?: unknown }).status : undefined;
60
+ const action = voted ? "The vote" : "Taking the vote back";
61
+ if (status === 401) return { status: "sign in", reason: `${action} did not go through: sign in again to vote.` };
62
+ if (status === 402 || status === 403) return { status: "Pro only", reason: `${action} did not go through: voting is for verified Pro members.` };
63
+ if (status === 404) return { status: "removed", reason: "This item is no longer on the roadmap." };
64
+ if (status === 409) return { status: "closed", reason: "This item shipped, so its votes are final." };
65
+ const message = error instanceof Error && error.message.trim() ? error.message.trim() : "the request failed";
66
+ return { status: "failed", reason: `${action} did not go through: ${message.replace(/\.$/, "")}.` };
67
+ }
@@ -0,0 +1,244 @@
1
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type RefObject } from "react";
2
+ import { apiClient } from "../../../api-client";
3
+ import { usePlanAccess } from "../../../api-client/plan-access";
4
+ import type { RoadmapItem, RoadmapVote } from "../../../api-client/roadmap";
5
+ import {
6
+ buildSectionedRows,
7
+ DataTableStackView,
8
+ EMPTY_TABLE_CELL,
9
+ isSectionedItemRow,
10
+ renderSectionedRowHeader,
11
+ usePaneStatusFooter,
12
+ useRowNotes,
13
+ type DataTableCell,
14
+ type DataTableColumn,
15
+ type PaneFooterSegment,
16
+ type PaneHint,
17
+ type SectionedRow,
18
+ } from "../../../components";
19
+ import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
20
+ import { DetailReader, type DetailFact } from "../../../components/reader/detail-reader";
21
+ import { useAsyncResource, usePluginPaneState } from "../../../public/react";
22
+ import { useThemeColors } from "../../../theme/theme-context";
23
+ import type { PaneProps } from "../../../types/plugin";
24
+ import { Box, TextAttributes, type ScrollBoxRenderable } from "../../../ui";
25
+ import { formatShortDate } from "../../../utils/datetime-format";
26
+ import {
27
+ isVotable,
28
+ optimisticVote,
29
+ ROADMAP_STATUS_LABELS,
30
+ roadmapSections,
31
+ voteFailure,
32
+ votesText,
33
+ withLocalVotes,
34
+ type VoteFailure,
35
+ } from "./model";
36
+
37
+ /** The roadmap calls the pane makes; tests hand it their own. */
38
+ export type RoadmapClient = Pick<typeof apiClient, "getRoadmap" | "setRoadmapVote">;
39
+ export const RoadmapClientContext = createContext<RoadmapClient | null>(null);
40
+
41
+ type Row = SectionedRow<RoadmapItem>;
42
+ type ColumnId = "title" | "blurb" | "votes" | "shipped";
43
+ type Column = DataTableColumn & { id: ColumnId };
44
+
45
+ const rowKey = (row: Row) => row.key;
46
+ const itemKey = (item: RoadmapItem) => item.id;
47
+ const voteSignature = (item: RoadmapItem) => `${item.votes}:${item.voted}`;
48
+
49
+ /** Said once, quietly, to everyone who cannot vote: no upgrade pitch. */
50
+ const PRO_VOTES_SEGMENT: PaneFooterSegment = { id: "pro-votes", parts: [{ text: "Pro members vote", tone: "muted" }] };
51
+ const NO_SEGMENTS: PaneFooterSegment[] = [];
52
+
53
+ function shippedDate(item: RoadmapItem): string {
54
+ return item.shippedAt ? formatShortDate(item.shippedAt, { fallback: "" }) : "";
55
+ }
56
+
57
+ /** `votesCell` is what the votes cell says: the count, or a vote's failure on that row. */
58
+ function buildColumns(items: readonly RoadmapItem[], width: number, votesCell: (item: RoadmapItem) => string): Column[] {
59
+ const longest = Math.max(8, ...items.map((item) => item.title.length));
60
+ const narrow = width < 64;
61
+ const columns: Column[] = narrow
62
+ ? [{ id: "title", label: "Feature", width: Math.min(longest, 24), flexGrow: 1, align: "left" }]
63
+ : [
64
+ { id: "title", label: "Feature", width: Math.min(longest, width >= 120 ? 36 : 26), align: "left" },
65
+ { id: "blurb", label: "Summary", width: 16, flexGrow: 1, align: "left" },
66
+ ];
67
+ columns.push({ id: "votes", label: "Votes", width: Math.max(5, ...items.map((item) => votesCell(item).length)), align: "right" });
68
+ if (!narrow && items.some((item) => item.shippedAt)) {
69
+ columns.push({ id: "shipped", label: "Shipped", width: 12, align: "left" });
70
+ }
71
+ return columns;
72
+ }
73
+
74
+ function RoadmapDetail({ item, failure, width, scrollRef }: {
75
+ item: RoadmapItem;
76
+ failure: VoteFailure | null;
77
+ width: number;
78
+ scrollRef: RefObject<ScrollBoxRenderable | null>;
79
+ }) {
80
+ const colors = useThemeColors();
81
+ // The stack title names the item; the facts and the blurb follow.
82
+ const facts: DetailFact[] = [
83
+ { label: "Status", value: ROADMAP_STATUS_LABELS[item.status] },
84
+ { label: "Votes", value: item.voted ? `${item.votes}, yours included` : String(item.votes) },
85
+ ...(item.shippedAt ? [{ label: "Shipped", value: shippedDate(item) }] : []),
86
+ ...(failure ? [{ label: "Vote", value: failure.reason, color: colors.negative }] : []),
87
+ ];
88
+ return (
89
+ <DetailReader
90
+ ref={scrollRef}
91
+ resetScrollKey={item.id}
92
+ width={width}
93
+ facts={facts}
94
+ body={item.blurb || null}
95
+ bodyFormat="text"
96
+ />
97
+ );
98
+ }
99
+
100
+ export function RoadmapPane({ focused, width, height }: PaneProps) {
101
+ const colors = useThemeColors();
102
+ const client = useContext(RoadmapClientContext) ?? apiClient;
103
+ const access = usePlanAccess();
104
+ // A sign-in, a sign-out or a new plan asks again: the votes and the right to
105
+ // cast one are the account's. The new loader starts from no list, so the
106
+ // last account's marks and vote hint go at once, not when (or if) the new
107
+ // read lands. A refresh of the same session keeps its list.
108
+ const sessionKey = JSON.stringify([apiClient.getCurrentUser()?.id ?? null, access.emailVerified, access.hasProAccess]);
109
+ const loader = useCallback((_force: boolean) => client.getRoadmap(), [client, sessionKey]);
110
+ const resource = useAsyncResource(loader);
111
+ const sessionRef = useRef(sessionKey);
112
+ sessionRef.current = sessionKey;
113
+ const payload = resource.data;
114
+ const canVote = payload?.canVote === true;
115
+
116
+ // This session's votes, over the list until the next read replaces it.
117
+ const [localVotes, setLocalVotes] = useState<Record<string, RoadmapVote>>({});
118
+ useEffect(() => setLocalVotes({}), [resource.updatedAt]);
119
+ const items = useMemo(() => withLocalVotes(payload, localVotes), [payload, localVotes]);
120
+ const rows = useMemo(() => buildSectionedRows(roadmapSections(items), itemKey), [items]);
121
+ const { noteFor, setNote, clearNotes } = useRowNotes<RoadmapItem, VoteFailure>(items, { key: itemKey, signature: voteSignature });
122
+ // Another account's votes and refusals are not this one's.
123
+ useEffect(() => {
124
+ setLocalVotes({});
125
+ clearNotes();
126
+ }, [sessionKey, clearNotes]);
127
+
128
+ const [selectedId, setSelectedId] = usePluginPaneState<string | null>("selectedItemId", null);
129
+ const [openId, setOpenId] = usePluginPaneState<string | null>("openItemId", null);
130
+ const selected = items.find((item) => item.id === selectedId) ?? rows.find(isSectionedItemRow)?.item ?? null;
131
+ const open = openId ? items.find((item) => item.id === openId) ?? null : null;
132
+ useEffect(() => {
133
+ // An item that left the roadmap closes once the list that dropped it is in.
134
+ if (openId && payload && !open) setOpenId(null);
135
+ }, [open, openId, payload]);
136
+ const detailScrollRef = useRef<ScrollBoxRenderable>(null);
137
+
138
+ const inFlight = useRef(new Set<string>());
139
+ const reload = resource.reload;
140
+ const toggleVote = useCallback(async (item: RoadmapItem) => {
141
+ if (!canVote || !isVotable(item) || inFlight.current.has(item.id)) return;
142
+ const voted = !item.voted;
143
+ const session = sessionRef.current;
144
+ inFlight.current.add(item.id);
145
+ setNote(item.id, null);
146
+ setLocalVotes((current) => ({ ...current, [item.id]: optimisticVote(item, voted) }));
147
+ try {
148
+ const answer = await client.setRoadmapVote(item.id, voted);
149
+ // An answer for the account that just left changes nothing on screen.
150
+ if (sessionRef.current !== session) return;
151
+ setLocalVotes((current) => ({ ...current, [item.id]: answer }));
152
+ } catch (error) {
153
+ if (sessionRef.current !== session) return;
154
+ setLocalVotes((current) => ({ ...current, [item.id]: { id: item.id, votes: item.votes, voted: item.voted } }));
155
+ setNote(item.id, voteFailure(error, voted));
156
+ } finally {
157
+ inFlight.current.delete(item.id);
158
+ }
159
+ }, [canVote, client, setNote]);
160
+
161
+ usePaneRefreshKey(() => void reload(), { focused });
162
+
163
+ const target = open ?? selected;
164
+ const hints = useMemo<PaneHint[]>(() => (
165
+ canVote && target && isVotable(target)
166
+ ? [{ id: "vote", key: "v", label: target.voted ? " unvote" : "ote", title: target.voted ? "Take vote back" : "Vote", onPress: () => void toggleVote(target) }]
167
+ : []
168
+ ), [canVote, target, toggleVote]);
169
+ usePaneStatusFooter({
170
+ registrationId: "roadmap",
171
+ // A failed refresh keeps the list; the failure goes in the footer.
172
+ loading: resource.loading && !!payload,
173
+ error: payload ? resource.error : null,
174
+ info: !canVote && items.length > 0 ? [PRO_VOTES_SEGMENT] : NO_SEGMENTS,
175
+ hints,
176
+ });
177
+
178
+ const columns = useMemo(
179
+ () => buildColumns(items, width, (item) => noteFor(item)?.status ?? votesText(item)),
180
+ [items, noteFor, width],
181
+ );
182
+ const renderCell = useCallback((row: Row, column: Column): DataTableCell => {
183
+ if (!isSectionedItemRow(row)) return EMPTY_TABLE_CELL;
184
+ const item = row.item;
185
+ switch (column.id) {
186
+ case "title":
187
+ return { text: item.title, color: colors.textBright };
188
+ case "blurb":
189
+ return { text: item.blurb, color: colors.textDim };
190
+ case "votes": {
191
+ const failure = noteFor(item);
192
+ if (failure) return { text: failure.status, color: colors.negative };
193
+ return {
194
+ text: votesText(item),
195
+ value: item.votes,
196
+ color: item.voted ? colors.positive : isVotable(item) ? colors.text : colors.textDim,
197
+ ...(item.voted ? { attributes: TextAttributes.BOLD } : {}),
198
+ };
199
+ }
200
+ case "shipped":
201
+ return { text: shippedDate(item), color: colors.textDim };
202
+ }
203
+ }, [colors, noteFor]);
204
+
205
+ return (
206
+ <DataTableStackView<Row, Column>
207
+ focused={focused}
208
+ detailOpen={!!open}
209
+ onBack={() => setOpenId(null)}
210
+ detailTitle={open?.title}
211
+ detailContent={open ? (
212
+ <RoadmapDetail item={open} failure={noteFor(open)} width={width} scrollRef={detailScrollRef} />
213
+ ) : <Box flexGrow={1} />}
214
+ detailScrollRef={detailScrollRef}
215
+ rootWidth={width}
216
+ rootHeight={height}
217
+ // A refresh keeps the list on screen; only a first read waits or fails in the body.
218
+ status={payload ? undefined : {
219
+ loading: resource.loading,
220
+ error: resource.error,
221
+ subject: "Roadmap",
222
+ retry: { onRetry: reload, pending: resource.loading },
223
+ }}
224
+ emptyStateTitle="Nothing on the roadmap yet."
225
+ columns={columns}
226
+ items={rows}
227
+ getItemKey={rowKey}
228
+ isNavigable={isSectionedItemRow}
229
+ renderSectionHeader={renderSectionedRowHeader}
230
+ selection={{
231
+ kind: "id",
232
+ selectedId: selected?.id ?? null,
233
+ getId: rowKey,
234
+ onChange: (id) => setSelectedId(String(id)),
235
+ }}
236
+ onActivate={(row) => { if (isSectionedItemRow(row)) setOpenId(row.item.id); }}
237
+ sortColumnId={null}
238
+ sortDirection="asc"
239
+ renderCell={renderCell}
240
+ selectedTextOverridesCellColor
241
+ showHorizontalScrollbar={false}
242
+ />
243
+ );
244
+ }
@@ -81,6 +81,22 @@ function printRoot(underlying: string): string {
81
81
  return underlying.toUpperCase().replace(/\d+$/, "");
82
82
  }
83
83
 
84
+ /**
85
+ * Whose prints the pane shows: one name (`FLOW NVDA`), which takes the place
86
+ * of the universe filter, or the market, narrowed to the user's own tickers
87
+ * when the universe filter says so.
88
+ */
89
+ export interface FlowScope {
90
+ symbol?: string | null;
91
+ watchlist: ReadonlySet<string>;
92
+ }
93
+
94
+ /** The listed symbol a `FLOW <ticker>` pane is about, as the pane setting or report argument names it. */
95
+ export function flowScopeSymbol(value: unknown): string | null {
96
+ const symbol = typeof value === "string" ? value.trim().toUpperCase() : "";
97
+ return symbol || null;
98
+ }
99
+
84
100
  /**
85
101
  * The shared feed is published once for everyone, so every user preference is a
86
102
  * local predicate over the same events. Never push these upstream.
@@ -88,13 +104,14 @@ function printRoot(underlying: string): string {
88
104
  export function filterFlowEvents(
89
105
  events: readonly ScannerFlowEvent[] | undefined,
90
106
  filters: FlowFilters,
91
- watchlist: ReadonlySet<string>,
107
+ scope: FlowScope,
92
108
  now = Date.now(),
93
109
  ): ScannerFlowEvent[] {
94
110
  const minPremium = Number(filters.minPremium);
95
111
  const minVolOi = filters.volOi === "off" ? null : Number(filters.volOi);
96
112
  const maxExpiryDays = filters.expiry === "all" ? null : Number(filters.expiry);
97
- const roots = filters.universe === "watchlist" ? new Set([...watchlist].map(flowOptionRoot)) : null;
113
+ const roots = scope.symbol ? new Set([flowOptionRoot(scope.symbol)])
114
+ : filters.universe === "watchlist" ? new Set([...scope.watchlist].map(flowOptionRoot)) : null;
98
115
 
99
116
  return (events ?? []).filter((event) => {
100
117
  if (!(event.premium >= minPremium)) return false;
@@ -121,7 +138,15 @@ export function flowEmptyState(
121
138
  received: number,
122
139
  visible: number,
123
140
  status: ScannerStatus | undefined,
141
+ symbol: string | null = null,
124
142
  ): { title: string; hint: string } {
143
+ // One name's prints are mostly recorded ones, and a quiet name has none at all.
144
+ if (symbol) {
145
+ return {
146
+ title: `No ${symbol} prints under these filters.`,
147
+ hint: "Flow is recorded for the most active names, the day's movers and names with a flow alert.",
148
+ };
149
+ }
125
150
  if (received > visible) {
126
151
  return {
127
152
  title: `${received - visible} ${received - visible === 1 ? "print" : "prints"} hidden by filters.`,
@@ -219,22 +244,43 @@ export function mergeFlowRows(
219
244
  /** The pane's filters as a query for recorded prints, so each page is rows it will show. */
220
245
  export function flowHistoryQuery(
221
246
  filters: FlowFilters,
222
- watchlist: ReadonlySet<string>,
247
+ scope: FlowScope,
223
248
  before?: { at: number; id: string },
224
249
  limit = FLOW_HISTORY_PAGE,
225
250
  ): ScannerFlowHistoryQuery {
226
251
  return {
227
252
  ...(before ? { before } : {}),
228
253
  limit,
254
+ ...flowFilterQuery(filters),
255
+ ...(scope.symbol ? { symbols: [scope.symbol.toUpperCase()] }
256
+ : filters.universe === "watchlist" ? { symbols: [...scope.watchlist].map((symbol) => symbol.toUpperCase()).sort() } : {}),
257
+ };
258
+ }
259
+
260
+ function flowFilterQuery(filters: FlowFilters): Omit<ScannerFlowHistoryQuery, "before" | "limit" | "symbols"> {
261
+ return {
229
262
  minPremium: Number(filters.minPremium),
230
263
  ...(filters.side === "calls" ? { right: "C" as const } : filters.side === "puts" ? { right: "P" as const } : {}),
231
264
  ...(filters.kind === "sweeps" ? { kind: "sweep" as const } : filters.kind === "blocks" ? { kind: "block" as const } : {}),
232
265
  ...(filters.volOi === "off" ? {} : { minVolOi: Number(filters.volOi) }),
233
266
  ...(filters.expiry === "all" ? {} : { maxExpiryDays: Number(filters.expiry) }),
234
- ...(filters.universe === "watchlist" ? { symbols: [...watchlist].map((symbol) => symbol.toUpperCase()).sort() } : {}),
235
267
  };
236
268
  }
237
269
 
270
+ /**
271
+ * The session summary's query as the Cloud route reads it: the same filters as
272
+ * the recorded prints, with one name as `symbol`, which also asks for its strikes.
273
+ */
274
+ export function flowSessionSearch(filters: FlowFilters, scope: FlowScope): string {
275
+ const params = new URLSearchParams(flowHistorySearch({
276
+ ...flowFilterQuery(filters),
277
+ ...(!scope.symbol && filters.universe === "watchlist"
278
+ ? { symbols: [...scope.watchlist].map((symbol) => symbol.toUpperCase()).sort() } : {}),
279
+ }));
280
+ if (scope.symbol) params.set("symbol", scope.symbol.toUpperCase());
281
+ return params.toString();
282
+ }
283
+
238
284
  function localDay(at: number): string {
239
285
  const date = new Date(at);
240
286
  return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;