gloomberb 0.10.1 → 0.10.2
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 +8 -1
- package/src/api-client/auth.ts +1 -0
- package/src/api-client/index.test.ts +359 -108
- package/src/api-client/index.ts +24 -13
- package/src/api-client/socket.ts +70 -35
- package/src/api-client/types.ts +1 -1
- package/src/app/language-observer.test.tsx +82 -0
- package/src/app/language-observer.tsx +13 -0
- package/src/app/pane-runtime/index.tsx +2 -2
- package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
- package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
- package/src/app/pane-runtime/plugin-bindings.ts +2 -2
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
- package/src/app/runtime/broker-import.ts +2 -2
- package/src/app/runtime/plugin-bindings.ts +2 -2
- package/src/app/runtime/startup.ts +2 -2
- package/src/app.tsx +16 -7
- package/src/brokers/account-cache.ts +6 -6
- package/src/brokers/broker-ticker-sync.ts +3 -3
- package/src/brokers/remote-broker-adapter.ts +2 -2
- package/src/brokers/sync-broker-instance.ts +5 -6
- package/src/cli/commands/api.ts +3 -6
- package/src/cli/commands/automation.ts +7 -15
- package/src/cli/commands/market.ts +26 -61
- package/src/cli/commands/overview.ts +34 -45
- package/src/cli/commands/search.ts +25 -27
- package/src/cli/commands/system.ts +27 -57
- package/src/cli/commands/ticker.ts +62 -63
- package/src/cli/context.test.ts +63 -0
- package/src/cli/context.ts +15 -1
- package/src/cli/index.ts +1 -1
- package/src/cli/pane-functions/data.ts +2 -19
- package/src/cli/pane-functions/index.ts +26 -25
- package/src/cli/pane-functions/report.ts +1 -1
- package/src/cli/pane-functions/screenshot.ts +3 -3
- package/src/cli/scoped-context.ts +41 -0
- package/src/components/chart/composite/composite-chart.test.tsx +615 -18
- package/src/components/chart/composite/composite-chart.tsx +179 -79
- package/src/components/chart/composite/format.test.ts +20 -7
- package/src/components/chart/composite/index.ts +0 -21
- package/src/components/chart/composite/interactions.test.ts +90 -3
- package/src/components/chart/composite/interactions.ts +189 -24
- package/src/components/chart/composite/price-series.ts +0 -2
- package/src/components/chart/composite/rasterizer.ts +10 -9
- package/src/components/chart/composite/renderers.test.ts +108 -2
- package/src/components/chart/composite/scene.test.ts +36 -0
- package/src/components/chart/composite/scene.ts +5 -2
- package/src/components/chart/composite/text-renderer.ts +37 -25
- package/src/components/chart/composite/time-axis.test.ts +243 -0
- package/src/components/chart/composite/time-axis.ts +608 -0
- package/src/components/chart/composite/types.ts +12 -1
- package/src/components/chart/core/data.ts +2 -159
- package/src/components/chart/core/date-window.ts +6 -225
- package/src/components/chart/core/palette.ts +1 -12
- package/src/components/chart/core/pointer.ts +0 -142
- package/src/components/chart/core/types.ts +1 -58
- package/src/components/chart/cursor-motion.ts +2 -4
- package/src/components/chart/indicators/types.ts +0 -9
- package/src/components/chart/market-session.ts +0 -11
- package/src/components/chart/native/kitty/protocol.ts +1 -1
- package/src/components/chart/native/raster/layout.ts +3 -10
- package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
- package/src/components/chart/static/chart/surface.tsx +1 -2
- package/src/components/command-bar/action-runtime.ts +2 -2
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/panel/body.tsx +2 -2
- package/src/components/command-bar/panel/index.tsx +11 -9
- package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
- package/src/components/command-bar/surface/index.tsx +2 -2
- package/src/components/command-bar/workflow/body.tsx +3 -3
- package/src/components/command-bar/workflow/collection-actions.ts +2 -2
- package/src/components/command-bar/workflow/coordinator.ts +2 -2
- package/src/components/command-bar/workflow/field-row.tsx +25 -11
- package/src/components/command-bar/workflow/tickers.ts +2 -2
- package/src/components/feed-data-table/stack-view.test.tsx +28 -36
- package/src/components/feed-data-table/stack-view.tsx +3 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -6
- package/src/components/layout/header.tsx +13 -9
- package/src/components/layout/pane/content.test.tsx +42 -3
- package/src/components/layout/pane/content.tsx +3 -1
- package/src/components/layout/pane/footer/index.test.tsx +45 -2
- package/src/components/layout/pane/footer/registration.tsx +3 -1
- package/src/components/layout/shell/index.tsx +1 -2
- package/src/components/layout/shell/native/surfaces.ts +1 -1
- package/src/components/layout/shell/native/window-state.ts +1 -1
- package/src/components/layout/status-bar.tsx +12 -8
- package/src/components/layout/window-controls.tsx +8 -6
- package/src/components/onboarding/onboarding-wizard.tsx +3 -1
- package/src/components/pane-settings-dialog/desktop.tsx +0 -2
- package/src/components/pane-settings-dialog.tsx +0 -2
- package/src/components/pane-template-wizard.tsx +10 -9
- package/src/components/price-performance.tsx +2 -113
- package/src/components/price-sparkline/model.ts +15 -17
- package/src/components/price-sparkline/view.tsx +8 -7
- package/src/components/speedometer-gauge/desktop.tsx +2 -1
- package/src/components/speedometer-gauge/index.tsx +0 -2
- package/src/components/speedometer-gauge/terminal.tsx +24 -5
- package/src/components/ticker/badge/index.tsx +1 -18
- package/src/components/ui/data-table/opentui/index.tsx +3 -3
- package/src/components/ui/frame.tsx +1 -2
- package/src/components/ui/page-stack-view.test.tsx +126 -0
- package/src/components/ui/page-stack-view.tsx +24 -12
- package/src/components/ui/tabs.tsx +7 -3
- package/src/components/ui/ui.test.tsx +23 -2
- package/src/components/use-external-link-footer.ts +4 -2
- package/src/core/app-service-ports.ts +46 -0
- package/src/core/app-services.test.ts +66 -0
- package/src/core/app-services.ts +17 -15
- package/src/data/config/chart-settings.ts +3 -10
- package/src/data/config/store/index.test.ts +99 -2
- package/src/data/config/store/migrations.ts +278 -0
- package/src/data/config/store/normalize.ts +44 -186
- package/src/data/resource-store.ts +73 -12
- package/src/data/sqlite/cache.test.ts +104 -15
- package/src/i18n/index.test.ts +17 -0
- package/src/i18n/index.ts +23 -6
- package/src/i18n/ja.ts +13 -0
- package/src/i18n/ko.ts +13 -0
- package/src/i18n/react.ts +6 -0
- package/src/i18n/zh-cn.ts +13 -0
- package/src/i18n/zh-tw.ts +13 -0
- package/src/market-data/coordinator/auxiliary.ts +7 -3
- package/src/market-data/coordinator/chart.ts +1 -1
- package/src/market-data/coordinator/index.test.ts +26 -0
- package/src/market-data/coordinator/index.ts +11 -5
- package/src/market-data/coordinator/quotes.ts +4 -40
- package/src/market-data/coordinator/subscriptions.test.ts +89 -1
- package/src/market-data/hooks.tsx +47 -1
- package/src/market-data/market/status.ts +6 -6
- package/src/market-data/performance.ts +1 -1
- package/src/market-data/quote-subscription-target.test.ts +37 -0
- package/src/market-data/quote-subscription-target.ts +60 -0
- package/src/market-data/quotes/freshness.ts +1 -0
- package/src/market-data/quotes/resolution.ts +4 -0
- package/src/market-data/request-types.ts +3 -3
- package/src/news/aggregator.test.ts +97 -1
- package/src/news/aggregator.ts +100 -57
- package/src/news/hooks.test.tsx +64 -0
- package/src/news/hooks.ts +18 -25
- package/src/plugins/builtin/account-management/footer.ts +4 -2
- package/src/plugins/builtin/account-management/pane.tsx +9 -4
- package/src/plugins/builtin/ai/agent-history.ts +11 -7
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
- package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
- package/src/plugins/builtin/ai/index.test.ts +2 -2
- package/src/plugins/builtin/ai/index.tsx +10 -11
- package/src/plugins/builtin/ai/pane-settings.ts +1 -1
- package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
- package/src/plugins/builtin/ai/providers.ts +1 -9
- package/src/plugins/builtin/ai/runner.ts +0 -6
- package/src/plugins/builtin/ai/screener/footer.ts +4 -1
- package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
- package/src/plugins/builtin/ai/workspace/model.ts +2 -27
- package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
- package/src/plugins/builtin/analytics/metrics.ts +1 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
- package/src/plugins/builtin/broker-manager/index.tsx +4 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
- package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
- package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
- package/src/plugins/builtin/chart-composer/index.tsx +0 -6
- package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
- package/src/plugins/builtin/chart-composer/presets.ts +1 -19
- package/src/plugins/builtin/chart-composer/settings.ts +1 -1
- package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
- package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
- package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
- package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
- package/src/plugins/builtin/chat/controller/session.ts +2 -0
- package/src/plugins/builtin/chat/controller/state.ts +7 -0
- package/src/plugins/builtin/chat/controller.test.ts +73 -20
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
- package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
- package/src/plugins/builtin/correlation/compute.ts +4 -10
- package/src/plugins/builtin/econ/index.tsx +17 -19
- package/src/plugins/builtin/holders/types.ts +0 -1
- package/src/plugins/builtin/insider/index.tsx +6 -31
- package/src/plugins/builtin/news/index.tsx +5 -7
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
- package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
- package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
- package/src/plugins/builtin/news/wire/read-state.ts +18 -28
- package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
- package/src/plugins/builtin/options/footer.test.ts +115 -0
- package/src/plugins/builtin/options/footer.ts +110 -0
- package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
- package/src/plugins/builtin/options/live-quotes.ts +147 -0
- package/src/plugins/builtin/options/view.test.tsx +71 -1
- package/src/plugins/builtin/options/view.tsx +39 -24
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
- package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
- package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
- package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
- package/src/plugins/builtin/sec/filing-content.ts +94 -0
- package/src/plugins/builtin/sec/index.tsx +21 -104
- package/src/plugins/builtin/sectors/sector-model.ts +1 -7
- package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
- package/src/plugins/builtin/shared/pane-footer.ts +82 -0
- package/src/plugins/builtin/shared/read-state.ts +64 -3
- package/src/plugins/builtin/substack/read-state.ts +16 -41
- package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
- package/src/plugins/builtin/yield-curve/index.tsx +11 -9
- package/src/plugins/ibkr/dialogs.tsx +3 -3
- package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
- package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
- package/src/plugins/ibkr/gateway/history.ts +2 -2
- package/src/plugins/ibkr/gateway/selection.ts +2 -2
- package/src/plugins/ibkr/gateway/service/index.ts +2 -2
- package/src/plugins/ibkr/gateway/service/native.ts +2 -2
- package/src/plugins/plugin-persistence.ts +3 -4
- package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
- package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
- package/src/plugins/prediction-markets/detail/book.tsx +2 -2
- package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
- package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
- package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
- package/src/plugins/registry/context.ts +3 -4
- package/src/plugins/registry/index.ts +5 -6
- package/src/react/input.test.ts +126 -3
- package/src/react/input.ts +120 -1
- package/src/remote/app-host.tsx +2 -13
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
- package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
- package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
- package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
- package/src/renderers/electrobun/bun/index.ts +123 -83
- package/src/renderers/electrobun/shared/protocol.ts +146 -7
- package/src/renderers/electrobun/view/app-services.ts +5 -8
- package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
- package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
- package/src/renderers/electrobun/view/config-host.ts +1 -1
- package/src/renderers/electrobun/view/context-menu.ts +1 -17
- package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
- package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
- package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
- package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
- package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
- package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
- package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
- package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
- package/src/renderers/electrobun/view/host/box.tsx +0 -1
- package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/input.tsx +0 -1
- package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/native.ts +0 -1
- package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
- package/src/renderers/electrobun/view/host/text.tsx +0 -1
- package/src/renderers/electrobun/view/http-fetch.ts +3 -11
- package/src/renderers/electrobun/view/input-host.tsx +10 -53
- package/src/renderers/electrobun/view/key-event.ts +0 -1
- package/src/renderers/electrobun/view/main.tsx +3 -3
- package/src/renderers/electrobun/view/native-renderer.ts +0 -1
- package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
- package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
- package/src/renderers/electrobun/view/toast-host.tsx +0 -1
- package/src/renderers/electrobun/view/ui-host.tsx +1 -2
- package/src/renderers/electrobun/view/update-host.ts +1 -1
- package/src/renderers/opentui/chart-surface.tsx +2 -1
- package/src/renderers/opentui/dialog-host.tsx +20 -4
- package/src/renderers/opentui/image/loader.ts +12 -2
- package/src/renderers/opentui/input-host.tsx +7 -49
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
- package/src/sources/gloomberb-cloud/index.test.ts +173 -0
- package/src/sources/gloomberb-cloud/index.ts +42 -20
- package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
- package/src/sources/provider-router/cache.ts +1 -1
- package/src/sources/provider-router/document-routes.ts +10 -5
- package/src/sources/provider-router/financials.test.ts +14 -0
- package/src/sources/provider-router/financials.ts +6 -1
- package/src/sources/provider-router/history.test.ts +163 -1
- package/src/sources/provider-router/history.ts +278 -347
- package/src/sources/provider-router/index.ts +2 -2
- package/src/sources/provider-router/routing.test.ts +39 -0
- package/src/sources/provider-router/routing.ts +24 -11
- package/src/sources/provider-router/supplemental.ts +37 -17
- package/src/sources/sec-edgar/content.ts +2 -12
- package/src/sources/sec-edgar.ts +2 -13
- package/src/sources/yahoo-finance/history.ts +2 -2
- package/src/sources/yahoo-finance/options.ts +5 -0
- package/src/sources/yahoo-finance.ts +2 -2
- package/src/state/app/bootstrap.ts +2 -2
- package/src/state/app/context/index.tsx +2 -2
- package/src/state/hooks/quote-streaming.ts +66 -1
- package/src/state/hooks/session-snapshot.ts +2 -2
- package/src/state/selectors.ts +0 -9
- package/src/sync/controller.test.ts +98 -0
- package/src/sync/controller.ts +29 -5
- package/src/sync/react.ts +2 -2
- package/src/sync/types.ts +2 -2
- package/src/theme/colors.test.ts +10 -0
- package/src/theme/colors.ts +75 -78
- package/src/theme/theme-context.test.tsx +67 -0
- package/src/theme/theme-context.tsx +9 -5
- package/src/tickers/open-target.ts +2 -2
- package/src/tickers/search/upsert.ts +2 -2
- package/src/time-series/alignment.ts +0 -7
- package/src/time-series/chart-data.ts +145 -0
- package/src/time-series/date-window.ts +44 -0
- package/src/time-series/history-window.ts +21 -0
- package/src/time-series/hooks.test.ts +59 -0
- package/src/time-series/hooks.ts +33 -12
- package/src/time-series/market-resolution.ts +2 -2
- package/src/time-series/market.ts +1 -1
- package/src/time-series/range.ts +9 -0
- package/src/{components/chart/core → time-series}/resolution.ts +3 -102
- package/src/time-series/resolve.test.ts +203 -1
- package/src/time-series/resolve.ts +96 -18
- package/src/time-series/spec-studies.test.ts +15 -0
- package/src/time-series/spec.ts +2 -2
- package/src/time-series/studies.ts +5 -2
- package/src/time-series/types.ts +3 -1
- package/src/time-series/use-chart-resolution.test.tsx +358 -0
- package/src/time-series/use-chart-resolution.ts +46 -4
- package/src/types/broker.ts +2 -2
- package/src/types/data-provider.ts +3 -3
- package/src/types/financials.ts +12 -0
- package/src/types/plugin.ts +2 -2
- package/src/ui/context-menu.tsx +3 -2
- package/src/ui/dialog.tsx +2 -2
- package/src/utils/html-entities.ts +3 -5
- package/src/utils/main-thread-monitor.ts +1 -1
- package/src/utils/price-history.ts +5 -5
- package/src/version.ts +3 -1
- package/src/components/chart/core/viewport.test.ts +0 -8
- package/src/components/chart/core/viewport.ts +0 -24
- package/src/time-series/index.ts +0 -14
- /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
|
@@ -4,6 +4,7 @@ import type { DataProvider, QuoteSubscriptionTarget } from "../../../../types/da
|
|
|
4
4
|
import type { Quote, TickerFinancials } from "../../../../types/financials";
|
|
5
5
|
import { backendRequest, getElectrobunBackendInitSnapshot, onCapabilityEvent } from "../backend-rpc";
|
|
6
6
|
import { createCapabilityInvoker } from "./capability-invoker";
|
|
7
|
+
import { RemoteQuoteSubscriptionRegistry } from "./quote-subscription-registry";
|
|
7
8
|
|
|
8
9
|
const ASSET_DATA_CAPABILITY_ID = "asset-data.asset-data-router";
|
|
9
10
|
const NEWS_CAPABILITY_ID = "news.core";
|
|
@@ -75,17 +76,10 @@ export function createRemoteAssetDataClient(): RemoteAssetDataClient {
|
|
|
75
76
|
const assetDataOperations = getRendererOperationIds(ASSET_DATA_CAPABILITY_ID);
|
|
76
77
|
const newsOperations = getRendererOperationIds(NEWS_CAPABILITY_ID);
|
|
77
78
|
let nextSubscriptionId = 1;
|
|
78
|
-
const quoteListeners = new Map<string, {
|
|
79
|
-
target: QuoteSubscriptionTarget;
|
|
80
|
-
listeners: Set<(target: QuoteSubscriptionTarget, quote: Quote) => void>;
|
|
81
|
-
}>();
|
|
82
79
|
let quoteBackendSubscriptionId: string | null = null;
|
|
83
80
|
let disposeQuoteBackendMessages: (() => void) | null = null;
|
|
84
81
|
let quoteBackendFlushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
85
82
|
|
|
86
|
-
const quoteTargetKey = (target: QuoteSubscriptionTarget) =>
|
|
87
|
-
`${target.symbol}:${target.exchange ?? ""}:${target.context?.brokerId ?? ""}:${target.context?.brokerInstanceId ?? ""}:${target.context?.instrument?.conId ?? target.context?.instrument?.localSymbol ?? target.context?.instrument?.symbol ?? ""}`;
|
|
88
|
-
|
|
89
83
|
const scheduleQuoteBackendSubscriptionFlush = () => {
|
|
90
84
|
if (quoteBackendFlushTimer) return;
|
|
91
85
|
quoteBackendFlushTimer = setTimeout(() => {
|
|
@@ -94,9 +88,11 @@ export function createRemoteAssetDataClient(): RemoteAssetDataClient {
|
|
|
94
88
|
}, 25);
|
|
95
89
|
};
|
|
96
90
|
|
|
91
|
+
const quoteSubscriptions = new RemoteQuoteSubscriptionRegistry(scheduleQuoteBackendSubscriptionFlush);
|
|
92
|
+
|
|
97
93
|
const flushQuoteBackendSubscription = () => {
|
|
98
94
|
if (!hasRendererOperation(assetDataOperations, "subscribeQuotes")) return;
|
|
99
|
-
const targets =
|
|
95
|
+
const targets = quoteSubscriptions.backendTargets();
|
|
100
96
|
const previousSubscriptionId = quoteBackendSubscriptionId;
|
|
101
97
|
if (previousSubscriptionId) {
|
|
102
98
|
quoteBackendSubscriptionId = null;
|
|
@@ -110,10 +106,7 @@ export function createRemoteAssetDataClient(): RemoteAssetDataClient {
|
|
|
110
106
|
quoteBackendSubscriptionId = subscriptionId;
|
|
111
107
|
disposeQuoteBackendMessages = onCapabilityEvent(subscriptionId, (message) => {
|
|
112
108
|
const event = message.event as { target: QuoteSubscriptionTarget; quote: Quote };
|
|
113
|
-
|
|
114
|
-
for (const listener of quoteListeners.get(key)?.listeners ?? []) {
|
|
115
|
-
listener(event.target, event.quote);
|
|
116
|
-
}
|
|
109
|
+
quoteSubscriptions.dispatch(event.target, event.quote);
|
|
117
110
|
});
|
|
118
111
|
void backendRequest("capability.subscribe", {
|
|
119
112
|
subscriptionId,
|
|
@@ -145,38 +138,7 @@ export function createRemoteAssetDataClient(): RemoteAssetDataClient {
|
|
|
145
138
|
),
|
|
146
139
|
subscribeQuotes: (targets, onQuote) => {
|
|
147
140
|
if (!hasRendererOperation(assetDataOperations, "subscribeQuotes")) return () => {};
|
|
148
|
-
|
|
149
|
-
targets.map((target) => [
|
|
150
|
-
quoteTargetKey(target),
|
|
151
|
-
target,
|
|
152
|
-
] as const),
|
|
153
|
-
).values()];
|
|
154
|
-
if (uniqueTargets.length === 0) return () => {};
|
|
155
|
-
|
|
156
|
-
let disposed = false;
|
|
157
|
-
for (const target of uniqueTargets) {
|
|
158
|
-
const key = quoteTargetKey(target);
|
|
159
|
-
const entry = quoteListeners.get(key) ?? { target, listeners: new Set() };
|
|
160
|
-
entry.target = target;
|
|
161
|
-
entry.listeners.add(onQuote);
|
|
162
|
-
quoteListeners.set(key, entry);
|
|
163
|
-
}
|
|
164
|
-
scheduleQuoteBackendSubscriptionFlush();
|
|
165
|
-
|
|
166
|
-
return () => {
|
|
167
|
-
if (disposed) return;
|
|
168
|
-
disposed = true;
|
|
169
|
-
for (const target of uniqueTargets) {
|
|
170
|
-
const key = quoteTargetKey(target);
|
|
171
|
-
const entry = quoteListeners.get(key);
|
|
172
|
-
if (!entry) continue;
|
|
173
|
-
entry.listeners.delete(onQuote);
|
|
174
|
-
if (entry.listeners.size === 0) {
|
|
175
|
-
quoteListeners.delete(key);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
scheduleQuoteBackendSubscriptionFlush();
|
|
179
|
-
};
|
|
141
|
+
return quoteSubscriptions.subscribe(targets, onQuote);
|
|
180
142
|
},
|
|
181
143
|
};
|
|
182
144
|
|
|
@@ -16,7 +16,9 @@ class RemoteSessionStore {
|
|
|
16
16
|
schemaVersion: number;
|
|
17
17
|
}>({
|
|
18
18
|
delayMs: SESSION_SAVE_DEBOUNCE_MS,
|
|
19
|
-
save: ({ sessionId, value, schemaVersion }) =>
|
|
19
|
+
save: async ({ sessionId, value, schemaVersion }) => {
|
|
20
|
+
await backendRequest("session.set", { sessionId, value, schemaVersion });
|
|
21
|
+
},
|
|
20
22
|
});
|
|
21
23
|
|
|
22
24
|
get<T>(sessionId = "app", schemaVersion = 1) {
|
|
@@ -138,7 +140,9 @@ class RemotePluginStateStore {
|
|
|
138
140
|
this.pendingBackendSaves.clear();
|
|
139
141
|
const save = this.backendSaveInFlight
|
|
140
142
|
.catch(() => {})
|
|
141
|
-
.then(() =>
|
|
143
|
+
.then(async () => {
|
|
144
|
+
await backendRequest("pluginState.setMany", { entries });
|
|
145
|
+
})
|
|
142
146
|
.catch(() => {});
|
|
143
147
|
this.backendSaveInFlight = save;
|
|
144
148
|
return save;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { QuoteSubscriptionTarget } from "../../../../types/data-provider";
|
|
3
|
+
import type { Quote } from "../../../../types/financials";
|
|
4
|
+
import { RemoteQuoteSubscriptionRegistry } from "./quote-subscription-registry";
|
|
5
|
+
|
|
6
|
+
const lowPriorityTarget: QuoteSubscriptionTarget = {
|
|
7
|
+
symbol: "TSLA",
|
|
8
|
+
exchange: "NASDAQ",
|
|
9
|
+
surface: "watchlist",
|
|
10
|
+
visible: true,
|
|
11
|
+
selected: false,
|
|
12
|
+
weight: 10,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const highPriorityTarget: QuoteSubscriptionTarget = {
|
|
16
|
+
...lowPriorityTarget,
|
|
17
|
+
surface: "detail",
|
|
18
|
+
selected: true,
|
|
19
|
+
weight: 100,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const quote: Quote = {
|
|
23
|
+
symbol: "TSLA",
|
|
24
|
+
price: 250,
|
|
25
|
+
currency: "USD",
|
|
26
|
+
change: 5,
|
|
27
|
+
changePercent: 2,
|
|
28
|
+
lastUpdated: 1_700_000_000_000,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("RemoteQuoteSubscriptionRegistry", () => {
|
|
32
|
+
test("recomputes the backend target and listener set when an overlapping subscription leaves", () => {
|
|
33
|
+
let changes = 0;
|
|
34
|
+
const lowEvents: Array<{ target: QuoteSubscriptionTarget; quote: Quote }> = [];
|
|
35
|
+
const highEvents: Array<{ target: QuoteSubscriptionTarget; quote: Quote }> = [];
|
|
36
|
+
const registry = new RemoteQuoteSubscriptionRegistry(() => { changes += 1; });
|
|
37
|
+
|
|
38
|
+
const unsubscribeLow = registry.subscribe([lowPriorityTarget], (target, event) => lowEvents.push({ target, quote: event }));
|
|
39
|
+
const unsubscribeHigh = registry.subscribe([highPriorityTarget], (target, event) => highEvents.push({ target, quote: event }));
|
|
40
|
+
|
|
41
|
+
expect(registry.backendTargets()).toEqual([highPriorityTarget]);
|
|
42
|
+
registry.dispatch(highPriorityTarget, quote);
|
|
43
|
+
expect(lowEvents).toEqual([{ target: lowPriorityTarget, quote }]);
|
|
44
|
+
expect(highEvents).toEqual([{ target: highPriorityTarget, quote }]);
|
|
45
|
+
|
|
46
|
+
unsubscribeHigh();
|
|
47
|
+
expect(registry.backendTargets()).toEqual([lowPriorityTarget]);
|
|
48
|
+
registry.dispatch(lowPriorityTarget, quote);
|
|
49
|
+
expect(lowEvents).toEqual([
|
|
50
|
+
{ target: lowPriorityTarget, quote },
|
|
51
|
+
{ target: lowPriorityTarget, quote },
|
|
52
|
+
]);
|
|
53
|
+
expect(highEvents).toEqual([{ target: highPriorityTarget, quote }]);
|
|
54
|
+
|
|
55
|
+
unsubscribeLow();
|
|
56
|
+
expect(registry.backendTargets()).toEqual([]);
|
|
57
|
+
expect(changes).toBe(4);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("preserves options stream scope across the desktop backend bridge", () => {
|
|
61
|
+
const target: QuoteSubscriptionTarget = {
|
|
62
|
+
symbol: "AAPL260731C00110000",
|
|
63
|
+
exchange: "OPTIONS",
|
|
64
|
+
surface: "options",
|
|
65
|
+
visible: true,
|
|
66
|
+
selected: true,
|
|
67
|
+
weight: 100,
|
|
68
|
+
};
|
|
69
|
+
const optionQuote: Quote = {
|
|
70
|
+
...quote,
|
|
71
|
+
symbol: target.symbol,
|
|
72
|
+
price: 2.5,
|
|
73
|
+
bid: 2.45,
|
|
74
|
+
ask: 2.55,
|
|
75
|
+
dataSource: "live",
|
|
76
|
+
};
|
|
77
|
+
const events: Quote[] = [];
|
|
78
|
+
const registry = new RemoteQuoteSubscriptionRegistry();
|
|
79
|
+
|
|
80
|
+
registry.subscribe([target], (_eventTarget, event) => events.push(event));
|
|
81
|
+
|
|
82
|
+
expect(registry.backendTargets()).toEqual([target]);
|
|
83
|
+
registry.dispatch(target, optionQuote);
|
|
84
|
+
expect(events).toEqual([optionQuote]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mergeQuoteSubscriptionTargets } from "../../../../market-data/quote-subscription-target";
|
|
2
|
+
import type { QuoteSubscriptionTarget } from "../../../../types/data-provider";
|
|
3
|
+
import type { Quote } from "../../../../types/financials";
|
|
4
|
+
|
|
5
|
+
export type RemoteQuoteListener = (target: QuoteSubscriptionTarget, quote: Quote) => void;
|
|
6
|
+
|
|
7
|
+
interface RemoteQuoteSubscriptionEntry {
|
|
8
|
+
target: QuoteSubscriptionTarget;
|
|
9
|
+
listener: RemoteQuoteListener;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function quoteTargetKey(target: QuoteSubscriptionTarget): string {
|
|
13
|
+
return `${target.symbol}:${target.exchange ?? ""}:${target.context?.brokerId ?? ""}:${target.context?.brokerInstanceId ?? ""}:${target.context?.instrument?.conId ?? target.context?.instrument?.localSymbol ?? target.context?.instrument?.symbol ?? ""}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class RemoteQuoteSubscriptionRegistry {
|
|
17
|
+
private nextSubscriptionId = 1;
|
|
18
|
+
private readonly entriesByTarget = new Map<string, Map<number, RemoteQuoteSubscriptionEntry>>();
|
|
19
|
+
|
|
20
|
+
constructor(private readonly onChange: () => void = () => {}) {}
|
|
21
|
+
|
|
22
|
+
subscribe(targets: QuoteSubscriptionTarget[], listener: RemoteQuoteListener): () => void {
|
|
23
|
+
const groupedTargets = new Map<string, QuoteSubscriptionTarget[]>();
|
|
24
|
+
for (const target of targets) {
|
|
25
|
+
const key = quoteTargetKey(target);
|
|
26
|
+
const group = groupedTargets.get(key) ?? [];
|
|
27
|
+
group.push(target);
|
|
28
|
+
groupedTargets.set(key, group);
|
|
29
|
+
}
|
|
30
|
+
if (groupedTargets.size === 0) return () => {};
|
|
31
|
+
|
|
32
|
+
const subscriptionId = this.nextSubscriptionId++;
|
|
33
|
+
for (const [key, candidates] of groupedTargets) {
|
|
34
|
+
const target = mergeQuoteSubscriptionTargets(candidates);
|
|
35
|
+
if (!target) continue;
|
|
36
|
+
const entries = this.entriesByTarget.get(key) ?? new Map();
|
|
37
|
+
entries.set(subscriptionId, { target, listener });
|
|
38
|
+
this.entriesByTarget.set(key, entries);
|
|
39
|
+
}
|
|
40
|
+
this.onChange();
|
|
41
|
+
|
|
42
|
+
let disposed = false;
|
|
43
|
+
return () => {
|
|
44
|
+
if (disposed) return;
|
|
45
|
+
disposed = true;
|
|
46
|
+
for (const key of groupedTargets.keys()) {
|
|
47
|
+
const entries = this.entriesByTarget.get(key);
|
|
48
|
+
if (!entries) continue;
|
|
49
|
+
entries.delete(subscriptionId);
|
|
50
|
+
if (entries.size === 0) this.entriesByTarget.delete(key);
|
|
51
|
+
}
|
|
52
|
+
this.onChange();
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
backendTargets(): QuoteSubscriptionTarget[] {
|
|
57
|
+
const targets: QuoteSubscriptionTarget[] = [];
|
|
58
|
+
for (const entries of this.entriesByTarget.values()) {
|
|
59
|
+
const target = mergeQuoteSubscriptionTargets(
|
|
60
|
+
[...entries.values()].map((entry) => entry.target),
|
|
61
|
+
);
|
|
62
|
+
if (target) targets.push(target);
|
|
63
|
+
}
|
|
64
|
+
return targets;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
dispatch(target: QuoteSubscriptionTarget, quote: Quote): void {
|
|
68
|
+
for (const entry of this.entriesByTarget.get(quoteTargetKey(target))?.values() ?? []) {
|
|
69
|
+
entry.listener(entry.target, quote);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -3,11 +3,11 @@ import { backendRequest } from "../backend-rpc";
|
|
|
3
3
|
|
|
4
4
|
export class RemoteTickerRepository {
|
|
5
5
|
async loadAllTickers(): Promise<TickerRecord[]> {
|
|
6
|
-
return backendRequest
|
|
6
|
+
return backendRequest("ticker.loadAll");
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
async loadTicker(symbol: string): Promise<TickerRecord | null> {
|
|
10
|
-
return backendRequest
|
|
10
|
+
return backendRequest("ticker.load", { symbol });
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
async saveTicker(ticker: TickerRecord): Promise<void> {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/// <reference lib="dom" />
|
|
2
1
|
/** @jsxImportSource react */
|
|
3
2
|
import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
|
|
4
3
|
import type { RendererHost, UiHost } from "../../../ui/host";
|
|
@@ -190,7 +189,7 @@ export const webRendererHost: RendererHost = {
|
|
|
190
189
|
try {
|
|
191
190
|
return await navigator.clipboard.readText();
|
|
192
191
|
} catch {
|
|
193
|
-
return await backendRequest
|
|
192
|
+
return await backendRequest("host.readText");
|
|
194
193
|
}
|
|
195
194
|
},
|
|
196
195
|
notify(notification) {
|
|
@@ -9,7 +9,7 @@ import { backendRequest, onUpdateProgress } from "./backend-rpc";
|
|
|
9
9
|
export function installElectrobunUpdateHost(): void {
|
|
10
10
|
setUpdateHost({
|
|
11
11
|
checkForUpdateDetailed(currentVersion: string): Promise<UpdateCheckResult> {
|
|
12
|
-
return backendRequest
|
|
12
|
+
return backendRequest("update.check", { currentVersion });
|
|
13
13
|
},
|
|
14
14
|
performUpdate(release: ReleaseInfo, onProgress: (progress: UpdateProgress) => void): Promise<void> {
|
|
15
15
|
return new Promise((resolve) => {
|
|
@@ -96,7 +96,8 @@ export const OpenTuiChartSurface = forwardRef<unknown, ChartSurfaceProps>(functi
|
|
|
96
96
|
const surfaceId = useRef(`opentui-chart:${nextChartSurfaceId++}`).current;
|
|
97
97
|
const renderableRef = useRef<NativeRenderableNode | null>(null);
|
|
98
98
|
const [target, setTarget] = useState<SurfaceTarget | null>(null);
|
|
99
|
-
const
|
|
99
|
+
const firstBitmap = Array.isArray(bitmaps) ? bitmaps[0] : null;
|
|
100
|
+
const nativeBitmap = (firstBitmap ?? bitmap ?? null) as NativeChartBitmap | null;
|
|
100
101
|
const nativeCrosshair = (crosshair ?? null) as ChartCrosshairOverlay | null;
|
|
101
102
|
const nativeBitmapKey = useMemo(() => (nativeBitmap ? bitmapKey(nativeBitmap) : null), [nativeBitmap]);
|
|
102
103
|
|
|
@@ -2,6 +2,10 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import {
|
|
4
4
|
DialogProvider as OpenTuiDialogProvider,
|
|
5
|
+
type AlertContext as OpenTuiAlertContext,
|
|
6
|
+
type AlertOptions as OpenTuiAlertOptions,
|
|
7
|
+
type PromptContext as OpenTuiPromptContext,
|
|
8
|
+
type PromptOptions as OpenTuiPromptOptions,
|
|
5
9
|
useDialog as useOpenTuiDialog,
|
|
6
10
|
useDialogState as useOpenTuiDialogState,
|
|
7
11
|
} from "@opentui-ui/dialog/react";
|
|
@@ -45,9 +49,21 @@ function OpenTuiDialogBridge({ children }: { children: ReactNode }) {
|
|
|
45
49
|
const openTuiDialog = useOpenTuiDialog();
|
|
46
50
|
const isOpen = useOpenTuiDialogState((state) => state.isOpen);
|
|
47
51
|
const dialog = useMemo<DialogApi>(() => {
|
|
48
|
-
const
|
|
52
|
+
const wrapAlertOptions = (options: Record<string, unknown>): OpenTuiAlertOptions => ({
|
|
49
53
|
...options,
|
|
50
|
-
content: (context:
|
|
54
|
+
content: (context: OpenTuiAlertContext) => {
|
|
55
|
+
const dialogId = normalizeDialogId(context.dialogId);
|
|
56
|
+
return (
|
|
57
|
+
<OpenTuiDialogContentBridge dialog={dialog} dialogId={dialogId}>
|
|
58
|
+
{renderDialogContent(options.content, { ...context, dialogId })}
|
|
59
|
+
</OpenTuiDialogContentBridge>
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const wrapPromptOptions = <T,>(options: Record<string, unknown>): OpenTuiPromptOptions<T> => ({
|
|
65
|
+
...options,
|
|
66
|
+
content: (context: OpenTuiPromptContext<T>) => {
|
|
51
67
|
const dialogId = normalizeDialogId(context.dialogId);
|
|
52
68
|
return (
|
|
53
69
|
<OpenTuiDialogContentBridge dialog={dialog} dialogId={dialogId}>
|
|
@@ -58,8 +74,8 @@ function OpenTuiDialogBridge({ children }: { children: ReactNode }) {
|
|
|
58
74
|
});
|
|
59
75
|
|
|
60
76
|
return {
|
|
61
|
-
alert: (options) => openTuiDialog.alert(
|
|
62
|
-
prompt: (options) => openTuiDialog.prompt(
|
|
77
|
+
alert: (options) => openTuiDialog.alert(wrapAlertOptions(options)),
|
|
78
|
+
prompt: <T,>(options: Record<string, unknown>) => openTuiDialog.prompt<T>(wrapPromptOptions<T>(options)),
|
|
63
79
|
};
|
|
64
80
|
}, [openTuiDialog]);
|
|
65
81
|
|
|
@@ -7,13 +7,22 @@ export interface JimpImageLike {
|
|
|
7
7
|
bitmap: {
|
|
8
8
|
width: number;
|
|
9
9
|
height: number;
|
|
10
|
-
data:
|
|
10
|
+
data: ArrayLike<number>;
|
|
11
11
|
};
|
|
12
12
|
clone(): JimpImageLike;
|
|
13
13
|
contain(options: { w: number; h: number }): JimpImageLike;
|
|
14
14
|
cover(options: { w: number; h: number }): JimpImageLike;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function adaptJimpImage(image: JimpImageLike): JimpImageLike {
|
|
18
|
+
return {
|
|
19
|
+
bitmap: image.bitmap,
|
|
20
|
+
clone: () => adaptJimpImage(image.clone()),
|
|
21
|
+
contain: (options) => adaptJimpImage(image.contain(options)),
|
|
22
|
+
cover: (options) => adaptJimpImage(image.cover(options)),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
interface ImageBitmapOptions {
|
|
18
27
|
width: number;
|
|
19
28
|
height: number;
|
|
@@ -58,7 +67,8 @@ function loadSourceImage(src: string): Promise<JimpImageLike> {
|
|
|
58
67
|
if (cached) return cached;
|
|
59
68
|
|
|
60
69
|
const promise = fetchImageBytes(src)
|
|
61
|
-
.then((bytes) => Jimp.read(bytes)
|
|
70
|
+
.then((bytes) => Jimp.read(bytes))
|
|
71
|
+
.then(adaptJimpImage)
|
|
62
72
|
.catch((error) => {
|
|
63
73
|
sourceImageCache.delete(src);
|
|
64
74
|
throw error;
|
|
@@ -1,74 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, type ReactNode } from "react";
|
|
2
2
|
import { useNativeRenderer } from "../../ui";
|
|
3
3
|
import {
|
|
4
|
+
createShortcutRegistry,
|
|
4
5
|
InputHostProvider,
|
|
5
|
-
|
|
6
|
+
useRegisteredShortcut,
|
|
6
7
|
type InputHost,
|
|
7
|
-
type KeyEventLike,
|
|
8
|
-
type ShortcutOptions,
|
|
9
8
|
} from "../../react/input";
|
|
10
9
|
import { toKeyEventLike, useKeyboard, useTerminalDimensions } from "./host";
|
|
11
10
|
|
|
12
|
-
interface ShortcutEntry {
|
|
13
|
-
handlerRef: { current: (event: KeyEventLike) => void };
|
|
14
|
-
enabledRef: { current: boolean };
|
|
15
|
-
allowEditableRef: { current: boolean };
|
|
16
|
-
phase: NonNullable<ShortcutOptions["phase"]>;
|
|
17
|
-
order: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
let nextShortcutOrder = 1;
|
|
21
|
-
|
|
22
11
|
export function OpenTuiInputHostProvider({ children }: { children: ReactNode }) {
|
|
23
12
|
const renderer = useNativeRenderer();
|
|
24
|
-
const
|
|
25
|
-
const dispatchShortcut = (event: KeyEventLike) => {
|
|
26
|
-
const shortcuts = shortcutsRef.current;
|
|
27
|
-
for (const phase of ["before", "normal", "after"] as const) {
|
|
28
|
-
if (phase === "after" && (event.defaultPrevented || event.propagationStopped)) return;
|
|
29
|
-
for (const entry of shortcuts) {
|
|
30
|
-
if (entry.phase !== phase || !entry.enabledRef.current) continue;
|
|
31
|
-
if (!shouldDeliverShortcut(event, entry.allowEditableRef.current)) continue;
|
|
32
|
-
entry.handlerRef.current(event);
|
|
33
|
-
if (event.propagationStopped) return;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
};
|
|
13
|
+
const shortcutRegistry = useMemo(() => createShortcutRegistry(), []);
|
|
37
14
|
|
|
38
15
|
useKeyboard((event) => {
|
|
39
16
|
const shortcutEvent = toKeyEventLike(event);
|
|
40
17
|
shortcutEvent.targetEditable = renderer.currentFocusedEditor != null;
|
|
41
|
-
|
|
18
|
+
shortcutRegistry.dispatch(shortcutEvent);
|
|
42
19
|
});
|
|
43
20
|
|
|
44
21
|
const host = useMemo<InputHost>(() => ({
|
|
45
22
|
useShortcut(handler, options) {
|
|
46
|
-
|
|
47
|
-
const enabledRef = useRef(options?.enabled !== false);
|
|
48
|
-
const allowEditableRef = useRef(options?.allowEditable === true);
|
|
49
|
-
handlerRef.current = handler;
|
|
50
|
-
enabledRef.current = options?.enabled !== false;
|
|
51
|
-
allowEditableRef.current = options?.allowEditable === true;
|
|
52
|
-
|
|
53
|
-
useLayoutEffect(() => {
|
|
54
|
-
const entry: ShortcutEntry = {
|
|
55
|
-
handlerRef,
|
|
56
|
-
enabledRef,
|
|
57
|
-
allowEditableRef,
|
|
58
|
-
phase: options?.phase ?? "normal",
|
|
59
|
-
order: nextShortcutOrder++,
|
|
60
|
-
};
|
|
61
|
-
shortcutsRef.current = [...shortcutsRef.current, entry].sort((a, b) => a.order - b.order);
|
|
62
|
-
return () => {
|
|
63
|
-
shortcutsRef.current = shortcutsRef.current.filter((current) => current !== entry);
|
|
64
|
-
};
|
|
65
|
-
}, [options?.phase, options?.scope]);
|
|
23
|
+
useRegisteredShortcut(shortcutRegistry, handler, options);
|
|
66
24
|
},
|
|
67
25
|
useViewport() {
|
|
68
26
|
const dimensions = useTerminalDimensions();
|
|
69
27
|
return { width: dimensions.width, height: dimensions.height };
|
|
70
28
|
},
|
|
71
|
-
}), []);
|
|
29
|
+
}), [shortcutRegistry]);
|
|
72
30
|
|
|
73
31
|
return (
|
|
74
32
|
<InputHostProvider host={host}>
|
|
@@ -24,6 +24,7 @@ import { createPiAiHost } from "../../plugins/builtin/ai/pi";
|
|
|
24
24
|
import {
|
|
25
25
|
installAiRunHost,
|
|
26
26
|
} from "../../plugins/builtin/ai/runner";
|
|
27
|
+
import { createAppServices } from "../../core/app-services";
|
|
27
28
|
|
|
28
29
|
const AI_STARTUP_READINESS_TIMEOUT_MS = 5_000;
|
|
29
30
|
|
|
@@ -140,6 +141,7 @@ export async function startOpenTuiApp(options: StartOpenTuiAppOptions = {}): Pro
|
|
|
140
141
|
>
|
|
141
142
|
<App
|
|
142
143
|
config={config}
|
|
144
|
+
servicesFactory={createAppServices}
|
|
143
145
|
externalPlugins={externalPlugins}
|
|
144
146
|
cliLaunchRequest={cliLaunchRequest}
|
|
145
147
|
remoteControlAdapter={remoteControlAdapter}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { PricePoint } from "../../types/financials";
|
|
2
|
+
import { getPricePointTimestamp } from "../../utils/price-history";
|
|
3
|
+
|
|
4
|
+
const MAX_NEIGHBOR_GAP_MS = 60 * 60 * 1000;
|
|
5
|
+
const STABLE_NEIGHBOR_CLOSE_RATIO = 0.01;
|
|
6
|
+
const ISOLATED_OHLC_OUTLIER_RATIO = 0.04;
|
|
7
|
+
|
|
8
|
+
function relativeDifference(value: number, reference: number): number {
|
|
9
|
+
return Math.abs(value - reference) / reference;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isIsolatedOhlcOutlier(
|
|
13
|
+
previous: PricePoint,
|
|
14
|
+
point: PricePoint,
|
|
15
|
+
next: PricePoint,
|
|
16
|
+
): boolean {
|
|
17
|
+
const previousGap = getPricePointTimestamp(point) - getPricePointTimestamp(previous);
|
|
18
|
+
const nextGap = getPricePointTimestamp(next) - getPricePointTimestamp(point);
|
|
19
|
+
if (
|
|
20
|
+
previousGap <= 0
|
|
21
|
+
|| nextGap <= 0
|
|
22
|
+
|| previousGap > MAX_NEIGHBOR_GAP_MS
|
|
23
|
+
|| nextGap > MAX_NEIGHBOR_GAP_MS
|
|
24
|
+
|| relativeDifference(previous.close, point.close) > STABLE_NEIGHBOR_CLOSE_RATIO
|
|
25
|
+
|| relativeDifference(next.close, point.close) > STABLE_NEIGHBOR_CLOSE_RATIO
|
|
26
|
+
) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return [point.open, point.high, point.low].some((value) => (
|
|
31
|
+
typeof value === "number"
|
|
32
|
+
&& Number.isFinite(value)
|
|
33
|
+
&& relativeDifference(value, point.close) > ISOLATED_OHLC_OUTLIER_RATIO
|
|
34
|
+
));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function hasMalformedCloudIntradayHistory(points: readonly PricePoint[]): boolean {
|
|
38
|
+
for (let index = 1; index < points.length - 1; index += 1) {
|
|
39
|
+
if (isIsolatedOhlcOutlier(points[index - 1]!, points[index]!, points[index + 1]!)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|