gloomberb 0.17.1 → 0.17.3

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 (237) hide show
  1. package/package.json +1 -1
  2. package/src/{plugins/builtin/cloud → account}/auth-dialog.tsx +7 -7
  3. package/src/api-client/earnings.ts +4 -2
  4. package/src/api-client/futures-curve.ts +16 -0
  5. package/src/api-client/index.ts +18 -8
  6. package/src/api-client/types.ts +3 -1
  7. package/src/app/global-shortcuts.ts +10 -0
  8. package/src/app/keybindings/actions.ts +16 -0
  9. package/src/app/runtime/desktop-deeplink.ts +1 -1
  10. package/src/app.tsx +1 -1
  11. package/src/architecture/core-builtin-allowlist.json +4 -9
  12. package/src/cli/commands/market.ts +94 -25
  13. package/src/cli/commands/options-chain.ts +29 -33
  14. package/src/cli/commands/options-leaps.ts +462 -63
  15. package/src/cli/commands/search.ts +53 -9
  16. package/src/cli/commands/ticker.ts +100 -21
  17. package/src/cli/desktop-pane-shot.ts +34 -9
  18. package/src/cli/fx-pair.ts +17 -0
  19. package/src/cli/glossary.ts +134 -6
  20. package/src/cli/history-rows.ts +16 -4
  21. package/src/cli/index.ts +44 -12
  22. package/src/cli/known-gaps.ts +43 -2
  23. package/src/cli/listing-arg.ts +19 -4
  24. package/src/cli/listing-suggestion.ts +33 -2
  25. package/src/cli/pane-functions/catalog.ts +6 -3
  26. package/src/cli/pane-functions/dom.ts +56 -8
  27. package/src/cli/pane-functions/freshness.ts +48 -5
  28. package/src/cli/pane-functions/function-help.ts +2 -2
  29. package/src/cli/pane-functions/headless.ts +85 -19
  30. package/src/cli/pane-functions/index.ts +65 -7
  31. package/src/cli/pane-functions/market-session.ts +69 -6
  32. package/src/cli/pane-functions/report-notices.ts +15 -0
  33. package/src/cli/pane-functions/resolver.ts +4 -2
  34. package/src/cli/pane-functions/screenshot.ts +14 -1
  35. package/src/cli/scroll-truncation.ts +5 -1
  36. package/src/cli/symbol-args.ts +60 -3
  37. package/src/cli/symbol-list.ts +19 -7
  38. package/src/cli/unknown-command.ts +70 -0
  39. package/src/components/chart/native/surface/visibility.ts +37 -7
  40. package/src/components/command-bar/workflow/builtin.ts +2 -2
  41. package/src/components/command-bar/workflow/collection-actions.ts +1 -1
  42. package/src/components/feedback-dialog.tsx +1 -1
  43. package/src/components/layout/header.tsx +348 -102
  44. package/src/components/layout/shell/index.tsx +1 -1
  45. package/src/components/layout/status-bar-groups.ts +1 -1
  46. package/src/components/layout/status-bar.tsx +2 -2
  47. package/src/components/markdown-text.tsx +50 -64
  48. package/src/components/market-board/index.tsx +12 -6
  49. package/src/components/reader/article-reader.tsx +1 -1
  50. package/src/components/reader/detail-reader.tsx +37 -12
  51. package/src/components/reader/reader-text.tsx +32 -17
  52. package/src/components/ticker/badge/index.tsx +2 -0
  53. package/src/components/ui/display.tsx +6 -1
  54. package/src/components/ui/external-link.tsx +10 -2
  55. package/src/data/memory-resource-store.ts +1 -1
  56. package/src/data/resource-store.ts +13 -1
  57. package/src/docs/snapshot.json +13 -13
  58. package/src/i18n/es.ts +1 -0
  59. package/src/i18n/ja.ts +1 -0
  60. package/src/i18n/ko.ts +1 -0
  61. package/src/i18n/zh-cn.ts +1 -0
  62. package/src/i18n/zh-tw.ts +1 -0
  63. package/src/layout-marketplace/gallery.tsx +2 -2
  64. package/src/layout-marketplace/linked-sync.ts +1 -1
  65. package/src/market-data/futures-contract.ts +10 -0
  66. package/src/market-data/market/format.ts +7 -2
  67. package/src/market-data/market/freshness.ts +28 -1
  68. package/src/market-data/published-apac-sessions.ts +12 -2
  69. package/src/market-data/quotes/regular-session-valuation.ts +32 -1
  70. package/src/market-data/quotes/resolution.ts +2 -0
  71. package/src/plugins/builtin/account-management/pane.tsx +17 -13
  72. package/src/plugins/builtin/attention/headless.ts +19 -10
  73. package/src/plugins/builtin/attention/model.ts +22 -0
  74. package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
  75. package/src/plugins/builtin/changelog/index.tsx +11 -8
  76. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
  77. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  78. package/src/plugins/builtin/chart-composer/presets.ts +33 -1
  79. package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
  80. package/src/plugins/builtin/chat/attachments/model.ts +5 -3
  81. package/src/plugins/builtin/chat/channel-labels.ts +2 -2
  82. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
  83. package/src/plugins/builtin/chat/content/footer.tsx +6 -0
  84. package/src/plugins/builtin/chat/content/index.tsx +2 -2
  85. package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
  86. package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
  87. package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
  88. package/src/plugins/builtin/chat/conversations.ts +1 -1
  89. package/src/plugins/builtin/chat/index.tsx +2 -2
  90. package/src/plugins/builtin/chat/layout.ts +91 -31
  91. package/src/plugins/builtin/chat/markdown.ts +94 -0
  92. package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
  93. package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
  94. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  95. package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
  96. package/src/plugins/builtin/chat/share/flow.tsx +1 -1
  97. package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
  98. package/src/plugins/builtin/chat/sidebar.tsx +2 -2
  99. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
  100. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  101. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  102. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
  103. package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
  104. package/src/plugins/builtin/cloud/team/capability.ts +2 -2
  105. package/src/plugins/builtin/cloud/team/command.ts +4 -4
  106. package/src/plugins/builtin/cloud/team/module.tsx +3 -3
  107. package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
  108. package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
  109. package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
  110. package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
  111. package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
  112. package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
  113. package/src/plugins/builtin/cloud/team/views.ts +2 -2
  114. package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
  115. package/src/plugins/builtin/cloud/thesis/detail.tsx +1 -1
  116. package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
  117. package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
  118. package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
  119. package/src/plugins/builtin/company-attention/headless.ts +8 -5
  120. package/src/plugins/builtin/company-attention/model.ts +53 -2
  121. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  122. package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
  123. package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
  124. package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
  125. package/src/plugins/builtin/cot/client.ts +12 -4
  126. package/src/plugins/builtin/cot/pane.tsx +2 -2
  127. package/src/plugins/builtin/crypto-board/model.ts +7 -3
  128. package/src/plugins/builtin/custom-view/pane.tsx +2 -2
  129. package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
  130. package/src/plugins/builtin/debug/index.tsx +15 -15
  131. package/src/plugins/builtin/dividend-yield/client.ts +8 -1
  132. package/src/plugins/builtin/dividend-yield/headless.ts +11 -2
  133. package/src/plugins/builtin/dividend-yield/pane.tsx +8 -5
  134. package/src/plugins/builtin/docs/blocks.tsx +13 -2
  135. package/src/plugins/builtin/docs/index.tsx +5 -3
  136. package/src/plugins/builtin/docs/pane.tsx +24 -6
  137. package/src/plugins/builtin/docs/targets.ts +5 -3
  138. package/src/plugins/builtin/earnings/board-model.ts +18 -1
  139. package/src/plugins/builtin/earnings/board-table.ts +18 -3
  140. package/src/plugins/builtin/earnings/headless.ts +13 -2
  141. package/src/plugins/builtin/earnings/history-model.ts +1 -1
  142. package/src/plugins/builtin/earnings/history-table.ts +2 -2
  143. package/src/plugins/builtin/earnings-calls/data.ts +22 -0
  144. package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
  145. package/src/plugins/builtin/equity-screener/headless.ts +4 -3
  146. package/src/plugins/builtin/equity-screener/live.ts +27 -1
  147. package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
  148. package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
  149. package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
  150. package/src/plugins/builtin/futures/index.tsx +32 -4
  151. package/src/plugins/builtin/futures/model.ts +65 -3
  152. package/src/plugins/builtin/futures/table.tsx +31 -11
  153. package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
  154. package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
  155. package/src/plugins/builtin/holders/pane.tsx +6 -1
  156. package/src/plugins/builtin/mna/client.ts +12 -2
  157. package/src/plugins/builtin/mna/detail.tsx +2 -2
  158. package/src/plugins/builtin/mna/pane.tsx +2 -2
  159. package/src/plugins/builtin/notes/index.tsx +1 -1
  160. package/src/plugins/builtin/notes/owner.tsx +7 -6
  161. package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
  162. package/src/plugins/builtin/options/carry.ts +36 -0
  163. package/src/plugins/builtin/options/open-interest-as-of.ts +16 -5
  164. package/src/plugins/builtin/options/view.tsx +19 -7
  165. package/src/plugins/builtin/options-calculator/pane.tsx +58 -4
  166. package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
  167. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  168. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
  169. package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
  170. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
  171. package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
  172. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  173. package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
  174. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -9
  175. package/src/plugins/builtin/research/event-model.ts +21 -4
  176. package/src/plugins/builtin/research/headless-schema.ts +31 -1
  177. package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
  178. package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
  179. package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
  180. package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
  181. package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
  182. package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
  183. package/src/plugins/builtin/research-search/data.ts +11 -0
  184. package/src/plugins/builtin/research-search/pane.tsx +13 -2
  185. package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
  186. package/src/plugins/builtin/seasonality/client.ts +23 -0
  187. package/src/plugins/builtin/seasonality/pane.tsx +4 -2
  188. package/src/plugins/builtin/sectors/index.tsx +7 -3
  189. package/src/plugins/builtin/series-inventory.ts +1 -0
  190. package/src/plugins/builtin/shared/earnings-report.ts +10 -4
  191. package/src/plugins/builtin/shared/report-freshness.ts +6 -2
  192. package/src/plugins/builtin/short-interest/headless.ts +13 -3
  193. package/src/plugins/builtin/short-interest/index.tsx +1 -1
  194. package/src/plugins/builtin/short-interest/model.ts +62 -3
  195. package/src/plugins/builtin/short-interest/pane.tsx +2 -2
  196. package/src/plugins/builtin/short-interest/schedule.ts +107 -0
  197. package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
  198. package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
  199. package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
  200. package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
  201. package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
  202. package/src/plugins/builtin/themes/headless.ts +1 -1
  203. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
  204. package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
  205. package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
  206. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
  207. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
  208. package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
  209. package/src/plugins/builtin/volatility/chart-model.ts +1 -1
  210. package/src/plugins/builtin/volatility/charts.tsx +1 -1
  211. package/src/renderers/dom/styles.css +10 -2
  212. package/src/sources/gloomberb-cloud/normalizers.ts +3 -0
  213. package/src/sources/provider-router/batches.ts +2 -1
  214. package/src/sources/provider-router/cache.ts +3 -0
  215. package/src/sync/baseline.ts +19 -14
  216. package/src/sync/controller.ts +140 -40
  217. package/src/sync/core-contributors.ts +256 -32
  218. package/src/sync/types.ts +44 -0
  219. package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
  220. package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
  221. package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
  222. package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
  223. package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
  224. package/src/time-series/resolve.ts +19 -2
  225. package/src/types/data-provider.ts +11 -0
  226. package/src/types/financials.ts +46 -4
  227. package/src/types/headless.ts +43 -0
  228. package/src/types/plugin.ts +7 -0
  229. package/src/utils/decimal-comma.ts +18 -0
  230. package/src/utils/declared-dividends.ts +39 -0
  231. package/src/utils/exchanges.ts +7 -0
  232. package/src/utils/forward-dividend.ts +62 -9
  233. package/src/utils/fundamentals.ts +13 -2
  234. package/src/utils/markdown.ts +180 -0
  235. package/src/utils/market-capitalization.ts +22 -0
  236. package/src/utils/option-expiry.ts +16 -0
  237. package/src/utils/sec.ts +2 -1
