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.
- package/package.json +1 -1
- package/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +18 -8
- package/src/api-client/types.ts +3 -1
- package/src/cli/commands/market.ts +60 -9
- package/src/cli/commands/search.ts +43 -2
- package/src/cli/commands/ticker.ts +100 -21
- package/src/cli/desktop-pane-shot.ts +34 -9
- package/src/cli/fx-pair.ts +17 -0
- package/src/cli/glossary.ts +134 -6
- package/src/cli/history-rows.ts +16 -4
- package/src/cli/index.ts +44 -12
- package/src/cli/known-gaps.ts +21 -2
- package/src/cli/listing-arg.ts +19 -4
- package/src/cli/listing-suggestion.ts +33 -2
- package/src/cli/pane-functions/dom.ts +39 -6
- package/src/cli/pane-functions/freshness.ts +48 -5
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/headless.ts +85 -19
- package/src/cli/pane-functions/index.ts +60 -4
- package/src/cli/pane-functions/market-session.ts +69 -6
- package/src/cli/pane-functions/resolver.ts +4 -2
- package/src/cli/pane-functions/screenshot.ts +14 -1
- package/src/cli/scroll-truncation.ts +5 -1
- package/src/cli/symbol-args.ts +41 -3
- package/src/cli/symbol-list.ts +19 -7
- package/src/cli/unknown-command.ts +70 -0
- package/src/components/chart/native/surface/visibility.ts +37 -7
- package/src/components/layout/header.tsx +348 -102
- package/src/components/markdown-text.tsx +40 -66
- package/src/components/market-board/index.tsx +12 -6
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/data/memory-resource-store.ts +1 -1
- package/src/data/resource-store.ts +13 -1
- package/src/docs/snapshot.json +13 -13
- package/src/i18n/es.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/zh-cn.ts +1 -0
- package/src/i18n/zh-tw.ts +1 -0
- package/src/market-data/futures-contract.ts +10 -0
- package/src/market-data/market/format.ts +7 -2
- package/src/market-data/market/freshness.ts +25 -1
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/market-data/quotes/resolution.ts +2 -0
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/changelog/index.tsx +11 -8
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
- package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/attachments/model.ts +5 -3
- package/src/plugins/builtin/chat/content/footer.tsx +6 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
- package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
- package/src/plugins/builtin/chat/layout.ts +91 -31
- package/src/plugins/builtin/chat/markdown.ts +94 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
- package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
- package/src/plugins/builtin/company-attention/headless.ts +8 -5
- package/src/plugins/builtin/company-attention/model.ts +53 -2
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
- package/src/plugins/builtin/cot/client.ts +12 -4
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
- package/src/plugins/builtin/debug/index.tsx +15 -15
- package/src/plugins/builtin/dividend-yield/client.ts +8 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
- package/src/plugins/builtin/docs/index.tsx +5 -3
- package/src/plugins/builtin/docs/pane.tsx +18 -1
- package/src/plugins/builtin/docs/targets.ts +5 -3
- package/src/plugins/builtin/earnings-calls/data.ts +22 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
- package/src/plugins/builtin/equity-screener/headless.ts +4 -3
- package/src/plugins/builtin/equity-screener/live.ts +27 -1
- package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
- package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
- package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +32 -4
- package/src/plugins/builtin/futures/model.ts +65 -3
- package/src/plugins/builtin/futures/table.tsx +31 -11
- package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
- package/src/plugins/builtin/holders/pane.tsx +6 -1
- package/src/plugins/builtin/mna/client.ts +12 -2
- package/src/plugins/builtin/mna/detail.tsx +2 -2
- package/src/plugins/builtin/mna/pane.tsx +2 -2
- package/src/plugins/builtin/notes/owner.tsx +5 -4
- package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
- package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -9
- package/src/plugins/builtin/research/event-model.ts +14 -0
- package/src/plugins/builtin/research/headless-schema.ts +31 -1
- package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
- package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
- package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
- package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
- package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
- package/src/plugins/builtin/research-search/data.ts +11 -0
- package/src/plugins/builtin/research-search/pane.tsx +13 -2
- package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
- package/src/plugins/builtin/seasonality/client.ts +23 -0
- package/src/plugins/builtin/seasonality/pane.tsx +4 -2
- package/src/plugins/builtin/sectors/index.tsx +7 -3
- package/src/plugins/builtin/shared/report-freshness.ts +6 -2
- package/src/plugins/builtin/short-interest/headless.ts +13 -3
- package/src/plugins/builtin/short-interest/index.tsx +1 -1
- package/src/plugins/builtin/short-interest/model.ts +62 -3
- package/src/plugins/builtin/short-interest/pane.tsx +2 -2
- package/src/plugins/builtin/short-interest/schedule.ts +107 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
- package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
- package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
- package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
- package/src/plugins/builtin/themes/headless.ts +1 -1
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
- package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
- package/src/plugins/builtin/volatility/chart-model.ts +1 -1
- package/src/plugins/builtin/volatility/charts.tsx +1 -1
- package/src/renderers/dom/styles.css +6 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
- package/src/sources/provider-router/batches.ts +2 -1
- package/src/sources/provider-router/cache.ts +3 -0
- package/src/sync/baseline.ts +19 -14
- package/src/sync/controller.ts +140 -40
- package/src/sync/core-contributors.ts +256 -32
- package/src/sync/types.ts +44 -0
- package/src/time-series/resolve.ts +19 -2
- package/src/types/data-provider.ts +8 -0
- package/src/types/financials.ts +40 -4
- package/src/types/headless.ts +43 -0
- package/src/types/plugin.ts +7 -0
- package/src/utils/declared-dividends.ts +39 -0
- package/src/utils/forward-dividend.ts +34 -11
- package/src/utils/fundamentals.ts +13 -2
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MarketState } from "../../types/financials";
|
|
1
|
+
import type { MarketState, QuoteIndexSession } from "../../types/financials";
|
|
2
2
|
import { canonicalExchange, EXCHANGE_TIME_ZONES, isUsListingExchange } from "../../utils/exchanges";
|
|
3
3
|
import { hasPublishedApacCalendar, isPublishedApacClosure } from "../published-apac-sessions";
|
|
4
4
|
import { hasPublishedCnCalendar, isPublishedCnClosure } from "../published-cn-sessions";
|
|
@@ -675,3 +675,27 @@ function isTimestampStaleForExchangeSessionUnsafe(
|
|
|
675
675
|
}
|
|
676
676
|
return false;
|
|
677
677
|
}
|
|
678
|
+
|
|
679
|
+
function readSessionWindow(value: unknown): { start: number; end: number } | null {
|
|
680
|
+
const { start, end } = (value ?? {}) as { start?: unknown; end?: unknown };
|
|
681
|
+
return typeof start === "number" && typeof end === "number" && Number.isFinite(start) && Number.isFinite(end) && end > start
|
|
682
|
+
? { start, end }
|
|
683
|
+
: null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* The regular sessions an index quote carries, read without trusting the
|
|
688
|
+
* shape it arrived in: its zone, the session its day follows and the latest
|
|
689
|
+
* ones. Null when the zone or the current session is missing.
|
|
690
|
+
*/
|
|
691
|
+
export function readIndexSession(value: unknown): QuoteIndexSession | null {
|
|
692
|
+
const { timeZone, current, regular } = (value ?? {}) as { timeZone?: unknown; current?: unknown; regular?: unknown };
|
|
693
|
+
const now = readSessionWindow(current);
|
|
694
|
+
if (typeof timeZone !== "string" || !timeZone.includes("/") || !now) return null;
|
|
695
|
+
return {
|
|
696
|
+
timeZone,
|
|
697
|
+
current: now,
|
|
698
|
+
// The latest two: the one a print after the close belongs to, and the one before it.
|
|
699
|
+
regular: Array.isArray(regular) ? regular.flatMap((window) => readSessionWindow(window) ?? []).slice(-2) : [],
|
|
700
|
+
};
|
|
701
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Fundamentals, Quote, TickerFinancials } from "../../types/financials";
|
|
1
|
+
import type { EnterpriseValueCheck, Fundamentals, Quote, TickerFinancials } from "../../types/financials";
|
|
2
2
|
import { priceEarningsEps } from "../../utils/fundamentals";
|
|
3
3
|
import { getRegularSessionDisplay } from "../market/status";
|
|
4
4
|
|
|
@@ -39,6 +39,33 @@ interface FigureMoves {
|
|
|
39
39
|
enterpriseValue: number;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* The served EV check at the moved figures, so its note reads the EV and the
|
|
44
|
+
* market cap the rows show: the EV it describes is the moved one, and the
|
|
45
|
+
* computed EV moves with the cap. A withdrawn EV, shown nowhere, moves with
|
|
46
|
+
* the cap as the computed one does. The status stays the one Gloom Cloud
|
|
47
|
+
* judged.
|
|
48
|
+
*/
|
|
49
|
+
function movedEnterpriseValueCheck(
|
|
50
|
+
check: EnterpriseValueCheck | undefined,
|
|
51
|
+
served: number | undefined,
|
|
52
|
+
enterpriseValue: number | undefined,
|
|
53
|
+
movedCap: number | undefined,
|
|
54
|
+
): EnterpriseValueCheck | undefined {
|
|
55
|
+
if (!check || movedCap == null || !Number.isFinite(movedCap) || !Number.isFinite(check.marketCap)) return check;
|
|
56
|
+
const shift = movedCap - check.marketCap;
|
|
57
|
+
if (shift === 0 && served === enterpriseValue) return check;
|
|
58
|
+
const source = check.status !== "withdrawn" && served === check.source && enterpriseValue != null
|
|
59
|
+
? enterpriseValue
|
|
60
|
+
: check.source + shift;
|
|
61
|
+
return {
|
|
62
|
+
...check,
|
|
63
|
+
source,
|
|
64
|
+
marketCap: movedCap,
|
|
65
|
+
...(check.computed != null && Number.isFinite(check.computed) ? { computed: check.computed + shift } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
42
69
|
function moveFigures(fundamentals: Fundamentals, currency: string | undefined, moves: FigureMoves): Fundamentals {
|
|
43
70
|
const cap = fundamentals.marketCapCurrency === currency ? fundamentals.marketCap : undefined;
|
|
44
71
|
const movedCap = cap != null ? scaledAmount(cap, moves.marketCap) : undefined;
|
|
@@ -47,8 +74,12 @@ function moveFigures(fundamentals: Fundamentals, currency: string | undefined, m
|
|
|
47
74
|
const enterpriseValue = served != null && Number.isFinite(served) && served !== 0 && movedCap != null && Number.isFinite(movedCap)
|
|
48
75
|
? Math.round(served + movedCap * (1 - 1 / moves.enterpriseValue)) : served;
|
|
49
76
|
const enterpriseValueRatio = positive(served) && positive(enterpriseValue) ? enterpriseValue / served : null;
|
|
77
|
+
const check = fundamentals.enterpriseValueCheck?.currency === currency
|
|
78
|
+
? movedEnterpriseValueCheck(fundamentals.enterpriseValueCheck, served, enterpriseValue, movedCap)
|
|
79
|
+
: fundamentals.enterpriseValueCheck;
|
|
50
80
|
return {
|
|
51
81
|
...fundamentals,
|
|
82
|
+
...(check ? { enterpriseValueCheck: check } : {}),
|
|
52
83
|
trailingPE: scaled(fundamentals.trailingPE, moves.trailingPE),
|
|
53
84
|
forwardPE: scaled(fundamentals.forwardPE, moves.forwardPE),
|
|
54
85
|
dividendYield: scaled(fundamentals.dividendYield, 1 / moves.dividendYield),
|
|
@@ -532,6 +532,8 @@ export function resolveCanonicalQuote(
|
|
|
532
532
|
overnightChange: priceProvider?.overnightChange,
|
|
533
533
|
overnightChangePercent: priceProvider?.overnightChangePercent,
|
|
534
534
|
overnightPriceKind: priceProvider?.overnightPriceKind,
|
|
535
|
+
// An index's regular sessions come with the print they date.
|
|
536
|
+
indexSession: priceProvider?.indexSession,
|
|
535
537
|
bid: resolved.bid as Quote["bid"],
|
|
536
538
|
ask: resolved.ask as Quote["ask"],
|
|
537
539
|
bidSize: resolved.bidSize as Quote["bidSize"],
|
|
@@ -1019,19 +1019,23 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
1019
1019
|
/>
|
|
1020
1020
|
</Box>
|
|
1021
1021
|
<Box flexDirection="row" gap={1}>
|
|
1022
|
-
<
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1022
|
+
<Box ref={fieldNodeRef("passwordAction")} flexDirection="row">
|
|
1023
|
+
<Button
|
|
1024
|
+
label={t("Change Password")}
|
|
1025
|
+
active={activeField === "passwordAction"}
|
|
1026
|
+
onPress={openPasswordDialog}
|
|
1027
|
+
disabled={!!busy}
|
|
1028
|
+
/>
|
|
1029
|
+
</Box>
|
|
1030
|
+
<Box ref={fieldNodeRef("deleteAccountAction")} flexDirection="row">
|
|
1031
|
+
<Button
|
|
1032
|
+
label={busy === "delete" ? t("Deleting...") : t("Delete Account")}
|
|
1033
|
+
variant="danger"
|
|
1034
|
+
active={activeField === "deleteAccountAction"}
|
|
1035
|
+
onPress={() => { void deleteAccount(); }}
|
|
1036
|
+
disabled={!!busy}
|
|
1037
|
+
/>
|
|
1038
|
+
</Box>
|
|
1035
1039
|
</Box>
|
|
1036
1040
|
</>
|
|
1037
1041
|
) : null}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
2
2
|
import { fetchAttention } from "./client";
|
|
3
|
-
import { ATTENTION_TABS, attentionTab, attentionWindow, attentionRows, WINDOWS } from "./model";
|
|
3
|
+
import { ATTENTION_TABS, attentionPreviewNote, attentionTab, attentionWindow, attentionRows, emptyAttention, WINDOWS } from "./model";
|
|
4
4
|
export const attentionHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
5
5
|
shape: "bundle", argument: { kind: "free-text", optional: true, placeholder: "TICKER", description: "Optional listing-qualified ticker, such as 6758:JPX." },
|
|
6
6
|
options: [{ key: "tab", type: "enum", values: ATTENTION_TABS.map(({ value }) => ({ value })), defaultValue: "ranking", description: "Rankings, abnormal attention, groups, history or evidence." },
|
|
@@ -11,25 +11,34 @@ export const attentionHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
11
11
|
async load(args, ctx) {
|
|
12
12
|
const argument = Array.isArray(args.argument) ? args.argument.join(" ") : args.argument;
|
|
13
13
|
const tab = attentionTab(args.options.tab);
|
|
14
|
-
const
|
|
14
|
+
const window = attentionWindow(args.options.window);
|
|
15
|
+
const asked = argument?.trim().toUpperCase() || undefined;
|
|
16
|
+
let data = await fetchAttention(window, asked, ctx.apiClient);
|
|
17
|
+
// The free preview picks its rows before it looks the ticker up, so one outside them comes back empty: show the preview's own rows and say they are not the ticker's.
|
|
18
|
+
const outside = !!asked && data.entitlement === "preview" && !data.rows.length && data.counts.rows > 0;
|
|
19
|
+
if (outside) data = await fetchAttention(window, undefined, ctx.apiClient);
|
|
20
|
+
const note = attentionPreviewNote(data, asked, outside);
|
|
21
|
+
const lock = note ? { errors: [note] } : {};
|
|
22
|
+
// A board that has published nothing yet says so, whatever the plan.
|
|
23
|
+
const empty = data.status === "ready" ? {} : { empty: emptyAttention(data) };
|
|
15
24
|
const metadata = { asOf: data.asOf, stale: data.stale, complete: data.status === "ready" && !data.truncated,
|
|
16
25
|
status: data.status, periodStart: data.periodStart, periodEnd: data.periodEnd, entitlement: data.entitlement,
|
|
17
26
|
privacy: data.privacy, coverage: data.coverage, methodologyUrl: data.methodologyUrl, selectedListing: data.selectedListing ?? null, historyEvidence: data.historyEvidence ?? null, unit: "rounded researcher-hours" };
|
|
18
27
|
// Published hour by hour, so not a feed, but the service says when publication has fallen behind.
|
|
19
28
|
const freshness = { status: data.stale ? "stale" as const : "not-a-feed" as const, basis: "hourly publication" };
|
|
20
|
-
if (tab === "sectors" || tab === "countries") return { metadata, freshness, sections: [{ title: tab === "sectors" ? "Sector attention" : "Country attention",
|
|
21
|
-
columns: [{ key: "name", header: "Group" }, { key: "researchUnits", header: "Research hours" }, { key: "sharePct", header: "Share %" }, { key: "tickers", header: "Tickers" }], rows: data[tab].map((row) => ({ ...row })) }] };
|
|
22
|
-
if (tab === "history") return { metadata, freshness, sections: [{ title: "Published hourly history", columns: [{ key: "symbol", header: "Ticker" },
|
|
29
|
+
if (tab === "sectors" || tab === "countries") return { metadata, freshness, ...lock, sections: [{ title: tab === "sectors" ? "Sector attention" : "Country attention",
|
|
30
|
+
columns: [{ key: "name", header: "Group" }, { key: "researchUnits", header: "Research hours" }, { key: "sharePct", header: "Share %" }, { key: "tickers", header: "Tickers" }], rows: data[tab].map((row) => ({ ...row })), ...empty }] };
|
|
31
|
+
if (tab === "history") return { metadata, freshness, ...lock, sections: [{ title: "Published hourly history", columns: [{ key: "symbol", header: "Ticker" },
|
|
23
32
|
{ key: "bucketStart", header: "Hour (UTC)" }, { key: "researchUnits", header: "Research hours" }],
|
|
24
|
-
rows: data.history && data.selectedListing ? data.history.map((point) => ({ symbol: data.selectedListing!.symbol, ...point })) : data.rows.flatMap((row) => row.history.map((point) => ({ symbol: row.symbol, ...point }))) }] };
|
|
25
|
-
if (tab === "evidence") return { metadata, freshness, sections: [{ title: "Publication evidence", columns: [{ key: "symbol", header: "Ticker" },
|
|
33
|
+
rows: data.history && data.selectedListing ? data.history.map((point) => ({ symbol: data.selectedListing!.symbol, ...point })) : data.rows.flatMap((row) => row.history.map((point) => ({ symbol: row.symbol, ...point }))), ...empty }] };
|
|
34
|
+
if (tab === "evidence") return { metadata, freshness, ...lock, sections: [{ title: "Publication evidence", columns: [{ key: "symbol", header: "Ticker" },
|
|
26
35
|
{ key: "periodStart", header: "Period start" }, { key: "periodEnd", header: "Period end" }, { key: "asOf", header: "Published" },
|
|
27
36
|
{ key: "confidence", header: "Quality" }, { key: "marketAsOf", header: "Market as of" }],
|
|
28
|
-
rows: data.rows.map((row) => ({ symbol: row.symbol, ...row.evidence, baselinePeriods: row.baselinePeriods, marketAsOf: row.marketAsOf, news: row.news })) }] };
|
|
29
|
-
return { metadata, freshness, sections: [{ title: tab === "abnormal" ? "Abnormal attention" : "Research ranking", columns: [
|
|
37
|
+
rows: data.rows.map((row) => ({ symbol: row.symbol, ...row.evidence, baselinePeriods: row.baselinePeriods, marketAsOf: row.marketAsOf, news: row.news })), ...empty }] };
|
|
38
|
+
return { metadata, freshness, ...lock, sections: [{ title: tab === "abnormal" ? "Abnormal attention" : "Research ranking", columns: [
|
|
30
39
|
{ key: "rank", header: "Rank" }, { key: "symbol", header: "Ticker" }, { key: "name", header: "Name" }, { key: "researchUnits", header: "Research hours" },
|
|
31
40
|
{ key: "sharePct", header: "Share %" }, { key: "zScore", header: "Latest-hour Z-score" }, { key: "priceChangePct", header: "Price %" },
|
|
32
41
|
{ key: "relativeVolume", header: "Relative volume" }, { key: "sector", header: "Sector" }, { key: "country", header: "Country" },
|
|
33
|
-
], rows: attentionRows(data.rows, tab, "", tab === "abnormal" ? "zScore" : "rank", tab === "abnormal" ? "desc" : "asc").map((row) => ({ ...row })) }] };
|
|
42
|
+
], rows: attentionRows(data.rows, tab, "", tab === "abnormal" ? "zScore" : "rank", tab === "abnormal" ? "desc" : "asc").map((row) => ({ ...row })), ...empty }] };
|
|
34
43
|
},
|
|
35
44
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AttentionPayload, AttentionPoint, AttentionRow, AttentionWindow } from "../../../api-client/attention";
|
|
2
2
|
import type { DataTableColumn } from "../../../components";
|
|
3
|
+
import { countOf, freePreviewNote, previewCantFilter } from "../shared/free-preview";
|
|
3
4
|
export const ATTENTION_TABS = [{ value: "ranking", label: "Ranking" }, { value: "abnormal", label: "Abnormal" },
|
|
4
5
|
{ value: "sectors", label: "Sectors" }, { value: "countries", label: "Countries" }, { value: "history", label: "History" }, { value: "evidence", label: "Evidence" }] as const;
|
|
5
6
|
export type AttentionTab = typeof ATTENTION_TABS[number]["value"];
|
|
@@ -44,6 +45,27 @@ export function rowCell(row: AttentionRow, column: string) {
|
|
|
44
45
|
export const emptyAttention = (data: AttentionPayload | null | undefined) => data?.status === "disabled"
|
|
45
46
|
? "Research attention collection is not enabled yet." : "Waiting for privacy-qualified research activity.";
|
|
46
47
|
|
|
48
|
+
/** How many tickers the free preview lists: the top of the board by research hours. */
|
|
49
|
+
const PREVIEW_ROWS = 3;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What the free preview of the board shows and what Pro adds, for the lock
|
|
53
|
+
* line of `fn ATTN`. The server picks the preview's rows before it looks a
|
|
54
|
+
* ticker up, so a ticker outside them comes back empty and `outside` says the
|
|
55
|
+
* rows are the board's, not the ticker's. Null when there is nothing to
|
|
56
|
+
* preview, which includes a board still collecting.
|
|
57
|
+
*/
|
|
58
|
+
export function attentionPreviewNote(data: AttentionPayload, asked: string | undefined, outside: boolean): string | null {
|
|
59
|
+
if (data.entitlement !== "preview" || !data.rows.length) return null;
|
|
60
|
+
const total = Math.max(data.counts.rows, data.rows.length);
|
|
61
|
+
const listed = total > data.rows.length ? `${data.rows.length} of ${total.toLocaleString("en-US")} tickers` : countOf(data.rows.length, "ticker");
|
|
62
|
+
const shows = asked && outside ? `the top ${listed}, not ${asked}: ${previewCantFilter(["ticker"])}`
|
|
63
|
+
: asked ? `${data.rows[0]!.symbol}, one of the top ${PREVIEW_ROWS} of ${total.toLocaleString("en-US")} tickers, for the latest hour`
|
|
64
|
+
: `the top ${listed}, for the latest hour`;
|
|
65
|
+
const hidden = total - PREVIEW_ROWS;
|
|
66
|
+
return freePreviewNote({ shows, adds: `${hidden > 0 ? `the other ${hidden.toLocaleString("en-US")} tickers and ` : ""}hourly history for any ticker` });
|
|
67
|
+
}
|
|
68
|
+
|
|
47
69
|
/** An unpublished hour is a gap, never a measured zero or an interpolated count. */
|
|
48
70
|
export function historyPoints(history: readonly AttentionPoint[]) {
|
|
49
71
|
const points = [...history].sort((a, b) => a.bucketStart.localeCompare(b.bucketStart));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { Box, ScrollBox } from "../../../ui";
|
|
3
3
|
import { DataTableView, FieldGrid, KeyValueRow, Notice, QueryBar, Section, usePaneNoticeFooter, usePaneTabs, type GridField, type SelectControl } from "../../../components";
|
|
4
|
+
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
4
5
|
import { usePaneSettingValue, usePaneStateValue, useShortcut } from "../../../public/react";
|
|
5
6
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
6
7
|
import { colors } from "../../../theme/colors";
|
|
@@ -70,6 +71,7 @@ export function BondCalculatorPane({ focused, width, height }: PaneProps) {
|
|
|
70
71
|
}, [result, draft.quote, update]);
|
|
71
72
|
const selectTab = useCallback((value: string) => { setActiveField(null); setTab(value); }, [setTab]);
|
|
72
73
|
const { strip: tabStrip, rows: tabRows } = usePaneTabs({ tabs: TABS, activeValue: tab, onSelect: selectTab, focused: focused && !activeField, dense: true });
|
|
74
|
+
usePaneRefreshKey(() => { void load(); }, { focused });
|
|
73
75
|
useShortcut((event) => {
|
|
74
76
|
if (event.defaultPrevented || event.propagationStopped || event.ctrl || event.alt || event.meta || event.super) return;
|
|
75
77
|
const consume = () => { event.preventDefault(); event.stopPropagation(); };
|
|
@@ -77,7 +79,6 @@ export function BondCalculatorPane({ focused, width, height }: PaneProps) {
|
|
|
77
79
|
// Shifted letters are global chords (Shift+R refreshes everything).
|
|
78
80
|
if (event.targetEditable || event.shift) return;
|
|
79
81
|
if (event.name === "e") { consume(); setTab("valuation"); setActiveField("settlement"); }
|
|
80
|
-
else if (event.name === "r") { consume(); void load(); }
|
|
81
82
|
else if ((event.name === "j" || event.name === "k") && tab === "valuation") {
|
|
82
83
|
consume();
|
|
83
84
|
const index = activeField ? FIELDS.indexOf(activeField) : -1;
|
|
@@ -3,6 +3,7 @@ import { Box, TextAttributes, type ScrollBoxRenderable } from "../../../ui";
|
|
|
3
3
|
import { usePaneInstance } from "../../../state/app/context";
|
|
4
4
|
import {
|
|
5
5
|
DataTableStackView,
|
|
6
|
+
PaneLinkMenu,
|
|
6
7
|
PaneStatusBody,
|
|
7
8
|
useExternalLinkFooter,
|
|
8
9
|
type DataTableCell,
|
|
@@ -66,14 +67,16 @@ function ChangelogDetail({
|
|
|
66
67
|
}) {
|
|
67
68
|
// The stack title already carries the version.
|
|
68
69
|
return (
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
<PaneLinkMenu>
|
|
71
|
+
<DetailReader
|
|
72
|
+
ref={scrollRef}
|
|
73
|
+
resetScrollKey={release.id}
|
|
74
|
+
width={width}
|
|
75
|
+
meta={[formatShortDate(release.publishedAt, { fallback: "" })]}
|
|
76
|
+
body={release.body}
|
|
77
|
+
bodyFormat="markdown"
|
|
78
|
+
/>
|
|
79
|
+
</PaneLinkMenu>
|
|
77
80
|
);
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -79,6 +79,7 @@ export function DesktopSeriesEditorFields({ fields }: { fields: SeriesEditorFiel
|
|
|
79
79
|
<Box flexDirection="row" width="100%" style={{ gap: 6 }}>
|
|
80
80
|
<Box flexGrow={1} minWidth={0}>
|
|
81
81
|
<SelectField
|
|
82
|
+
label={field.label}
|
|
82
83
|
value={field.value}
|
|
83
84
|
options={field.options}
|
|
84
85
|
width="100%"
|
|
@@ -89,6 +90,7 @@ export function DesktopSeriesEditorFields({ fields }: { fields: SeriesEditorFiel
|
|
|
89
90
|
</Box>
|
|
90
91
|
) : (
|
|
91
92
|
<SelectField
|
|
93
|
+
label={field.label}
|
|
92
94
|
value={field.value}
|
|
93
95
|
options={field.options}
|
|
94
96
|
width="100%"
|
|
@@ -8,6 +8,7 @@ import { useThemeColors } from "../../../../theme/theme-context";
|
|
|
8
8
|
import { t, tf } from "../../../../i18n";
|
|
9
9
|
import type { ChatAttachment } from "../../../../api-client";
|
|
10
10
|
import type { ChatDraftAttachment } from "../controller/attachments";
|
|
11
|
+
import { chatPlainText } from "../markdown";
|
|
11
12
|
import {
|
|
12
13
|
CHAT_IMAGE_TYPES,
|
|
13
14
|
chatImageReviewNote,
|
|
@@ -355,7 +356,7 @@ export function DesktopChatAttachmentStrip({
|
|
|
355
356
|
}
|
|
356
357
|
|
|
357
358
|
function imageAlt(index: number, count: number, caption: string, author: string): string {
|
|
358
|
-
const text = caption.replace(/\s+/g, " ").trim();
|
|
359
|
+
const text = chatPlainText(caption).replace(/\s+/g, " ").trim();
|
|
359
360
|
const position = count > 1 ? ` ${index + 1} of ${count}` : "";
|
|
360
361
|
if (text) return `Image${position}: ${text.length > 120 ? `${text.slice(0, 117)}...` : text}`;
|
|
361
362
|
return `Image${position} from @${author}`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApiRequestError } from "../../../../api-client/errors";
|
|
2
2
|
import type { ChatAttachment, ChatMessage } from "../../../../api-client";
|
|
3
|
+
import { chatPlainText } from "../markdown";
|
|
3
4
|
|
|
4
5
|
/** What the server takes: four images a message, each up to 5 MB, as PNG, JPEG, WebP or GIF. */
|
|
5
6
|
export const MAX_CHAT_IMAGES = 4;
|
|
@@ -76,10 +77,11 @@ function chatImageCountLabel(count: number): string {
|
|
|
76
77
|
return count > 1 ? `[${count} images]` : "[image]";
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
/** A message read as one line: its images as a placeholder in front of the text. */
|
|
80
|
+
/** A message read as one line, without its markdown: its images as a placeholder in front of the text. */
|
|
80
81
|
export function chatTextWithImages(content: string, imageCount = 0): string {
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
const plain = chatPlainText(content);
|
|
83
|
+
if (imageCount <= 0) return plain;
|
|
84
|
+
const text = plain.trim();
|
|
83
85
|
const placeholder = chatImageCountLabel(imageCount);
|
|
84
86
|
return text ? `${placeholder} ${text}` : placeholder;
|
|
85
87
|
}
|
|
@@ -278,6 +278,11 @@ export function useChatFooter({
|
|
|
278
278
|
if (replyToId) {
|
|
279
279
|
menu.push({ id: "original-message", label: "Go to Original Message", onSelect: () => latest.current.jumpToMessage(replyToId) });
|
|
280
280
|
}
|
|
281
|
+
// The message as its author wrote it, markdown and all; a selection copies what the row draws.
|
|
282
|
+
const messageText = composing ? "" : selectedMessage?.content ?? "";
|
|
283
|
+
if (messageText) {
|
|
284
|
+
menu.push({ id: "copy-message", label: "Copy Message Text", onSelect: () => { void rendererHost.copyText(messageText); } });
|
|
285
|
+
}
|
|
281
286
|
if (needsProfileSetup) {
|
|
282
287
|
menu.push({ id: "profile-setup", label: "Set Up Profile", onSelect: () => latest.current.openProfileSetup() });
|
|
283
288
|
}
|
|
@@ -302,6 +307,7 @@ export function useChatFooter({
|
|
|
302
307
|
notificationsEnabled,
|
|
303
308
|
openTargetTitle,
|
|
304
309
|
openTargets.length,
|
|
310
|
+
rendererHost,
|
|
305
311
|
replyToId,
|
|
306
312
|
selectedIdx,
|
|
307
313
|
selectedMessage,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { InlineTickerCatalogEntry } from "../../../../state/hooks/inline-tickers";
|
|
2
2
|
import type { ChatAttachment } from "../../../../api-client";
|
|
3
|
-
import {
|
|
3
|
+
import { chatMessageTokens } from "../markdown";
|
|
4
4
|
import { chatImageLabel } from "../attachments/model";
|
|
5
5
|
import { wirePaneTarget, type ChatMessagePaneLink } from "../pane-links/model";
|
|
6
6
|
import { chatPaneLinkPane, peekSharePreview } from "../pane-links/previews";
|
|
@@ -26,7 +26,8 @@ function paneLinkLabel(link: ChatMessagePaneLink): string {
|
|
|
26
26
|
* What a message lets you open, in reading order: its chips (the panes its
|
|
27
27
|
* share links and Wire link open), the ticker badges its text draws (a symbol
|
|
28
28
|
* the catalog does not know stays plain text), its links, then its images.
|
|
29
|
-
* `content` is the text the message draws, without the links its chips stand
|
|
29
|
+
* `content` is the text the message draws, without the links its chips stand
|
|
30
|
+
* for. Code is text, so nothing in it is offered.
|
|
30
31
|
*/
|
|
31
32
|
export function chatMessageOpenTargets(
|
|
32
33
|
content: string,
|
|
@@ -36,7 +37,14 @@ export function chatMessageOpenTargets(
|
|
|
36
37
|
): ChatOpenTarget[] {
|
|
37
38
|
const targets: ChatOpenTarget[] = paneLinks.map((link) => ({ kind: "pane", link, label: paneLinkLabel(link) }));
|
|
38
39
|
const seen = new Set<string>();
|
|
39
|
-
|
|
40
|
+
// The link just listed: a label with marks in it ("[the **keys**](url)") is several tokens of one link.
|
|
41
|
+
let open: Extract<ChatOpenTarget, { kind: "link" }> | null = null;
|
|
42
|
+
for (const token of chatMessageTokens(content)) {
|
|
43
|
+
if (token.kind === "link" && open?.url === token.url) {
|
|
44
|
+
open.label += token.value;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
open = null;
|
|
40
48
|
if (token.kind === "ticker") {
|
|
41
49
|
const entry = catalog[token.symbol];
|
|
42
50
|
if (!entry || entry.status === "missing" || seen.has(`ticker:${token.symbol}`)) continue;
|
|
@@ -44,7 +52,8 @@ export function chatMessageOpenTargets(
|
|
|
44
52
|
targets.push({ kind: "ticker", symbol: token.symbol });
|
|
45
53
|
} else if (token.kind === "link" && !seen.has(`link:${token.url}`)) {
|
|
46
54
|
seen.add(`link:${token.url}`);
|
|
47
|
-
|
|
55
|
+
open = { kind: "link", url: token.url, label: token.value };
|
|
56
|
+
targets.push(open);
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
attachments.forEach((attachment, index) => {
|
|
@@ -10,6 +10,7 @@ import { DesktopChatMessage, desktopChatMessageWidth } from "../message/desktop"
|
|
|
10
10
|
import { UserProfilePopover } from "../message/profile-popover";
|
|
11
11
|
import { TerminalChatMessage } from "../message/terminal";
|
|
12
12
|
import { NewMessagesJump } from "./new-messages";
|
|
13
|
+
import { useWholeMessageCopy } from "./whole-message-copy";
|
|
13
14
|
|
|
14
15
|
interface MutableRef<T> {
|
|
15
16
|
current: T;
|
|
@@ -138,6 +139,7 @@ export function ChatTranscript({
|
|
|
138
139
|
useLayoutEffect(() => {
|
|
139
140
|
takeBackScrollPlace();
|
|
140
141
|
}, [takeBackScrollPlace]);
|
|
142
|
+
useWholeMessageCopy(nativePaneChrome === true, messages);
|
|
141
143
|
const reactionProps = {
|
|
142
144
|
reactionsEnabled,
|
|
143
145
|
currentUserId: user?.id ?? null,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import type { ChatMessage } from "../../../../api-client";
|
|
3
|
+
|
|
4
|
+
const BODY_SELECTOR = "[data-gloom-role=chat-message-body]";
|
|
5
|
+
const MESSAGE_ID_ATTRIBUTE = "data-gloom-chat-message-id";
|
|
6
|
+
|
|
7
|
+
/** Whether the selection takes in every character the body draws, first to last. */
|
|
8
|
+
function coversBody(range: Range, body: Element): boolean {
|
|
9
|
+
const walker = body.ownerDocument.createTreeWalker(body, 4 /* NodeFilter.SHOW_TEXT */);
|
|
10
|
+
let first: [Node, number] | null = null;
|
|
11
|
+
let last: [Node, number] | null = null;
|
|
12
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
13
|
+
const text = node.textContent ?? "";
|
|
14
|
+
const start = text.search(/\S/);
|
|
15
|
+
if (start < 0) continue;
|
|
16
|
+
first ??= [node, start];
|
|
17
|
+
last = [node, text.trimEnd().length];
|
|
18
|
+
}
|
|
19
|
+
if (!first || !last) return false;
|
|
20
|
+
try {
|
|
21
|
+
return range.comparePoint(first[0], first[1]) === 0 && range.comparePoint(last[0], last[1]) === 0;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What a copy of the selection puts on the clipboard when it takes in whole
|
|
29
|
+
* messages of this list: each as it was written, marks and all, one per line.
|
|
30
|
+
* Null when it takes in part of a message, which copies as drawn, or anything
|
|
31
|
+
* outside this list's messages.
|
|
32
|
+
*/
|
|
33
|
+
function wholeMessagesText(doc: Document, messages: readonly ChatMessage[]): string | null {
|
|
34
|
+
const selection = doc.getSelection();
|
|
35
|
+
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return null;
|
|
36
|
+
const range = selection.getRangeAt(0);
|
|
37
|
+
const bodies = Array.from(doc.querySelectorAll(BODY_SELECTOR)).filter((body) => range.intersectsNode(body));
|
|
38
|
+
if (bodies.length === 0) return null;
|
|
39
|
+
const parts: string[] = [];
|
|
40
|
+
for (const body of bodies) {
|
|
41
|
+
const id = body.closest(`[${MESSAGE_ID_ATTRIBUTE}]`)?.getAttribute(MESSAGE_ID_ATTRIBUTE);
|
|
42
|
+
const message = id ? messages.find((candidate) => candidate.id === id) : undefined;
|
|
43
|
+
if (!message || !coversBody(range, body)) return null;
|
|
44
|
+
parts.push(message.content);
|
|
45
|
+
}
|
|
46
|
+
return parts.join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Desktop and web: copying whole messages copies what their authors wrote,
|
|
51
|
+
* markdown and share links included, not the text as drawn. Copying part of a
|
|
52
|
+
* message copies what it shows.
|
|
53
|
+
*/
|
|
54
|
+
export function useWholeMessageCopy(enabled: boolean, messages: readonly ChatMessage[]): void {
|
|
55
|
+
const messagesRef = useRef(messages);
|
|
56
|
+
messagesRef.current = messages;
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const doc = (globalThis as { document?: Document }).document;
|
|
59
|
+
if (!enabled || !doc) return;
|
|
60
|
+
const onCopy = (event: ClipboardEvent) => {
|
|
61
|
+
if (event.defaultPrevented || !event.clipboardData) return;
|
|
62
|
+
const text = wholeMessagesText(doc, messagesRef.current);
|
|
63
|
+
if (text === null) return;
|
|
64
|
+
event.clipboardData.setData("text/plain", text);
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
};
|
|
67
|
+
doc.addEventListener("copy", onCopy);
|
|
68
|
+
return () => doc.removeEventListener("copy", onCopy);
|
|
69
|
+
}, [enabled]);
|
|
70
|
+
}
|