gloomberb 0.9.7 → 0.9.9
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/auth.ts +20 -0
- package/src/api-client/index.test.ts +3 -3
- package/src/api-client/index.ts +48 -6
- package/src/api-client/request.test.ts +63 -0
- package/src/api-client/request.ts +53 -5
- package/src/api-client/types.ts +45 -2
- package/src/app/pane-runtime/layout-placement.ts +0 -1
- package/src/app/runtime/desktop-deeplink.test.ts +87 -0
- package/src/app/runtime/desktop-deeplink.ts +496 -0
- package/src/app.tsx +34 -0
- package/src/brokers/account-cache.ts +2 -10
- package/src/brokers/broker-portfolio-sync.ts +0 -1
- package/src/brokers/broker-ticker-sync.ts +20 -16
- package/src/brokers/remote-broker-adapter.ts +3 -0
- package/src/capabilities/registry.test.ts +25 -0
- package/src/capabilities/registry.ts +22 -5
- package/src/cli/commands/automation.ts +1 -3
- package/src/cli/commands/command-utils.ts +0 -7
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/plugins.ts +34 -17
- package/src/cli/index.ts +2 -2
- package/src/cli.test.ts +0 -157
- package/src/components/chart/bar-chart-renderer.test.ts +87 -12
- package/src/components/chart/bar-chart-renderer.ts +111 -55
- package/src/components/chart/comparison/renderer.ts +0 -1
- package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
- package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
- package/src/components/chart/core/index.test.ts +0 -24
- package/src/components/chart/core/renderer.ts +0 -4
- package/src/components/chart/core/resolution.test.ts +0 -17
- package/src/components/chart/core/resolution.ts +0 -26
- package/src/components/chart/core/scroll.test.ts +0 -6
- package/src/components/chart/core/scroll.ts +0 -22
- package/src/components/chart/core/viewport.test.ts +2 -23
- package/src/components/chart/core/viewport.ts +0 -27
- package/src/components/chart/market-session.ts +1 -1
- package/src/components/chart/native/surface/hooks.ts +10 -3
- package/src/components/chart/static/bar-chart-surface.tsx +104 -20
- package/src/components/chart/stock-chart/data-runtime.ts +0 -1
- package/src/components/chart/stock-chart/footer.test.ts +65 -0
- package/src/components/chart/stock-chart/footer.ts +68 -13
- package/src/components/chart/stock-chart/resolved.tsx +18 -4
- package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
- package/src/components/command-bar/helpers.test.ts +1 -80
- package/src/components/command-bar/helpers.ts +1 -36
- package/src/components/command-bar/pane-settings.ts +1 -1
- package/src/components/command-bar/pane-templates/items.ts +11 -3
- package/src/components/command-bar/panel/runtime.ts +0 -2
- package/src/components/command-bar/routes/root/results.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
- package/src/components/command-bar/surface/index.test.tsx +52 -16
- package/src/components/command-bar/surface/index.tsx +0 -1
- package/src/components/command-bar/workflow/ops.ts +81 -20
- package/src/components/feed-data-table/stack-view.test.tsx +36 -130
- package/src/components/feed-data-table/stack-view.tsx +0 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/shell/index.test.tsx +2 -113
- package/src/components/layout/shell/index.tsx +0 -1
- package/src/components/layout/shell/menu.ts +0 -1
- package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
- package/src/components/onboarding/onboarding-wizard.tsx +3 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -23
- package/src/components/price-selector-dialog.tsx +3 -3
- package/src/components/ticker/badge/text.tsx +0 -1
- package/src/components/toggle-list.tsx +29 -73
- package/src/components/ui/button.tsx +4 -1
- package/src/components/ui/checkbox.tsx +76 -0
- package/src/components/ui/choice-dialog.tsx +15 -3
- package/src/components/ui/frame.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/message-composer.tsx +4 -0
- package/src/components/ui/native-select.tsx +4 -2
- package/src/components/ui/page-stack-view.tsx +1 -1
- package/src/components/ui/ui.test.tsx +0 -78
- package/src/core/state/app/layout-reducer.ts +0 -1
- package/src/data/config/layout.ts +3 -3
- package/src/data/resource-store.ts +6 -0
- package/src/data/sqlite/cache.test.ts +5 -0
- package/src/data/ticker-store.ts +0 -7
- package/src/market-data/coordinator/auxiliary.ts +1 -44
- package/src/market-data/coordinator/entries.ts +0 -1
- package/src/market-data/coordinator/events.test.ts +49 -0
- package/src/market-data/coordinator/events.ts +25 -4
- package/src/market-data/coordinator/index.test.ts +223 -10
- package/src/market-data/coordinator/index.ts +41 -21
- package/src/market-data/coordinator/quotes.ts +13 -11
- package/src/market-data/coordinator/subscriptions.test.ts +1 -0
- package/src/market-data/hooks.tsx +17 -13
- package/src/market-data/market/freshness.ts +58 -1
- package/src/market-data/market/status.ts +19 -1
- package/src/market-data/query-store.ts +0 -4
- package/src/market-data/quotes/freshness.test.ts +49 -1
- package/src/market-data/quotes/freshness.ts +8 -0
- package/src/market-data/quotes/resolution.test.ts +185 -0
- package/src/market-data/quotes/resolution.ts +96 -10
- package/src/market-data/request-types.test.ts +61 -1
- package/src/market-data/request-types.ts +42 -8
- package/src/market-data/result-types.ts +0 -11
- package/src/market-data/selectors.ts +1 -5
- package/src/news/aggregator.ts +5 -1
- package/src/pane-settings.test.ts +0 -18
- package/src/plugins/builtin/account-management/footer.ts +2 -5
- package/src/plugins/builtin/account-management/form-components.tsx +175 -59
- package/src/plugins/builtin/account-management/keyboard.ts +32 -2
- package/src/plugins/builtin/account-management/model.test.ts +99 -4
- package/src/plugins/builtin/account-management/model.ts +259 -23
- package/src/plugins/builtin/account-management/pane.tsx +784 -155
- package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
- package/src/plugins/builtin/buildout/format.ts +2 -11
- package/src/plugins/builtin/chat/chat.test.tsx +127 -1
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
- package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
- package/src/plugins/builtin/chat/content/composer.tsx +84 -1
- package/src/plugins/builtin/chat/content/index.tsx +153 -16
- package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
- package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
- package/src/plugins/builtin/chat/content/mentions.ts +88 -0
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
- package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
- package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
- package/src/plugins/builtin/chat/controller/index.ts +0 -10
- package/src/plugins/builtin/chat/controller.test.ts +5 -5
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
- package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
- package/src/plugins/builtin/chat/sidebar.tsx +26 -2
- package/src/plugins/builtin/chat/test-harness.tsx +22 -12
- package/src/plugins/builtin/cloud/plugin.tsx +8 -1
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
- package/src/plugins/builtin/congress-trades/model.ts +2 -7
- package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
- package/src/plugins/builtin/correlation/index.tsx +4 -1
- package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
- package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
- package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
- package/src/plugins/builtin/earnings/index.tsx +0 -9
- package/src/plugins/builtin/econ/index.tsx +1 -5
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/pane.tsx +0 -5
- package/src/plugins/builtin/insider/index.tsx +1 -8
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
- package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -2
- package/src/plugins/builtin/news/index.tsx +0 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
- package/src/plugins/builtin/options/view.test.tsx +1 -38
- package/src/plugins/builtin/plugin-groups.ts +17 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
- package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
- package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
- package/src/plugins/builtin/sec/filing-documents.ts +0 -11
- package/src/plugins/builtin/sec/index.tsx +0 -2
- package/src/plugins/builtin/substack/article-detail.tsx +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +0 -15
- package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
- package/src/plugins/builtin/world-indices/index.tsx +0 -4
- package/src/plugins/builtin/world-indices/model.ts +0 -7
- package/src/plugins/ibkr/account-cache.ts +1 -1
- package/src/plugins/ibkr/gateway/market-data.ts +12 -5
- package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
- package/src/plugins/ibkr/portfolio-performance.ts +1 -1
- package/src/plugins/ibkr/trade/utils.ts +1 -7
- package/src/plugins/loader.ts +2 -1
- package/src/plugins/prediction-markets/detail.test.tsx +0 -34
- package/src/plugins/registry/context.ts +15 -0
- package/src/plugins/registry/contributions.ts +22 -11
- package/src/plugins/registry/index.test.ts +44 -0
- package/src/plugins/registry/index.ts +75 -26
- package/src/remote/controller.ts +1 -7
- package/src/remote/json-patch.ts +15 -1
- package/src/remote/server.ts +16 -6
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
- package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +42 -0
- package/src/renderers/electrobun/shared/protocol.ts +5 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
- package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
- package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
- package/src/renderers/electrobun/view/host/box.tsx +0 -2
- package/src/renderers/electrobun/view/host/mouse.ts +0 -11
- package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
- package/src/renderers/electrobun/view/host/text.tsx +1 -2
- package/src/renderers/electrobun/view/http-fetch.ts +15 -2
- package/src/renderers/electrobun/view/main.tsx +3 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
- package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
- package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
- package/src/renderers/electrobun/view/ui-host.tsx +2 -0
- package/src/renderers/opentui/host.tsx +11 -3
- package/src/renderers/opentui/input-host.tsx +5 -1
- package/src/sources/gloomberb-cloud/index.test.ts +0 -72
- package/src/sources/gloomberb-cloud/index.ts +1 -30
- package/src/sources/gloomberb-cloud/news.ts +0 -12
- package/src/sources/provider-router/batches.ts +11 -3
- package/src/sources/provider-router/cached-financials.test.ts +123 -0
- package/src/sources/provider-router/financial-routes.ts +77 -5
- package/src/sources/provider-router/financials.test.ts +49 -0
- package/src/sources/provider-router/financials.ts +40 -3
- package/src/sources/provider-router/index.test.ts +314 -0
- package/src/sources/provider-router/primary.ts +17 -3
- package/src/sources/provider-router/search.ts +5 -0
- package/src/sources/provider-router/streaming.ts +6 -1
- package/src/sources/provider-router/supplemental.ts +4 -3
- package/src/sources/sec-edgar.test.ts +42 -0
- package/src/sources/sec-edgar.ts +1 -2
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/sources/yahoo-finance/mappers.test.ts +29 -0
- package/src/sources/yahoo-finance/mappers.ts +2 -1
- package/src/sources/yahoo-finance/options.ts +59 -14
- package/src/sources/yahoo-finance/quote-summary.ts +4 -1
- package/src/sources/yahoo-finance/requests.ts +2 -1
- package/src/sources/yahoo-finance/snapshots.ts +13 -2
- package/src/sources/yahoo-finance.test.ts +40 -21
- package/src/sources/yahoo-finance.ts +19 -10
- package/src/state/app/context/index.test.tsx +0 -81
- package/src/state/app/context/selector.test.tsx +6 -2
- package/src/state/hooks/quote-streaming.test.ts +14 -1
- package/src/state/hooks/quote-streaming.ts +5 -4
- package/src/sync/controller.test.ts +31 -0
- package/src/sync/controller.ts +300 -0
- package/src/sync/core-contributors.test.ts +251 -0
- package/src/sync/core-contributors.ts +400 -0
- package/src/sync/profile-analytics.ts +39 -0
- package/src/sync/react.ts +53 -0
- package/src/sync/types.ts +77 -0
- package/src/test-support/data-provider.ts +0 -1
- package/src/types/desktop-deeplink.ts +7 -0
- package/src/types/plugin.ts +3 -0
- package/src/ui/host.tsx +15 -1
- package/src/ui/index.tsx +2 -0
- package/src/updater.test.ts +0 -21
- package/src/updater.ts +20 -16
- package/src/utils/async-deadline.ts +26 -0
- package/src/utils/exchanges.test.ts +11 -1
- package/src/utils/exchanges.ts +5 -1
- package/src/utils/selection-clipboard.ts +2 -2
- package/src/version.ts +1 -1
- package/src/components/layout/pane/header.test.tsx +0 -39
- package/src/plugins/builtin/chat/channels.test.ts +0 -28
- package/src/plugins/builtin/chat/index.tsx +0 -4
- package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
- package/src/plugins/builtin/earnings/index.test.ts +0 -12
- package/src/plugins/builtin/help/index.test.tsx +0 -242
- package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
- package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
- package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
- package/src/renderers/electrobun/view/host/text.test.ts +0 -22
- package/src/renderers/opentui/dialog-host.test.tsx +0 -44
- /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { useEffect, type Dispatch } from "react";
|
|
2
|
+
import { getDockedPaneIds } from "../../plugins/pane-manager";
|
|
3
|
+
import type { PluginRegistry } from "../../plugins/registry";
|
|
4
|
+
import type { AppAction, AppState } from "../../state/app/context";
|
|
5
|
+
import {
|
|
6
|
+
findPaneInstance,
|
|
7
|
+
resolveFollowBindingInstance,
|
|
8
|
+
TICKER_RESEARCH_PANE_ID,
|
|
9
|
+
} from "../../types/config";
|
|
10
|
+
import type { DesktopDeepLinkBridge } from "../../types/desktop-deeplink";
|
|
11
|
+
import type { DesktopWindowBridge } from "../../types/desktop-window";
|
|
12
|
+
|
|
13
|
+
type CloudDeepLinkRoute = {
|
|
14
|
+
kind: "cloud-alerts" | "cloud-roundup";
|
|
15
|
+
week: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type CollectionDeepLinkKind = "collection" | "portfolio" | "watchlist";
|
|
19
|
+
type AlertDeepLinkCondition = "above" | "below" | "crosses";
|
|
20
|
+
type NewsDeepLinkKind = "breaking" | "feed" | "ticker" | "top";
|
|
21
|
+
|
|
22
|
+
export type DesktopDeepLinkAction =
|
|
23
|
+
| { type: "open-account-management"; route: CloudDeepLinkRoute; message: string }
|
|
24
|
+
| { type: "open-ticker"; symbol: string; tabId: string | null; message: string }
|
|
25
|
+
| { type: "open-collection"; kind: CollectionDeepLinkKind; collectionId: string; message: string }
|
|
26
|
+
| {
|
|
27
|
+
type: "create-alert";
|
|
28
|
+
values: { symbol: string; condition: AlertDeepLinkCondition; price: string };
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
| { type: "open-chat-channel"; channelId: string; message: string }
|
|
32
|
+
| { type: "open-chat-dm"; participants: string; message: string }
|
|
33
|
+
| { type: "open-news"; kind: NewsDeepLinkKind; symbol: string | null; message: string }
|
|
34
|
+
| { type: "unsupported"; message: string };
|
|
35
|
+
|
|
36
|
+
interface ParsedGloomUrl {
|
|
37
|
+
url: URL;
|
|
38
|
+
host: string;
|
|
39
|
+
segments: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface DesktopDeepLinkHandlerOptions {
|
|
43
|
+
dispatch: Dispatch<AppAction>;
|
|
44
|
+
pluginRegistry: PluginRegistry;
|
|
45
|
+
stateRef: { current: AppState };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function weekSuffix(week: string | null): string {
|
|
49
|
+
return week ? ` for ${week}` : "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function safeDecode(value: string): string {
|
|
53
|
+
try {
|
|
54
|
+
return decodeURIComponent(value);
|
|
55
|
+
} catch {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function splitPathSegments(pathname: string): string[] {
|
|
61
|
+
return pathname
|
|
62
|
+
.split("/")
|
|
63
|
+
.map((segment) => safeDecode(segment).trim())
|
|
64
|
+
.filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseGloomUrl(rawUrl: string): ParsedGloomUrl | null {
|
|
68
|
+
let url: URL;
|
|
69
|
+
try {
|
|
70
|
+
url = new URL(rawUrl);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (url.protocol !== "gloomberb:") return null;
|
|
75
|
+
|
|
76
|
+
const host = url.hostname.trim().toLowerCase();
|
|
77
|
+
if (host) {
|
|
78
|
+
return { url, host, segments: splitPathSegments(url.pathname) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const segments = splitPathSegments(url.pathname);
|
|
82
|
+
const opaqueHost = segments.shift()?.toLowerCase();
|
|
83
|
+
return opaqueHost ? { url, host: opaqueHost, segments } : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function param(url: URL, ...names: string[]): string | null {
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
const value = url.searchParams.get(name)?.trim();
|
|
89
|
+
if (value) return value;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeSymbol(value: string | null | undefined): string | null {
|
|
95
|
+
const trimmed = value?.trim().replace(/^\$/, "");
|
|
96
|
+
if (!trimmed) return null;
|
|
97
|
+
return trimmed.toUpperCase();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeAlertCondition(value: string | null): AlertDeepLinkCondition | null {
|
|
101
|
+
switch (value?.trim().toLowerCase()) {
|
|
102
|
+
case ">":
|
|
103
|
+
case "above":
|
|
104
|
+
case "over":
|
|
105
|
+
case "gt":
|
|
106
|
+
return "above";
|
|
107
|
+
case "<":
|
|
108
|
+
case "below":
|
|
109
|
+
case "under":
|
|
110
|
+
case "lt":
|
|
111
|
+
return "below";
|
|
112
|
+
case "x":
|
|
113
|
+
case "cross":
|
|
114
|
+
case "crosses":
|
|
115
|
+
return "crosses";
|
|
116
|
+
default:
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizePrice(value: string | null): string | null {
|
|
122
|
+
const trimmed = value?.trim().replace(/^\$/, "");
|
|
123
|
+
if (!trimmed) return null;
|
|
124
|
+
const price = Number.parseFloat(trimmed);
|
|
125
|
+
return Number.isFinite(price) && price > 0 ? String(price) : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function tickerMessage(symbol: string, tabId: string | null): string {
|
|
129
|
+
return tabId ? `Opened ${symbol} ${tabId} tab.` : `Opened ${symbol}.`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function collectionMessage(kind: CollectionDeepLinkKind, collectionId: string): string {
|
|
133
|
+
const label = kind === "watchlist" ? "watchlist" : kind === "portfolio" ? "portfolio" : "collection";
|
|
134
|
+
return `Opened ${label} ${collectionId}.`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseCloudDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
|
|
138
|
+
const route = parsed.segments[0] ?? "";
|
|
139
|
+
const week = parsed.url.searchParams.get("week");
|
|
140
|
+
if (route === "roundup") {
|
|
141
|
+
return {
|
|
142
|
+
type: "open-account-management",
|
|
143
|
+
route: { kind: "cloud-roundup", week },
|
|
144
|
+
message: `Opened weekly roundup settings${weekSuffix(week)}.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (route === "alerts") {
|
|
148
|
+
return {
|
|
149
|
+
type: "open-account-management",
|
|
150
|
+
route: { kind: "cloud-alerts", week },
|
|
151
|
+
message: `Opened portfolio alert settings${weekSuffix(week)}.`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return { type: "unsupported", message: "Unsupported Gloomberb cloud link." };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseTickerDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
|
|
158
|
+
const symbol = normalizeSymbol(parsed.segments[0] ?? param(parsed.url, "symbol", "ticker"));
|
|
159
|
+
if (!symbol) return { type: "unsupported", message: "Ticker links need a symbol." };
|
|
160
|
+
const tabId = param(parsed.url, "tab");
|
|
161
|
+
return { type: "open-ticker", symbol, tabId, message: tickerMessage(symbol, tabId) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseCollectionDeepLink(parsed: ParsedGloomUrl, kind: CollectionDeepLinkKind): DesktopDeepLinkAction {
|
|
165
|
+
const collectionId = parsed.segments[0] ?? param(parsed.url, "id", "collection");
|
|
166
|
+
if (!collectionId) return { type: "unsupported", message: "Collection links need an id." };
|
|
167
|
+
return {
|
|
168
|
+
type: "open-collection",
|
|
169
|
+
kind,
|
|
170
|
+
collectionId,
|
|
171
|
+
message: collectionMessage(kind, collectionId),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseAlertDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
|
|
176
|
+
if ((parsed.segments[0] ?? "new") !== "new") {
|
|
177
|
+
return { type: "unsupported", message: "Unsupported Gloomberb alert link." };
|
|
178
|
+
}
|
|
179
|
+
const symbol = normalizeSymbol(param(parsed.url, "symbol", "ticker"));
|
|
180
|
+
const condition = normalizeAlertCondition(param(parsed.url, "condition", "side", "trigger"));
|
|
181
|
+
const price = normalizePrice(param(parsed.url, "price", "target"));
|
|
182
|
+
if (!symbol || !condition || !price) {
|
|
183
|
+
return { type: "unsupported", message: "Alert links need symbol, condition, and price." };
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
type: "create-alert",
|
|
187
|
+
values: { symbol, condition, price },
|
|
188
|
+
message: `Created ${symbol} ${condition} ${price} alert.`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function parseChatDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
|
|
193
|
+
const route = parsed.segments[0] ?? "default";
|
|
194
|
+
if (route === "channel") {
|
|
195
|
+
const channelId = parsed.segments[1] ?? param(parsed.url, "id", "channel");
|
|
196
|
+
if (!channelId) return { type: "unsupported", message: "Chat channel links need a channel id." };
|
|
197
|
+
return { type: "open-chat-channel", channelId, message: `Opened chat ${channelId}.` };
|
|
198
|
+
}
|
|
199
|
+
if (route === "dm") {
|
|
200
|
+
const participants = param(parsed.url, "users", "participants", "user") ?? parsed.segments.slice(1).join(",");
|
|
201
|
+
if (!participants.trim()) return { type: "unsupported", message: "DM links need at least one username." };
|
|
202
|
+
return { type: "open-chat-dm", participants, message: "Opened DM." };
|
|
203
|
+
}
|
|
204
|
+
if (route === "default") {
|
|
205
|
+
return { type: "open-chat-channel", channelId: "", message: "Opened chat." };
|
|
206
|
+
}
|
|
207
|
+
return { type: "open-chat-channel", channelId: route, message: `Opened chat ${route}.` };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseNewsDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
|
|
211
|
+
const ticker = normalizeSymbol(param(parsed.url, "ticker", "symbol") ?? (parsed.segments[0] === "ticker" ? parsed.segments[1] : null));
|
|
212
|
+
if (ticker) return { type: "open-news", kind: "ticker", symbol: ticker, message: `Opened ${ticker} news.` };
|
|
213
|
+
|
|
214
|
+
const route = parsed.segments[0] ?? "top";
|
|
215
|
+
if (route === "breaking") {
|
|
216
|
+
return { type: "open-news", kind: "breaking", symbol: null, message: "Opened breaking news." };
|
|
217
|
+
}
|
|
218
|
+
if (route === "feed") {
|
|
219
|
+
return { type: "open-news", kind: "feed", symbol: null, message: "Opened news feed." };
|
|
220
|
+
}
|
|
221
|
+
if (route === "top") {
|
|
222
|
+
return { type: "open-news", kind: "top", symbol: null, message: "Opened top news." };
|
|
223
|
+
}
|
|
224
|
+
return { type: "unsupported", message: "Unsupported Gloomberb news link." };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function resolveDesktopDeepLinkAction(rawUrl: string): DesktopDeepLinkAction {
|
|
228
|
+
const parsed = parseGloomUrl(rawUrl);
|
|
229
|
+
if (!parsed) return { type: "unsupported", message: "Unsupported Gloomberb link." };
|
|
230
|
+
|
|
231
|
+
switch (parsed.host) {
|
|
232
|
+
case "cloud":
|
|
233
|
+
return parseCloudDeepLink(parsed);
|
|
234
|
+
case "ticker":
|
|
235
|
+
return parseTickerDeepLink(parsed);
|
|
236
|
+
case "portfolio":
|
|
237
|
+
return parseCollectionDeepLink(parsed, "portfolio");
|
|
238
|
+
case "watchlist":
|
|
239
|
+
return parseCollectionDeepLink(parsed, "watchlist");
|
|
240
|
+
case "collection":
|
|
241
|
+
return parseCollectionDeepLink(parsed, "collection");
|
|
242
|
+
case "alert":
|
|
243
|
+
return parseAlertDeepLink(parsed);
|
|
244
|
+
case "chat":
|
|
245
|
+
return parseChatDeepLink(parsed);
|
|
246
|
+
case "news":
|
|
247
|
+
return parseNewsDeepLink(parsed);
|
|
248
|
+
default:
|
|
249
|
+
return { type: "unsupported", message: "Unsupported Gloomberb link." };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function notifyError(pluginRegistry: PluginRegistry, body: string): void {
|
|
254
|
+
pluginRegistry.notify({ body, type: "error" });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function notifySuccess(pluginRegistry: PluginRegistry, body: string): void {
|
|
258
|
+
pluginRegistry.notify({ body, type: "success" });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function requirePane(pluginRegistry: PluginRegistry, paneId: string, unavailableMessage: string): boolean {
|
|
262
|
+
if (pluginRegistry.panes.has(paneId)) return true;
|
|
263
|
+
notifyError(pluginRegistry, unavailableMessage);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function resolveCollectionRecord(
|
|
268
|
+
state: AppState,
|
|
269
|
+
action: Extract<DesktopDeepLinkAction, { type: "open-collection" }>,
|
|
270
|
+
): { name?: string } | null {
|
|
271
|
+
const portfolio = state.config.portfolios.find((entry) => entry.id === action.collectionId);
|
|
272
|
+
const watchlist = state.config.watchlists.find((entry) => entry.id === action.collectionId);
|
|
273
|
+
if (action.kind === "portfolio") return portfolio ?? null;
|
|
274
|
+
if (action.kind === "watchlist") return watchlist ?? null;
|
|
275
|
+
return portfolio ?? watchlist ?? null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function visiblePaneIds(state: AppState): Set<string> {
|
|
279
|
+
return new Set([
|
|
280
|
+
...getDockedPaneIds(state.config.layout),
|
|
281
|
+
...state.config.layout.floating.map((entry) => entry.instanceId),
|
|
282
|
+
...state.config.layout.detached.map((entry) => entry.instanceId),
|
|
283
|
+
]);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isVisiblePane(state: AppState, paneId: string | null | undefined): paneId is string {
|
|
287
|
+
return !!paneId && visiblePaneIds(state).has(paneId);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function resolveVisibleCollectionPaneId(state: AppState): string | null {
|
|
291
|
+
const followedPaneId = resolveFollowBindingInstance(
|
|
292
|
+
state.config.layout,
|
|
293
|
+
state.focusedPaneId,
|
|
294
|
+
(instance) => instance.paneId === "portfolio-list",
|
|
295
|
+
)?.instanceId;
|
|
296
|
+
if (isVisiblePane(state, followedPaneId)) return followedPaneId;
|
|
297
|
+
|
|
298
|
+
const focusedPane = state.focusedPaneId ? findPaneInstance(state.config.layout, state.focusedPaneId) : null;
|
|
299
|
+
if (focusedPane?.paneId === "portfolio-list" && isVisiblePane(state, focusedPane.instanceId)) {
|
|
300
|
+
return focusedPane.instanceId;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const visibleIds = visiblePaneIds(state);
|
|
304
|
+
return state.config.layout.instances.find((instance) => (
|
|
305
|
+
instance.paneId === "portfolio-list" && visibleIds.has(instance.instanceId)
|
|
306
|
+
))?.instanceId ?? null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function applyCollectionSelection(
|
|
310
|
+
collectionId: string,
|
|
311
|
+
{ dispatch, stateRef }: Pick<DesktopDeepLinkHandlerOptions, "dispatch" | "stateRef">,
|
|
312
|
+
): boolean {
|
|
313
|
+
const targetPaneId = resolveVisibleCollectionPaneId(stateRef.current);
|
|
314
|
+
if (!targetPaneId) return false;
|
|
315
|
+
dispatch({ type: "UPDATE_PANE_STATE", paneId: targetPaneId, patch: { collectionId } });
|
|
316
|
+
dispatch({ type: "FOCUS_PANE", paneId: targetPaneId });
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function retryCollectionSelection(
|
|
321
|
+
collectionId: string,
|
|
322
|
+
options: Pick<DesktopDeepLinkHandlerOptions, "dispatch" | "stateRef">,
|
|
323
|
+
): void {
|
|
324
|
+
globalThis.setTimeout(() => {
|
|
325
|
+
applyCollectionSelection(collectionId, options);
|
|
326
|
+
}, 50);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function handleOpenTicker(
|
|
330
|
+
action: Extract<DesktopDeepLinkAction, { type: "open-ticker" }>,
|
|
331
|
+
pluginRegistry: PluginRegistry,
|
|
332
|
+
): void {
|
|
333
|
+
if (!requirePane(pluginRegistry, TICKER_RESEARCH_PANE_ID, "Ticker research is unavailable.")) return;
|
|
334
|
+
if (action.tabId && !pluginRegistry.getTickerResearchTabPluginId(action.tabId)) {
|
|
335
|
+
notifyError(pluginRegistry, `Ticker tab "${action.tabId}" is unavailable.`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
pluginRegistry.pinTicker(action.symbol, {
|
|
340
|
+
floating: true,
|
|
341
|
+
paneType: TICKER_RESEARCH_PANE_ID,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (action.tabId) {
|
|
345
|
+
globalThis.setTimeout(() => {
|
|
346
|
+
pluginRegistry.switchTab(action.tabId!);
|
|
347
|
+
}, 80);
|
|
348
|
+
}
|
|
349
|
+
notifySuccess(pluginRegistry, action.message);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function handleOpenCollection(
|
|
353
|
+
action: Extract<DesktopDeepLinkAction, { type: "open-collection" }>,
|
|
354
|
+
options: DesktopDeepLinkHandlerOptions,
|
|
355
|
+
): void {
|
|
356
|
+
const collection = resolveCollectionRecord(options.stateRef.current, action);
|
|
357
|
+
if (!collection) {
|
|
358
|
+
notifyError(options.pluginRegistry, `Collection "${action.collectionId}" is unavailable.`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (!requirePane(options.pluginRegistry, "portfolio-list", "Collections are unavailable.")) return;
|
|
362
|
+
|
|
363
|
+
options.pluginRegistry.showPane("portfolio-list");
|
|
364
|
+
if (!applyCollectionSelection(action.collectionId, options)) {
|
|
365
|
+
retryCollectionSelection(action.collectionId, options);
|
|
366
|
+
}
|
|
367
|
+
notifySuccess(options.pluginRegistry, collection.name ? `Opened ${collection.name}.` : action.message);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function handleCreateAlert(
|
|
371
|
+
action: Extract<DesktopDeepLinkAction, { type: "create-alert" }>,
|
|
372
|
+
pluginRegistry: PluginRegistry,
|
|
373
|
+
): void {
|
|
374
|
+
const command = pluginRegistry.commands.get("set-alert");
|
|
375
|
+
if (!command) {
|
|
376
|
+
notifyError(pluginRegistry, "Alerts are unavailable.");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
void Promise.resolve(command.execute(action.values)).catch((error) => {
|
|
380
|
+
notifyError(pluginRegistry, error instanceof Error ? error.message : "Failed to create alert.");
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function handleOpenChatChannel(
|
|
385
|
+
action: Extract<DesktopDeepLinkAction, { type: "open-chat-channel" }>,
|
|
386
|
+
pluginRegistry: PluginRegistry,
|
|
387
|
+
): void {
|
|
388
|
+
if (!pluginRegistry.paneTemplates.has("new-chat-pane")) {
|
|
389
|
+
notifyError(pluginRegistry, "Chat is unavailable.");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const options = action.channelId ? { arg: action.channelId } : undefined;
|
|
393
|
+
void pluginRegistry.createPaneFromTemplateAsyncFn("new-chat-pane", options).then(() => {
|
|
394
|
+
notifySuccess(pluginRegistry, action.message);
|
|
395
|
+
}).catch((error) => {
|
|
396
|
+
notifyError(pluginRegistry, error instanceof Error ? error.message : "Failed to open chat.");
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function handleOpenChatDm(
|
|
401
|
+
action: Extract<DesktopDeepLinkAction, { type: "open-chat-dm" }>,
|
|
402
|
+
pluginRegistry: PluginRegistry,
|
|
403
|
+
): void {
|
|
404
|
+
const command = pluginRegistry.commands.get("direct-message");
|
|
405
|
+
if (!command) {
|
|
406
|
+
notifyError(pluginRegistry, "Direct messages are unavailable.");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
void Promise.resolve(command.execute({ participants: action.participants })).then(() => {
|
|
410
|
+
notifySuccess(pluginRegistry, action.message);
|
|
411
|
+
}).catch((error) => {
|
|
412
|
+
notifyError(pluginRegistry, error instanceof Error ? error.message : "Failed to open DM.");
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function handleOpenNews(
|
|
417
|
+
action: Extract<DesktopDeepLinkAction, { type: "open-news" }>,
|
|
418
|
+
pluginRegistry: PluginRegistry,
|
|
419
|
+
): void {
|
|
420
|
+
if (action.kind === "ticker") {
|
|
421
|
+
if (!action.symbol || !pluginRegistry.paneTemplates.has("ticker-news-pane")) {
|
|
422
|
+
notifyError(pluginRegistry, "Ticker news is unavailable.");
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
void pluginRegistry.createPaneFromTemplateAsyncFn("ticker-news-pane", { symbol: action.symbol }).then(() => {
|
|
426
|
+
notifySuccess(pluginRegistry, action.message);
|
|
427
|
+
}).catch((error) => {
|
|
428
|
+
notifyError(pluginRegistry, error instanceof Error ? error.message : "Failed to open ticker news.");
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const paneId = action.kind === "breaking"
|
|
434
|
+
? "news-breaking"
|
|
435
|
+
: action.kind === "feed"
|
|
436
|
+
? "news-feed"
|
|
437
|
+
: "news-top";
|
|
438
|
+
if (!requirePane(pluginRegistry, paneId, "News is unavailable.")) return;
|
|
439
|
+
pluginRegistry.showPane(paneId);
|
|
440
|
+
notifySuccess(pluginRegistry, action.message);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function handleDesktopDeepLink(rawUrl: string, options: DesktopDeepLinkHandlerOptions): void {
|
|
444
|
+
const action = resolveDesktopDeepLinkAction(rawUrl);
|
|
445
|
+
if (action.type === "unsupported") {
|
|
446
|
+
notifyError(options.pluginRegistry, action.message);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
switch (action.type) {
|
|
451
|
+
case "open-account-management":
|
|
452
|
+
if (!requirePane(options.pluginRegistry, "account-management", "Account management is unavailable.")) return;
|
|
453
|
+
options.pluginRegistry.showPane("account-management");
|
|
454
|
+
notifySuccess(options.pluginRegistry, action.message);
|
|
455
|
+
return;
|
|
456
|
+
case "open-ticker":
|
|
457
|
+
handleOpenTicker(action, options.pluginRegistry);
|
|
458
|
+
return;
|
|
459
|
+
case "open-collection":
|
|
460
|
+
handleOpenCollection(action, options);
|
|
461
|
+
return;
|
|
462
|
+
case "create-alert":
|
|
463
|
+
handleCreateAlert(action, options.pluginRegistry);
|
|
464
|
+
return;
|
|
465
|
+
case "open-chat-channel":
|
|
466
|
+
handleOpenChatChannel(action, options.pluginRegistry);
|
|
467
|
+
return;
|
|
468
|
+
case "open-chat-dm":
|
|
469
|
+
handleOpenChatDm(action, options.pluginRegistry);
|
|
470
|
+
return;
|
|
471
|
+
case "open-news":
|
|
472
|
+
handleOpenNews(action, options.pluginRegistry);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function useDesktopDeepLinkRuntime({
|
|
478
|
+
desktopDeepLinkBridge,
|
|
479
|
+
desktopWindowKind,
|
|
480
|
+
dispatch,
|
|
481
|
+
pluginRegistry,
|
|
482
|
+
stateRef,
|
|
483
|
+
}: {
|
|
484
|
+
desktopDeepLinkBridge?: DesktopDeepLinkBridge;
|
|
485
|
+
desktopWindowKind?: DesktopWindowBridge["kind"];
|
|
486
|
+
dispatch: Dispatch<AppAction>;
|
|
487
|
+
pluginRegistry: PluginRegistry;
|
|
488
|
+
stateRef: { current: AppState };
|
|
489
|
+
}) {
|
|
490
|
+
useEffect(() => {
|
|
491
|
+
if (desktopWindowKind !== "main" || !desktopDeepLinkBridge) return;
|
|
492
|
+
return desktopDeepLinkBridge.subscribe((deeplink) => {
|
|
493
|
+
handleDesktopDeepLink(deeplink.url, { dispatch, pluginRegistry, stateRef });
|
|
494
|
+
});
|
|
495
|
+
}, [desktopDeepLinkBridge, desktopWindowKind, dispatch, pluginRegistry, stateRef]);
|
|
496
|
+
}
|
package/src/app.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import { PluginRegistry } from "./plugins/registry";
|
|
|
22
22
|
import type { TickerRepository } from "./data/ticker-repository";
|
|
23
23
|
import { ThemeProvider, useThemeColors } from "./theme/theme-context";
|
|
24
24
|
import type { AppConfig } from "./types/config";
|
|
25
|
+
import type { DesktopDeepLinkBridge } from "./types/desktop-deeplink";
|
|
25
26
|
import type { CliLaunchRequest } from "./types/plugin";
|
|
26
27
|
import type { DataProvider } from "./types/data-provider";
|
|
27
28
|
import type { DesktopDockPreviewState, DesktopSharedStateSnapshot, DesktopThemePreviewState, DesktopWindowBridge } from "./types/desktop-window";
|
|
@@ -32,6 +33,7 @@ import type { MarketDataCoordinator } from "./market-data/coordinator";
|
|
|
32
33
|
import { createAppNotifier } from "./notifications/app-notifier";
|
|
33
34
|
import { createAppServices } from "./core/app-services";
|
|
34
35
|
import { useBrokerImportRuntime } from "./app/runtime/broker-import";
|
|
36
|
+
import { useDesktopDeepLinkRuntime } from "./app/runtime/desktop-deeplink";
|
|
35
37
|
import { useDesktopApplicationMenuRuntime } from "./app/runtime/desktop-menu";
|
|
36
38
|
import { useAppGlobalShortcuts } from "./app/global-shortcuts";
|
|
37
39
|
import { useAppPaneRuntime } from "./app/pane-runtime";
|
|
@@ -39,6 +41,8 @@ import { bindPluginRegistryRuntimeAccess } from "./app/runtime/plugin-bindings";
|
|
|
39
41
|
import { useAppStartupRuntime } from "./app/runtime/startup";
|
|
40
42
|
import { useTickerRefreshRuntime } from "./app/runtime/ticker-refresh";
|
|
41
43
|
import { useAppUpdateRuntime } from "./app/runtime/update";
|
|
44
|
+
import { createCoreSyncContributors } from "./sync/core-contributors";
|
|
45
|
+
import { useCloudSyncRuntime } from "./sync/react";
|
|
42
46
|
import { RemoteControlHost, type RemoteControlAdapter } from "./remote/app-host";
|
|
43
47
|
import { RemoteUiRegistryProvider } from "./remote/semantic-tree";
|
|
44
48
|
import {
|
|
@@ -57,6 +61,7 @@ interface AppInnerProps {
|
|
|
57
61
|
sessionSnapshot?: AppSessionSnapshot | null;
|
|
58
62
|
desktopWindowBridge?: DesktopWindowBridge;
|
|
59
63
|
desktopApplicationMenuBridge?: DesktopApplicationMenuBridge;
|
|
64
|
+
desktopDeepLinkBridge?: DesktopDeepLinkBridge;
|
|
60
65
|
remoteControlAdapter?: RemoteControlAdapter;
|
|
61
66
|
}
|
|
62
67
|
|
|
@@ -86,6 +91,7 @@ function AppInner({
|
|
|
86
91
|
sessionSnapshot = null,
|
|
87
92
|
desktopWindowBridge,
|
|
88
93
|
desktopApplicationMenuBridge,
|
|
94
|
+
desktopDeepLinkBridge,
|
|
89
95
|
remoteControlAdapter,
|
|
90
96
|
}: AppInnerProps) {
|
|
91
97
|
const dispatch = useAppDispatch();
|
|
@@ -173,6 +179,15 @@ function AppInner({
|
|
|
173
179
|
});
|
|
174
180
|
}, [desktopWindowBridge]);
|
|
175
181
|
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
const disposers = createCoreSyncContributors().map((contributor) => (
|
|
184
|
+
pluginRegistry.registerSyncContributorForPlugin("core", contributor)
|
|
185
|
+
));
|
|
186
|
+
return () => {
|
|
187
|
+
for (const dispose of disposers) dispose();
|
|
188
|
+
};
|
|
189
|
+
}, [pluginRegistry]);
|
|
190
|
+
|
|
176
191
|
const {
|
|
177
192
|
primeCachedFinancials,
|
|
178
193
|
refreshQuote,
|
|
@@ -214,6 +229,14 @@ function AppInner({
|
|
|
214
229
|
stateRef,
|
|
215
230
|
});
|
|
216
231
|
|
|
232
|
+
useDesktopDeepLinkRuntime({
|
|
233
|
+
desktopDeepLinkBridge,
|
|
234
|
+
desktopWindowKind: desktopWindowBridge?.kind,
|
|
235
|
+
dispatch,
|
|
236
|
+
pluginRegistry,
|
|
237
|
+
stateRef,
|
|
238
|
+
});
|
|
239
|
+
|
|
217
240
|
const focusedTickerSymbol = getFocusedTickerSymbol(state);
|
|
218
241
|
useAppStartupRuntime({
|
|
219
242
|
appActive,
|
|
@@ -243,6 +266,14 @@ function AppInner({
|
|
|
243
266
|
tickerRepository,
|
|
244
267
|
});
|
|
245
268
|
|
|
269
|
+
useCloudSyncRuntime({
|
|
270
|
+
state,
|
|
271
|
+
dispatch,
|
|
272
|
+
tickerRepository,
|
|
273
|
+
pluginRegistry,
|
|
274
|
+
initialized: state.initialized && desktopWindowBridge?.kind !== "detached",
|
|
275
|
+
});
|
|
276
|
+
|
|
246
277
|
useAppPaneRuntime({
|
|
247
278
|
dataProvider,
|
|
248
279
|
detachedPaneId,
|
|
@@ -353,6 +384,7 @@ interface AppProps {
|
|
|
353
384
|
cliLaunchRequest?: CliLaunchRequest | null;
|
|
354
385
|
desktopWindowBridge?: DesktopWindowBridge;
|
|
355
386
|
desktopApplicationMenuBridge?: DesktopApplicationMenuBridge;
|
|
387
|
+
desktopDeepLinkBridge?: DesktopDeepLinkBridge;
|
|
356
388
|
desktopSnapshot?: DesktopSharedStateSnapshot | null;
|
|
357
389
|
desktopThemePreview?: DesktopThemePreviewState | null;
|
|
358
390
|
remoteControlAdapter?: RemoteControlAdapter;
|
|
@@ -364,6 +396,7 @@ export function App({
|
|
|
364
396
|
cliLaunchRequest = null,
|
|
365
397
|
desktopWindowBridge,
|
|
366
398
|
desktopApplicationMenuBridge,
|
|
399
|
+
desktopDeepLinkBridge,
|
|
367
400
|
desktopSnapshot = null,
|
|
368
401
|
desktopThemePreview = null,
|
|
369
402
|
remoteControlAdapter,
|
|
@@ -451,6 +484,7 @@ export function App({
|
|
|
451
484
|
sessionSnapshot={sessionSnapshot}
|
|
452
485
|
desktopWindowBridge={desktopWindowBridge}
|
|
453
486
|
desktopApplicationMenuBridge={desktopApplicationMenuBridge}
|
|
487
|
+
desktopDeepLinkBridge={desktopDeepLinkBridge}
|
|
454
488
|
remoteControlAdapter={remoteControlAdapter}
|
|
455
489
|
/>
|
|
456
490
|
</AppProvider>
|
|
@@ -3,6 +3,7 @@ import type { BrokerAdapter } from "../types/broker";
|
|
|
3
3
|
import type { BrokerInstanceConfig } from "../types/config";
|
|
4
4
|
import type { CachePolicy } from "../types/persistence";
|
|
5
5
|
import type { BrokerAccount } from "../types/trading";
|
|
6
|
+
import { fnv1aHashString } from "../utils/hash";
|
|
6
7
|
|
|
7
8
|
const BROKER_ACCOUNT_SNAPSHOT_KIND = "account-snapshot";
|
|
8
9
|
const BROKER_ACCOUNT_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
@@ -16,15 +17,6 @@ interface PersistedBrokerAccountSnapshot {
|
|
|
16
17
|
brokerType?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function hashString(input: string): string {
|
|
20
|
-
let hash = 0x811c9dc5;
|
|
21
|
-
for (let index = 0; index < input.length; index += 1) {
|
|
22
|
-
hash ^= input.charCodeAt(index);
|
|
23
|
-
hash = Math.imul(hash, 0x01000193);
|
|
24
|
-
}
|
|
25
|
-
return `fnv1a:${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
20
|
function brokerAccountNamespace(instance: BrokerInstanceConfig): string {
|
|
29
21
|
return `plugin:${instance.brokerType}`;
|
|
30
22
|
}
|
|
@@ -36,7 +28,7 @@ export function getBrokerAccountCacheSourceKey(
|
|
|
36
28
|
if (broker?.getAccountCacheSourceKey) {
|
|
37
29
|
return broker.getAccountCacheSourceKey(instance);
|
|
38
30
|
}
|
|
39
|
-
return
|
|
31
|
+
return fnv1aHashString(JSON.stringify({
|
|
40
32
|
brokerType: instance.brokerType,
|
|
41
33
|
config: broker?.toConfigValues?.(instance) ?? instance.config,
|
|
42
34
|
}));
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AppConfig, BrokerInstanceConfig } from "../types/config";
|
|
2
|
-
import type { BrokerContractRef } from "../types/instrument";
|
|
3
2
|
import type { Portfolio, TickerPosition, TickerRecord } from "../types/ticker";
|
|
4
3
|
import { buildBrokerPortfolioId, getBrokerInstance, isBrokerPortfolioId } from "../utils/broker-instances";
|
|
5
4
|
|
|
@@ -78,29 +78,33 @@ function updateExistingTicker(
|
|
|
78
78
|
positionEntry: TickerPosition,
|
|
79
79
|
brokerContract?: BrokerContractRef,
|
|
80
80
|
): TickerRecord {
|
|
81
|
-
if (position.name && ticker.metadata.name === ticker.metadata.ticker) {
|
|
82
|
-
ticker.metadata.name = position.name;
|
|
83
|
-
}
|
|
84
|
-
if (position.assetCategory && !ticker.metadata.assetCategory) {
|
|
85
|
-
ticker.metadata.assetCategory = position.assetCategory;
|
|
86
|
-
}
|
|
87
|
-
if (position.isin && !ticker.metadata.isin) {
|
|
88
|
-
ticker.metadata.isin = position.isin;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
81
|
const otherPositions = ticker.metadata.positions.filter(
|
|
92
82
|
(entry) => !(entry.portfolio === portfolioId && entry.broker === instance.brokerType),
|
|
93
83
|
);
|
|
94
|
-
|
|
95
|
-
ticker.metadata.broker_contracts = mergeBrokerContracts(
|
|
84
|
+
const brokerContracts = mergeBrokerContracts(
|
|
96
85
|
ticker.metadata.broker_contracts ?? [],
|
|
97
86
|
brokerContract ? [brokerContract] : [],
|
|
98
87
|
);
|
|
99
|
-
|
|
100
|
-
ticker.metadata.portfolios
|
|
101
|
-
|
|
88
|
+
const portfolios = ticker.metadata.portfolios.includes(portfolioId)
|
|
89
|
+
? ticker.metadata.portfolios
|
|
90
|
+
: [...ticker.metadata.portfolios, portfolioId];
|
|
102
91
|
|
|
103
|
-
return
|
|
92
|
+
return {
|
|
93
|
+
...ticker,
|
|
94
|
+
metadata: {
|
|
95
|
+
...ticker.metadata,
|
|
96
|
+
name: position.name && ticker.metadata.name === ticker.metadata.ticker
|
|
97
|
+
? position.name
|
|
98
|
+
: ticker.metadata.name,
|
|
99
|
+
assetCategory: position.assetCategory && !ticker.metadata.assetCategory
|
|
100
|
+
? position.assetCategory
|
|
101
|
+
: ticker.metadata.assetCategory,
|
|
102
|
+
isin: position.isin && !ticker.metadata.isin ? position.isin : ticker.metadata.isin,
|
|
103
|
+
positions: [...otherPositions, positionEntry],
|
|
104
|
+
broker_contracts: brokerContracts,
|
|
105
|
+
portfolios,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
export async function upsertBrokerPositionTicker({
|