gloomberb 0.17.2 → 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 (159) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/futures-curve.ts +16 -0
  3. package/src/api-client/index.ts +18 -8
  4. package/src/api-client/types.ts +3 -1
  5. package/src/cli/commands/market.ts +60 -9
  6. package/src/cli/commands/search.ts +43 -2
  7. package/src/cli/commands/ticker.ts +100 -21
  8. package/src/cli/desktop-pane-shot.ts +34 -9
  9. package/src/cli/fx-pair.ts +17 -0
  10. package/src/cli/glossary.ts +134 -6
  11. package/src/cli/history-rows.ts +16 -4
  12. package/src/cli/index.ts +44 -12
  13. package/src/cli/known-gaps.ts +21 -2
  14. package/src/cli/listing-arg.ts +19 -4
  15. package/src/cli/listing-suggestion.ts +33 -2
  16. package/src/cli/pane-functions/dom.ts +39 -6
  17. package/src/cli/pane-functions/freshness.ts +48 -5
  18. package/src/cli/pane-functions/function-help.ts +1 -1
  19. package/src/cli/pane-functions/headless.ts +85 -19
  20. package/src/cli/pane-functions/index.ts +60 -4
  21. package/src/cli/pane-functions/market-session.ts +69 -6
  22. package/src/cli/pane-functions/resolver.ts +4 -2
  23. package/src/cli/pane-functions/screenshot.ts +14 -1
  24. package/src/cli/scroll-truncation.ts +5 -1
  25. package/src/cli/symbol-args.ts +41 -3
  26. package/src/cli/symbol-list.ts +19 -7
  27. package/src/cli/unknown-command.ts +70 -0
  28. package/src/components/chart/native/surface/visibility.ts +37 -7
  29. package/src/components/layout/header.tsx +348 -102
  30. package/src/components/markdown-text.tsx +40 -66
  31. package/src/components/market-board/index.tsx +12 -6
  32. package/src/components/reader/detail-reader.tsx +28 -6
  33. package/src/components/ticker/badge/index.tsx +2 -0
  34. package/src/components/ui/display.tsx +6 -1
  35. package/src/components/ui/external-link.tsx +10 -2
  36. package/src/data/memory-resource-store.ts +1 -1
  37. package/src/data/resource-store.ts +13 -1
  38. package/src/docs/snapshot.json +13 -13
  39. package/src/i18n/es.ts +1 -0
  40. package/src/i18n/ja.ts +1 -0
  41. package/src/i18n/ko.ts +1 -0
  42. package/src/i18n/zh-cn.ts +1 -0
  43. package/src/i18n/zh-tw.ts +1 -0
  44. package/src/market-data/futures-contract.ts +10 -0
  45. package/src/market-data/market/format.ts +7 -2
  46. package/src/market-data/market/freshness.ts +25 -1
  47. package/src/market-data/quotes/regular-session-valuation.ts +32 -1
  48. package/src/market-data/quotes/resolution.ts +2 -0
  49. package/src/plugins/builtin/account-management/pane.tsx +17 -13
  50. package/src/plugins/builtin/attention/headless.ts +19 -10
  51. package/src/plugins/builtin/attention/model.ts +22 -0
  52. package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
  53. package/src/plugins/builtin/changelog/index.tsx +11 -8
  54. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
  55. package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
  56. package/src/plugins/builtin/chat/attachments/model.ts +5 -3
  57. package/src/plugins/builtin/chat/content/footer.tsx +6 -0
  58. package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
  59. package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
  60. package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
  61. package/src/plugins/builtin/chat/layout.ts +91 -31
  62. package/src/plugins/builtin/chat/markdown.ts +94 -0
  63. package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
  64. package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
  65. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  66. package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
  67. package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
  68. package/src/plugins/builtin/company-attention/headless.ts +8 -5
  69. package/src/plugins/builtin/company-attention/model.ts +53 -2
  70. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  71. package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
  72. package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
  73. package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
  74. package/src/plugins/builtin/cot/client.ts +12 -4
  75. package/src/plugins/builtin/cot/pane.tsx +2 -2
  76. package/src/plugins/builtin/crypto-board/model.ts +7 -3
  77. package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
  78. package/src/plugins/builtin/debug/index.tsx +15 -15
  79. package/src/plugins/builtin/dividend-yield/client.ts +8 -1
  80. package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
  81. package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
  82. package/src/plugins/builtin/docs/index.tsx +5 -3
  83. package/src/plugins/builtin/docs/pane.tsx +18 -1
  84. package/src/plugins/builtin/docs/targets.ts +5 -3
  85. package/src/plugins/builtin/earnings-calls/data.ts +22 -0
  86. package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
  87. package/src/plugins/builtin/equity-screener/headless.ts +4 -3
  88. package/src/plugins/builtin/equity-screener/live.ts +27 -1
  89. package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
  90. package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
  91. package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
  92. package/src/plugins/builtin/futures/index.tsx +32 -4
  93. package/src/plugins/builtin/futures/model.ts +65 -3
  94. package/src/plugins/builtin/futures/table.tsx +31 -11
  95. package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
  96. package/src/plugins/builtin/holders/pane.tsx +6 -1
  97. package/src/plugins/builtin/mna/client.ts +12 -2
  98. package/src/plugins/builtin/mna/detail.tsx +2 -2
  99. package/src/plugins/builtin/mna/pane.tsx +2 -2
  100. package/src/plugins/builtin/notes/owner.tsx +5 -4
  101. package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
  102. package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
  103. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  104. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
  105. package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
  106. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
  107. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -9
  108. package/src/plugins/builtin/research/event-model.ts +14 -0
  109. package/src/plugins/builtin/research/headless-schema.ts +31 -1
  110. package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
  111. package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
  112. package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
  113. package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
  114. package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
  115. package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
  116. package/src/plugins/builtin/research-search/data.ts +11 -0
  117. package/src/plugins/builtin/research-search/pane.tsx +13 -2
  118. package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
  119. package/src/plugins/builtin/seasonality/client.ts +23 -0
  120. package/src/plugins/builtin/seasonality/pane.tsx +4 -2
  121. package/src/plugins/builtin/sectors/index.tsx +7 -3
  122. package/src/plugins/builtin/shared/report-freshness.ts +6 -2
  123. package/src/plugins/builtin/short-interest/headless.ts +13 -3
  124. package/src/plugins/builtin/short-interest/index.tsx +1 -1
  125. package/src/plugins/builtin/short-interest/model.ts +62 -3
  126. package/src/plugins/builtin/short-interest/pane.tsx +2 -2
  127. package/src/plugins/builtin/short-interest/schedule.ts +107 -0
  128. package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
  129. package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
  130. package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
  131. package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
  132. package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
  133. package/src/plugins/builtin/themes/headless.ts +1 -1
  134. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
  135. package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
  136. package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
  137. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
  138. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
  139. package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
  140. package/src/plugins/builtin/volatility/chart-model.ts +1 -1
  141. package/src/plugins/builtin/volatility/charts.tsx +1 -1
  142. package/src/renderers/dom/styles.css +6 -0
  143. package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
  144. package/src/sources/provider-router/batches.ts +2 -1
  145. package/src/sources/provider-router/cache.ts +3 -0
  146. package/src/sync/baseline.ts +19 -14
  147. package/src/sync/controller.ts +140 -40
  148. package/src/sync/core-contributors.ts +256 -32
  149. package/src/sync/types.ts +44 -0
  150. package/src/time-series/resolve.ts +19 -2
  151. package/src/types/data-provider.ts +8 -0
  152. package/src/types/financials.ts +40 -4
  153. package/src/types/headless.ts +43 -0
  154. package/src/types/plugin.ts +7 -0
  155. package/src/utils/declared-dividends.ts +39 -0
  156. package/src/utils/forward-dividend.ts +34 -11
  157. package/src/utils/fundamentals.ts +13 -2
  158. package/src/utils/markdown.ts +180 -0
  159. package/src/utils/market-capitalization.ts +22 -0