@@ -23,10 +23,10 @@ import { fail } from "../errors";
23
23
  import type { MarketContext } from "../types";
24
24
  import type { CliCommandContext, GloomPlugin } from "../../types/plugin";
25
25
  import { indexRootFor, indexRootTryLine } from "../index-roots";
26
- import { knownGapMessage } from "../known-gaps";
26
+ import { knownGapMessage, settingsPointer } from "../known-gaps";
27
27
  import { instrumentTypeLabel } from "../../tickers/instrument-kind";
28
28
  import { canonicalExchange, isUsListingExchange } from "../../utils/exchanges";
29
- import { groupByCompany } from "../../tickers/company-name";
29
+ import { groupByCompany, sameCompany } from "../../tickers/company-name";
30
30
  import { findCatalogSeries, renderSeriesMatches, type SeriesMatch } from "../series-search";
31
31
  import { centralBankCiting, centralBankNamed, type CentralBank } from "../../utils/central-banks";
32
32
  import { createPaneCatalog } from "../pane-functions/discovery";
@@ -120,7 +120,7 @@ export async function searchCandidatesForCli({
120
120
  const quote = BARE_SYMBOL.test(symbol) && candidates.some((candidate) => candidate.symbol.toUpperCase() === symbol)
121
121
  ? await (quoted ?? loadSearchedQuote(symbol, savedExchange, dataProvider))
122
122
  : null;
123
- return withQuotedNames(symbol, withPrimaryListingFirst(symbol, candidates, quote), quote).slice(0, totalLimit);
123
+ return withQuotedNames(symbol, withHomeListingPrimary(symbol, withPrimaryListingFirst(symbol, candidates, quote)), quote).slice(0, totalLimit);
124
124
  } catch {
125
125
  return [];
126
126
  }
@@ -217,6 +217,47 @@ function withPrimaryListingFirst(
217
217
  ));
