@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
@@ -38,10 +38,18 @@ export interface RiskConversion {
38
38
  export const FX_UNAVAILABLE = "Foreign holdings: daily FX closes unavailable";
39
39
  export const FX_NO_PAIR = "Foreign holdings: no daily FX pair for this currency";
40
40
  export const FX_STALE = "Foreign holdings: daily FX history is stale";
41
- export const FX_GAP = "Foreign holdings: daily FX history has gaps on the holding's dates";
41
+ export const FX_GAP = "Foreign holdings: daily FX history has gaps longer than three days on the holding's dates";
42
42
  export const FX_DISAGREES = "Foreign holdings: daily FX closes disagree with the current rate";
43
43
  export const LOCAL_GAP = "Foreign holdings: local daily history has a gap longer than a market holiday";
44
44
 
45
+ /**
46
+ * Most FX trading days in a row (weekdays) a pair may go without a daily
47
+ * close and still be bridged with the last close before them: Good Friday and
48
+ * Easter Monday, 25 December, 1 January, which the daily source skips while
49
+ * US sessions continue. A longer run is missing data and rejects the series.
50
+ */
51
+ const MAX_BRIDGED_FX_DAYS = 3;
52
+
45
53
  /**
46
54
  * Longest run of calendar days a listing may go without a close while US
47
55
  * sessions continue (Golden Week, Lunar New Year, national holidays). A longer
@@ -70,6 +78,42 @@ export function accountFxReason(currency: string, problem: string): string {
70
78
  return `${pair} ${tail ?? "daily FX closes unavailable"}`;
71
79
  }
72
80
 
81
+ /**
82
+ * The FX close a date converts at: the pair's own close that day, or the last
83
+ * close before it across at most MAX_BRIDGED_FX_DAYS weekdays without one
84
+ * (`bridged`). Null when the gap is longer, or no close comes before it.
85
+ */
86
+ export function bridgedFxClose(rateOn: (date: string) => number | undefined, date: string): { rate: number; bridged: boolean } | null {
87
+ let missing = 0;
88
+ for (let time = Date.parse(`${date}T00:00:00Z`), step = 0; step <= 7 + MAX_BRIDGED_FX_DAYS; step++, time -= DAY_MS) {
89
+ const day = new Date(time).toISOString().slice(0, 10);
90
+ const rate = rateOn(day);
91
+ if (rate != null && rate > 0) return { rate, bridged: day !== date };
92
+ const weekday = new Date(time).getUTCDay();
93
+ if (weekday !== 0 && weekday !== 6 && ++missing > MAX_BRIDGED_FX_DAYS) return null;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * "GBP/USD has no daily close on 2025-04-18 and 2025-04-21; the returns across
100
+ * them use the close before": what a conversion bridged (bridgedFxClose), said
101
+ * once per pair. Null when it bridged nothing.
102
+ */
103
+ export function fxBridgeNote(pair: string, dates: Iterable<string>): string | null {
104
+ const days = [...new Set(dates)].sort();
105
+ if (!days.length) return null;
106
+ const shown = days.length > 4 ? [...days.slice(0, 3), `${days.length - 3} more days`] : days;
107
+ const list = shown.length === 1 ? shown[0]! : `${shown.slice(0, -1).join(", ")} and ${shown.at(-1)!}`;
108
+ return `${pair} has no daily close on ${list}; the returns across ${days.length === 1 ? "it use the close before it" : "them use the close before each"}.`;
109
+ }
110
+
111
+ /** Account returns, and the dates whose FX close was the last one before them (bridgedFxClose). */
112
+ export interface AccountReturns {
113
+ returns: DatedReturn[];
114
+ bridged: string[];
115
+ }
116
+
73
117
  /**
74
118
  * Restates daily USD returns in the account currency at the FX closes of each
75
119
  * return's own start and end dates: the holding's USD value is divided by the
@@ -77,23 +121,29 @@ export function accountFxReason(currency: string, problem: string): string {
77
121
  * (1 + r) * fx(start) / fx(end) - 1. A listing already converted from its own
78
122
  * currency to USD therefore converts at the cross of the two closes of the
79
123
  * same date. Returns before the first FX close or after the last are dropped,
80
- * so the series starts and ends with the FX history; a return inside it with an
81
- * endpoint that has no FX close rejects the whole series instead of bridging it.
124
+ * so the series starts and ends with the FX history. An endpoint without an FX
125
+ * close converts at the last close before it across a gap of up to
126
+ * MAX_BRIDGED_FX_DAYS FX trading days (Good Friday and Easter Monday), and
127
+ * says so in `bridged`; a longer gap rejects the whole series.
82
128
  */
83
129
  export function convertReturnsToAccount(
84
130
  returns: readonly DatedReturn[],
85
131
  fx: Pick<AccountFx, "currency" | "closes" | "first" | "asOf">,
86
- ): DatedReturn[] {
132
+ ): AccountReturns {
87
133
  if (!fx.first || !fx.asOf) throw new Error(accountFxReason(fx.currency, FX_UNAVAILABLE));
88
134
  const converted: DatedReturn[] = [];
135
+ const bridged = new Set<string>();
136
+ const rateOn = (date: string) => fx.closes[date];
89
137
  for (const point of returns) {
90
138
  if (point.startDateKey < fx.first || point.dateKey > fx.asOf) continue;
91
- const start = fx.closes[point.startDateKey],
92
- end = fx.closes[point.dateKey];
93
- if (!(start! > 0) || !(end! > 0)) throw new Error(accountFxReason(fx.currency, FX_GAP));
94
- converted.push({ ...point, value: (1 + point.value) * (start! / end!) - 1 });
139
+ const start = bridgedFxClose(rateOn, point.startDateKey),
140
+ end = bridgedFxClose(rateOn, point.dateKey);
141
+ if (!start || !end) throw new Error(accountFxReason(fx.currency, FX_GAP));
142
+ if (start.bridged) bridged.add(point.startDateKey);
143
+ if (end.bridged) bridged.add(point.dateKey);
144
+ converted.push({ ...point, value: (1 + point.value) * (start.rate / end.rate) - 1 });
95
145
  }
96
- return converted;
146
+ return { returns: converted, bridged: [...bridged].sort() };
97
147
  }
98
148
 
99
149
  /** The pair that restates a listing currency in USD, or null when there is none. */
@@ -127,7 +177,9 @@ export function validateFxHistory(
127
177
  * On a US session the listing did not trade (its own holiday), the holding is
128
178
  * worth its last local close at that day's FX close: its USD value moves with
129
179
  * the currency alone, as it would in the account. A US session with no FX
130
- * close, or a local silence longer than any market holiday, rejects the whole
180
+ * close converts at the last close before it across a gap of up to
181
+ * MAX_BRIDGED_FX_DAYS FX trading days, listed in `bridged`; a longer FX gap,
182
+ * or a local silence longer than any market holiday, rejects the whole
131
183
  * series instead of bridging it. The series ends at the earlier of the last
132
184
  * local close and the last FX close, so no close is carried past the data.
133
185
  */
@@ -135,7 +187,7 @@ export function convertClosesToUsd(
135
187
  local: readonly { date: string; close: number }[],
136
188
  fx: FxCloses,
137
189
  divisor: number,
138
- ): PricePoint[] {
190
+ ): { points: PricePoint[]; bridged: string[] } {
139
191
  const points = [...local]
140
192
  .map((point) => ({ date: point.date.slice(0, 10), close: point.close }))
141
193
  .sort((left, right) => left.date.localeCompare(right.date));
@@ -143,6 +195,8 @@ export function convertClosesToUsd(
143
195
  const start = points[0]!.date > fx.first ? points[0]!.date : fx.first;
144
196
  const end = points.at(-1)!.date < fx.asOf ? points.at(-1)!.date : fx.asOf;
145
197
  const converted: PricePoint[] = [];
198
+ const bridged: string[] = [];
199
+ const rateOn = (date: string) => fx.closes.get(date);
146
200
  let cursor = 0;
147
201
  for (let time = Date.parse(`${start}T00:00:00Z`); time <= Date.parse(`${end}T00:00:00Z`); time += DAY_MS) {
148
202
  const date = new Date(time).toISOString().slice(0, 10);
@@ -153,11 +207,12 @@ export function convertClosesToUsd(
153
207
  const last = points[cursor]!;
154
208
  if (last.date > date) continue;
155
209
  if (time - Date.parse(`${last.date}T00:00:00Z`) > MAX_LOCAL_CLOSED_DAYS * DAY_MS) throw new Error(LOCAL_GAP);
156
- const rate = fx.closes.get(date);
157
- if (rate == null) throw new Error(FX_GAP);
158
- const close = (last.close * rate) / divisor;
210
+ const fxClose = bridgedFxClose(rateOn, date);
211
+ if (!fxClose) throw new Error(FX_GAP);
212
+ if (fxClose.bridged) bridged.push(date);
213
+ const close = (last.close * fxClose.rate) / divisor;
159
214
  converted.push({ date: new Date(time), open: close, high: close, low: close, close });
160
215
  }
161
216
  if (converted.length < 2) throw new Error("Daily history unavailable");
162
- return converted;
217
+ return { points: converted, bridged };
163
218
  }
@@ -3,6 +3,7 @@ import type { HeadlessPaneDefinition } from "../../../types/plugin";
3
3
  import { fetchPortfolioRiskMarket, RISK_HISTORY_LIMIT } from "./risk-client";
4
4
  import { parsePortfolioRiskEvidence } from "./risk-evidence";
5
5
  import { describeCollections } from "../../../portfolio/collection-name";
6
+ import { shellWord } from "../../../utils/shell-word";
6
7
  import { formatShocks, parseAssetShifts, parseFxShifts, riskCashLines } from "./risk-book";
7
8
  import {
8
9
  BASKET_VIEWS,
@@ -48,6 +49,22 @@ function reportPortfolioName(metadata: Record<string, unknown> | undefined): str
48
49
  return typeof name === "string" ? name : undefined;
49
50
  }
50
51
 
52
+ /**
53
+ * What a portfolio with nothing to estimate says, as one line: the commands
54
+ * that fill it. Risk is read from lots, so a symbol listed without shares
55
+ * (`portfolio add`) is no holding yet; `portfolio position set` gives it
56
+ * shares, and adds the symbol too when it is not listed.
57
+ */
58
+ function emptyPortfolioHint(name: string, listed: readonly string[]): string {
59
+ const portfolio = shellWord(name);
60
+ if (listed.length > 0) {
61
+ return `${name} lists ${listed.slice(0, 3).join(", ")}${listed.length > 3 ? ` and ${listed.length - 3} more` : ""} but holds no shares: `
62
+ + `gloom portfolio position set ${portfolio} ${shellWord(listed[0]!)} 10 150.`;
63
+ }
64
+ return `No holdings in ${name}. Add symbols with gloom portfolio add ${portfolio} AAPL,MSFT, `
65
+ + `then their shares with gloom portfolio position set ${portfolio} AAPL 10 150.`;
66
+ }
67
+
51
68
  /** Rows of one view in the compact result; the pane and reports keep every row. */
52
69
  const COMPACT_ROW_LIMIT = 100;
53
70
  /** The correlation view is every pair of holdings; the compact result keeps the strongest. */
@@ -260,6 +277,16 @@ export const portfolioRiskHeadless: HeadlessPaneDefinition<"bundle"> = {
260
277
  throw new Error(
261
278
  "Imported evidence belongs to a different portfolio or currency",
262
279
  );
280
+ // A portfolio with no holdings and no cash has nothing to estimate: say how to fill it, not a table of dashes.
281
+ if (tickers.length === 0 && cash.length === 0 && !evidence) {
282
+ const listed = local.tickers.filter((ticker) => ticker.metadata.portfolios.includes(id)).map((ticker) => ticker.metadata.ticker);
283
+ return {
284
+ sections: [],
285
+ complete: false,
286
+ empty: emptyPortfolioHint(local.portfolio.name, listed),
287
+ metadata: { portfolio: { id, name: local.portfolio.name, currency: local.portfolio.currency } },
288
+ };
289
+ }
263
290
  const requestedView = isRiskView(args.options.view) ? args.options.view : null;
264
291
  const view: string = requestedView ?? "risk";
265
292
  const localOnly =
@@ -48,7 +48,16 @@ import {
48
48
  type RiskHoldingRequest,
49
49
  type RiskMarketSnapshot,
50
50
  } from "./risk-client";
51
- import { accountFxPair, accountFxReason, convertReturnsToAccount, FX_UNAVAILABLE, type AccountFx } from "./risk-fx";
51
+ import {
52
+ accountFxPair,
53
+ accountFxReason,
54
+ bridgedFxClose,
55
+ convertReturnsToAccount,
56
+ FX_UNAVAILABLE,
57
+ fxBridgeNote,
58
+ riskConversion,
59
+ type AccountFx,
60
+ } from "./risk-fx";
52
61
 
53
62
  interface PortfolioRiskHolding {
54
63
  id: string;
@@ -77,6 +86,8 @@ interface PortfolioRiskHolding {
77
86
  historyAsOf: string | null;
78
87
  /** Daily returns in the account currency. */
79
88
  returns: DatedReturn[];
89
+ /** Dates its returns converted at the FX close before them, by pair ("GBP/USD"), its daily closes having none (bridgedFxClose). */
90
+ fxBridged: ReadonlyMap<string, readonly string[]>;
80
91
  error: string | null;
81
92
  /** Why the holding is outside the basket; null when it qualifies. */
82
93
  leftOut: string | null;
@@ -386,15 +397,43 @@ function cashReason(line: RiskCashLine, accountCurrency: string): string {
386
397
  ? "Cash is outside the equity basket"
387
398
  : `Cash is outside the equity basket; its ${accountCurrency} value moves with ${own}`;
388
399
  }
400
+ /** One note per pair whose missing daily closes the basket's returns were converted across (fxBridgeNote). */
401
+ function fxBridgeNotes(members: readonly Pick<PortfolioRiskHolding, "fxBridged">[]): string[] {
402
+ const byPair = new Map<string, Set<string>>();
403
+ for (const member of members)
404
+ for (const [pair, dates] of member.fxBridged) {
405
+ const set = byPair.get(pair) ?? new Set<string>();
406
+ for (const date of dates) set.add(date);
407
+ byPair.set(pair, set);
408
+ }
409
+ return [...byPair].flatMap(([pair, dates]) => fxBridgeNote(pair, dates) ?? []);
410
+ }
389
411
  /** USD returns in the account currency, or none with the reason its daily closes cannot convert them. */
390
- function accountReturns(returns: readonly DatedReturn[], account: AccountFx): { returns: DatedReturn[]; error: string | null } {
391
- if (account.error) return { returns: [], error: account.error };
412
+ function accountReturns(
413
+ returns: readonly DatedReturn[],
414
+ account: AccountFx,
415
+ ): { returns: DatedReturn[]; bridged: string[]; error: string | null } {
416
+ if (account.error) return { returns: [], bridged: [], error: account.error };
392
417
  try {
393
- return { returns: convertReturnsToAccount(returns, account), error: null };
418
+ return { ...convertReturnsToAccount(returns, account), error: null };
394
419
  } catch (error) {
395
- return { returns: [], error: error instanceof Error ? error.message : accountFxReason(account.currency, FX_UNAVAILABLE) };
420
+ return { returns: [], bridged: [], error: error instanceof Error ? error.message : accountFxReason(account.currency, FX_UNAVAILABLE) };
396
421
  }
397
422
  }
423
+ /** The dates a holding's returns converted at the FX close before them, by pair: its listing's into USD, then USD into the account's. */
424
+ function holdingFxBridges(
425
+ listingCurrency: string | null,
426
+ listingBridged: readonly string[] | undefined,
427
+ account: AccountFx | null | undefined,
428
+ accountBridged: readonly string[] | undefined,
429
+ ): Map<string, readonly string[]> {
430
+ const bridges = new Map<string, readonly string[]>();
431
+ const listingPair = listingCurrency && listingBridged?.length ? accountFxPair(riskConversion(listingCurrency)?.currency ?? listingCurrency) : null;
432
+ if (listingPair) bridges.set(listingPair, listingBridged!);
433
+ const accountPair = account?.pair;
434
+ if (accountPair && accountBridged?.length) bridges.set(accountPair, [...(bridges.get(accountPair) ?? []), ...accountBridged]);
435
+ return bridges;
436
+ }
398
437
  /**
399
438
  * "returns in TRY at daily USD/TRY closes": the currency the basket views of a
400
439
  * non-USD account are in and how they got there; null for a USD account, or
@@ -474,6 +513,8 @@ export function riskCoverageNotices(coverage: RiskCoverage | undefined): string[
474
513
  `${row.symbol} (${row.share == null ? "value unknown" : `${shareText(row.share)} of the book`}) is excluded from the risk estimate: ${lowerFirst(row.reason.replace(/\.$/, ""))}.`,
475
514
  );
476
515
  }
516
+ /** What an empty portfolio's pane says after "No holdings in this portfolio.": where holdings come from. */
517
+ export const NO_HOLDINGS_HINT = "Add holdings from the Portfolio pane (PF), or connect a broker in BR to sync them.";
477
518
  /** Why the basket views show no estimate, when too little of the holdings qualifies. */
478
519
  export function riskCoverageShortfall(
479
520
  coverage: RiskCoverage | undefined,
@@ -690,6 +731,7 @@ export function buildPortfolioRisk(
690
731
  // Converted returns end with the account currency's last daily close.
691
732
  historyAsOf: (inAccount ? inAccount.returns.at(-1)?.dateKey : null) ?? history?.asOf ?? null,
692
733
  returns,
734
+ fxBridged: holdingFxBridges(convertedFrom, history?.converted?.fxBridged, account, inAccount?.bridged),
693
735
  error,
694
736
  leftOut,
695
737
  };
@@ -774,6 +816,8 @@ export function buildPortfolioRisk(
774
816
  "Converted holdings are matched to the US close of the same date; where their market closes at another time, correlations and betas to US holdings read lower than they are.",
775
817
  ]
776
818
  : []),
819
+ // A pair's daily closes skip Good Friday, Easter Monday, 25 Dec and 1 Jan; returns across them use the close before.
820
+ ...fxBridgeNotes(members),
777
821
  ];
778
822
  const warnings = [
779
823
  ...market.warnings,
@@ -796,12 +840,14 @@ export function buildPortfolioRisk(
796
840
  return (instrument && byId.get(riskInstrumentId(instrument))?.returns) || [];
797
841
  };
798
842
  // SPY's USD returns are the market factor and the equity stress; the risk
799
- // rows compare the basket with SPY in the basket's own currency. A session
800
- // without an account close is a gap in that benchmark, never bridged, so it
801
- // cannot blank the holdings the closes do cover.
843
+ // rows compare the basket with SPY in the basket's own currency, bridged
844
+ // across missing account closes as the holdings are (bridgedFxClose). A
845
+ // session past a longer gap is a gap in that benchmark, so it cannot blank
846
+ // the holdings the closes do cover.
802
847
  const spy = proxy(RISK_BENCHMARK);
848
+ const accountClose = (date: string) => account ? bridgedFxClose((day) => account.closes[day], date) : null;
803
849
  const benchmark = account
804
- ? accountReturns(spy.filter((point) => account.closes[point.startDateKey] && account.closes[point.dateKey]), account).returns
850
+ ? accountReturns(spy.filter((point) => accountClose(point.startDateKey) && accountClose(point.dateKey)), account).returns
805
851
  : spy;
806
852
  // Since the filing only for a portfolio that has one; any other keeps the trailing window and says why.
807
853
  const filed = window.since === "filed" ? portfolio.importedFrom?.filed ?? null : null;
@@ -30,6 +30,7 @@ import {
30
30
  useAsyncResource,
31
31
  useAutoRefresh,
32
32
  usePaneSettingValue,
33
+ usePluginAppActions,
33
34
  usePluginPaneState,
34
35
  useShortcut,
35
36
  } from "../../../public/react";
@@ -54,6 +55,7 @@ import {
54
55
  HOLDINGS_SHARE_UNIT,
55
56
  HOLDINGS_SUMMARY_ROW_IDS,
56
57
  isShockRow,
58
+ NO_HOLDINGS_HINT,
57
59
  portfolioRiskRequests,
58
60
  portfolioRiskTickers,
59
61
  riskCoverageLine,
@@ -234,6 +236,7 @@ function RiskDetail({
234
236
  export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
235
237
  const instance = usePaneInstance(),
236
238
  renderer = useRendererHost();
239
+ const { showPane } = usePluginAppActions();
237
240
  const portfolios = useAppSelector((state) => state.config.portfolios),
238
241
  tickers = useAppSelector((state) => state.tickers),
239
242
  brokerAccounts = useAppSelector((state) => state.brokerAccounts);
@@ -403,14 +406,16 @@ export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
403
406
  const model = derived.model;
404
407
  const marketLoading = resource.loading && !resource.data;
405
408
  // A basket view with nothing to estimate says why in the body, not with a table of dashes.
406
- const basketBody = useMemo<{ loading?: boolean; error?: string; title?: string; message?: string } | null>(() => {
409
+ const basketBody = useMemo<{ loading?: boolean; error?: string; title?: string; message?: string; noHoldings?: boolean } | null>(() => {
407
410
  if (!model || !BASKET_VIEWS.has(view)) return null;
408
411
  if (!frozen && !resource.data)
409
412
  return marketLoading ? { loading: true } : resource.error ? { error: resource.error } : null;
410
413
  if (view === "holdings") return null;
411
414
  const shortfall = riskCoverageShortfall(model.coverage);
412
415
  // The currency and price shocks reprice the book without the basket, so they stay; the footer says why the basket scenarios do not.
413
- return shortfall && view === "stress" && model.rows.stress.some(isShockRow) ? null : shortfall;
416
+ if (shortfall && view === "stress" && model.rows.stress.some(isShockRow)) return null;
417
+ // A portfolio with no holdings says where they come from, as the other panes' empty states do.
418
+ return shortfall && !model.coverage?.holdings ? { ...shortfall, message: NO_HOLDINGS_HINT, noHoldings: true } : shortfall;
414
419
  }, [frozen, marketLoading, model, resource.data, resource.error, view]);
415
420
  // Stress with too little covered for the basket scenarios, its shocks shown alone.
416
421
  const stressShortfall = useMemo(
@@ -816,6 +821,7 @@ export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
816
821
  loadingLabel="Loading portfolio risk"
817
822
  emptyTitle={basketBody.title}
818
823
  emptyMessage={basketBody.message || undefined}
824
+ actions={basketBody.noHoldings ? <Button label="Open Portfolio" compact onPress={() => showPane("portfolio-list")} /> : undefined}
819
825
  />
820
826
  ) : evidenceMissing ? (
821
827
  <PaneStatusBody
@@ -5,11 +5,12 @@ import { attentionHeadless } from "./headless";
5
5
  import { TrendingPane } from "./trending";
6
6
  import { AttentionPane } from "./pane";
7
7
  import { ATTENTION_SERIES } from "./series";
8
+ import { ATTENTION_LISTED } from "./listing";
8
9
  export const attentionModule: PluginModule = {
9
10
  panes: [{ id: "attention", name: "Research Attention", icon: "A", component: AttentionPane, defaultPosition: "right", defaultMode: "floating",
10
11
  defaultFloatingSize: { width: 126, height: 32 }, tableExport: true, headless: attentionHeadless },
11
12
  { id: "attention-trending", reportFreshness: { status: "not-a-feed", basis: "hourly publication" }, name: "Gloom Trending", icon: "A", component: TrendingPane, defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 58, height: 12 } }],
12
- paneTemplates: [{ id: "attention-pane", paneId: "attention", label: "Research Attention",
13
+ paneTemplates: [{ id: "attention-pane", paneId: "attention", label: "Research Attention", listed: ATTENTION_LISTED,
13
14
  description: "Pro research attention: rankings, abnormal activity, sectors, countries and privacy-qualified hourly history.",
14
15
  keywords: ["attention", "trending", "research", "activity"], shortcut: { prefix: "ATTN", argKind: "text", argPlaceholder: "TICKER", argOptional: true },
15
16
  headless: attentionHeadless, createInstance: (_ctx, options) => ({ placement: "floating", settings: { window: attentionWindow(options?.values?.window), symbol: (options?.arg ?? options?.symbol)?.trim().toUpperCase() ?? "" } }) },
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Whether ATTN is listed: in command-bar search, Help, `gloom catalog` and
3
+ * search, the plugin directory and chat suggestions, and its two screener
4
+ * fields (ATTN HRS, ATTN Z) in the EQS field and metric pickers.
5
+ *
6
+ * Research attention publishes an hour only once enough opted-in users
7
+ * researched in it, and it has published nothing for weeks, so ATTN stays
8
+ * unlisted until it does. Typing ATTN still opens it, a screen that already
9
+ * uses its fields keeps them, and the Attention Counts setting and its
10
+ * collection are unchanged. Set it to true the day ATTN publishes again,
11
+ * with the server's own switch, and refresh the docs snapshot
12
+ * (`bun run docs:snapshot`) once the site lists ATTN.
13
+ */
14
+ export const ATTENTION_LISTED = false;
@@ -11,7 +11,7 @@ import {
11
11
  resetSignedInBrokerCatalog,
12
12
  subscribeSignedInBrokers,
13
13
  } from "../../../brokers/signed-in/catalog";
14
- import { DataTableStackView, EmptyState } from "../../../components";
14
+ import { Button, DataTableStackView, EmptyState } from "../../../components";
15
15
  import { t } from "../../../i18n";
16
16
  import { useAppLanguage } from "../../../i18n/react";
17
17
  import {
@@ -340,10 +340,14 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
340
340
  sortDirection="asc"
341
341
  getItemKey={(row) => row.id}
342
342
  renderCell={renderCell}
343
- // The footer's a adds one.
343
+ // The same add the footer's a and the command bar start; as the state's first
344
+ // action it also answers Enter.
344
345
  emptyContent={(
345
346
  <Box width="100%" paddingX={1} paddingY={1}>
346
- <EmptyState title={t("No broker profiles.")} />
347
+ <EmptyState
348
+ title={t("No broker profiles.")}
349
+ actions={<Button label="Add Broker Account" onPress={startAdd} />}
350
+ />
347
351
  </Box>
348
352
  )}
349
353
  emptyStateTitle={t("No broker profiles.")}
@@ -12,11 +12,19 @@ import {
12
12
  activityLabel,
13
13
  criticalityColor,
14
14
  metricColor,
15
+ moneyAmount,
16
+ moneyText,
15
17
  text,
16
18
  tickerSymbol,
17
19
  } from "./format";
18
20
  import { favoriteKey } from "./table-model";
19
21
 
22
+ /** A dollar figure in the compact form, with its whole number for an export. */
23
+ function moneyCell(value: string | null | undefined, color: string): DataTableCell {
24
+ const amount = moneyAmount(value);
25
+ return { text: moneyText(value), ...(amount == null ? {} : { value: amount }), color };
26
+ }
27
+
20
28
  interface BuildoutCellContext {
21
29
  favoriteBusyKey: string | null;
22
30
  /** The row's last star did not take. */
@@ -40,7 +48,7 @@ export function renderBuildoutCell(
40
48
  case "companyCount":
41
49
  return { text: list.companyCount == null ? "-" : String(list.companyCount), color: colors.textDim };
42
50
  case "totalMarketCap":
43
- return { text: text(list.totalMarketCap), color: colors.textDim };
51
+ return moneyCell(list.totalMarketCap, colors.textDim);
44
52
  case "avgSectorGrowth":
45
53
  return { text: text(list.avgSectorGrowth), color: metricColor(list.avgSectorGrowth) };
46
54
  case "avgReturn1y":
@@ -87,16 +95,16 @@ export function renderBuildoutCell(
87
95
  case "criticality":
88
96
  return { text: text(company.aiCriticality), color: criticalityColor(company.aiCriticality), attributes: TextAttributes.BOLD };
89
97
  case "marketCap":
90
- return { text: text(company.marketCap), color: colors.textDim };
98
+ return moneyCell(company.marketCap, colors.textDim);
91
99
  case "revenue":
92
- return { text: text(company.revenue), color: colors.textDim };
100
+ return moneyCell(company.revenue, colors.textDim);
93
101
  case "revenueGrowth":
94
102
  return {
95
103
  text: text(company.revenueGrowthYoy ?? company.lastQuarterGrowth),
96
104
  color: metricColor(company.revenueGrowthYoy ?? company.lastQuarterGrowth),
97
105
  };
98
106
  case "netIncome":
99
- return { text: text(company.netIncome), color: metricColor(company.netIncome) };
107
+ return moneyCell(company.netIncome, metricColor(company.netIncome));
100
108
  case "margin":
101
109
  return { text: text(company.profitMargins), color: metricColor(company.profitMargins) };
102
110
  case "forwardPE":
@@ -44,6 +44,44 @@ export function metricNumber(value: unknown): number | null {
44
44
  return (negative ? -parsed : parsed) * multiplier;
45
45
  }
46
46
 
47
+ /** A dollar figure as the service prints it ("$5094.1B", "$-4.9B"); anything else is not money to reformat. */
48
+ const DOLLAR_STRING = /^-?\$-?[\d,]*\.?\d+\s*[KMBT]?$/i;
49
+
50
+ /** The dollars in a string the service prints as money, or null for anything else. */
51
+ export function moneyAmount(value: unknown): number | null {
52
+ return typeof value === "string" && DOLLAR_STRING.test(value.trim()) ? metricNumber(value) : null;
53
+ }
54
+
55
+ const MONEY_UNITS = [
56
+ { divisor: 1e12, suffix: "T" },
57
+ { divisor: 1e9, suffix: "B" },
58
+ { divisor: 1e6, suffix: "M" },
59
+ { divisor: 1e3, suffix: "K" },
60
+ ] as const;
61
+
62
+ /**
63
+ * Dollars in the one compact form every TheBuildout table uses: "$5.1T",
64
+ * "$356.4B", "-$4.9B". The unit is picked after rounding, so $999.96B is
65
+ * "$1.0T" and not "$1000.0B".
66
+ */
67
+ export function formatCompactMoney(amount: number): string {
68
+ const abs = Math.abs(amount);
69
+ let index = MONEY_UNITS.findIndex((unit) => abs >= unit.divisor);
70
+ let digits = index < 0 ? abs.toFixed(0) : (abs / MONEY_UNITS[index]!.divisor).toFixed(1);
71
+ if (index > 0 && Number(digits) >= 1000) {
72
+ index -= 1;
73
+ digits = (abs / MONEY_UNITS[index]!.divisor).toFixed(1);
74
+ }
75
+ const sign = amount < 0 && /[1-9]/.test(digits) ? "-" : "";
76
+ return `${sign}$${digits}${index < 0 ? "" : MONEY_UNITS[index]!.suffix}`;
77
+ }
78
+
79
+ /** A table cell's text for a dollar figure: compact when it is money, the service's own text otherwise, "-" when absent. */
80
+ export function moneyText(value: string | null | undefined): string {
81
+ const amount = moneyAmount(value);
82
+ return amount == null ? text(value) : formatCompactMoney(amount);
83
+ }
84
+
47
85
  export function dateShort(value: string | null | undefined) {
48
86
  const date = parseDisplayDate(value);
49
87
  if (!date) return "-";
@@ -1,4 +1,4 @@
1
- import { metricNumber, textOrNull, tickerSymbol, uniqueStrings } from "../format";
1
+ import { metricNumber, moneyAmount, textOrNull, tickerSymbol, uniqueStrings } from "../format";
2
2
  import type {
3
3
  BuildoutCompany,
4
4
  BuildoutList,
@@ -124,7 +124,42 @@ export function normalizeList(raw: RawObject): BuildoutList {
124
124
  };
125
125
  }
126
126
 
127
+ /**
128
+ * The statement figures a depositary receipt can carry in the issuer's
129
+ * reporting currency under a "$" and the receipt's USD currency: the service
130
+ * converts a foreign listing's figures and keeps the local ones as "...Original",
131
+ * but for an ADR it prints the local amounts as they are and names no currency
132
+ * (SK hynix's receipt shows the won revenue of its Korean listing as dollars).
133
+ */
134
+ const STATEMENT_FIELDS = ["revenue", "netIncome", "freeCashFlow", "operatingCashFlow", "totalCash", "totalDebt", "dilutedEps"] as const;
135
+
136
+ /** A yearly figure this many times the company's whole market value is not dollars. */
137
+ const IMPLAUSIBLE_MULTIPLE_OF_MARKET_CAP = 10;
138
+
139
+ /**
140
+ * A USD row with nothing converted whose revenue or net income dwarfs its own
141
+ * market cap has statements in another currency, and the row does not say
142
+ * which: the figures are left out rather than shown as wrong dollars. The
143
+ * ordinary range of revenue to market cap is a few times; the rows this
144
+ * catches sit at 15 to 4,000.
145
+ */
146
+ function withoutUnconvertedStatements(company: BuildoutCompany): BuildoutCompany {
147
+ const converted = [company.revenueOriginal, company.netIncomeOriginal].some((value) => textOrNull(value) != null);
148
+ const usd = (company.currency?.trim().toUpperCase() || "USD") === "USD";
149
+ const cap = moneyAmount(company.marketCap);
150
+ if (converted || !usd || cap == null || cap <= 0) return company;
151
+ const revenue = moneyAmount(company.revenue);
152
+ const income = moneyAmount(company.netIncome);
153
+ const limit = cap * IMPLAUSIBLE_MULTIPLE_OF_MARKET_CAP;
154
+ if ((revenue == null || Math.abs(revenue) <= limit) && (income == null || Math.abs(income) <= limit)) return company;
155
+ return { ...company, ...Object.fromEntries(STATEMENT_FIELDS.map((field) => [field, null])) };
156
+ }
157
+
127
158
  export function normalizeCompany(raw: RawObject): BuildoutCompany {
159
+ return withoutUnconvertedStatements(readCompany(raw));
160
+ }
161
+
162
+ function readCompany(raw: RawObject): BuildoutCompany {
128
163
  const ticker = tickerSymbol(stringField(raw, "ticker"));
129
164
  const name = stringField(raw, "name") ?? ticker ?? "Company";
130
165
  const supplyChain = objectField(raw, "supplyChain", "supply_chain");
@@ -160,7 +160,10 @@ export function BuildoutPane({ focused, width, height }: PaneProps) {
160
160
  const favoriteNotes = useRowNotes<BuildoutRow, string>(rows, { key: favoriteNoteKey });
161
161
  const { setNote: setFavoriteNote, noteFor: favoriteNoteFor } = favoriteNotes;
162
162
  clearFavoriteNotesRef.current = favoriteNotes.clearNotes;
163
- const columns = useMemo(() => columnsForTab(activeTab, selectedList, canFavorite), [activeTab, canFavorite, selectedList]);
163
+ const columns = useMemo(
164
+ () => columnsForTab(activeTab, selectedList, canFavorite, width),
165
+ [activeTab, canFavorite, selectedList, width],
166
+ );
164
167
  const selectedRow = rows[selectedIndex] ?? rows[0] ?? null;
165
168
  // The table's cells carry their own badges; this catalog only serves the open
166
169
  // detail: its row's tickers, plus the loaded rows' tickers its text mentions.
@@ -8,6 +8,7 @@ import type {
8
8
  BuildoutTabId,
9
9
  SortComparable,
10
10
  } from "./model/types";
11
+ import { dropColumnsToFit } from "../../../components/ui/table-layout";
11
12
  import type { SortDirection } from "../../../utils/sort-values";
12
13
  import { metricNumber, sourceDetailEntries, textOrNull, tickerSymbol } from "./format";
13
14
 
@@ -171,9 +172,29 @@ export function activeRows(
171
172
  return state.intel.items.map((item) => ({ kind: "intel", item }));
172
173
  }
173
174
 
174
- export function columnsForTab(activeTab: BuildoutTabId, selectedList: BuildoutList | null, canFavorite: boolean) {
175
+ /**
176
+ * What the Companies tab gives up when the pane is too narrow for its figures,
177
+ * least needed first: the descriptions, then the columns that only add to a
178
+ * figure already shown, instead of the right edge cutting Market Cap or a
179
+ * median. The star and the names stay at every width.
180
+ */
181
+ const LIST_DROP_ORDER: readonly BuildoutColumnId[] = ["listDescription", "avgMargin", "avgReturn1y", "avgSectorGrowth"];
182
+ const COMPANY_DROP_ORDER: readonly BuildoutColumnId[] = [
183
+ "description", "employees", "dividendYield", "forwardPE", "netIncome", "margin", "sectorTech", "criticality",
184
+ ];
185
+
186
+ /** The columns of a tab for a pane `width` cells wide; the table's scrollbar keeps its cell. */
187
+ export function columnsForTab(
188
+ activeTab: BuildoutTabId,
189
+ selectedList: BuildoutList | null,
190
+ canFavorite: boolean,
191
+ width: number,
192
+ ): BuildoutColumn[] {
175
193
  if (activeTab === "companies") {
176
- return selectedList && canFavorite ? [favoriteColumn, ...companyColumns] : selectedList ? companyColumns : listColumns;
194
+ const columns = selectedList
195
+ ? canFavorite ? [favoriteColumn, ...companyColumns] : companyColumns
196
+ : listColumns;
197
+ return dropColumnsToFit(columns, width - 1, selectedList ? COMPANY_DROP_ORDER : LIST_DROP_ORDER);
177
198
  }
178
199
  if (activeTab === "sites") return canFavorite ? [favoriteColumn, ...siteColumns] : siteColumns;
179
200
  return intelColumns;
@@ -1,11 +1,14 @@
1
1
  // Identity of the built-in composite plugins, shared by the native catalog and
2
2
  // the browser catalog (catalog-browser.ts). Keep this file import-free so the
3
- // browser catalog can read it without pulling in native-only modules.
3
+ // browser catalog can read it without pulling in native-only modules; the
4
+ // one import, ATTN's listing flag, has none of its own.
4
5
  //
5
6
  // Screeners & Movers and Ownership & Insiders ship fewer panes on
6
7
  // the web, so each catalog gives them their own description rather than
7
8
  // advertising panes they lack.
8
9
 
10
+ import { ATTENTION_LISTED } from "./attention/listing";
11
+
9
12
  export const applicationPluginMeta = {
10
13
  id: "application",
11
14
  name: "Application",
@@ -116,7 +119,8 @@ export const altDataPluginMeta = {
116
119
  stateId: "ticker-research",
117
120
  name: "Supply Chain & Alt Data",
118
121
  version: "1.0.0",
119
- description: "Supply chains and exposure, government awards, hiring, apps and job postings, social mentions, research attention, GPU prices, and power queues.",
122
+ // Research attention joins once ATTN is listed again (attention/listing.ts).
123
+ description: `Supply chains and exposure, government awards, hiring, apps and job postings, social mentions, ${ATTENTION_LISTED ? "research attention, " : ""}GPU prices, and power queues.`,
120
124
  toggleable: true,
121
125
  };
122
126