gloomberb 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -11
- package/package.json +13 -5
- package/src/app.tsx +414 -191
- package/src/architecture/import-boundaries.test.ts +118 -0
- package/src/assets/gloomberb-logo.png +0 -0
- package/src/assets/gloomberb-logo.svg +73 -0
- package/src/brokers/account-cache.ts +157 -0
- package/src/brokers/profile-form.test.ts +73 -0
- package/src/brokers/profile-form.ts +109 -0
- package/src/brokers/remote-broker-adapter.ts +91 -0
- package/src/brokers/sync-broker-instance.test.ts +124 -1
- package/src/brokers/sync-broker-instance.ts +113 -17
- package/src/capabilities/factories.ts +100 -0
- package/src/capabilities/index.ts +4 -0
- package/src/capabilities/registry.test.ts +137 -0
- package/src/capabilities/registry.ts +131 -0
- package/src/capabilities/service-capabilities.ts +27 -0
- package/src/capabilities/types.ts +84 -0
- package/src/cli/commands/ticker.ts +11 -3
- package/src/cli/context.ts +19 -13
- package/src/cli/helpers.ts +1 -0
- package/src/cli/types.ts +2 -2
- package/src/components/chart/chart-controller.ts +4 -24
- package/src/components/chart/chart-data.ts +3 -14
- package/src/components/chart/chart-indicator-selector.tsx +28 -6
- package/src/components/chart/chart-pane-settings.ts +15 -28
- package/src/components/chart/chart-renderer.ts +74 -99
- package/src/components/chart/chart-resolution.ts +10 -10
- package/src/components/chart/chart-scroll.test.ts +6 -5
- package/src/components/chart/chart-scroll.ts +2 -2
- package/src/components/chart/chart-types.ts +0 -1
- package/src/components/chart/chart-viewport.ts +2 -1
- package/src/components/chart/chart.test.ts +37 -0
- package/src/components/chart/comparison-chart-data.ts +3 -17
- package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
- package/src/components/chart/comparison-chart-renderer.ts +1 -1
- package/src/components/chart/comparison-stock-chart.tsx +318 -158
- package/src/components/chart/cursor-motion.ts +3 -2
- package/src/components/chart/indicators/bands.test.ts +0 -14
- package/src/components/chart/indicators/moving-averages.test.ts +0 -15
- package/src/components/chart/indicators/options.ts +3 -3
- package/src/components/chart/indicators/oscillators.test.ts +0 -21
- package/src/components/chart/indicators/types.ts +1 -2
- package/src/components/chart/native/chart-rasterizer.ts +37 -44
- package/src/components/chart/native/kitty-adapter.ts +2 -5
- package/src/components/chart/native/kitty-manager.ts +1 -1
- package/src/components/chart/native/kitty-protocol.ts +0 -1
- package/src/components/chart/native/kitty-support.ts +1 -1
- package/src/components/chart/native/kitty.test.ts +53 -13
- package/src/components/chart/native/renderer-selection.ts +1 -1
- package/src/components/chart/native/surface-manager.ts +1 -1
- package/src/components/chart/native/surface-visibility.ts +48 -0
- package/src/components/chart/static-chart-surface.test.tsx +51 -0
- package/src/components/chart/static-chart-surface.tsx +191 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
- package/src/components/chart/stock-chart.tsx +452 -191
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +269 -20
- package/src/components/command-bar/command-bar.tsx +830 -353
- package/src/components/command-bar/command-registry.ts +10 -20
- package/src/components/command-bar/helpers.test.ts +1 -1
- package/src/components/command-bar/helpers.ts +9 -11
- package/src/components/command-bar/pane-template-display.ts +4 -8
- package/src/components/command-bar/root-shortcuts.ts +12 -11
- package/src/components/command-bar/view-model.test.ts +8 -42
- package/src/components/command-bar/view-model.ts +8 -36
- package/src/components/command-bar/workflow-ops.test.ts +76 -2
- package/src/components/command-bar/workflow-ops.ts +40 -9
- package/src/components/command-bar/workflow-types.ts +2 -1
- package/src/components/data-table-stack-view.test.tsx +206 -0
- package/src/components/data-table-stack-view.tsx +152 -0
- package/src/components/data-table-view.test.tsx +188 -0
- package/src/components/data-table-view.tsx +279 -0
- package/src/components/feed-data-table-stack-view.test.tsx +225 -0
- package/src/components/feed-data-table-stack-view.tsx +365 -0
- package/src/components/index.ts +26 -31
- package/src/components/layout/detached-pane-shell.tsx +185 -0
- package/src/components/layout/floating-pane.tsx +87 -14
- package/src/components/layout/header.tsx +139 -49
- package/src/components/layout/pane-content.tsx +42 -0
- package/src/components/layout/pane-footer.test.tsx +195 -0
- package/src/components/layout/pane-footer.tsx +425 -0
- package/src/components/layout/pane-header.test.tsx +39 -0
- package/src/components/layout/pane-header.tsx +192 -14
- package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
- package/src/components/layout/pane-sizing.ts +9 -10
- package/src/components/layout/pane-title.ts +35 -0
- package/src/components/layout/pane.tsx +52 -11
- package/src/components/layout/shell.test.tsx +315 -107
- package/src/components/layout/shell.tsx +1010 -368
- package/src/components/layout/status-bar.test.tsx +17 -10
- package/src/components/layout/status-bar.tsx +367 -100
- package/src/components/layout/titlebar-overlay.ts +2 -0
- package/src/components/markdown-editor.tsx +29 -14
- package/src/components/markdown-text.tsx +25 -24
- package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
- package/src/components/onboarding/onboarding-wizard.tsx +347 -473
- package/src/components/pane-settings-dialog.tsx +521 -89
- package/src/components/pane-template-wizard.test.tsx +1 -1
- package/src/components/pane-template-wizard.tsx +38 -37
- package/src/components/price-selector-dialog.tsx +22 -21
- package/src/components/ticker-badge-list.tsx +53 -0
- package/src/components/ticker-badge-text.test.tsx +44 -1
- package/src/components/ticker-badge-text.tsx +78 -38
- package/src/components/ticker-badge.tsx +28 -7
- package/src/components/ticker-list-table-view.tsx +248 -0
- package/src/components/ticker-list-table.test.tsx +47 -1
- package/src/components/ticker-list-table.tsx +212 -41
- package/src/components/toggle-list.tsx +114 -8
- package/src/components/ui/button.tsx +24 -16
- package/src/components/ui/choice-dialog.tsx +89 -0
- package/src/components/ui/data-table.tsx +313 -114
- package/src/components/ui/external-link.tsx +52 -13
- package/src/components/ui/fields.tsx +44 -30
- package/src/components/ui/frame.tsx +24 -57
- package/src/components/ui/index.ts +15 -50
- package/src/components/ui/list-view.tsx +65 -25
- package/src/components/ui/loading.tsx +5 -60
- package/src/components/ui/message-composer.test.tsx +81 -0
- package/src/components/ui/message-composer.tsx +139 -0
- package/src/components/ui/multi-select-dialog.tsx +100 -32
- package/src/components/ui/multi-select.ts +112 -0
- package/src/components/ui/native-select.tsx +120 -0
- package/src/components/ui/page-stack-view.tsx +49 -19
- package/src/components/ui/remote-image.tsx +50 -0
- package/src/components/ui/row-value-cache.test.ts +17 -0
- package/src/components/ui/row-value-cache.ts +41 -0
- package/src/components/ui/shortcut-hint.tsx +55 -0
- package/src/components/ui/status.tsx +12 -67
- package/src/components/ui/table-layout.ts +59 -0
- package/src/components/ui/tabs.tsx +249 -27
- package/src/components/ui/toggle.tsx +19 -127
- package/src/components/ui/ui.test.tsx +266 -53
- package/src/components/use-double-click-activation.ts +7 -2
- package/src/components/use-external-link-footer.ts +79 -0
- package/src/core/app-services.ts +95 -0
- package/src/core/state/app-state.ts +886 -0
- package/src/{state → core/state}/session-persistence.ts +7 -6
- package/src/data/config-layout.ts +225 -0
- package/src/data/config-store-node.ts +390 -0
- package/src/data/config-store.test.ts +72 -0
- package/src/data/config-store.ts +35 -478
- package/src/data/sqlite-json.ts +2 -5
- package/src/data/sqlite-retry.ts +1 -0
- package/src/index.tsx +2 -68
- package/src/market-data/coordinator-subscriptions.test.ts +125 -0
- package/src/market-data/coordinator.test.ts +73 -4
- package/src/market-data/coordinator.ts +190 -24
- package/src/market-data/hooks.test.tsx +1 -1
- package/src/market-data/hooks.tsx +109 -87
- package/src/market-data/query-store.ts +3 -3
- package/src/market-data/request-types.test.ts +0 -14
- package/src/market-data/result-types.ts +1 -0
- package/src/news/aggregator.test.ts +267 -0
- package/src/news/aggregator.ts +452 -0
- package/src/news/hooks.ts +39 -0
- package/src/news/ticker-symbols.ts +21 -0
- package/src/news/types.ts +77 -0
- package/src/notifications/app-notifier.ts +21 -10
- package/src/pane-settings.test.ts +0 -2
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
- package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
- package/src/plugins/builtin/ai/providers.ts +1 -9
- package/src/plugins/builtin/ai/runner.ts +41 -5
- package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
- package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
- package/src/plugins/builtin/ai/settings.ts +0 -1
- package/src/plugins/builtin/ai/utils.ts +1 -71
- package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
- package/src/plugins/builtin/alerts/index.test.tsx +61 -82
- package/src/plugins/builtin/alerts/index.tsx +263 -155
- package/src/plugins/builtin/alerts/types.ts +7 -0
- package/src/plugins/builtin/analytics/index.test.tsx +13 -8
- package/src/plugins/builtin/analytics/index.tsx +78 -120
- package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
- package/src/plugins/builtin/ask-ai.test.tsx +55 -54
- package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
- package/src/plugins/builtin/broker-manager/index.tsx +759 -0
- package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
- package/src/plugins/builtin/broker-manager/model.ts +142 -0
- package/src/plugins/builtin/chat-controller.test.ts +129 -0
- package/src/plugins/builtin/chat-controller.ts +85 -14
- package/src/plugins/builtin/chat.test.tsx +283 -36
- package/src/plugins/builtin/chat.tsx +363 -170
- package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
- package/src/plugins/builtin/cloud-tweets.tsx +896 -0
- package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
- package/src/plugins/builtin/comparison-chart.tsx +11 -11
- package/src/plugins/builtin/correlation/compute.test.ts +0 -17
- package/src/plugins/builtin/correlation/index.tsx +51 -46
- package/src/plugins/builtin/correlation/settings.ts +1 -2
- package/src/plugins/builtin/debug.tsx +99 -70
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
- package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
- package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
- package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
- package/src/plugins/builtin/earnings/index.tsx +187 -94
- package/src/plugins/builtin/econ/calendar-source.ts +9 -116
- package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
- package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
- package/src/plugins/builtin/econ/index.tsx +236 -421
- package/src/plugins/builtin/econ/types.ts +1 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
- package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
- package/src/plugins/builtin/help.test.tsx +120 -60
- package/src/plugins/builtin/help.tsx +266 -82
- package/src/plugins/builtin/holders/index.test.ts +45 -0
- package/src/plugins/builtin/holders/index.tsx +949 -0
- package/src/plugins/builtin/insider/index.tsx +207 -253
- package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
- package/src/plugins/builtin/insider/insider-data.ts +1 -0
- package/src/plugins/builtin/layout-manager.tsx +12 -40
- package/src/plugins/builtin/market-movers/index.tsx +70 -144
- package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
- package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
- package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
- package/src/plugins/builtin/news-wire/categories.ts +114 -0
- package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
- package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
- package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
- package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
- package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
- package/src/plugins/builtin/news-wire/hash.ts +9 -0
- package/src/plugins/builtin/news-wire/index.tsx +92 -0
- package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
- package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
- package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
- package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
- package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
- package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
- package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
- package/src/plugins/builtin/news-wire/read-state.ts +76 -0
- package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
- package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
- package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
- package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
- package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
- package/src/plugins/builtin/news.tsx +98 -25
- package/src/plugins/builtin/notes-files.ts +51 -10
- package/src/plugins/builtin/notes.tsx +332 -131
- package/src/plugins/builtin/options.test.tsx +185 -0
- package/src/plugins/builtin/options.tsx +346 -206
- package/src/plugins/builtin/plugin-groups.ts +92 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
- package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
- package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
- package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
- package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
- package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
- package/src/plugins/builtin/portfolio-list.test.ts +60 -1
- package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
- package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
- package/src/plugins/builtin/research/index.tsx +724 -0
- package/src/plugins/builtin/sec.tsx +67 -19
- package/src/plugins/builtin/sectors/index.tsx +42 -80
- package/src/plugins/builtin/sectors/sector-data.ts +1 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
- package/src/plugins/builtin/ticker-detail/pane.tsx +143 -49
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
- package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +82 -10
- package/src/plugins/builtin/ticker-detail.test.tsx +258 -33
- package/src/plugins/builtin/ticker-surface.ts +68 -0
- package/src/plugins/builtin/world-indices/index.tsx +44 -82
- package/src/plugins/builtin/world-indices/indices.ts +1 -0
- package/src/plugins/builtin/yahoo.ts +55 -1
- package/src/plugins/builtin/yield-curve/index.tsx +87 -145
- package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
- package/src/plugins/catalog-ui.test.ts +24 -0
- package/src/plugins/catalog-ui.ts +44 -0
- package/src/plugins/catalog.ts +2 -50
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/account-cache.test.ts +15 -14
- package/src/plugins/ibkr/account-cache.ts +15 -120
- package/src/plugins/ibkr/broker-adapter.ts +67 -0
- package/src/plugins/ibkr/config.test.ts +0 -18
- package/src/plugins/ibkr/dialogs.tsx +14 -56
- package/src/plugins/ibkr/flex.test.ts +57 -2
- package/src/plugins/ibkr/flex.ts +116 -12
- package/src/plugins/ibkr/gateway-helpers.ts +8 -3
- package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
- package/src/plugins/ibkr/gateway-service.test.ts +47 -9
- package/src/plugins/ibkr/gateway-service.ts +264 -2235
- package/src/plugins/ibkr/gateway-types.ts +27 -0
- package/src/plugins/ibkr/index.test.tsx +3 -11
- package/src/plugins/ibkr/index.tsx +1 -0
- package/src/plugins/ibkr/price-normalization.test.ts +1 -6
- package/src/plugins/ibkr/price-normalization.ts +1 -0
- package/src/plugins/ibkr/trade-badge.tsx +6 -5
- package/src/plugins/ibkr/trade-tab.test.tsx +55 -3
- package/src/plugins/ibkr/trade-tab.tsx +166 -117
- package/src/plugins/ibkr/trading-pane.tsx +117 -83
- package/src/plugins/pane-manager.test.ts +3 -1
- package/src/plugins/pane-manager.ts +93 -19
- package/src/plugins/plugin-runtime.test.tsx +176 -74
- package/src/plugins/plugin-runtime.tsx +228 -10
- package/src/plugins/prediction-markets/cache.ts +49 -0
- package/src/plugins/prediction-markets/categories.ts +0 -10
- package/src/plugins/prediction-markets/chart.test.tsx +1 -1
- package/src/plugins/prediction-markets/chart.tsx +21 -20
- package/src/plugins/prediction-markets/cli.ts +1 -0
- package/src/plugins/prediction-markets/controller-data.ts +135 -89
- package/src/plugins/prediction-markets/controller.ts +37 -108
- package/src/plugins/prediction-markets/detail/book.tsx +23 -22
- package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
- package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
- package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
- package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
- package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
- package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
- package/src/plugins/prediction-markets/detail.test.tsx +1 -1
- package/src/plugins/prediction-markets/keyboard.ts +1 -0
- package/src/plugins/prediction-markets/launch.test.ts +1 -1
- package/src/plugins/prediction-markets/launch.ts +1 -1
- package/src/plugins/prediction-markets/metrics.ts +25 -12
- package/src/plugins/prediction-markets/pane.test.tsx +16 -7
- package/src/plugins/prediction-markets/pane.tsx +180 -82
- package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
- package/src/plugins/prediction-markets/services/fetch.ts +27 -10
- package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/settings.ts +0 -5
- package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
- package/src/plugins/registry.test.ts +307 -0
- package/src/plugins/registry.ts +293 -82
- package/src/react/input.ts +61 -0
- package/src/react/plugins/plugin-slot.tsx +21 -0
- package/src/react/use-raf-callback.ts +44 -0
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
- package/src/renderers/electrobun/bun/application-menu.ts +118 -0
- package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
- package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
- package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
- package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
- package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
- package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
- package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
- package/src/renderers/electrobun/bun/index.ts +1149 -0
- package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
- package/src/renderers/electrobun/bun/window-frame.ts +46 -0
- package/src/renderers/electrobun/shared/protocol.ts +72 -0
- package/src/renderers/electrobun/view/ai-host.ts +93 -0
- package/src/renderers/electrobun/view/app-services.ts +348 -0
- package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
- package/src/renderers/electrobun/view/config-host.ts +35 -0
- package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
- package/src/renderers/electrobun/view/context-menu.ts +164 -0
- package/src/renderers/electrobun/view/data-table.tsx +773 -0
- package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
- package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
- package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
- package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
- package/src/renderers/electrobun/view/http-fetch.ts +113 -0
- package/src/renderers/electrobun/view/input-host.tsx +112 -0
- package/src/renderers/electrobun/view/key-event.test.ts +34 -0
- package/src/renderers/electrobun/view/key-event.ts +134 -0
- package/src/renderers/electrobun/view/main.tsx +104 -0
- package/src/renderers/electrobun/view/native-renderer.ts +110 -0
- package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
- package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
- package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
- package/src/renderers/electrobun/view/notes-files.ts +56 -0
- package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
- package/src/renderers/electrobun/view/resource-store.ts +106 -0
- package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
- package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
- package/src/renderers/electrobun/view/toast-host.tsx +65 -0
- package/src/renderers/electrobun/view/ui-host.tsx +1509 -0
- package/src/renderers/electrobun/view/update-host.ts +38 -0
- package/src/renderers/opentui/dialog-host.test.tsx +44 -0
- package/src/renderers/opentui/dialog-host.tsx +56 -0
- package/src/renderers/opentui/host.tsx +140 -0
- package/src/renderers/opentui/image-loader.test.ts +25 -0
- package/src/renderers/opentui/image-loader.ts +105 -0
- package/src/renderers/opentui/image-surface.tsx +266 -0
- package/src/renderers/opentui/input-host.tsx +63 -0
- package/src/renderers/opentui/start.tsx +71 -0
- package/src/renderers/opentui/test-utils.tsx +76 -0
- package/src/renderers/opentui/toast-host.tsx +10 -0
- package/src/renderers/opentui/ui-host.tsx +98 -0
- package/src/sources/gloomberb-cloud.test.ts +288 -17
- package/src/sources/gloomberb-cloud.ts +176 -9
- package/src/sources/provider-errors.ts +1 -0
- package/src/sources/provider-router.test.ts +220 -81
- package/src/sources/provider-router.ts +294 -111
- package/src/sources/sec-edgar.ts +19 -6
- package/src/sources/yahoo-finance.test.ts +0 -5
- package/src/sources/yahoo-finance.ts +420 -11
- package/src/state/app-activity.ts +7 -7
- package/src/state/app-bootstrap.ts +114 -40
- package/src/state/app-context-selector.test.tsx +70 -1
- package/src/state/app-context.test.ts +71 -3
- package/src/state/app-context.test.tsx +14 -7
- package/src/state/app-context.tsx +209 -869
- package/src/state/config-save-scheduler.ts +35 -0
- package/src/state/pane-scroll-registry.tsx +254 -0
- package/src/state/persist-scheduler.test.ts +67 -0
- package/src/state/persist-scheduler.ts +69 -0
- package/src/state/selectors-ui.ts +55 -0
- package/src/state/selectors.ts +15 -71
- package/src/state/session-persistence.test.ts +1 -1
- package/src/state/ticker-refresh-queue.ts +52 -3
- package/src/state/use-inline-tickers.ts +18 -16
- package/src/state/use-quote-streaming.test.ts +0 -6
- package/src/state/use-quote-streaming.test.tsx +1 -1
- package/src/state/use-quote-streaming.ts +3 -2
- package/src/state/use-session-snapshot.ts +32 -17
- package/src/test-support/plugin-runtime.ts +149 -0
- package/src/theme/color-utils.ts +1 -0
- package/src/theme/colors.ts +16 -0
- package/src/types/broker.ts +16 -1
- package/src/types/capability-route-source.ts +23 -0
- package/src/types/config.test.ts +23 -0
- package/src/types/config.ts +145 -10
- package/src/types/context-menu.ts +95 -0
- package/src/types/data-provider.ts +34 -3
- package/src/types/desktop-menu.ts +13 -0
- package/src/types/desktop-window.ts +29 -0
- package/src/types/financials.ts +123 -2
- package/src/types/instrument.ts +1 -0
- package/src/types/news-source.ts +9 -0
- package/src/types/persistence.ts +1 -0
- package/src/types/plugin.ts +32 -8
- package/src/ui/ascii-font.ts +85 -0
- package/src/ui/context-menu.test.ts +94 -0
- package/src/ui/context-menu.tsx +433 -0
- package/src/ui/dialog.tsx +55 -0
- package/src/ui/host.tsx +364 -0
- package/src/ui/index.tsx +134 -0
- package/src/ui/toast.tsx +40 -0
- package/src/updater.test.ts +69 -0
- package/src/updater.ts +61 -18
- package/src/utils/api-client.test.ts +99 -9
- package/src/utils/api-client.ts +378 -18
- package/src/utils/back-navigation.ts +6 -0
- package/src/utils/cli-output.ts +1 -0
- package/src/utils/currency-units.test.ts +1 -6
- package/src/utils/currency-units.ts +1 -0
- package/src/utils/debug-log.ts +68 -0
- package/src/utils/dom-screenshot.ts +326 -0
- package/src/utils/exchange-rate-map.ts +1 -0
- package/src/utils/exchanges.ts +214 -0
- package/src/utils/format.test.ts +7 -0
- package/src/utils/format.ts +1 -0
- package/src/utils/fuzzy-search.ts +1 -0
- package/src/utils/html-entities.test.ts +8 -0
- package/src/utils/html-entities.ts +32 -0
- package/src/utils/http-transport.ts +11 -0
- package/src/utils/main-thread-monitor.ts +127 -0
- package/src/utils/perf-marks.ts +54 -0
- package/src/utils/quote-freshness.ts +27 -50
- package/src/utils/quote-resolution.test.ts +5 -5
- package/src/utils/quote-resolution.ts +0 -1
- package/src/utils/quote-time.ts +1 -5
- package/src/utils/sec.test.ts +0 -12
- package/src/utils/selection-clipboard.ts +14 -6
- package/src/utils/shortcut-labels.test.ts +20 -0
- package/src/utils/shortcut-labels.ts +67 -0
- package/src/utils/slugify.ts +1 -0
- package/src/utils/terminal-input-reset.ts +1 -0
- package/src/utils/text-wrap.ts +82 -0
- package/src/utils/throttled-fetch.test.ts +19 -0
- package/src/utils/throttled-fetch.ts +11 -9
- package/src/utils/ticker-list.test.ts +1 -7
- package/src/utils/ticker-list.ts +1 -0
- package/src/utils/timestamp.ts +29 -0
- package/src/utils/tweet-text.test.ts +13 -0
- package/src/utils/tweet-text.ts +18 -0
- package/src/utils/url.ts +11 -0
- package/src/version.ts +1 -1
- package/src/components/chart/chart-control-hint.tsx +0 -39
- package/src/components/detail-feed-view.test.tsx +0 -180
- package/src/components/detail-feed-view.tsx +0 -531
- package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
- package/src/components/layout/pixel-mouse.test.ts +0 -40
- package/src/components/spinner.tsx +0 -2
- package/src/components/tab-bar.tsx +0 -17
- package/src/components/ui/multi-select-chips.tsx +0 -69
- package/src/plugins/builtin/ai/index.test.tsx +0 -139
- package/src/plugins/builtin/ask-ai.tsx +0 -9
- package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
- package/src/plugins/builtin/correlation/settings.test.ts +0 -34
- package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
- package/src/plugins/builtin/econ/fred-client.ts +0 -79
- package/src/plugins/builtin/fred-settings.ts +0 -10
- package/src/plugins/builtin/options-availability.test.tsx +0 -179
- package/src/plugins/builtin/options-availability.ts +0 -158
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
- package/src/plugins/prediction-markets/status.ts +0 -24
- package/src/plugins/prediction-markets/table.tsx +0 -87
- package/src/utils/quote-time.test.ts +0 -22
- package/src/utils/selection-clipboard.test.ts +0 -43
- package/src/utils/terminal-input-reset.test.ts +0 -50
package/src/app.tsx
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
|
+
import { Box, ContextMenuProvider, useNativeRenderer, useRendererHost } from "./ui";
|
|
2
|
+
import { ToastViewport, useToastHost } from "./ui/toast";
|
|
1
3
|
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import type { CliRenderer } from "@opentui/core";
|
|
4
|
+
import { useShortcut } from "./react/input";
|
|
4
5
|
import {
|
|
5
6
|
AppProvider,
|
|
6
7
|
getFocusedCollectionId,
|
|
7
8
|
getFocusedTickerSymbol,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
useAppDispatch,
|
|
10
|
+
useAppSelector,
|
|
11
|
+
useAppStateRef,
|
|
12
|
+
type AppState,
|
|
11
13
|
} from "./state/app-context";
|
|
12
14
|
import { bindAppActivity, useAppActive } from "./state/app-activity";
|
|
13
15
|
import { copyActiveSelection, isCopyShortcut, isPasteShortcut, pasteSystemClipboard } from "./utils/selection-clipboard";
|
|
14
16
|
import { Header } from "./components/layout/header";
|
|
15
17
|
import { StatusBar } from "./components/layout/status-bar";
|
|
16
18
|
import { Shell } from "./components/layout/shell";
|
|
19
|
+
import { DetachedPaneShell } from "./components/layout/detached-pane-shell";
|
|
17
20
|
import { CommandBar } from "./components/command-bar/command-bar";
|
|
18
21
|
import { OnboardingWizard } from "./components/onboarding/onboarding-wizard";
|
|
19
|
-
import {
|
|
22
|
+
import { useDialog, useDialogState } from "./ui/dialog";
|
|
20
23
|
import { PluginRegistry } from "./plugins/registry";
|
|
21
|
-
import {
|
|
22
|
-
import { TickerRepository } from "./data/ticker-repository";
|
|
23
|
-
import { ProviderRouter } from "./sources/provider-router";
|
|
24
|
+
import type { TickerRepository } from "./data/ticker-repository";
|
|
24
25
|
import { colors, syncTheme } from "./theme/colors";
|
|
25
26
|
import {
|
|
26
27
|
createPaneInstance,
|
|
27
28
|
findPaneInstance,
|
|
28
29
|
findPrimaryPaneInstance,
|
|
29
30
|
isTickerPaneId,
|
|
31
|
+
materializeDetachedPanesAsFloating,
|
|
30
32
|
normalizePaneLayout,
|
|
31
33
|
resolveFollowBindingInstance,
|
|
32
34
|
resolvePaneInstance,
|
|
@@ -42,22 +44,19 @@ import type { TickerRecord } from "./types/ticker";
|
|
|
42
44
|
import type { DataProvider } from "./types/data-provider";
|
|
43
45
|
import type { TickerFinancials } from "./types/financials";
|
|
44
46
|
import type { BrokerAccount } from "./types/trading";
|
|
47
|
+
import type { DesktopDockPreviewState, DesktopSharedStateSnapshot, DesktopWindowBridge } from "./types/desktop-window";
|
|
48
|
+
import type { DesktopApplicationMenuBridge } from "./types/desktop-menu";
|
|
45
49
|
import { resolveTickerSearch, upsertTickerFromSearchResult } from "./utils/ticker-search";
|
|
46
50
|
|
|
47
|
-
// Built-in plugins
|
|
48
51
|
import {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
clearPersistedBrokerAccounts,
|
|
53
|
+
getBrokerAccountCacheSourceKey,
|
|
54
|
+
loadPersistedBrokerAccountMap,
|
|
55
|
+
} from "./brokers/account-cache";
|
|
53
56
|
import { chatController } from "./plugins/builtin/chat-controller";
|
|
54
57
|
import { setLayoutManagerDispatch } from "./plugins/builtin/layout-manager";
|
|
55
|
-
import { getLoadablePlugins } from "./plugins/catalog";
|
|
56
|
-
import { saveConfig } from "./data/config-store";
|
|
57
|
-
import { Toaster, toast } from "@opentui-ui/toast/react";
|
|
58
58
|
import { canSelfUpdate, checkForUpdateDetailed, performUpdate, type ReleaseInfo } from "./updater";
|
|
59
59
|
import { VERSION } from "./version";
|
|
60
|
-
import { join } from "path";
|
|
61
60
|
import {
|
|
62
61
|
addPaneFloating,
|
|
63
62
|
addPaneToLayout,
|
|
@@ -66,9 +65,11 @@ import {
|
|
|
66
65
|
getDockLeafLayouts,
|
|
67
66
|
getDockedPaneIds,
|
|
68
67
|
getLeafRect,
|
|
68
|
+
gridlockAllPanes,
|
|
69
69
|
isPaneInLayout,
|
|
70
70
|
removePane,
|
|
71
71
|
} from "./plugins/pane-manager";
|
|
72
|
+
import { notifyGridlockComplete } from "./plugins/gridlock-notification";
|
|
72
73
|
import {
|
|
73
74
|
createBrokerInstanceId,
|
|
74
75
|
getBrokerInstance,
|
|
@@ -78,7 +79,7 @@ import {
|
|
|
78
79
|
APP_SESSION_SCHEMA_VERSION,
|
|
79
80
|
reconcileAppSessionSnapshot,
|
|
80
81
|
type AppSessionSnapshot,
|
|
81
|
-
} from "./state/session-persistence";
|
|
82
|
+
} from "./core/state/session-persistence";
|
|
82
83
|
import { initializeAppState } from "./state/app-bootstrap";
|
|
83
84
|
import { TickerRefreshQueue } from "./state/ticker-refresh-queue";
|
|
84
85
|
import { PaneSettingsDialogContent } from "./components/pane-settings-dialog";
|
|
@@ -94,10 +95,21 @@ import {
|
|
|
94
95
|
} from "./components/command-bar/workflow-ops";
|
|
95
96
|
import { getPaneTemplateDisplayLabel } from "./components/command-bar/pane-template-display";
|
|
96
97
|
import { debugLog } from "./utils/debug-log";
|
|
97
|
-
import { MarketDataCoordinator
|
|
98
|
+
import type { MarketDataCoordinator } from "./market-data/coordinator";
|
|
98
99
|
import { instrumentFromTicker } from "./market-data/request-types";
|
|
99
|
-
import {
|
|
100
|
+
import {
|
|
101
|
+
restoreBrokerPortfoliosFromTickerPositions,
|
|
102
|
+
syncBrokerInstance,
|
|
103
|
+
syncBrokerInstances,
|
|
104
|
+
type SyncBrokerInstanceResult,
|
|
105
|
+
} from "./brokers/sync-broker-instance";
|
|
100
106
|
import { createAppNotifier } from "./notifications/app-notifier";
|
|
107
|
+
import { createAppServices } from "./core/app-services";
|
|
108
|
+
import {
|
|
109
|
+
saveConfigImmediately,
|
|
110
|
+
scheduleConfigSave,
|
|
111
|
+
} from "./state/config-save-scheduler";
|
|
112
|
+
import { measurePerf, measurePerfAsync } from "./utils/perf-marks";
|
|
101
113
|
|
|
102
114
|
/** Global-level dedup: prevents concurrent refresh calls for the same symbol. */
|
|
103
115
|
const refreshInFlight: Set<string> = (globalThis as any).__refreshInFlight ??= new Set<string>();
|
|
@@ -113,35 +125,72 @@ function isTickerContextPaneInstance(instance: PaneInstanceConfig): boolean {
|
|
|
113
125
|
return instance.paneId === "portfolio-list" || isTickerPaneId(instance.paneId);
|
|
114
126
|
}
|
|
115
127
|
|
|
116
|
-
function summarizeError(error: unknown): Record<string, string> {
|
|
117
|
-
if (error instanceof Error) {
|
|
118
|
-
return {
|
|
119
|
-
name: error.name,
|
|
120
|
-
message: error.message,
|
|
121
|
-
stack: error.stack || "",
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
return { message: String(error) };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
128
|
interface AppInnerProps {
|
|
128
129
|
pluginRegistry: PluginRegistry;
|
|
129
130
|
tickerRepository: TickerRepository;
|
|
130
131
|
dataProvider: DataProvider;
|
|
131
132
|
marketData: MarketDataCoordinator;
|
|
132
133
|
sessionSnapshot?: AppSessionSnapshot | null;
|
|
134
|
+
desktopWindowBridge?: DesktopWindowBridge;
|
|
135
|
+
desktopApplicationMenuBridge?: DesktopApplicationMenuBridge;
|
|
133
136
|
}
|
|
134
137
|
|
|
135
|
-
function AppInner({
|
|
136
|
-
|
|
138
|
+
function AppInner({
|
|
139
|
+
pluginRegistry,
|
|
140
|
+
tickerRepository,
|
|
141
|
+
dataProvider,
|
|
142
|
+
marketData,
|
|
143
|
+
sessionSnapshot = null,
|
|
144
|
+
desktopWindowBridge,
|
|
145
|
+
desktopApplicationMenuBridge,
|
|
146
|
+
}: AppInnerProps) {
|
|
147
|
+
const dispatch = useAppDispatch();
|
|
148
|
+
const stateRef = useAppStateRef();
|
|
149
|
+
const config = useAppSelector((state) => state.config);
|
|
150
|
+
const tickers = useAppSelector((state) => state.tickers);
|
|
151
|
+
const paneState = useAppSelector((state) => state.paneState);
|
|
152
|
+
const focusedPaneId = useAppSelector((state) => state.focusedPaneId);
|
|
153
|
+
const initialized = useAppSelector((state) => state.initialized);
|
|
154
|
+
const commandBarOpen = useAppSelector((state) => state.commandBarOpen);
|
|
155
|
+
const inputCaptured = useAppSelector((state) => state.inputCaptured);
|
|
156
|
+
const updateAvailable = useAppSelector((state) => state.updateAvailable);
|
|
157
|
+
const updateProgress = useAppSelector((state) => state.updateProgress);
|
|
158
|
+
const updateCheckInProgress = useAppSelector((state) => state.updateCheckInProgress);
|
|
159
|
+
const state = useMemo(() => ({
|
|
160
|
+
...stateRef.current,
|
|
161
|
+
config,
|
|
162
|
+
tickers,
|
|
163
|
+
paneState,
|
|
164
|
+
focusedPaneId,
|
|
165
|
+
initialized,
|
|
166
|
+
commandBarOpen,
|
|
167
|
+
inputCaptured,
|
|
168
|
+
updateAvailable,
|
|
169
|
+
updateProgress,
|
|
170
|
+
updateCheckInProgress,
|
|
171
|
+
}) as AppState, [
|
|
172
|
+
commandBarOpen,
|
|
173
|
+
config,
|
|
174
|
+
focusedPaneId,
|
|
175
|
+
initialized,
|
|
176
|
+
inputCaptured,
|
|
177
|
+
paneState,
|
|
178
|
+
stateRef,
|
|
179
|
+
tickers,
|
|
180
|
+
updateAvailable,
|
|
181
|
+
updateCheckInProgress,
|
|
182
|
+
updateProgress,
|
|
183
|
+
]);
|
|
137
184
|
const appActive = useAppActive();
|
|
138
185
|
const appActiveRef = useRef(appActive);
|
|
139
|
-
const
|
|
186
|
+
const rendererHost = useRendererHost();
|
|
187
|
+
const nativeRenderer = useNativeRenderer();
|
|
140
188
|
const dialog = useDialog();
|
|
141
|
-
const
|
|
142
|
-
|
|
189
|
+
const toast = useToastHost();
|
|
190
|
+
const isDetachedWindow = desktopWindowBridge?.kind === "detached";
|
|
191
|
+
const detachedPaneId = isDetachedWindow ? desktopWindowBridge.paneId ?? null : null;
|
|
192
|
+
const [desktopDockPreview, setDesktopDockPreview] = useState<DesktopDockPreviewState | null>(null);
|
|
143
193
|
appActiveRef.current = appActive;
|
|
144
|
-
const focusedCollectionId = getFocusedCollectionId(state);
|
|
145
194
|
const appNotifier = useMemo(() => createAppNotifier({
|
|
146
195
|
isAppActive: () => appActiveRef.current,
|
|
147
196
|
renderToast: (notification) => {
|
|
@@ -166,27 +215,23 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
166
215
|
else if (type === "error") toastId = toast.error(notification.body, options);
|
|
167
216
|
else toastId = toast.info(notification.body, options);
|
|
168
217
|
},
|
|
169
|
-
|
|
218
|
+
desktop: rendererHost.supportsNativeDesktopNotifications ? rendererHost : undefined,
|
|
219
|
+
}), [rendererHost, toast]);
|
|
170
220
|
const notify = useCallback((body: string, options?: { type?: "info" | "success" | "error" }) => {
|
|
171
221
|
pluginRegistry.notify({ body, ...options });
|
|
172
222
|
}, [pluginRegistry]);
|
|
173
223
|
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (desktopWindowBridge?.kind !== "main" || !desktopWindowBridge.subscribeDockPreview) return;
|
|
226
|
+
return desktopWindowBridge.subscribeDockPreview((preview) => {
|
|
227
|
+
setDesktopDockPreview(preview);
|
|
228
|
+
});
|
|
229
|
+
}, [desktopWindowBridge]);
|
|
230
|
+
|
|
174
231
|
const resolvePaneTarget = useCallback((paneId: string, layout: LayoutConfig = state.config.layout): string | null => {
|
|
175
232
|
return resolvePaneInstance(layout, paneId)?.instanceId ?? null;
|
|
176
233
|
}, [state.config.layout]);
|
|
177
234
|
|
|
178
|
-
const getPreferredPortfolio = useCallback((ticker: TickerRecord | null) => {
|
|
179
|
-
const focusedPortfolio = state.config.portfolios.find((portfolio) => portfolio.id === focusedCollectionId);
|
|
180
|
-
if (focusedPortfolio) return focusedPortfolio;
|
|
181
|
-
if (ticker) {
|
|
182
|
-
for (const portfolioId of ticker.metadata.portfolios) {
|
|
183
|
-
const portfolio = state.config.portfolios.find((entry) => entry.id === portfolioId);
|
|
184
|
-
if (portfolio) return portfolio;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return state.config.portfolios[0] ?? null;
|
|
188
|
-
}, [focusedCollectionId, state.config.portfolios]);
|
|
189
|
-
|
|
190
235
|
const runPaneTemplateWizard = useCallback(async (steps: WizardStep[]): Promise<Record<string, string> | null> => {
|
|
191
236
|
const values: Record<string, string> = {};
|
|
192
237
|
|
|
@@ -293,7 +338,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
293
338
|
index === state.config.activeLayoutIndex ? { ...savedLayout, layout: ensured.layout } : savedLayout
|
|
294
339
|
));
|
|
295
340
|
dispatch({ type: "UPDATE_LAYOUT", layout: ensured.layout });
|
|
296
|
-
|
|
341
|
+
scheduleConfigSave({ ...state.config, layout: ensured.layout, layouts });
|
|
297
342
|
}
|
|
298
343
|
return ensured.instance.instanceId;
|
|
299
344
|
})();
|
|
@@ -474,26 +519,17 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
474
519
|
marketData.primeCachedFinancials(primeEntries);
|
|
475
520
|
}, [marketData]);
|
|
476
521
|
|
|
477
|
-
|
|
478
|
-
const importBrokerPositions = useCallback(async (
|
|
522
|
+
const applyBrokerImportResult = useCallback(async (
|
|
479
523
|
instanceId: string,
|
|
480
|
-
|
|
524
|
+
result: SyncBrokerInstanceResult,
|
|
525
|
+
baseConfig: AppConfig,
|
|
481
526
|
options?: { refreshImportedTickers?: boolean },
|
|
482
527
|
) => {
|
|
483
|
-
const result = await syncBrokerInstance({
|
|
484
|
-
config: state.config,
|
|
485
|
-
instanceId,
|
|
486
|
-
brokers: pluginRegistry.brokers,
|
|
487
|
-
tickerRepository,
|
|
488
|
-
existingTickers: tickerMap ?? new Map(state.tickers),
|
|
489
|
-
resources: pluginRegistry.persistence.resources,
|
|
490
|
-
});
|
|
491
|
-
|
|
492
528
|
dispatch({ type: "SET_BROKER_ACCOUNTS", instanceId, accounts: result.brokerAccounts });
|
|
493
529
|
|
|
494
|
-
if (result.config !==
|
|
530
|
+
if (result.config !== baseConfig) {
|
|
495
531
|
dispatch({ type: "SET_CONFIG", config: result.config });
|
|
496
|
-
await
|
|
532
|
+
await saveConfigImmediately(result.config);
|
|
497
533
|
pluginRegistry.events.emit("config:changed", { config: result.config });
|
|
498
534
|
}
|
|
499
535
|
|
|
@@ -505,31 +541,57 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
505
541
|
}
|
|
506
542
|
|
|
507
543
|
for (const position of result.positions) {
|
|
508
|
-
// Skip Yahoo Finance for
|
|
544
|
+
// Skip Yahoo Finance for broker option symbols; position marks are already available.
|
|
509
545
|
// Position data (markPrice, marketValue, unrealizedPnl) is used directly.
|
|
510
546
|
if (options?.refreshImportedTickers !== false && position.assetCategory !== "OPT") {
|
|
511
547
|
refreshQuote(position.ticker, position.exchange, undefined, 1);
|
|
512
548
|
}
|
|
513
549
|
}
|
|
550
|
+
}, [dispatch, pluginRegistry.events, refreshQuote]);
|
|
551
|
+
|
|
552
|
+
// Import positions from a single broker instance
|
|
553
|
+
const importBrokerPositions = useCallback(async (
|
|
554
|
+
instanceId: string,
|
|
555
|
+
tickerMap?: Map<string, TickerRecord>,
|
|
556
|
+
options?: { refreshImportedTickers?: boolean; config?: AppConfig },
|
|
557
|
+
) => {
|
|
558
|
+
const baseConfig = options?.config ?? stateRef.current.config;
|
|
559
|
+
const result = await syncBrokerInstance({
|
|
560
|
+
config: baseConfig,
|
|
561
|
+
instanceId,
|
|
562
|
+
brokers: pluginRegistry.brokers,
|
|
563
|
+
tickerRepository,
|
|
564
|
+
existingTickers: tickerMap ?? new Map(stateRef.current.tickers),
|
|
565
|
+
resources: pluginRegistry.persistence.resources,
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
await applyBrokerImportResult(instanceId, result, baseConfig, options);
|
|
514
569
|
|
|
515
570
|
return result;
|
|
516
|
-
}, [
|
|
571
|
+
}, [applyBrokerImportResult, pluginRegistry.brokers, pluginRegistry.persistence.resources, stateRef, tickerRepository]);
|
|
517
572
|
|
|
518
573
|
// Auto-import positions from all configured broker instances
|
|
519
574
|
const autoImportBrokerPositions = useCallback(async (tickerMap: Map<string, TickerRecord>) => {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (result) {
|
|
526
|
-
nextTickerMap = result.tickers;
|
|
527
|
-
}
|
|
528
|
-
} catch {
|
|
529
|
-
// Silently fail — broker import is best-effort on startup
|
|
530
|
-
}
|
|
575
|
+
const restoredConfig = restoreBrokerPortfoliosFromTickerPositions(stateRef.current.config, tickerMap.values());
|
|
576
|
+
if (restoredConfig !== stateRef.current.config) {
|
|
577
|
+
dispatch({ type: "SET_CONFIG", config: restoredConfig });
|
|
578
|
+
await saveConfigImmediately(restoredConfig);
|
|
579
|
+
pluginRegistry.events.emit("config:changed", { config: restoredConfig });
|
|
531
580
|
}
|
|
532
|
-
|
|
581
|
+
|
|
582
|
+
await syncBrokerInstances({
|
|
583
|
+
config: restoredConfig,
|
|
584
|
+
brokers: pluginRegistry.brokers,
|
|
585
|
+
tickerRepository,
|
|
586
|
+
existingTickers: tickerMap,
|
|
587
|
+
resources: pluginRegistry.persistence.resources,
|
|
588
|
+
onResult: async (result, instance, previousConfig) => {
|
|
589
|
+
await applyBrokerImportResult(instance.id, result, previousConfig, {
|
|
590
|
+
refreshImportedTickers: false,
|
|
591
|
+
});
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
}, [applyBrokerImportResult, dispatch, pluginRegistry.brokers, pluginRegistry.events, pluginRegistry.persistence.resources, stateRef, tickerRepository]);
|
|
533
595
|
|
|
534
596
|
const startUpdate = useCallback((release: ReleaseInfo) => {
|
|
535
597
|
dispatch({ type: "SET_UPDATE_PROGRESS", progress: { phase: "downloading", percent: 0 } });
|
|
@@ -576,14 +638,64 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
576
638
|
|
|
577
639
|
// Check for updates on mount
|
|
578
640
|
useEffect(() => {
|
|
641
|
+
if (isDetachedWindow) return;
|
|
579
642
|
void runUpdateCheck(false);
|
|
580
|
-
}, [runUpdateCheck]);
|
|
643
|
+
}, [isDetachedWindow, runUpdateCheck]);
|
|
644
|
+
|
|
645
|
+
useEffect(() => {
|
|
646
|
+
if (desktopWindowBridge?.kind !== "main" || !desktopApplicationMenuBridge) return;
|
|
647
|
+
return desktopApplicationMenuBridge.subscribe((command) => {
|
|
648
|
+
switch (command.type) {
|
|
649
|
+
case "open-command-bar":
|
|
650
|
+
dispatch({ type: "SET_COMMAND_BAR", open: true, query: command.query });
|
|
651
|
+
break;
|
|
652
|
+
case "open-plugin-workflow":
|
|
653
|
+
pluginRegistry.openPluginCommandWorkflowFn(command.commandId);
|
|
654
|
+
break;
|
|
655
|
+
case "open-url":
|
|
656
|
+
void rendererHost.openExternal(command.url).catch((error) => {
|
|
657
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
658
|
+
pluginRegistry.notify({ body: `Failed to open link: ${message}`, type: "error" });
|
|
659
|
+
});
|
|
660
|
+
break;
|
|
661
|
+
case "check-for-updates":
|
|
662
|
+
void runUpdateCheck(true);
|
|
663
|
+
break;
|
|
664
|
+
case "toggle-status-bar":
|
|
665
|
+
dispatch({ type: "TOGGLE_STATUS_BAR" });
|
|
666
|
+
break;
|
|
667
|
+
case "layout-undo":
|
|
668
|
+
dispatch({ type: "UNDO_LAYOUT" });
|
|
669
|
+
break;
|
|
670
|
+
case "layout-redo":
|
|
671
|
+
dispatch({ type: "REDO_LAYOUT" });
|
|
672
|
+
break;
|
|
673
|
+
case "layout-gridlock": {
|
|
674
|
+
const { width, height } = pluginRegistry.getTermSizeFn();
|
|
675
|
+
pluginRegistry.updateLayoutFn(gridlockAllPanes(stateRef.current.config.layout, { x: 0, y: 0, width, height }));
|
|
676
|
+
notifyGridlockComplete(pluginRegistry.notify.bind(pluginRegistry), () => {
|
|
677
|
+
dispatch({ type: "UNDO_LAYOUT" });
|
|
678
|
+
});
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}, [
|
|
684
|
+
desktopApplicationMenuBridge,
|
|
685
|
+
desktopWindowBridge?.kind,
|
|
686
|
+
dispatch,
|
|
687
|
+
pluginRegistry,
|
|
688
|
+
rendererHost,
|
|
689
|
+
runUpdateCheck,
|
|
690
|
+
stateRef,
|
|
691
|
+
]);
|
|
581
692
|
|
|
582
693
|
useEffect(() => {
|
|
694
|
+
if (isDetachedWindow) return;
|
|
583
695
|
if (!state.updateAvailable || state.updateProgress || state.updateCheckInProgress) return;
|
|
584
696
|
if (!canSelfUpdate(state.updateAvailable)) return;
|
|
585
697
|
startUpdate(state.updateAvailable);
|
|
586
|
-
}, [startUpdate, state.updateAvailable, state.updateCheckInProgress, state.updateProgress]);
|
|
698
|
+
}, [isDetachedWindow, startUpdate, state.updateAvailable, state.updateCheckInProgress, state.updateProgress]);
|
|
587
699
|
|
|
588
700
|
// Load tickers on mount
|
|
589
701
|
useEffect(() => {
|
|
@@ -593,12 +705,13 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
593
705
|
try {
|
|
594
706
|
let persistedBrokerAccounts: Record<string, BrokerAccount[]> = {};
|
|
595
707
|
try {
|
|
596
|
-
persistedBrokerAccounts =
|
|
708
|
+
persistedBrokerAccounts = loadPersistedBrokerAccountMap(
|
|
597
709
|
pluginRegistry.persistence.resources,
|
|
598
710
|
state.config.brokerInstances,
|
|
711
|
+
pluginRegistry.brokers,
|
|
599
712
|
);
|
|
600
713
|
} catch {}
|
|
601
|
-
await initializeAppState({
|
|
714
|
+
await measurePerfAsync("startup.app.initialize-state", () => initializeAppState({
|
|
602
715
|
config: state.config,
|
|
603
716
|
tickerRepository,
|
|
604
717
|
dataProvider,
|
|
@@ -609,6 +722,10 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
609
722
|
refreshQuote,
|
|
610
723
|
autoImportBrokerPositions,
|
|
611
724
|
persistedBrokerAccounts,
|
|
725
|
+
}), {
|
|
726
|
+
brokerInstanceCount: state.config.brokerInstances.length,
|
|
727
|
+
layoutPaneCount: state.config.layout.instances.length,
|
|
728
|
+
sessionHydrationTargetCount: sessionSnapshot?.hydrationTargets.length ?? 0,
|
|
612
729
|
});
|
|
613
730
|
} catch (err) {
|
|
614
731
|
// Will show empty state
|
|
@@ -622,6 +739,10 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
622
739
|
if (!focusedTickerSymbol) return;
|
|
623
740
|
const ticker = state.tickers.get(focusedTickerSymbol);
|
|
624
741
|
if (!ticker) return;
|
|
742
|
+
appLog.info("focused ticker prefetch scheduled", {
|
|
743
|
+
symbol: ticker.metadata.ticker,
|
|
744
|
+
exchange: ticker.metadata.exchange,
|
|
745
|
+
});
|
|
625
746
|
marketData.prefetchTicker(instrumentFromTicker(ticker, ticker.metadata.ticker));
|
|
626
747
|
}, [focusedTickerSymbol, marketData, state.tickers]);
|
|
627
748
|
|
|
@@ -640,21 +761,25 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
640
761
|
pluginRegistry.getPluginConfigValueFn = (pluginId, key) => (
|
|
641
762
|
(state.config.pluginConfig[pluginId]?.[key] as any) ?? null
|
|
642
763
|
);
|
|
643
|
-
|
|
764
|
+
const setPluginConfigValues = async (pluginId: string, values: Record<string, unknown>) => {
|
|
644
765
|
const nextConfig = {
|
|
645
766
|
...state.config,
|
|
646
767
|
pluginConfig: {
|
|
647
768
|
...state.config.pluginConfig,
|
|
648
769
|
[pluginId]: {
|
|
649
770
|
...(state.config.pluginConfig[pluginId] ?? {}),
|
|
650
|
-
|
|
771
|
+
...values,
|
|
651
772
|
},
|
|
652
773
|
},
|
|
653
774
|
};
|
|
654
775
|
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
655
|
-
await
|
|
776
|
+
await saveConfigImmediately(nextConfig);
|
|
656
777
|
pluginRegistry.events.emit("config:changed", { config: nextConfig });
|
|
657
778
|
};
|
|
779
|
+
pluginRegistry.setPluginConfigValueFn = async (pluginId, key, value) => {
|
|
780
|
+
await setPluginConfigValues(pluginId, { [key]: value });
|
|
781
|
+
};
|
|
782
|
+
pluginRegistry.setPluginConfigValuesFn = setPluginConfigValues;
|
|
658
783
|
pluginRegistry.deletePluginConfigValueFn = async (pluginId, key) => {
|
|
659
784
|
const currentPluginConfig = state.config.pluginConfig[pluginId];
|
|
660
785
|
if (!currentPluginConfig || !(key in currentPluginConfig)) return;
|
|
@@ -674,11 +799,14 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
674
799
|
pluginConfig: nextAllPluginConfig,
|
|
675
800
|
};
|
|
676
801
|
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
677
|
-
await
|
|
802
|
+
await saveConfigImmediately(nextConfig);
|
|
678
803
|
pluginRegistry.events.emit("config:changed", { config: nextConfig });
|
|
679
804
|
};
|
|
680
|
-
|
|
681
|
-
|
|
805
|
+
const configurableProvider = dataProvider as DataProvider & {
|
|
806
|
+
setConfigAccessor?: (accessor: () => AppConfig) => void;
|
|
807
|
+
};
|
|
808
|
+
if (typeof configurableProvider.setConfigAccessor === "function") {
|
|
809
|
+
configurableProvider.setConfigAccessor(() => state.config);
|
|
682
810
|
}
|
|
683
811
|
pluginRegistry.createBrokerInstanceFn = async (brokerType, label, values) => {
|
|
684
812
|
const instanceId = createBrokerInstanceId(
|
|
@@ -699,34 +827,58 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
699
827
|
brokerInstances: [...state.config.brokerInstances, instance],
|
|
700
828
|
};
|
|
701
829
|
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
702
|
-
await
|
|
830
|
+
await saveConfigImmediately(nextConfig);
|
|
703
831
|
pluginRegistry.events.emit("config:changed", { config: nextConfig });
|
|
704
832
|
return instance;
|
|
705
833
|
};
|
|
706
|
-
pluginRegistry.
|
|
834
|
+
pluginRegistry.connectBrokerInstanceFn = async (instanceId) => {
|
|
835
|
+
const instance = getBrokerInstance(state.config.brokerInstances, instanceId);
|
|
836
|
+
if (!instance) throw new Error("Broker profile not found.");
|
|
837
|
+
if (instance.enabled === false) throw new Error(`Broker profile "${instance.label}" is disabled.`);
|
|
838
|
+
|
|
839
|
+
const broker = pluginRegistry.brokers.get(instance.brokerType);
|
|
840
|
+
if (!broker) throw new Error(`Broker "${instance.brokerType}" is not available.`);
|
|
841
|
+
|
|
842
|
+
const valid = await broker.validate(instance).catch(() => false);
|
|
843
|
+
if (!valid) throw new Error(`${broker.name} setup is incomplete.`);
|
|
844
|
+
|
|
845
|
+
await broker.connect?.(instance);
|
|
846
|
+
if (broker.listAccounts) {
|
|
847
|
+
const accounts = await broker.listAccounts(instance);
|
|
848
|
+
dispatch({ type: "SET_BROKER_ACCOUNTS", instanceId, accounts });
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
pluginRegistry.updateBrokerInstanceFn = async (instanceId, values, options = {}) => {
|
|
707
852
|
const currentInstance = state.config.brokerInstances.find((instance) => instance.id === instanceId);
|
|
708
853
|
const nextInstances = state.config.brokerInstances.map((instance) =>
|
|
709
854
|
instance.id === instanceId
|
|
710
|
-
? {
|
|
711
|
-
...instance,
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
855
|
+
? (() => {
|
|
856
|
+
const nextValues = options.replaceConfig ? values : { ...instance.config, ...values };
|
|
857
|
+
return {
|
|
858
|
+
...instance,
|
|
859
|
+
label: options.label ?? instance.label,
|
|
860
|
+
enabled: options.enabled ?? instance.enabled,
|
|
861
|
+
connectionMode: typeof nextValues.connectionMode === "string" ? nextValues.connectionMode : instance.connectionMode,
|
|
862
|
+
config: nextValues,
|
|
863
|
+
};
|
|
864
|
+
})()
|
|
715
865
|
: instance,
|
|
716
866
|
);
|
|
717
867
|
const nextInstance = nextInstances.find((instance) => instance.id === instanceId);
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
&&
|
|
721
|
-
|
|
722
|
-
|
|
868
|
+
const broker = currentInstance ? pluginRegistry.brokers.get(currentInstance.brokerType) : null;
|
|
869
|
+
const shouldClearBrokerAccounts = currentInstance
|
|
870
|
+
&& nextInstance
|
|
871
|
+
&& currentInstance.brokerType === nextInstance.brokerType
|
|
872
|
+
&& getBrokerAccountCacheSourceKey(currentInstance, broker) !== getBrokerAccountCacheSourceKey(nextInstance, broker);
|
|
873
|
+
if (shouldClearBrokerAccounts) {
|
|
874
|
+
clearPersistedBrokerAccounts(pluginRegistry.persistence.resources, currentInstance);
|
|
723
875
|
}
|
|
724
876
|
const nextConfig = {
|
|
725
877
|
...state.config,
|
|
726
878
|
brokerInstances: nextInstances,
|
|
727
879
|
};
|
|
728
880
|
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
729
|
-
await
|
|
881
|
+
await saveConfigImmediately(nextConfig);
|
|
730
882
|
pluginRegistry.events.emit("config:changed", { config: nextConfig });
|
|
731
883
|
};
|
|
732
884
|
pluginRegistry.syncBrokerInstanceFn = async (instanceId) => {
|
|
@@ -736,7 +888,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
736
888
|
const instance = getBrokerInstance(state.config.brokerInstances, instanceId);
|
|
737
889
|
if (!instance) return;
|
|
738
890
|
|
|
739
|
-
|
|
891
|
+
clearPersistedBrokerAccounts(pluginRegistry.persistence.resources, instance);
|
|
740
892
|
|
|
741
893
|
const broker = pluginRegistry.brokers.get(instance.brokerType);
|
|
742
894
|
await broker?.disconnect?.(instance).catch(() => {});
|
|
@@ -793,16 +945,23 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
793
945
|
|
|
794
946
|
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
795
947
|
dispatch({ type: "SET_TICKERS", tickers: nextTickers });
|
|
796
|
-
await
|
|
948
|
+
await saveConfigImmediately(nextConfig);
|
|
797
949
|
pluginRegistry.events.emit("config:changed", { config: nextConfig });
|
|
798
950
|
};
|
|
799
951
|
|
|
800
952
|
// Wire up navigation functions
|
|
801
953
|
pluginRegistry.selectTickerFn = (symbol, paneId) => selectTickerInPane(symbol, paneId);
|
|
802
|
-
pluginRegistry.switchPanelFn = (panel) =>
|
|
954
|
+
pluginRegistry.switchPanelFn = (panel) => {
|
|
955
|
+
if (isDetachedWindow) return;
|
|
956
|
+
dispatch({ type: "SET_ACTIVE_PANEL", panel });
|
|
957
|
+
};
|
|
803
958
|
pluginRegistry.switchTabFn = (tabId, paneId) => switchDetailTab(tabId, paneId);
|
|
804
|
-
pluginRegistry.openCommandBarFn = (query) =>
|
|
959
|
+
pluginRegistry.openCommandBarFn = (query) => {
|
|
960
|
+
if (isDetachedWindow) return;
|
|
961
|
+
dispatch({ type: "SET_COMMAND_BAR", open: true, query });
|
|
962
|
+
};
|
|
805
963
|
pluginRegistry.openPluginCommandWorkflowFn = (commandId) => {
|
|
964
|
+
if (isDetachedWindow) return;
|
|
806
965
|
dispatch({
|
|
807
966
|
type: "SET_COMMAND_BAR",
|
|
808
967
|
open: true,
|
|
@@ -820,7 +979,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
820
979
|
dispatch({ type: "PUSH_LAYOUT_HISTORY" });
|
|
821
980
|
}
|
|
822
981
|
dispatch({ type: "UPDATE_LAYOUT", layout: normalizedLayout });
|
|
823
|
-
|
|
982
|
+
scheduleConfigSave({ ...currentState.config, layout: normalizedLayout, layouts });
|
|
824
983
|
};
|
|
825
984
|
const resolvePanelForPane = useCallback((paneId: string, layout: LayoutConfig = state.config.layout): "left" | "right" => {
|
|
826
985
|
const instanceId = resolvePaneTarget(paneId, layout);
|
|
@@ -1022,10 +1181,17 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1022
1181
|
}, [dataProvider, dialog, dispatch, persistLayout, pluginRegistry, resolvePaneTarget, tickerRepository]);
|
|
1023
1182
|
|
|
1024
1183
|
pluginRegistry.getLayoutFn = () => state.config.layout;
|
|
1025
|
-
pluginRegistry.updateLayoutFn = (layout) =>
|
|
1184
|
+
pluginRegistry.updateLayoutFn = (layout) => {
|
|
1185
|
+
if (isDetachedWindow) return;
|
|
1186
|
+
persistLayout(layout);
|
|
1187
|
+
};
|
|
1026
1188
|
pluginRegistry.openPaneSettingsFn = (paneId) => { void openPaneSettings(paneId); };
|
|
1027
|
-
pluginRegistry.showPaneFn = (paneId) =>
|
|
1189
|
+
pluginRegistry.showPaneFn = (paneId) => {
|
|
1190
|
+
if (isDetachedWindow) return;
|
|
1191
|
+
showPane(paneId);
|
|
1192
|
+
};
|
|
1028
1193
|
pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
|
|
1194
|
+
if (isDetachedWindow) return;
|
|
1029
1195
|
await createPaneTemplateOrThrow(templateId, options, {
|
|
1030
1196
|
dataProvider,
|
|
1031
1197
|
tickerRepository,
|
|
@@ -1036,7 +1202,10 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1036
1202
|
placePaneInstance,
|
|
1037
1203
|
});
|
|
1038
1204
|
};
|
|
1039
|
-
pluginRegistry.createPaneFromTemplateFn = (templateId, options) => {
|
|
1205
|
+
pluginRegistry.createPaneFromTemplateFn = (templateId, options) => {
|
|
1206
|
+
if (isDetachedWindow) return;
|
|
1207
|
+
void createPaneFromTemplate(templateId, options);
|
|
1208
|
+
};
|
|
1040
1209
|
pluginRegistry.applyPaneSettingValueFn = async (paneId, field, value) => {
|
|
1041
1210
|
await applyPaneSettingFieldValueShared(paneId, field, value, {
|
|
1042
1211
|
dataProvider,
|
|
@@ -1048,11 +1217,18 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1048
1217
|
});
|
|
1049
1218
|
};
|
|
1050
1219
|
pluginRegistry.hidePaneFn = (paneId) => {
|
|
1220
|
+
if (isDetachedWindow) return;
|
|
1051
1221
|
const instanceId = resolvePaneTarget(paneId);
|
|
1052
1222
|
if (!instanceId || !isPaneInLayout(state.config.layout, instanceId)) return;
|
|
1053
1223
|
persistLayout(removePane(state.config.layout, instanceId));
|
|
1054
1224
|
};
|
|
1055
1225
|
pluginRegistry.focusPaneFn = (paneId) => {
|
|
1226
|
+
if (isDetachedWindow) {
|
|
1227
|
+
if (paneId === detachedPaneId) {
|
|
1228
|
+
dispatch({ type: "FOCUS_PANE", paneId });
|
|
1229
|
+
}
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1056
1232
|
const instanceId = resolvePaneTarget(paneId);
|
|
1057
1233
|
if (!instanceId || !isPaneInLayout(state.config.layout, instanceId)) {
|
|
1058
1234
|
showPane(paneId);
|
|
@@ -1069,6 +1245,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1069
1245
|
activatePane(instanceId, layout);
|
|
1070
1246
|
};
|
|
1071
1247
|
pluginRegistry.pinTickerFn = (symbol, options) => {
|
|
1248
|
+
if (isDetachedWindow) return;
|
|
1072
1249
|
const paneType = options?.paneType ?? "ticker-detail";
|
|
1073
1250
|
const paneDef = pluginRegistry.panes.get(paneType);
|
|
1074
1251
|
if (!paneDef) return;
|
|
@@ -1096,6 +1273,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1096
1273
|
};
|
|
1097
1274
|
|
|
1098
1275
|
pluginRegistry.navigateTickerFn = (rawSymbol) => {
|
|
1276
|
+
if (isDetachedWindow) return;
|
|
1099
1277
|
(async () => {
|
|
1100
1278
|
try {
|
|
1101
1279
|
// Resolve or create the ticker in the local database
|
|
@@ -1170,7 +1348,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1170
1348
|
activatePane(detailPane.instanceId, nextLayout);
|
|
1171
1349
|
}
|
|
1172
1350
|
} else {
|
|
1173
|
-
pluginRegistry.
|
|
1351
|
+
pluginRegistry.pinTicker(symbol, { floating: false });
|
|
1174
1352
|
}
|
|
1175
1353
|
} catch (err) {
|
|
1176
1354
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1194,7 +1372,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1194
1372
|
useEffect(() => {
|
|
1195
1373
|
if (state.config.layouts !== prevLayouts.current) {
|
|
1196
1374
|
prevLayouts.current = state.config.layouts;
|
|
1197
|
-
|
|
1375
|
+
scheduleConfigSave(state.config);
|
|
1198
1376
|
}
|
|
1199
1377
|
}, [state.config.layouts, state.config]);
|
|
1200
1378
|
|
|
@@ -1216,13 +1394,13 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1216
1394
|
const focusedDetailTab = state.focusedPaneId ? state.paneState[state.focusedPaneId]?.activeTabId : undefined;
|
|
1217
1395
|
|
|
1218
1396
|
// Global keyboard shortcuts
|
|
1219
|
-
|
|
1220
|
-
if (isCopyShortcut(event) && copyActiveSelection(
|
|
1397
|
+
useShortcut((event) => {
|
|
1398
|
+
if (isCopyShortcut(event) && copyActiveSelection(nativeRenderer)) {
|
|
1221
1399
|
event.preventDefault();
|
|
1222
1400
|
event.stopPropagation();
|
|
1223
1401
|
return;
|
|
1224
1402
|
}
|
|
1225
|
-
if (isPasteShortcut(event) && pasteSystemClipboard(
|
|
1403
|
+
if (isPasteShortcut(event) && pasteSystemClipboard(nativeRenderer)) {
|
|
1226
1404
|
event.preventDefault();
|
|
1227
1405
|
event.stopPropagation();
|
|
1228
1406
|
return;
|
|
@@ -1231,18 +1409,25 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1231
1409
|
// Skip all global shortcuts when a dialog is open
|
|
1232
1410
|
if (dialogOpen) return;
|
|
1233
1411
|
|
|
1234
|
-
// Ctrl+P: toggle command bar (backtick handled in command-bar
|
|
1235
|
-
if (
|
|
1412
|
+
// Ctrl+P / Cmd+K: toggle command bar (backtick close is handled in command-bar.tsx)
|
|
1413
|
+
if (!isDetachedWindow && (
|
|
1414
|
+
(event.name === "p" && event.ctrl)
|
|
1415
|
+
|| (event.name === "k" && (event.meta || event.super))
|
|
1416
|
+
)) {
|
|
1417
|
+
event.preventDefault();
|
|
1418
|
+
event.stopPropagation();
|
|
1236
1419
|
dispatch({ type: "TOGGLE_COMMAND_BAR" });
|
|
1237
1420
|
return;
|
|
1238
1421
|
}
|
|
1239
1422
|
// Backtick opens command bar (close is handled in command-bar.tsx)
|
|
1240
|
-
if (event.name === "`" && !state.commandBarOpen) {
|
|
1423
|
+
if (!isDetachedWindow && event.name === "`" && !state.commandBarOpen) {
|
|
1424
|
+
event.preventDefault();
|
|
1425
|
+
event.stopPropagation();
|
|
1241
1426
|
dispatch({ type: "SET_COMMAND_BAR", open: true, query: "" });
|
|
1242
1427
|
return;
|
|
1243
1428
|
}
|
|
1244
1429
|
// Ctrl+1-9: switch layouts (works even when input is captured)
|
|
1245
|
-
if (/^[1-9]$/.test(event.name ?? "") && event.ctrl && (state.config.layouts ?? []).length > 1) {
|
|
1430
|
+
if (!isDetachedWindow && /^[1-9]$/.test(event.name ?? "") && event.ctrl && (state.config.layouts ?? []).length > 1) {
|
|
1246
1431
|
const idx = parseInt(event.name!, 10) - 1;
|
|
1247
1432
|
const layouts = state.config.layouts ?? [];
|
|
1248
1433
|
if (idx < layouts.length && idx !== state.config.activeLayoutIndex) {
|
|
@@ -1268,8 +1453,8 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1268
1453
|
} else {
|
|
1269
1454
|
dispatch({ type: "FOCUS_NEXT", paneOrder });
|
|
1270
1455
|
}
|
|
1271
|
-
} else if (event.name === "q" && !(focusedPane?.paneId === "ticker-detail" && focusedDetailTab === "financials")) {
|
|
1272
|
-
|
|
1456
|
+
} else if (!isDetachedWindow && event.name === "q" && !(focusedPane?.paneId === "ticker-detail" && focusedDetailTab === "financials")) {
|
|
1457
|
+
rendererHost.requestExit();
|
|
1273
1458
|
} else if (event.name === "r") {
|
|
1274
1459
|
// Refresh focused ticker context.
|
|
1275
1460
|
if (focusedTickerSymbol) {
|
|
@@ -1281,7 +1466,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1281
1466
|
for (const t of state.tickers.values()) {
|
|
1282
1467
|
refreshTicker(t.metadata.ticker, t.metadata.exchange, t, 1);
|
|
1283
1468
|
}
|
|
1284
|
-
} else if (event.name === "a" && focusedTickerSymbol) {
|
|
1469
|
+
} else if (!isDetachedWindow && event.name === "a" && focusedTickerSymbol) {
|
|
1285
1470
|
// Open ticker actions
|
|
1286
1471
|
const actions = [...pluginRegistry.tickerActions.values()];
|
|
1287
1472
|
const ticker = state.tickers.get(focusedTickerSymbol);
|
|
@@ -1306,53 +1491,90 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1306
1491
|
}
|
|
1307
1492
|
});
|
|
1308
1493
|
|
|
1494
|
+
if (desktopWindowBridge?.kind === "detached" && desktopWindowBridge.paneId) {
|
|
1495
|
+
return (
|
|
1496
|
+
<ContextMenuProvider pluginRegistry={pluginRegistry}>
|
|
1497
|
+
<Box flexDirection="column" flexGrow={1} backgroundColor={colors.bg}>
|
|
1498
|
+
<DetachedPaneShell
|
|
1499
|
+
pluginRegistry={pluginRegistry}
|
|
1500
|
+
desktopWindowBridge={{ ...desktopWindowBridge, kind: "detached", paneId: desktopWindowBridge.paneId }}
|
|
1501
|
+
/>
|
|
1502
|
+
<ToastViewport position="bottom-right" />
|
|
1503
|
+
</Box>
|
|
1504
|
+
</ContextMenuProvider>
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1309
1508
|
return (
|
|
1310
|
-
<
|
|
1311
|
-
<
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
{state.commandBarOpen && (
|
|
1315
|
-
<CommandBar
|
|
1316
|
-
dataProvider={dataProvider}
|
|
1317
|
-
tickerRepository={tickerRepository}
|
|
1509
|
+
<ContextMenuProvider pluginRegistry={pluginRegistry}>
|
|
1510
|
+
<Box flexDirection="column" flexGrow={1} backgroundColor={colors.bg}>
|
|
1511
|
+
<Header />
|
|
1512
|
+
<Shell
|
|
1318
1513
|
pluginRegistry={pluginRegistry}
|
|
1319
|
-
|
|
1320
|
-
|
|
1514
|
+
desktopWindowBridge={desktopWindowBridge}
|
|
1515
|
+
desktopDockPreview={desktopDockPreview}
|
|
1321
1516
|
/>
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1517
|
+
<StatusBar />
|
|
1518
|
+
{state.commandBarOpen && (
|
|
1519
|
+
<CommandBar
|
|
1520
|
+
dataProvider={dataProvider}
|
|
1521
|
+
tickerRepository={tickerRepository}
|
|
1522
|
+
pluginRegistry={pluginRegistry}
|
|
1523
|
+
quitApp={() => rendererHost.requestExit()}
|
|
1524
|
+
onCheckForUpdates={() => runUpdateCheck(true)}
|
|
1525
|
+
/>
|
|
1526
|
+
)}
|
|
1527
|
+
<ToastViewport position="bottom-right" />
|
|
1528
|
+
</Box>
|
|
1529
|
+
</ContextMenuProvider>
|
|
1325
1530
|
);
|
|
1326
1531
|
}
|
|
1327
1532
|
|
|
1328
1533
|
interface AppProps {
|
|
1329
1534
|
config: AppConfig;
|
|
1330
|
-
renderer: CliRenderer;
|
|
1331
1535
|
externalPlugins?: import("./plugins/loader").LoadedExternalPlugin[];
|
|
1332
1536
|
cliLaunchRequest?: CliLaunchRequest | null;
|
|
1537
|
+
desktopWindowBridge?: DesktopWindowBridge;
|
|
1538
|
+
desktopApplicationMenuBridge?: DesktopApplicationMenuBridge;
|
|
1539
|
+
desktopSnapshot?: DesktopSharedStateSnapshot | null;
|
|
1333
1540
|
}
|
|
1334
1541
|
|
|
1335
1542
|
export function App({
|
|
1336
1543
|
config: initialConfig,
|
|
1337
|
-
renderer,
|
|
1338
1544
|
externalPlugins = [],
|
|
1339
1545
|
cliLaunchRequest = null,
|
|
1546
|
+
desktopWindowBridge,
|
|
1547
|
+
desktopApplicationMenuBridge,
|
|
1548
|
+
desktopSnapshot = null,
|
|
1340
1549
|
}: AppProps) {
|
|
1550
|
+
const renderer = useNativeRenderer();
|
|
1551
|
+
const effectiveInitialConfig = useMemo(() => {
|
|
1552
|
+
const baseConfig = desktopSnapshot?.config ?? initialConfig;
|
|
1553
|
+
if (desktopWindowBridge) return baseConfig;
|
|
1554
|
+
return {
|
|
1555
|
+
...baseConfig,
|
|
1556
|
+
layout: materializeDetachedPanesAsFloating(baseConfig.layout),
|
|
1557
|
+
layouts: baseConfig.layouts.map((entry) => ({
|
|
1558
|
+
...entry,
|
|
1559
|
+
layout: materializeDetachedPanesAsFloating(entry.layout),
|
|
1560
|
+
})),
|
|
1561
|
+
};
|
|
1562
|
+
}, [desktopSnapshot?.config, desktopWindowBridge, initialConfig]);
|
|
1341
1563
|
const initialCliLaunch = useMemo(() => {
|
|
1342
1564
|
if (!cliLaunchRequest) {
|
|
1343
|
-
return { config:
|
|
1565
|
+
return { config: effectiveInitialConfig, launchState: undefined };
|
|
1344
1566
|
}
|
|
1345
|
-
return cliLaunchRequest.applyConfig(
|
|
1567
|
+
return cliLaunchRequest.applyConfig(effectiveInitialConfig, {
|
|
1346
1568
|
terminalWidth: renderer.terminalWidth,
|
|
1347
1569
|
terminalHeight: renderer.terminalHeight,
|
|
1348
1570
|
});
|
|
1349
|
-
}, [cliLaunchRequest,
|
|
1571
|
+
}, [cliLaunchRequest, effectiveInitialConfig, renderer.terminalHeight, renderer.terminalWidth]);
|
|
1350
1572
|
const cliLaunchStateRef = useRef(initialCliLaunch.launchState);
|
|
1351
1573
|
|
|
1352
1574
|
const [config, setConfig] = useState(() => {
|
|
1353
1575
|
return initialCliLaunch.config;
|
|
1354
1576
|
});
|
|
1355
|
-
const [showOnboarding, setShowOnboarding] = useState(!
|
|
1577
|
+
const [showOnboarding, setShowOnboarding] = useState(!effectiveInitialConfig.onboardingComplete);
|
|
1356
1578
|
|
|
1357
1579
|
// Keep the shared palette aligned before this render builds any JSX that reads `colors`.
|
|
1358
1580
|
syncTheme(config.theme);
|
|
@@ -1360,44 +1582,44 @@ export function App({
|
|
|
1360
1582
|
useEffect(() => bindAppActivity(renderer), [renderer]);
|
|
1361
1583
|
|
|
1362
1584
|
const services = useMemo(() => {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
pluginRegistry.getConfigFn = () => config;
|
|
1372
|
-
pluginRegistry.getLayoutFn = () => config.layout;
|
|
1373
|
-
|
|
1374
|
-
for (const plugin of getLoadablePlugins(externalPlugins)) {
|
|
1375
|
-
pluginRegistry.register(plugin);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
return {
|
|
1379
|
-
persistence,
|
|
1380
|
-
tickerRepository,
|
|
1381
|
-
providerRouter,
|
|
1382
|
-
dataProvider,
|
|
1383
|
-
marketData,
|
|
1384
|
-
pluginRegistry,
|
|
1385
|
-
};
|
|
1386
|
-
}, [config.dataDir, externalPlugins, renderer]);
|
|
1387
|
-
|
|
1388
|
-
if (services.marketData !== null) {
|
|
1389
|
-
setSharedMarketDataCoordinator(services.marketData);
|
|
1390
|
-
}
|
|
1585
|
+
return measurePerf("startup.app.create-services", () => (
|
|
1586
|
+
createAppServices({ config, externalPlugins })
|
|
1587
|
+
), {
|
|
1588
|
+
externalPluginCount: externalPlugins.length,
|
|
1589
|
+
disabledPluginCount: config.disabledPlugins.length,
|
|
1590
|
+
brokerInstanceCount: config.brokerInstances.length,
|
|
1591
|
+
});
|
|
1592
|
+
}, [config.dataDir, externalPlugins]);
|
|
1391
1593
|
|
|
1392
1594
|
useEffect(() => {
|
|
1393
|
-
return () =>
|
|
1394
|
-
setSharedMarketDataCoordinator(null);
|
|
1395
|
-
services.pluginRegistry.destroy();
|
|
1396
|
-
services.persistence.close();
|
|
1397
|
-
};
|
|
1595
|
+
return () => services.destroy();
|
|
1398
1596
|
}, [services]);
|
|
1399
1597
|
|
|
1400
1598
|
const sessionSnapshot = useMemo(() => {
|
|
1599
|
+
if (desktopWindowBridge?.kind === "detached") {
|
|
1600
|
+
const baseSessionSnapshot = reconcileAppSessionSnapshot(
|
|
1601
|
+
config,
|
|
1602
|
+
services.persistence.sessions.get<AppSessionSnapshot>(APP_SESSION_ID, APP_SESSION_SCHEMA_VERSION)?.value ?? null,
|
|
1603
|
+
) ?? {
|
|
1604
|
+
paneState: {},
|
|
1605
|
+
focusedPaneId: null,
|
|
1606
|
+
activePanel: "left" as const,
|
|
1607
|
+
statusBarVisible: true,
|
|
1608
|
+
openPaneIds: [],
|
|
1609
|
+
hydrationTargets: [],
|
|
1610
|
+
exchangeCurrencies: [],
|
|
1611
|
+
savedAt: Date.now(),
|
|
1612
|
+
};
|
|
1613
|
+
return desktopSnapshot
|
|
1614
|
+
? {
|
|
1615
|
+
...baseSessionSnapshot,
|
|
1616
|
+
paneState: desktopSnapshot.paneState,
|
|
1617
|
+
focusedPaneId: desktopSnapshot.focusedPaneId,
|
|
1618
|
+
activePanel: desktopSnapshot.activePanel,
|
|
1619
|
+
statusBarVisible: desktopSnapshot.statusBarVisible,
|
|
1620
|
+
}
|
|
1621
|
+
: null;
|
|
1622
|
+
}
|
|
1401
1623
|
const persisted = services.persistence.sessions.get<AppSessionSnapshot>(APP_SESSION_ID, APP_SESSION_SCHEMA_VERSION)?.value ?? null;
|
|
1402
1624
|
const reconciled = reconcileAppSessionSnapshot(config, persisted);
|
|
1403
1625
|
if (!cliLaunchRequest?.applySessionSnapshot) {
|
|
@@ -1408,7 +1630,7 @@ export function App({
|
|
|
1408
1630
|
reconciled,
|
|
1409
1631
|
cliLaunchStateRef.current,
|
|
1410
1632
|
);
|
|
1411
|
-
}, [cliLaunchRequest, config, services.persistence.sessions]);
|
|
1633
|
+
}, [cliLaunchRequest, config, desktopSnapshot, desktopWindowBridge?.kind, services.persistence.sessions]);
|
|
1412
1634
|
|
|
1413
1635
|
if (showOnboarding) {
|
|
1414
1636
|
return (
|
|
@@ -1424,21 +1646,22 @@ export function App({
|
|
|
1424
1646
|
}
|
|
1425
1647
|
|
|
1426
1648
|
return (
|
|
1427
|
-
<AppProvider
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1649
|
+
<AppProvider
|
|
1650
|
+
config={config}
|
|
1651
|
+
sessionStore={desktopWindowBridge?.kind === "detached" ? undefined : services.persistence.sessions}
|
|
1652
|
+
sessionSnapshot={sessionSnapshot}
|
|
1653
|
+
desktopBridge={desktopWindowBridge}
|
|
1654
|
+
desktopSnapshot={desktopSnapshot}
|
|
1655
|
+
>
|
|
1656
|
+
<AppInner
|
|
1657
|
+
pluginRegistry={services.pluginRegistry}
|
|
1658
|
+
tickerRepository={services.tickerRepository}
|
|
1659
|
+
dataProvider={services.dataProvider}
|
|
1660
|
+
marketData={services.marketData}
|
|
1661
|
+
sessionSnapshot={sessionSnapshot}
|
|
1662
|
+
desktopWindowBridge={desktopWindowBridge}
|
|
1663
|
+
desktopApplicationMenuBridge={desktopApplicationMenuBridge}
|
|
1664
|
+
/>
|
|
1442
1665
|
</AppProvider>
|
|
1443
1666
|
);
|
|
1444
1667
|
}
|