218
218
  }
219
219
 
220
+ /** Venues that only quote shares listed elsewhere: the OTC tiers, Frankfurt's open market and Germany's regional exchanges. */
221
+ const SECONDARY_VENUES = new Set(["OTC", "PNK", "OTCQX", "OTCQB", "FWB2", "MUNICH", "XSTU", "XDUS", "XBER", "XHAM", "HANOVER"]);
222
+
223
+ /**
224
+ * How far a listing is from being its company's own line: 0 a plain one, 1 a
225
+ * line that only quotes shares listed elsewhere (a regional venue, OTC, a
226
+ * Milan 4MSFT or London 0XXX code), 2 a depositary receipt.
227
+ */
228
+ function listingDistance(candidate: TickerSearchCandidate): 0 | 1 | 2 {
229
+ if (/depositary|^(?:ADR|ADS|GDR|CDR|BDR)$/i.test(candidate.instrumentType ?? "")) return 2;
230
+ return SECONDARY_VENUES.has(candidateExchange(candidate)) || /\d/.test(candidate.symbol) && /[A-Za-z]/.test(candidate.symbol) ? 1 : 0;
231
+ }
232
+
233
+ /**
234
+ * A company searched by name lists its home line as the primary listing. A
235
+ * search row names no home exchange or volume, so a company's lines go by
236
+ * how far each is from being its own line (`listingDistance`), then by the
237
+ * search score the service gives the listings people look up most. The
238
+ * listing search put first, often the one-off cross-listing whose short name
239
+ * matched the query (LUFTHANSA on Budapest), gives way only to a closer line,
240
+ * or to one searched at least twice as often, or any scored line when it has
241
+ * no score. A listing searched by its own symbol is not moved.
242
+ */
243
+ function withHomeListingPrimary(symbol: string, candidates: TickerSearchCandidate[]): TickerSearchCandidate[] {
244
+ const primary = candidates.find((candidate) => candidate.category === "Primary Listing");
245
+ if (!primary || primary.kind !== "search" || primary.symbol.toUpperCase() === symbol) return candidates;
246
+ const company = resolveSearchName(primary);
247
+ const lines = candidates.filter((candidate) => candidate.kind === "search" && candidate.category !== "Funds & Derivatives"
248
+ && (candidate.instrumentClass === "equity" || candidate.instrumentClass === "other") && sameCompany(resolveSearchName(candidate), company));
249
+ const score = (candidate: TickerSearchCandidate) => candidate.result?.popularity ?? candidate.popularity ?? null;
250
+ const [home] = [...lines].sort((a, b) => listingDistance(a) - listingDistance(b) || (score(b) ?? -1) - (score(a) ?? -1) || candidates.indexOf(a) - candidates.indexOf(b));
251
+ if (!home || home === primary) return candidates;
252
+ const closer = listingDistance(home) < listingDistance(primary);
253
+ const [homeScore, primaryScore] = [score(home), score(primary)];
254
+ const moreSearched = listingDistance(home) === listingDistance(primary) && homeScore != null && (primaryScore == null || homeScore >= primaryScore * 2);
255
+ if (!closer && !moreSearched) return candidates;
256
+ const rest = candidates.filter((candidate) => candidate !== home);
257
+ const at = rest.indexOf(primary);
258
+ return [...rest.slice(0, at), { ...home, category: "Primary Listing" as const }, { ...primary, category: "Other Listings" as const }, ...rest.slice(at + 1)];
259
+ }
260
+
220
261
  /** The candidate on the venue the quote prices; the US exchanges share one symbol, so a quote on any of them is its one US line. */
