@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
@@ -6,16 +6,20 @@ import {
6
6
  validateOrganizationTickers,
7
7
  type OrganizationTicker,
8
8
  } from "../../../api-client/organization-tickers";
9
+ import { createTtlCache, type TtlCache } from "../../../data/ttl-cache";
10
+ import { findSponsorListings, type ListingFinder } from "./sponsor-listing";
9
11
 
10
12
  /**
11
- * A lead sponsor's ticker, only ever as Gloom Cloud resolved it. `none` is an
12
- * answer (no listed company, or a name that fits several); `failed` means it
13
- * was not looked up at all and says nothing about the sponsor.
13
+ * A lead sponsor's ticker, as Gloom Cloud resolved it, or, for a company it
14
+ * has no reviewed name for, as the listing search finds it (`listingSearch`).
15
+ * `none` is an answer (no listed company, or a name that fits several);
16
+ * `failed` means it was not looked up at all and says nothing about the
17
+ * sponsor.
14
18
  */
15
19
  export type SponsorTicker =
16
20
  | { kind: "pending" }
17
21
  | { kind: "listed"; ticker: string; exchange: string | null; entityName: string | null }
18
- | { kind: "none"; entityName: string | null; ambiguous: boolean }
22
+ | { kind: "none"; entityName: string | null; ambiguous: boolean; listingSearch?: "done" }
19
23
  | { kind: "failed"; reason: string };
20
24
 
21
25
  /** A ticker only when one company and its listing were found; an ambiguous name never gets one. */
@@ -46,17 +50,21 @@ const sponsorKey = (name: string) => name.trim().replace(/\s+/g, " ").toLowerCas
46
50
  * never stored as having no ticker because the lookup did not happen.
47
51
  */
