gloomberb 0.19.0 → 0.19.2

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 (304) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +2 -1
  4. package/patches/electrobun@1.18.1.patch +23 -1
  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/data.ts +2 -1
  9. package/src/api-client/errors.ts +38 -0
  10. package/src/api-client/hiring.ts +3 -0
  11. package/src/api-client/index.ts +4 -0
  12. package/src/api-client/ipo.ts +24 -1
  13. package/src/api-client/mna.ts +3 -0
  14. package/src/api-client/paths.ts +9 -1
  15. package/src/api-client/roadmap.ts +85 -0
  16. package/src/api-client/types.ts +12 -0
  17. package/src/app/runtime/startup.ts +3 -1
  18. package/src/architecture/core-builtin-allowlist.json +2 -2
  19. package/src/capabilities/chart-series.ts +2 -1
  20. package/src/cli/commands/market.ts +44 -16
  21. package/src/cli/commands/news-search.ts +22 -0
  22. package/src/cli/commands/overview.ts +84 -19
  23. package/src/cli/commands/search.ts +13 -13
  24. package/src/cli/commands/ticker.ts +32 -2
  25. package/src/cli/context.ts +1 -0
  26. package/src/cli/errors.ts +17 -2
  27. package/src/cli/glossary.ts +8 -1
  28. package/src/cli/history-rows.ts +35 -7
  29. package/src/cli/index-roots.ts +0 -7
  30. package/src/cli/known-gaps.ts +11 -0
  31. package/src/cli/listing-arg.ts +74 -11
  32. package/src/cli/pane-functions/capabilities.ts +1 -0
  33. package/src/cli/pane-functions/discovery.ts +21 -1
  34. package/src/cli/pane-functions/freshness.ts +7 -1
  35. package/src/cli/pane-functions/function-help.ts +7 -0
  36. package/src/cli/pane-functions/index.ts +6 -3
  37. package/src/cli/quote-answer.ts +30 -7
  38. package/src/cli/saved-copy-persistence.ts +88 -0
  39. package/src/components/command-bar/list/model.ts +5 -0
  40. package/src/components/command-bar/routes/root/runtime.ts +29 -8
  41. package/src/components/command-bar/selection-runtime.ts +64 -32
  42. package/src/components/command-bar/surface/index.tsx +2 -0
  43. package/src/components/layout/detached-pane-shell.tsx +2 -2
  44. package/src/components/layout/header.tsx +2 -2
  45. package/src/components/layout/pane/capture-stamp.ts +2 -2
  46. package/src/components/layout/pane/footer/index.tsx +23 -6
  47. package/src/components/layout/pane/footer/model.ts +116 -16
  48. package/src/components/layout/pane/header.tsx +2 -2
  49. package/src/components/layout/pane/status-footer.ts +37 -3
  50. package/src/components/layout/status-widget-space.ts +9 -0
  51. package/src/components/onboarding/onboarding-frame.tsx +2 -1
  52. package/src/components/onboarding/onboarding-wizard.tsx +6 -3
  53. package/src/components/ticker/list-table-view.tsx +4 -20
  54. package/src/components/ui/display.tsx +29 -3
  55. package/src/components/ui/icon.tsx +12 -3
  56. package/src/core/app-services.ts +11 -2
  57. package/src/data/app-persistence.ts +9 -7
  58. package/src/data/plugin-cache.ts +20 -3
  59. package/src/data/plugin-state-store.ts +81 -24
  60. package/src/data/resource-store.ts +130 -58
  61. package/src/data/saved-copies.ts +8 -0
  62. package/src/data/session-store.ts +31 -8
  63. package/src/data/sqlite/access.ts +313 -0
  64. package/src/data/sqlite/checkpointer.ts +198 -0
  65. package/src/data/sqlite/database.ts +47 -4
  66. package/src/data/sqlite/retry.ts +50 -9
  67. package/src/data/ticker-store.ts +57 -23
  68. package/src/docs/snapshot.json +32 -5
  69. package/src/i18n/es.ts +2 -4
  70. package/src/i18n/ja.ts +2 -4
  71. package/src/i18n/ko.ts +2 -4
  72. package/src/i18n/zh-cn.ts +2 -4
  73. package/src/i18n/zh-tw.ts +2 -4
  74. package/src/market-data/depositary-pair.ts +25 -1
  75. package/src/market-data/quotes/metadata.ts +7 -0
  76. package/src/market-data/realized-volatility.ts +4 -2
  77. package/src/news/aggregator.ts +7 -1
  78. package/src/news/types.ts +6 -0
  79. package/src/plugins/auto-update-node.ts +10 -6
  80. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  81. package/src/plugins/builtin/alerts/cli.ts +34 -11
  82. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  83. package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
  84. package/src/plugins/builtin/alerts/headless.ts +10 -6
  85. package/src/plugins/builtin/cds/client.ts +142 -30
  86. package/src/plugins/builtin/cds/headless.ts +35 -8
  87. package/src/plugins/builtin/cds/model.ts +145 -18
  88. package/src/plugins/builtin/cds/pane.tsx +16 -5
  89. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  90. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  91. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  92. package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
  93. package/src/plugins/builtin/chart-composer/index.tsx +4 -5
  94. package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
  95. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  96. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  97. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  98. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  99. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  100. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  101. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  102. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  103. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  104. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  105. package/src/plugins/builtin/clinical-trials/model.ts +86 -22
  106. package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
  107. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  108. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
  109. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  110. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  111. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  112. package/src/plugins/builtin/company-attention/pane.tsx +3 -0
  113. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  114. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  115. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  116. package/src/plugins/builtin/composite-plugins.ts +2 -1
  117. package/src/plugins/builtin/cot/client.ts +28 -2
  118. package/src/plugins/builtin/cot/headless.ts +4 -1
  119. package/src/plugins/builtin/cot/index.tsx +4 -2
  120. package/src/plugins/builtin/cot/model.ts +58 -7
  121. package/src/plugins/builtin/cot/pane.tsx +6 -2
  122. package/src/plugins/builtin/cpi/client.ts +6 -5
  123. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  124. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  125. package/src/plugins/builtin/credit-documents/headless.ts +3 -3
  126. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  127. package/src/plugins/builtin/crypto-board/series.ts +4 -4
  128. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  129. package/src/plugins/builtin/doe/client.ts +6 -5
  130. package/src/plugins/builtin/doe/headless.ts +3 -2
  131. package/src/plugins/builtin/doe/pane.tsx +13 -3
  132. package/src/plugins/builtin/doe/prices.ts +2 -3
  133. package/src/plugins/builtin/doe/series.ts +4 -4
  134. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  135. package/src/plugins/builtin/earnings-calls/data.ts +37 -17
  136. package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
  137. package/src/plugins/builtin/earnings-calls/pane.tsx +34 -14
  138. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  139. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  140. package/src/plugins/builtin/econ/detail-view.tsx +18 -4
  141. package/src/plugins/builtin/econ/figures.ts +48 -0
  142. package/src/plugins/builtin/econ/index.tsx +14 -5
  143. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  144. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  145. package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
  146. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  147. package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
  148. package/src/plugins/builtin/executives/data.ts +6 -1
  149. package/src/plugins/builtin/executives/pane.tsx +9 -21
  150. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  151. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  152. package/src/plugins/builtin/gpu/client.ts +4 -1
  153. package/src/plugins/builtin/gpu/series.ts +2 -3
  154. package/src/plugins/builtin/help/index.tsx +61 -59
  155. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  156. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  157. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  158. package/src/plugins/builtin/holders/beneficial-view.tsx +4 -2
  159. package/src/plugins/builtin/holders/headless.ts +4 -2
  160. package/src/plugins/builtin/holders/pane.tsx +3 -0
  161. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  162. package/src/plugins/builtin/insider/headless.ts +2 -2
  163. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  164. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  165. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  166. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  167. package/src/plugins/builtin/jodi/client.ts +4 -1
  168. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  169. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  170. package/src/plugins/builtin/macro-day/model.ts +39 -5
  171. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  172. package/src/plugins/builtin/market-movers/client.ts +3 -0
  173. package/src/plugins/builtin/market-movers/headless.ts +3 -1
  174. package/src/plugins/builtin/market-movers/index.tsx +10 -5
  175. package/src/plugins/builtin/market-movers/screener.ts +27 -6
  176. package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
  177. package/src/plugins/builtin/mna/headless.ts +2 -0
  178. package/src/plugins/builtin/mna/pane.tsx +3 -1
  179. package/src/plugins/builtin/money-markets/series.ts +3 -5
  180. package/src/plugins/builtin/news/headless.ts +3 -1
  181. package/src/plugins/builtin/news/index.tsx +3 -0
  182. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
  183. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  184. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  185. package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
  186. package/src/plugins/builtin/power/gas-client.ts +2 -1
  187. package/src/plugins/builtin/power/prices-client.ts +3 -2
  188. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  189. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  190. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  191. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  192. package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
  193. package/src/plugins/builtin/research/event-model.ts +30 -4
  194. package/src/plugins/builtin/research/index.tsx +7 -3
  195. package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
  196. package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
  197. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  198. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  199. package/src/plugins/builtin/risk-factors/data.ts +12 -6
  200. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  201. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  202. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  203. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  204. package/src/plugins/builtin/roadmap/model.ts +67 -0
  205. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  206. package/src/plugins/builtin/scanner/live-headless.ts +21 -2
  207. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  208. package/src/plugins/builtin/sec/headless.ts +2 -1
  209. package/src/plugins/builtin/sec/index.tsx +3 -0
  210. package/src/plugins/builtin/shared/cloud-resource.ts +20 -7
  211. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  212. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  213. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  214. package/src/plugins/builtin/shared/report-freshness.ts +8 -0
  215. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  216. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  217. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  218. package/src/plugins/builtin/short-volume/series.ts +2 -3
  219. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  220. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  221. package/src/plugins/builtin/thirteenf/api.ts +15 -9
  222. package/src/plugins/builtin/thirteenf/data.ts +39 -11
  223. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  224. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  225. package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
  226. package/src/plugins/builtin/thirteenf/types.ts +2 -0
  227. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
  228. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  229. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  230. package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
  231. package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
  232. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  233. package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
  234. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  235. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  236. package/src/plugins/builtin/treasury-auctions/headless.ts +24 -13
  237. package/src/plugins/builtin/treasury-auctions/pane.tsx +18 -6
  238. package/src/plugins/builtin/volatility/client.ts +43 -16
  239. package/src/plugins/builtin/volatility/headless.ts +5 -2
  240. package/src/plugins/builtin/volatility/model.ts +17 -6
  241. package/src/plugins/builtin/volatility/pane.tsx +11 -4
  242. package/src/plugins/builtin/volatility/series.ts +2 -1
  243. package/src/plugins/builtin/yield-curve/client.ts +59 -1
  244. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  245. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  246. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  247. package/src/plugins/catalog-browser.ts +2 -1
  248. package/src/plugins/host-link.ts +1 -1
  249. package/src/plugins/installer.ts +50 -39
  250. package/src/portfolio/column-values.ts +11 -7
  251. package/src/renderers/dom/desktop/icons.tsx +10 -2
  252. package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
  253. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  254. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  255. package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
  256. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  257. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  258. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  259. package/src/renderers/electrobun/bun/index.ts +26 -2
  260. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  261. package/src/renderers/electrobun/bun/window/create.ts +45 -4
  262. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  263. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  264. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  265. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  266. package/src/renderers/electrobun/launcher/preload.ts +13 -0
  267. package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
  268. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  269. package/src/renderers/electrobun/view/news-source.ts +2 -0
  270. package/src/renderers/electrobun/view/ui-host.tsx +16 -1
  271. package/src/series/resolve.ts +2 -1
  272. package/src/series/types.ts +5 -0
  273. package/src/sources/gloomberb-cloud/fred-series.ts +8 -4
  274. package/src/sources/gloomberb-cloud/index.ts +42 -8
  275. package/src/telemetry/stall-reports-node.ts +13 -2
  276. package/src/telemetry/stall-reports.ts +189 -13
  277. package/src/test-support/outage.ts +8 -0
  278. package/src/tickers/company-name-listing.ts +110 -40
  279. package/src/tickers/company-name.ts +71 -2
  280. package/src/tickers/index-roots.ts +7 -0
  281. package/src/tickers/listing-fate.ts +2 -2
  282. package/src/tickers/search/home-listing.ts +93 -8
  283. package/src/tickers/search/index.ts +7 -3
  284. package/src/tickers/search/ranking.ts +5 -1
  285. package/src/tickers/search/upsert.ts +13 -0
  286. package/src/tickers/ticker-changes.ts +8 -4
  287. package/src/time-series/market.ts +5 -3
  288. package/src/time-series/price-comparison.ts +20 -2
  289. package/src/time-series/resolve.ts +20 -1
  290. package/src/time-series/types.ts +7 -0
  291. package/src/types/company-from.ts +40 -0
  292. package/src/types/data-provider.ts +7 -0
  293. package/src/types/financials.ts +45 -3
  294. package/src/types/headless.ts +6 -0
  295. package/src/ui/host.tsx +6 -1
  296. package/src/utils/crypto-pair.ts +121 -7
  297. package/src/utils/declared-dividends.ts +1 -1
  298. package/src/utils/financial-statements.ts +73 -16
  299. package/src/utils/fundamentals.ts +53 -10
  300. package/src/utils/lease-liabilities.ts +54 -0
  301. package/src/utils/main-thread-stall.ts +47 -2
  302. package/src/utils/perf-marks.ts +23 -11
  303. package/src/utils/price-history-integrity.ts +47 -1
  304. package/src/version.ts +2 -2
