gloomberb 0.16.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/data.ts +18 -3
- package/src/api-client/index.ts +3 -0
- package/src/api-client/members.ts +26 -0
- package/src/api-client/perps.ts +4 -0
- package/src/api-client/types.ts +7 -1
- package/src/app/global-shortcuts.ts +14 -6
- package/src/app/keybindings/actions.ts +1 -1
- package/src/app/runtime/update.ts +38 -2
- package/src/app.tsx +10 -6
- package/src/cli/pane-functions/function-help.ts +9 -2
- package/src/components/command-bar/commands/collection.ts +5 -1
- package/src/components/command-bar/routes/root/command-items.ts +9 -2
- package/src/components/command-bar/routes/root/plugin-install.ts +2 -2
- package/src/components/command-bar/routes/root/provider-search.ts +19 -5
- package/src/components/command-bar/routes/root/runtime.ts +14 -5
- package/src/components/command-bar/routes/ticker-search/results.ts +31 -5
- package/src/components/command-bar/routes/ticker-search/route.ts +2 -2
- package/src/components/command-bar/routing/actions.ts +2 -0
- package/src/components/command-bar/selection-runtime.ts +38 -0
- package/src/components/command-bar/surface/index.tsx +7 -2
- package/src/components/index.ts +2 -2
- package/src/components/layout/floating-pane.tsx +6 -0
- package/src/components/layout/header.tsx +24 -7
- package/src/components/layout/missing-pane.tsx +2 -2
- package/src/components/layout/pane/header.tsx +63 -30
- package/src/components/layout/pane/index.tsx +6 -0
- package/src/components/layout/pane/title.ts +31 -7
- package/src/components/layout/shell/drag/index.ts +6 -5
- package/src/components/layout/shell/drag/runtime.ts +3 -0
- package/src/components/layout/shell/index.tsx +6 -0
- package/src/components/layout/shell/pane/layers.tsx +15 -7
- package/src/components/layout/shell/terminal-pointer-runtime.ts +24 -3
- package/src/components/layout/shell/ticker-link-menu.ts +23 -9
- package/src/components/metric-treemap/heat-surface.tsx +718 -0
- package/src/components/metric-treemap/heat-tiles.ts +210 -0
- package/src/components/metric-treemap/index.tsx +2 -0
- package/src/components/metric-treemap/layout.ts +6 -0
- package/src/components/metric-treemap/scene.ts +432 -0
- package/src/components/pane-settings-dialog/field-dialogs.tsx +1 -0
- package/src/components/toggle-list.tsx +38 -12
- package/src/components/ui/country-flag-art.tsx +449 -0
- package/src/components/ui/country-flag.tsx +41 -0
- package/src/components/ui/data-table/types.ts +13 -0
- package/src/components/ui/icon.tsx +2 -0
- package/src/components/ui/index.ts +4 -0
- package/src/components/ui/multi-select/dialog.tsx +108 -22
- package/src/components/ui/range-track.tsx +46 -0
- package/src/components/ui/tabs.tsx +4 -0
- package/src/core/state/app/types.ts +10 -1
- package/src/i18n/es.ts +5 -3
- package/src/i18n/ja.ts +5 -3
- package/src/i18n/ko.ts +5 -3
- package/src/i18n/zh-cn.ts +5 -3
- package/src/i18n/zh-tw.ts +5 -3
- package/src/layout/ticker-navigation.ts +28 -2
- package/src/market-data/quotes/contributions.ts +6 -0
- package/src/market-data/quotes/freshness.ts +36 -0
- package/src/market-data/quotes/resolution.ts +22 -8
- package/src/market-data/quotes/time.ts +16 -0
- package/src/plugins/builtin/analytics/risk-client.ts +185 -59
- package/src/plugins/builtin/analytics/risk-headless.ts +55 -49
- package/src/plugins/builtin/analytics/risk-math.ts +33 -13
- package/src/plugins/builtin/analytics/risk-model.ts +250 -60
- package/src/plugins/builtin/analytics/risk-pane.tsx +61 -30
- package/src/plugins/builtin/analytics/risk-test-data.ts +182 -0
- package/src/plugins/builtin/buildout/cells.tsx +15 -12
- package/src/plugins/builtin/buildout/detail/company.tsx +3 -3
- package/src/plugins/builtin/buildout/detail/index.tsx +0 -1
- package/src/plugins/builtin/buildout/detail/intel.tsx +6 -6
- package/src/plugins/builtin/buildout/detail/site.tsx +4 -7
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -19
- package/src/plugins/builtin/catalysts/index.tsx +2 -2
- package/src/plugins/builtin/catalysts/pane.tsx +3 -1
- package/src/plugins/builtin/cds/index.tsx +1 -0
- package/src/plugins/builtin/debt-maturities/wall-bar.tsx +14 -41
- package/src/plugins/builtin/dividend-yield/client.ts +4 -2
- package/src/plugins/builtin/earnings/client.ts +2 -2
- package/src/plugins/builtin/earnings-calls/index.tsx +1 -0
- package/src/plugins/builtin/fx-matrix/direction.ts +81 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +71 -33
- package/src/plugins/builtin/fx-matrix/live-legs.ts +17 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +68 -14
- package/src/plugins/builtin/fx-matrix/settings.ts +62 -0
- package/src/plugins/builtin/jobs/index.tsx +1 -0
- package/src/plugins/builtin/jobs/share-bars.tsx +5 -28
- package/src/plugins/builtin/market-heatmap/data.ts +28 -2
- package/src/plugins/builtin/market-heatmap/index.tsx +250 -79
- package/src/plugins/builtin/market-heatmap/live.ts +88 -0
- package/src/plugins/builtin/market-heatmap/portfolio.ts +27 -7
- package/src/plugins/builtin/market-movers/client.ts +3 -2
- package/src/plugins/builtin/market-movers/headless.ts +8 -1
- package/src/plugins/builtin/market-movers/screener.ts +18 -5
- package/src/plugins/builtin/members/client.ts +36 -0
- package/src/plugins/builtin/members/headless.ts +33 -0
- package/src/plugins/builtin/members/index.tsx +20 -0
- package/src/plugins/builtin/members/model.ts +60 -0
- package/src/plugins/builtin/members/pane.tsx +168 -0
- package/src/plugins/builtin/mna/index.tsx +2 -1
- package/src/plugins/builtin/mna/pane.tsx +5 -2
- package/src/plugins/builtin/options/index.tsx +2 -1
- package/src/plugins/builtin/pe-band/client.ts +29 -2
- package/src/plugins/builtin/pe-band/headless.ts +2 -2
- package/src/plugins/builtin/pe-band/model.ts +11 -7
- package/src/plugins/builtin/pe-band/pane.tsx +2 -1
- package/src/plugins/builtin/pe-band/report-dates.ts +78 -0
- package/src/plugins/builtin/perps/client.ts +17 -3
- package/src/plugins/builtin/perps/equity-identity.ts +16 -0
- package/src/plugins/builtin/perps/equity-row.tsx +6 -4
- package/src/plugins/builtin/plugin-marketplace/activation.ts +19 -0
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -2
- package/src/plugins/builtin/plugin-marketplace/store.ts +2 -0
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +43 -2
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +48 -5
- package/src/plugins/builtin/scanner/hilo-bars.tsx +12 -70
- package/src/plugins/builtin/scanner/hilo-model.ts +0 -18
- package/src/plugins/builtin/sectors/move-bar.tsx +10 -32
- package/src/plugins/builtin/social-mentions/index.tsx +1 -1
- package/src/plugins/builtin/thirteenf/index.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +5 -60
- package/src/plugins/builtin/ticker-detail/overview/pane.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +7 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +20 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -2
- package/src/plugins/builtin/vol-surface/index.tsx +1 -1
- package/src/plugins/catalog-browser.ts +2 -0
- package/src/plugins/catalog-manifest.ts +1 -0
- package/src/plugins/catalog-ui.ts +3 -1
- package/src/plugins/catalog.ts +1 -1
- package/src/plugins/host-link.ts +33 -1
- package/src/plugins/installer.ts +48 -12
- package/src/plugins/loader.ts +18 -3
- package/src/plugins/manager-node.ts +15 -5
- package/src/plugins/registry/index.ts +1 -1
- package/src/plugins/registry/pane-settings.ts +25 -7
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/renderers/dom/data-table/row.tsx +50 -16
- package/src/renderers/dom/desktop/icons.tsx +1 -0
- package/src/renderers/dom/host/box.tsx +69 -26
- package/src/renderers/dom/host/tabs.tsx +37 -1
- package/src/renderers/dom/styles.css +40 -5
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +11 -6
- package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +181 -87
- package/src/renderers/electrobun/bun/index.ts +73 -6
- package/src/renderers/electrobun/bun/window/remembered-frame.ts +122 -0
- package/src/renderers/electrobun/shared/protocol.ts +3 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +2 -1
- package/src/renderers/electrobun/view/main.tsx +7 -1
- package/src/renderers/electrobun/view/renderer-window-error.ts +18 -0
- package/src/renderers/electrobun/view/update-host.ts +45 -21
- package/src/sources/gloomberb-cloud/normalizers.ts +9 -2
- package/src/sources/provider-router/financials.ts +3 -1
- package/src/telemetry/benign-window-error.ts +7 -0
- package/src/telemetry/crash-reports-dom.ts +2 -0
- package/src/test-support/daily-nav.ts +25 -0
- package/src/theme/heat-colors.ts +174 -1
- package/src/tickers/instrument-kind.ts +34 -7
- package/src/tickers/search/asset-classes.ts +24 -9
- package/src/tickers/search/index.ts +44 -11
- package/src/tickers/search/ranking.ts +23 -3
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +18 -5
- package/src/ui/context-menu.tsx +1 -0
- package/src/ui/host.tsx +2 -0
- package/src/updater/index.ts +29 -2
package/package.json
CHANGED
package/src/api-client/data.ts
CHANGED
|
@@ -181,6 +181,18 @@ export class CloudDataApi {
|
|
|
181
181
|
return this.request<T>(`/cloud/equity-screener/${path}`, init);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
getMemberFunds() {
|
|
185
|
+
return this.request<import("./members").MemberFundsPayload>("/cloud/members");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getFundMembers(fund: string) {
|
|
189
|
+
return this.request<import("./members").FundMembersPayload>(`/cloud/members/${encodeURIComponent(fund)}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
getFundChanges(fund: string) {
|
|
193
|
+
return this.request<import("./members").FundChangesPayload>(`/cloud/members/${encodeURIComponent(fund)}/changes`);
|
|
194
|
+
}
|
|
195
|
+
|
|
184
196
|
getCloudThemes() {
|
|
185
197
|
return this.request<import("./themes").ThemesPayload>("/cloud/themes");
|
|
186
198
|
}
|
|
@@ -220,8 +232,8 @@ export class CloudDataApi {
|
|
|
220
232
|
return this.request<EarningsCalendarPayload>(`/cloud/earnings/calendar?${params}`, { signal: AbortSignal.timeout(30_000) });
|
|
221
233
|
}
|
|
222
234
|
|
|
223
|
-
async getCloudEarningsHistory(symbol: string): Promise<EarningsHistoryPayload> {
|
|
224
|
-
const params = new URLSearchParams({ symbol, limit:
|
|
235
|
+
async getCloudEarningsHistory(symbol: string, limit = 13): Promise<EarningsHistoryPayload> {
|
|
236
|
+
const params = new URLSearchParams({ symbol, limit: String(limit) });
|
|
225
237
|
return this.request<EarningsHistoryPayload>(`/cloud/earnings/history?${params}`, { signal: AbortSignal.timeout(30_000) });
|
|
226
238
|
}
|
|
227
239
|
|
|
@@ -478,7 +490,10 @@ export class CloudDataApi {
|
|
|
478
490
|
}
|
|
479
491
|
async getCloudPerpsRankings(): Promise<PerpRankingsPayload> { return this.request("/cloud/perps/rankings"); }
|
|
480
492
|
async getCloudPerpsCompare(baseAsset: string): Promise<PerpBoardPayload> { return this.request(`/cloud/perps/compare?${new URLSearchParams({ baseAsset })}`); }
|
|
481
|
-
async getCloudPerpsEquity(symbol: string): Promise<PerpBoardPayload> {
|
|
493
|
+
async getCloudPerpsEquity(symbol: string, exchange?: string): Promise<PerpBoardPayload> {
|
|
494
|
+
const query = exchange === undefined ? "" : `?${new URLSearchParams({ identity: "listing", exchange })}`;
|
|
495
|
+
return this.request(`/cloud/perps/equity/${encodeURIComponent(symbol)}${query}`);
|
|
496
|
+
}
|
|
482
497
|
async getCloudPerpsMarket(marketId: string): Promise<PerpMarketPayload> { return this.request(`/cloud/perps/market?${new URLSearchParams({ marketId })}`); }
|
|
483
498
|
|
|
484
499
|
async analyzeCloudExposure(request: ExposureRequest): Promise<ExposurePayload> {
|
package/src/api-client/index.ts
CHANGED
|
@@ -742,6 +742,9 @@ class GloomApiClient {
|
|
|
742
742
|
getCloudEstimateRevisions = this.data.getCloudEstimateRevisions.bind(this.data);
|
|
743
743
|
getCloudMoneyMarkets = this.data.getCloudMoneyMarkets.bind(this.data);
|
|
744
744
|
equityScreener = this.data.equityScreener.bind(this.data);
|
|
745
|
+
getMemberFunds = this.data.getMemberFunds.bind(this.data);
|
|
746
|
+
getFundMembers = this.data.getFundMembers.bind(this.data);
|
|
747
|
+
getFundChanges = this.data.getFundChanges.bind(this.data);
|
|
745
748
|
getCloudThemes = this.data.getCloudThemes.bind(this.data);
|
|
746
749
|
getCloudThemeMembers = this.data.getCloudThemeMembers.bind(this.data);
|
|
747
750
|
impliedVolatility = this.data.impliedVolatility.bind(this.data);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface MemberFund { ticker: string; name: string; aliases: string[]; changes: "sp500" | "announcements" | "none" }
|
|
2
|
+
export interface FundMember {
|
|
3
|
+
id: string; symbol: string | null; name: string; sector: string | null;
|
|
4
|
+
weight: number | null; shares: number; holdingPrice: number; marketValue: number;
|
|
5
|
+
price: number | null; priceAsOf: string | null; changePercent: number | null; dailyAsOf: string | null;
|
|
6
|
+
return1WPercent: number | null; return1MPercent: number | null; returnYtdPercent: number | null;
|
|
7
|
+
contribution: number | null; stale: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface FundMembersPayload {
|
|
10
|
+
fund: MemberFund; asOf: string; fetchedAt: string; excludedDerivatives: number; stale: boolean;
|
|
11
|
+
quotesUnavailable: boolean; snapshotAsOf: string | null; fundDailyAsOf: string | null; quoteCap: number;
|
|
12
|
+
members: FundMember[];
|
|
13
|
+
aggregate: { sum: number | null; fundReturn: number | null; residual: number | null; tolerancePp: number;
|
|
14
|
+
withinTolerance: boolean | null; covered: number; total: number; coveredWeight: number; holdingsWeight: number; fresh1D: number };
|
|
15
|
+
}
|
|
16
|
+
export interface FundChange {
|
|
17
|
+
id: string; fund: string; effectiveDate: string | null; announcedAt: string | null;
|
|
18
|
+
added: string | null; removed: string | null; reason: string; headline: string | null; url: string;
|
|
19
|
+
kind: "history" | "announcement"; daysToGo: number | null;
|
|
20
|
+
estimates: { symbol: string; usd: number; advUsd: number; days: number; asOf: string; advAsOf: string | null }[];
|
|
21
|
+
}
|
|
22
|
+
export interface FundChangesPayload {
|
|
23
|
+
fund: MemberFund; changes: FundChange[]; asOf: string | null; stale: boolean; available: boolean;
|
|
24
|
+
estimateLabel: string; attribution: { text: string; url: string } | null;
|
|
25
|
+
}
|
|
26
|
+
export interface MemberFundsPayload { funds: (MemberFund & { asOf: string | null })[] }
|
package/src/api-client/perps.ts
CHANGED
|
@@ -71,6 +71,7 @@ interface PerpCandle {
|
|
|
71
71
|
|
|
72
72
|
interface UnderlyingReference {
|
|
73
73
|
symbol: string
|
|
74
|
+
listingExchange?: string
|
|
74
75
|
price: number
|
|
75
76
|
currency: string
|
|
76
77
|
asOf: string
|
|
@@ -79,6 +80,7 @@ interface UnderlyingReference {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
export interface PerpBoardRow extends PerpMarket {
|
|
83
|
+
equityMatch?: { listing: PerpEquityListing; underlying: PerpEquityListing; basis: "listing" | "issuer" }
|
|
82
84
|
stale: boolean
|
|
83
85
|
oiChange1h: number | null
|
|
84
86
|
oiChange24h: number | null
|
|
@@ -105,9 +107,11 @@ type PerpHistoryPoint = {
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
export interface PerpBoardPayload {
|
|
110
|
+
listing?: PerpEquityListing | null;
|
|
108
111
|
status: string; asOf: string | null; rows: PerpBoardRow[]; total: number; locked: number;
|
|
109
112
|
access: "pro" | "preview"; sources?: unknown[];
|
|
110
113
|
}
|
|
114
|
+
export interface PerpEquityListing { symbol: string; exchange: string }
|
|
111
115
|
export interface PerpHistoryPayload {
|
|
112
116
|
status: string; marketId: string; rows: PerpHistoryPoint[]; funding: PerpFunding[]; candles: PerpCandle[];
|
|
113
117
|
locked: boolean; access: "pro" | "preview"; asOf: string | null;
|
package/src/api-client/types.ts
CHANGED
|
@@ -295,7 +295,9 @@ export interface CloudSyncPushResponse {
|
|
|
295
295
|
settings: SyncSettings;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
export interface CloudQuotePayload extends Omit<Quote, "change" | "changePercent"> {
|
|
298
|
+
export interface CloudQuotePayload extends Omit<Quote, "change" | "changePercent" | "priceBasis" | "priceObservation"> {
|
|
299
|
+
/** The service labels daily NAV here; the app separates observation kind from price units. */
|
|
300
|
+
priceBasis?: Quote["priceBasis"] | "nav";
|
|
299
301
|
// JSON encodes the provider's unavailable numeric values as null.
|
|
300
302
|
change?: number | null;
|
|
301
303
|
changePercent?: number | null;
|
|
@@ -1375,6 +1377,10 @@ export interface CloudMarketScreenerItem {
|
|
|
1375
1377
|
change: number;
|
|
1376
1378
|
changePercent: number;
|
|
1377
1379
|
volume: number;
|
|
1380
|
+
/** Available average volume for this listing, independent of public ranking membership. */
|
|
1381
|
+
avgVolume?: number;
|
|
1382
|
+
/** Observation time of the optional retained metadata. */
|
|
1383
|
+
metadataAsOf?: string;
|
|
1378
1384
|
tradeCount?: number;
|
|
1379
1385
|
currency: string;
|
|
1380
1386
|
high52w?: number;
|
|
@@ -8,7 +8,7 @@ import type { AppAction, AppState } from "../state/app/context";
|
|
|
8
8
|
import type { TickerRecord } from "../types/ticker";
|
|
9
9
|
import { findPaneInstance } from "../types/config";
|
|
10
10
|
import type { ReleaseInfo } from "../updater";
|
|
11
|
-
import { canSelfUpdate } from "../updater";
|
|
11
|
+
import { canRestartToApply, canSelfUpdate } from "../updater";
|
|
12
12
|
import { getVisiblePaneCycleOrder } from "../components/layout/pane/cycle-order";
|
|
13
13
|
import { requestFunctionHelp, useFunctionHelpHost } from "../plugins/builtin/help/function-card";
|
|
14
14
|
import { buildRegistryHelpIndex, helpFunctionForPane } from "../plugins/builtin/help/function-index";
|
|
@@ -34,9 +34,10 @@ import {
|
|
|
34
34
|
const KEYBINDING_NOTICE_DURATION_MS = 15_000;
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Whether the install-update key has something to
|
|
37
|
+
* Whether the install-update key has something to retry: a release this build
|
|
38
38
|
* can install itself whose last attempt failed. A fresh one downloads on its
|
|
39
|
-
* own, and a manual or managed one names its own command.
|
|
39
|
+
* own, and a manual or managed one names its own command. (A downloaded
|
|
40
|
+
* desktop update has its own use of the key: restarting into it.)
|
|
40
41
|
*/
|
|
41
42
|
export function canRetryUpdate(
|
|
42
43
|
state: Pick<AppState, "updateAvailable" | "updateProgress" | "updateCheckInProgress">,
|
|
@@ -91,6 +92,7 @@ export function useAppGlobalShortcuts({
|
|
|
91
92
|
keybindings = getDefaultKeybindings(),
|
|
92
93
|
pluginRegistry,
|
|
93
94
|
refreshTicker,
|
|
95
|
+
restartToApplyUpdate,
|
|
94
96
|
startUpdate,
|
|
95
97
|
state,
|
|
96
98
|
}: {
|
|
@@ -101,6 +103,8 @@ export function useAppGlobalShortcuts({
|
|
|
101
103
|
keybindings?: ResolvedKeybindings;
|
|
102
104
|
pluginRegistry: PluginRegistry;
|
|
103
105
|
refreshTicker: (symbol: string, exchange?: string, tickerOverride?: TickerRecord | null, priority?: number) => void;
|
|
106
|
+
/** Applies a downloaded desktop update and relaunches. Runs only on the user's key press. */
|
|
107
|
+
restartToApplyUpdate: () => void;
|
|
104
108
|
startUpdate: (release: ReleaseInfo) => void;
|
|
105
109
|
state: AppState;
|
|
106
110
|
}) {
|
|
@@ -249,9 +253,13 @@ export function useAppGlobalShortcuts({
|
|
|
249
253
|
refreshTicker(ticker.metadata.ticker, ticker.metadata.exchange, ticker, 1);
|
|
250
254
|
}
|
|
251
255
|
} else if (action === "install-update") {
|
|
252
|
-
// A release that can install itself
|
|
253
|
-
// for the one that
|
|
254
|
-
if (
|
|
256
|
+
// A release that can install itself downloads on its own, so the key is
|
|
257
|
+
// for the one that waits for a restart and for the one that failed.
|
|
258
|
+
if (!isDetachedWindow && canRestartToApply(state.updateProgress)) {
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
event.stopPropagation();
|
|
261
|
+
restartToApplyUpdate();
|
|
262
|
+
} else if (canRetryUpdate(state)) {
|
|
255
263
|
event.preventDefault();
|
|
256
264
|
event.stopPropagation();
|
|
257
265
|
startUpdate(state.updateAvailable!);
|
|
@@ -129,7 +129,7 @@ export const KEYBINDING_ACTIONS: readonly KeybindingActionDef[] = [
|
|
|
129
129
|
{
|
|
130
130
|
id: "install-update",
|
|
131
131
|
category: "Global Keys",
|
|
132
|
-
description: "
|
|
132
|
+
description: "Restart into a downloaded app update, or retry one that failed.",
|
|
133
133
|
defaults: ["U"],
|
|
134
134
|
},
|
|
135
135
|
{
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { useCallback, useEffect, type Dispatch } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, type Dispatch } from "react";
|
|
2
2
|
import type { PluginRegistry } from "../../plugins/registry";
|
|
3
3
|
import { saveConfigImmediately } from "../../state/config-save-scheduler";
|
|
4
4
|
import type { AppAction, AppState } from "../../state/app/context";
|
|
5
5
|
import {
|
|
6
|
+
applyUpdate,
|
|
7
|
+
canRestartToApply,
|
|
6
8
|
canSelfUpdate,
|
|
7
9
|
checkForUpdateDetailed,
|
|
8
10
|
performUpdate,
|
|
@@ -15,6 +17,11 @@ import { runAutomated } from "../../telemetry/usage-counts";
|
|
|
15
17
|
|
|
16
18
|
const UPDATE_CHECK_INTERVAL_MS = 60 * 60_000; // hourly
|
|
17
19
|
|
|
20
|
+
/** An update is being downloaded, applied, or is downloaded and waiting for a restart. */
|
|
21
|
+
function updateInFlight(progress: AppState["updateProgress"]): boolean {
|
|
22
|
+
return progress?.phase === "downloading" || progress?.phase === "replacing" || progress?.phase === "ready";
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
export function useAppUpdateRuntime({
|
|
19
26
|
enabled = true,
|
|
20
27
|
dispatch,
|
|
@@ -36,7 +43,13 @@ export function useAppUpdateRuntime({
|
|
|
36
43
|
}): {
|
|
37
44
|
runUpdateCheck: (manual?: boolean) => Promise<void>;
|
|
38
45
|
startUpdate: (release: ReleaseInfo) => void;
|
|
46
|
+
/** Installs the downloaded desktop update and relaunches. Does nothing unless one is ready. */
|
|
47
|
+
restartToApplyUpdate: () => void;
|
|
48
|
+
/** The command bar's Check for Updates: restarts into a downloaded update, otherwise checks. */
|
|
49
|
+
runUpdateCommand: () => void;
|
|
39
50
|
} {
|
|
51
|
+
const restartingRef = useRef(false);
|
|
52
|
+
|
|
40
53
|
const startUpdate = useCallback((release: ReleaseInfo) => {
|
|
41
54
|
dispatch({ type: "SET_UPDATE_PROGRESS", progress: { phase: "downloading", percent: 0 } });
|
|
42
55
|
void performUpdate(release, (progress) => {
|
|
@@ -44,7 +57,23 @@ export function useAppUpdateRuntime({
|
|
|
44
57
|
});
|
|
45
58
|
}, [dispatch]);
|
|
46
59
|
|
|
60
|
+
const restartToApplyUpdate = useCallback(() => {
|
|
61
|
+
if (!enabled || isDetachedWindow) return;
|
|
62
|
+
// The app relaunches here, so this must only ever follow the user's own
|
|
63
|
+
// request, and a second one while the first is working changes nothing.
|
|
64
|
+
if (restartingRef.current || !canRestartToApply(stateRef.current.updateProgress)) return;
|
|
65
|
+
restartingRef.current = true;
|
|
66
|
+
dispatch({ type: "SET_UPDATE_PROGRESS", progress: { phase: "replacing" } });
|
|
67
|
+
void applyUpdate((progress) => {
|
|
68
|
+
if (progress.phase === "error") restartingRef.current = false;
|
|
69
|
+
dispatch({ type: "SET_UPDATE_PROGRESS", progress });
|
|
70
|
+
});
|
|
71
|
+
}, [dispatch, enabled, isDetachedWindow, stateRef]);
|
|
72
|
+
|
|
47
73
|
const runUpdateCheck = useCallback(async (manual = false) => {
|
|
74
|
+
// A staged update is the answer until it is applied: another check would
|
|
75
|
+
// swap the release under the "ready" state, and the host has nothing to add.
|
|
76
|
+
if (updateInFlight(stateRef.current.updateProgress)) return;
|
|
48
77
|
if (manual) {
|
|
49
78
|
dispatch({ type: "SET_UPDATE_CHECK_IN_PROGRESS", checking: true });
|
|
50
79
|
dispatch({ type: "SET_UPDATE_NOTICE", notice: null });
|
|
@@ -88,7 +117,12 @@ export function useAppUpdateRuntime({
|
|
|
88
117
|
}
|
|
89
118
|
|
|
90
119
|
dispatch({ type: "SET_UPDATE_NOTICE", notice: `Update check failed: ${result.error}` });
|
|
91
|
-
}, [dispatch]);
|
|
120
|
+
}, [dispatch, stateRef]);
|
|
121
|
+
|
|
122
|
+
const runUpdateCommand = useCallback(() => {
|
|
123
|
+
if (canRestartToApply(stateRef.current.updateProgress)) restartToApplyUpdate();
|
|
124
|
+
else void runUpdateCheck(true);
|
|
125
|
+
}, [restartToApplyUpdate, runUpdateCheck, stateRef]);
|
|
92
126
|
|
|
93
127
|
useEffect(() => {
|
|
94
128
|
if (!enabled || isDetachedWindow) return;
|
|
@@ -121,7 +155,9 @@ export function useAppUpdateRuntime({
|
|
|
121
155
|
}, [enabled, isDetachedWindow, startUpdate, updateAvailable, updateCheckInProgress, updateProgress]);
|
|
122
156
|
|
|
123
157
|
return {
|
|
158
|
+
restartToApplyUpdate,
|
|
124
159
|
runUpdateCheck,
|
|
160
|
+
runUpdateCommand,
|
|
125
161
|
startUpdate,
|
|
126
162
|
};
|
|
127
163
|
}
|
package/src/app.tsx
CHANGED
|
@@ -268,7 +268,7 @@ function AppInner({
|
|
|
268
268
|
tickerRepository,
|
|
269
269
|
});
|
|
270
270
|
|
|
271
|
-
const { runUpdateCheck, startUpdate } = useAppUpdateRuntime({
|
|
271
|
+
const { restartToApplyUpdate, runUpdateCheck, runUpdateCommand, startUpdate } = useAppUpdateRuntime({
|
|
272
272
|
enabled: updatesEnabled,
|
|
273
273
|
dispatch,
|
|
274
274
|
isDetachedWindow,
|
|
@@ -409,6 +409,7 @@ function AppInner({
|
|
|
409
409
|
keybindings,
|
|
410
410
|
pluginRegistry,
|
|
411
411
|
refreshTicker,
|
|
412
|
+
restartToApplyUpdate,
|
|
412
413
|
startUpdate,
|
|
413
414
|
state,
|
|
414
415
|
});
|
|
@@ -450,10 +451,13 @@ function AppInner({
|
|
|
450
451
|
desktopWindowBridge={desktopWindowBridge}
|
|
451
452
|
>
|
|
452
453
|
<ThemedAppRoot>
|
|
453
|
-
<Header
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
454
|
+
<Header
|
|
455
|
+
onOpenHelp={() => {
|
|
456
|
+
recordFunctionOpen({ shortcut: "HELP", externalPluginId: null });
|
|
457
|
+
pluginRegistry.showPane("help");
|
|
458
|
+
}}
|
|
459
|
+
onRestartForUpdate={updatesEnabled && !isDetachedWindow ? restartToApplyUpdate : undefined}
|
|
460
|
+
/>
|
|
457
461
|
<TransientLayoutProvider>
|
|
458
462
|
<Shell
|
|
459
463
|
pluginRegistry={pluginRegistry}
|
|
@@ -486,7 +490,7 @@ function AppInner({
|
|
|
486
490
|
tickerRepository={tickerRepository}
|
|
487
491
|
pluginRegistry={pluginRegistry}
|
|
488
492
|
quitApp={() => rendererHost.requestExit()}
|
|
489
|
-
onCheckForUpdates={updatesEnabled ?
|
|
493
|
+
onCheckForUpdates={updatesEnabled ? runUpdateCommand : undefined}
|
|
490
494
|
onNativeOccluderChange={setCommandBarNativeOccluder}
|
|
491
495
|
/>
|
|
492
496
|
)}
|
|
@@ -660,6 +660,13 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
|
660
660
|
data: QUOTES,
|
|
661
661
|
bloomberg: ["IMAP"],
|
|
662
662
|
},
|
|
663
|
+
MEMB: {
|
|
664
|
+
summary: "ETF holdings with weights, shares, member returns, daily contributions and index changes. SPX and SPY use IVV holdings.",
|
|
665
|
+
usage: ["MEMB", "MEMB SPY", "MEMB IWM"],
|
|
666
|
+
keys: [TABS, OPEN],
|
|
667
|
+
data: same("Dated fund holdings and partial delayed member returns. Nasdaq-100 is not covered."),
|
|
668
|
+
bloomberg: ["MEMB", "MRR", "IMOV"],
|
|
669
|
+
},
|
|
663
670
|
THEM: {
|
|
664
671
|
summary: "Curated thematic baskets with equal-weight returns and breadth. Open a theme to see its members, leaders and laggards. The Themes tab of BI.",
|
|
665
672
|
usage: ["THEM", "THEM nuclear"],
|
|
@@ -675,14 +682,14 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
|
675
682
|
bloomberg: ["RRG"],
|
|
676
683
|
},
|
|
677
684
|
HM: {
|
|
678
|
-
summary: "The largest US stocks and ETFs as a treemap sized by market cap or assets and colored by the day's move.",
|
|
685
|
+
summary: "The largest US stocks and ETFs as a treemap sized by the square root of market cap or assets, or by plain market cap in its settings, and colored by the day's move.",
|
|
679
686
|
usage: ["HM"],
|
|
680
687
|
keys: [OPEN],
|
|
681
688
|
data: QUOTES,
|
|
682
689
|
bloomberg: ["IMAP"],
|
|
683
690
|
},
|
|
684
691
|
FXC: {
|
|
685
|
-
summary: "A cross-rate matrix for the major currencies.",
|
|
692
|
+
summary: "A cross-rate matrix for the major currencies, or for any of 45 chosen in its settings.",
|
|
686
693
|
usage: ["FXC"],
|
|
687
694
|
keys: [],
|
|
688
695
|
data: FX,
|
|
@@ -34,6 +34,8 @@ export async function executeCollectionCommandAction(options: {
|
|
|
34
34
|
buildWorkflowDeps: () => WorkflowDeps;
|
|
35
35
|
openModeRoute: (screen: "ticker-search", initialQuery?: string, payload?: Record<string, unknown>) => void;
|
|
36
36
|
openAddToPortfolioWorkflow: (ticker: TickerRecord, preferredPortfolioId?: string | null) => void;
|
|
37
|
+
/** Join the named collection without the position form. */
|
|
38
|
+
directMembership?: boolean;
|
|
37
39
|
pushRoute: (route: CommandBarRoute) => void;
|
|
38
40
|
notify: (body: string, options?: { type?: "info" | "success" | "error" }) => void;
|
|
39
41
|
closeAll: (options?: { revertThemePreview?: boolean }) => void;
|
|
@@ -73,7 +75,9 @@ export async function executeCollectionCommandAction(options: {
|
|
|
73
75
|
};
|
|
74
76
|
if (!selectSavedOwner(options.explicitTargetId ?? options.activeCollectionId)) return;
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
// A venue picked from the add row joins that collection directly. The AP
|
|
79
|
+
// command still asks for the position.
|
|
80
|
+
if (kind === "portfolio" && action === "add" && !options.directMembership) {
|
|
77
81
|
const manualPortfolios = stateForCommand.config.portfolios.filter(isManualPortfolio);
|
|
78
82
|
if (manualPortfolios.length === 0) {
|
|
79
83
|
options.notify("Create a manual portfolio first.", { type: "info" });
|
|
@@ -3,6 +3,7 @@ import type { TickerRecord } from "../../../../types/ticker";
|
|
|
3
3
|
import { isManualPortfolio } from "../../../../plugins/builtin/portfolio-list/mutations";
|
|
4
4
|
import type { Command } from "../../commands/registry";
|
|
5
5
|
import type { ResultItem } from "../../list/model";
|
|
6
|
+
import { canRestartToApply } from "../../../../updater";
|
|
6
7
|
|
|
7
8
|
interface RootCommandItemBuilderOptions {
|
|
8
9
|
activeCollectionId: string | null;
|
|
@@ -84,6 +85,9 @@ export function createRootCommandItemBuilder({
|
|
|
84
85
|
return activeTickerSymbol ? `Remove ${activeTickerSymbol} from Portfolio` : command.label;
|
|
85
86
|
case "set-portfolio-position":
|
|
86
87
|
return activeTickerSymbol ? `Set Position for ${activeTickerSymbol}` : command.label;
|
|
88
|
+
// The row only shows its label, so a downloaded update is announced there.
|
|
89
|
+
case "check-for-updates":
|
|
90
|
+
return state.updateProgress?.phase === "ready" ? "Update ready, restart to apply" : command.label;
|
|
87
91
|
default:
|
|
88
92
|
return command.label;
|
|
89
93
|
}
|
|
@@ -128,8 +132,9 @@ export function createRootCommandItemBuilder({
|
|
|
128
132
|
return `Downloading v${state.updateAvailable?.version}: ${state.updateProgress.percent ?? 0}%`;
|
|
129
133
|
}
|
|
130
134
|
if (state.updateProgress?.phase === "replacing") return "Installing update";
|
|
135
|
+
if (state.updateProgress?.phase === "ready") return "Update ready, restart to apply";
|
|
131
136
|
if (state.updateProgress?.phase === "done") {
|
|
132
|
-
return state.updateProgress.message ?? "Update
|
|
137
|
+
return state.updateProgress.message ?? "Update ready, restart to apply";
|
|
133
138
|
}
|
|
134
139
|
if (state.updateProgress?.phase === "error") {
|
|
135
140
|
return `Update failed: ${state.updateProgress.error}`;
|
|
@@ -178,6 +183,8 @@ export function createRootCommandItemBuilder({
|
|
|
178
183
|
switch (command.id) {
|
|
179
184
|
case "set-portfolio-position":
|
|
180
185
|
return "edit position update position modify position manual position portfolio position";
|
|
186
|
+
case "check-for-updates":
|
|
187
|
+
return state.updateProgress?.phase === "ready" ? "check for updates restart relaunch" : "";
|
|
181
188
|
default:
|
|
182
189
|
return "";
|
|
183
190
|
}
|
|
@@ -196,7 +203,7 @@ export function createRootCommandItemBuilder({
|
|
|
196
203
|
searchText: smartSearchText(command),
|
|
197
204
|
disabled:
|
|
198
205
|
command.id === "check-for-updates" &&
|
|
199
|
-
(state.updateCheckInProgress || !!state.updateProgress),
|
|
206
|
+
(state.updateCheckInProgress || (!!state.updateProgress && !canRestartToApply(state.updateProgress))),
|
|
200
207
|
action: () => runDirectCommand(command, arg),
|
|
201
208
|
};
|
|
202
209
|
};
|
|
@@ -3,7 +3,7 @@ import type { PluginRegistry } from "../../../../plugins/registry";
|
|
|
3
3
|
import { findAbsorbedPlugin } from "../../../../plugins/absorbed";
|
|
4
4
|
import { isOfficialPluginRepo } from "../../../../plugins/auto-update";
|
|
5
5
|
import { getCurrentPluginTarget, runsExternalPlugins } from "../../../../plugins/current-target";
|
|
6
|
-
import {
|
|
6
|
+
import { activateInstall } from "../../../../plugins/builtin/plugin-marketplace/activation";
|
|
7
7
|
import { loadRegistry, registryPluginUrl } from "../../../../plugins/builtin/plugin-marketplace/feed";
|
|
8
8
|
import {
|
|
9
9
|
installConsent,
|
|
@@ -217,7 +217,7 @@ function buildPluginInstallItem(
|
|
|
217
217
|
onConfirm: async () => {
|
|
218
218
|
const result = await manager.install(repo, pin);
|
|
219
219
|
if (!result.ok) throw new Error(`Could not install ${plugin.name}: ${result.error}`);
|
|
220
|
-
const activated = await
|
|
220
|
+
const activated = await activateInstall(result, host, manager);
|
|
221
221
|
if (!activated.ok) throw new Error(`${plugin.name} installed but did not load: ${activated.error}`);
|
|
222
222
|
if (activated.restart) {
|
|
223
223
|
context.notify(`Restart to finish installing ${plugin.name}.`, "info");
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
mergePlainRootTickerResults,
|
|
9
9
|
mergeTickerSearchResultItems,
|
|
10
10
|
QUICK_LOOK_TICKER_SEARCH_OPTIONS,
|
|
11
|
+
venueDropdownResults,
|
|
11
12
|
} from "../ticker-search/results";
|
|
12
13
|
import { orderListResults, type ResultItem } from "../../list/model";
|
|
13
14
|
import type { CommandBarCategoryPriorities, CommandBarSectionOrder } from "../../view-model";
|
|
@@ -27,6 +28,8 @@ export function useRootProviderSearch(options: {
|
|
|
27
28
|
brokerInstanceId?: string | null,
|
|
28
29
|
) => TickerSearchCandidate[] | null;
|
|
29
30
|
rootPlainTickerSearchArg: string | null;
|
|
31
|
+
/** Set when the query is `NET:` or `NET:N`. Empty string lists every venue. */
|
|
32
|
+
listingSuffix: string | null;
|
|
30
33
|
rootResultItems: ResultItem[];
|
|
31
34
|
rootTickerSearchArg: string | null;
|
|
32
35
|
tickers: AppState["tickers"];
|
|
@@ -52,6 +55,7 @@ export function useRootProviderSearch(options: {
|
|
|
52
55
|
portfolios,
|
|
53
56
|
readTickerSearchCache,
|
|
54
57
|
rootPlainTickerSearchArg,
|
|
58
|
+
listingSuffix,
|
|
55
59
|
rootResultItems,
|
|
56
60
|
rootTickerSearchArg,
|
|
57
61
|
tickers,
|
|
@@ -181,6 +185,13 @@ export function useRootProviderSearch(options: {
|
|
|
181
185
|
}, []);
|
|
182
186
|
|
|
183
187
|
const rootResults = useMemo(() => {
|
|
188
|
+
if (listingSuffix != null) {
|
|
189
|
+
if (rootTickerSearchArg && rootProviderResultsQuery === rootTickerSearchArg && rootProviderResults) {
|
|
190
|
+
return venueDropdownResults(rootTickerSearchArg, rootProviderResults, listingSuffix);
|
|
191
|
+
}
|
|
192
|
+
// The venue list replaces commands. Nothing else shows while it loads.
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
184
195
|
if (rootTickerSearchArg && rootProviderResultsQuery === rootTickerSearchArg && rootProviderResults) {
|
|
185
196
|
if (rootPlainTickerSearchArg) {
|
|
186
197
|
return mergePlainRootTickerResults(rootPlainTickerSearchArg, rootProviderResults, rootResultItems);
|
|
@@ -189,17 +200,20 @@ export function useRootProviderSearch(options: {
|
|
|
189
200
|
}
|
|
190
201
|
return rootResultItems;
|
|
191
202
|
}, [
|
|
203
|
+
listingSuffix,
|
|
192
204
|
rootPlainTickerSearchArg,
|
|
193
205
|
rootProviderResults,
|
|
194
206
|
rootProviderResultsQuery,
|
|
195
207
|
rootResultItems,
|
|
196
208
|
rootTickerSearchArg,
|
|
197
209
|
]);
|
|
198
|
-
const rootSectionOrder: CommandBarSectionOrder =
|
|
199
|
-
? "
|
|
200
|
-
:
|
|
201
|
-
? "
|
|
202
|
-
:
|
|
210
|
+
const rootSectionOrder: CommandBarSectionOrder = listingSuffix != null
|
|
211
|
+
? "ranked"
|
|
212
|
+
: rootPlainTickerSearchArg
|
|
213
|
+
? "app-first"
|
|
214
|
+
: rootTickerSearchArg
|
|
215
|
+
? "ranked"
|
|
216
|
+
: "default";
|
|
203
217
|
const orderedRootResults = useMemo(
|
|
204
218
|
() => orderListResults(rootResults, { sectionOrder: rootSectionOrder, categoryPriorities }),
|
|
205
219
|
[categoryPriorities, rootResults, rootSectionOrder],
|
|
@@ -4,7 +4,7 @@ import type { AppState } from "../../../../state/app/context";
|
|
|
4
4
|
import type { DataProvider } from "../../../../types/data-provider";
|
|
5
5
|
import type { CommandDef, PaneTemplateCreateOptions, PaneTemplateDef } from "../../../../types/plugin";
|
|
6
6
|
import type { TickerRecord } from "../../../../types/ticker";
|
|
7
|
-
import type
|
|
7
|
+
import { listingChoiceQuery, symbolSearchQuery, type TickerSearchCandidate } from "../../../../tickers/search";
|
|
8
8
|
import type { AssistRowHandlers } from "../../assist/model";
|
|
9
9
|
import { matchPrefix, type Command } from "../../commands/registry";
|
|
10
10
|
import type { ResultItem } from "../../list/model";
|
|
@@ -246,11 +246,19 @@ export function useCommandBarRootRuntime({
|
|
|
246
246
|
// Free text that no prefix claims also goes to symbol search, so "nvidia"
|
|
247
247
|
// finds NVDA without the backtick. Skipped when a local row already carries
|
|
248
248
|
// that exact name, since an "Exact Match" symbol would otherwise outrank it.
|
|
249
|
+
// `NET:` is that symbol's venue dropdown. The search itself is the bare symbol.
|
|
250
|
+
const rootListingChoice = useMemo(() => {
|
|
251
|
+
if (currentRoute || activeMatch || rootShortcutIntent.kind !== "none") return null;
|
|
252
|
+
return listingChoiceQuery(rootQuery);
|
|
253
|
+
}, [activeMatch, currentRoute, rootQuery, rootShortcutIntent.kind]);
|
|
249
254
|
const rootPlainTickerSearchArg = useMemo(() => {
|
|
250
255
|
if (currentRoute || activeMatch || rootShortcutIntent.kind !== "none") return null;
|
|
256
|
+
// An explicit venue choice also searches one-letter symbols and names shared with local actions.
|
|
257
|
+
if (rootListingChoice) return rootListingChoice.symbol;
|
|
251
258
|
const trimmed = rootQuery.trim();
|
|
252
|
-
|
|
253
|
-
|
|
259
|
+
const symbolQuery = symbolSearchQuery(trimmed);
|
|
260
|
+
if (symbolQuery.length < 2) return null;
|
|
261
|
+
const normalizedQuery = normalizeCommandTickerSearchText(symbolQuery);
|
|
254
262
|
// The install row for "TV" is labelled TV, but it leads on purpose and
|
|
255
263
|
// must not keep Grupo Televisa out of the list.
|
|
256
264
|
const hasExactLocalRow = rootResultModel.items.some((item) => (
|
|
@@ -259,8 +267,8 @@ export function useCommandBarRootRuntime({
|
|
|
259
267
|
&& item.kind !== "search"
|
|
260
268
|
&& normalizeCommandTickerSearchText(item.label) === normalizedQuery
|
|
261
269
|
));
|
|
262
|
-
return hasExactLocalRow ? null :
|
|
263
|
-
}, [activeMatch, currentRoute, pluginInstallItem, rootQuery, rootResultModel.items, rootShortcutIntent.kind]);
|
|
270
|
+
return hasExactLocalRow ? null : symbolQuery;
|
|
271
|
+
}, [activeMatch, currentRoute, pluginInstallItem, rootListingChoice, rootQuery, rootResultModel.items, rootShortcutIntent.kind]);
|
|
264
272
|
const rootTickerSearchArg = rootSecurityDescriptionArg ?? rootPlainTickerSearchArg;
|
|
265
273
|
|
|
266
274
|
const {
|
|
@@ -278,6 +286,7 @@ export function useCommandBarRootRuntime({
|
|
|
278
286
|
portfolios: state.config.portfolios,
|
|
279
287
|
readTickerSearchCache,
|
|
280
288
|
rootPlainTickerSearchArg,
|
|
289
|
+
listingSuffix: rootPlainTickerSearchArg && rootListingChoice ? rootListingChoice.suffix : null,
|
|
281
290
|
rootResultItems: rootResultModel.items,
|
|
282
291
|
rootTickerSearchArg,
|
|
283
292
|
tickers: state.tickers,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
parseAssetClassQuery,
|
|
11
11
|
} from "../../../../tickers/search/asset-classes";
|
|
12
12
|
import type { ResultItem } from "../../list/model";
|
|
13
|
-
import { canonicalExchange, parsePublicTickerKey } from "../../../../utils/exchanges";
|
|
13
|
+
import { canonicalExchange, parsePublicTickerKey, publicExchange } from "../../../../utils/exchanges";
|
|
14
14
|
import { compactSearchText, getIssuerGroupKey, isExplicitMarketSymbol } from "../../../../tickers/search/ranking";
|
|
15
15
|
|
|
16
16
|
export const QUICK_LOOK_TICKER_SEARCH_OPTIONS = { includeOptionContracts: false } as const;
|
|
@@ -43,10 +43,10 @@ function rawInstrumentType(candidate: Pick<TickerSearchCandidate, "result" | "ti
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* Class tag for the badge column
|
|
47
|
-
* CUR, OPT, FUT, IDX, ETF
|
|
48
|
-
*
|
|
49
|
-
* short enough.
|
|
46
|
+
* Class tag for the badge column. A query can end with the same code: EQ,
|
|
47
|
+
* CUR, CRYP, OPT, FUT, IDX, ETF, FUND. A fund or derivative whose type string
|
|
48
|
+
* did not classify still badges FUND or DERIV. Anything else gets none, and
|
|
49
|
+
* the row shows its exchange code there when the code is short enough.
|
|
50
50
|
*/
|
|
51
51
|
export function formatInstrumentBadge(
|
|
52
52
|
candidate: Pick<TickerSearchCandidate, "instrumentClass" | "result" | "ticker">
|
|
@@ -167,6 +167,32 @@ export function mergePlainRootTickerResults(
|
|
|
167
167
|
];
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
function venueMatchesSuffix(item: ResultItem, suffix: string): boolean {
|
|
171
|
+
if (!suffix) return true;
|
|
172
|
+
const displayed = (item.right ?? "").trim().toUpperCase();
|
|
173
|
+
const fromLabel = parsePublicTickerKey(item.label.trim().toUpperCase()).exchange ?? "";
|
|
174
|
+
const canonicalDisplayed = canonicalExchange(displayed);
|
|
175
|
+
const targets = [displayed, canonicalDisplayed, fromLabel, publicExchange(canonicalDisplayed || displayed)];
|
|
176
|
+
if (targets.some((value) => value.startsWith(suffix))) return true;
|
|
177
|
+
const aliased = canonicalExchange(suffix);
|
|
178
|
+
return aliased !== suffix && targets.some((value) => canonicalExchange(value) === aliased);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The colon query's dropdown: one row per exact venue, in ranked order. A
|
|
183
|
+
* typed exchange prefix keeps the rows it matches.
|
|
184
|
+
*/
|
|
185
|
+
export function venueDropdownResults(symbol: string, items: ResultItem[], suffix: string): ResultItem[] {
|
|
186
|
+
const rows: ResultItem[] = [];
|
|
187
|
+
for (const item of items) {
|
|
188
|
+
if (item.kind !== "ticker" && item.kind !== "search") continue;
|
|
189
|
+
if (!isExactTickerResultMatch(item, symbol)) continue;
|
|
190
|
+
if (!venueMatchesSuffix(item, suffix)) continue;
|
|
191
|
+
rows.push(item.category === "Exact Match" ? item : { ...item, category: "Exact Match" });
|
|
192
|
+
}
|
|
193
|
+
return rows;
|
|
194
|
+
}
|
|
195
|
+
|
|
170
196
|
/**
|
|
171
197
|
* Up to the cap, in this order: the first exact row of each security, the
|
|
172
198
|
* exact symbol's other exchanges, then looser hits. The rows keep their
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import type { AppState } from "../../../../state/app/context";
|
|
3
3
|
import type { DataProvider } from "../../../../types/data-provider";
|
|
4
4
|
import type { TickerSearchCandidate } from "../../../../tickers/search";
|
|
5
|
-
import { searchTickerCandidates } from "../../../../tickers/search";
|
|
5
|
+
import { searchTickerCandidates, symbolSearchQuery } from "../../../../tickers/search";
|
|
6
6
|
import {
|
|
7
7
|
COMMAND_BAR_TICKER_SEARCH_LIMIT,
|
|
8
8
|
mergeTickerSearchResultItems,
|
|
@@ -64,7 +64,7 @@ export function useTickerSearchRouteResults(options: {
|
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const searchQuery = routeQuery
|
|
67
|
+
const searchQuery = symbolSearchQuery(routeQuery);
|
|
68
68
|
if (!searchQuery) {
|
|
69
69
|
searchRequestIdRef.current += 1;
|
|
70
70
|
setTickerSearchPending(false);
|