gloomberb 0.11.0 → 0.11.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/README.md +38 -15
- package/README.zh-CN.md +12 -12
- package/package.json +10 -3
- package/src/api-client/auth.ts +14 -10
- package/src/api-client/data.ts +54 -0
- package/src/api-client/index.test.ts +124 -0
- package/src/api-client/index.ts +139 -4
- package/src/api-client/paths.ts +89 -0
- package/src/api-client/request.ts +15 -2
- package/src/api-client/socket-health.test.ts +1 -0
- package/src/api-client/socket.ts +2 -1
- package/src/api-client/types.ts +163 -0
- package/src/app/pane-runtime/index.tsx +4 -1
- package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
- package/src/app/pane-runtime/plugin-bindings.ts +25 -4
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
- package/src/app/runtime/desktop-deeplink.test.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +66 -2
- package/src/app/runtime/desktop-menu.ts +3 -0
- package/src/app/runtime/plugin-bindings.ts +30 -28
- package/src/app/runtime/update.ts +8 -6
- package/src/app.tsx +21 -7
- package/src/architecture/import-boundaries.test.ts +5 -1
- package/src/capabilities/factories.ts +4 -0
- package/src/cli/commands/automation.test.ts +9 -0
- package/src/cli/commands/automation.ts +22 -73
- package/src/cli/index.ts +10 -6
- package/src/cli/registry.ts +1 -1
- package/src/components/chart/composite/composite-chart.test.tsx +45 -2
- package/src/components/chart/composite/composite-chart.tsx +16 -12
- package/src/components/chart/composite/format.test.ts +34 -0
- package/src/components/chart/composite/format.ts +33 -10
- package/src/components/chart/composite/scene.ts +0 -21
- package/src/components/chart/composite/tools.ts +49 -0
- package/src/components/chart/core/axis-format.ts +13 -3
- package/src/components/chart/core/index.test.ts +7 -0
- package/src/components/chart/core/renderer.ts +3 -4
- package/src/components/command-bar/commands/direct/index.ts +4 -0
- package/src/components/command-bar/commands/registry.ts +8 -1
- package/src/components/command-bar/layout-items/current-layout.ts +1 -1
- package/src/components/command-bar/pane-templates/workflow.ts +4 -0
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
- package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
- package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
- package/src/components/command-bar/surface/index.test.tsx +6 -6
- package/src/components/command-bar/surface/index.tsx +4 -1
- package/src/components/command-bar/surface/pane.test.tsx +66 -2
- package/src/components/command-bar/view-model.test.ts +2 -1
- package/src/components/command-bar/workflow/ops.test.ts +54 -0
- package/src/components/command-bar/workflow/ops.ts +1 -1
- package/src/components/company-logo.test.ts +19 -0
- package/src/components/company-logo.tsx +60 -0
- package/src/components/feed-data-table/stack-view.tsx +7 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +85 -15
- package/src/components/layout/header.tsx +5 -5
- package/src/components/layout/pane/title.ts +31 -3
- package/src/components/layout/shell/active-drag.ts +4 -11
- package/src/components/layout/shell/drag/index.ts +4 -20
- package/src/components/layout/shell/drag/runtime.ts +3 -4
- package/src/components/layout/shell/index.test.tsx +17 -3
- package/src/components/layout/shell/index.tsx +89 -31
- package/src/components/layout/shell/menu.test.ts +37 -1
- package/src/components/layout/shell/menu.ts +14 -0
- package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
- package/src/components/layout/shell/pane/actions.ts +0 -5
- package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
- package/src/components/layout/shell/shortcuts.ts +6 -3
- package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
- package/src/components/layout/shell/ticker-link-menu.ts +78 -0
- package/src/components/layout/status-bar.test.tsx +62 -61
- package/src/components/layout/status-bar.tsx +53 -75
- package/src/components/onboarding/account-step/form-panel.tsx +4 -2
- package/src/components/onboarding/account-step/index.tsx +1 -1
- package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
- package/src/components/onboarding/onboarding-wizard.tsx +13 -5
- package/src/components/onboarding/wizard-account.ts +4 -23
- package/src/components/table-view-shared.test.ts +11 -0
- package/src/components/table-view-shared.tsx +26 -0
- package/src/components/ui/data-table/index.tsx +19 -1
- package/src/components/ui/fields.tsx +5 -1
- package/src/core/state/app/layout-reducer.ts +36 -0
- package/src/core/state/app/layout.ts +10 -5
- package/src/core/state/app/state.ts +0 -13
- package/src/core/state/app/types.ts +6 -4
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/config/layout.ts +2 -0
- package/src/data/config/store/index.test.ts +27 -6
- package/src/data/config/store/normalize.ts +3 -5
- package/src/data/plugin-state-store.ts +11 -0
- package/src/data/sqlite/cache.test.ts +10 -0
- package/src/i18n/es.ts +13 -5
- package/src/i18n/ja.ts +13 -5
- package/src/i18n/ko.ts +13 -5
- package/src/i18n/zh-cn.ts +13 -5
- package/src/i18n/zh-tw.ts +13 -5
- package/src/layout-marketplace/api.test.ts +54 -0
- package/src/layout-marketplace/api.ts +65 -0
- package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
- package/src/layout-marketplace/gallery-desktop.tsx +375 -0
- package/src/layout-marketplace/gallery-terminal.tsx +376 -0
- package/src/layout-marketplace/gallery.test.tsx +84 -0
- package/src/layout-marketplace/gallery.tsx +323 -0
- package/src/layout-marketplace/geometry.test.ts +47 -0
- package/src/layout-marketplace/geometry.ts +80 -0
- package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
- package/src/layout-marketplace/mini-workspace.tsx +428 -0
- package/src/layout-marketplace/model.test.ts +118 -0
- package/src/layout-marketplace/model.ts +171 -0
- package/src/layout-marketplace/name-dialog.tsx +60 -0
- package/src/layout-marketplace/pane-imagery.ts +85 -0
- package/src/layout-marketplace/pane.tsx +20 -0
- package/src/layout-marketplace/payload.test.ts +246 -0
- package/src/layout-marketplace/payload.ts +624 -0
- package/src/layout-marketplace/use-marketplace.ts +63 -0
- package/src/market-data/coordinator/index.test.ts +2 -2
- package/src/market-data/coordinator/index.ts +1 -1
- package/src/market-data/hooks.tsx +0 -41
- package/src/market-data/market/format.test.ts +7 -0
- package/src/market-data/market/freshness.ts +5 -2
- package/src/market-data/quotes/freshness.test.ts +16 -0
- package/src/news/aggregator.test.ts +92 -3
- package/src/news/aggregator.ts +98 -18
- package/src/news/hooks.test.tsx +2 -0
- package/src/news/hooks.ts +27 -1
- package/src/news/news-model.ts +5 -2
- package/src/news/types.ts +7 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
- package/src/plugins/broker-sync/normalize.ts +42 -5
- package/src/plugins/builtin/account-management/pane.tsx +5 -5
- package/src/plugins/builtin/ai/index.tsx +18 -2
- package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
- package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
- package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
- package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
- package/src/plugins/builtin/alerts/index.test.tsx +49 -6
- package/src/plugins/builtin/alerts/index.tsx +11 -6
- package/src/plugins/builtin/alerts/pane.tsx +51 -9
- package/src/plugins/builtin/alerts/quotes.ts +11 -3
- package/src/plugins/builtin/alerts/types.ts +1 -0
- package/src/plugins/builtin/analytics/index.tsx +11 -1
- package/src/plugins/builtin/analytics/pane-model.ts +6 -4
- package/src/plugins/builtin/broker-manager/index.tsx +3 -0
- package/src/plugins/builtin/buildout/model/index.ts +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
- package/src/plugins/builtin/cds/client.test.ts +83 -0
- package/src/plugins/builtin/cds/client.ts +67 -0
- package/src/plugins/builtin/cds/index.tsx +41 -0
- package/src/plugins/builtin/cds/model.test.ts +232 -0
- package/src/plugins/builtin/cds/model.ts +401 -0
- package/src/plugins/builtin/cds/pane.test.tsx +132 -0
- package/src/plugins/builtin/cds/pane.tsx +358 -0
- package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
- package/src/plugins/builtin/chart-composer/index.tsx +88 -2
- package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
- package/src/plugins/builtin/chat/content/index.tsx +4 -1
- package/src/plugins/builtin/chat/controller/channels.ts +7 -1
- package/src/plugins/builtin/chat/controller/index.ts +14 -9
- package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
- package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
- package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
- package/src/plugins/builtin/chat/controller/send.ts +1 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
- package/src/plugins/builtin/chat/controller/state.ts +1 -3
- package/src/plugins/builtin/chat/controller/storage.ts +0 -2
- package/src/plugins/builtin/chat/controller/utils.ts +4 -6
- package/src/plugins/builtin/chat/controller/view.ts +2 -2
- package/src/plugins/builtin/chat/controller.test.ts +142 -14
- package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
- package/src/plugins/builtin/chat/profile-popover.ts +53 -22
- package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
- package/src/plugins/builtin/chat/sidebar.tsx +63 -0
- package/src/plugins/builtin/chat/test-harness.tsx +29 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
- package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
- package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
- package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
- package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
- package/src/plugins/builtin/cloud/browser.tsx +9 -0
- package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
- package/src/plugins/builtin/cloud/device-signin.ts +2 -8
- package/src/plugins/builtin/cloud/index.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +14 -24
- package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
- package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
- package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
- package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
- package/src/plugins/builtin/composite-plugins.ts +3 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
- package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
- package/src/plugins/builtin/congress-trades/model.ts +69 -1
- package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
- package/src/plugins/builtin/connections/index.ts +3 -0
- package/src/plugins/builtin/dividend-yield/client.ts +18 -0
- package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
- package/src/plugins/builtin/earnings/index.tsx +5 -3
- package/src/plugins/builtin/earnings/model.test.ts +19 -1
- package/src/plugins/builtin/earnings/model.ts +17 -15
- package/src/plugins/builtin/econ/calendar-model.ts +0 -5
- package/src/plugins/builtin/econ/index.test.tsx +6 -1
- package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
- package/src/plugins/builtin/insider/index.tsx +27 -11
- package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
- package/src/plugins/builtin/layout-manager/index.tsx +17 -4
- package/src/plugins/builtin/news/index.tsx +11 -6
- package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
- package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
- package/src/plugins/builtin/news/wire/index.tsx +3 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
- package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
- package/src/plugins/builtin/notes/files.ts +80 -0
- package/src/plugins/builtin/notes/index.tsx +3 -0
- package/src/plugins/builtin/notes/sync.test.ts +49 -0
- package/src/plugins/builtin/notes/sync.ts +98 -0
- package/src/plugins/builtin/options/index.tsx +1 -0
- package/src/plugins/builtin/options/view.test.tsx +25 -3
- package/src/plugins/builtin/options/view.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
- package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
- package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
- package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
- package/src/plugins/builtin/research/index.test.ts +14 -0
- package/src/plugins/builtin/research/index.tsx +29 -0
- package/src/plugins/builtin/sec/index.tsx +24 -9
- package/src/plugins/builtin/sectors/index.tsx +14 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
- package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
- package/src/plugins/builtin/shared/public-pane.ts +9 -0
- package/src/plugins/builtin/shared/public-share.ts +23 -0
- package/src/plugins/builtin/shared/ticker-source.ts +32 -0
- package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
- package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
- package/src/plugins/builtin/substack/api/cache.ts +7 -3
- package/src/plugins/builtin/substack/api/loaders.ts +36 -12
- package/src/plugins/builtin/substack/api/types.ts +2 -0
- package/src/plugins/builtin/substack/pane-state.ts +1 -1
- package/src/plugins/builtin/substack/pane.tsx +55 -12
- package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +40 -18
- package/src/plugins/builtin/thirteenf/data.ts +15 -7
- package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
- package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
- package/src/plugins/builtin/world-indices/index.tsx +2 -0
- package/src/plugins/catalog-browser.test.ts +78 -0
- package/src/plugins/catalog-browser.ts +136 -0
- package/src/plugins/gridlock-notification.ts +2 -1
- package/src/plugins/ibkr/flex/index.test.ts +19 -0
- package/src/plugins/ibkr/flex/index.ts +1 -1
- package/src/plugins/pane-manager/gridlock.ts +87 -3
- package/src/plugins/pane-manager.test.ts +30 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +2 -2
- package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
- package/src/plugins/prediction-markets/controller/data.ts +7 -1
- package/src/plugins/prediction-markets/controller/index.ts +9 -2
- package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
- package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
- package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
- package/src/plugins/prediction-markets/detail.test.tsx +29 -2
- package/src/plugins/prediction-markets/index.tsx +8 -4
- package/src/plugins/prediction-markets/navigation.ts +0 -1
- package/src/plugins/prediction-markets/pane.test.tsx +77 -0
- package/src/plugins/prediction-markets/pane.tsx +12 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
- package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
- package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
- package/src/plugins/registry/index.ts +2 -0
- package/src/plugins/ticker-navigation.test.ts +24 -0
- package/src/plugins/ticker-navigation.ts +40 -0
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/app-services.ts +61 -0
- package/src/renderers/browser/cloud-transport.test.ts +68 -0
- package/src/renderers/browser/cloud-transport.ts +48 -0
- package/src/renderers/browser/config-host.ts +58 -0
- package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
- package/src/renderers/browser/deeplink-bridge.ts +22 -0
- package/src/renderers/browser/error-boundary.tsx +23 -0
- package/src/renderers/browser/main.tsx +64 -0
- package/src/renderers/browser/persistence.ts +74 -0
- package/src/renderers/browser/storage.test.ts +66 -0
- package/src/renderers/browser/storage.ts +50 -0
- package/src/renderers/browser/ticker-repository.ts +59 -0
- package/src/renderers/browser/ui-host.tsx +40 -0
- package/src/renderers/cloudflare/worker.test.ts +136 -0
- package/src/renderers/cloudflare/worker.ts +116 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +14 -3
- package/src/renderers/electrobun/view/build-assets.ts +12 -10
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
- package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
- package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
- package/src/renderers/electrobun/view/host/box.tsx +11 -6
- package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
- package/src/renderers/electrobun/view/host/input.tsx +11 -7
- package/src/renderers/electrobun/view/host/native.ts +0 -5
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
- package/src/renderers/electrobun/view/styles.css +107 -7
- package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
- package/src/renderers/electrobun/view/toast-host.tsx +69 -43
- package/src/renderers/electrobun/view/ui-host.tsx +6 -137
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/renderers/share/layout-view.test.tsx +58 -0
- package/src/renderers/share/layout-view.tsx +133 -0
- package/src/renderers/share/main.tsx +102 -0
- package/src/renderers/share/styles.css +54 -0
- package/src/renderers/share/view.test.tsx +100 -0
- package/src/renderers/share/view.tsx +150 -0
- package/src/shares/api.test.ts +105 -0
- package/src/shares/api.ts +127 -0
- package/src/shares/chart-snapshot.test.ts +34 -0
- package/src/shares/chart-snapshot.ts +28 -0
- package/src/shares/location.test.ts +19 -0
- package/src/shares/location.ts +11 -0
- package/src/shares/pane.test.ts +110 -0
- package/src/shares/pane.ts +54 -0
- package/src/shares/payload.ts +177 -0
- package/src/sources/gloomberb-cloud/index.test.ts +61 -1
- package/src/sources/gloomberb-cloud/index.ts +86 -14
- package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
- package/src/sources/provider-router/history.test.ts +2 -2
- package/src/sources/provider-router/news.test.ts +34 -0
- package/src/sources/provider-router/news.ts +15 -1
- package/src/sources/sec-edgar.test.ts +59 -0
- package/src/sources/sec-edgar.ts +63 -23
- package/src/sources/yahoo-finance/mappers.ts +3 -0
- package/src/sources/yahoo-finance/snapshots.ts +10 -2
- package/src/sources/yahoo-finance/symbols.test.ts +10 -0
- package/src/sources/yahoo-finance/symbols.ts +13 -8
- package/src/sources/yahoo-finance.test.ts +1 -1
- package/src/sources/yahoo-finance.ts +6 -2
- package/src/state/app/bootstrap.ts +6 -7
- package/src/state/app/context/index.test.ts +41 -1
- package/src/state/app/context/index.test.tsx +34 -16
- package/src/state/selectors-ui.ts +0 -8
- package/src/sync/baseline.ts +32 -0
- package/src/sync/controller.test.ts +124 -1
- package/src/sync/controller.ts +54 -12
- package/src/sync/core-contributors.test.ts +44 -0
- package/src/sync/core-contributors.ts +59 -10
- package/src/sync/react.ts +22 -2
- package/src/sync/types.ts +12 -0
- package/src/theme/font-scale.ts +0 -11
- package/src/tickers/search/index.test.ts +27 -0
- package/src/tickers/search/upsert.ts +10 -0
- package/src/types/capability-route-source.ts +2 -1
- package/src/types/config.ts +35 -5
- package/src/types/desktop-menu.ts +1 -0
- package/src/types/plugin.ts +36 -0
- package/src/ui/context-menu.tsx +1 -1
- package/src/ui/host.tsx +4 -0
- package/src/ui/toast.tsx +2 -0
- package/src/utils/back-navigation.ts +0 -4
- package/src/utils/price-history.ts +11 -10
- package/src/utils/sec.test.ts +6 -0
- package/src/utils/sec.ts +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createInitialState } from "../../state/app/context";
|
|
3
|
+
import { createTestDataProvider } from "../../test-support/data-provider";
|
|
4
|
+
import { createDefaultConfig, type PaneInstanceConfig } from "../../types/config";
|
|
5
|
+
import type { PaneDef } from "../../types/plugin";
|
|
6
|
+
import { bindAppPanePluginRegistry } from "./plugin-bindings";
|
|
7
|
+
|
|
8
|
+
const paneDef: PaneDef = {
|
|
9
|
+
id: "prediction-markets",
|
|
10
|
+
name: "Prediction Markets",
|
|
11
|
+
component: () => null,
|
|
12
|
+
defaultPosition: "left",
|
|
13
|
+
defaultMode: "floating",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function bindPortablePaneRuntime(disabledPlugins: string[] = []) {
|
|
17
|
+
const config = createDefaultConfig("/tmp/gloomberb-portable-pane-test");
|
|
18
|
+
config.disabledPlugins = disabledPlugins;
|
|
19
|
+
const state = createInitialState(config);
|
|
20
|
+
const actions: any[] = [];
|
|
21
|
+
const built: PaneInstanceConfig[] = [];
|
|
22
|
+
const placed: Array<{ instance: PaneInstanceConfig; options: unknown }> = [];
|
|
23
|
+
const pluginRegistry = {
|
|
24
|
+
panes: new Map([[paneDef.id, paneDef]]),
|
|
25
|
+
getPanePluginId: () => "prediction-markets",
|
|
26
|
+
getTermSizeFn: () => ({ width: 120, height: 40 }),
|
|
27
|
+
} as any;
|
|
28
|
+
|
|
29
|
+
bindAppPanePluginRegistry({
|
|
30
|
+
activatePane() {},
|
|
31
|
+
buildPaneInstance: (paneId, options) => {
|
|
32
|
+
const instance = { paneId, ...options } as PaneInstanceConfig;
|
|
33
|
+
built.push(instance);
|
|
34
|
+
return instance;
|
|
35
|
+
},
|
|
36
|
+
createPaneFromTemplate: async () => {},
|
|
37
|
+
dataProvider: createTestDataProvider(),
|
|
38
|
+
detachedPaneId: null,
|
|
39
|
+
dispatch: (action) => actions.push(action),
|
|
40
|
+
focusVisiblePane() {},
|
|
41
|
+
isDetachedWindow: false,
|
|
42
|
+
openPaneSettings: async () => {},
|
|
43
|
+
openPinnedTicker: async () => {},
|
|
44
|
+
persistLayout() {},
|
|
45
|
+
placePaneInstance: (instance, _def, options) => placed.push({ instance, options }),
|
|
46
|
+
placePinnedTickerTarget() {},
|
|
47
|
+
pluginRegistry,
|
|
48
|
+
publishTickerOpenTarget() {},
|
|
49
|
+
resolveOpenTickerTarget: async () => null,
|
|
50
|
+
resolvePaneTarget: () => null,
|
|
51
|
+
selectTickerInPane() {},
|
|
52
|
+
showPane() {},
|
|
53
|
+
state,
|
|
54
|
+
stateRef: { current: state },
|
|
55
|
+
switchTickerResearchTab() {},
|
|
56
|
+
tickerRepository: {} as any,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return { actions, built, placed, pluginRegistry };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const portablePane = {
|
|
63
|
+
schemaVersion: 2 as const,
|
|
64
|
+
sourceConfigVersion: 13,
|
|
65
|
+
layout: {
|
|
66
|
+
dockRoot: null,
|
|
67
|
+
instances: [{
|
|
68
|
+
instanceId: "p1",
|
|
69
|
+
paneId: "prediction-markets",
|
|
70
|
+
title: "Fed Markets",
|
|
71
|
+
binding: { kind: "none" as const },
|
|
72
|
+
params: { query: "fed" },
|
|
73
|
+
settings: { dense: true },
|
|
74
|
+
}],
|
|
75
|
+
floating: [{ instanceId: "p1", x: 0, y: 0, width: 100, height: 30 }],
|
|
76
|
+
detached: [],
|
|
77
|
+
},
|
|
78
|
+
paneState: {
|
|
79
|
+
p1: { pluginState: { "prediction-markets": { searchQuery: "fed" } } },
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
describe("portable pane runtime", () => {
|
|
84
|
+
test("creates a fresh floating pane and restores its complete state", async () => {
|
|
85
|
+
const runtime = bindPortablePaneRuntime();
|
|
86
|
+
|
|
87
|
+
await runtime.pluginRegistry.openPortablePaneShareAsyncFn(portablePane);
|
|
88
|
+
|
|
89
|
+
expect(runtime.built).toHaveLength(1);
|
|
90
|
+
expect(runtime.built[0]).toMatchObject({
|
|
91
|
+
paneId: "prediction-markets",
|
|
92
|
+
title: "Fed Markets",
|
|
93
|
+
binding: { kind: "none" },
|
|
94
|
+
params: { query: "fed" },
|
|
95
|
+
settings: { dense: true },
|
|
96
|
+
});
|
|
97
|
+
expect(runtime.built[0]!.instanceId).toMatch(/^prediction-markets:shared-/);
|
|
98
|
+
expect(runtime.placed).toEqual([{
|
|
99
|
+
instance: runtime.built[0],
|
|
100
|
+
options: { placement: "floating" },
|
|
101
|
+
}]);
|
|
102
|
+
expect(runtime.actions).toContainEqual({
|
|
103
|
+
type: "REPLACE_PANE_STATE",
|
|
104
|
+
paneId: runtime.built[0]!.instanceId,
|
|
105
|
+
state: portablePane.paneState.p1,
|
|
106
|
+
});
|
|
107
|
+
expect(runtime.actions.some((action) => action.type === "INSTALL_LAYOUT_COPY")).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("rejects panes owned by a disabled plugin", async () => {
|
|
111
|
+
const runtime = bindPortablePaneRuntime(["prediction-markets"]);
|
|
112
|
+
await expect(runtime.pluginRegistry.openPortablePaneShareAsyncFn(portablePane)).rejects.toThrow(
|
|
113
|
+
"unavailable",
|
|
114
|
+
);
|
|
115
|
+
expect(runtime.built).toHaveLength(0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "../../components/command-bar/workflow/ops";
|
|
6
6
|
import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
|
|
7
7
|
import { setLayoutManagerDispatch } from "../../plugins/builtin/layout-manager";
|
|
8
|
+
import { materializeMarketplaceLayout } from "../../layout-marketplace/payload";
|
|
8
9
|
import {
|
|
9
10
|
isPaneInLayout,
|
|
10
11
|
removePane,
|
|
@@ -112,7 +113,7 @@ export function bindAppPanePluginRegistry({
|
|
|
112
113
|
launch: { kind: "plugin-command", commandId },
|
|
113
114
|
});
|
|
114
115
|
};
|
|
115
|
-
pluginRegistry.getLayoutFn = () =>
|
|
116
|
+
pluginRegistry.getLayoutFn = () => stateRef.current.config.layout;
|
|
116
117
|
pluginRegistry.updateLayoutFn = (layout) => {
|
|
117
118
|
if (isDetachedWindow) return;
|
|
118
119
|
persistLayout(layout);
|
|
@@ -134,6 +135,25 @@ export function bindAppPanePluginRegistry({
|
|
|
134
135
|
placePaneInstance,
|
|
135
136
|
});
|
|
136
137
|
};
|
|
138
|
+
pluginRegistry.openPortablePaneShareAsyncFn = async (payload) => {
|
|
139
|
+
if (isDetachedWindow) throw new Error("Open shared panes in the main window.");
|
|
140
|
+
const materialized = materializeMarketplaceLayout(payload);
|
|
141
|
+
const sharedPane = materialized.layout.instances[0];
|
|
142
|
+
if (!sharedPane) throw new Error("This shared pane is invalid.");
|
|
143
|
+
const paneDef = pluginRegistry.panes.get(sharedPane.paneId);
|
|
144
|
+
const ownerId = pluginRegistry.getPanePluginId(sharedPane.paneId);
|
|
145
|
+
if (!paneDef || (ownerId && stateRef.current.config.disabledPlugins.includes(ownerId))) {
|
|
146
|
+
throw new Error("This shared pane is unavailable in this version of Gloomberb.");
|
|
147
|
+
}
|
|
148
|
+
const instance = buildPaneInstance(sharedPane.paneId, sharedPane);
|
|
149
|
+
if (!instance) throw new Error("This shared pane could not be created.");
|
|
150
|
+
dispatch({
|
|
151
|
+
type: "REPLACE_PANE_STATE",
|
|
152
|
+
paneId: instance.instanceId,
|
|
153
|
+
state: materialized.paneState[instance.instanceId] ?? {},
|
|
154
|
+
});
|
|
155
|
+
placePaneInstance(instance, paneDef, { placement: "floating" });
|
|
156
|
+
};
|
|
137
157
|
pluginRegistry.createPaneFromTemplateFn = (templateId, options) => {
|
|
138
158
|
if (isDetachedWindow) return;
|
|
139
159
|
void createPaneFromTemplate(templateId, options);
|
|
@@ -151,8 +171,9 @@ export function bindAppPanePluginRegistry({
|
|
|
151
171
|
pluginRegistry.hidePaneFn = (paneId) => {
|
|
152
172
|
if (isDetachedWindow) return;
|
|
153
173
|
const instanceId = resolvePaneTarget(paneId);
|
|
154
|
-
|
|
155
|
-
|
|
174
|
+
const layout = stateRef.current.config.layout;
|
|
175
|
+
if (!instanceId || !isPaneInLayout(layout, instanceId)) return;
|
|
176
|
+
persistLayout(removePane(layout, instanceId));
|
|
156
177
|
};
|
|
157
178
|
pluginRegistry.focusPaneFn = (paneId) => {
|
|
158
179
|
if (isDetachedWindow) {
|
|
@@ -162,7 +183,7 @@ export function bindAppPanePluginRegistry({
|
|
|
162
183
|
return;
|
|
163
184
|
}
|
|
164
185
|
const instanceId = resolvePaneTarget(paneId);
|
|
165
|
-
if (!instanceId || !isPaneInLayout(
|
|
186
|
+
if (!instanceId || !isPaneInLayout(stateRef.current.config.layout, instanceId)) {
|
|
166
187
|
showPane(paneId);
|
|
167
188
|
return;
|
|
168
189
|
}
|
|
@@ -3,9 +3,12 @@ import {
|
|
|
3
3
|
addPaneFloating,
|
|
4
4
|
addPaneToLayout,
|
|
5
5
|
findDockLeaf,
|
|
6
|
-
isPaneInLayout,
|
|
7
6
|
} from "../../plugins/pane-manager";
|
|
8
7
|
import type { PluginRegistry } from "../../plugins/registry";
|
|
8
|
+
import {
|
|
9
|
+
findTickerResearchFollower,
|
|
10
|
+
listVisibleTickerSourcePanes,
|
|
11
|
+
} from "../../plugins/ticker-navigation";
|
|
9
12
|
import type {
|
|
10
13
|
AppAction,
|
|
11
14
|
AppState,
|
|
@@ -45,6 +48,7 @@ export function useAppTickerInspectorRuntime({
|
|
|
45
48
|
pluginRegistry,
|
|
46
49
|
state,
|
|
47
50
|
}: UseAppTickerInspectorRuntimeOptions) {
|
|
51
|
+
// Only for APIs that must land in a collection (selecting a symbol inside a list).
|
|
48
52
|
const resolveCollectionSourcePaneId = useCallback((preferredPaneId?: string | null) => {
|
|
49
53
|
return resolveFollowBindingInstance(state.config.layout, preferredPaneId, isCollectionPaneInstance)?.instanceId
|
|
50
54
|
?? resolveFollowBindingInstance(state.config.layout, state.focusedPaneId, isCollectionPaneInstance)?.instanceId
|
|
@@ -52,6 +56,18 @@ export function useAppTickerInspectorRuntime({
|
|
|
52
56
|
?? null;
|
|
53
57
|
}, [state.config.layout, state.focusedPaneId]);
|
|
54
58
|
|
|
59
|
+
/** Any visible pane that publishes a cursor symbol can drive a follow binding. */
|
|
60
|
+
const resolveTickerSourcePaneId = useCallback((preferredPaneId?: string | null) => {
|
|
61
|
+
const isTickerSource = (instance: PaneInstanceConfig) => (
|
|
62
|
+
pluginRegistry.panes.get(instance.paneId)?.tickerSource === true
|
|
63
|
+
);
|
|
64
|
+
return resolveFollowBindingInstance(state.config.layout, preferredPaneId, isTickerSource)?.instanceId
|
|
65
|
+
?? resolveFollowBindingInstance(state.config.layout, state.focusedPaneId, isTickerSource)?.instanceId
|
|
66
|
+
?? findPrimaryPaneInstance(state.config.layout, "portfolio-list")?.instanceId
|
|
67
|
+
?? listVisibleTickerSourcePanes(state.config.layout, pluginRegistry.panes)[0]?.instanceId
|
|
68
|
+
?? null;
|
|
69
|
+
}, [pluginRegistry, state.config.layout, state.focusedPaneId]);
|
|
70
|
+
|
|
55
71
|
const resolveTickerContextSourcePaneId = useCallback((preferredPaneId?: string | null) => {
|
|
56
72
|
return resolveFollowBindingInstance(state.config.layout, preferredPaneId, isTickerContextPaneInstance)?.instanceId
|
|
57
73
|
?? resolveFollowBindingInstance(state.config.layout, state.focusedPaneId, isTickerContextPaneInstance)?.instanceId
|
|
@@ -67,12 +83,7 @@ export function useAppTickerInspectorRuntime({
|
|
|
67
83
|
}, [dispatch, resolveCollectionSourcePaneId]);
|
|
68
84
|
|
|
69
85
|
const resolveInspectorPane = useCallback((sourcePaneId: string): PaneInstanceConfig | null => {
|
|
70
|
-
return state.config.layout
|
|
71
|
-
instance.paneId === TICKER_RESEARCH_PANE_ID
|
|
72
|
-
&& instance.binding?.kind === "follow"
|
|
73
|
-
&& instance.binding.sourceInstanceId === sourcePaneId
|
|
74
|
-
&& isPaneInLayout(state.config.layout, instance.instanceId),
|
|
75
|
-
) ?? null;
|
|
86
|
+
return findTickerResearchFollower(state.config.layout, sourcePaneId);
|
|
76
87
|
}, [state.config.layout]);
|
|
77
88
|
|
|
78
89
|
const ensureInspectorPane = useCallback((sourcePaneId: string) => {
|
|
@@ -104,7 +115,7 @@ export function useAppTickerInspectorRuntime({
|
|
|
104
115
|
if (target?.paneId === TICKER_RESEARCH_PANE_ID) return target.instanceId;
|
|
105
116
|
const focused = state.focusedPaneId ? resolvePaneInstance(state.config.layout, state.focusedPaneId) : null;
|
|
106
117
|
if (focused?.paneId === TICKER_RESEARCH_PANE_ID) return focused.instanceId;
|
|
107
|
-
const sourcePaneId =
|
|
118
|
+
const sourcePaneId = resolveTickerSourcePaneId(preferredPaneId);
|
|
108
119
|
if (!sourcePaneId) return null;
|
|
109
120
|
const ensured = ensureInspectorPane(sourcePaneId);
|
|
110
121
|
if (!ensured) return null;
|
|
@@ -120,14 +131,14 @@ export function useAppTickerInspectorRuntime({
|
|
|
120
131
|
dispatch,
|
|
121
132
|
ensureInspectorPane,
|
|
122
133
|
persistLayout,
|
|
123
|
-
|
|
134
|
+
resolveTickerSourcePaneId,
|
|
124
135
|
state.config.layout,
|
|
125
136
|
state.focusedPaneId,
|
|
126
137
|
]);
|
|
127
138
|
|
|
128
139
|
const buildPaneBinding = useCallback((paneType: string, preferredPaneId?: string | null): PaneBinding | null => {
|
|
129
140
|
if (normalizePaneId(paneType) === TICKER_RESEARCH_PANE_ID) {
|
|
130
|
-
const sourceInstanceId =
|
|
141
|
+
const sourceInstanceId = resolveTickerSourcePaneId(preferredPaneId);
|
|
131
142
|
return sourceInstanceId ? { kind: "follow", sourceInstanceId } : null;
|
|
132
143
|
}
|
|
133
144
|
if (isTickerPaneId(paneType)) {
|
|
@@ -135,12 +146,12 @@ export function useAppTickerInspectorRuntime({
|
|
|
135
146
|
return sourceInstanceId ? { kind: "follow", sourceInstanceId } : null;
|
|
136
147
|
}
|
|
137
148
|
return { kind: "none" };
|
|
138
|
-
}, [
|
|
149
|
+
}, [resolveTickerContextSourcePaneId, resolveTickerSourcePaneId]);
|
|
139
150
|
|
|
140
151
|
const showTickerResearchPane = useCallback(() => {
|
|
141
|
-
const sourcePaneId =
|
|
152
|
+
const sourcePaneId = resolveTickerSourcePaneId();
|
|
142
153
|
if (!sourcePaneId) {
|
|
143
|
-
notify("Open a
|
|
154
|
+
notify("Open a ticker source pane first to inspect a ticker.");
|
|
144
155
|
return;
|
|
145
156
|
}
|
|
146
157
|
const ensured = ensureInspectorPane(sourcePaneId);
|
|
@@ -154,7 +165,7 @@ export function useAppTickerInspectorRuntime({
|
|
|
154
165
|
ensureInspectorPane,
|
|
155
166
|
notify,
|
|
156
167
|
persistLayout,
|
|
157
|
-
|
|
168
|
+
resolveTickerSourcePaneId,
|
|
158
169
|
state.config.layout,
|
|
159
170
|
]);
|
|
160
171
|
|
|
@@ -101,6 +101,22 @@ describe("desktop deeplinks", () => {
|
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
test("routes only opaque 32-character social share ids", () => {
|
|
105
|
+
const id = "0123456789abcdef0123456789abcdef";
|
|
106
|
+
expect(resolveDesktopDeepLinkAction(`gloomberb://share/${id}`)).toEqual({
|
|
107
|
+
type: "open-share",
|
|
108
|
+
id,
|
|
109
|
+
message: "Opened shared pane.",
|
|
110
|
+
});
|
|
111
|
+
expect(resolveDesktopDeepLinkAction(`gloomberb://layout/${id}`)).toEqual({
|
|
112
|
+
type: "open-layout",
|
|
113
|
+
id,
|
|
114
|
+
message: "Added shared layout.",
|
|
115
|
+
});
|
|
116
|
+
expect(resolveDesktopDeepLinkAction("gloomberb://share/not-valid").type).toBe("unsupported");
|
|
117
|
+
expect(resolveDesktopDeepLinkAction("gloomberb://layout/not-valid").type).toBe("unsupported");
|
|
118
|
+
});
|
|
119
|
+
|
|
104
120
|
test("rejects command-bar, non-gloomberb, and malformed links", () => {
|
|
105
121
|
expect(resolveDesktopDeepLinkAction("gloomberb://command?query=profile").type).toBe("unsupported");
|
|
106
122
|
expect(resolveDesktopDeepLinkAction("gloomberb://search/NVDA").type).toBe("unsupported");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, type Dispatch } from "react";
|
|
2
|
+
import { apiClient } from "../../api-client";
|
|
2
3
|
import { getDockedPaneIds } from "../../plugins/pane-manager";
|
|
3
4
|
import type { PluginRegistry } from "../../plugins/registry";
|
|
4
5
|
import type { AppAction, AppState } from "../../state/app/context";
|
|
@@ -11,6 +12,9 @@ import type { DesktopDeepLinkBridge } from "../../types/desktop-deeplink";
|
|
|
11
12
|
import type { DesktopWindowBridge } from "../../types/desktop-window";
|
|
12
13
|
import { requestAccountManagementTab } from "../../plugins/builtin/account-management/navigation";
|
|
13
14
|
import { chatController } from "../../plugins/builtin/chat/controller";
|
|
15
|
+
import { getShare } from "../../shares/api";
|
|
16
|
+
import { openPaneShare } from "../../shares/pane";
|
|
17
|
+
import { materializeMarketplaceLayout } from "../../layout-marketplace/payload";
|
|
14
18
|
|
|
15
19
|
type CloudDeepLinkRoute = {
|
|
16
20
|
kind: "cloud-alerts" | "cloud-emails" | "cloud-roundup" | "cloud-success";
|
|
@@ -20,6 +24,7 @@ type CloudDeepLinkRoute = {
|
|
|
20
24
|
type CollectionDeepLinkKind = "collection" | "portfolio" | "watchlist";
|
|
21
25
|
type AlertDeepLinkCondition = "above" | "below" | "crosses";
|
|
22
26
|
type NewsDeepLinkKind = "breaking" | "feed" | "ticker" | "top";
|
|
27
|
+
const SHARE_ID = /^[a-f0-9]{32}$/;
|
|
23
28
|
|
|
24
29
|
export type DesktopDeepLinkAction =
|
|
25
30
|
| { type: "open-account-management"; route: CloudDeepLinkRoute; message: string }
|
|
@@ -33,6 +38,8 @@ export type DesktopDeepLinkAction =
|
|
|
33
38
|
| { type: "open-chat-channel"; channelId: string; messageId: string | null; message: string }
|
|
34
39
|
| { type: "open-chat-dm"; participants: string; message: string }
|
|
35
40
|
| { type: "open-news"; kind: NewsDeepLinkKind; symbol: string | null; message: string }
|
|
41
|
+
| { type: "open-share"; id: string; message: string }
|
|
42
|
+
| { type: "open-layout"; id: string; message: string }
|
|
36
43
|
| { type: "unsupported"; message: string };
|
|
37
44
|
|
|
38
45
|
interface ParsedGloomUrl {
|
|
@@ -241,6 +248,20 @@ function parseNewsDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
|
|
|
241
248
|
return { type: "unsupported", message: "Unsupported Gloomberb news link." };
|
|
242
249
|
}
|
|
243
250
|
|
|
251
|
+
function parseLayoutDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
|
|
252
|
+
const id = parsed.segments[0] ?? param(parsed.url, "id", "layout");
|
|
253
|
+
return id && SHARE_ID.test(id)
|
|
254
|
+
? { type: "open-layout", id, message: "Added shared layout." }
|
|
255
|
+
: { type: "unsupported", message: "Layout links need a valid id." };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function parseShareDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
|
|
259
|
+
const id = parsed.segments[0] ?? param(parsed.url, "id", "share");
|
|
260
|
+
return id && SHARE_ID.test(id)
|
|
261
|
+
? { type: "open-share", id, message: "Opened shared pane." }
|
|
262
|
+
: { type: "unsupported", message: "Share links need a valid id." };
|
|
263
|
+
}
|
|
264
|
+
|
|
244
265
|
export function resolveDesktopDeepLinkAction(rawUrl: string): DesktopDeepLinkAction {
|
|
245
266
|
const parsed = parseGloomUrl(rawUrl);
|
|
246
267
|
if (!parsed) return { type: "unsupported", message: "Unsupported Gloomberb link." };
|
|
@@ -262,6 +283,10 @@ export function resolveDesktopDeepLinkAction(rawUrl: string): DesktopDeepLinkAct
|
|
|
262
283
|
return parseChatDeepLink(parsed);
|
|
263
284
|
case "news":
|
|
264
285
|
return parseNewsDeepLink(parsed);
|
|
286
|
+
case "share":
|
|
287
|
+
return parseShareDeepLink(parsed);
|
|
288
|
+
case "layout":
|
|
289
|
+
return parseLayoutDeepLink(parsed);
|
|
265
290
|
default:
|
|
266
291
|
return { type: "unsupported", message: "Unsupported Gloomberb link." };
|
|
267
292
|
}
|
|
@@ -462,6 +487,33 @@ function handleOpenNews(
|
|
|
462
487
|
notifySuccess(pluginRegistry, action.message);
|
|
463
488
|
}
|
|
464
489
|
|
|
490
|
+
async function handleOpenLayout(
|
|
491
|
+
action: Extract<DesktopDeepLinkAction, { type: "open-layout" }>,
|
|
492
|
+
options: DesktopDeepLinkHandlerOptions,
|
|
493
|
+
): Promise<void> {
|
|
494
|
+
const entry = await apiClient.getMarketplaceLayout(action.id);
|
|
495
|
+
if (!entry) throw new Error("This shared layout is unavailable.");
|
|
496
|
+
const installed = materializeMarketplaceLayout(entry);
|
|
497
|
+
options.dispatch({
|
|
498
|
+
type: "INSTALL_LAYOUT_COPY",
|
|
499
|
+
name: entry.name,
|
|
500
|
+
layout: installed.layout,
|
|
501
|
+
paneState: installed.paneState,
|
|
502
|
+
});
|
|
503
|
+
notifySuccess(options.pluginRegistry, `Layout "${entry.name}" added.`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function handleOpenShare(
|
|
507
|
+
action: Extract<DesktopDeepLinkAction, { type: "open-share" }>,
|
|
508
|
+
pluginRegistry: PluginRegistry,
|
|
509
|
+
): Promise<void> {
|
|
510
|
+
const share = await getShare(action.id, fetch, { trackView: false });
|
|
511
|
+
if (!share) throw new Error("This share is unavailable or has expired.");
|
|
512
|
+
if (share.kind !== "pane") throw new Error("This link contains a snapshot, not a live pane.");
|
|
513
|
+
await openPaneShare(pluginRegistry, share.data);
|
|
514
|
+
notifySuccess(pluginRegistry, action.message);
|
|
515
|
+
}
|
|
516
|
+
|
|
465
517
|
export function handleDesktopDeepLink(rawUrl: string, options: DesktopDeepLinkHandlerOptions): void {
|
|
466
518
|
const action = resolveDesktopDeepLinkAction(rawUrl);
|
|
467
519
|
if (action.type === "unsupported") {
|
|
@@ -499,6 +551,16 @@ export function handleDesktopDeepLink(rawUrl: string, options: DesktopDeepLinkHa
|
|
|
499
551
|
case "open-news":
|
|
500
552
|
handleOpenNews(action, options.pluginRegistry);
|
|
501
553
|
return;
|
|
554
|
+
case "open-share":
|
|
555
|
+
void handleOpenShare(action, options.pluginRegistry).catch((error) => {
|
|
556
|
+
notifyError(options.pluginRegistry, error instanceof Error ? error.message : "Could not open shared pane.");
|
|
557
|
+
});
|
|
558
|
+
return;
|
|
559
|
+
case "open-layout":
|
|
560
|
+
void handleOpenLayout(action, options).catch((error) => {
|
|
561
|
+
notifyError(options.pluginRegistry, error instanceof Error ? error.message : "Could not open shared layout.");
|
|
562
|
+
});
|
|
563
|
+
return;
|
|
502
564
|
}
|
|
503
565
|
}
|
|
504
566
|
|
|
@@ -506,19 +568,21 @@ export function useDesktopDeepLinkRuntime({
|
|
|
506
568
|
desktopDeepLinkBridge,
|
|
507
569
|
desktopWindowKind,
|
|
508
570
|
dispatch,
|
|
571
|
+
initialized,
|
|
509
572
|
pluginRegistry,
|
|
510
573
|
stateRef,
|
|
511
574
|
}: {
|
|
512
575
|
desktopDeepLinkBridge?: DesktopDeepLinkBridge;
|
|
513
576
|
desktopWindowKind?: DesktopWindowBridge["kind"];
|
|
514
577
|
dispatch: Dispatch<AppAction>;
|
|
578
|
+
initialized: boolean;
|
|
515
579
|
pluginRegistry: PluginRegistry;
|
|
516
580
|
stateRef: { current: AppState };
|
|
517
581
|
}) {
|
|
518
582
|
useEffect(() => {
|
|
519
|
-
if (desktopWindowKind
|
|
583
|
+
if (!initialized || desktopWindowKind === "detached" || !desktopDeepLinkBridge) return;
|
|
520
584
|
return desktopDeepLinkBridge.subscribe((deeplink) => {
|
|
521
585
|
handleDesktopDeepLink(deeplink.url, { dispatch, pluginRegistry, stateRef });
|
|
522
586
|
});
|
|
523
|
-
}, [desktopDeepLinkBridge, desktopWindowKind, dispatch, pluginRegistry, stateRef]);
|
|
587
|
+
}, [desktopDeepLinkBridge, desktopWindowKind, dispatch, initialized, pluginRegistry, stateRef]);
|
|
524
588
|
}
|
|
@@ -46,6 +46,9 @@ export function useDesktopApplicationMenuRuntime({
|
|
|
46
46
|
case "toggle-status-bar":
|
|
47
47
|
dispatch({ type: "TOGGLE_STATUS_BAR" });
|
|
48
48
|
break;
|
|
49
|
+
case "open-layout-gallery":
|
|
50
|
+
pluginRegistry.showPane("layout-marketplace");
|
|
51
|
+
break;
|
|
49
52
|
case "layout-undo":
|
|
50
53
|
dispatch({ type: "UNDO_LAYOUT" });
|
|
51
54
|
break;
|
|
@@ -23,7 +23,7 @@ export function bindPluginRegistryRuntimeAccess({
|
|
|
23
23
|
importBrokerPositions,
|
|
24
24
|
marketData,
|
|
25
25
|
pluginRegistry,
|
|
26
|
-
|
|
26
|
+
stateRef,
|
|
27
27
|
tickerRepository,
|
|
28
28
|
}: {
|
|
29
29
|
dataProvider: DataProvider;
|
|
@@ -31,31 +31,32 @@ export function bindPluginRegistryRuntimeAccess({
|
|
|
31
31
|
importBrokerPositions: (instanceId: string) => Promise<unknown>;
|
|
32
32
|
marketData: MarketDataCoordinator;
|
|
33
33
|
pluginRegistry: PluginRegistry;
|
|
34
|
-
|
|
34
|
+
stateRef: { current: AppState };
|
|
35
35
|
tickerRepository: AppTickerRepositoryPort;
|
|
36
36
|
}) {
|
|
37
|
-
pluginRegistry.getTickerFn = (symbol) =>
|
|
37
|
+
pluginRegistry.getTickerFn = (symbol) => stateRef.current.tickers.get(symbol) ?? null;
|
|
38
38
|
pluginRegistry.getDataFn = (symbol) => {
|
|
39
|
-
const ticker =
|
|
39
|
+
const ticker = stateRef.current.tickers.get(symbol) ?? null;
|
|
40
40
|
const instrument = instrumentFromTicker(ticker, symbol);
|
|
41
41
|
return instrument ? marketData.getTickerFinancialsSync(instrument) : null;
|
|
42
42
|
};
|
|
43
|
-
pluginRegistry.getConfigFn = () =>
|
|
44
|
-
pluginRegistry.getPaneRuntimeStateFn = (paneId) =>
|
|
43
|
+
pluginRegistry.getConfigFn = () => stateRef.current.config;
|
|
44
|
+
pluginRegistry.getPaneRuntimeStateFn = (paneId) => stateRef.current.paneState[paneId] ?? null;
|
|
45
45
|
pluginRegistry.updatePaneRuntimeStateFn = (paneId, patch) => {
|
|
46
46
|
dispatch({ type: "UPDATE_PANE_STATE", paneId, patch });
|
|
47
47
|
};
|
|
48
48
|
pluginRegistry.getPluginConfigValueFn = (pluginId, key) => (
|
|
49
|
-
(
|
|
49
|
+
(stateRef.current.config.pluginConfig[pluginId]?.[key] as any) ?? null
|
|
50
50
|
);
|
|
51
51
|
|
|
52
52
|
const setPluginConfigValues = async (pluginId: string, values: Record<string, unknown>) => {
|
|
53
|
+
const currentConfig = stateRef.current.config;
|
|
53
54
|
const nextConfig = {
|
|
54
|
-
...
|
|
55
|
+
...currentConfig,
|
|
55
56
|
pluginConfig: {
|
|
56
|
-
...
|
|
57
|
+
...currentConfig.pluginConfig,
|
|
57
58
|
[pluginId]: {
|
|
58
|
-
...(
|
|
59
|
+
...(currentConfig.pluginConfig[pluginId] ?? {}),
|
|
59
60
|
...values,
|
|
60
61
|
},
|
|
61
62
|
},
|
|
@@ -70,13 +71,14 @@ export function bindPluginRegistryRuntimeAccess({
|
|
|
70
71
|
};
|
|
71
72
|
pluginRegistry.setPluginConfigValuesFn = setPluginConfigValues;
|
|
72
73
|
pluginRegistry.deletePluginConfigValueFn = async (pluginId, key) => {
|
|
73
|
-
const
|
|
74
|
+
const currentConfig = stateRef.current.config;
|
|
75
|
+
const currentPluginConfig = currentConfig.pluginConfig[pluginId];
|
|
74
76
|
if (!currentPluginConfig || !(key in currentPluginConfig)) return;
|
|
75
77
|
|
|
76
78
|
const nextPluginConfig = { ...currentPluginConfig };
|
|
77
79
|
delete nextPluginConfig[key];
|
|
78
80
|
|
|
79
|
-
const nextAllPluginConfig = { ...
|
|
81
|
+
const nextAllPluginConfig = { ...currentConfig.pluginConfig };
|
|
80
82
|
if (Object.keys(nextPluginConfig).length === 0) {
|
|
81
83
|
delete nextAllPluginConfig[pluginId];
|
|
82
84
|
} else {
|
|
@@ -84,7 +86,7 @@ export function bindPluginRegistryRuntimeAccess({
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
const nextConfig = {
|
|
87
|
-
...
|
|
89
|
+
...currentConfig,
|
|
88
90
|
pluginConfig: nextAllPluginConfig,
|
|
89
91
|
};
|
|
90
92
|
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
@@ -96,14 +98,14 @@ export function bindPluginRegistryRuntimeAccess({
|
|
|
96
98
|
setConfigAccessor?: (accessor: () => AppConfig) => void;
|
|
97
99
|
};
|
|
98
100
|
if (typeof configurableProvider.setConfigAccessor === "function") {
|
|
99
|
-
configurableProvider.setConfigAccessor(() =>
|
|
101
|
+
configurableProvider.setConfigAccessor(() => stateRef.current.config);
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
pluginRegistry.createBrokerInstanceFn = async (brokerType, label, values) => {
|
|
103
105
|
const instanceId = createBrokerInstanceId(
|
|
104
106
|
brokerType,
|
|
105
107
|
label,
|
|
106
|
-
|
|
108
|
+
stateRef.current.config.brokerInstances.map((instance) => instance.id),
|
|
107
109
|
);
|
|
108
110
|
const instance: BrokerInstanceConfig = {
|
|
109
111
|
id: instanceId,
|
|
@@ -114,8 +116,8 @@ export function bindPluginRegistryRuntimeAccess({
|
|
|
114
116
|
enabled: true,
|
|
115
117
|
};
|
|
116
118
|
const nextConfig = {
|
|
117
|
-
...
|
|
118
|
-
brokerInstances: [...
|
|
119
|
+
...stateRef.current.config,
|
|
120
|
+
brokerInstances: [...stateRef.current.config.brokerInstances, instance],
|
|
119
121
|
};
|
|
120
122
|
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
121
123
|
await saveConfigImmediately(nextConfig);
|
|
@@ -124,7 +126,7 @@ export function bindPluginRegistryRuntimeAccess({
|
|
|
124
126
|
};
|
|
125
127
|
|
|
126
128
|
pluginRegistry.connectBrokerInstanceFn = async (instanceId) => {
|
|
127
|
-
const instance = getBrokerInstance(
|
|
129
|
+
const instance = getBrokerInstance(stateRef.current.config.brokerInstances, instanceId);
|
|
128
130
|
if (!instance) throw new Error("Broker profile not found.");
|
|
129
131
|
if (instance.enabled === false) throw new Error(`Broker profile "${instance.label}" is disabled.`);
|
|
130
132
|
|
|
@@ -142,8 +144,8 @@ export function bindPluginRegistryRuntimeAccess({
|
|
|
142
144
|
};
|
|
143
145
|
|
|
144
146
|
pluginRegistry.updateBrokerInstanceFn = async (instanceId, values, options = {}) => {
|
|
145
|
-
const currentInstance =
|
|
146
|
-
const nextInstances =
|
|
147
|
+
const currentInstance = stateRef.current.config.brokerInstances.find((instance) => instance.id === instanceId);
|
|
148
|
+
const nextInstances = stateRef.current.config.brokerInstances.map((instance) =>
|
|
147
149
|
instance.id === instanceId
|
|
148
150
|
? (() => {
|
|
149
151
|
const nextValues = options.replaceConfig ? values : { ...instance.config, ...values };
|
|
@@ -167,7 +169,7 @@ export function bindPluginRegistryRuntimeAccess({
|
|
|
167
169
|
clearPersistedBrokerAccounts(pluginRegistry.persistence.resources, currentInstance);
|
|
168
170
|
}
|
|
169
171
|
const nextConfig = {
|
|
170
|
-
...
|
|
172
|
+
...stateRef.current.config,
|
|
171
173
|
brokerInstances: nextInstances,
|
|
172
174
|
};
|
|
173
175
|
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
@@ -180,7 +182,7 @@ export function bindPluginRegistryRuntimeAccess({
|
|
|
180
182
|
};
|
|
181
183
|
|
|
182
184
|
pluginRegistry.removeBrokerInstanceFn = async (instanceId) => {
|
|
183
|
-
const instance = getBrokerInstance(
|
|
185
|
+
const instance = getBrokerInstance(stateRef.current.config.brokerInstances, instanceId);
|
|
184
186
|
if (!instance) return;
|
|
185
187
|
|
|
186
188
|
clearPersistedBrokerAccounts(pluginRegistry.persistence.resources, instance);
|
|
@@ -189,15 +191,15 @@ export function bindPluginRegistryRuntimeAccess({
|
|
|
189
191
|
await broker?.disconnect?.(instance).catch(() => {});
|
|
190
192
|
|
|
191
193
|
const removedPortfolioIds = new Set(
|
|
192
|
-
|
|
194
|
+
stateRef.current.config.portfolios
|
|
193
195
|
.filter((portfolio) => portfolio.brokerInstanceId === instanceId)
|
|
194
196
|
.map((portfolio) => portfolio.id),
|
|
195
197
|
);
|
|
196
198
|
|
|
197
|
-
const nextPortfolios =
|
|
198
|
-
const nextTickers = new Map(
|
|
199
|
+
const nextPortfolios = stateRef.current.config.portfolios.filter((portfolio) => !removedPortfolioIds.has(portfolio.id));
|
|
200
|
+
const nextTickers = new Map(stateRef.current.tickers);
|
|
199
201
|
|
|
200
|
-
for (const ticker of
|
|
202
|
+
for (const ticker of stateRef.current.tickers.values()) {
|
|
201
203
|
const nextPositions = ticker.metadata.positions.filter((position) => position.brokerInstanceId !== instanceId);
|
|
202
204
|
const nextPortfolioRefs = ticker.metadata.portfolios.filter((portfolioId) => !removedPortfolioIds.has(portfolioId));
|
|
203
205
|
const nextBrokerContracts = (ticker.metadata.broker_contracts ?? []).filter((contract) => contract.brokerInstanceId !== instanceId);
|
|
@@ -233,8 +235,8 @@ export function bindPluginRegistryRuntimeAccess({
|
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
const nextConfig = {
|
|
236
|
-
...
|
|
237
|
-
brokerInstances:
|
|
238
|
+
...stateRef.current.config,
|
|
239
|
+
brokerInstances: stateRef.current.config.brokerInstances.filter((entry) => entry.id !== instanceId),
|
|
238
240
|
portfolios: nextPortfolios,
|
|
239
241
|
};
|
|
240
242
|
|