@@ -7,16 +7,19 @@ import {
7
7
  type OrganizationTicker,
8
8
  } from "../../../api-client/organization-tickers";
9
9
  import { createTtlCache, type TtlCache } from "../../../data/ttl-cache";
10
+ import { findSponsorListings, type ListingFinder } from "./sponsor-listing";
10
11
 
11
12
  /**
12
- * A lead sponsor's ticker, only ever as Gloom Cloud resolved it. `none` is an
13
- * answer (no listed company, or a name that fits several); `failed` means it
14
- * 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.
15
18
  */
16
19
  export type SponsorTicker =
17
20
  | { kind: "pending" }
18
21
  | { kind: "listed"; ticker: string; exchange: string | null; entityName: string | null }
19
- | { kind: "none"; entityName: string | null; ambiguous: boolean }
22
+ | { kind: "none"; entityName: string | null; ambiguous: boolean; listingSearch?: "done" }
20
23
  | { kind: "failed"; reason: string };
21
24
 
22
25
  /** A ticker only when one company and its listing were found; an ambiguous name never gets one. */
@@ -50,6 +53,8 @@ export class SponsorTickerStore {
50
53
  private readonly answers: TtlCache<SponsorTicker>;
51
54
  private readonly failures = new Map<string, string>();
52
55
  private readonly inFlight = new Set<string>();
56
+ private readonly searching = new Set<string>();
57
+ private readonly searchFailures = new Set<string>();
53
58
  private readonly listeners = new Set<() => void>();
54
59
 
55
60
  constructor(private readonly lookup: Lookup, now: () => number = Date.now) {
@@ -104,24 +109,94 @@ export class SponsorTickerStore {
104
109
  }));
