gloomberb 0.5.1 → 0.6.0
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 +12 -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 +304 -108
- 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 +30 -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/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 +438 -141
- 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 +40 -0
- package/src/components/layout/pane-footer.test.tsx +195 -0
- package/src/components/layout/pane-footer.tsx +410 -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-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 +75 -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 +135 -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 +16 -10
- package/src/plugins/builtin/ticker-detail.test.tsx +156 -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 +2 -2
- package/src/plugins/ibkr/trade-tab.tsx +164 -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 +53 -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 +65 -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 +1490 -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 +134 -0
- package/src/renderers/opentui/input-host.tsx +24 -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 +97 -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 +205 -869
- package/src/state/config-save-scheduler.ts +35 -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 +361 -0
- package/src/ui/index.tsx +126 -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
|
@@ -72,6 +72,7 @@ describe("sanitizeLayout", () => {
|
|
|
72
72
|
},
|
|
73
73
|
],
|
|
74
74
|
floating: [],
|
|
75
|
+
detached: [],
|
|
75
76
|
}, DEFAULT_LAYOUT);
|
|
76
77
|
|
|
77
78
|
expect(findPaneInstance(layout, "ticker-detail:main")?.binding).toEqual({
|
|
@@ -92,6 +93,7 @@ describe("sanitizeLayout", () => {
|
|
|
92
93
|
},
|
|
93
94
|
],
|
|
94
95
|
floating: [],
|
|
96
|
+
detached: [],
|
|
95
97
|
}, DEFAULT_LAYOUT);
|
|
96
98
|
|
|
97
99
|
expect(findPaneInstance(layout, "ticker-detail:main")).toBeUndefined();
|
|
@@ -141,6 +143,7 @@ describe("sanitizeLayout", () => {
|
|
|
141
143
|
},
|
|
142
144
|
],
|
|
143
145
|
floating: [],
|
|
146
|
+
detached: [],
|
|
144
147
|
}, DEFAULT_LAYOUT);
|
|
145
148
|
|
|
146
149
|
expect(layout.dockRoot?.kind).toBe("split");
|
|
@@ -168,6 +171,7 @@ describe("sanitizeLayout", () => {
|
|
|
168
171
|
],
|
|
169
172
|
docked: [{ instanceId: "portfolio-list:main", columnIndex: 0 }],
|
|
170
173
|
floating: [],
|
|
174
|
+
detached: [],
|
|
171
175
|
}, DEFAULT_LAYOUT);
|
|
172
176
|
|
|
173
177
|
expect(layout).toEqual(DEFAULT_LAYOUT);
|
|
@@ -175,6 +179,25 @@ describe("sanitizeLayout", () => {
|
|
|
175
179
|
});
|
|
176
180
|
|
|
177
181
|
describe("loadConfig", () => {
|
|
182
|
+
test("defaults detached layouts to an empty list for older configs", async () => {
|
|
183
|
+
const dataDir = await createTempConfigDir();
|
|
184
|
+
const layoutWithoutDetached = {
|
|
185
|
+
...DEFAULT_LAYOUT,
|
|
186
|
+
detached: undefined,
|
|
187
|
+
};
|
|
188
|
+
await writeConfigJson(dataDir, createSavedConfig({
|
|
189
|
+
configVersion: CURRENT_CONFIG_VERSION - 1,
|
|
190
|
+
layout: layoutWithoutDetached,
|
|
191
|
+
layouts: [{ name: "Default", layout: layoutWithoutDetached }],
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
const config = await loadConfig(dataDir);
|
|
195
|
+
|
|
196
|
+
expect(config.configVersion).toBe(CURRENT_CONFIG_VERSION);
|
|
197
|
+
expect(config.layout.detached).toEqual([]);
|
|
198
|
+
expect(config.layouts[0]?.layout.detached).toEqual([]);
|
|
199
|
+
});
|
|
200
|
+
|
|
178
201
|
test("fills in missing chart preferences for older configs", async () => {
|
|
179
202
|
const dataDir = await createTempConfigDir();
|
|
180
203
|
await writeConfigJson(dataDir, createSavedConfig({
|
|
@@ -243,6 +266,54 @@ describe("loadConfig", () => {
|
|
|
243
266
|
expect(config.disabledPlugins).toEqual(["chat", "news"]);
|
|
244
267
|
});
|
|
245
268
|
|
|
269
|
+
test("migrates disabled built-in feature plugins to grouped plugin ids", async () => {
|
|
270
|
+
const dataDir = await createTempConfigDir();
|
|
271
|
+
await writeConfigJson(dataDir, createSavedConfig({
|
|
272
|
+
configVersion: CURRENT_CONFIG_VERSION - 1,
|
|
273
|
+
disabledPlugins: ["options", "sec", "world-indices", "earnings-calendar", "options"],
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
const config = await loadConfig(dataDir);
|
|
277
|
+
|
|
278
|
+
expect(config.disabledPlugins).toEqual(["company-research", "market-overview", "macro"]);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("migrates grouped built-in plugin config keys", async () => {
|
|
282
|
+
const dataDir = await createTempConfigDir();
|
|
283
|
+
await writeConfigJson(dataDir, createSavedConfig({
|
|
284
|
+
configVersion: CURRENT_CONFIG_VERSION - 1,
|
|
285
|
+
pluginConfig: {
|
|
286
|
+
options: {
|
|
287
|
+
selectedExpiration: "2026-06-19",
|
|
288
|
+
},
|
|
289
|
+
"company-research": {
|
|
290
|
+
preferredTab: "analyst-research",
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
const config = await loadConfig(dataDir);
|
|
296
|
+
|
|
297
|
+
expect(config.pluginConfig).toEqual({
|
|
298
|
+
"company-research": {
|
|
299
|
+
selectedExpiration: "2026-06-19",
|
|
300
|
+
preferredTab: "analyst-research",
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("enables Gloomberb Cloud when migrating older default configs", async () => {
|
|
306
|
+
const dataDir = await createTempConfigDir();
|
|
307
|
+
await writeConfigJson(dataDir, createSavedConfig({
|
|
308
|
+
configVersion: 12,
|
|
309
|
+
disabledPlugins: ["gloomberb-cloud", "news"],
|
|
310
|
+
}));
|
|
311
|
+
|
|
312
|
+
const config = await loadConfig(dataDir);
|
|
313
|
+
|
|
314
|
+
expect(config.disabledPlugins).toEqual(["news"]);
|
|
315
|
+
});
|
|
316
|
+
|
|
246
317
|
test("preserves IBKR gateway configs without migration rewrites", async () => {
|
|
247
318
|
const dataDir = await createTempConfigDir();
|
|
248
319
|
await writeConfigJson(dataDir, createSavedConfig({
|
|
@@ -315,6 +386,7 @@ describe("loadConfig", () => {
|
|
|
315
386
|
],
|
|
316
387
|
docked: [{ instanceId: "portfolio-list:main", columnIndex: 0 }],
|
|
317
388
|
floating: [],
|
|
389
|
+
detached: [],
|
|
318
390
|
};
|
|
319
391
|
|
|
320
392
|
await writeConfigJson(dataDir, createSavedConfig({
|
package/src/data/config-store.ts
CHANGED
|
@@ -1,507 +1,64 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join } from "path";
|
|
3
|
-
import type {
|
|
4
|
-
AppConfig,
|
|
5
|
-
BrokerInstanceConfig,
|
|
6
|
-
ChartPreferences,
|
|
7
|
-
FloatingPlacementMemory,
|
|
8
|
-
LayoutConfig,
|
|
9
|
-
PanePlacementMemory,
|
|
10
|
-
PaneBinding,
|
|
11
|
-
PaneInstanceConfig,
|
|
12
|
-
SavedLayout,
|
|
13
|
-
} from "../types/config";
|
|
14
|
-
import {
|
|
15
|
-
cloneLayout,
|
|
16
|
-
createDefaultConfig,
|
|
17
|
-
createPaneInstanceId,
|
|
18
|
-
CURRENT_CONFIG_VERSION,
|
|
19
|
-
DEFAULT_COLUMNS,
|
|
20
|
-
DEFAULT_PORTFOLIO_COLUMN_IDS,
|
|
21
|
-
clonePaneSettings,
|
|
22
|
-
normalizePaneLayout,
|
|
23
|
-
} from "../types/config";
|
|
24
|
-
import type { Portfolio, Watchlist } from "../types/ticker";
|
|
25
|
-
import { debugLog } from "../utils/debug-log";
|
|
1
|
+
import type { AppConfig } from "../types/config";
|
|
26
2
|
|
|
27
|
-
|
|
28
|
-
const LEGACY_MAIN_PORTFOLIO_COLUMN_IDS = DEFAULT_COLUMNS.map((column) => column.id);
|
|
3
|
+
export { sanitizeLayout } from "./config-layout";
|
|
29
4
|
|
|
30
|
-
|
|
31
|
-
|
|
5
|
+
export interface ConfigStoreHost {
|
|
6
|
+
getDataDir(): Promise<string | null>;
|
|
7
|
+
loadConfig(dataDir: string): Promise<AppConfig>;
|
|
8
|
+
saveConfig(config: AppConfig): Promise<void>;
|
|
9
|
+
initDataDir(dataDir: string): Promise<AppConfig>;
|
|
10
|
+
resetAllData(dataDir: string): Promise<void>;
|
|
11
|
+
exportConfig(config: AppConfig, destPath: string): Promise<void>;
|
|
12
|
+
importConfig(dataDir: string, srcPath: string): Promise<AppConfig>;
|
|
32
13
|
}
|
|
33
14
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
15
|
+
let configuredHost: ConfigStoreHost | null = null;
|
|
16
|
+
let nodeHostPromise: Promise<ConfigStoreHost> | null = null;
|
|
37
17
|
|
|
38
|
-
export
|
|
39
|
-
|
|
40
|
-
const raw = await readFile(getGlobalConfigFile(), "utf-8");
|
|
41
|
-
const config = JSON.parse(raw) as { dataDir?: string };
|
|
42
|
-
return config.dataDir || null;
|
|
43
|
-
} catch {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
18
|
+
export function setConfigStoreHost(host: ConfigStoreHost | null): void {
|
|
19
|
+
configuredHost = host;
|
|
46
20
|
}
|
|
47
21
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
22
|
+
async function loadNodeHost(): Promise<ConfigStoreHost> {
|
|
23
|
+
if (!nodeHostPromise) {
|
|
24
|
+
const modulePath = "./config-store-node";
|
|
25
|
+
nodeHostPromise = import(modulePath) as Promise<ConfigStoreHost>;
|
|
26
|
+
}
|
|
27
|
+
return nodeHostPromise;
|
|
52
28
|
}
|
|
53
29
|
|
|
54
|
-
async function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const raw = await readFile(configPath, "utf-8");
|
|
59
|
-
const saved = JSON.parse(raw) as Record<string, unknown>;
|
|
60
|
-
return normalizeConfig(saved, dataDir);
|
|
61
|
-
} catch {
|
|
62
|
-
return { config: createDefaultConfig(dataDir), needsSave: true };
|
|
30
|
+
async function getHost(): Promise<ConfigStoreHost> {
|
|
31
|
+
if (configuredHost) return configuredHost;
|
|
32
|
+
if (typeof Bun === "undefined") {
|
|
33
|
+
throw new Error("Config store host is not configured for this renderer.");
|
|
63
34
|
}
|
|
35
|
+
return loadNodeHost();
|
|
64
36
|
}
|
|
65
37
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
typeof saved.configVersion !== "number" || saved.configVersion < CURRENT_CONFIG_VERSION;
|
|
70
|
-
const directLayout = migrateLegacyPortfolioDefaultColumns(
|
|
71
|
-
sanitizeLayout(saved.layout, defaults.layout),
|
|
72
|
-
shouldMigratePortfolioDefaults,
|
|
73
|
-
);
|
|
74
|
-
const layouts = sanitizeSavedLayouts(saved.layouts, directLayout).map((entry) => ({
|
|
75
|
-
...entry,
|
|
76
|
-
layout: migrateLegacyPortfolioDefaultColumns(entry.layout, shouldMigratePortfolioDefaults),
|
|
77
|
-
}));
|
|
78
|
-
const activeLayoutIndex = sanitizeActiveLayoutIndex(saved.activeLayoutIndex, layouts.length);
|
|
79
|
-
const layout = cloneLayout(layouts[activeLayoutIndex]?.layout ?? directLayout);
|
|
80
|
-
const syncedLayouts = layouts.map((entry, index) => (
|
|
81
|
-
index === activeLayoutIndex ? { ...entry, layout: cloneLayout(layout) } : entry
|
|
82
|
-
));
|
|
83
|
-
|
|
84
|
-
const config: AppConfig = {
|
|
85
|
-
dataDir,
|
|
86
|
-
configVersion: CURRENT_CONFIG_VERSION,
|
|
87
|
-
baseCurrency: typeof saved.baseCurrency === "string" ? saved.baseCurrency : defaults.baseCurrency,
|
|
88
|
-
refreshIntervalMinutes: typeof saved.refreshIntervalMinutes === "number" ? saved.refreshIntervalMinutes : defaults.refreshIntervalMinutes,
|
|
89
|
-
portfolios: sanitizePortfolios(saved.portfolios, defaults.portfolios),
|
|
90
|
-
watchlists: sanitizeWatchlists(saved.watchlists, defaults.watchlists),
|
|
91
|
-
layout,
|
|
92
|
-
layouts: syncedLayouts,
|
|
93
|
-
activeLayoutIndex,
|
|
94
|
-
brokerInstances: sanitizeBrokerInstances(saved.brokerInstances),
|
|
95
|
-
plugins: sanitizeStringArray(saved.plugins, defaults.plugins),
|
|
96
|
-
disabledPlugins: sanitizeDisabledPlugins(saved, defaults.disabledPlugins),
|
|
97
|
-
pluginConfig: sanitizePluginConfig(saved.pluginConfig),
|
|
98
|
-
theme: typeof saved.theme === "string" ? saved.theme : defaults.theme,
|
|
99
|
-
chartPreferences: sanitizeChartPreferences(saved.chartPreferences, defaults.chartPreferences),
|
|
100
|
-
recentTickers: sanitizeStringArray(saved.recentTickers, defaults.recentTickers),
|
|
101
|
-
onboardingComplete: typeof saved.onboardingComplete === "boolean" ? saved.onboardingComplete : defaults.onboardingComplete,
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const needsSave =
|
|
105
|
-
saved.configVersion !== CURRENT_CONFIG_VERSION
|
|
106
|
-
|| !isLayoutConfig(saved.layout)
|
|
107
|
-
|| !Array.isArray((saved.layout as { instances?: unknown })?.instances)
|
|
108
|
-
|| !Array.isArray(saved.layouts)
|
|
109
|
-
|| !Array.isArray(saved.brokerInstances)
|
|
110
|
-
|| !isPluginConfigMap(saved.pluginConfig)
|
|
111
|
-
|| !isChartPreferences(saved.chartPreferences)
|
|
112
|
-
|| typeof saved.activeLayoutIndex !== "number";
|
|
38
|
+
export async function getDataDir(): Promise<string | null> {
|
|
39
|
+
return (await getHost()).getDataDir();
|
|
40
|
+
}
|
|
113
41
|
|
|
114
|
-
|
|
42
|
+
export async function loadConfig(dataDir: string): Promise<AppConfig> {
|
|
43
|
+
return (await getHost()).loadConfig(dataDir);
|
|
115
44
|
}
|
|
116
45
|
|
|
117
46
|
export async function saveConfig(config: AppConfig): Promise<void> {
|
|
118
|
-
|
|
119
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
120
|
-
|
|
121
|
-
const activeLayoutIndex = sanitizeActiveLayoutIndex(config.activeLayoutIndex, config.layouts.length || 1);
|
|
122
|
-
const layout = sanitizeLayout(config.layout, createDefaultConfig(config.dataDir).layout);
|
|
123
|
-
const layouts = sanitizeSavedLayouts(config.layouts, layout).map((entry, index) => (
|
|
124
|
-
index === activeLayoutIndex ? { ...entry, layout: cloneLayout(layout) } : entry
|
|
125
|
-
));
|
|
126
|
-
|
|
127
|
-
const persisted: AppConfig = {
|
|
128
|
-
...config,
|
|
129
|
-
configVersion: CURRENT_CONFIG_VERSION,
|
|
130
|
-
portfolios: sanitizePortfolios(config.portfolios, []),
|
|
131
|
-
watchlists: sanitizeWatchlists(config.watchlists, []),
|
|
132
|
-
layout,
|
|
133
|
-
layouts,
|
|
134
|
-
activeLayoutIndex,
|
|
135
|
-
brokerInstances: sanitizeBrokerInstances(config.brokerInstances),
|
|
136
|
-
plugins: sanitizeStringArray(config.plugins, []),
|
|
137
|
-
disabledPlugins: sanitizeDisabledPluginList(config.disabledPlugins),
|
|
138
|
-
pluginConfig: sanitizePluginConfig(config.pluginConfig),
|
|
139
|
-
chartPreferences: sanitizeChartPreferences(config.chartPreferences, createDefaultConfig(config.dataDir).chartPreferences),
|
|
140
|
-
recentTickers: sanitizeStringArray(config.recentTickers, []),
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
await writeFile(configPath, JSON.stringify(persisted, null, 2), "utf-8");
|
|
47
|
+
return (await getHost()).saveConfig(config);
|
|
144
48
|
}
|
|
145
49
|
|
|
146
50
|
export async function initDataDir(dataDir: string): Promise<AppConfig> {
|
|
147
|
-
|
|
148
|
-
await mkdir(dataDir, { recursive: true });
|
|
149
|
-
const { config, needsSave } = await loadConfigState(dataDir);
|
|
150
|
-
if (needsSave) {
|
|
151
|
-
await saveConfig(config);
|
|
152
|
-
}
|
|
153
|
-
return config;
|
|
51
|
+
return (await getHost()).initDataDir(dataDir);
|
|
154
52
|
}
|
|
155
53
|
|
|
156
54
|
export async function resetAllData(dataDir: string): Promise<void> {
|
|
157
|
-
await
|
|
55
|
+
return (await getHost()).resetAllData(dataDir);
|
|
158
56
|
}
|
|
159
57
|
|
|
160
58
|
export async function exportConfig(config: AppConfig, destPath: string): Promise<void> {
|
|
161
|
-
|
|
162
|
-
await writeFile(destPath, JSON.stringify(rest, null, 2), "utf-8");
|
|
59
|
+
return (await getHost()).exportConfig(config, destPath);
|
|
163
60
|
}
|
|
164
61
|
|
|
165
62
|
export async function importConfig(dataDir: string, srcPath: string): Promise<AppConfig> {
|
|
166
|
-
|
|
167
|
-
const saved = JSON.parse(raw) as Record<string, unknown>;
|
|
168
|
-
const { config } = normalizeConfig(saved, dataDir);
|
|
169
|
-
await saveConfig(config);
|
|
170
|
-
return config;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function sanitizeStringArray(value: unknown, fallback: string[]): string[] {
|
|
174
|
-
return Array.isArray(value)
|
|
175
|
-
? value.filter((entry): entry is string => typeof entry === "string")
|
|
176
|
-
: fallback;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function sanitizeDisabledPluginList(value: unknown): string[] {
|
|
180
|
-
return [...new Set(sanitizeStringArray(value, []))];
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function sanitizeDisabledPlugins(saved: Record<string, unknown>, fallback: string[]): string[] {
|
|
184
|
-
return sanitizeDisabledPluginList(saved.disabledPlugins ?? fallback);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
188
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function isPluginConfigMap(value: unknown): value is Record<string, Record<string, unknown>> {
|
|
192
|
-
if (!isPlainRecord(value)) return false;
|
|
193
|
-
return Object.values(value).every((entry) => isPlainRecord(entry));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function sanitizePluginConfig(value: unknown): Record<string, Record<string, unknown>> {
|
|
197
|
-
if (!isPluginConfigMap(value)) return {};
|
|
198
|
-
return Object.fromEntries(
|
|
199
|
-
Object.entries(value).map(([pluginId, state]) => [pluginId, { ...state }]),
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function isChartPreferences(value: unknown): value is ChartPreferences {
|
|
204
|
-
if (!value || typeof value !== "object") return false;
|
|
205
|
-
const defaultRenderMode = (value as ChartPreferences).defaultRenderMode;
|
|
206
|
-
const renderer = (value as ChartPreferences).renderer;
|
|
207
|
-
return (defaultRenderMode === "area"
|
|
208
|
-
|| defaultRenderMode === "line"
|
|
209
|
-
|| defaultRenderMode === "candles"
|
|
210
|
-
|| defaultRenderMode === "ohlc")
|
|
211
|
-
&& (renderer === "auto" || renderer === "kitty" || renderer === "braille");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function sanitizeChartPreferences(value: unknown, fallback: ChartPreferences): ChartPreferences {
|
|
215
|
-
if (!value || typeof value !== "object") return { ...fallback };
|
|
216
|
-
|
|
217
|
-
const candidate = value as Partial<ChartPreferences>;
|
|
218
|
-
const defaultRenderMode = candidate.defaultRenderMode === "area"
|
|
219
|
-
|| candidate.defaultRenderMode === "line"
|
|
220
|
-
|| candidate.defaultRenderMode === "candles"
|
|
221
|
-
|| candidate.defaultRenderMode === "ohlc"
|
|
222
|
-
? candidate.defaultRenderMode
|
|
223
|
-
: fallback.defaultRenderMode;
|
|
224
|
-
const renderer = candidate.renderer === "auto"
|
|
225
|
-
|| candidate.renderer === "kitty"
|
|
226
|
-
|| candidate.renderer === "braille"
|
|
227
|
-
? candidate.renderer
|
|
228
|
-
: fallback.renderer;
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
defaultRenderMode,
|
|
232
|
-
renderer,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function sanitizePortfolios(value: unknown, fallback: Portfolio[]): Portfolio[] {
|
|
237
|
-
if (!Array.isArray(value)) return fallback.map((portfolio) => ({ ...portfolio }));
|
|
238
|
-
return value
|
|
239
|
-
.filter((entry): entry is Portfolio =>
|
|
240
|
-
!!entry
|
|
241
|
-
&& typeof entry === "object"
|
|
242
|
-
&& typeof (entry as Portfolio).id === "string"
|
|
243
|
-
&& typeof (entry as Portfolio).name === "string"
|
|
244
|
-
&& typeof (entry as Portfolio).currency === "string",
|
|
245
|
-
)
|
|
246
|
-
.map((entry) => ({ ...entry }));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function sanitizeWatchlists(value: unknown, fallback: Watchlist[]): Watchlist[] {
|
|
250
|
-
if (!Array.isArray(value)) return fallback.map((watchlist) => ({ ...watchlist }));
|
|
251
|
-
return value
|
|
252
|
-
.filter((entry): entry is Watchlist =>
|
|
253
|
-
!!entry
|
|
254
|
-
&& typeof entry === "object"
|
|
255
|
-
&& typeof (entry as Watchlist).id === "string"
|
|
256
|
-
&& typeof (entry as Watchlist).name === "string",
|
|
257
|
-
)
|
|
258
|
-
.map((entry) => ({ ...entry }));
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function sanitizeBrokerInstances(value: unknown): BrokerInstanceConfig[] {
|
|
262
|
-
if (!Array.isArray(value)) return [];
|
|
263
|
-
return value
|
|
264
|
-
.filter((entry): entry is BrokerInstanceConfig =>
|
|
265
|
-
!!entry
|
|
266
|
-
&& typeof entry === "object"
|
|
267
|
-
&& typeof (entry as BrokerInstanceConfig).id === "string"
|
|
268
|
-
&& typeof (entry as BrokerInstanceConfig).brokerType === "string"
|
|
269
|
-
&& typeof (entry as BrokerInstanceConfig).label === "string"
|
|
270
|
-
&& typeof (entry as BrokerInstanceConfig).config === "object"
|
|
271
|
-
&& (entry as BrokerInstanceConfig).config !== null,
|
|
272
|
-
)
|
|
273
|
-
.map((entry) => ({
|
|
274
|
-
...entry,
|
|
275
|
-
enabled: entry.enabled ?? true,
|
|
276
|
-
config: { ...entry.config },
|
|
277
|
-
}));
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function isLayoutConfig(value: unknown): value is LayoutConfig {
|
|
281
|
-
return !!value
|
|
282
|
-
&& typeof value === "object"
|
|
283
|
-
&& Array.isArray((value as LayoutConfig).instances)
|
|
284
|
-
&& Array.isArray((value as LayoutConfig).floating)
|
|
285
|
-
&& "dockRoot" in (value as Record<string, unknown>);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function sanitizePaneBinding(value: unknown, fallback: PaneBinding = { kind: "none" }): PaneBinding {
|
|
289
|
-
if (!value || typeof value !== "object") return fallback;
|
|
290
|
-
if ((value as PaneBinding).kind === "fixed" && typeof (value as Extract<PaneBinding, { kind: "fixed" }>).symbol === "string") {
|
|
291
|
-
return { kind: "fixed", symbol: (value as Extract<PaneBinding, { kind: "fixed" }>).symbol };
|
|
292
|
-
}
|
|
293
|
-
if ((value as PaneBinding).kind === "follow" && typeof (value as Extract<PaneBinding, { kind: "follow" }>).sourceInstanceId === "string") {
|
|
294
|
-
return { kind: "follow", sourceInstanceId: (value as Extract<PaneBinding, { kind: "follow" }>).sourceInstanceId };
|
|
295
|
-
}
|
|
296
|
-
if ((value as PaneBinding).kind === "none") return { kind: "none" };
|
|
297
|
-
return fallback;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function sanitizeFloatingPlacementMemory(value: unknown): FloatingPlacementMemory | undefined {
|
|
301
|
-
if (!value || typeof value !== "object") return undefined;
|
|
302
|
-
const x = typeof (value as FloatingPlacementMemory).x === "number" ? Math.max(0, Math.round((value as FloatingPlacementMemory).x)) : null;
|
|
303
|
-
const y = typeof (value as FloatingPlacementMemory).y === "number" ? Math.max(0, Math.round((value as FloatingPlacementMemory).y)) : null;
|
|
304
|
-
const width = typeof (value as FloatingPlacementMemory).width === "number" ? Math.max(1, Math.round((value as FloatingPlacementMemory).width)) : null;
|
|
305
|
-
const height = typeof (value as FloatingPlacementMemory).height === "number" ? Math.max(1, Math.round((value as FloatingPlacementMemory).height)) : null;
|
|
306
|
-
if (x === null || y === null || width === null || height === null) return undefined;
|
|
307
|
-
return { x, y, width, height };
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function sanitizePlacementMemory(value: unknown): PanePlacementMemory | undefined {
|
|
311
|
-
if (!value || typeof value !== "object") return undefined;
|
|
312
|
-
|
|
313
|
-
const docked = (() => {
|
|
314
|
-
const raw = (value as PanePlacementMemory).docked;
|
|
315
|
-
if (!raw || typeof raw !== "object") return undefined;
|
|
316
|
-
const path = Array.isArray((raw as { path?: unknown }).path)
|
|
317
|
-
? (raw as { path?: unknown }).path
|
|
318
|
-
?.filter((segment): segment is 0 | 1 => segment === 0 || segment === 1)
|
|
319
|
-
: undefined;
|
|
320
|
-
const anchorInstanceId = typeof (raw as { anchorInstanceId?: unknown }).anchorInstanceId === "string"
|
|
321
|
-
? (raw as { anchorInstanceId: string }).anchorInstanceId
|
|
322
|
-
: undefined;
|
|
323
|
-
const position = ["left", "right", "above", "below"].includes(String((raw as { position?: unknown }).position))
|
|
324
|
-
? (raw as { position: "left" | "right" | "above" | "below" }).position
|
|
325
|
-
: undefined;
|
|
326
|
-
if (!path && !anchorInstanceId && !position) return undefined;
|
|
327
|
-
return {
|
|
328
|
-
path,
|
|
329
|
-
anchorInstanceId,
|
|
330
|
-
position,
|
|
331
|
-
};
|
|
332
|
-
})();
|
|
333
|
-
|
|
334
|
-
const floating = sanitizeFloatingPlacementMemory((value as PanePlacementMemory).floating);
|
|
335
|
-
if (!docked && !floating) return undefined;
|
|
336
|
-
return { docked, floating };
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function sanitizePaneInstances(value: unknown, fallback: LayoutConfig): PaneInstanceConfig[] {
|
|
340
|
-
if (!Array.isArray(value)) return cloneLayout(fallback).instances;
|
|
341
|
-
const seen = new Set<string>();
|
|
342
|
-
const instances = value
|
|
343
|
-
.filter((entry): entry is PaneInstanceConfig =>
|
|
344
|
-
!!entry
|
|
345
|
-
&& typeof entry === "object"
|
|
346
|
-
&& typeof (entry as PaneInstanceConfig).instanceId === "string"
|
|
347
|
-
&& typeof (entry as PaneInstanceConfig).paneId === "string",
|
|
348
|
-
)
|
|
349
|
-
.map((entry) => {
|
|
350
|
-
const instanceId = seen.has(entry.instanceId) ? createPaneInstanceId(entry.paneId) : entry.instanceId;
|
|
351
|
-
seen.add(instanceId);
|
|
352
|
-
return {
|
|
353
|
-
instanceId,
|
|
354
|
-
paneId: entry.paneId,
|
|
355
|
-
title: typeof entry.title === "string" ? entry.title : undefined,
|
|
356
|
-
binding: sanitizePaneBinding(entry.binding),
|
|
357
|
-
params: entry.params && typeof entry.params === "object"
|
|
358
|
-
? Object.fromEntries(
|
|
359
|
-
Object.entries(entry.params).filter((param): param is [string, string] => typeof param[1] === "string"),
|
|
360
|
-
)
|
|
361
|
-
: undefined,
|
|
362
|
-
settings: sanitizePaneSettings(entry.settings),
|
|
363
|
-
placementMemory: sanitizePlacementMemory(entry.placementMemory),
|
|
364
|
-
};
|
|
365
|
-
});
|
|
366
|
-
return instances.length > 0 ? instances : cloneLayout(fallback).instances;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function sanitizePaneSettings(value: unknown): Record<string, unknown> | undefined {
|
|
370
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
371
|
-
|
|
372
|
-
const sanitizeValue = (entry: unknown): unknown => {
|
|
373
|
-
if (entry == null) return entry;
|
|
374
|
-
if (typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean") {
|
|
375
|
-
return entry;
|
|
376
|
-
}
|
|
377
|
-
if (Array.isArray(entry)) {
|
|
378
|
-
return entry
|
|
379
|
-
.map((child) => sanitizeValue(child))
|
|
380
|
-
.filter((child) => child !== undefined);
|
|
381
|
-
}
|
|
382
|
-
if (typeof entry === "object") {
|
|
383
|
-
return Object.fromEntries(
|
|
384
|
-
Object.entries(entry as Record<string, unknown>)
|
|
385
|
-
.map(([key, child]) => [key, sanitizeValue(child)])
|
|
386
|
-
.filter(([, child]) => child !== undefined),
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
return undefined;
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
const settings = Object.fromEntries(
|
|
393
|
-
Object.entries(value as Record<string, unknown>)
|
|
394
|
-
.map(([key, entry]) => [key, sanitizeValue(entry)])
|
|
395
|
-
.filter(([, entry]) => entry !== undefined),
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
return Object.keys(settings).length > 0 ? clonePaneSettings(settings) : undefined;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function hasExactColumnIds(value: unknown, expected: string[]): boolean {
|
|
402
|
-
return Array.isArray(value)
|
|
403
|
-
&& value.length === expected.length
|
|
404
|
-
&& value.every((entry, index) => entry === expected[index]);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function migrateLegacyPortfolioDefaultColumns(layout: LayoutConfig, enabled: boolean): LayoutConfig {
|
|
408
|
-
if (!enabled) return layout;
|
|
409
|
-
|
|
410
|
-
const instanceIndex = layout.instances.findIndex((instance) =>
|
|
411
|
-
instance.instanceId === "portfolio-list:main"
|
|
412
|
-
&& instance.paneId === "portfolio-list"
|
|
413
|
-
&& hasExactColumnIds(instance.settings?.columnIds, LEGACY_MAIN_PORTFOLIO_COLUMN_IDS)
|
|
414
|
-
);
|
|
415
|
-
if (instanceIndex < 0) return layout;
|
|
416
|
-
|
|
417
|
-
const nextLayout = cloneLayout(layout);
|
|
418
|
-
nextLayout.instances[instanceIndex] = {
|
|
419
|
-
...nextLayout.instances[instanceIndex]!,
|
|
420
|
-
settings: {
|
|
421
|
-
...(nextLayout.instances[instanceIndex]?.settings ?? {}),
|
|
422
|
-
columnIds: [...DEFAULT_PORTFOLIO_COLUMN_IDS],
|
|
423
|
-
},
|
|
424
|
-
};
|
|
425
|
-
return nextLayout;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function getDefaultFollowSourceInstanceId(instances: PaneInstanceConfig[]): string | null {
|
|
429
|
-
return instances.find((instance) => instance.paneId === "portfolio-list")?.instanceId ?? null;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function sanitizeFloatingEntries(value: unknown, validInstanceIds: Set<string>): LayoutConfig["floating"] {
|
|
433
|
-
if (!Array.isArray(value)) return [];
|
|
434
|
-
return value
|
|
435
|
-
.filter((entry): entry is LayoutConfig["floating"][number] =>
|
|
436
|
-
!!entry
|
|
437
|
-
&& typeof entry === "object"
|
|
438
|
-
&& typeof (entry as LayoutConfig["floating"][number]).instanceId === "string"
|
|
439
|
-
&& typeof (entry as LayoutConfig["floating"][number]).x === "number"
|
|
440
|
-
&& typeof (entry as LayoutConfig["floating"][number]).y === "number"
|
|
441
|
-
&& typeof (entry as LayoutConfig["floating"][number]).width === "number"
|
|
442
|
-
&& typeof (entry as LayoutConfig["floating"][number]).height === "number",
|
|
443
|
-
)
|
|
444
|
-
.filter((entry) => validInstanceIds.has(entry.instanceId))
|
|
445
|
-
.map((entry) => ({
|
|
446
|
-
...entry,
|
|
447
|
-
x: Math.max(0, Math.round(entry.x)),
|
|
448
|
-
y: Math.max(0, Math.round(entry.y)),
|
|
449
|
-
width: Math.max(1, Math.round(entry.width)),
|
|
450
|
-
height: Math.max(1, Math.round(entry.height)),
|
|
451
|
-
zIndex: typeof entry.zIndex === "number" ? Math.round(entry.zIndex) : entry.zIndex,
|
|
452
|
-
}));
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
export function sanitizeLayout(value: unknown, fallback: LayoutConfig): LayoutConfig {
|
|
456
|
-
if (!isLayoutConfig(value)) {
|
|
457
|
-
return cloneLayout(fallback);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (!Array.isArray((value as LayoutConfig & { instances?: unknown }).instances)) {
|
|
461
|
-
const layout = cloneLayout(fallback);
|
|
462
|
-
return normalizePaneLayout(layout, {
|
|
463
|
-
defaultFollowSourceInstanceId: getDefaultFollowSourceInstanceId(layout.instances),
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const instances = sanitizePaneInstances((value as LayoutConfig & { instances?: unknown }).instances, fallback);
|
|
468
|
-
const validInstanceIds = new Set(instances.map((entry) => entry.instanceId));
|
|
469
|
-
const dockRoot = (value as { dockRoot?: LayoutConfig["dockRoot"] }).dockRoot ?? null;
|
|
470
|
-
const floating = sanitizeFloatingEntries((value as { floating?: unknown }).floating, validInstanceIds);
|
|
471
|
-
|
|
472
|
-
const layout: LayoutConfig = {
|
|
473
|
-
dockRoot,
|
|
474
|
-
instances,
|
|
475
|
-
floating,
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
return normalizePaneLayout(layout, {
|
|
479
|
-
defaultFollowSourceInstanceId: getDefaultFollowSourceInstanceId(layout.instances),
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function sanitizeSavedLayouts(value: unknown, fallbackLayout: LayoutConfig): SavedLayout[] {
|
|
484
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
485
|
-
return [{ name: "Default", layout: cloneLayout(fallbackLayout) }];
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const layouts = value
|
|
489
|
-
.filter((entry): entry is SavedLayout =>
|
|
490
|
-
!!entry
|
|
491
|
-
&& typeof entry === "object"
|
|
492
|
-
&& typeof (entry as SavedLayout).name === "string",
|
|
493
|
-
)
|
|
494
|
-
.map((entry) => ({
|
|
495
|
-
name: entry.name,
|
|
496
|
-
layout: sanitizeLayout(entry.layout, fallbackLayout),
|
|
497
|
-
}));
|
|
498
|
-
|
|
499
|
-
return layouts.length > 0 ? layouts : [{ name: "Default", layout: cloneLayout(fallbackLayout) }];
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function sanitizeActiveLayoutIndex(value: unknown, layoutCount: number): number {
|
|
503
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value >= layoutCount) {
|
|
504
|
-
return 0;
|
|
505
|
-
}
|
|
506
|
-
return value;
|
|
63
|
+
return (await getHost()).importConfig(dataDir, srcPath);
|
|
507
64
|
}
|
package/src/data/sqlite-json.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
export type JsonPrimitive = string | number | boolean | null;
|
|
2
3
|
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
|
3
4
|
|
|
@@ -5,14 +6,10 @@ export function serializeJson(value: unknown): string {
|
|
|
5
6
|
return JSON.stringify(value);
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
export function parseJson<T>(value: string): T {
|
|
9
|
-
return JSON.parse(value) as T;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
9
|
export function safeParseJson<T>(value: string | null): T | null {
|
|
13
10
|
if (!value) return null;
|
|
14
11
|
try {
|
|
15
|
-
return
|
|
12
|
+
return JSON.parse(value) as T;
|
|
16
13
|
} catch {
|
|
17
14
|
return null;
|
|
18
15
|
}
|
package/src/data/sqlite-retry.ts
CHANGED