221
262
  function quotedCandidate(candidates: readonly TickerSearchCandidate[], quote: Quote | null): TickerSearchCandidate | undefined {
222
263
  const quoted = canonicalExchange(quote?.listingExchangeName || quote?.exchangeName);
@@ -325,9 +366,9 @@ function catalogHint(query: string): string {
325
366
 
326
367
  /**
327
368
  * The notes beside the results: the index a bare root names, data the query
328
- * asks for that no source carries yet, the central bank it names, and for an
329
- * empty search the catalog, unless a known gap, the bank or the functions
330
- * listed already said where to look instead.
369
+ * asks for that no source carries yet, the central bank it names, the setting
370
+ * it names ("light theme"), and for an empty search the catalog, unless one of
371
+ * those or the functions listed already said where to look instead.
331
372
  */
332
373
  function searchNotes(
333
374
  query: string,
@@ -337,9 +378,10 @@ function searchNotes(
337
378
  ): string[] {
338
379
  const gap = knownGapMessage(query);
339
380
  const bank = searchedCentralBank(query);
381
+ const setting = settingsPointer(query);
340
382
  const empty = candidates.length === 0 && !fate.stopped;
341
- return [...fate.notes, indexRootNote(query, candidates), gap, bank ? centralBankLine(bank) : null,
342
- empty && !gap && !bank && functions.length === 0 ? catalogHint(query) : null]
383
+ return [...fate.notes, indexRootNote(query, candidates), gap, bank ? centralBankLine(bank) : null, setting,
384
+ empty && !gap && !bank && !setting && functions.length === 0 ? catalogHint(query) : null]
343
385
  .filter((note): note is string => !!note);
344
386
  }
345
387
 
@@ -379,6 +421,8 @@ export function buildSearchReport({
379
421
  if (gap) lines.push("", gap);
380
422
  const bank = searchedCentralBank(query);
381
423
  if (bank) lines.push("", centralBankLine(bank));
424
+ const setting = settingsPointer(query);
425
+ if (setting) lines.push("", setting);
382
426
  const charted = renderSeriesMatches(query, series, SEARCH_SERIES_LIMIT, cliTerminalWidth() ?? 100, "catalog");
383
427
  // A query that names the data ("carbon", "day-ahead") lists its series first; words that happen to
384
428
  // be in a series name ("apple") list them after the tickers.
@@ -390,7 +434,7 @@ export function buildSearchReport({
390
434
  if (candidates.length === 0) {
391
435
  if (fate.stopped) return lines.join("\n");
392
436
  if (functions.length > 0) lines.push("", renderFunctionMatches(functions));
393
- else if (!gap && !bank && !charted) lines.push(cliStyles.muted("No matches found."), cliStyles.muted(catalogHint(query)));
437
+ else if (!gap && !bank && !setting && !charted) lines.push(cliStyles.muted("No matches found."), cliStyles.muted(catalogHint(query)));
394
438
  return lines.join("\n");
395
439
  }
396
440
 
@@ -3,7 +3,7 @@ import { formatReportedMoney } from "../../utils/reported-money";
3
3
  import { convertedPriceEarningsNote, enterpriseValueLabel, enterpriseValueReconciliationNote, epsConversionNote, fundamentalsCurrency, reportedEnterpriseValue, ttmConversionNote } from "../../utils/fundamentals";
4
4
  import { latestFinancialPeriod } from "../../utils/latest-financial-period";
5
5
  import { exportedFundamentals, formatPriceEarnings, priceEarningsOnEarnings } from "../../utils/price-earnings";
6
- import { describeFundamentalMarketCap, selectMarketCapitalization } from "../../utils/market-capitalization";
6
+ import { describeFundamentalMarketCap, marketCapShareBasisNote, selectMarketCapitalization } from "../../utils/market-capitalization";
7
7
  import {
8
8
  formatCompact,
9
9
  formatCurrency,
@@ -32,9 +32,9 @@ import {
32
32
  import type { FuturesContractTermsPayload } from "../../api-client/futures-curve";
33
33
  import { futuresContractLines, futuresSettleText, isFuturesInstrument, loadFuturesContractTerms } from "../../market-data/futures-contract";
34
34
  import type { AppConfig } from "../../types/config";
35
- import type { FinancialStatement, Quote, TickerFinancials } from "../../types/financials";
35
+ import type { DeclaredDividendAction, FinancialStatement, Quote, TickerFinancials } from "../../types/financials";
36
36
  import { quoteTradingDay } from "../../market-data/quotes/day-range";
37
- import { atRegularSessionPrice } from "../../market-data/quotes/regular-session-valuation";
37
+ import { atRegularSessionPrice, regularSessionValuationPrice } from "../../market-data/quotes/regular-session-valuation";
38
38
  import { computeTickerPriceReturns } from "../../market-data/ticker-price-returns";
39
39
  import { instrumentTypeLabel, isCryptoVenuePerpetual, resolveTickerInstrumentKind } from "../../tickers/instrument-kind";
40
40
  import type { SecFilingItem } from "../../types/data-provider";
@@ -69,6 +69,7 @@ import { exportEntriesTable, reportFooterLines, type CliReportTables } from "../
69
69
  import type { ReportFreshness } from "../pane-functions/freshness";
70
70
  import { fundamentalsFreshness, quotesFreshness } from "../freshness";
71
71
  import { forwardDividendBasis } from "../../utils/forward-dividend";
72
+ import { declaredDividends } from "../../utils/declared-dividends";
72
73
  import { notTheCoinNote } from "./crypto-hints";
73
74
 
74
75
  const NEWS_ITEM_LIMIT = 5;
@@ -347,11 +348,38 @@ function percentFigure(fraction: number | null | undefined, format: (value: numb
347
348
  return fraction != null ? { text: format(fraction), value: fraction * 100, unit: "%" } : { text: "—" };
348
349
  }
349
350
 
350
- /** A forward yield says what it annualizes and when, as DVD does; any other yield is the figure alone. */
351
- function forwardYieldFigure(fundamentals: TickerFinancials["fundamentals"]): MetricFigure {
351
+ /** The declared, unpaid distributions a forward yield dated ahead is named from, in their own currency. */
352
+ export interface ForwardYieldDeclarations {
353
+ rows: DeclaredDividendAction[];
354
+ currency?: string;
355
+ }
356
+
357
+ /**
358
+ * The listing's declared distributions, read only for a forward yield whose
359
+ * ex-date is still ahead (the one case its basis names one); null otherwise,
360
+ * and when they cannot be read, which leaves the basis to its dates.
361
+ */
362
+ export async function loadForwardYieldDeclarations(
363
+ dataProvider: Pick<MarketContext["dataProvider"], "getCorporateActions">,
364
+ listing: { symbol: string; exchange: string },
365
+ fundamentals: TickerFinancials["fundamentals"],
366
+ now = new Date(),
367
+ ): Promise<ForwardYieldDeclarations | null> {
368
+ const exDate = fundamentals?.dividendYieldBasis === "forward" ? fundamentals.exDividendDate ?? "" : "";
369
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(exDate) || exDate <= now.toISOString().slice(0, 10) || !dataProvider.getCorporateActions) return null;
370
+ try {
371
+ const actions = await dataProvider.getCorporateActions(listing.symbol, listing.exchange);
372
+ return { rows: declaredDividends(actions, now), ...(actions.currency ? { currency: actions.currency } : {}) };
373
+ } catch {
374
+ return null;
375
+ }
376
+ }
377
+
378
+ /** A forward yield says what it annualizes and when, as DVD does (a declared distribution by its ex-date and stated amount); any other yield is the figure alone. */
379
+ function forwardYieldFigure(fundamentals: TickerFinancials["fundamentals"], declared: ForwardYieldDeclarations | null): MetricFigure {
352
380
  const figure = percentFigure(fundamentals?.dividendYield, formatFractionPercentCell);
353
381
  return fundamentals?.dividendYieldBasis === "forward" && fundamentals.dividendYield != null
354
- ? { ...figure, text: `${figure.text} (${forwardDividendBasis(fundamentals.exDividendDate)})` }
382
+ ? { ...figure, text: `${figure.text} (${forwardDividendBasis(fundamentals.exDividendDate, { declared: declared?.rows, currency: declared?.currency })})` }
355
383
  : figure;
356
384
  }
357
385
 
@@ -368,6 +396,7 @@ function fundamentalsMetrics(
368
396
  priceReturns: { return1Y?: number | null; return3Y?: number | null },
369
397
  reportingCurrency: string | undefined,
370
398
  enterpriseValue = reportedMoney(reportedEnterpriseValue(fundamentals), enterpriseValueCurrency(quote, fundamentals)),
399
+ declared: ForwardYieldDeclarations | null = null,
371
400
  ): FundamentalsMetric[] {
372
401
  const signed = (value: number) => colorBySign(formatPercent(value), value);
373
402
  const receipt = fundamentals?.sharesOutstanding != null && sharesOutstandingInReceipts(quote, fundamentals, profile?.description);
@@ -382,7 +411,7 @@ function fundamentalsMetrics(
382
411
  { label: "EPS (TTM)", ...reportedMoney(fundamentals?.eps, fundamentals?.epsCurrency?.trim() || reportingCurrency, true) },
383
412
  {
384
413
  label: `Dividend Yield${fundamentals?.dividendYieldBasis ? ` (${fundamentals.dividendYieldBasis})` : ""}`,
385
- ...forwardYieldFigure(fundamentals),
414
+ ...forwardYieldFigure(fundamentals, declared),
386
415
  },
387
416
  { label: "Revenue (TTM)", ...reportedMoney(fundamentals?.revenue, reportingCurrency) },
388
417
  { label: "Net Income (TTM)", ...reportedMoney(fundamentals?.netIncome, reportingCurrency) },
@@ -404,6 +433,34 @@ function fundamentalsMetrics(
404
433
  ];
405
434
  }
406
435
 
436
+ /**
437
+ * The float and the short interest `fundamentals` prints after the share count: the shares short, short % of float
438
+ * and days to cover of one settlement, dated, as the JSON gives them and `SI` and `SIW` show them.
439
+ */
440
+ function floatAndShortInterestMetrics(
441
+ quote: TickerFinancials["quote"],
442
+ fundamentals: TickerFinancials["fundamentals"],
443
+ profile: TickerFinancials["profile"],
444
+ ): FundamentalsMetric[] {
445
+ const finite = (value: number | undefined): value is number => value != null && Number.isFinite(value);
446
+ const float = fundamentals?.floatShares;
447
+ const short = fundamentals?.sharesShort;
448
+ const days = fundamentals?.shortRatio;
449
+ const date = fundamentals?.shortInterestDate;
450
+ const receipt = finite(float) && sharesOutstandingInReceipts(quote, fundamentals, profile?.description);
451
+ return [
452
+ finite(float) && float > 0
453
+ ? { label: "Float", text: formatCompact(float), value: float, ...(receipt ? { unit: "ADR equivalent" } : {}) }
454
+ : { label: "Float", text: "—" },
455
+ finite(short)
456
+ ? { label: "Short Interest", text: `${formatCompact(short)}${date ? ` (settled ${date})` : ""}`, value: short, unit: "shares" }
457
+ : { label: "Short Interest", text: "—" },
458
+ ...finite(short) && date ? [{ label: "Short Interest Settlement", text: date, value: date, exportOnly: true }] : [],
459
+ { label: "Short % of Float", ...percentFigure(fundamentals?.shortPercentOfFloat, formatFractionPercentCell) },
460
+ finite(days) ? { label: "Days to Cover", text: formatNumber(days, 2), value: days } : { label: "Days to Cover", text: "—" },
461
+ ];
462
+ }
463
+
407
464
  function metricLines(metrics: readonly FundamentalsMetric[]): Array<[string, string]> {
408
465
  return metrics.filter((metric) => !metric.exportOnly).map((metric) => [metric.label, metric.text]);
409
466
  }
@@ -411,28 +468,35 @@ function metricLines(metrics: readonly FundamentalsMetric[]): Array<[string, str
411
468
  const TRAILING_MONEY_METRICS = new Set(["EPS (TTM)", "Revenue (TTM)", "Net Income (TTM)", "Operating Cash Flow (TTM)", "Free Cash Flow (TTM)"]);
412
469
 
413
470
  /**
414
- * The notes under shown fundamentals: how a labelled or withdrawn EV compares with market cap + debt - cash, what a
415
- * converted TTM block was converted from, an EPS in another currency than the amounts beside it, and what a P/E on
416
- * such an EPS divides.
471
+ * The notes under shown fundamentals: a market cap that does not rest on the share count shown, how a labelled or
472
+ * withdrawn EV compares with market cap + debt - cash, what a converted TTM block was converted from, an EPS in
473
+ * another currency than the amounts beside it, and what a P/E on such an EPS divides. `priced` is the snapshot at
474
+ * the regular session price (atRegularSessionPrice), the figures the lines show, so each note reads those.
417
475
  */
418
476
  function fundamentalsNotes(
419
- fundamentals: TickerFinancials["fundamentals"],
477
+ priced: Pick<TickerFinancials, "quote" | "fundamentals">,
420
478
  metrics: readonly FundamentalsMetric[],
421
479
  quoteCurrency: string | undefined,
422
480
  ): string[] {
481
+ const fundamentals = priced.fundamentals;
423
482
  const shown = (label: string) => metrics.some((metric) => metric.label === label && metric.text !== "—");
483
+ const shareBasis = shown("Market Cap") && shown("Shares Outstanding")
484
+ ? marketCapShareBasisNote(selectMarketCapitalization(priced.quote, fundamentals), fundamentals?.sharesOutstanding,
485
+ regularSessionValuationPrice(priced.quote), priced.quote?.currency)
486
+ : undefined;
424
487
  const conversion = [...TRAILING_MONEY_METRICS].some(shown) ? ttmConversionNote(fundamentals) : undefined;
425
488
  const eps = shown("EPS (TTM)") ? epsConversionNote(fundamentals) : undefined;
426
489
  const priceEarnings = metrics.some((metric) => metric.label === "P/E (TTM)" && typeof metric.value === "number")
427
490
  ? convertedPriceEarningsNote(fundamentals, quoteCurrency)
428
491
  : undefined;
429
492
  const enterpriseValue = shown("Enterprise Value") ? enterpriseValueReconciliationNote(fundamentals) : undefined;
430
- return [enterpriseValue, conversion, eps, priceEarnings].filter((note): note is string => !!note);
493
+ return [shareBasis, enterpriseValue, conversion, eps, priceEarnings].filter((note): note is string => !!note);
431
494
  }
432
495
 
433
- const VALUATION_METRICS = new Set(["Market Cap", "Enterprise Value", "P/E (TTM)", "Forward P/E", "PEG", "EPS (TTM)"]);
496
+ // The share count is the market cap's basis: the cap is the count times the price, or a note says why not.
497
+ const VALUATION_METRICS = new Set(["Market Cap", "Shares Outstanding", "Enterprise Value", "P/E (TTM)", "Forward P/E", "PEG", "EPS (TTM)"]);
434
498
 
435
- type FundamentalsReportData = TickerFinancials & { symbol: string; exchange?: string };
499
+ type FundamentalsReportData = TickerFinancials & { symbol: string; exchange?: string; declaredDividends?: ForwardYieldDeclarations | null };
436
500
 
437
501
  /**
438
502
  * What a fund's report cannot show yet, and for a US listing where its SEC
@@ -502,9 +566,16 @@ function fundamentalsReportMetrics(snapshot: FundamentalsReportData, view: "fund
502
566
  const marketCap: MetricFigure = capitalization
503
567
  ? { text: `${formatCompact(capitalization.value)} ${capitalization.currency}`, value: capitalization.value, unit: capitalization.currency }
504
568
  : { text: "—" };
505
- const metrics = fundamentalsMetrics(quote, fundamentals, financials.profile, marketCap, computeTickerPriceReturns(financials), fundamentalsCurrency(financials));
506
- if (view === "fundamentals") return metrics.filter((metric) => metric.text !== "—");
507
- const valuation = metrics.filter(({ label, text }) => (VALUATION_METRICS.has(label) || label.startsWith("Dividend Yield")) && text !== "—");
569
+ const metrics = fundamentalsMetrics(quote, fundamentals, financials.profile, marketCap, computeTickerPriceReturns(financials), fundamentalsCurrency(financials),
570
+ undefined, snapshot.declaredDividends ?? null);
571
+ if (view === "fundamentals") {
572
+ return [...metrics, ...floatAndShortInterestMetrics(quote, fundamentals, financials.profile)]
573
+ .filter((metric) => metric.text !== "—");
574
+ }
575
+ const valued = metrics.filter(({ label, text }) => (VALUATION_METRICS.has(label) || label.startsWith("Dividend Yield")) && text !== "—");
576
+ // The share count follows the market cap it is the basis of.
577
+ const shares = valued.filter((metric) => metric.label === "Shares Outstanding");
578
+ const valuation = valued.flatMap((metric) => metric.label === "Shares Outstanding" ? [] : metric.label === "Market Cap" ? [metric, ...shares] : [metric]);
508
579
  // A listing with no valuation (a fund) says so rather than printing its price alone.
509
580
  return valuation.length === 0 ? [] : [...valuationPriceMetrics(financials).filter((metric) => metric.text !== "—"), ...valuation];
510
581
  }
@@ -532,7 +603,7 @@ export function renderFundamentalsReport(
532
603
  const fund = fundFactsNote(symbol, financials.exchange || quote?.exchangeName, financials);
533
604
  if (lines.length === before) lines.push("", cliStyles.muted([`No ${view} reported for ${financials.symbol}.`, fund].filter(Boolean).join(" ")));
534
605
  else if (fund) lines.push(cliStyles.muted(fund));
535
- for (const note of fundamentalsNotes(financials.fundamentals, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
606
+ for (const note of fundamentalsNotes(atRegularSessionPrice(financials), metrics, quote?.currency)) lines.push(cliStyles.muted(note));
536
607
  if (view === "fundamentals") appendTextSection(lines, "Description", profile?.description);
537
608
  return lines.join("\n");
538
609
  }
@@ -572,7 +643,7 @@ export function fundamentalsReportTables(
572
643
  notes: [
573
644
  ...metrics.length === 0
574
645
  ? [`No ${view} reported for ${financials.symbol}.`]
575
- : fundamentalsNotes(financials.fundamentals, metrics, financials.quote?.currency),
646
+ : fundamentalsNotes(atRegularSessionPrice(financials), metrics, financials.quote?.currency),
576
647
  ...fund ? [fund] : [],
577
648
  ],
578
649
  }),
@@ -605,8 +676,11 @@ export async function buildTickerReport({
605
676
  recentNews = [],
606
677
  recentSecFilings = [],
607
678
  futuresContract = null,
679
+ declaredDividends: declared = null,
608
680
  }: {
609
681
  symbol: string;
682
+ /** The declared distributions a forward yield dated ahead is named from (loadForwardYieldDeclarations). */
683
+ declaredDividends?: ForwardYieldDeclarations | null;
610
684
  /** The exchange the command named, as a canonical code. */
611
685
  listingExchange?: string;
612
686
  /** A futures symbol's contract terms and latest confirmed settlement. */
@@ -732,10 +806,11 @@ export async function buildTickerReport({
732
806
  ]);
733
807
  }
734
808
 
735
- const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials), { text: enterpriseValueText });
809
+ const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials),
810
+ { text: enterpriseValueText }, declared);
736
811
  const beforeFundamentals = lines.length;
737
812
  appendMetricSection(lines, "Fundamentals", metricLines(metrics));
738
- for (const note of fundamentalsNotes(fundamentals, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
813
+ for (const note of fundamentalsNotes(priced, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
739
814
  // Where a fund's facts would go: under its fundamentals, or in their place.
740
815
  const fund = fundFactsNote(quote?.symbol ?? symbol,
741
816
  quote?.tradingExchangeName || listingExchange || quote?.listingExchangeName || quote?.exchangeName || tickerFile?.metadata.exchange, financials, tickerFile);
@@ -948,6 +1023,9 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
948
1023
 
949
1024
  const notesFiles = new NotesFiles(dataDir);
950
1025
  const listingName = listingIdentity(listing, quote).name;
1026
+ // The text report's yield basis, read beside the rest for a forward yield dated ahead only; it never fails the report.
1027
+ const declarations = dependencies.printResult ? null
1028
+ : loadForwardYieldDeclarations(dataProvider, { symbol: requestSymbol, exchange }, resolvedFinancials.fundamentals);
951
1029
  const [notesResult, newsResult, secFilingsResult] = await Promise.allSettled([
952
1030
  notesFiles.load(tickerFile?.metadata.ticker ?? normalized),
953
1031
  dataProvider.getNews({
@@ -1013,6 +1091,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
1013
1091
  recentNews,
1014
1092
  recentSecFilings,
1015
1093
  futuresContract,
1094
+ declaredDividends: await declarations,
1016
1095
  }));
1017
1096
  if (freshness) console.log(`\n${cliFreshnessFooter(freshness)}`);
1018
1097
  });
@@ -60,9 +60,12 @@ export interface DesktopPaneShotPayload {
60
60
  statusLine?: boolean;
61
61
  /**
62
62
  * The tallest the capture may grow, in cells, to show every row its tables
63
- * scroll past; absent keeps the requested height (`--height`, most panes).
63
+ * scroll past or all the text its reader scrolls past; absent keeps the
64
+ * requested height (`--height`, most panes).
64
65
  */
65
66
  maxHeightCells?: number;
67
+ /** The `data-gloom-role`s of the scrolling boxes the capture grows to fit; the tables' bodies when absent. */
68
+ fitRoles?: string[];
66
69
  tickers: TickerRecord[];
67
70
  /** Broker accounts as last synced, by profile, so a broker portfolio shows its equity and cash. */
68
71
  brokerAccounts?: Record<string, BrokerAccount[]>;
@@ -127,6 +130,12 @@ interface DesktopPaneShotSurface {
127
130
 
128
131
  export interface DesktopPaneShotRenderResult {
129
132
  visibleText: string;
133
+ /**
134
+ * The visible text a line per block of the page, as the browser lays it out,
135
+ * where `visibleText` has run them together: the words of a pane that is prose
136
+ * and not a table, to be read as paragraphs. Absent in a capture that did not read it.
137
+ */
138
+ visibleLines?: string[];
130
139
  /**
131
140
  * The rows a report reads, when a table reports every row (an option chain's
132
141
  * strike window) rather than those in the viewport; `rows` stays what the
@@ -222,7 +231,13 @@ export async function renderDesktopPaneScreenshot(
222
231
  userDataDir: join(tempDir, "chrome-profile"),
223
232
  captureImage: options.captureImage !== false,
224
233
  statusLine: payload.statusLine ? options.statusLine : undefined,
225
- ...(payload.maxHeightCells ? { fitRows: { heightCells: payload.heightCells, maxHeightCells: payload.maxHeightCells } } : {}),
234
+ ...(payload.maxHeightCells ? {
235
+ fitRows: {
236
+ heightCells: payload.heightCells,
237
+ maxHeightCells: payload.maxHeightCells,
238
+ roles: payload.fitRoles ?? ["data-table-body-scroll"],
239
+ },
240
+ } : {}),
226
241
  });
227
242
  } finally {
228
243
  server?.stop(true);
@@ -458,7 +473,7 @@ async function capturePageScreenshot({
458
473
  userDataDir: string;
459
474
  captureImage: boolean;
460
475
  statusLine?: ShotStatusLine;
461
- fitRows?: { heightCells: number; maxHeightCells: number };
476
+ fitRows?: { heightCells: number; maxHeightCells: number; roles: readonly string[] };
462
477
  }): Promise<DesktopPaneShotRenderResult> {
463
478
  await mkdir(userDataDir, { recursive: true });
464
479
  const port = 43000 + Math.floor(Math.random() * 10000);
@@ -531,9 +546,10 @@ async function capturePageScreenshot({
531
546
  }
532
547
 
533
548
  /**
534
- * Grows the viewport and the pane by the rows their tables scroll past, up to
535
- * `maxHeightCells`, then waits for the taller pane to settle: a board a
536
- * default capture would cut (FUT's last sectors) shows every row.
549
+ * Grows the viewport and the pane by the rows their tables scroll past, or the
550
+ * text a reader's body scrolls past, up to `maxHeightCells`, then waits for the
551
+ * taller pane to settle: a board a default capture would cut (FUT's last
552
+ * sectors) shows every row, and a docs page its whole text.
537
553
  */
538
554
  async function growToFitRows(session: CdpSession, size: {
539
555
  widthPx: number;
@@ -541,9 +557,11 @@ async function growToFitRows(session: CdpSession, size: {
541
557
  deviceScaleFactor: number;
542
558
  heightCells: number;
543
559
  maxHeightCells: number;
560
+ roles: readonly string[];
544
561
  }): Promise<void> {
562
+ const selector = size.roles.map((role) => `[data-gloom-role="${role}"]`).join(", ");
545
563
  const result = await session.send("Runtime.evaluate", {
546
- expression: `Math.max(0, ...[...document.querySelectorAll('[data-gloom-role="data-table-body-scroll"]')]
564
+ expression: `Math.max(0, ...[...document.querySelectorAll(${JSON.stringify(selector)})]
547
565
  .map((element) => element.scrollHeight - element.clientHeight))`,
548
566
  returnByValue: true,
549
567
  }) as { result?: { value?: unknown } };
@@ -646,6 +664,7 @@ async function readRenderedPaneState(session: CdpSession): Promise<DesktopPaneSh
646
664
  const table = element.closest('[data-gloom-role="data-table"]');
647
665
  return !!table && reportedTables.has(table);
648
666
  };
667
+ const managesRows = (element) => element.getAttribute("data-gloom-role") === "data-table-body-scroll";
649
668
  const markScrollTruncation = (element) => {
650
669
  if (!(element instanceof HTMLElement) || !isVisible(element)) return;
651
670
  const reasons = scrollReasons({
@@ -655,8 +674,10 @@ async function readRenderedPaneState(session: CdpSession): Promise<DesktopPaneSh
655
674
  scrollLeft: element.scrollLeft,
656
675
  scrollWidth: element.scrollWidth,
657
676
  clientWidth: element.clientWidth,
658
- scrollbarY: element.getAttribute("data-gloom-scrollbar-y") === "visible",
659
- scrollbarX: element.getAttribute("data-gloom-scrollbar-x") === "visible",
677
+ // A scroll box draws its scrollbar by default and its content is all in the page, so its
678
+ // measures say what is cut; only a table's body holds rows it has not drawn.
679
+ scrollbarY: managesRows(element) && element.getAttribute("data-gloom-scrollbar-y") === "visible",
680
+ scrollbarX: managesRows(element) && element.getAttribute("data-gloom-scrollbar-x") === "visible",
660
681
  });
661
682
  reasons.forEach((reason) => truncationReasons.add(reason));
662
683
  if (!inReportedTable(element)) reasons.forEach((reason) => reportTruncationReasons.add(reason));
@@ -810,8 +831,12 @@ async function readRenderedPaneState(session: CdpSession): Promise<DesktopPaneSh
810
831
  if (textShowsEllipsis && !truncationReasons.includes("one or more cells are visibly clipped")) {
811
832
  truncationReasons.push("one or more cells are visibly clipped");
812
833
  }
834
+ const visibleLines = (value?.visibleText ?? "").split("\n")
835
+ .map((line) => line.replace(/\u00a0/g, " ").trimEnd())
836
+ .filter((line) => line.trim().length > 0);
813
837
  return {
814
838
  visibleText,
839
+ visibleLines,
815
840
  footerText: typeof value?.footerText === "string" ? value.footerText : "",
816
841
  visibleKeyValues: Array.isArray(value?.visibleKeyValues) ? value.visibleKeyValues : [],
817
842
  ...(Array.isArray(value?.surfaces) && value.surfaces.length > 0 ? { surfaces: value.surfaces } : {}),
@@ -13,11 +13,28 @@ export interface FxRequest {
13
13
 
14
14
  const CURRENCY_CODE = /^[A-Z]{3}$/;
15
15
 
16
+ let knownCurrencies: ReadonlySet<string> | undefined;
17
+
18
+ /** An ISO 4217 code the runtime lists, or the offshore yuan it has none for. */
19
+ function isKnownCurrency(code: string): boolean {
20
+ if (!knownCurrencies) {
21
+ try {
22
+ knownCurrencies = new Set([...Intl.supportedValuesOf("currency"), "CNH"]);
23
+ } catch {
24
+ knownCurrencies = new Set();
25
+ }
26
+ }
27
+ return knownCurrencies.has(code);
28
+ }
29
+
16
30
  /** The request, or the sentence saying what is wrong with the argument. */
17
31
  export function parseFxRequest(raw: string, baseCurrency: string): FxRequest | { error: string } {
18
32
  const text = raw.trim().toUpperCase();
19
33
  const parts = text.split("/").map((part) => part.trim());
20
34
  if (parts.length > 2 || parts.some((part) => !part)) return { error: `"${raw}" is not a currency or a pair such as USD/NGN.` };
35
+ // EURUSD is the pair typed without its slash: say how to write it.
36
+ const joined = parts.length === 1 && /^[A-Z]{6}$/.test(text) && isKnownCurrency(text.slice(0, 3)) && isKnownCurrency(text.slice(3));
37
+ if (joined) return { error: `${text} is not a currency code; write the pair as ${text.slice(0, 3)}/${text.slice(3)}.` };
21
38
  const bad = parts.filter((part) => !CURRENCY_CODE.test(part));
22
39
  if (bad.length > 0) return { error: `${bad.join(" and ")} ${bad.length > 1 ? "are not currency codes" : "is not a currency code"}; use three letters such as NGN.` };
23
40
  return parts.length === 2