48
52
  export class SponsorTickerStore {
49
- private readonly answers = new Map<string, { value: SponsorTicker; expiresAt: number }>();
53
+ private readonly answers: TtlCache<SponsorTicker>;
50
54
  private readonly failures = new Map<string, string>();
51
55
  private readonly inFlight = new Set<string>();
56
+ private readonly searching = new Set<string>();
57
+ private readonly searchFailures = new Set<string>();
52
58
  private readonly listeners = new Set<() => void>();
53
59
 
54
- constructor(private readonly lookup: Lookup, private readonly now: () => number = Date.now) {}
60
+ constructor(private readonly lookup: Lookup, now: () => number = Date.now) {
61
+ this.answers = createTtlCache<SponsorTicker>({ ttlMs: CACHE_TTL_MS, now });
62
+ }
55
63
 
56
64
  get(name: string): SponsorTicker {
57
65
  const key = sponsorKey(name);
58
- const answer = this.answers.get(key);
59
- if (answer && answer.expiresAt > this.now()) return answer.value;
66
+ const answer = this.answers.peek(key);
67
+ if (answer) return answer;
60
68
  const reason = this.failures.get(key);
61
69
  return reason ? { kind: "failed", reason } : { kind: "pending" };
62
70
  }
@@ -86,8 +94,7 @@ export class SponsorTickerStore {
86
94
  keys.forEach((key) => this.inFlight.add(key));
87
95
  try {
88
96
  const results = await this.lookup(batch, signal);
89
- const expiresAt = this.now() + CACHE_TTL_MS;
90
- results.forEach((result, index) => this.answers.set(keys[index]!, { value: sponsorTickerFrom(result), expiresAt }));
97
+ results.forEach((result, index) => this.answers.set(keys[index]!, sponsorTickerFrom(result)));
91
98
  } catch (error) {
92
99
  // An abandoned lookup (its pane closed) is neither an answer nor a failure.
93
100
  if (!signal?.aborted) {
@@ -102,24 +109,94 @@ export class SponsorTickerStore {
102
109
  }));
103
110
  }
104
111
 
112
+ /**
113
+ * `load`, then `searchListings` for the companies among the names, but the
114
+ * caller waits no longer than `ms`: the lookups go on and tickers fill in
115
+ * when they answer.
116
+ */
117
+ async loadWithin(
118
+ names: readonly string[],
119
+ ms: number,
120
+ signal?: AbortSignal,
121
+ listings?: { companies: readonly string[]; find?: ListingFinder },
122
+ ): Promise<void> {
123
+ let timer: ReturnType<typeof setTimeout> | undefined;
124
+ const patience = new Promise<void>((resolve) => { timer = setTimeout(resolve, ms); });
125
+ const lookups = this.load(names, signal).then(() => listings ? this.searchListings(listings.companies, listings.find, signal) : undefined);
126
+ try {
127
+ await Promise.race([lookups, patience]);
128
+ } finally {
129
+ clearTimeout(timer);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Looks the companies Gloom Cloud had no name for up in Gloom Cloud's
135
+ * search, the one that finds `Bayer` when it is typed, and keeps a primary
136
+ * listing as the sponsor's ticker (`findSponsorListings` says which names
137
+ * qualify). Only names Cloud answered with no company, no listing and no
138
+ * ambiguity are asked: a company it knows, and a name that fits several,
139
+ * stay as it said. Each name is searched once, all in one request; a search
140
+ * that failed waits for `retryFailed`, and tells nothing about the sponsor.
141
+ */
142
+ async searchListings(names: readonly string[], find: ListingFinder = findSponsorListings, signal?: AbortSignal): Promise<void> {
143
+ const asked = new Map<string, string>();
144
+ for (const name of names) {
145
+ const trimmed = name.trim();
146
+ const key = sponsorKey(trimmed);
147
+ const answer = this.answers.peek(key);
148
+ if (!trimmed || asked.has(key) || this.searching.has(key) || this.searchFailures.has(key)) continue;
149
+ if (answer?.kind !== "none" || answer.ambiguous || answer.entityName || answer.listingSearch) continue;
150
+ asked.set(key, trimmed);
151
+ }
152
+ if (asked.size === 0) return;
153
+ const keys = [...asked.keys()];
154
+ keys.forEach((key) => this.searching.add(key));
155
+ try {
156
+ const found = await find([...asked.values()], signal);
157
+ if (signal?.aborted) return;
158
+ let changed = false;
159
+ for (const [key, name] of asked) {
160
+ const listing = found.get(name) ?? null;
161
+ this.answers.set(key, listing
162
+ ? { kind: "listed", ticker: listing.ticker, exchange: listing.exchange, entityName: listing.entityName }
163
+ : { kind: "none", entityName: null, ambiguous: false, listingSearch: "done" });
164
+ changed ||= listing !== null;
165
+ }
166
+ if (changed) this.listeners.forEach((listener) => listener());
167
+ } catch {
168
+ // A failed search says nothing about the sponsor; it is asked again on retry.
169
+ if (!signal?.aborted) keys.forEach((key) => this.searchFailures.add(key));
170
+ } finally {
171
+ keys.forEach((key) => this.searching.delete(key));
172
+ }
173
+ }
174
+
105
175
  /** Forgets failed lookups, so the next `load` asks again. */
106
176
  retryFailed(): void {
107
- if (this.failures.size === 0) return;
177
+ if (this.failures.size === 0 && this.searchFailures.size === 0) return;
108
178
  this.failures.clear();
179
+ this.searchFailures.clear();
109
180
  this.listeners.forEach((listener) => listener());
110
181
  }
111
182
  }
112
183
 
113
- const sponsorTickerStore = new SponsorTickerStore(async (names, signal) => (
184
+ export const sponsorTickerStore = new SponsorTickerStore(async (names, signal) => (
114
185
  validateOrganizationTickers(await apiClient.getCloudOrganizationTickers(names, signal), names)
115
186
  ));
116
187
 
188
+ export interface SponsorTickersOptions {
189
+ /** The company sponsors among `names`: the ones Cloud's search may look up when its ticker lookup has no name for them. */
190
+ companies?: readonly string[];
191
+ store?: SponsorTickerStore;
192
+ }
193
+
117
194
  /**
118
195
  * The tickers for the sponsors on screen. Rows draw at once and tickers fill
119
196
  * in when the lookup answers; `retry` asks again for the ones that failed.
120
197
  * `sessionKey` changing (a sign-in) retries them too.
121
198
  */
122
- export function useSponsorTickers(names: readonly string[], sessionKey: string, store: SponsorTickerStore = sponsorTickerStore) {
199
+ export function useSponsorTickers(names: readonly string[], sessionKey: string, { companies, store = sponsorTickerStore }: SponsorTickersOptions = {}) {
123
200
  const [version, setVersion] = useState(0);
124
201
  const [epoch, setEpoch] = useState(0);
125
202
  const [signal, setSignal] = useState<AbortSignal | null>(null);
@@ -134,10 +211,13 @@ export function useSponsorTickers(names: readonly string[], sessionKey: string,
134
211
  store.retryFailed();
135
212
  }, [sessionKey, store]);
136
213
  const namesKey = names.join("\n");
214
+ const companiesKey = (companies ?? []).join("\n");
137
215
  useEffect(() => {
138
216
  if (!signal || signal.aborted || !namesKey) return;
139
- void store.load(namesKey.split("\n"), signal);
140
- }, [epoch, namesKey, sessionKey, signal, store, version]);
217
+ void store.load(namesKey.split("\n"), signal).then(() => {
218
+ if (companiesKey) return store.searchListings(companiesKey.split("\n"), findSponsorListings, signal);
219
+ });
220
+ }, [companiesKey, epoch, namesKey, sessionKey, signal, store, version]);
141
221
  const retry = useCallback(() => {
142
222
  store.retryFailed();
143
223
  setEpoch((current) => current + 1);
@@ -0,0 +1,50 @@
1
+ import type { PageRequest, RowPage } from "../../../components";
2
+ import type { ClinicalTrial, ClinicalTrialsPage, StudiesQuery } from "./client";
3
+
4
+ /** What one stage of the list asks the registry for on top of the search: a sponsor class to keep or to leave out. */
5
+ export type TrialStage = Pick<StudiesQuery, "sponsorClasses" | "excludedSponsorClasses">;
6
+
7
+ /** The companies' studies first and everything else after them. */
8
+ export const COMPANIES_FIRST: readonly TrialStage[] = [{ sponsorClasses: ["INDUSTRY"] }, { excludedSponsorClasses: ["INDUSTRY"] }];
9
+
10
+ /** Where a page continues: which stage it is in, and the registry's token inside it. */
11
+ interface StageCursor {
12
+ stage: number;
13
+ token?: string;
14
+ }
15
+
16
+ /**
17
+ * Pages the registry through `stages` one after the other: a stage's studies
18
+ * come newest first, and when it has no more the next stage starts, so the
19
+ * list reaches a university's study only once the companies' are in. The
20
+ * registry pages by an opaque token, which the next page's offset keys; the
21
+ * first page always starts at the first stage, so a reload starts over. A
22
+ * stage with nothing in it goes straight on to the next, so an empty page is
23
+ * never the end of the list while a stage is left. `onPage` runs on a page's
24
+ * studies before it is handed over.
25
+ */
26
+ export function createTrialPageLoader({ list, query, stages, onPage }: {
27
+ list: (query: StudiesQuery, signal: AbortSignal) => Promise<ClinicalTrialsPage>;
28
+ query: StudiesQuery;
29
+ stages: readonly TrialStage[];
30
+ onPage?: (trials: ClinicalTrial[], signal: AbortSignal) => Promise<void>;
31
+ }): (request: Pick<PageRequest, "offset" | "signal">) => Promise<RowPage<ClinicalTrial>> {
32
+ const cursors = new Map<number, StageCursor>();
33
+ return async ({ offset, signal }) => {
34
+ let cursor: StageCursor | undefined = offset === 0 ? { stage: 0 } : cursors.get(offset);
35
+ while (cursor) {
36
+ const page: ClinicalTrialsPage = await list({ ...query, ...stages[cursor.stage], pageToken: cursor.token }, signal);
37
+ const next: StageCursor | null = page.nextPageToken ? { stage: cursor.stage, token: page.nextPageToken }
38
+ : cursor.stage + 1 < stages.length ? { stage: cursor.stage + 1 } : null;
39
+ if (page.trials.length === 0 && next && !page.nextPageToken) {
40
+ cursor = next;
41
+ continue;
42
+ }
43
+ const nextOffset = offset + page.trials.length;
44
+ if (next) cursors.set(nextOffset, next);
45
+ await onPage?.(page.trials, signal);
46
+ return { rows: page.trials, hasMore: !!next, nextOffset };
47
+ }
48
+ return { rows: [], hasMore: false };
49
+ };
50
+ }
@@ -142,6 +142,8 @@ function headlessCandidates(registry: PaneFunctionCatalog): ManifestCandidate[]
142
142
  const pane = registry.panes.get(template.paneId);
143
143
  if (!pane) continue;
144
144
  templatedPaneIds.add(pane.id);
145
+ // An unlisted function (`listed: false`) is no tool, and its pane none in its place.
146
+ if (template.listed === false) continue;
145
147
  const definition = definitionFor(template, pane);
146
148
  if (!definition) continue;
147
149
  const token = template.shortcut?.prefix ?? template.id;
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../utils/log-failure";
1
2
  import { useCallback, useMemo, useRef, type ReactNode } from "react";
2
3
  import { isAccessDenied } from "../../../api-client/errors";
3
4
  import type { HiringBuildingFilter, HiringBuildingShift } from "../../../api-client/hiring";
@@ -60,7 +61,10 @@ export function HiringBuildingView({ symbol, width, height, focused, accessKey,
60
61
  try {
61
62
  const payload = snapshotSetting ? validateBuilding(snapshotSetting) : null;
62
63
  return payload && ("filter" in payload ? !symbol : payload.symbol.toUpperCase() === symbol?.toUpperCase()) ? payload : null;
63
- } catch { return null; }
64
+ } catch (error) {
65
+ logFailure("company-attention.building", "Validate hiring building snapshot failed")(error);
66
+ return null;
67
+ }
64
68
  }, [snapshotSetting, symbol]);
65
69
  const loader = useCallback((force: boolean) => snapshot ? Promise.resolve({ payload: snapshot, stale: false, refreshError: null }) : loadBuilding(symbol, accessKey, filter, force), [snapshot, symbol, accessKey, filter]);
66
70
  const resource = useAsyncResource(loader, { initialData: () => cachedBuilding(symbol, accessKey, filter), clearOnError: isAccessDenied, keepPreviousData: true });
@@ -3,6 +3,7 @@ import type { AppAttentionFilter, AppAttentionPayload, AppRankPayload } from "..
3
3
  import type { HiringBoard, HiringBuildingBoard, HiringBuildingFilter, HiringBuildingShift, HiringCompanyBuilding, HiringCompanyMoves, HiringCompanySignals, HiringMixRow, HiringMove, HiringMovePosting, HiringMovesBoard, HiringMovesFilter, HiringMoveWeek, HiringPayload, HiringSignalGroup, HiringSignalsBoard, HiringSignalsFilter, HiringSummary, HiringWeekSignal } from "../../../api-client/hiring";
4
4
  import type { CloudJobsPostingDetail } from "../../../api-client/types";
5
5
  import { createPluginCache } from "../../../data/plugin-cache";
6
+ import { createTtlCache } from "../../../data/ttl-cache";
6
7
  import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
7
8
  import type { AppFocus, AttentionKind } from "./model";
8
9
 
@@ -19,7 +20,10 @@ const finite = (value: unknown) => typeof value === "number" && Number.isFinite(
19
20
  const positive = (value: unknown) => finite(value) && (value as number) >= 0;
20
21
  const fraction = (value: unknown) => positive(value) && (value as number) <= 1;
21
22
  const date = (value: unknown) => typeof value === "string" && Number.isFinite(Date.parse(value));
22
- const url = (value: unknown) => { try { return typeof value === "string" && ["https:", "http:"].includes(new URL(value).protocol); } catch { return false; } };
23
+ const url = (value: unknown) => { try { return typeof value === "string" && ["https:", "http:"].includes(new URL(value).protocol); } catch {
24
+ // Intentional ignore: malformed URLs fail this web link field validator.
25
+ return false;
26
+ } };
23
27
  function numericTree(value: unknown): boolean {
24
28
  if (typeof value === "number") return Number.isFinite(value);
25
29
  if (Array.isArray(value)) return value.every(numericTree);
@@ -165,21 +169,16 @@ export async function fetchAppRank(focus: AppFocus, days: number, offset = 0, si
165
169
  export const loadAppRank = (focus: AppFocus, access: string, days: number, force = false) => loadCloudResource(appRankCache, `${access}:${focus.store}:${focus.appId}:${focus.country}:${focus.chart}:${days}`, () => fetchAppRank(focus, days), { force });
166
170
 
167
171
  const POSTING_TTL_MS = 5 * 60_000;
168
- const postingCache = new Map<string, { at: number; posting: Promise<CloudJobsPostingDetail> }>();
172
+ const postings = createTtlCache<CloudJobsPostingDetail>({ ttlMs: POSTING_TTL_MS });
169
173
  /**
170
174
  * The page HIRE's evidence row was read from: its title, place, date, pay,
171
175
  * hours and description. Kept a few minutes, so stepping back and forth
172
176
  * through the evidence does not read it again.
173
177
  */
174
178
  export function fetchPostingDetail(ticker: string, externalId: string, client: Pick<typeof apiClient, "getCloudJobsPosting"> = apiClient): Promise<CloudJobsPostingDetail> {
175
- const key = `${ticker.toUpperCase()}:${externalId}`;
176
- const cached = postingCache.get(key);
177
- if (cached && Date.now() - cached.at < POSTING_TTL_MS) return cached.posting;
178
- const posting = client.getCloudJobsPosting(ticker, { externalId }).then((detail) => {
179
+ return postings.load(`${ticker.toUpperCase()}:${externalId}`, async () => {
180
+ const detail = await client.getCloudJobsPosting(ticker, { externalId });
179
181
  if (!detail || typeof detail.title !== "string" || detail.externalId !== externalId) throw new Error("The server returned an unreadable posting.");
180
182
  return detail;
181
183
  });
182
- postingCache.set(key, { at: Date.now(), posting });
183
- posting.catch(() => postingCache.delete(key));
184
- return posting;
185
184
  }
@@ -45,7 +45,10 @@ export function attentionScreenshotEvidence(kind: AttentionKind): PaneScreenshot
45
45
  read(value) {
46
46
  if (!isRecord(value) || value.kind !== kind || value.version !== 1 || value.complete !== true || !isRecord(value.payload)
47
47
  || !["table", "chart", "mix", "peers", "evidence", ...(kind === "hiring" ? ["moves", "expansion", "anomalies", "building"] : [])].includes(String(value.tab)) || !Array.isArray(value.rowIds) || !value.rowIds.every((id) => typeof id === "string") || value.plottedValueCount !== value.rowIds.length) return null;
48
- try { if (!(kind === "apps" && Array.isArray(value.payload.rankHistory))) validateAttention(kind, value.payload as unknown as AttentionPayload); return value as unknown as AttentionEvidence; } catch { return null; }
48
+ try { if (!(kind === "apps" && Array.isArray(value.payload.rankHistory))) validateAttention(kind, value.payload as unknown as AttentionPayload); return value as unknown as AttentionEvidence; } catch {
49
+ // Intentional ignore: invalid captured observations are rejected as evidence by returning null.
50
+ return null;
51
+ }
49
52
  },
50
53
  mismatches(evidence, { resolved, payload }) {
51
54
  const settings = payload.config.layout.instances.find((entry) => entry.instanceId === payload.paneId)?.settings;
@@ -3,6 +3,7 @@ import type { DataTableColumn } from "../../../components";
3
3
  import type { AppAttentionPayload, AppChart } from "../../../api-client/app-attention";
4
4
  import type { HiringBoard, HiringPayload } from "../../../api-client/hiring";
5
5
  import { fetchAppRank, fetchAttention, fetchBuilding, fetchMoves, fetchSignals, type BuildingPayload, type SignalsPayload } from "./client";
6
+ import { companyDataNotes } from "../../../types/company-from";
6
7
  import { BUILDING_FILTERS, BUILDING_VIEWS, buildingBoardModel, buildingNumber, buildingPreviewNote, companyBuildingModel, isBuildingFilter, type BuildingSection } from "./building-model";
7
8
  import { appsModel, appsPreviewNote, attentionNumber, attentionSectionTitle, attentionTabs, hiringModel, hiringPreviewNote, remoteText, type AttentionKind } from "./model";
8
9
  import { companyMovesModel, isMovesFilter, movesBoardModel, movesNumber, movesPreviewNote, MOVES_FILTERS, MOVES_VIEWS, type MovesSection } from "./moves-model";
@@ -146,7 +147,7 @@ export function attentionHeadless(kind: AttentionKind): HeadlessPaneDefinition<"
146
147
  const why = movesModel?.latest?.why;
147
148
  const company = args.symbols[0] ? signalsReads[0]?.data : undefined;
148
149
  const held = company && !("group" in company) && company.held ? holdText(company.held) : null;
149
- const notes = [...model.notices, ...(nextOffset !== null ? [`Additional observations available with --offset ${nextOffset}.`] : []),
150
+ const notes = [...model.notices, ...companyDataNotes("companyFrom" in data ? data.companyFrom : null), ...(nextOffset !== null ? [`Additional observations available with --offset ${nextOffset}.`] : []),
150
151
  ...(movesData instanceof Error ? [movesData.message] : []), ...(why ? [why] : []),
151
152
  ...(signalsData instanceof Error ? [signalsData.message] : []), ...(held ? [`No expansion or anomaly signal this week: ${held.toLowerCase()}.`] : []),
152
153
  ...(buildingData instanceof Error ? [buildingData.message] : []), ...(buildingHeld ? [`No mix to compare this week: ${buildingHeld.toLowerCase()}.`] : [])];
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../utils/log-failure";
1
2
  import { useCallback, useMemo, useRef, type ReactNode } from "react";
2
3
  import { isAccessDenied } from "../../../api-client/errors";
3
4
  import type { HiringMovesFilter } from "../../../api-client/hiring";
@@ -63,7 +64,10 @@ export function HiringMovesView({ symbol, width, height, focused, accessKey, com
63
64
  try {
64
65
  const payload = snapshotSetting ? validateMoves(snapshotSetting) : null;
65
66
  return payload && ("moves" in payload ? !symbol : payload.symbol.toUpperCase() === symbol?.toUpperCase()) ? payload : null;
66
- } catch { return null; }
67
+ } catch (error) {
68
+ logFailure("company-attention.moves", "Validate hiring moves snapshot failed")(error);
69
+ return null;
70
+ }
67
71
  }, [snapshotSetting, symbol]);
68
72
  const loader = useCallback((force: boolean) => snapshot ? Promise.resolve({ payload: snapshot, stale: false, refreshError: null }) : loadMoves(symbol, accessKey, filter, force), [snapshot, symbol, accessKey, filter]);
69
73
  const resource = useAsyncResource(loader, { initialData: () => cachedMoves(symbol, accessKey, filter), clearOnError: isAccessDenied, keepPreviousData: true });
@@ -1,9 +1,11 @@
1
+ import { logFailure } from "../../../utils/log-failure";
1
2
  import { useCallback, useMemo, useRef } from "react";
2
3
  import { isAccessDenied } from "../../../api-client/errors";
3
4
  import { usePlanAccess } from "../../../api-client/plan-access";
4
5
  import type { AppAttentionFilter, AppAttentionPayload, AppChart } from "../../../api-client/app-attention";
5
6
  import type { HiringBoard, HiringPayload } from "../../../api-client/hiring";
6
7
  import { ChartTableHeader, DataTableView, PageStackView, PaneFooterScope, PaneLinkMenu, QueryBar, useChartTableSelection, usePagedRows, useTableLoadMore, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type PaneHint } from "../../../components";
8
+ import { useCompanyDataFooter } from "../../../components/layout/pane/status-footer";
7
9
  import { DetailReader, type DetailFact } from "../../../components/reader/detail-reader";
8
10
  import type { CloudJobsPostingDetail } from "../../../api-client/types";
9
11
  import { descriptionText, postingFacts, postingTitle } from "../shared/job-posting";
@@ -57,7 +59,10 @@ function Evidence({ row, width, posting, source }: Size & { row: AttentionRow; p
57
59
  }
58
60
  /** A link's site, as a reader names it: "sec.gov". */
59
61
  function linkName(url: string): string {
60
- try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return "Source"; }
62
+ try { return new URL(url).hostname.replace(/^www\./, ""); } catch {
63
+ // Intentional ignore: a malformed link has no site to name, so it reads as Source.
64
+ return "Source";
65
+ }
61
66
  }
62
67
 
63
68
  export function HiringPane(props: Size) {
@@ -105,7 +110,10 @@ function AttentionView({ kind, symbol, width, height, focused, initialTab }: Siz
105
110
  const [mix, setMix] = usePluginPaneState<string>(`${kind}:mix`, kind === "hiring" ? "functions" : "countries");
106
111
  const filters = useMemo<AppAttentionFilter>(() => kind === "apps" ? { ...(country ? { country } : {}), ...(chart ? { chart: chart as AppChart } : {}), days: Number(days) || 90 } : {}, [kind, country, chart, days]);
107
112
  const [snapshotSetting] = usePaneSettingValue<AttentionPayload | null>("attentionSnapshot", null);
108
- const snapshot = useMemo(() => { try { return snapshotSetting ? validateAttention(kind, snapshotSetting) : null; } catch { return null; } }, [kind, snapshotSetting]);
113
+ const snapshot = useMemo(() => { try { return snapshotSetting ? validateAttention(kind, snapshotSetting) : null; } catch (error) {
114
+ logFailure("company-attention.pane", "Validate attention snapshot failed")(error);
115
+ return null;
116
+ } }, [kind, snapshotSetting]);
109
117
  const loader = useCallback((force: boolean) => snapshot ? Promise.resolve({ payload: snapshot, stale: false, refreshError: null }) : loadAttention(kind, symbol, accessKey, filters, force), [kind, symbol, accessKey, filters, snapshot]);
110
118
  const resource = useAsyncResource(loader, { initialData: () => cachedAttention(kind, symbol, accessKey, filters), clearOnError: isAccessDenied });
111
119
  const data = resource.data?.payload;
@@ -177,6 +185,8 @@ function AttentionView({ kind, symbol, width, height, focused, initialTab }: Siz
177
185
  usePaneStatusFooter({ registrationId: `${kind}:status`, enabled: !openCompany && !app && !moves, loading: resource.loading || paged.loadingMore || (!!evidence && postingResource.loading), error: data ? resource.error ?? paged.moreError?.message ?? paged.error?.message : null, stale: resource.data?.stale,
178
186
  info: [...(model?.asOf ? [{ id: "observed", asOf: true, parts: [{ text: `as of ${model.asOf.slice(0, 10)}`, tone: "muted" as const }] }] : []), ...(model?.preview ? [{ id: "preview", parts: [{ text: "Pro preview", tone: "warning" as const }] }] : [])], hints });
179
187
  usePaneNoticeFooter({ registrationId: `${kind}:notices`, enabled: !openCompany && !app && !moves, focused, notices: [...(model?.notices ?? []), ...(resource.data?.refreshError ? [resource.data.refreshError] : [])] });
188
+ // A receipt's or foreign line's hiring or apps are its company's, read from the home line.
189
+ useCompanyDataFooter(data && "companyFrom" in data ? data.companyFrom : null, !openCompany && !app && !moves);
180
190
  if (signInWall) return <SignInWall placement={kind === "hiring" ? "hiring-momentum-signin" : "app-attention-signin"} action={kind === "hiring" ? "view hiring momentum" : "view app attention"} needsVerification={session.needsVerification} />;
181
191
  const countryOptions = data && kind === "apps" ? (data as AppAttentionPayload).coverage.configuredCountries : [];
182
192
  const query = kind === "hiring" && tab !== "mix" ? undefined : <QueryBar width={width} filters={kind === "apps" ? [
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../utils/log-failure";
1
2
  import { useCallback, useMemo, useRef, type ReactNode } from "react";
2
3
  import { isAccessDenied } from "../../../api-client/errors";
3
4
  import type { HiringSignalGroup, HiringSignalsFilter, HiringWeekSignal } from "../../../api-client/hiring";
@@ -60,7 +61,10 @@ export function HiringSignalsView({ group, symbol, width, height, focused, acces
60
61
  try {
61
62
  const payload = snapshotSetting ? validateSignals(snapshotSetting) : null;
62
63
  return payload && ("group" in payload ? !symbol && payload.group === group : payload.symbol.toUpperCase() === symbol?.toUpperCase()) ? payload : null;
63
- } catch { return null; }
64
+ } catch (error) {
65
+ logFailure("company-attention.signals", "Validate hiring signals snapshot failed")(error);
66
+ return null;
67
+ }
64
68
  }, [snapshotSetting, symbol, group]);
65
69
  const loader = useCallback((force: boolean) => snapshot ? Promise.resolve({ payload: snapshot, stale: false, refreshError: null }) : loadSignals(group, symbol, accessKey, filter, force), [snapshot, group, symbol, accessKey, filter]);
66
70
  const resource = useAsyncResource(loader, { initialData: () => cachedSignals(group, symbol, accessKey, filter), clearOnError: isAccessDenied, keepPreviousData: true });
@@ -1,11 +1,12 @@
1
1
  import type { DataTableColumn } from "../../../components";
2
2
  import { DATE_OPTION_EXAMPLES, isoDateOption } from "../../../utils/calendar-date";
3
3
  import { optionFlag } from "../../../cli/pane-functions/capabilities";
4
+ import { companyDataNotes } from "../../../types/company-from";
4
5
  import type { HeadlessBundleSection, HeadlessPaneColumn, HeadlessPaneDefinition } from "../../../types/plugin";
5
6
  import { withoutQuietColumns } from "../shared/research-cells";
6
7
  import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
7
8
  import { companyQuery, fetchCompanyData, type CompanyDataset, type CompanyMode } from "./client";
8
- import { allGuidance, allObservations, companyCellText, companyColumns, companyPreviewNote, KPI_COVERAGE_NOTE, kpiFreeLimitNote, QUIET_COLUMNS,
9
+ import { allGuidance, allObservations, companyCellText, companyColumns, companyEmptyState, companyPreviewNote, KPI_COVERAGE_NOTE, kpiFreeLimitNote, QUIET_COLUMNS,
9
10
  type CompanyRow } from "./model";
10
11
  import type { KpiObservation } from "../../../api-client/company-kpis";
11
12
 
@@ -107,8 +108,12 @@ export function companyHeadless(mode: CompanyMode): HeadlessPaneDefinition<"bund
107
108
  const latest = ("series" in data ? data.series.map((series) => series.latest) : data.guidance).filter(keep);
108
109
  const ignored = data.access === "preview" ? PRO_FILTERS.filter((key) => typeof args.options[key] === "string" && args.options[key] !== "") : [];
109
110
  const notes = [ageNote(data, latest, mode),
110
- ignored.length ? `The free preview is fixed: ${ignored.map(optionFlag).join(", ")} ${ignored.length > 1 ? "need" : "needs"} Gloom Pro.` : null].filter((note): note is string => !!note);
111
- return { complete: !data.truncated, errors: data.truncated ? [data.access === "preview" ? companyPreviewNote(data, mode === "guidance") : "Response limit reached; narrow the metric or date range"] : [],
111
+ ignored.length ? `The free preview is fixed: ${ignored.map(optionFlag).join(", ")} ${ignored.length > 1 ? "need" : "needs"} Gloom Pro.` : null,
112
+ ...companyDataNotes(data.companyFrom)].filter((note): note is string => !!note);
113
+ // A company with nothing on file says so once, as the pane does, rather than "not covered".
114
+ const nothingOnFile = datasetRows(data).length === 0 && data.lockedRows === 0 && !requested;
115
+ const { title, hint } = companyEmptyState(mode, symbol, { tab: "table", hasRows: !nothingOnFile }, "cli");
116
+ return { complete: !data.truncated, ...(nothingOnFile ? { empty: `${title} ${hint}` } : {}), errors: data.truncated ? [data.access === "preview" ? companyPreviewNote(data, mode === "guidance") : "Response limit reached; narrow the metric or date range"] : [],
112
117
  ...(notes.length ? { notes } : {}),
113
118
  sections: "series" in data ? [
114
119
  section("Latest KPIs", data.series.map((series) => series.latest), { first: true }),
@@ -1,9 +1,11 @@
1
1
  import type { GuidancePayload, KpiGuidance, KpiObservation, KpiSeries, KpiUnit, KpisPayload } from "../../../api-client/company-kpis";
2
2
  import type { DataTableColumn } from "../../../components";
3
3
  import { scalarPoint, staticSeries } from "../../../components/chart/static/series";
4
+ import { getTableWidth, tableColumnWidth } from "../../../components/ui/table-layout";
4
5
  import type { CompositeChartXAxis } from "../../../components/chart/composite/types";
5
6
  import { formatCompact, formatNumber } from "../../../utils/format";
6
7
  import { freePreviewNote } from "../shared/free-preview";
8
+ import type { CompanyMode } from "./client";
7
9
 
8
10
  export type CompanyRow = KpiObservation | KpiGuidance;
9
11
  export const isGuidance = (row: CompanyRow): row is KpiGuidance => "issuedDate" in row;
@@ -89,13 +91,30 @@ function unitSuffix(row: Pick<CompanyRow, "unit" | "currency" | "dimensions">):
89
91
  if (row.unit === "currency_per_unit" || row.unit === "volume") return ` ${unitLabel(row)}`;
90
92
  return "";
91
93
  }
92
- /** Raised reads as good news, a cut as bad, a reiteration as no news. */
93
- export const directionTone = (direction: KpiGuidance["direction"]) => direction === "raised" ? "positive" as const : direction === "cut" ? "negative" as const
94
- : direction === "withdrawn" ? "warning" as const : direction === "initiated" ? "text" as const : "muted" as const;
94
+ /** Raised reads as good news, a cut as bad, a reiteration as no news, and a New guide as itself whichever way the service reached it. */
95
+ export const directionTone = (guide: Pick<KpiGuidance, "direction" | "previousId">) => guide.direction === "raised" ? "positive" as const : guide.direction === "cut" ? "negative" as const
96
+ : guide.direction === "withdrawn" ? "warning" as const : directionLabel(guide) === "New" ? "text" as const : "muted" as const;
95
97
  /** A change is good when it moves the way the metric's owner wants it to. */
96
98
  export const changeTone = (row: KpiObservation, value: number | null) => value === null || value === 0 || row.metric.favorable === "neutral" ? "muted" as const
97
99
  : (value > 0) === (row.metric.favorable === "higher") ? "positive" as const : "negative" as const;
98
- export const directionLabel = (direction: KpiGuidance["direction"]) => direction === "not_comparable" ? "Not comparable" : direction.charAt(0).toUpperCase() + direction.slice(1);
100
+ /**
101
+ * What a guide did against the one before it for the same period, in plain
102
+ * words. A guide with no earlier one to set it against is New, whether it is
103
+ * the company's first or the first for its period. The service sets a guide it
104
+ * cannot rank (a different hedge or conditions) beside its predecessor as not
105
+ * comparable: that one Changed, with no word for up or down.
106
+ */
107
+ export function directionLabel(guide: Pick<KpiGuidance, "direction" | "previousId">): string {
108
+ switch (guide.direction) {
109
+ case "raised": return "Raised";
110
+ case "cut": return "Lowered";
111
+ case "reiterated": return "Maintained";
112
+ case "mixed": return "Mixed";
113
+ case "withdrawn": return "Withdrawn";
114
+ case "initiated": return "New";
115
+ case "not_comparable": return guide.previousId ? "Changed" : "New";
116
+ }
117
+ }
99
118
  export const outcomeLabel = (row: KpiGuidance) => !row.actual ? "Pending" : row.actual.favorable === "in_line" ? "In range"
100
119
  : row.actual.favorable === "neutral" ? row.actual.outcome === "above" ? "Above" : row.actual.outcome === "below" ? "Below" : "In range"
101
120
  : row.actual.favorable === "beat" ? "Beat" : "Miss";
@@ -163,7 +182,7 @@ export const QUIET_COLUMNS = { basis: ["Reported"], scope: ["Consolidated"] };
163
182
  /** The text of a table cell as the pane draws it; the report prints the same words. */
164
183
  export function companyCellText(row: CompanyRow, column: string, observations: KpiObservation[]): string {
165
184
  const guide = isGuidance(row) ? row : null;
166
- if (column === "change") return guide ? directionLabel(guide.direction) : changeText(observationChange(row as KpiObservation, observations));
185
+ if (column === "change") return guide ? directionLabel(guide) : changeText(observationChange(row as KpiObservation, observations));
167
186
  return column === "metric" ? row.metric.name : column === "period" ? fiscalLabel(row) : column === "value" ? `${guide ? rangeText(guide) : observationValue(row as KpiObservation)}${guide?.status === "withdrawn" || guide?.hedge === "qualitative" ? "" : unitSuffix(row)}`
168
187
  : column === "unit" ? unitLabel(row)
169
188
  : column === "basis" ? basisLabel(row.basis) : column === "scope" ? dimensionLabel(row.dimensions) || "Consolidated"
@@ -195,6 +214,55 @@ export function companyColumns(guidance: boolean, evidence: boolean, history: bo
195
214
  return order ? shown.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id)) : shown;
196
215
  }
197
216
 
217
+ /** Cells a column gives up on a narrow pane, before any column is dropped: a date needs 10, a range about 19. */
218
+ const NARROW_WIDTHS: Record<string, number> = { value: 19, change: 11, issued: 10, actual: 12, outcome: 9, basis: 12, scope: 18 };
219
+ /** The metric keeps at least this much: a name such as "Revenue growth", and its series detail only when there is room. */
220
+ const METRIC_FLOOR = 14;
221
+
222
+ /**
223
+ * The table's columns at a pane `width` in cells, so none is cut at the right edge.
224
+ * A table that already fits is left as it is. Otherwise the columns take their
225
+ * narrow widths, and the metric, which flexes, keeps what is left down to its
226
+ * floor. Below that a column whose meaning is in the detail goes (Action in History,
227
+ * where the outcome says it, then Issued), never one that names the row. A table that
228
+ * cannot fit even so keeps its narrow widths and scrolls sideways, as it always has.
229
+ */
230
+ export function fitCompanyColumns(columns: readonly DataTableColumn[], width: number, history = false): DataTableColumn[] {
231
+ const room = width - 1;
232
+ if (getTableWidth(columns) <= room) return [...columns];
233
+ const narrow = (list: readonly DataTableColumn[]) => list.map((column) => column.id in NARROW_WIDTHS ? { ...column, width: Math.min(column.width, NARROW_WIDTHS[column.id]!) } : column);
234
+ const attempt = (list: readonly DataTableColumn[]): DataTableColumn[] | null => {
235
+ const tight = narrow(list);
236
+ const metric = tight.find((column) => column.id === "metric");
237
+ if (!metric) return getTableWidth(tight) <= room ? tight : null;
238
+ const fitted = Math.min(metric.width, room - (getTableWidth(tight) - tableColumnWidth(metric)));
239
+ return fitted >= METRIC_FLOOR ? tight.map((column) => column === metric ? { ...column, width: fitted } : column) : null;
240
+ };
241
+ let shown = [...columns];
242
+ for (const drop of [null, ...(history ? ["change", "issued"] : ["issued"])]) {
243
+ if (drop) shown = shown.filter((column) => column.id !== drop);
244
+ const fitted = attempt(shown);
245
+ if (fitted) return fitted;
246
+ }
247
+ return narrow(columns);
248
+ }
249
+
250
+ /** Companies to point an empty pane at, one that has data and one for when the pane is for the first. */
251
+ const EXAMPLE_COMPANIES: Record<CompanyMode, readonly string[]> = { guidance: ["WMT", "DAL"], kpis: ["AAPL", "NVDA"] };
252
+
253
+ /**
254
+ * What an empty table says. A company with nothing on file for the function says so
255
+ * plainly and names one that has some; a selection or tab that narrows what the company
256
+ * has says what is missing from it. `cli` types `fn GUIDE WMT`, the app's command bar `GUIDE WMT`.
257
+ */
258
+ export function companyEmptyState(mode: CompanyMode, symbol: string, view: { tab: string; hasRows: boolean }, surface: "cli" | "pane" = "pane"): { title: string; hint?: string } {
259
+ if (view.hasRows) return { title: view.tab === "history" ? "Actuals have not been matched to these guidance periods yet." : "No disclosed observations for this selection yet." };
260
+ const examples = EXAMPLE_COMPANIES[mode];
261
+ const example = examples.find((candidate) => candidate !== symbol.trim().toUpperCase()) ?? examples[0]!;
262
+ const command = `${mode === "kpis" ? "KPIS" : "GUIDE"} ${example}`;
263
+ return { title: `No ${mode === "kpis" ? "KPIs" : "guidance"} on file for ${symbol}.`, hint: `Try ${surface === "cli" ? `\`fn ${command}\`` : command}.` };
264
+ }
265
+
198
266
  /** What a missing KPI means, as KPIS states it among its limitations and EQS beside a KPI column with gaps. */
199
267
  export const KPI_COVERAGE_NOTE = "Coverage follows available company disclosures; missing data is not zero.";
200
268