@@ -1,7 +1,7 @@
1
1
  import type { ScrollBoxRenderable } from "../../../ui";
2
2
  import type { ChatMessage } from "../../../api-client";
3
3
  import { splitLongTextSegmentByDisplayWidth, truncateWithEllipsis } from "../../../utils/text-wrap";
4
- import { tokenizeInlineContent, type InlineContentToken } from "../../../utils/inline-content-tokenizer";
4
+ import { parseMarkdownLines } from "../../../utils/markdown";
5
5
  import { displayWidth } from "../../../utils/format";
6
6
  import {
7
7
  getTickerBadgeText,
@@ -12,6 +12,7 @@ import type { InlineTickerCatalogEntry } from "../../../state/hooks/inline-ticke
12
12
  import { STICKY_BOTTOM_THRESHOLD_PX } from "../../../ui/sticky-scroll";
13
13
  import { terminalReactionRows } from "./reactions";
14
14
  import { chatMessagePaneLinks } from "./pane-links/model";
15
+ import { chatInlineTokens, type ChatInlineToken, type ChatTextStyle } from "./markdown";
15
16
 
16
17
  const MESSAGE_GROUP_THRESHOLD_MS = 5 * 60 * 1000;
17
18
  const MESSAGE_SELECTION_BOTTOM_INSET = 1;
@@ -38,9 +39,8 @@ export function formatInlinePreview(text: string, width: number) {
38
39
  return truncateWithEllipsis(normalizeInlinePreview(text), width);
39
40
  }
40
41
 
41
- type ChatBodyInlineToken = InlineContentToken;
42
-
43
- export type ChatBodyLine = ChatBodyInlineToken[];
42
+ /** One row of a message in the terminal: its tokens, already wrapped to the body's width. */
43
+ export type ChatBodyLine = ChatInlineToken[];
44
44
 
45
45
  function wrapTextLines(text: string, width: number) {
46
46
  const safeWidth = Math.max(width, 1);
@@ -97,7 +97,7 @@ export function chatBadgeTextWidth(symbol: string, entry: InlineTickerCatalogEnt
97
97
  }
98
98
 
99
99
  function inlineTokenWidth(
100
- token: InlineContentToken,
100
+ token: ChatInlineToken,
101
101
  catalog: Record<string, InlineTickerCatalogEntry>,
102
102
  ): number {
103
103
  if (token.kind !== "ticker") return displayWidth(token.value);
@@ -110,11 +110,8 @@ function inlineTokenWidth(
110
110
  return chatBadgeTextWidth(token.symbol, entry) + TICKER_BADGE_CHROME_WIDTH;
111
111
  }
112
112
 
113
- function tokenWithValue(token: InlineContentToken, value: string): InlineContentToken {
114
- if (token.kind === "link") return { ...token, value };
115
- if (token.kind === "username") return { ...token, value };
116
- if (token.kind === "ticker") return { ...token, value };
117
- return { kind: "text", value };
113
+ function tokenWithValue(token: ChatInlineToken, value: string): ChatInlineToken {
114
+ return { ...token, value };
118
115
  }
119
116
 
120
117
  function firstDisplayWidthChunk(value: string, width: number): string {
@@ -136,8 +133,27 @@ function trimLineEnd(line: ChatBodyLine): ChatBodyLine {
136
133
  return next;
137
134
  }
138
135
 
139
- function wrapInlineContentLine(
140
- text: string,
136
+ function sameStyle(a: ChatTextStyle | undefined, b: ChatTextStyle | undefined): boolean {
137
+ return !!a?.bold === !!b?.bold && !!a?.italic === !!b?.italic && !!a?.strike === !!b?.strike
138
+ && !!a?.code === !!b?.code && !!a?.muted === !!b?.muted;
139
+ }
140
+
141
+ /** Neighbouring words of one style draw as one run. */
142
+ function joinTextTokens(line: ChatBodyLine): ChatBodyLine {
143
+ const joined: ChatBodyLine = [];
144
+ for (const token of line) {
145
+ const previous = joined[joined.length - 1];
146
+ if (token.kind === "text" && previous?.kind === "text" && sameStyle(previous.style, token.style)) {
147
+ joined[joined.length - 1] = { ...previous, value: previous.value + token.value };
148
+ } else {
149
+ joined.push(token);
150
+ }
151
+ }
152
+ return joined;
153
+ }
154
+
155
+ function wrapInlineTokens(
156
+ tokens: readonly ChatInlineToken[],
141
157
  width: number,
142
158
  catalog: Record<string, InlineTickerCatalogEntry>,
143
159
  ): ChatBodyLine[] {
@@ -147,12 +163,12 @@ function wrapInlineContentLine(
147
163
  let currentWidth = 0;
148
164
 
149
165
  const finishLine = () => {
150
- lines.push(trimLineEnd(current));
166
+ lines.push(joinTextTokens(trimLineEnd(current)));
151
167
  current = [];
152
168
  currentWidth = 0;
153
169
  };
154
170
 
155
- const appendSegment = (token: InlineContentToken, rawValue: string, trimStartOnWrap: boolean) => {
171
+ const appendSegment = (token: ChatInlineToken, rawValue: string, trimStartOnWrap: boolean) => {
156
172
  let value = currentWidth === 0 && trimStartOnWrap ? rawValue.trimStart() : rawValue;
157
173
  if (!value) return;
158
174
 
@@ -202,53 +218,99 @@ function wrapInlineContentLine(
202
218
  }
203
219
  };
204
220
 
205
- const appendTextPart = (rawPart: string) => {
221
+ const appendTextPart = (token: ChatInlineToken, rawPart: string) => {
206
222
  let part = rawPart;
207
223
  const partWidth = displayWidth(part);
208
224
  if (currentWidth > 0 && partWidth > safeWidth - currentWidth && partWidth <= safeWidth) {
209
225
  finishLine();
210
226
  part = part.trimStart();
211
227
  }
212
- appendSegment({ kind: "text", value: part }, part, true);
228
+ appendSegment(token, part, true);
213
229
  };
214
230
 
215
- const appendText = (value: string) => {
216
- const parts = value.match(/\S+\s*|\s+/g) ?? [value];
231
+ const appendText = (token: ChatInlineToken) => {
232
+ const parts = token.value.match(/\S+\s*|\s+/g) ?? [token.value];
217
233
  for (const part of parts) {
218
- appendTextPart(part);
234
+ appendTextPart(token, part);
219
235
  }
220
236
  };
221
237
 
222
- const appendInlineToken = (token: InlineContentToken) => {
238
+ const appendInlineToken = (token: ChatInlineToken) => {
223
239
  const tokenWidth = inlineTokenWidth(token, catalog);
224
240
  if (currentWidth > 0 && currentWidth + tokenWidth > safeWidth) finishLine();
225
241
  appendSegment(token, token.value, false);
226
242
  };
227
243
 
228
- for (const token of tokenizeInlineContent(text)) {
244
+ for (const token of tokens) {
229
245
  if (token.kind === "text") {
230
- appendText(token.value);
246
+ appendText(token);
231
247
  continue;
232
248
  }
233
249
  appendInlineToken(token);
234
250
  }
235
251
 
236
- if (current.length > 0 || lines.length === 0) lines.push(trimLineEnd(current));
252
+ if (current.length > 0 || lines.length === 0) finishLine();
237
253
  return lines;
238
254
  }
239
255
 
256
+ const QUOTE_BAR: ChatInlineToken = { kind: "text", value: "\u2502 ", style: { muted: true } };
257
+
258
+ /** Each wrapped row starts with `first` or, after the first, `rest`: a list item's marker and the hang under it, a quote's bar. */
259
+ function withPrefix(lines: ChatBodyLine[], first: ChatInlineToken, rest: ChatInlineToken): ChatBodyLine[] {
260
+ return lines.map((line, index) => [index === 0 ? first : rest, ...line]);
261
+ }
262
+
263
+ /** A line of a code block, as written, broken where the row ends. */
264
+ function codeRows(text: string, width: number): ChatBodyLine[] {
265
+ const value = text.replace(/\t/g, " ");
266
+ if (!value) return [[]];
267
+ return splitLongTextSegmentByDisplayWidth(value, Math.max(width, 1))
268
+ .map((chunk): ChatBodyLine => [{ kind: "text", value: chunk, style: { code: true } }]);
269
+ }
270
+
271
+ /**
272
+ * A message's rows in the terminal: its markdown read line by line (a heading
273
+ * bold, a list item's later rows hung under its text, a quote barred on every
274
+ * row, a code block as written) and wrapped to `width`.
275
+ */
240
276
  export function getMessageBodyTokenLines(
241
277
  text: string,
242
278
  width: number,
243
279
  catalog: Record<string, InlineTickerCatalogEntry>,
244
280
  ): ChatBodyLine[] {
245
281
  const lines: ChatBodyLine[] = [];
246
- for (const paragraph of text.split("\n")) {
247
- if (!paragraph) {
248
- lines.push([]);
249
- continue;
282
+ for (const line of parseMarkdownLines(text)) {
283
+ switch (line.kind) {
284
+ case "blank":
285
+ lines.push([]);
286
+ break;
287
+ case "code":
288
+ lines.push(...codeRows(line.text, width));
289
+ break;
290
+ case "heading":
291
+ lines.push(...wrapInlineTokens(
292
+ chatInlineTokens(line.text).map((token) => ({ ...token, style: { ...token.style, bold: true } })),
293
+ width,
294
+ catalog,
295
+ ));
296
+ break;
297
+ case "quote":
298
+ lines.push(...withPrefix(wrapInlineTokens(chatInlineTokens(line.text), width - 2, catalog), QUOTE_BAR, QUOTE_BAR));
299
+ break;
300
+ case "list": {
301
+ const marker = `${" ".repeat(line.indent)}${/^\d/.test(line.marker) ? line.marker : "\u2022"} `;
302
+ const markerWidth = displayWidth(marker);
303
+ lines.push(...withPrefix(
304
+ wrapInlineTokens(chatInlineTokens(line.text), width - markerWidth, catalog),
305
+ { kind: "text", value: marker, style: { muted: true } },
306
+ { kind: "text", value: " ".repeat(markerWidth) },
307
+ ));
308
+ break;
309
+ }
310
+ case "text":
311
+ lines.push(...wrapInlineTokens(chatInlineTokens(line.text), width, catalog));
312
+ break;
250
313
  }
251
- lines.push(...wrapInlineContentLine(paragraph, width, catalog));
252
314
  }
253
315
  return lines.length > 0 ? lines : [[]];
254
316
  }
@@ -267,9 +329,7 @@ export function estimateMessageHeight(
267
329
  // The terminal draws a row per image and one for a review note; an image-only message has no text row.
268
330
  const bodyLineCount = content.length === 0 && (imageCount > 0 || links.length > 0)
269
331
  ? 0
270
- : catalog
271
- ? getMessageBodyTokenLines(content, contentLineWidth, catalog).length
272
- : wrapTextLines(content, contentLineWidth).length;
332
+ : getMessageBodyTokenLines(content, contentLineWidth, catalog ?? {}).length;
273
333
  const reviewNoteHeight = message.attachmentReview ? 1 : 0;
274
334
  const reactionRows = message.reactions?.length ? terminalReactionRows(message.reactions, contentLineWidth).length : 0;
275
335
  return headerHeight + (message.replyTo ? 1 : 0) + bodyLineCount + links.length + imageCount + reviewNoteHeight + reactionRows;
@@ -0,0 +1,94 @@
1
+ import { markdownPlainText, parseInlineMarkdown, parseMarkdownLines, type MarkdownSegment } from "../../../utils/markdown";
2
+ import { safeExternalUrl } from "../../../utils/external-url";
3
+ import { tokenizeInlineContent, type InlineContentToken } from "../../../utils/inline-content-tokenizer";
4
+
5
+ /** How a run of message text is drawn. `muted` is the dim of a list marker, a quote's bar or a link's host. */
6
+ export interface ChatTextStyle {
7
+ bold?: boolean;
8
+ italic?: boolean;
9
+ strike?: boolean;
10
+ code?: boolean;
11
+ muted?: boolean;
12
+ }
13
+
14
+ /** Message text, a link, a $TICKER or an @mention, with the marks around it. */
15
+ export type ChatInlineToken = InlineContentToken & { style?: ChatTextStyle };
16
+
17
+ /** A message link opens in the browser, so only a web address is read as one. */
18
+ function isChatLinkHref(href: string): boolean {
19
+ return safeExternalUrl(href) !== null;
20
+ }
21
+
22
+ /** A message as one plain line's worth of text: no marks, a link as its label. */
23
+ export function chatPlainText(text: string): string {
24
+ return markdownPlainText(text, { links: isChatLinkHref });
25
+ }
26
+
27
+ function segmentStyle(segment: MarkdownSegment): ChatTextStyle | undefined {
28
+ const style: ChatTextStyle = {};
29
+ if (segment.bold) style.bold = true;
30
+ if (segment.italic) style.italic = true;
31
+ if (segment.strike) style.strike = true;
32
+ if (segment.code) style.code = true;
33
+ return Object.keys(style).length > 0 ? style : undefined;
34
+ }
35
+
36
+ function withStyle<T extends InlineContentToken>(token: T, style: ChatTextStyle | undefined): T & { style?: ChatTextStyle } {
37
+ return style ? { ...token, style } : token;
38
+ }
39
+
40
+ function linkHost(url: string): string {
41
+ try {
42
+ return new URL(url).hostname.toLowerCase().replace(/^www\./, "");
43
+ } catch {
44
+ return "";
45
+ }
46
+ }
47
+
48
+ /** Whether a link's label names the host it goes to, as a whole name: "x.com" does not name x.co. */
49
+ function labelNamesHost(label: string, host: string): boolean {
50
+ const text = label.toLowerCase().replace(/\bwww\./g, "");
51
+ for (let at = text.indexOf(host); at >= 0; at = text.indexOf(host, at + 1)) {
52
+ const before = text[at - 1] ?? "";
53
+ const after = text.slice(at + host.length);
54
+ if (!/[a-z0-9.-]/.test(before) && !/^(?:[a-z0-9-]|\.[a-z0-9])/.test(after)) return true;
55
+ }
56
+ return false;
57
+ }
58
+
59
+ /**
60
+ * One line of a message as tokens. A written link (`[label](url)`) shows its
61
+ * label, followed by the host it goes to unless the label already names it,
62
+ * so a label never hides where a link leads. Code is text as written: no
63
+ * links, mentions or tickers in it.
64
+ */
65
+ export function chatInlineTokens(text: string): ChatInlineToken[] {
66
+ const segments = parseInlineMarkdown(text, { links: isChatLinkHref });
67
+ const tokens: ChatInlineToken[] = [];
68
+ let label = "";
69
+ segments.forEach((segment, index) => {
70
+ const style = segmentStyle(segment);
71
+ if (segment.link !== undefined) {
72
+ const url = safeExternalUrl(segment.link) ?? segment.link;
73
+ tokens.push(withStyle({ kind: "link", value: segment.text, url }, style));
74
+ label += segment.text;
75
+ // A label with marks in it is several segments of one link.
76
+ if (segments[index + 1]?.link === segment.link) return;
77
+ const host = linkHost(url);
78
+ if (host && !labelNamesHost(label, host)) tokens.push({ kind: "text", value: ` (${host})`, style: { muted: true } });
79
+ label = "";
80
+ return;
81
+ }
82
+ if (segment.code) {
83
+ tokens.push(withStyle({ kind: "text", value: segment.text }, style));
84
+ return;
85
+ }
86
+ for (const token of tokenizeInlineContent(segment.text)) tokens.push(withStyle(token, style));
87
+ });
88
+ return tokens;
89
+ }
90
+
91
+ /** Every token a message draws outside its code blocks, in reading order. */
92
+ export function chatMessageTokens(text: string): ChatInlineToken[] {
93
+ return parseMarkdownLines(text).flatMap((line) => line.kind === "blank" || line.kind === "code" ? [] : chatInlineTokens(line.text));
94
+ }
@@ -12,7 +12,7 @@ import { ChatMessageActions, ChatMessageHeader, chatHeaderFitsEveryIcon } from "
12
12
  import { MESSAGE_ACTION_WIDTH } from "../layout";
13
13
  import { canReactToMessage } from "../reactions";
14
14
  import { DesktopChatReactions, DesktopReactionAction } from "./reactions";
15
- import { ResponsiveTickerBadgeText } from "./inline-tokens";
15
+ import { DesktopChatMessageText } from "./inline-tokens";
16
16
  import { DesktopChatPaneChip } from "./pane-chip";
17
17
  import { chatMessagePaneLinks } from "../pane-links/model";
18
18
  import { useSlowPendingSend } from "./pending-send";
@@ -192,10 +192,11 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
192
192
  style={{ minWidth: 0, alignItems: "flex-start" }}
193
193
  >
194
194
  <Box flexGrow={1} data-gloom-role="chat-message-body" data-gloom-selectable="true" style={{ minWidth: 0 }}>
195
- <ResponsiveTickerBadgeText
195
+ <DesktopChatMessageText
196
196
  text={content}
197
197
  catalog={catalog}
198
198
  textColor={state.bodyColor}
199
+ mutedColor={state.bodyMutedColor}
199
200
  openTicker={openTicker}
200
201
  userByUsername={userByUsername}
201
202
  onUserHover={onUserHover}