@gloom-sh/terminal 0.19.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +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/hiring.ts +3 -0
  10. package/src/api-client/index.ts +4 -0
  11. package/src/api-client/ipo.ts +24 -1
  12. package/src/api-client/mna.ts +3 -0
  13. package/src/api-client/paths.ts +9 -1
  14. package/src/api-client/roadmap.ts +85 -0
  15. package/src/api-client/types.ts +12 -0
  16. package/src/architecture/core-builtin-allowlist.json +2 -2
  17. package/src/cli/commands/market.ts +35 -12
  18. package/src/cli/commands/news-search.ts +22 -0
  19. package/src/cli/commands/overview.ts +86 -20
  20. package/src/cli/commands/ticker.ts +32 -2
  21. package/src/cli/errors.ts +17 -2
  22. package/src/cli/glossary.ts +6 -0
  23. package/src/cli/index-roots.ts +0 -7
  24. package/src/cli/listing-arg.ts +57 -10
  25. package/src/cli/pane-functions/capabilities.ts +1 -0
  26. package/src/cli/pane-functions/discovery.ts +21 -1
  27. package/src/cli/pane-functions/freshness.ts +7 -1
  28. package/src/cli/pane-functions/function-help.ts +7 -0
  29. package/src/cli/pane-functions/index.ts +6 -3
  30. package/src/cli/quote-answer.ts +1 -1
  31. package/src/cli/saved-copy-persistence.ts +88 -0
  32. package/src/components/command-bar/list/model.ts +5 -0
  33. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  34. package/src/components/command-bar/selection-runtime.ts +52 -3
  35. package/src/components/command-bar/surface/index.tsx +2 -0
  36. package/src/components/layout/pane/capture-stamp.ts +2 -2
  37. package/src/components/layout/pane/footer/index.tsx +22 -6
  38. package/src/components/layout/pane/footer/model.ts +83 -15
  39. package/src/components/layout/pane/status-footer.ts +35 -2
  40. package/src/components/layout/status-widget-space.ts +9 -0
  41. package/src/components/ticker/list-table-view.tsx +4 -20
  42. package/src/components/ui/display.tsx +18 -3
  43. package/src/components/ui/icon.tsx +4 -1
  44. package/src/data/plugin-cache.ts +5 -3
  45. package/src/data/saved-copies.ts +8 -0
  46. package/src/docs/snapshot.json +32 -5
  47. package/src/i18n/es.ts +2 -4
  48. package/src/i18n/ja.ts +2 -4
  49. package/src/i18n/ko.ts +2 -4
  50. package/src/i18n/zh-cn.ts +2 -4
  51. package/src/i18n/zh-tw.ts +2 -4
  52. package/src/market-data/depositary-pair.ts +25 -1
  53. package/src/market-data/quotes/metadata.ts +7 -0
  54. package/src/news/aggregator.ts +7 -1
  55. package/src/news/types.ts +6 -0
  56. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  57. package/src/plugins/builtin/alerts/cli.ts +34 -11
  58. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  59. package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
  60. package/src/plugins/builtin/alerts/headless.ts +10 -6
  61. package/src/plugins/builtin/cds/client.ts +142 -30
  62. package/src/plugins/builtin/cds/headless.ts +35 -8
  63. package/src/plugins/builtin/cds/model.ts +145 -18
  64. package/src/plugins/builtin/cds/pane.tsx +16 -5
  65. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  66. package/src/plugins/builtin/chart-composer/index.tsx +4 -5
  67. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  68. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  69. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  70. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  71. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  72. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  73. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  74. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  75. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  76. package/src/plugins/builtin/clinical-trials/model.ts +86 -22
  77. package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
  78. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  79. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
  80. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  81. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  82. package/src/plugins/builtin/company-attention/pane.tsx +3 -0
  83. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  84. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  85. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  86. package/src/plugins/builtin/composite-plugins.ts +2 -1
  87. package/src/plugins/builtin/credit-documents/headless.ts +3 -3
  88. package/src/plugins/builtin/doe/headless.ts +3 -2
  89. package/src/plugins/builtin/doe/pane.tsx +13 -3
  90. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  91. package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
  92. package/src/plugins/builtin/earnings-calls/pane.tsx +7 -0
  93. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  94. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  95. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  96. package/src/plugins/builtin/econ/figures.ts +48 -0
  97. package/src/plugins/builtin/econ/index.tsx +14 -5
  98. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  99. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  100. package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
  101. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  102. package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
  103. package/src/plugins/builtin/help/index.tsx +61 -59
  104. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  105. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  106. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  107. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  108. package/src/plugins/builtin/holders/headless.ts +4 -2
  109. package/src/plugins/builtin/holders/pane.tsx +3 -0
  110. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  111. package/src/plugins/builtin/insider/headless.ts +2 -2
  112. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  113. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  114. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  115. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  116. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  117. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  118. package/src/plugins/builtin/macro-day/model.ts +39 -5
  119. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  120. package/src/plugins/builtin/market-movers/client.ts +2 -0
  121. package/src/plugins/builtin/market-movers/headless.ts +3 -1
  122. package/src/plugins/builtin/market-movers/screener.ts +8 -4
  123. package/src/plugins/builtin/mna/headless.ts +2 -0
  124. package/src/plugins/builtin/mna/pane.tsx +3 -1
  125. package/src/plugins/builtin/news/headless.ts +3 -1
  126. package/src/plugins/builtin/news/index.tsx +3 -0
  127. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
  128. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  129. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  130. package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
  131. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  132. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  133. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  134. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  135. package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
  136. package/src/plugins/builtin/research/event-model.ts +30 -4
  137. package/src/plugins/builtin/research/index.tsx +7 -3
  138. package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
  139. package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
  140. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  141. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  142. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  143. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  144. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  145. package/src/plugins/builtin/roadmap/model.ts +67 -0
  146. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  147. package/src/plugins/builtin/scanner/live-headless.ts +21 -2
  148. package/src/plugins/builtin/sec/headless.ts +2 -1
  149. package/src/plugins/builtin/sec/index.tsx +3 -0
  150. package/src/plugins/builtin/shared/cloud-resource.ts +13 -5
  151. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  152. package/src/plugins/builtin/shared/report-freshness.ts +8 -0
  153. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  154. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  155. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  156. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  157. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  158. package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
  159. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  160. package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
  161. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  162. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  163. package/src/plugins/builtin/volatility/pane.tsx +5 -2
  164. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  165. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  166. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  167. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  168. package/src/plugins/catalog-browser.ts +2 -1
  169. package/src/plugins/host-link.ts +1 -1
  170. package/src/portfolio/column-values.ts +11 -7
  171. package/src/renderers/dom/desktop/icons.tsx +7 -0
  172. package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
  173. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  174. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  175. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  176. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  177. package/src/renderers/electrobun/bun/index.ts +26 -2
  178. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  179. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  180. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  181. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  182. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  183. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  184. package/src/renderers/electrobun/view/news-source.ts +2 -0
  185. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  186. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  187. package/src/sources/gloomberb-cloud/index.ts +42 -8
  188. package/src/tickers/company-name-listing.ts +53 -13
  189. package/src/tickers/company-name.ts +6 -1
  190. package/src/tickers/index-roots.ts +7 -0
  191. package/src/tickers/listing-fate.ts +2 -2
  192. package/src/tickers/search/index.ts +6 -2
  193. package/src/tickers/search/ranking.ts +5 -1
  194. package/src/tickers/search/upsert.ts +13 -0
  195. package/src/tickers/ticker-changes.ts +8 -4
  196. package/src/types/company-from.ts +40 -0
  197. package/src/types/data-provider.ts +7 -0
  198. package/src/types/financials.ts +27 -0
  199. package/src/types/headless.ts +6 -0
  200. package/src/utils/crypto-pair.ts +121 -7
  201. package/src/utils/declared-dividends.ts +1 -1
  202. package/src/utils/fundamentals.ts +36 -5
  203. package/src/version.ts +2 -2
