gloomberb 0.17.2 → 0.17.4
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 +2 -1
- package/src/api-client/chat.ts +28 -0
- package/src/api-client/errors.ts +11 -1
- package/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +22 -9
- package/src/api-client/normalizers.ts +21 -0
- package/src/api-client/socket.ts +27 -0
- package/src/api-client/types.ts +3 -1
- package/src/app/runtime/startup.ts +4 -0
- 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/index.ts +1 -1
- 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/metric-treemap/heat-surface.tsx +76 -14
- package/src/components/metric-treemap/heat-tiles.ts +5 -3
- package/src/components/metric-treemap/index.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/button.tsx +24 -9
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/components/ui/index.ts +1 -1
- package/src/components/ui/status.tsx +148 -5
- 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/layout-marketplace/gallery-desktop.tsx +2 -2
- package/src/market-data/futures-board.ts +44 -1
- 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/calendar-tab.tsx +1 -1
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/alerts/cli.ts +72 -11
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/awards/headless.ts +5 -1
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/catalysts/headless.ts +5 -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/controller/connections.ts +18 -0
- package/src/plugins/builtin/chat/controller/fetch.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +49 -21
- package/src/plugins/builtin/chat/controller/notifications.ts +132 -72
- package/src/plugins/builtin/chat/controller/realtime.ts +37 -1
- 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/model.ts +20 -0
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- 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 +19 -2
- 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 +33 -6
- package/src/plugins/builtin/futures/model.ts +69 -5
- package/src/plugins/builtin/futures/series.ts +3 -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/jobs/pane.tsx +12 -9
- package/src/plugins/builtin/market-heatmap/index.tsx +6 -15
- 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/news/headless.ts +4 -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/plugin-marketplace/pane.tsx +1 -1
- 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 +4 -12
- 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/build-assets.ts +17 -4
- package/src/renderers/dom/desktop/controls.tsx +2 -0
- package/src/renderers/dom/styles.css +6 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +4 -3
- package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -3
- package/src/renderers/electrobun/bun/desktop/macos-objc.ts +17 -6
- package/src/sources/gloomberb-cloud/normalizers.ts +13 -4
- 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 +24 -5
- 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/ui/resize-settle.ts +128 -0
- package/src/utils/calendar-date.ts +56 -0
- package/src/utils/decimal-comma.ts +31 -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/futures-generic.ts +25 -4
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
- package/src/utils/plugin-proxy-hosts.json +5 -0
- package/src/utils/utc-time.ts +14 -3
|
@@ -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"],
|
|
@@ -165,7 +165,7 @@ export function CalendarAccountTab({ width, sessionMarker }: { width: number; se
|
|
|
165
165
|
<EmptyState
|
|
166
166
|
status="error"
|
|
167
167
|
title={loadError}
|
|
168
|
-
|
|
168
|
+
retry={{ onRetry: () => load(), pending: loading }}
|
|
169
169
|
/>
|
|
170
170
|
);
|
|
171
171
|
}
|
|
@@ -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}
|
|
@@ -2,12 +2,14 @@ import { withConfigData, withMarketData } from "../../../cli/scoped-context";
|
|
|
2
2
|
import { CLI_COMMAND_GROUPS } from "../../../cli/help";
|
|
3
3
|
import { dryRunNote } from "../../../cli/helpers";
|
|
4
4
|
import { requireArg, takeOption } from "../../../cli/commands/command-utils";
|
|
5
|
-
import { EXCHANGE_OPTION, listingReadNotes, requireListingArg } from "../../../cli/listing-arg";
|
|
5
|
+
import { EXCHANGE_OPTION, listingReadNotes, requireListingArg, type ListingArg } from "../../../cli/listing-arg";
|
|
6
6
|
import { saveConfig } from "../../../data/config/store";
|
|
7
7
|
import type { AppConfig } from "../../../types/config";
|
|
8
|
-
import type { CliCommandDef } from "../../../types/plugin";
|
|
8
|
+
import type { CliCommandContext, CliCommandDef } from "../../../types/plugin";
|
|
9
|
+
import { getActiveQuoteDisplay } from "../../../market-data/market/status";
|
|
10
|
+
import { settleWithin } from "../../../utils/async-deadline";
|
|
9
11
|
import { cliStyles } from "../../../utils/cli-output";
|
|
10
|
-
import { ambiguousNumberMessage, parsePastedNumber } from "../../../utils/decimal-comma";
|
|
12
|
+
import { ambiguousNumberMessage, meantThousandsDot, parsePastedNumber, thousandsDotReadings, type ThousandsDotReadings } from "../../../utils/decimal-comma";
|
|
11
13
|
import { newYorkToday } from "../earnings/board-model";
|
|
12
14
|
import { createAlert, deserializeAlerts, rearmAlert, serializeAlerts } from "./alert-engine";
|
|
13
15
|
import { ALERTS_KEY } from "./constants";
|
|
@@ -43,20 +45,28 @@ const DAYS_RULE = `--days takes a whole number of days from 1 to ${MAX_LEAD_DAYS
|
|
|
43
45
|
|
|
44
46
|
/** What `alerts add` was asked for, before any listing or date is looked up. */
|
|
45
47
|
export type AlertAddRequest =
|
|
46
|
-
| {
|
|
48
|
+
| {
|
|
49
|
+
kind: "price"; symbol: string; condition: AlertCondition; price: number;
|
|
50
|
+
/** A price typed as 81.000, which the last price may show was 81000 with a dot between thousands. */
|
|
51
|
+
thousandsDot?: ThousandsDotReadings & { text: string };
|
|
52
|
+
}
|
|
47
53
|
| { kind: "earnings"; symbol: string; leadDays: number }
|
|
48
54
|
| { kind: "macro"; event: MacroAlertEvent; leadDays: number };
|
|
49
55
|
|
|
50
56
|
const CONDITIONS: readonly AlertCondition[] = ["above", "below", "crosses"];
|
|
51
57
|
|
|
58
|
+
/** The first word of a price typed with a space between thousands and no quotes (`81 000`): one to three digits. */
|
|
59
|
+
const LEADING_GROUP = /^[+-]?\d{1,3}$/;
|
|
60
|
+
|
|
52
61
|
/**
|
|
53
62
|
* Reads `alerts add` arguments: `<symbol> above 250`, `<symbol> earnings`, or
|
|
54
63
|
* `CPI` / `FOMC`, with `--days` (already taken out of `args`, `null` when the
|
|
55
|
-
* flag came without a value) for the event forms only
|
|
56
|
-
* names what the command
|
|
64
|
+
* flag came without a value) for the event forms only, and `--exchange`, which
|
|
65
|
+
* a command to type again keeps. Throws a message that names what the command
|
|
66
|
+
* takes.
|
|
57
67
|
*/
|
|
58
|
-
export function parseAlertAddArgs(args: readonly string[], days: string | null | undefined): AlertAddRequest {
|
|
59
|
-
const [first, second, third] = args;
|
|
68
|
+
export function parseAlertAddArgs(args: readonly string[], days: string | null | undefined, exchange?: string | null): AlertAddRequest {
|
|
69
|
+
const [first, second, third, ...rest] = args;
|
|
60
70
|
if (!first) throw new Error(ADD_USAGE);
|
|
61
71
|
const leadDays = () => {
|
|
62
72
|
const parsed = days === null ? null : parseLeadDays(days);
|
|
@@ -83,11 +93,57 @@ export function parseAlertAddArgs(args: readonly string[], days: string | null |
|
|
|
83
93
|
}
|
|
84
94
|
if (days !== undefined) throw new Error("--days is for earnings and CPI or FOMC alerts, not price alerts.");
|
|
85
95
|
if (third === undefined) throw new Error(ADD_USAGE);
|
|
96
|
+
const retype = (price: number) => retypeCommand(first, condition, price, exchange);
|
|
97
|
+
// `81 000` without quotes arrives as 81 and a stray 000; neither reading is taken for granted.
|
|
98
|
+
const spaced = rest.length > 0 && LEADING_GROUP.test(third) ? parsePastedNumber([third, ...rest].join(" ")) : null;
|
|
99
|
+
if (spaced?.kind === "number") {
|
|
100
|
+
const typed = Number(third);
|
|
101
|
+
throw new Error(`${[third, ...rest].join(" ")} could be ${spaced.value} (thousands) or ${typed} and a stray ${rest.join(" ")}; type ${retype(spaced.value)} or ${retype(typed)}`);
|
|
102
|
+
}
|
|
86
103
|
// A price pasted from a sheet (1.350,5, 1,350.5, 1 350,5) reads as it is written; `1,350` could be either, so it asks.
|
|
87
104
|
const pasted = parsePastedNumber(third);
|
|
88
105
|
if (pasted.kind === "ambiguous") throw new Error(ambiguousNumberMessage(third, pasted));
|
|
89
106
|
if (pasted.kind === "invalid") throw new Error("Alert price must be a finite number.");
|
|
90
|
-
|
|
107
|
+
if (rest.length > 0) throw new Error(`A price alert takes one price, not "${rest.join(" ")}" after ${third}; type ${retype(pasted.value)}`);
|
|
108
|
+
const thousandsDot = thousandsDotReadings(third);
|
|
109
|
+
return { kind: "price", symbol: first, condition, price: pasted.value, ...(thousandsDot ? { thousandsDot: { ...thousandsDot, text: third.trim() } } : {}) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `alerts add` as typed, with another price: what an error tells someone to type. */
|
|
113
|
+
function retypeCommand(symbol: string, condition: AlertCondition, price: number, exchange: string | null | undefined): string {
|
|
114
|
+
return `gloomberb alerts add ${symbol} ${condition} ${price}${exchange ? ` --exchange ${exchange}` : ""}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const LAST_PRICE_TIMEOUT_MS = 10_000;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The listing's last price, as the alert will check it, for telling 81.000
|
|
121
|
+
* typed for 81000 from 81; null when there is none, and the price is then
|
|
122
|
+
* read as written.
|
|
123
|
+
*/
|
|
124
|
+
async function lastPrice(listing: ListingArg, ctx: Pick<CliCommandContext, "initMarketData">): Promise<number | null> {
|
|
125
|
+
const quote = await settleWithin(
|
|
126
|
+
withMarketData(ctx, (market) => market.dataProvider.getQuote(listing.key.toUpperCase(), listing.exchange)),
|
|
127
|
+
LAST_PRICE_TIMEOUT_MS,
|
|
128
|
+
);
|
|
129
|
+
const price = quote ? getActiveQuoteDisplay(quote)?.price ?? quote.price : null;
|
|
130
|
+
return price != null && Number.isFinite(price) && price !== 0 ? price : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Asks which price `81.000` meant when the last price shows it was a dot
|
|
135
|
+
* between thousands: both readings, and the command to type for each.
|
|
136
|
+
*/
|
|
137
|
+
export function thousandsDotMessage(
|
|
138
|
+
request: Extract<AlertAddRequest, { kind: "price" }>,
|
|
139
|
+
listing: string,
|
|
140
|
+
last: number | null,
|
|
141
|
+
exchange?: string | null,
|
|
142
|
+
): string | null {
|
|
143
|
+
const dot = request.thousandsDot;
|
|
144
|
+
if (!dot || !meantThousandsDot(dot, last)) return null;
|
|
145
|
+
const retype = (price: number) => retypeCommand(request.symbol, request.condition, price, exchange);
|
|
146
|
+
return `${dot.text} could be ${dot.thousands} (thousands) or ${dot.decimal} (decimal point), and ${listing.toUpperCase()} last traded at ${Number(last!.toFixed(2))}; type ${retype(dot.thousands)} or ${retype(dot.decimal)}`;
|
|
91
147
|
}
|
|
92
148
|
|
|
93
149
|
/** One row of `alerts list`: a price alert as stored, or an event alert with its next date. */
|
|
@@ -222,13 +278,17 @@ export const alertsCliCommand: CliCommandDef = {
|
|
|
222
278
|
|
|
223
279
|
const parseAdd = (): AlertAddRequest => {
|
|
224
280
|
try {
|
|
225
|
-
return parseAlertAddArgs(args.slice(1), days);
|
|
281
|
+
return parseAlertAddArgs(args.slice(1), days, exchangeOption);
|
|
226
282
|
} catch (error) {
|
|
227
283
|
return ctx.fail(error instanceof Error ? error.message : String(error));
|
|
228
284
|
}
|
|
229
285
|
};
|
|
230
286
|
const request = action === "add" ? parseAdd() : null;
|
|
231
287
|
const priceListing = request?.kind === "price" ? await requireListingArg(request.symbol, exchangeOption, ctx) : null;
|
|
288
|
+
if (request?.kind === "price" && request.thousandsDot && priceListing) {
|
|
289
|
+
const question = thousandsDotMessage(request, priceListing.key, await lastPrice(priceListing, ctx), exchangeOption);
|
|
290
|
+
if (question) ctx.fail(question);
|
|
291
|
+
}
|
|
232
292
|
// What a typed listing was read as (7203:TSE as 7203:JPX), said beside what was added.
|
|
233
293
|
let read = priceListing ? listingReadNotes(priceListing) : [];
|
|
234
294
|
let newEventRule: EventAlertRule | null = null;
|
|
@@ -309,7 +369,8 @@ export const alertsCliCommand: CliCommandDef = {
|
|
|
309
369
|
const alert = createAlert(symbol, condition, price, priceListing?.exchange);
|
|
310
370
|
await save({ alerts: [...alerts, alert] });
|
|
311
371
|
ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, alert }, ...(read.length > 0 ? { warnings: read } : {}) }, {
|
|
312
|
-
|
|
372
|
+
// No full stop after the price, which would read as part of it (below 81.).
|
|
373
|
+
text: (data) => `Added alert ${data.alert.id}: ${symbol} ${condition} ${price}${dryRunNote(data.dryRun)}`,
|
|
313
374
|
});
|
|
314
375
|
return;
|
|
315
376
|
}
|
|
@@ -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 type { AwardFilter, AwardRow, AwardType } from "../../../api-client/awards";
|
|
2
2
|
import { notATickerSymbol } from "../../../sources/provider-errors";
|
|
3
3
|
import type { HeadlessBundleSection, HeadlessPaneColumn, HeadlessPaneContext, HeadlessPaneDefinition, HeadlessPaneRow } from "../../../types/headless";
|
|
4
|
+
import { isoDateOption } from "../../../utils/calendar-date";
|
|
4
5
|
import { resolveHeadlessInstrument } from "../shared/headless-market-data";
|
|
5
6
|
import { fetchAward, fetchAwards } from "./client";
|
|
6
7
|
import { AWARD_TABS, awardScope, awardsPreviewNote, awardTypeLabel, futureAwardDatesNote, money } from "./model";
|
|
@@ -85,7 +86,10 @@ export const awardsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
85
86
|
options: [
|
|
86
87
|
{ key: "tab", type: "enum", settingKey: "tab", defaultValue: "feed", values: [...AWARD_TABS.map(({ value }) => ({ value })), { value: "all" }], description: "Feed, company history, agencies, sectors, events, or all sections." },
|
|
87
88
|
{ key: "view", type: "enum", settingKey: "leaderView", defaultValue: "companies", values: [{ value: "companies" }, { value: "sectors" }], description: "Sectors tab: company leaders or sector totals." },
|
|
88
|
-
...
|
|
89
|
+
...FILTER_KEYS.map((key) => key === "from" || key === "to"
|
|
90
|
+
? { key, type: "string" as const, settingKey: key, placeholder: "YYYY-MM-DD", description: `${key === "from" ? "Start" : "End"} of the date range. Dates use YYYY-MM-DD.`,
|
|
91
|
+
normalize: (value: string) => isoDateOption(value, key) }
|
|
92
|
+
: { key, type: "string" as const, settingKey: key, description: `Filter by ${key}.` }),
|
|
89
93
|
{ key: "type", type: "enum", settingKey: "awardType", defaultValue: "prime", values: [{ value: "prime" }, { value: "subaward" }, { value: "notice" }, { value: "modification" }], description: "Keep record types separate." },
|
|
90
94
|
{ key: "cursor", type: "string", description: "Continue after the previous response's nextCursor." },
|
|
91
95
|
{ key: "limit", type: "integer", minimum: 1, maximum: 200, defaultValue: 100, description: "Maximum feed rows in this response." },
|
|
@@ -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;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CATALYST_TYPES } from "../../../api-client/catalysts";
|
|
2
2
|
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
3
|
+
import { isoDateOption } from "../../../utils/calendar-date";
|
|
3
4
|
import { fetchCatalystDetail, fetchCatalysts } from "./client";
|
|
4
5
|
import { catalystCell, catalystChangeText, catalystDate, catalystQuery, catalystTickers } from "./model";
|
|
5
6
|
|
|
@@ -11,7 +12,10 @@ function definition(litigation: boolean): HeadlessPaneDefinition<"bundle"> {
|
|
|
11
12
|
options: [
|
|
12
13
|
{ key: "tab", type: "enum", values: [{ value: "calendar" }, { value: "changes" }], defaultValue: "calendar", description: "Dated events or observed revisions." },
|
|
13
14
|
{ key: "type", type: "enum", values: CATALYST_TYPES.map((value) => ({ value })), description: "Event type." },
|
|
14
|
-
...["agency", "country", "sector", "status"
|
|
15
|
+
...["agency", "country", "sector", "status"].map((key) => ({ key, type: "string" as const, description: `Filter by ${key}.` })),
|
|
16
|
+
{ key: "search", type: "string", description: "Filter by text." },
|
|
17
|
+
{ key: "from", type: "string", placeholder: "YYYY-MM-DD", description: "Start of the calendar window. Dates use YYYY-MM-DD.", normalize: (value) => isoDateOption(value, "from") },
|
|
18
|
+
{ key: "to", type: "string", placeholder: "YYYY-MM-DD", description: "End of the calendar window. Dates use YYYY-MM-DD.", normalize: (value) => isoDateOption(value, "to") },
|
|
15
19
|
{ key: "dateField", type: "enum", values: ["any", "announced", "effective", "deadline", "observed"].map((value) => ({ value })), defaultValue: "any", description: "Date field for the calendar window." },
|
|
16
20
|
{ key: "upcoming", type: "boolean", defaultValue: false, description: "Only future effective dates and deadlines." },
|
|
17
21
|
{ key: "offset", type: "integer", defaultValue: 0, description: "Zero-based event offset." },
|
|
@@ -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,
|