105
110
  }
106
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
+
107
175
  /** Forgets failed lookups, so the next `load` asks again. */
108
176
  retryFailed(): void {
109
- if (this.failures.size === 0) return;
177
+ if (this.failures.size === 0 && this.searchFailures.size === 0) return;
110
178
  this.failures.clear();
179
+ this.searchFailures.clear();
111
180
  this.listeners.forEach((listener) => listener());
112
181
  }
113
182
  }
114
183
 
115
- const sponsorTickerStore = new SponsorTickerStore(async (names, signal) => (
184
+ export const sponsorTickerStore = new SponsorTickerStore(async (names, signal) => (
116
185
  validateOrganizationTickers(await apiClient.getCloudOrganizationTickers(names, signal), names)
117
186
  ));
118
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
+
119
194
  /**
120
195
  * The tickers for the sponsors on screen. Rows draw at once and tickers fill
121
196
  * in when the lookup answers; `retry` asks again for the ones that failed.
122
197
  * `sessionKey` changing (a sign-in) retries them too.
123
198
  */
124
- export function useSponsorTickers(names: readonly string[], sessionKey: string, store: SponsorTickerStore = sponsorTickerStore) {
199
+ export function useSponsorTickers(names: readonly string[], sessionKey: string, { companies, store = sponsorTickerStore }: SponsorTickersOptions = {}) {
125
200
  const [version, setVersion] = useState(0);
126
201
  const [epoch, setEpoch] = useState(0);
127
202
  const [signal, setSignal] = useState<AbortSignal | null>(null);
@@ -136,10 +211,13 @@ export function useSponsorTickers(names: readonly string[], sessionKey: string,
136
211
  store.retryFailed();
137
212
  }, [sessionKey, store]);
138
213
  const namesKey = names.join("\n");
214
+ const companiesKey = (companies ?? []).join("\n");
139
215
  useEffect(() => {
140
216
  if (!signal || signal.aborted || !namesKey) return;
141
- void store.load(namesKey.split("\n"), signal);
142
- }, [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]);
143
221
  const retry = useCallback(() => {
144
222
  store.retryFailed();
145
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
+ }
@@ -1,4 +1,5 @@
1
1
  import { apiClient } from "../../../api-client";
2
+ import { sourceDidNotAnswer } from "../../../api-client/errors";
2
3
  import type { PsdBoardPayload } from "../../../api-client/commodity-balances";
3
4
  import { unavailableOnServer } from "../shared/cloud-resource";
4
5
  import type { BalanceRow, PriceForecast } from "./model";
@@ -37,7 +38,9 @@ function readBoard(payload: PsdBoardPayload): CommodityBalances {
37
38
  || (payload.marketYear != null && !text(payload.marketYear))) {
38
39
  throw new Error("The server returned an invalid crop balance board");
39
40
  }
40
- if (payload.status === "unavailable" || payload.rows.length === 0) throw new Error(CROPS_NOT_AVAILABLE);
41
+ // The server's source did not answer this time, which may pass; a board with no rows has none yet.
42
+ if (payload.status === "unavailable") throw sourceDidNotAnswer(CROPS_NOT_AVAILABLE);
43
+ if (payload.rows.length === 0) throw new Error(CROPS_NOT_AVAILABLE);
41
44
  return {
42
45
  rows: payload.rows,
43
46
  prices: payload.prices,
@@ -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()}.`] : [])];
@@ -5,6 +5,7 @@ import { usePlanAccess } from "../../../api-client/plan-access";
5
5
  import type { AppAttentionFilter, AppAttentionPayload, AppChart } from "../../../api-client/app-attention";
6
6
  import type { HiringBoard, HiringPayload } from "../../../api-client/hiring";
7
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";
8
9
  import { DetailReader, type DetailFact } from "../../../components/reader/detail-reader";
9
10
  import type { CloudJobsPostingDetail } from "../../../api-client/types";
10
11
  import { descriptionText, postingFacts, postingTitle } from "../shared/job-posting";
@@ -184,6 +185,8 @@ function AttentionView({ kind, symbol, width, height, focused, initialTab }: Siz
184
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,
185
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 });
186
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);
187
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} />;
188
191
  const countryOptions = data && kind === "apps" ? (data as AppAttentionPayload).coverage.configuredCountries : [];
189
192
  const query = kind === "hiring" && tab !== "mix" ? undefined : <QueryBar width={width} filters={kind === "apps" ? [
@@ -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
 
@@ -5,6 +5,7 @@ import type { KpiObservation } from "../../../api-client/company-kpis";
5
5
  import { listingAbroad } from "../../../api-client/paths";
6
6
  import { ChartTableHeader, DataTableStackView, DetailScrollBody, EmptyState, KeyValueRow, PaneStatusBody, QueryBar, SectionHeading,
7
7
  useChartTableSelection, usePaneMenuItems, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableCell, type PaneHint } from "../../../components";
8
+ import { useCompanyDataFooter } from "../../../components/layout/pane/status-footer";
8
9
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
9
10
  import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "../../../public/react";
10
11
  import { usePaneTickerIdentity } from "../../../state/hooks/pane-ticker";
@@ -20,7 +21,7 @@ import { Blurred, LockedOverlay, UpgradeLabel } from "../shared/locked-rows";
20
21
  import { isCloudSessionRequired, useResearchCloudSession } from "../shared/research-cloud-session";
21
22
  import { cachedCompanyData, loadCompanyData, KPI_UNAVAILABLE, validateCompanyData, type CompanyDataset, type CompanyMode } from "./client";
22
23
  import { useCompanyEvidence } from "./screenshot-evidence";
23
- import { allGuidance, allObservations, basisLabel, changeText, changeTone, companyCellText, companyColumns, dimensionLabel, directionLabel, directionTone, guideGroupKey, guidanceSeries, fiscalLabel,
24
+ import { allGuidance, allObservations, basisLabel, changeText, changeTone, companyCellText, companyColumns, companyEmptyState, dimensionLabel, directionLabel, directionTone, fitCompanyColumns, guideGroupKey, guidanceSeries, fiscalLabel,
24
25
  isGuidance, exactObservation, observationValue, observationChange, observationChart, periodOrder, outcomeLabel, QUIET_COLUMNS, rangeText, scopeWords, seriesDetail, seriesLabel, seriesLabels, seriesName,
25
26
  seriesOrder, unitLabel, valueText, companyPreviewNote, type CompanyRow } from "./model";
26
27
  import { PriceSparkline } from "../../../components/price-sparkline/view";
@@ -47,7 +48,7 @@ function DisclosureEvidence({ row, sourceId, onSource, width }: { row: CompanyRo
47
48
  <KeyValueRow label="Revision" value={`${row.revision}${row.current ? " · current" : " · superseded"}${!isGuidance(row) && (row.conflict || row.contested) ? " · conflicting disclosures" : ""}`} labelWidth={18} />
48
49
  {row.revisionReason ? <KeyValueRow label="Correction" value={row.revisionReason} labelWidth={18} /> : null}
49
50
  {isGuidance(row) ? <>
50
- <KeyValueRow label="Issued" value={row.issuedDate} detail={directionLabel(row.direction)} labelWidth={18} />
51
+ <KeyValueRow label="Issued" value={row.issuedDate} detail={directionLabel(row)} labelWidth={18} />
51
52
  <KeyValueRow label="Source range" value={row.rangeText} labelWidth={18} />
52
53
  {row.conditions ? <KeyValueRow label="Conditions" value={row.conditions} labelWidth={18} /> : null}
53
54
  <KeyValueRow label="Later actual" value={row.actual ? valueText(row, row.actual.value) : "Pending"} detail={row.actual ? outcomeLabel(row) : undefined} labelWidth={18} />
@@ -130,7 +131,7 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
130
131
  : sort.column === "actual" ? guide?.actual?.value ?? null : sort.column === "outcome" ? guide ? outcomeLabel(guide) : null
131
132
  : sort.column === "basis" ? row.basis : sort.column === "unit" ? unitLabel(row) : sort.column === "scope" ? dimensionLabel(row.dimensions)
132
133
  : sort.column === "published" ? row.evidence[0]!.publishedAt : sort.column === "revision" ? row.revision
133
- : sort.column === "quote" ? row.evidence[0]!.quote : sort.column === "change" ? guide ? guide.direction : observationChange(row as KpiObservation, observations).value
134
+ : sort.column === "quote" ? row.evidence[0]!.quote : sort.column === "change" ? guide ? directionLabel(guide) : observationChange(row as KpiObservation, observations).value
134
135
  : periodOrder(row);
135
136
  };
136
137
  const direction = sort.direction === "asc" ? 1 : -1;
@@ -158,8 +159,13 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
158
159
  }
159
160
  return map;
160
161
  }, [kpis]);
161
- const columns = useMemo(() => withoutQuietColumns(companyColumns(mode === "guidance", tab === "evidence", tab === "history", mode === "kpis" && tab === "table" && trends.size > 0 && width >= 100, desktop),
162
- rows, (row, id) => id === "basis" ? basisLabel(row.basis) : id === "scope" ? dimensionLabel(row.dimensions) || "Consolidated" : "", QUIET_COLUMNS), [mode, tab, trends, rows, width, desktop]);
162
+ const columns = useMemo(() => {
163
+ const shown = withoutQuietColumns(companyColumns(mode === "guidance", tab === "evidence", tab === "history", mode === "kpis" && tab === "table" && trends.size > 0 && width >= 100, desktop),
164
+ rows, (row, id) => id === "basis" ? basisLabel(row.basis) : id === "scope" ? dimensionLabel(row.dimensions) || "Consolidated" : "", QUIET_COLUMNS);
165
+ // Evidence is a wide ledger that scrolls; the other tabs fit the pane.
166
+ return tab === "evidence" ? shown : fitCompanyColumns(shown, width, tab === "history");
167
+ }, [mode, tab, trends, rows, width, desktop]);
168
+ const emptyState = companyEmptyState(mode, symbol ?? "", { tab, hasRows: allRows.length > 0 });
163
169
  const metricOptions = useMemo(() => [{ value: "all", label: "All metrics" }, ...[...new Map(allRows.map((row) => [row.metricId, { value: row.metricId, label: row.metric.name }])).values()]], [allRows]);
164
170
  const kpiChart = useMemo(() => observationChart(rows.filter((row): row is KpiObservation => !isGuidance(row)), colors.warning), [rows, colors.warning]);
165
171
  const series = useMemo(() => tab !== "chart" ? [] : mode === "kpis" ? kpiChart.series
@@ -190,6 +196,8 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
190
196
  usePaneNoticeFooter({ registrationId: "company-kpis:notices", focused, notices: [resource.data?.refreshError,
191
197
  data?.truncated && data.access === "full" ? "The response reached its stored-record limit. Narrow the metric or date range in a CLI or REST query." : null,
192
198
  data?.coverage.conflicts ? "Conflicting disclosures are retained in Evidence and excluded from comparable trends." : null].filter((value): value is string => !!value) });
199
+ // A receipt's or foreign line's disclosures are its company's, read from the home line.
200
+ useCompanyDataFooter(data?.companyFrom);
193
201
  if (!symbol) return <EmptyState title="Select a ticker." />;
194
202
  if (signInWall) return <SignInWall placement="company-kpis-signin" action={mode === "guidance" ? "view company guidance" : "view company KPIs"} needsVerification={session.needsVerification} />;
195
203
  const bodyHeight = Math.max(3, height - tabRows);
@@ -212,7 +220,7 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
212
220
  return { text: "", content: <PriceSparkline priceHistory={points} width={column.width} period="all" trend={tone === "muted" ? "neutral" : tone} /> };
213
221
  }
214
222
  if (column.id === "change") {
215
- if (guide) return { text: directionLabel(guide.direction), color: toneColor(directionTone(guide.direction), colors), keepColorWhenSelected: guide.direction === "raised" || guide.direction === "cut" };
223
+ if (guide) return { text: directionLabel(guide), color: toneColor(directionTone(guide), colors), keepColorWhenSelected: guide.direction === "raised" || guide.direction === "cut" };
216
224
  const change = observationChange(row as KpiObservation, observations);
217
225
  return { text: changeText(change), value: change.value, color: toneColor(changeTone(row as KpiObservation, change.value), colors) };
218
226
  }
@@ -244,7 +252,7 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
244
252
  detailScrollRef={detailScrollRef} detailContent={openRow ? <DetailScrollBody ref={detailScrollRef} resetScrollKey={openRow.id}><DisclosureEvidence row={openRow} sourceId={sourceId} onSource={setSource} width={width} /></DetailScrollBody> : null}
245
253
  sortColumnId={sort.column} sortDirection={sort.direction} onHeaderClick={(column) => setSort((previous) => ({ column, direction: previous.column === column && previous.direction === "desc" ? "asc" : "desc" }))}
246
254
  resetScrollKey={`${mode}:${symbol}:${tab}:${metric}:${activePlot}`}
247
- emptyStateTitle={tab === "history" ? "Actuals have not been matched to these guidance periods yet." : "No disclosed observations for this selection yet."}
255
+ emptyStateTitle={emptyState.title} emptyStateHint={emptyState.hint}
248
256
  rootBefore={<ChartTableHeader width={width} height={bodyHeight} tableRows={items.length} tableColumns={columns} query={query}
249
257
  chart={tab === "chart" ? { series, ...(mode === "kpis" ? { xAxis: kpiChart.xAxis, viewport: kpiChart.viewport } : {}), empty: preview ? "Full history is available with Pro." : "History is accumulating. Two comparable disclosures are needed for a trend.",
250
258
  formatValue: (value) => selected ? `${valueText(selected, value)}${selected.currency ? ` ${unitLabel(selected)}` : ""}` : String(value),
@@ -14,6 +14,7 @@ import { portfolioAnalyticsModule } from "./analytics";
14
14
  import { brokerTradingModule } from "./broker-trading";
15
15
  import { brokerManagerModule } from "./broker-manager";
16
16
  import { changelogModule } from "./changelog";
17
+ import { roadmapModule } from "./roadmap";
17
18
  import { connectionsModule } from "./connections";
18
19
  import { pluginMarketplaceModule } from "./plugin-marketplace";
19
20
  import { relativeRotationModule } from "./relative-rotation";
@@ -116,7 +117,7 @@ import { FIXED_INCOME_HOSTS } from "./fixed-income/model";
116
117
 
117
118
  export const applicationPlugin = composeBuiltinPlugin({
118
119
  ...applicationPluginMeta,
119
- modules: [layoutManagerModule, pluginMarketplaceModule, helpModule, changelogModule, connectionsModule, docsModule],
120
+ modules: [layoutManagerModule, pluginMarketplaceModule, helpModule, changelogModule, connectionsModule, docsModule, roadmapModule],
120
121
  });
121
122
 
122
123
  export const portfolioPlugin = composeBuiltinPlugin({
@@ -3,7 +3,10 @@ import type { CotBoardPayload, CotClass, CotClassSummary, CotContractPayload, Co
3
3
  import { createPluginCache } from "../../../data/plugin-cache";
4
4
  import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
5
5
  import type { PricePoint } from "../../../types/financials";
6
- import { COT_CLASSES, cotPriceMapping } from "./model";
6
+ import { COT_CLASSES, cotFrontPriceUnit, cotPriceMapping } from "./model";
7
+ import { futuresPriceUnit, loadFuturesContractTerms, loadFuturesPriceUnit } from "../../../market-data/futures-contract";
8
+ import { futuresBoardContract } from "../../../market-data/futures-board";
9
+ import { settleWithin } from "../../../utils/async-deadline";
7
10
 
8
11
  export const cotBoardCache = createPluginCache<CotBoardPayload>({ kind: "cot-board", source: "gloom-cloud", schemaVersion: 1,
9
12
  policy: { staleMs: 15 * 60_000, expireMs: 8 * 24 * 60 * 60_000 } });
@@ -88,7 +91,12 @@ export async function fetchCotContract(contractCode: string, family: CotFamily,
88
91
  catch (error) { throw unavailable(error); }
89
92
  }
90
93
  export interface CotDetailData { payload: CotContractPayload; price: PricePoint[]; priceSymbol: string | null; priceAsOf: string | null; priceWarning: string | null }
91
- export async function loadCotDetail(contractCode: string, family: CotFamily, client: Pick<typeof apiClient, "getCloudCotContract" | "getCloudHistory"> = apiClient,
94
+ /**
95
+ * A market's report and its front price. The price's unit is not waited for
96
+ * here: it joins the legend when it is known (loadCotPriceUnit).
97
+ */
98
+ export async function loadCotDetail(contractCode: string, family: CotFamily,
99
+ client: Pick<typeof apiClient, "getCloudCotContract" | "getCloudHistory"> = apiClient,
92
100
  positions: CotPositions = "futures-only"): Promise<CotDetailData> {
93
101
  const payload = await fetchCotContract(contractCode, family, client, positions);
94
102
  const mapping = cotPriceMapping(contractCode);
@@ -109,3 +117,21 @@ export async function loadCotDetail(contractCode: string, family: CotFamily, cli
109
117
  priceWarning: result.stale ? "Front-price history is stale." : price.length ? null : "Front-price history unavailable." };
110
118
  } catch { return { payload, price: [], priceSymbol: mapping.priceSymbol, priceAsOf: null, priceWarning: "Front-price history unavailable. Positioning remains available." }; }
111
119
  }
120
+
121
+ /** How long the front price's legend waits for the contract terms before it writes the catalog's unit. */
122
+ const COT_PRICE_UNIT_WAIT_MS = 1_500;
123
+
124
+ /**
125
+ * The unit a market's front price is written in, as its legend writes it
126
+ * after the price (cotFrontPriceUnit): `USD/gal` for heating oil. It is the
127
+ * contract terms' unit, else the futures catalog's where the terms fail or
128
+ * have not answered within `waitMs`; null for a market with no front price.
129
+ * The chart draws without it, so a slow answer never holds the detail up.
130
+ */
131
+ export async function loadCotPriceUnit(contractCode: string, client: Pick<typeof apiClient, "getCloudFuturesContract"> = apiClient,
132
+ waitMs = COT_PRICE_UNIT_WAIT_MS): Promise<string | null> {
133
+ const symbol = cotPriceMapping(contractCode)?.priceSymbol;
134
+ if (!symbol) return null;
135
+ const terms = await settleWithin(loadFuturesPriceUnit(symbol, (key) => loadFuturesContractTerms(key, client)), waitMs);
136
+ return cotFrontPriceUnit(contractCode, terms ?? futuresPriceUnit(futuresBoardContract(symbol), null));
137
+ }
@@ -1,7 +1,7 @@
1
1
  import type { CotClass, CotClassSummary, CotContractPayload, CotHistoryPoint } from "../../../api-client/cot";
2
2
  import type { HeadlessPaneColumn, HeadlessPaneDefinition } from "../../../types/plugin";
3
3
  import { fetchCotBoard, fetchCotContract } from "./client";
4
- import { COT_CLASSES, COT_FAMILIES, COT_MAJOR_CODES, COT_POSITIONS, COT_SCOPES, cotClass, cotContractCode, cotContractUnitText, cotFamily, cotInteger, cotMarketName, cotPositions, cotPositionsText, cotScope, cotUnknownMarketMessage } from "./model";
4
+ import { COT_CLASSES, COT_FAMILIES, COT_MAJOR_CODES, COT_POSITIONS, COT_SCOPES, cotClass, cotContractCode, cotContractUnitText, cotFamily, cotInteger, cotMarketName, cotPositions, cotPositionsText, cotProxyFor, cotProxyNote, cotScope, cotUnknownMarketMessage } from "./model";
5
5
  import { COMMODITIES_VIEW_STATE } from "../commodities-energy/view-state";
6
6
  import { REPORT_ROWS } from "../shared/headless-options";
7
7
 
@@ -67,6 +67,7 @@ export const cotHeadless: HeadlessPaneDefinition<"bundle"> = {
67
67
  if (args.argument) {
68
68
  const code = cotContractCode(args.argument);
69
69
  if (!code) throw new Error(cotUnknownMarketMessage(args.argument));
70
+ const proxy = cotProxyFor(args.argument);
70
71
  const data = await fetchCotContract(code, family, ctx.apiClient, positions);
71
72
  const classes = data.positions.map((row) => row.id);
72
73
  const limit = args.options.limit == null ? null : Number(args.options.limit);
@@ -80,6 +81,8 @@ export const cotHeadless: HeadlessPaneDefinition<"bundle"> = {
80
81
  ...classes.map((id) => ({ key: `${camelClass(id)}Net`, header: `${data.positions.find((row) => row.id === id)?.label ?? id} net`, align: "right" as const, format: signed }))],
81
82
  rows: weeks.map((row) => ({ reportDate: row.reportDate, openInterest: row.openInterest,
82
83
  ...Object.fromEntries(row.positions.map((position) => [`${camelClass(position.id)}Net`, position.net])) })) }],
84
+ // "jet fuel" reads heating oil's positions, and says so.
85
+ ...(proxy ? { notes: [cotProxyNote(proxy)] } : {}),
83
86
  errors: data.gaps, metadata: { ...data, complete: data.status === "available" } };
84
87
  }
85
88
  const traderClass = cotClass(family, args.options.traderClass);
@@ -1,7 +1,7 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
2
  import { cotBoardCache } from "./client";
3
3
  import { cotHeadless } from "./headless";
4
- import { COT_ROOT_OPTIONS, cotContractCode, cotContractCodeForTicker, cotRoot, cotUnknownMarketMessage } from "./model";
4
+ import { COT_ROOT_OPTIONS, cotContractCode, cotContractCodeForTicker, cotProxyFor, cotRoot, cotUnknownMarketMessage } from "./model";
5
5
  import { CotPane } from "./pane";
6
6
  import { COT_SERIES } from "./series";
7
7
 
@@ -19,7 +19,9 @@ export const cotModule: PluginModule = {
19
19
  const input = options?.arg?.trim();
20
20
  const code = input ? cotContractCode(input) : cotContractCodeForTicker(context.activeTicker);
21
21
  if (input && !code) throw new Error(cotUnknownMarketMessage(input));
22
- return { title: code ? `COT ${cotRoot(code) ?? code}` : "CFTC Positioning", params: code ? { code } : undefined,
22
+ // "jet fuel" opens heating oil, and the title says which market stands in.
23
+ const proxy = input ? cotProxyFor(input) : null;
24
+ return { title: code ? `COT ${cotRoot(code) ?? code}${proxy ? ` (${proxy.subject} proxy)` : ""}` : "CFTC Positioning", params: code ? { code } : undefined,
23
25
  settings: { report: options?.values?.report ?? "legacy" }, placement: "floating" as const };
24
26
  },
25
27
  }],