@@ -29,8 +29,6 @@ import type { QuoteFlashDirection } from "../quote-flash";
29
29
  import { objectVersion } from "../../utils/object-version";
30
30
  import { getCompletedRegularSessionDisplay } from "../../market-data/market/status";
31
31
  import { usePaneFooter } from "../layout/pane/footer";
32
- import { Button } from "../ui/button";
33
- import { PaneStatusBody } from "../ui/status";
34
32
 
35
33
  export interface TickerTableCell {
36
34
  text: string;
@@ -99,11 +97,10 @@ export interface TickerListTableViewProps {
99
97
  emptyTitle?: string;
100
98
  emptyHint?: string;
101
99
  /**
102
- * Starts the pane's add-ticker flow. Given, the empty table offers a
103
- * "+ ticker" button for it and says so before the key.
100
+ * The pane's own key for adding a ticker, which the empty table names. The
101
+ * command bar's key when absent. The pane shows its add control once, in its
102
+ * own row, so the empty table points at the key and offers no second one.
104
103
  */
105
- onAddTicker?: () => void;
106
- /** The pane's key for the same flow, named after the click. The command bar's key when absent. */
107
104
  addTickerShortcut?: string;
108
105
  virtualize?: boolean;
109
106
  overscan?: number;
@@ -237,24 +234,12 @@ export function TickerListTableView({
237
234
  rowMenuAccelerators,
238
235
  emptyTitle = t("No tickers."),
239
236
  emptyHint,
240
- onAddTicker,
241
237
  addTickerShortcut,
242
238
  virtualize = true,
243
239
  overscan = 4,
244
240
  }: TickerListTableViewProps) {
245
241
  const commandBarShortcut = useCommandBarShortcut();
246
- const addLabel = `+ ${t("ticker")}`;
247
- const resolvedEmptyHint = emptyHint ?? (onAddTicker
248
- ? tf("Click {button} to add one, or press {shortcut}.", { button: addLabel, shortcut: addTickerShortcut ?? commandBarShortcut })
249
- : tf("Press {shortcut} to add one.", { shortcut: commandBarShortcut }));
250
- const emptyContent = onAddTicker ? (
251
- <PaneStatusBody
252
- empty
253
- emptyTitle={emptyTitle}
254
- emptyMessage={resolvedEmptyHint}
255
- actions={<Button label="Add ticker" displayLabel={addLabel} compact onPress={onAddTicker} />}
256
- />
257
- ) : undefined;
242
+ const resolvedEmptyHint = emptyHint ?? tf("Press {shortcut} to add one.", { shortcut: addTickerShortcut ?? commandBarShortcut });
258
243
  const renderer = useRendererHost();
259
244
  const { showContextMenu } = useContextMenu();
260
245
  const { nativeContextMenu } = useUiCapabilities();
@@ -398,7 +383,6 @@ export function TickerListTableView({
398
383
  getRowVersion={getRowVersion}
399
384
  emptyStateTitle={emptyTitle}
400
385
  emptyStateHint={resolvedEmptyHint}
401
- emptyContent={emptyContent}
402
386
  virtualize={virtualize}
403
387
  overscan={overscan}
404
388
  rootBefore={rootBefore}
@@ -7,6 +7,7 @@ import { Box, Text, TextAttributes, useUiCapabilities } from "../../ui";
7
7
  import { truncateToDisplayWidth } from "../../utils/format";
8
8
  import { CountryFlagLead } from "./country-flag";
9
9
  import { usePaneLinkMenuEntry } from "./external-link";
10
+ import { ICON_GLYPHS, Icon, type IconName } from "./icon";
10
11
  import { titleCase } from "./header-case";
11
12
  import { statToneColor, type StatTone } from "./stat-grid";
12
13
 
@@ -199,6 +200,10 @@ export interface BadgeProps {
199
200
  /** For a domain scale, such as a sentiment score or chart series. */
200
201
  color?: string;
201
202
  variant?: "subtle" | "solid";
203
+ /** A mark before the label, in its colour: an SVG on the desktop, its glyph in the terminal. */
204
+ icon?: IconName;
205
+ /** False for a label that is not English to translate, such as a username. */
206
+ translate?: boolean;
202
207
  }
203
208
 
204
209
  /**
@@ -210,8 +215,9 @@ export const NATIVE_BADGE_STYLE = { height: "calc(100% - 2px)", borderRadius: "v
210
215
  /** How far a subtle chip's fill is tinted toward its tone. */
211
216
  export const BADGE_TINT = 0.28;
212
217
  const NATIVE_BADGE_TEXT_STYLE = { fontWeight: "var(--gloom-weight-regular)" };
218
+ const NATIVE_ICON_BADGE_STYLE = { ...NATIVE_BADGE_STYLE, gap: "var(--gloom-space-xs)" };
213
219
 
214
- export function Badge({ label, tone = "neutral", color, variant = "subtle" }: BadgeProps) {
220
+ export function Badge({ label, tone = "neutral", color, variant = "subtle", icon, translate = true }: BadgeProps) {
215
221
  const colors = useThemeColors();
216
222
  const { nativePaneChrome } = useUiCapabilities();
217
223
  const accent = color ?? (tone === "neutral" ? colors.textDim : tone === "accent" ? colors.borderFocused : colors[tone]);
@@ -220,10 +226,19 @@ export function Badge({ label, tone = "neutral", color, variant = "subtle" }: Ba
220
226
  const background = neutral ? colors.selected : solid ? accent : blendHex(colors.bg, accent, BADGE_TINT);
221
227
  const baseForeground = neutral ? colors.selectedText : solid ? colors.bg : accent;
222
228
  const foreground = blendForContrast(baseForeground, background, higherContrast("#000000", "#ffffff", background), 4.5);
229
+ const text = translate ? t(label) : label;
230
+ const nativeIcon = nativePaneChrome && icon;
231
+ // The terminal draws the icon's glyph as text in front of the label.
232
+ const shown = icon && !nativePaneChrome ? [ICON_GLYPHS[icon], text].filter(Boolean).join(" ") : text;
223
233
  const chip = (
224
234
  <Box height={nativePaneChrome ? undefined : 1} paddingX={1} backgroundColor={background}
225
- data-gloom-ui="badge" justifyContent={nativePaneChrome ? "center" : undefined} style={nativePaneChrome ? NATIVE_BADGE_STYLE : undefined}>
226
- <Text fg={foreground} attributes={TextAttributes.BOLD} style={nativePaneChrome ? NATIVE_BADGE_TEXT_STYLE : undefined}>{t(label)}</Text>
235
+ data-gloom-ui="badge" {...(nativeIcon ? { flexDirection: "row", alignItems: "center" } as const : {})}
236
+ justifyContent={nativePaneChrome && !icon ? "center" : undefined}
237
+ style={nativePaneChrome ? icon ? NATIVE_ICON_BADGE_STYLE : NATIVE_BADGE_STYLE : undefined}>
238
+ {nativeIcon ? <Icon name={nativeIcon} color={foreground} /> : null}
239
+ {shown || !nativeIcon ? (
240
+ <Text fg={foreground} attributes={TextAttributes.BOLD} style={nativePaneChrome ? NATIVE_BADGE_TEXT_STYLE : undefined}>{shown}</Text>
241
+ ) : null}
227
242
  </Box>
228
243
  );
229
244
  return nativePaneChrome ? <Box height={1} flexShrink={0} justifyContent="center">{chip}</Box> : chip;
@@ -38,7 +38,8 @@ export type IconName =
38
38
  | "team"
39
39
  | "pro"
40
40
  | "clock"
41
- | "edit";
41
+ | "edit"
42
+ | "mail";
42
43
 
43
44
  /** Terminal glyph for each icon; the desktop host draws SVG. */
44
45
  export const ICON_GLYPHS: Record<IconName, string> = {
@@ -84,6 +85,8 @@ export const ICON_GLYPHS: Record<IconName, string> = {
84
85
  // Time, for data that arrives late (a delayed quote).
85
86
  clock: "◷",
86
87
  edit: "✎",
88
+ // A message written to you (a DM): text presentation, one cell.
89
+ mail: "✉",
87
90
  };
88
91
 
89
92
  export interface IconProps {
@@ -108,11 +108,13 @@ export function createPluginCache<T, Stored = T>(options: {
108
108
  }
109
109
  return { value, fetchedAt, staleAt: fetchedAt + options.policy.staleMs, expiresAt: fetchedAt + options.policy.expireMs, source: options.source };
110
110
  } });
111
+ // A retained copy carries its refresh failure, whatever was thrown (an Error without a message included).
112
+ const failed = "refreshError" in result;
111
113
  const error = result.refreshError;
112
114
  return {
113
- data: result.value, fetchedAt: result.fetchedAt, stale: !!error || result.staleAt <= Date.now(),
114
- source: error ? "stale-fallback" : result === before ? "cache" : "network",
115
- ...(error ? { error, refreshError: error instanceof Error ? error.message : String(error) } : {}),
115
+ data: result.value, fetchedAt: result.fetchedAt, stale: failed || result.staleAt <= Date.now(),
116
+ source: failed ? "stale-fallback" : result === before ? "cache" : "network",
117
+ ...(failed ? { error, refreshError: error instanceof Error ? error.message : String(error) } : {}),
116
118
  };
117
119
  },
118
120
  };
@@ -56,6 +56,14 @@ export function savedCopyMark(value: unknown): object {
56
56
  return savedAt != null ? { [SAVED_AT]: savedAt } : {};
57
57
  }
58
58
 
59
+ /**
60
+ * The refresh failure a command still prints for `result`: none when a saved
61
+ * copy answered for it (`servedFromSavedCopy`), whose closing line says so.
62
+ */
63
+ export function unansweredRefreshError(result: { refreshError?: string | null } | null | undefined): string | null {
64
+ return result?.refreshError && savedCopyAt(result) == null ? result.refreshError : null;
65
+ }
66
+
59
67
  /** When `value` itself was saved, if it is a saved copy served after a failed refresh; null otherwise. */
60
68
  export function savedCopyAt(value: unknown): number | null {
61
69
  const savedAt = value && typeof value === "object" ? (value as Marked)[SAVED_AT] : undefined;
@@ -1684,6 +1684,13 @@
1684
1684
  "category": "app",
1685
1685
  "lesson": "/academy/terminal-and-workspace/how-the-terminal-works/help-cards"
1686
1686
  },
1687
+ {
1688
+ "code": "ROADMAP",
1689
+ "name": "Roadmap",
1690
+ "description": "What is coming next",
1691
+ "detail": "What is being built, what is planned and what shipped, with each item's votes. Pro members vote on what is building or planned, once per item, and can take the vote back.",
1692
+ "category": "app"
1693
+ },
1687
1694
  {
1688
1695
  "code": "HELP",
1689
1696
  "name": "Help",
@@ -1799,7 +1806,7 @@
1799
1806
  },
1800
1807
  {
1801
1808
  "type": "code",
1802
- "code": "bun install -g gloomberb"
1809
+ "code": "bun install -g @gloom-sh/terminal"
1803
1810
  },
1804
1811
  {
1805
1812
  "type": "heading",
@@ -1853,10 +1860,14 @@
1853
1860
  ],
1854
1861
  [
1855
1862
  "Bun",
1856
- "`bun install -g gloomberb@latest`"
1863
+ "`bun install -g @gloom-sh/terminal@latest`"
1857
1864
  ]
1858
1865
  ]
1859
1866
  },
1867
+ {
1868
+ "type": "paragraph",
1869
+ "text": "An existing `gloomberb` install keeps updating as it is."
1870
+ },
1860
1871
  {
1861
1872
  "type": "paragraph",
1862
1873
  "text": "Installed before 0.18.4, when the command was `gloomberb`? Updating adds `gloom` beside it and `gloomberb` keeps working, with nothing to reinstall."
@@ -5469,7 +5480,7 @@
5469
5480
  "functions",
5470
5481
  "screenshots"
5471
5482
  ],
5472
- "updatedAt": "2026-10-12",
5483
+ "updatedAt": "2026-10-27",
5473
5484
  "blocks": [
5474
5485
  {
5475
5486
  "type": "paragraph",
@@ -5641,8 +5652,8 @@
5641
5652
  "Ticker and quick notes"
5642
5653
  ],
5643
5654
  [
5644
- "`alerts list`, `alerts add <symbol> <above|below|crosses> <price>`, `alerts delete|rearm <id>`",
5645
- "Price alerts"
5655
+ "`alerts list`, `alerts add ...`, `alerts delete|rearm <id>`",
5656
+ "Price, valuation, daily technical and event alerts, every kind the `ALRT` pane offers; the forms are below"
5646
5657
  ]
5647
5658
  ]
5648
5659
  },
@@ -5650,6 +5661,22 @@
5650
5661
  "type": "paragraph",
5651
5662
  "text": "[Portfolios and rebalancing](/docs/portfolio) walks through targets and the drift table. `CASH` in `portfolio target` is the cash line; `portfolio add` and `position set` refuse a bare `CASH` (the stock is `CASH:NASDAQ`). `portfolio` and `watchlist` write immediately and ignore `--dry-run`. Sign-in, account management and chat are app-only."
5652
5663
  },
5664
+ {
5665
+ "type": "paragraph",
5666
+ "text": "The alerts commands, as `gloom help alerts` lists them:"
5667
+ },
5668
+ {
5669
+ "type": "code",
5670
+ "code": "gloom alerts list\ngloom alerts add <symbol> <above|below|crosses> <price>\ngloom alerts add <symbol> <pe|ps|rsi(14)> <above|below> <level>\ngloom alerts add <symbol> ma(50) crosses <above|below> ma(200)\ngloom alerts add <symbol> closes <above|below> ma(200)\ngloom alerts add <symbol>/<benchmark> <above|below|crosses> <ratio>\ngloom alerts add <symbol> rrg <leading|weakening|lagging|improving> [--benchmark <symbol>]\ngloom alerts add <symbol> earnings [--days <n> | results]\ngloom alerts add <calendar event> [--country <code>] [--days <n>]\ngloom alerts add [symbol] news <keyword>\ngloom alerts add [symbol] flow [premium] [calls|puts] [sweeps|blocks]\ngloom alerts add <symbol> <iv|rr> <above|below> <level>\ngloom alerts add <OCC contract> iv spike <points>\ngloom alerts add <symbol> 52-week <high|low>\ngloom alerts add <symbol> unusual volume [<multiple>x]\ngloom alerts add <symbol> filing [8-K|10-K|10-Q|S-1|13D|13D/A|13G|13G/A|6-K|20-F]\ngloom alerts add <symbol> insider [buy|sell]\ngloom alerts add <symbol> short interest [increase|decrease] [<percent>%]\ngloom alerts add <symbol> analyst [upgrade|downgrade]\ngloom alerts add <symbol> hiring [accelerating|slowing]\ngloom alerts add perps <market> <funding|oi|premium|closed-premium> <above|below> <percent>\ngloom alerts add catalyst [type <type> | agency <agency> | country <code>]\ngloom alerts add <symbol> catalyst\ngloom alerts add congress [member name]\ngloom alerts add 13f <fund CIK>\ngloom alerts delete <id>\ngloom alerts rearm <id>"
5671
+ },
5672
+ {
5673
+ "type": "paragraph",
5674
+ "text": "Words none of these forms read are refused with the list of kinds. Adding a rule that already exists names the existing alert's ID and adds nothing, and `rearm` on an alert that has not gone off says it is already active. An RSI or moving-average alert is always saved: it reads the same daily closes `history` shows as final and says when it is first checked."
5675
+ },
5676
+ {
5677
+ "type": "paragraph",
5678
+ "text": "`alerts list` shows each alert's listing with its currency, its last reading with when it is from (or `not checked yet`) and when it was created, in ISO 8601 UTC. `alerts list --csv` keeps its columns in order (`id,kind,target,trigger,eventDate,status,lastCheckedPrice,triggeredAt`) and adds `createdAt,listing,currency,lastReading,lastReadingAt` after them. Creating, re-arming and deleting an alert go in the app's alert log beside the alerts that fired (`gloom fn ALRT --tab history`)."
5679
+ },
5653
5680
  {
5654
5681
  "type": "heading",
5655
5682
  "level": 2,
package/src/i18n/es.ts CHANGED
@@ -506,10 +506,8 @@ export const es: Record<string, string> = {
506
506
  "Pin Ticker": "Fijar ticker",
507
507
 
508
508
  // ── Help pane ────────────────────────────────────────────────
509
- "How To Use Gloom": "Cómo usar Gloom",
510
509
  "Not sure of a code? Click the search bar at the top, or press {key}, and type what you want in plain words, like \"top news\". The Functions tab lists everything.": "¿No sabes el código? Haz clic en la barra de búsqueda de arriba, o pulsa {key}, y escribe lo que quieres con tus palabras, como \"top news\". La pestaña Funciones las lista todas.",
511
510
  "Not sure of a code? Click the search bar at the top and type what you want in plain words, like \"top news\". The Functions tab lists everything.": "¿No sabes el código? Haz clic en la barra de búsqueda de arriba y escribe lo que quieres con tus palabras, como \"top news\". La pestaña Funciones las lista todas.",
512
- "Click anything to use it; the keys shown beside actions are shortcuts.": "Haz clic en cualquier cosa para usarla; las teclas junto a las acciones son atajos.",
513
511
  "Open Help": "Abrir la ayuda",
514
512
  "Command Prefixes": "Prefijos de comandos",
515
513
  "Window Templates": "Plantillas de ventana",
@@ -519,7 +517,6 @@ export const es: Record<string, string> = {
519
517
  "Scrolling": "Desplazamiento",
520
518
  "Open Debug Log": "Abrir registro de depuración",
521
519
  "GitHub Issues": "GitHub Issues",
522
- "Docked panes stay in the saved layout.": "Los paneles anclados permanecen en el diseño guardado.",
523
520
  "Floating panes can be dragged by the title bar and resized from the lower-right corner.": "Los paneles flotantes se arrastran por la barra de título y se redimensionan desde la esquina inferior derecha.",
524
521
  "Use Layout Actions for split, move, duplicate, close all floating panes, undo, redo, and layout presets.": "Usa Acciones de diseño para dividir, mover, duplicar, cerrar paneles flotantes, deshacer, rehacer y diseños predefinidos.",
525
522
  "Move through focused table and list rows.": "Moverse por las filas de tablas y listas activas.",
@@ -564,7 +561,6 @@ export const es: Record<string, string> = {
564
561
  "Go back from a nested command screen when the query is empty.": "Volver de una pantalla de comando anidada cuando la consulta está vacía.",
565
562
  "Reorder ordered multi-select choices.": "Reordenar opciones de selección múltiple ordenadas.",
566
563
  "Submit multiline command forms.": "Enviar formularios de comando multilínea.",
567
- "Layout Basics": "Fundamentos del diseño",
568
564
 
569
565
  // ── Onboarding ───────────────────────────────────────────────
570
566
  "The open terminal for modern finance.": "La terminal abierta para las finanzas modernas.",
@@ -1672,6 +1668,8 @@ export const es: Record<string, string> = {
1672
1668
  "Guides": "Guías",
1673
1669
  "Academy": "Academia",
1674
1670
  "offline, saved copy": "sin conexión, copia guardada",
1671
+ "company data from {listing}": "datos de la empresa de {listing}",
1672
+ "from {listing}": "de {listing}",
1675
1673
  "Open in Browser": "Abrir en el navegador",
1676
1674
  "No docs match \"{query}\".": "Nada en la documentación coincide con \"{query}\".",
1677
1675
  "No docs for {name} yet.": "Aún no hay documentación para {name}.",
package/src/i18n/ja.ts CHANGED
@@ -507,10 +507,8 @@ export const ja: Record<string, string> = {
507
507
  "Pin Ticker": "銘柄を固定",
508
508
 
509
509
  // ── Help pane ────────────────────────────────────────────────
510
- "How To Use Gloom": "Gloom の使用方法",
511
510
  "Not sure of a code? Click the search bar at the top, or press {key}, and type what you want in plain words, like \"top news\". The Functions tab lists everything.": "コードが分からない場合は、上部の検索バーをクリックするか {key} を押し、\"top news\" のように普段の言葉で入力してください。「関数」タブに全機能があります。",
512
511
  "Not sure of a code? Click the search bar at the top and type what you want in plain words, like \"top news\". The Functions tab lists everything.": "コードが分からない場合は、上部の検索バーをクリックし、\"top news\" のように普段の言葉で入力してください。「関数」タブに全機能があります。",
513
- "Click anything to use it; the keys shown beside actions are shortcuts.": "どこでもクリックして操作できます。操作の横に表示されるキーはショートカットです。",
514
512
  "Open Help": "ヘルプを開く",
515
513
  "Command Prefixes": "コマンド接頭辞",
516
514
  "Window Templates": "ウィンドウ テンプレート",
@@ -520,7 +518,6 @@ export const ja: Record<string, string> = {
520
518
  "Scrolling": "スクロール",
521
519
  "Open Debug Log": "デバッグ ログを開く",
522
520
  "GitHub Issues": "GitHub Issues",
523
- "Docked panes stay in the saved layout.": "ドッキングされたペインは保存されたレイアウトに残ります。",
524
521
  "Floating panes can be dragged by the title bar and resized from the lower-right corner.": "フローティング ペインは、タイトル バーをドラッグして、右下隅からサイズを変更できます。",
525
522
  "Use Layout Actions for split, move, duplicate, close all floating panes, undo, redo, and layout presets.": "レイアウト アクションを使用して、分割、移動、複製、すべてのフローティング ペインを閉じる、元に戻す、やり直し、およびレイアウト プリセットを実行します。",
526
523
  "Move through focused table and list rows.": "フォーカスされたテーブルとリストの行を移動します。",
@@ -565,7 +562,6 @@ export const ja: Record<string, string> = {
565
562
  "Go back from a nested command screen when the query is empty.": "クエリが空の場合、ネストされたコマンド画面から戻ります。",
566
563
  "Reorder ordered multi-select choices.": "順序付けされた複数選択の選択肢を並べ替えます。",
567
564
  "Submit multiline command forms.": "複数行のコマンド フォームを送信します。",
568
- "Layout Basics": "レイアウトの基本",
569
565
 
570
566
  // ── Onboarding ───────────────────────────────────────────────
571
567
  "The open terminal for modern finance.": "現代の金融に開かれたターミナル。",
@@ -1668,6 +1664,8 @@ export const ja: Record<string, string> = {
1668
1664
  "Guides": "ガイド",
1669
1665
  "Academy": "アカデミー",
1670
1666
  "offline, saved copy": "オフライン、保存済みのコピー",
1667
+ "company data from {listing}": "{listing} の企業データ",
1668
+ "from {listing}": "{listing} から",
1671
1669
  "Open in Browser": "ブラウザで開く",
1672
1670
  "No docs match \"{query}\".": "「{query}」に一致するドキュメントはありません。",
1673
1671
  "No docs for {name} yet.": "{name} のドキュメントはまだありません。",
package/src/i18n/ko.ts CHANGED
@@ -505,10 +505,8 @@ export const ko: Record<string, string> = {
505
505
  "Pin Ticker": "종목 고정",
506
506
 
507
507
  // ── Help pane ────────────────────────────────────────────────
508
- "How To Use Gloom": "Gloom 사용 방법",
509
508
  "Not sure of a code? Click the search bar at the top, or press {key}, and type what you want in plain words, like \"top news\". The Functions tab lists everything.": "코드를 모르겠다면 위쪽 검색창을 클릭하거나 {key}을 누르고 \"top news\"처럼 원하는 것을 평범한 말로 입력하세요. 기능 탭에 전체 목록이 있습니다.",
510
509
  "Not sure of a code? Click the search bar at the top and type what you want in plain words, like \"top news\". The Functions tab lists everything.": "코드를 모르겠다면 위쪽 검색창을 클릭하고 \"top news\"처럼 원하는 것을 평범한 말로 입력하세요. 기능 탭에 전체 목록이 있습니다.",
511
- "Click anything to use it; the keys shown beside actions are shortcuts.": "무엇이든 클릭해서 사용하세요. 동작 옆에 표시된 키는 단축키입니다.",
512
510
  "Open Help": "도움말 열기",
513
511
  "Command Prefixes": "명령 접두사",
514
512
  "Window Templates": "창 템플릿",
@@ -518,7 +516,6 @@ export const ko: Record<string, string> = {
518
516
  "Scrolling": "스크롤",
519
517
  "Open Debug Log": "디버그 로그 열기",
520
518
  "GitHub Issues": "GitHub 이슈",
521
- "Docked panes stay in the saved layout.": "도킹된 패널은 저장된 레이아웃에 유지됩니다.",
522
519
  "Floating panes can be dragged by the title bar and resized from the lower-right corner.": "플로팅 패널은 제목 표시줄로 이동하고 오른쪽 아래에서 크기를 조절할 수 있습니다.",
523
520
  "Use Layout Actions for split, move, duplicate, close all floating panes, undo, redo, and layout presets.": "레이아웃 작업에서 분할, 이동, 복제, 플로팅 패널 모두 닫기, 실행 취소, 다시 실행 및 프리셋을 관리합니다.",
524
521
  "Move through focused table and list rows.": "포커스된 표와 목록의 행을 이동합니다.",
@@ -563,7 +560,6 @@ export const ko: Record<string, string> = {
563
560
  "Go back from a nested command screen when the query is empty.": "쿼리가 비어 있으면 중첩된 명령 화면에서 돌아갑니다.",
564
561
  "Reorder ordered multi-select choices.": "순서가 있는 다중 선택 항목을 재정렬합니다.",
565
562
  "Submit multiline command forms.": "여러 줄 명령 양식을 제출합니다.",
566
- "Layout Basics": "레이아웃 기본 사항",
567
563
 
568
564
  // ── Onboarding ───────────────────────────────────────────────
569
565
  "The open terminal for modern finance.": "현대 금융을 위한 오픈 소스 터미널.",
@@ -1666,6 +1662,8 @@ export const ko: Record<string, string> = {
1666
1662
  "Guides": "가이드",
1667
1663
  "Academy": "아카데미",
1668
1664
  "offline, saved copy": "오프라인, 저장된 사본",
1665
+ "company data from {listing}": "{listing}의 기업 데이터",
1666
+ "from {listing}": "{listing}에서",
1669
1667
  "Open in Browser": "브라우저에서 열기",
1670
1668
  "No docs match \"{query}\".": "\"{query}\"와 일치하는 문서가 없습니다.",
1671
1669
  "No docs for {name} yet.": "{name} 문서가 아직 없습니다.",
package/src/i18n/zh-cn.ts CHANGED
@@ -507,10 +507,8 @@ export const zhCN: Record<string, string> = {
507
507
  "Pin Ticker": "固定股票",
508
508
 
509
509
  // ── Help pane ────────────────────────────────────────────────
510
- "How To Use Gloom": "如何使用 Gloom",
511
510
  "Not sure of a code? Click the search bar at the top, or press {key}, and type what you want in plain words, like \"top news\". The Functions tab lists everything.": "不知道代码?点击顶部的搜索栏,或按 {key},用日常语言输入你想要的,比如 \"top news\"。“功能”标签列出了全部功能。",
512
511
  "Not sure of a code? Click the search bar at the top and type what you want in plain words, like \"top news\". The Functions tab lists everything.": "不知道代码?点击顶部的搜索栏,用日常语言输入你想要的,比如 \"top news\"。“功能”标签列出了全部功能。",
513
- "Click anything to use it; the keys shown beside actions are shortcuts.": "点击任意内容即可使用;操作旁显示的按键是快捷键。",
514
512
  "Open Help": "打开帮助",
515
513
  "Command Prefixes": "命令前缀",
516
514
  "Window Templates": "窗口模板",
@@ -520,7 +518,6 @@ export const zhCN: Record<string, string> = {
520
518
  "Scrolling": "滚动",
521
519
  "Open Debug Log": "打开调试日志",
522
520
  "GitHub Issues": "GitHub 问题反馈",
523
- "Docked panes stay in the saved layout.": "停靠面板会保留在已存布局中。",
524
521
  "Floating panes can be dragged by the title bar and resized from the lower-right corner.": "浮动面板可拖动标题栏移动,右下角调整大小。",
525
522
  "Use Layout Actions for split, move, duplicate, close all floating panes, undo, redo, and layout presets.": "用布局操作完成拆分、移动、复制、关闭全部浮动面板、撤销、重做与布局预设。",
526
523
  "Move through focused table and list rows.": "在聚焦的表格与列表行间移动。",
@@ -565,7 +562,6 @@ export const zhCN: Record<string, string> = {
565
562
  "Go back from a nested command screen when the query is empty.": "查询为空时从嵌套命令界面返回。",
566
563
  "Reorder ordered multi-select choices.": "调整有序多选项的顺序。",
567
564
  "Submit multiline command forms.": "提交多行命令表单。",
568
- "Layout Basics": "布局基础",
569
565
 
570
566
  // ── Onboarding ───────────────────────────────────────────────
571
567
  "The open terminal for modern finance.": "面向现代金融的开源终端。",
@@ -1668,6 +1664,8 @@ export const zhCN: Record<string, string> = {
1668
1664
  "Guides": "指南",
1669
1665
  "Academy": "学院",
1670
1666
  "offline, saved copy": "离线,已保存的副本",
1667
+ "company data from {listing}": "公司数据来自 {listing}",
1668
+ "from {listing}": "来自 {listing}",
1671
1669
  "Open in Browser": "在浏览器中打开",
1672
1670
  "No docs match \"{query}\".": "没有与“{query}”匹配的文档。",
1673
1671
  "No docs for {name} yet.": "{name} 暂无文档。",
package/src/i18n/zh-tw.ts CHANGED
@@ -507,10 +507,8 @@ export const zhTW: Record<string, string> = {
507
507
  "Pin Ticker": "固定股票",
508
508
 
509
509
  // ── Help pane ────────────────────────────────────────────────
510
- "How To Use Gloom": "如何使用 Gloom",
511
510
  "Not sure of a code? Click the search bar at the top, or press {key}, and type what you want in plain words, like \"top news\". The Functions tab lists everything.": "不知道代碼?點擊頂部的搜尋列,或按 {key},用日常用語輸入你想要的,例如 \"top news\"。「功能」標籤列出了全部功能。",
512
511
  "Not sure of a code? Click the search bar at the top and type what you want in plain words, like \"top news\". The Functions tab lists everything.": "不知道代碼?點擊頂部的搜尋列,用日常用語輸入你想要的,例如 \"top news\"。「功能」標籤列出了全部功能。",
513
- "Click anything to use it; the keys shown beside actions are shortcuts.": "點擊任何內容即可使用;動作旁顯示的按鍵是快捷鍵。",
514
512
  "Open Help": "開啟說明",
515
513
  "Command Prefixes": "命令字首",
516
514
  "Window Templates": "視窗範本",
@@ -520,7 +518,6 @@ export const zhTW: Record<string, string> = {
520
518
  "Scrolling": "捲動",
521
519
  "Open Debug Log": "開啟除錯日誌",
522
520
  "GitHub Issues": "GitHub 問題回報",
523
- "Docked panes stay in the saved layout.": "停靠面板會保留在已儲存的版面設定中。",
524
521
  "Floating panes can be dragged by the title bar and resized from the lower-right corner.": "拖曳標題列可移動浮動面板,從右下角可調整大小。",
525
522
  "Use Layout Actions for split, move, duplicate, close all floating panes, undo, redo, and layout presets.": "用版面設定操作完成拆分、移動、複製、關閉全部浮動面板、撤銷、重做與版面設定預設。",
526
523
  "Move through focused table and list rows.": "在目前表格與清單的各列之間移動。",
@@ -565,7 +562,6 @@ export const zhTW: Record<string, string> = {
565
562
  "Go back from a nested command screen when the query is empty.": "查詢為空時,從巢狀命令畫面回到上一層。",
566
563
  "Reorder ordered multi-select choices.": "調整有序多選項的順序。",
567
564
  "Submit multiline command forms.": "提交多行命令表單。",
568
- "Layout Basics": "版面設定基礎",
569
565
 
570
566
  // ── Onboarding ───────────────────────────────────────────────
571
567
  "The open terminal for modern finance.": "現代金融的開源終端機。",
@@ -1668,6 +1664,8 @@ export const zhTW: Record<string, string> = {
1668
1664
  "Guides": "指南",
1669
1665
  "Academy": "學院",
1670
1666
  "offline, saved copy": "離線,已儲存的副本",
1667
+ "company data from {listing}": "公司資料來自 {listing}",
1668
+ "from {listing}": "來自 {listing}",
1671
1669
  "Open in Browser": "在瀏覽器中開啟",
1672
1670
  "No docs match \"{query}\".": "沒有符合「{query}」的文件。",
1673
1671
  "No docs for {name} yet.": "{name} 尚無文件。",
@@ -1,4 +1,4 @@
1
- import type { DepositaryPair, Fundamentals, Quote } from "../types/financials";
1
+ import type { CedearRatio, DepositaryPair, Fundamentals, Quote } from "../types/financials";
2
2
  import { canonicalExchange, parsePublicTickerKey } from "../utils/exchanges";
3
3
  import { formatNumber } from "../utils/format";
4
4
  import { formatUtcDayTime } from "../utils/utc-time";
@@ -41,6 +41,30 @@ export function ratioCount(ratio: number): string {
41
41
  return Number.isInteger(ratio) ? String(ratio) : String(Number(ratio.toPrecision(6)));
42
42
  }
43
43
 
44
+ type CedearRecords = { quote?: Pick<Quote, "cedearRatio"> | null; fundamentals?: Pick<Fundamentals, "cedearRatio"> | null };
45
+
46
+ const positiveCount = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value > 0;
47
+
48
+ /** A served CEDEAR ratio, when it is whole. */
49
+ function readCedearRatio(value: unknown): CedearRatio | null {
50
+ if (!value || typeof value !== "object") return null;
51
+ const ratio = value as Partial<CedearRatio>;
52
+ if (typeof ratio.listing !== "string" || !ratio.listing.trim()) return null;
53
+ if (typeof ratio.company !== "string" || !ratio.company.trim()) return null;
54
+ if (!positiveCount(ratio.receipts) || !positiveCount(ratio.shares)) return null;
55
+ return ratio as CedearRatio;
56
+ }
57
+
58
+ /** The CEDEAR ratio a listing's records state: the quote's, else the fundamentals'. */
59
+ export function listingCedearRatio(records: CedearRecords | null | undefined): CedearRatio | null {
60
+ return readCedearRatio(records?.quote?.cedearRatio) ?? readCedearRatio(records?.fundamentals?.cedearRatio);
61
+ }
62
+
63
+ /** A CEDEAR's ratio in its company's shares: `20 AAPL:BYMA = 1 Apple Inc. share`. */
64
+ export function cedearRatioText(ratio: CedearRatio): string {
65
+ return `${ratioCount(ratio.receipts)} ${ratio.listing} = ${ratioCount(ratio.shares)} ${ratio.company.trim()} share${ratio.shares === 1 ? "" : "s"}`;
66
+ }
67
+
44
68
  /** What a home line says of its receipt: `1 GGAL ADR = 10 of these shares`, and `1 of these shares` for one. */
45
69
  function homeLineRatioText(pair: DepositaryPair): string {
46
70
  return `1 ${pair.receipt} ADR = ${ratioCount(pair.ratio)} of these shares`;
@@ -2,6 +2,7 @@ import type { Quote, QuoteMetadata } from "../../types/financials";
2
2
  import { canonicalExchange, parsePublicTickerKey } from "../../utils/exchanges";
3
3
  import { getListingSymbolsToTry } from "../../sources/listing-symbols";
4
4
  import { currencyPairCode } from "../../utils/currency-pair";
5
+ import { exchangeLineCoinListing } from "../../utils/crypto-pair";
5
6
 
6
7
  export function quoteMetadataFromQuote(quote: Quote): QuoteMetadata {
7
8
  return {
@@ -44,6 +45,12 @@ export function mergeQuoteMetadata(known: QuoteMetadata | null | undefined, load
44
45
 
45
46
  /** Qualified listings must agree; provider suffix spellings use the existing exact-listing normalizer. */
46
47
  export function quoteMetadataMatchesTarget(metadata: QuoteMetadata, symbol: string, exchange?: string): boolean {
48
+ // A crypto exchange's line of a pair (BTC/GBP on COINBASE PRO) is priced as the crypto venue's listing, BTC-GBP on
49
+ // CCC, and a quote kept under the line's key is that listing's.
50
+ const line = exchangeLineCoinListing(symbol, exchange);
51
+ if (line) return quoteMetadataMatchesTarget(metadata, line.symbol, line.exchange);
52
+ const quotedLine = exchangeLineCoinListing(metadata.symbol, parsePublicTickerKey(metadata.symbol).exchange ? undefined : metadata.listingExchangeName);
53
+ if (quotedLine) metadata = { ...metadata, symbol: quotedLine.symbol, listingExchangeName: quotedLine.exchange };
47
54
  const target = parsePublicTickerKey(symbol);
48
55
  const actual = parsePublicTickerKey(metadata.symbol);
49
56
  // An index is one series whatever its venue is called: search files ^VIX under CXI and ^RUT under WCB, while
@@ -1,3 +1,4 @@
1
+ import type { CompanyFrom } from "../types/company-from";
1
2
  import { logFailure } from "../utils/log-failure";
2
3
  import type { NewsCapability } from "../capabilities";
3
4
  import type { ConnectionHealthRegistry } from "../core/connection-health";
@@ -63,6 +64,7 @@ interface SourceFetchResult {
63
64
  errors: string[];
64
65
  nextCursor: string | null;
65
66
  access: NewsAccess | null;
67
+ companyFrom?: CompanyFrom | null;
66
68
  }
67
69
 
68
70
  interface NewsQueryEntry {
@@ -451,6 +453,7 @@ export class NewsService {
451
453
  loadingMore: entry.state.loadingMore,
452
454
  // A page that does not say keeps what this session's earlier answer said.
453
455
  access: result.access ?? (sameSession ? entry.state.access ?? null : null),
456
+ companyFrom: result.companyFrom ?? null,
454
457
  };
455
458
  entry.state = state;
456
459
  entry.generation = generation;
@@ -550,7 +553,7 @@ export class NewsService {
550
553
  private async readSourcePage(
551
554
  source: NewsCapability,
552
555
  query: NewsQuery,
553
- ): Promise<{ articles: NewsArticle[]; nextCursor: string | null; error?: string | null; access?: NewsAccess | null }> {
556
+ ): Promise<{ articles: NewsArticle[]; nextCursor: string | null; error?: string | null; access?: NewsAccess | null; companyFrom?: CompanyFrom | null }> {
554
557
  if (source.provider.fetchNewsPage) {
555
558
  const page = await this.trackSourceRequest(
556
559
  source,
@@ -562,6 +565,7 @@ export class NewsService {
562
565
  nextCursor: page.nextCursor ?? null,
563
566
  error: page.error,
564
567
  access: page.access,
568
+ companyFrom: page.companyFrom ?? null,
565
569
  };
566
570
  }
567
571
  const articles = (await this.trackSourceRequest(
@@ -585,6 +589,7 @@ export class NewsService {
585
589
  errors: page.error ? [page.error] : [],
586
590
  nextCursor: page.nextCursor,
587
591
  access: page.access ?? null,
592
+ companyFrom: page.companyFrom ?? null,
588
593
  };
589
594
  if (page.articles.length > 0) return result;
590
595
  firstEmpty ??= result;
@@ -654,6 +659,7 @@ export class NewsService {
654
659
  nextCursor: entry.state.nextCursor,
655
660
  loadingMore: false,
656
661
  access: entry.state.access ?? null,
662
+ companyFrom: entry.state.companyFrom ?? null,
657
663
  };
658
664
  changed = true;
659
665
  }
package/src/news/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { CompanyFrom } from "../types/company-from";
2
+
1
3
  type NewsQueryScope = "global" | "ticker";
2
4
  export type NewsFeed = "latest" | "top" | "breaking" | "ticker" | "sector" | "topic";
3
5
  type NewsSentiment = "positive" | "neutral" | "negative";
@@ -95,6 +97,8 @@ export interface NewsPage {
95
97
  nextCursor?: string | null;
96
98
  /** Which news the source answered with; null or absent when the answer does not say. */
97
99
  access?: NewsAccess | null;
100
+ /** The company's home line the stories are read from, when not the listing asked (a receipt, a foreign line). */
101
+ companyFrom?: CompanyFrom | null;
98
102
  }
99
103
 
100
104
  export interface NewsQueryState {
@@ -107,4 +111,6 @@ export interface NewsQueryState {
107
111
  loadingMore: boolean;
108
112
  /** Which news the stories are, from the last answer; null or absent when it did not say. */
109
113
  access?: NewsAccess | null;
114
+ /** The company's home line the stories are read from, when not the listing asked. */
115
+ companyFrom?: CompanyFrom | null;
110
116
  }
@@ -3,7 +3,7 @@
3
3
  // release's rows as the calendar's feeds name it from week to week, and the
4
4
  // notifications sent before it and once its figures are out. CPI and FOMC are
5
5
  // two cases of the same alert, whose dates come from their own calendars.
6
- import { dateKey, eventCountry, matchesCountry, type CountryFilter } from "../econ/calendar-model";
6
+ import { dateKey, econEventName, eventCountry, matchesCountry, type CountryFilter } from "../econ/calendar-model";
7
7
  import { isAllDayEvent, hasNoReleaseTime, type EconEvent, type EconImpact } from "../econ/types";
8
8
  import { RENAMED_RELEASES } from "../shared/macro-releases";
9
9
  import { MACRO_EVENT_NAMES } from "./research-builder";
@@ -126,7 +126,11 @@ export interface CalendarEventChoice {
126
126
 
127
127
  const IMPACT_RANK: Record<EconImpact, number> = { high: 0, medium: 1, low: 2 };
128
128
 
129
- type RankedChoice = CalendarEventChoice & { words: string[] };
129
+ type RankedChoice = CalendarEventChoice & {
130
+ words: string[];
131
+ /** The words of the name `econ` shows it under, when that is not the feed's (the Fed's rate as its upper bound). */
132
+ shownWords: string[];
133
+ };
130
134
 
131
135
  /**
132
136
  * The calendar's releases, one per country and name, each with its next row
@@ -150,6 +154,7 @@ function calendarChoices(events: readonly EconEvent[], now: number): RankedChoic
150
154
  impact: rows.reduce<EconImpact>((best, event) => IMPACT_RANK[event.impact] < IMPACT_RANK[best] ? event.impact : best, "low"),
151
155
  next,
152
156
  words: calendarNameWords(named.event),
157
+ shownWords: calendarNameWords(econEventName(named)),
153
158
  };
154
159
  });
155
160
  }
@@ -246,7 +251,8 @@ function matchCalendarWords(
246
251
  now: number,
247
252
  ): CalendarEventMatch {
248
253
  const choices = calendarChoices(country ? events.filter((event) => matchesCountry(event, country)) : events, now);
249
- const exact = choices.filter((choice) => choice.words.join(" ") === words.join(" "));
254
+ // The name as the feed gives it, or as `econ` shows it: the alert keeps the feed's, which the calendar's rows carry.
255
+ const exact = choices.filter((choice) => [choice.words, choice.shownWords].some((name) => name.join(" ") === words.join(" ")));
250
256
  const naming = (typed: readonly string[], loose: boolean) => typed.length === 0 ? [] : choices.filter((choice) =>
251
257
  typed.every((word) => loose ? wordFound(word, choice.words) : choice.words.includes(word)));
252
258
  // CPI by the name one feed gives it everywhere but the US (China's Inflation Rate YoY).
@@ -278,7 +284,7 @@ function matchCalendarWords(
278
284
  return { kind: "match", choice: strip(ranked[0]!), others: ranked.slice(1).map(strip) };
279
285
  }
280
286
 
281
- function strip({ words: _words, ...choice }: RankedChoice): CalendarEventChoice {
287
+ function strip({ words: _words, shownWords: _shown, ...choice }: RankedChoice): CalendarEventChoice {
282
288
  return choice;
283
289
  }
284
290