gloomberb 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -15
- package/README.zh-CN.md +12 -12
- package/package.json +10 -3
- package/src/api-client/auth.ts +14 -10
- package/src/api-client/data.ts +54 -0
- package/src/api-client/index.test.ts +124 -0
- package/src/api-client/index.ts +139 -4
- package/src/api-client/paths.ts +89 -0
- package/src/api-client/request.ts +15 -2
- package/src/api-client/socket-health.test.ts +1 -0
- package/src/api-client/socket.ts +2 -1
- package/src/api-client/types.ts +163 -0
- package/src/app/pane-runtime/index.tsx +4 -1
- package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
- package/src/app/pane-runtime/plugin-bindings.ts +25 -4
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
- package/src/app/runtime/desktop-deeplink.test.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +66 -2
- package/src/app/runtime/desktop-menu.ts +3 -0
- package/src/app/runtime/plugin-bindings.ts +30 -28
- package/src/app/runtime/update.ts +8 -6
- package/src/app.tsx +21 -7
- package/src/architecture/import-boundaries.test.ts +5 -1
- package/src/capabilities/factories.ts +4 -0
- package/src/cli/commands/automation.test.ts +9 -0
- package/src/cli/commands/automation.ts +22 -73
- package/src/cli/index.ts +10 -6
- package/src/cli/registry.ts +1 -1
- package/src/components/chart/composite/composite-chart.test.tsx +45 -2
- package/src/components/chart/composite/composite-chart.tsx +16 -12
- package/src/components/chart/composite/format.test.ts +34 -0
- package/src/components/chart/composite/format.ts +33 -10
- package/src/components/chart/composite/scene.ts +0 -21
- package/src/components/chart/composite/tools.ts +49 -0
- package/src/components/chart/core/axis-format.ts +13 -3
- package/src/components/chart/core/index.test.ts +7 -0
- package/src/components/chart/core/renderer.ts +3 -4
- package/src/components/command-bar/commands/direct/index.ts +4 -0
- package/src/components/command-bar/commands/registry.ts +8 -1
- package/src/components/command-bar/layout-items/current-layout.ts +1 -1
- package/src/components/command-bar/pane-templates/workflow.ts +4 -0
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
- package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
- package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
- package/src/components/command-bar/surface/index.test.tsx +6 -6
- package/src/components/command-bar/surface/index.tsx +4 -1
- package/src/components/command-bar/surface/pane.test.tsx +66 -2
- package/src/components/command-bar/view-model.test.ts +2 -1
- package/src/components/command-bar/workflow/ops.test.ts +54 -0
- package/src/components/command-bar/workflow/ops.ts +1 -1
- package/src/components/company-logo.test.ts +19 -0
- package/src/components/company-logo.tsx +60 -0
- package/src/components/feed-data-table/stack-view.tsx +7 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +85 -15
- package/src/components/layout/header.tsx +5 -5
- package/src/components/layout/pane/title.ts +31 -3
- package/src/components/layout/shell/active-drag.ts +4 -11
- package/src/components/layout/shell/drag/index.ts +4 -20
- package/src/components/layout/shell/drag/runtime.ts +3 -4
- package/src/components/layout/shell/index.test.tsx +17 -3
- package/src/components/layout/shell/index.tsx +89 -31
- package/src/components/layout/shell/menu.test.ts +37 -1
- package/src/components/layout/shell/menu.ts +14 -0
- package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
- package/src/components/layout/shell/pane/actions.ts +0 -5
- package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
- package/src/components/layout/shell/shortcuts.ts +6 -3
- package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
- package/src/components/layout/shell/ticker-link-menu.ts +78 -0
- package/src/components/layout/status-bar.test.tsx +62 -61
- package/src/components/layout/status-bar.tsx +53 -75
- package/src/components/onboarding/account-step/form-panel.tsx +4 -2
- package/src/components/onboarding/account-step/index.tsx +1 -1
- package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
- package/src/components/onboarding/onboarding-wizard.tsx +13 -5
- package/src/components/onboarding/wizard-account.ts +4 -23
- package/src/components/table-view-shared.test.ts +11 -0
- package/src/components/table-view-shared.tsx +26 -0
- package/src/components/ui/data-table/index.tsx +19 -1
- package/src/components/ui/fields.tsx +5 -1
- package/src/core/state/app/layout-reducer.ts +36 -0
- package/src/core/state/app/layout.ts +10 -5
- package/src/core/state/app/state.ts +0 -13
- package/src/core/state/app/types.ts +6 -4
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/config/layout.ts +2 -0
- package/src/data/config/store/index.test.ts +27 -6
- package/src/data/config/store/normalize.ts +3 -5
- package/src/data/plugin-state-store.ts +11 -0
- package/src/data/sqlite/cache.test.ts +10 -0
- package/src/i18n/es.ts +13 -5
- package/src/i18n/ja.ts +13 -5
- package/src/i18n/ko.ts +13 -5
- package/src/i18n/zh-cn.ts +13 -5
- package/src/i18n/zh-tw.ts +13 -5
- package/src/layout-marketplace/api.test.ts +54 -0
- package/src/layout-marketplace/api.ts +65 -0
- package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
- package/src/layout-marketplace/gallery-desktop.tsx +375 -0
- package/src/layout-marketplace/gallery-terminal.tsx +376 -0
- package/src/layout-marketplace/gallery.test.tsx +84 -0
- package/src/layout-marketplace/gallery.tsx +323 -0
- package/src/layout-marketplace/geometry.test.ts +47 -0
- package/src/layout-marketplace/geometry.ts +80 -0
- package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
- package/src/layout-marketplace/mini-workspace.tsx +428 -0
- package/src/layout-marketplace/model.test.ts +118 -0
- package/src/layout-marketplace/model.ts +171 -0
- package/src/layout-marketplace/name-dialog.tsx +60 -0
- package/src/layout-marketplace/pane-imagery.ts +85 -0
- package/src/layout-marketplace/pane.tsx +20 -0
- package/src/layout-marketplace/payload.test.ts +246 -0
- package/src/layout-marketplace/payload.ts +624 -0
- package/src/layout-marketplace/use-marketplace.ts +63 -0
- package/src/market-data/coordinator/index.test.ts +2 -2
- package/src/market-data/coordinator/index.ts +1 -1
- package/src/market-data/hooks.tsx +0 -41
- package/src/market-data/market/format.test.ts +7 -0
- package/src/market-data/market/freshness.ts +5 -2
- package/src/market-data/quotes/freshness.test.ts +16 -0
- package/src/news/aggregator.test.ts +92 -3
- package/src/news/aggregator.ts +98 -18
- package/src/news/hooks.test.tsx +2 -0
- package/src/news/hooks.ts +27 -1
- package/src/news/news-model.ts +5 -2
- package/src/news/types.ts +7 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
- package/src/plugins/broker-sync/normalize.ts +42 -5
- package/src/plugins/builtin/account-management/pane.tsx +5 -5
- package/src/plugins/builtin/ai/index.tsx +18 -2
- package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
- package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
- package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
- package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
- package/src/plugins/builtin/alerts/index.test.tsx +49 -6
- package/src/plugins/builtin/alerts/index.tsx +11 -6
- package/src/plugins/builtin/alerts/pane.tsx +51 -9
- package/src/plugins/builtin/alerts/quotes.ts +11 -3
- package/src/plugins/builtin/alerts/types.ts +1 -0
- package/src/plugins/builtin/analytics/index.tsx +11 -1
- package/src/plugins/builtin/analytics/pane-model.ts +6 -4
- package/src/plugins/builtin/broker-manager/index.tsx +3 -0
- package/src/plugins/builtin/buildout/model/index.ts +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
- package/src/plugins/builtin/cds/client.test.ts +83 -0
- package/src/plugins/builtin/cds/client.ts +67 -0
- package/src/plugins/builtin/cds/index.tsx +41 -0
- package/src/plugins/builtin/cds/model.test.ts +232 -0
- package/src/plugins/builtin/cds/model.ts +401 -0
- package/src/plugins/builtin/cds/pane.test.tsx +132 -0
- package/src/plugins/builtin/cds/pane.tsx +358 -0
- package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
- package/src/plugins/builtin/chart-composer/index.tsx +88 -2
- package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
- package/src/plugins/builtin/chat/content/index.tsx +4 -1
- package/src/plugins/builtin/chat/controller/channels.ts +7 -1
- package/src/plugins/builtin/chat/controller/index.ts +14 -9
- package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
- package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
- package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
- package/src/plugins/builtin/chat/controller/send.ts +1 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
- package/src/plugins/builtin/chat/controller/state.ts +1 -3
- package/src/plugins/builtin/chat/controller/storage.ts +0 -2
- package/src/plugins/builtin/chat/controller/utils.ts +4 -6
- package/src/plugins/builtin/chat/controller/view.ts +2 -2
- package/src/plugins/builtin/chat/controller.test.ts +142 -14
- package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
- package/src/plugins/builtin/chat/profile-popover.ts +53 -22
- package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
- package/src/plugins/builtin/chat/sidebar.tsx +63 -0
- package/src/plugins/builtin/chat/test-harness.tsx +29 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
- package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
- package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
- package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
- package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
- package/src/plugins/builtin/cloud/browser.tsx +9 -0
- package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
- package/src/plugins/builtin/cloud/device-signin.ts +2 -8
- package/src/plugins/builtin/cloud/index.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +14 -24
- package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
- package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
- package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
- package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
- package/src/plugins/builtin/composite-plugins.ts +3 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
- package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
- package/src/plugins/builtin/congress-trades/model.ts +69 -1
- package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
- package/src/plugins/builtin/connections/index.ts +3 -0
- package/src/plugins/builtin/dividend-yield/client.ts +18 -0
- package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
- package/src/plugins/builtin/earnings/index.tsx +5 -3
- package/src/plugins/builtin/earnings/model.test.ts +19 -1
- package/src/plugins/builtin/earnings/model.ts +17 -15
- package/src/plugins/builtin/econ/calendar-model.ts +0 -5
- package/src/plugins/builtin/econ/index.test.tsx +6 -1
- package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
- package/src/plugins/builtin/insider/index.tsx +27 -11
- package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
- package/src/plugins/builtin/layout-manager/index.tsx +17 -4
- package/src/plugins/builtin/news/index.tsx +11 -6
- package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
- package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
- package/src/plugins/builtin/news/wire/index.tsx +3 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
- package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
- package/src/plugins/builtin/notes/files.ts +80 -0
- package/src/plugins/builtin/notes/index.tsx +3 -0
- package/src/plugins/builtin/notes/sync.test.ts +49 -0
- package/src/plugins/builtin/notes/sync.ts +98 -0
- package/src/plugins/builtin/options/index.tsx +1 -0
- package/src/plugins/builtin/options/view.test.tsx +25 -3
- package/src/plugins/builtin/options/view.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
- package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
- package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
- package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
- package/src/plugins/builtin/research/index.test.ts +14 -0
- package/src/plugins/builtin/research/index.tsx +29 -0
- package/src/plugins/builtin/sec/index.tsx +24 -9
- package/src/plugins/builtin/sectors/index.tsx +14 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
- package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
- package/src/plugins/builtin/shared/public-pane.ts +9 -0
- package/src/plugins/builtin/shared/public-share.ts +23 -0
- package/src/plugins/builtin/shared/ticker-source.ts +32 -0
- package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
- package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
- package/src/plugins/builtin/substack/api/cache.ts +7 -3
- package/src/plugins/builtin/substack/api/loaders.ts +36 -12
- package/src/plugins/builtin/substack/api/types.ts +2 -0
- package/src/plugins/builtin/substack/pane-state.ts +1 -1
- package/src/plugins/builtin/substack/pane.tsx +55 -12
- package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +40 -18
- package/src/plugins/builtin/thirteenf/data.ts +15 -7
- package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
- package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
- package/src/plugins/builtin/world-indices/index.tsx +2 -0
- package/src/plugins/catalog-browser.test.ts +78 -0
- package/src/plugins/catalog-browser.ts +136 -0
- package/src/plugins/gridlock-notification.ts +2 -1
- package/src/plugins/ibkr/flex/index.test.ts +19 -0
- package/src/plugins/ibkr/flex/index.ts +1 -1
- package/src/plugins/pane-manager/gridlock.ts +87 -3
- package/src/plugins/pane-manager.test.ts +30 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +2 -2
- package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
- package/src/plugins/prediction-markets/controller/data.ts +7 -1
- package/src/plugins/prediction-markets/controller/index.ts +9 -2
- package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
- package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
- package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
- package/src/plugins/prediction-markets/detail.test.tsx +29 -2
- package/src/plugins/prediction-markets/index.tsx +8 -4
- package/src/plugins/prediction-markets/navigation.ts +0 -1
- package/src/plugins/prediction-markets/pane.test.tsx +77 -0
- package/src/plugins/prediction-markets/pane.tsx +12 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
- package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
- package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
- package/src/plugins/registry/index.ts +2 -0
- package/src/plugins/ticker-navigation.test.ts +24 -0
- package/src/plugins/ticker-navigation.ts +40 -0
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/app-services.ts +61 -0
- package/src/renderers/browser/cloud-transport.test.ts +68 -0
- package/src/renderers/browser/cloud-transport.ts +48 -0
- package/src/renderers/browser/config-host.ts +58 -0
- package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
- package/src/renderers/browser/deeplink-bridge.ts +22 -0
- package/src/renderers/browser/error-boundary.tsx +23 -0
- package/src/renderers/browser/main.tsx +64 -0
- package/src/renderers/browser/persistence.ts +74 -0
- package/src/renderers/browser/storage.test.ts +66 -0
- package/src/renderers/browser/storage.ts +50 -0
- package/src/renderers/browser/ticker-repository.ts +59 -0
- package/src/renderers/browser/ui-host.tsx +40 -0
- package/src/renderers/cloudflare/worker.test.ts +136 -0
- package/src/renderers/cloudflare/worker.ts +116 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +14 -3
- package/src/renderers/electrobun/view/build-assets.ts +12 -10
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
- package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
- package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
- package/src/renderers/electrobun/view/host/box.tsx +11 -6
- package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
- package/src/renderers/electrobun/view/host/input.tsx +11 -7
- package/src/renderers/electrobun/view/host/native.ts +0 -5
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
- package/src/renderers/electrobun/view/styles.css +107 -7
- package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
- package/src/renderers/electrobun/view/toast-host.tsx +69 -43
- package/src/renderers/electrobun/view/ui-host.tsx +6 -137
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/renderers/share/layout-view.test.tsx +58 -0
- package/src/renderers/share/layout-view.tsx +133 -0
- package/src/renderers/share/main.tsx +102 -0
- package/src/renderers/share/styles.css +54 -0
- package/src/renderers/share/view.test.tsx +100 -0
- package/src/renderers/share/view.tsx +150 -0
- package/src/shares/api.test.ts +105 -0
- package/src/shares/api.ts +127 -0
- package/src/shares/chart-snapshot.test.ts +34 -0
- package/src/shares/chart-snapshot.ts +28 -0
- package/src/shares/location.test.ts +19 -0
- package/src/shares/location.ts +11 -0
- package/src/shares/pane.test.ts +110 -0
- package/src/shares/pane.ts +54 -0
- package/src/shares/payload.ts +177 -0
- package/src/sources/gloomberb-cloud/index.test.ts +61 -1
- package/src/sources/gloomberb-cloud/index.ts +86 -14
- package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
- package/src/sources/provider-router/history.test.ts +2 -2
- package/src/sources/provider-router/news.test.ts +34 -0
- package/src/sources/provider-router/news.ts +15 -1
- package/src/sources/sec-edgar.test.ts +59 -0
- package/src/sources/sec-edgar.ts +63 -23
- package/src/sources/yahoo-finance/mappers.ts +3 -0
- package/src/sources/yahoo-finance/snapshots.ts +10 -2
- package/src/sources/yahoo-finance/symbols.test.ts +10 -0
- package/src/sources/yahoo-finance/symbols.ts +13 -8
- package/src/sources/yahoo-finance.test.ts +1 -1
- package/src/sources/yahoo-finance.ts +6 -2
- package/src/state/app/bootstrap.ts +6 -7
- package/src/state/app/context/index.test.ts +41 -1
- package/src/state/app/context/index.test.tsx +34 -16
- package/src/state/selectors-ui.ts +0 -8
- package/src/sync/baseline.ts +32 -0
- package/src/sync/controller.test.ts +124 -1
- package/src/sync/controller.ts +54 -12
- package/src/sync/core-contributors.test.ts +44 -0
- package/src/sync/core-contributors.ts +59 -10
- package/src/sync/react.ts +22 -2
- package/src/sync/types.ts +12 -0
- package/src/theme/font-scale.ts +0 -11
- package/src/tickers/search/index.test.ts +27 -0
- package/src/tickers/search/upsert.ts +10 -0
- package/src/types/capability-route-source.ts +2 -1
- package/src/types/config.ts +35 -5
- package/src/types/desktop-menu.ts +1 -0
- package/src/types/plugin.ts +36 -0
- package/src/ui/context-menu.tsx +1 -1
- package/src/ui/host.tsx +4 -0
- package/src/ui/toast.tsx +2 -0
- package/src/utils/back-navigation.ts +0 -4
- package/src/utils/price-history.ts +11 -10
- package/src/utils/sec.test.ts +6 -0
- package/src/utils/sec.ts +1 -1
|
@@ -357,7 +357,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
357
357
|
const cachedFinancials = useAppSelector((state) => state.financials);
|
|
358
358
|
const cachedExchangeRates = useAppSelector((state) => state.exchangeRates);
|
|
359
359
|
const [sessionMarker, setSessionMarker] = useState(() => buildAccountSessionMarker());
|
|
360
|
-
const [hasSession, setHasSession] = useState(() =>
|
|
360
|
+
const [hasSession, setHasSession] = useState(() => apiClient.isSignedIn());
|
|
361
361
|
const [profile, setProfile] = useState<AccountProfile | null>(null);
|
|
362
362
|
const [pricing, setPricing] = useState<CloudPricing | null>(null);
|
|
363
363
|
const [draft, setDraft] = useState<AccountDraft>(() => profileToDraft(null));
|
|
@@ -552,7 +552,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
552
552
|
|
|
553
553
|
useEffect(() => {
|
|
554
554
|
const unsubscribe = chatController.subscribe((snapshot) => {
|
|
555
|
-
setHasSession(
|
|
555
|
+
setHasSession(apiClient.isSignedIn() || snapshot.hasSavedSession);
|
|
556
556
|
setSessionMarker(buildAccountSessionMarker());
|
|
557
557
|
});
|
|
558
558
|
void chatController.refreshSession().catch(() => {});
|
|
@@ -560,7 +560,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
560
560
|
}, []);
|
|
561
561
|
|
|
562
562
|
const loadProfile = useCallback(async () => {
|
|
563
|
-
if (!apiClient.
|
|
563
|
+
if (!apiClient.isSignedIn()) {
|
|
564
564
|
setProfile(null);
|
|
565
565
|
setDraft(profileToDraft(null));
|
|
566
566
|
return;
|
|
@@ -598,7 +598,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
598
598
|
}, []);
|
|
599
599
|
|
|
600
600
|
useEffect(() => {
|
|
601
|
-
if (!hasSession || !apiClient.
|
|
601
|
+
if (!hasSession || !apiClient.isSignedIn()) return;
|
|
602
602
|
if (syncStatus.phase !== "synced" || syncStatus.revision == null) return;
|
|
603
603
|
if (refreshedSyncRevisionRef.current === syncStatus.revision) return;
|
|
604
604
|
refreshedSyncRevisionRef.current = syncStatus.revision;
|
|
@@ -847,7 +847,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
847
847
|
turnOffEmailAlerts,
|
|
848
848
|
});
|
|
849
849
|
|
|
850
|
-
if (!hasSession && !apiClient.
|
|
850
|
+
if (!hasSession && !apiClient.isSignedIn()) {
|
|
851
851
|
return (
|
|
852
852
|
<Box padding={1}>
|
|
853
853
|
<CloudAuthNotice message={t("Log in to manage your Gloom Cloud account.")} />
|
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
resolveDefaultAiProviderId,
|
|
7
7
|
type AiProvider,
|
|
8
8
|
} from "./providers";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
AI_SCREENER_PANE_STATE_KEY,
|
|
11
|
+
AiScreenerPane,
|
|
12
|
+
} from "./screener/pane";
|
|
10
13
|
import { buildAiScreenerPaneSettingsDef, getAiScreenerPaneSettings } from "./settings";
|
|
11
14
|
import {
|
|
12
15
|
LOCAL_AGENT_WORKSPACE_SCHEMA_VERSION,
|
|
@@ -50,6 +53,7 @@ import {
|
|
|
50
53
|
LIVE_STREAMING_QUICK_SETTING,
|
|
51
54
|
withLiveStreamingSetting,
|
|
52
55
|
} from "../shared/live-streaming";
|
|
56
|
+
import { getPluginPaneStateValue } from "../../runtime";
|
|
53
57
|
|
|
54
58
|
function settingOrFallback(
|
|
55
59
|
settings: Record<string, unknown>,
|
|
@@ -269,6 +273,9 @@ export const aiPlugin: GloomPlugin = {
|
|
|
269
273
|
defaultPosition: "right",
|
|
270
274
|
defaultMode: "floating",
|
|
271
275
|
defaultFloatingSize: { width: 96, height: 32 },
|
|
276
|
+
portableShare: {
|
|
277
|
+
private: { title: true, params: true, settings: true, state: true },
|
|
278
|
+
},
|
|
272
279
|
settings: (context) => {
|
|
273
280
|
const providers = detectProviders();
|
|
274
281
|
const workspaceProviders = providers.filter((provider) => (
|
|
@@ -335,7 +342,11 @@ export const aiPlugin: GloomPlugin = {
|
|
|
335
342
|
defaultPosition: "right",
|
|
336
343
|
defaultMode: "floating",
|
|
337
344
|
defaultFloatingSize: { width: 76, height: 24 },
|
|
345
|
+
tickerSource: true,
|
|
338
346
|
quickSettings: [LIVE_STREAMING_QUICK_SETTING],
|
|
347
|
+
portableShare: {
|
|
348
|
+
private: { title: true, params: true, settings: true, state: true },
|
|
349
|
+
},
|
|
339
350
|
settings: (context) => {
|
|
340
351
|
const providers = detectProviders();
|
|
341
352
|
const screenerProviders = providers.filter((provider) => (
|
|
@@ -347,7 +358,12 @@ export const aiPlugin: GloomPlugin = {
|
|
|
347
358
|
const currentFallbackProviderId = resolveDefaultAiProviderId(screenerRunners);
|
|
348
359
|
const defaults = defaultsFromConfig(context.config, currentFallbackProviderId);
|
|
349
360
|
const providerIds = new Set(screenerProviders.map((provider) => provider.id));
|
|
350
|
-
const persisted =
|
|
361
|
+
const persisted = getPluginPaneStateValue<PersistedAiScreenerPaneState | null>(
|
|
362
|
+
context.paneState,
|
|
363
|
+
"ai",
|
|
364
|
+
AI_SCREENER_PANE_STATE_KEY,
|
|
365
|
+
null,
|
|
366
|
+
) ?? ctx.resume.getState<PersistedAiScreenerPaneState>(
|
|
351
367
|
`screener-pane:${context.paneId}`,
|
|
352
368
|
{ schemaVersion: 1 },
|
|
353
369
|
) ?? EMPTY_PANE_STATE;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { act, useReducer, useState } from "react";
|
|
2
|
+
import { act, useEffect, useReducer, useState } from "react";
|
|
3
3
|
import { TestDialogProvider, testRender } from "../../../../renderers/opentui/test-utils";
|
|
4
4
|
import { PaneFooterBar, PaneFooterProvider } from "../../../../components/layout/pane/footer";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
AppContext,
|
|
7
|
+
PaneInstanceProvider,
|
|
8
|
+
appReducer,
|
|
9
|
+
createInitialState,
|
|
10
|
+
type PaneRuntimeState,
|
|
11
|
+
} from "../../../../state/app/context";
|
|
6
12
|
import { createDefaultConfig } from "../../../../types/config";
|
|
7
13
|
import type { Quote, TickerFinancials } from "../../../../types/financials";
|
|
8
14
|
import type { TickerRecord } from "../../../../types/ticker";
|
|
@@ -11,7 +17,8 @@ import { createStatefulTestPluginRuntime } from "../../../../test-support/plugin
|
|
|
11
17
|
import { Box } from "../../../../ui";
|
|
12
18
|
import { PluginRenderProvider, type PluginRuntimeAccess } from "../../../runtime";
|
|
13
19
|
import { getSharedMarketData, setSharedMarketDataForTests, setSharedRegistryForTests } from "../../../registry";
|
|
14
|
-
import { AiScreenerPane } from "./pane";
|
|
20
|
+
import { AI_SCREENER_PANE_STATE_KEY, AiScreenerPane } from "./pane";
|
|
21
|
+
import { createScreenerTab, EMPTY_PANE_STATE } from "./model";
|
|
15
22
|
import { setDetectedProviders, type AiProvider } from "../providers";
|
|
16
23
|
import { setAiRunHost, setAiRuntimeCatalog, type AiRunHost } from "../runner";
|
|
17
24
|
|
|
@@ -99,10 +106,14 @@ function ScreenerHarness({
|
|
|
99
106
|
prompt,
|
|
100
107
|
providerId,
|
|
101
108
|
settings,
|
|
109
|
+
runtime: runtimeOverride,
|
|
110
|
+
onPaneState,
|
|
102
111
|
}: {
|
|
103
112
|
prompt: string;
|
|
104
113
|
providerId: string;
|
|
105
114
|
settings?: Record<string, unknown>;
|
|
115
|
+
runtime?: PluginRuntimeAccess;
|
|
116
|
+
onPaneState?: (state: PaneRuntimeState | undefined) => void;
|
|
106
117
|
}) {
|
|
107
118
|
const config = createDefaultConfig("/tmp/gloomberb-ai-screener");
|
|
108
119
|
config.layout.instances.push({
|
|
@@ -129,7 +140,11 @@ function ScreenerHarness({
|
|
|
129
140
|
]);
|
|
130
141
|
return initial;
|
|
131
142
|
});
|
|
132
|
-
const [runtime] = useState(() => makeRuntime());
|
|
143
|
+
const [runtime] = useState(() => runtimeOverride ?? makeRuntime());
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
onPaneState?.(state.paneState[PANE_ID]);
|
|
147
|
+
}, [onPaneState, state.paneState]);
|
|
133
148
|
|
|
134
149
|
return (
|
|
135
150
|
<AppContext value={{ state, dispatch }}>
|
|
@@ -185,6 +200,34 @@ afterEach(() => {
|
|
|
185
200
|
});
|
|
186
201
|
|
|
187
202
|
describe("AiScreenerPane", () => {
|
|
203
|
+
test("migrates legacy global tabs into pane-local state", async () => {
|
|
204
|
+
const runtime = makeRuntime();
|
|
205
|
+
const legacyState = {
|
|
206
|
+
tabs: [createScreenerTab("Find legacy candidates.", "anthropic")],
|
|
207
|
+
};
|
|
208
|
+
runtime.setResumeState("ai", `screener-pane:${PANE_ID}`, legacyState, 1);
|
|
209
|
+
setDetectedProviders([makeProvider("anthropic", "Claude")]);
|
|
210
|
+
let paneState: PaneRuntimeState | undefined;
|
|
211
|
+
|
|
212
|
+
testSetup = await testRender(
|
|
213
|
+
<ScreenerHarness
|
|
214
|
+
prompt=""
|
|
215
|
+
providerId="anthropic"
|
|
216
|
+
runtime={runtime}
|
|
217
|
+
onPaneState={(state) => { paneState = state; }}
|
|
218
|
+
/>,
|
|
219
|
+
{ width: 96, height: 18 },
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
await act(async () => {
|
|
223
|
+
await testSetup!.renderOnce();
|
|
224
|
+
await Bun.sleep(0);
|
|
225
|
+
await testSetup!.renderOnce();
|
|
226
|
+
});
|
|
227
|
+
expect(paneState?.pluginState?.ai?.[AI_SCREENER_PANE_STATE_KEY]).toEqual(legacyState);
|
|
228
|
+
expect(runtime.getResumeState("ai", `screener-pane:${PANE_ID}`, 1)).toEqual(EMPTY_PANE_STATE);
|
|
229
|
+
});
|
|
230
|
+
|
|
188
231
|
test("shows the account connection message before trying to run", async () => {
|
|
189
232
|
const provider = makeProvider("anthropic", "Claude");
|
|
190
233
|
setDetectedProviders([provider]);
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { Box, Text } from "../../../../ui";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type SetStateAction } from "react";
|
|
3
3
|
import type { PaneProps } from "../../../../types/plugin";
|
|
4
|
-
import { TICKER_RESEARCH_PANE_ID } from "../../../../types/config";
|
|
5
4
|
import {
|
|
6
5
|
useAppDispatch,
|
|
7
6
|
useAppSelector,
|
|
8
7
|
usePaneInstance,
|
|
9
8
|
usePaneInstanceId,
|
|
9
|
+
usePaneStateValue,
|
|
10
10
|
} from "../../../../state/app/context";
|
|
11
11
|
import {
|
|
12
12
|
useAssetData,
|
|
13
13
|
usePluginConfigState,
|
|
14
14
|
usePluginPaneState,
|
|
15
15
|
usePluginState,
|
|
16
|
-
usePluginTickerActions,
|
|
17
16
|
} from "../../../runtime";
|
|
17
|
+
import { useTickerSourceActivate } from "../../shared/ticker-source";
|
|
18
18
|
import { type DataTableKeyEvent } from "../../../../components";
|
|
19
19
|
import { colors } from "../../../../theme/colors";
|
|
20
20
|
import { t } from "../../../../i18n";
|
|
@@ -55,9 +55,11 @@ import {
|
|
|
55
55
|
import { AGE_TICK_MS } from "../../shared/auto-refresh";
|
|
56
56
|
import { useLiveStreamingSetting } from "../../shared/live-streaming";
|
|
57
57
|
|
|
58
|
+
export const AI_SCREENER_PANE_STATE_KEY = "screener";
|
|
59
|
+
|
|
58
60
|
export function AiScreenerPane({ focused, width, height }: PaneProps) {
|
|
59
61
|
const dataProvider = useAssetData();
|
|
60
|
-
const
|
|
62
|
+
const activateTicker = useTickerSourceActivate();
|
|
61
63
|
const paneId = usePaneInstanceId();
|
|
62
64
|
const paneInstance = usePaneInstance();
|
|
63
65
|
const liveStreaming = useLiveStreamingSetting();
|
|
@@ -79,13 +81,27 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
|
|
|
79
81
|
);
|
|
80
82
|
const defaultProviderId = defaults.providerId;
|
|
81
83
|
const defaultModelId = defaults.modelId;
|
|
82
|
-
const [
|
|
84
|
+
const [legacyPersistedState, setLegacyPersistedState] = usePluginState<PersistedAiScreenerPaneState>(
|
|
83
85
|
`screener-pane:${paneId}`,
|
|
84
86
|
EMPTY_PANE_STATE,
|
|
85
87
|
{ schemaVersion: 1 },
|
|
86
88
|
);
|
|
89
|
+
const [panePersistedState, setPanePersistedState] = usePluginPaneState<PersistedAiScreenerPaneState | null>(
|
|
90
|
+
AI_SCREENER_PANE_STATE_KEY,
|
|
91
|
+
null,
|
|
92
|
+
);
|
|
93
|
+
const persistedState = panePersistedState ?? legacyPersistedState;
|
|
94
|
+
const setPersistedState = useCallback((nextValue: SetStateAction<PersistedAiScreenerPaneState>) => {
|
|
95
|
+
setPanePersistedState((currentValue) => {
|
|
96
|
+
const previousValue = currentValue ?? legacyPersistedState;
|
|
97
|
+
return typeof nextValue === "function"
|
|
98
|
+
? (nextValue as (previous: PersistedAiScreenerPaneState) => PersistedAiScreenerPaneState)(previousValue)
|
|
99
|
+
: nextValue;
|
|
100
|
+
});
|
|
101
|
+
}, [legacyPersistedState, setPanePersistedState]);
|
|
87
102
|
const [activeTabId, setActiveTabId] = usePluginPaneState<string | null>("activeTabId", null);
|
|
88
|
-
|
|
103
|
+
// Top-level pane state, not plugin-scoped: this is what follower panes resolve.
|
|
104
|
+
const [cursorSymbol, setCursorSymbol] = usePaneStateValue<string | null>("cursorSymbol", null);
|
|
89
105
|
const [sorts, setSorts] = usePluginPaneState<Record<string, ScreenerSortPreference>>("sorts", {});
|
|
90
106
|
const [now, setNow] = useState(Date.now());
|
|
91
107
|
const initializedRef = useRef(false);
|
|
@@ -98,6 +114,15 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
|
|
|
98
114
|
}, []);
|
|
99
115
|
|
|
100
116
|
const tabs = useMemo(() => normalizeTabs(persistedState), [persistedState]);
|
|
117
|
+
|
|
118
|
+
// ponytail: legacy global data is ambiguous across layouts; the first mounted pane claims it.
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const legacyTabs = normalizeTabs(legacyPersistedState);
|
|
121
|
+
if (legacyTabs.length === 0) return;
|
|
122
|
+
if (panePersistedState === null) setPanePersistedState({ tabs: legacyTabs });
|
|
123
|
+
setLegacyPersistedState(EMPTY_PANE_STATE);
|
|
124
|
+
}, [legacyPersistedState, panePersistedState, setLegacyPersistedState, setPanePersistedState]);
|
|
125
|
+
|
|
101
126
|
const paneSettings = useMemo(
|
|
102
127
|
() => getAiScreenerPaneSettings(paneInstance?.settings),
|
|
103
128
|
[paneInstance?.settings],
|
|
@@ -301,12 +326,9 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
|
|
|
301
326
|
if (!isEnter || !cursorSymbol) return false;
|
|
302
327
|
event.preventDefault?.();
|
|
303
328
|
event.stopPropagation?.();
|
|
304
|
-
|
|
305
|
-
floating: !!event.shift,
|
|
306
|
-
paneType: TICKER_RESEARCH_PANE_ID,
|
|
307
|
-
});
|
|
329
|
+
activateTicker(cursorSymbol, { floating: !!event.shift, newPane: !!event.shift });
|
|
308
330
|
return true;
|
|
309
|
-
}, [
|
|
331
|
+
}, [activateTicker, cursorSymbol]);
|
|
310
332
|
|
|
311
333
|
const contentHeight = Math.max(height - 3, 4);
|
|
312
334
|
const editorProvider = editorState ? getAiProvider(editorState.providerId, providers) : null;
|
|
@@ -406,7 +428,8 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
|
|
|
406
428
|
onHeaderClick={handleHeaderClick}
|
|
407
429
|
onRootKeyDown={handleTableKeyDown}
|
|
408
430
|
onRowActivate={(ticker) => {
|
|
409
|
-
|
|
431
|
+
setCursorSymbol(ticker.metadata.ticker);
|
|
432
|
+
activateTicker(ticker.metadata.ticker, { floating: true });
|
|
410
433
|
}}
|
|
411
434
|
/>
|
|
412
435
|
)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { evaluateAlert, createAlert, serializeAlerts, deserializeAlerts } from "./alert-engine";
|
|
2
|
+
import { evaluateAlert, createAlert, editAlert, rearmAlert, serializeAlerts, deserializeAlerts } from "./alert-engine";
|
|
3
3
|
|
|
4
4
|
describe("evaluateAlert", () => {
|
|
5
5
|
test("above: triggers when price exceeds target", () => {
|
|
@@ -41,6 +41,58 @@ describe("evaluateAlert", () => {
|
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
describe("editAlert", () => {
|
|
45
|
+
test("keeps identity, re-arms, and clears trigger/quote lifecycle state", () => {
|
|
46
|
+
const alert = {
|
|
47
|
+
...createAlert("aapl", "above", 200, "NASDAQ"),
|
|
48
|
+
message: "watch this",
|
|
49
|
+
status: "triggered" as const,
|
|
50
|
+
triggeredAt: 123,
|
|
51
|
+
lastCheckedPrice: 205,
|
|
52
|
+
lastCheckedAt: 124,
|
|
53
|
+
lastCheckError: "boom",
|
|
54
|
+
lastQuoteUpdatedAt: 125,
|
|
55
|
+
lastQuoteSource: "live" as const,
|
|
56
|
+
lastQuoteProviderId: "yahoo",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const edited = editAlert(alert, "aapl", "crosses", 180);
|
|
60
|
+
|
|
61
|
+
expect(edited).toEqual({
|
|
62
|
+
id: alert.id,
|
|
63
|
+
symbol: "AAPL",
|
|
64
|
+
exchange: "NASDAQ",
|
|
65
|
+
condition: "crosses",
|
|
66
|
+
targetPrice: 180,
|
|
67
|
+
createdAt: alert.createdAt,
|
|
68
|
+
status: "active",
|
|
69
|
+
message: "watch this",
|
|
70
|
+
});
|
|
71
|
+
// A stale lastCheckedPrice would make `crosses` fire off the previous symbol's baseline.
|
|
72
|
+
expect(evaluateAlert(edited, 185)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("rearming a crosses alert drops the old quote baseline", () => {
|
|
76
|
+
const alert = {
|
|
77
|
+
...createAlert("AAPL", "crosses", 180),
|
|
78
|
+
status: "triggered" as const,
|
|
79
|
+
triggeredAt: 123,
|
|
80
|
+
lastCheckedPrice: 185,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const rearmed = rearmAlert(alert);
|
|
84
|
+
|
|
85
|
+
expect(rearmed.status).toBe("active");
|
|
86
|
+
expect(rearmed.lastCheckedPrice).toBeUndefined();
|
|
87
|
+
expect(evaluateAlert(rearmed, 175)).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("drops the exchange when the symbol changes", () => {
|
|
91
|
+
const alert = createAlert("AAPL", "above", 200, "NASDAQ");
|
|
92
|
+
expect(editAlert(alert, "tsla", "above", 200).exchange).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
44
96
|
describe("serializeAlerts / deserializeAlerts", () => {
|
|
45
97
|
test("roundtrips alerts", () => {
|
|
46
98
|
const alerts = [createAlert("AAPL", "above", 200), createAlert("TSLA", "below", 100)];
|
|
@@ -2,10 +2,16 @@ import type { AlertCondition, AlertRule } from "./types";
|
|
|
2
2
|
|
|
3
3
|
export type { AlertRule };
|
|
4
4
|
|
|
5
|
-
export function createAlert(
|
|
5
|
+
export function createAlert(
|
|
6
|
+
symbol: string,
|
|
7
|
+
condition: AlertCondition,
|
|
8
|
+
targetPrice: number,
|
|
9
|
+
exchange?: string,
|
|
10
|
+
): AlertRule {
|
|
6
11
|
return {
|
|
7
12
|
id: `alert-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
|
|
8
13
|
symbol: symbol.toUpperCase(),
|
|
14
|
+
exchange: exchange?.trim() || undefined,
|
|
9
15
|
condition,
|
|
10
16
|
targetPrice,
|
|
11
17
|
createdAt: Date.now(),
|
|
@@ -13,6 +19,33 @@ export function createAlert(symbol: string, condition: AlertCondition, targetPri
|
|
|
13
19
|
};
|
|
14
20
|
}
|
|
15
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Rebuilt from a whitelist rather than spread so every trigger/quote lifecycle
|
|
24
|
+
* field is dropped: a re-armed `crosses` alert must start from a fresh baseline.
|
|
25
|
+
*/
|
|
26
|
+
export function editAlert(
|
|
27
|
+
alert: AlertRule,
|
|
28
|
+
symbol: string,
|
|
29
|
+
condition: AlertCondition,
|
|
30
|
+
targetPrice: number,
|
|
31
|
+
): AlertRule {
|
|
32
|
+
const nextSymbol = symbol.trim().toUpperCase();
|
|
33
|
+
return {
|
|
34
|
+
id: alert.id,
|
|
35
|
+
symbol: nextSymbol,
|
|
36
|
+
exchange: nextSymbol === alert.symbol.trim().toUpperCase() ? alert.exchange : undefined,
|
|
37
|
+
condition,
|
|
38
|
+
targetPrice,
|
|
39
|
+
createdAt: alert.createdAt,
|
|
40
|
+
status: "active",
|
|
41
|
+
message: alert.message,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function rearmAlert(alert: AlertRule): AlertRule {
|
|
46
|
+
return editAlert(alert, alert.symbol, alert.condition, alert.targetPrice);
|
|
47
|
+
}
|
|
48
|
+
|
|
16
49
|
export function evaluateAlert(alert: AlertRule, currentPrice: number): boolean {
|
|
17
50
|
if (alert.status !== "active") return false;
|
|
18
51
|
|
|
@@ -148,13 +148,14 @@ function storedAlerts(): AlertRule[] {
|
|
|
148
148
|
return JSON.parse(value);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
afterEach(() => {
|
|
151
|
+
afterEach(async () => {
|
|
152
152
|
harnessState = null;
|
|
153
153
|
harnessDispatch = null;
|
|
154
|
-
if (testSetup)
|
|
155
|
-
|
|
156
|
-
testSetup
|
|
157
|
-
}
|
|
154
|
+
if (!testSetup) return;
|
|
155
|
+
await act(async () => {
|
|
156
|
+
testSetup!.renderer.destroy();
|
|
157
|
+
});
|
|
158
|
+
testSetup = undefined;
|
|
158
159
|
});
|
|
159
160
|
|
|
160
161
|
describe("AlertsPane", () => {
|
|
@@ -179,6 +180,7 @@ describe("AlertsPane", () => {
|
|
|
179
180
|
expect(frame).toContain("AAPL");
|
|
180
181
|
expect(frame).toContain("MSFT");
|
|
181
182
|
expect(frame).toContain("[a]dd alert");
|
|
183
|
+
expect(frame).toContain("[e]dit");
|
|
182
184
|
expect(frame).toContain("[d]elete");
|
|
183
185
|
expect(frame).not.toContain("Add Alert");
|
|
184
186
|
expect(frame).not.toContain("Enter");
|
|
@@ -209,6 +211,45 @@ describe("AlertsPane", () => {
|
|
|
209
211
|
expect(frame).toContain("[a]dd alert");
|
|
210
212
|
});
|
|
211
213
|
|
|
214
|
+
test("edits the selected alert through the prefilled edit dialog", async () => {
|
|
215
|
+
testSetup = await testRender(
|
|
216
|
+
<AlertsHarness alerts={[makeAlert("alert-aapl", "AAPL", "above", 200)]} />,
|
|
217
|
+
{ width: 110, height: 12 },
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
await renderSettled();
|
|
221
|
+
await act(async () => {
|
|
222
|
+
await testSetup!.mockInput.typeText("e");
|
|
223
|
+
await testSetup!.renderOnce();
|
|
224
|
+
await testSetup!.renderOnce();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const dialogFrame = testSetup.captureCharFrame();
|
|
228
|
+
expect(dialogFrame).toContain("Edit alert");
|
|
229
|
+
expect(dialogFrame).toContain("AAPL above 200");
|
|
230
|
+
|
|
231
|
+
await act(async () => {
|
|
232
|
+
for (const _ of "AAPL above 200") testSetup!.mockInput.pressBackspace();
|
|
233
|
+
await testSetup!.mockInput.typeText("MSFT crosses 310");
|
|
234
|
+
await testSetup!.renderOnce();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await act(async () => {
|
|
238
|
+
testSetup!.mockInput.pressEnter();
|
|
239
|
+
await testSetup!.renderOnce();
|
|
240
|
+
await testSetup!.renderOnce();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(storedAlerts()).toEqual([{
|
|
244
|
+
id: "alert-aapl",
|
|
245
|
+
symbol: "MSFT",
|
|
246
|
+
condition: "crosses",
|
|
247
|
+
targetPrice: 310,
|
|
248
|
+
createdAt: 1_700_000_000_000,
|
|
249
|
+
status: "active",
|
|
250
|
+
}]);
|
|
251
|
+
});
|
|
252
|
+
|
|
212
253
|
test("opens the shared alert workflow from keyboard and mouse", async () => {
|
|
213
254
|
const workflowCalls: string[] = [];
|
|
214
255
|
const runtime = makeRuntime({
|
|
@@ -229,7 +270,9 @@ describe("AlertsPane", () => {
|
|
|
229
270
|
});
|
|
230
271
|
expect(workflowCalls).toEqual(["set-alert"]);
|
|
231
272
|
|
|
232
|
-
|
|
273
|
+
await act(async () => {
|
|
274
|
+
testSetup!.renderer.destroy();
|
|
275
|
+
});
|
|
233
276
|
testSetup = await testRender(<AlertsHarness alerts={[]} runtime={runtime} />, {
|
|
234
277
|
width: 110,
|
|
235
278
|
height: 12,
|
|
@@ -102,13 +102,14 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
102
102
|
|
|
103
103
|
// One batched pass over the distinct symbols: the alerts pane reads the same
|
|
104
104
|
// persisted store, so this is the only place that talks to the provider.
|
|
105
|
-
const
|
|
106
|
-
const results = await Promise.all(
|
|
105
|
+
const quoteKeys = [...new Set(activeAlerts.map((alert) => `${alert.symbol}\0${alert.exchange ?? ""}`))];
|
|
106
|
+
const results = await Promise.all(quoteKeys.map(async (key): Promise<[string, Quote | string]> => {
|
|
107
|
+
const [symbol, exchange = ""] = key.split("\0");
|
|
107
108
|
try {
|
|
108
|
-
return [
|
|
109
|
+
return [key, await resolveAlertQuote(ctx.marketData, symbol ?? "", exchange)];
|
|
109
110
|
} catch (err) {
|
|
110
|
-
ctx.log.warn("poll: no quote", { symbol, error: String(err) });
|
|
111
|
-
return [
|
|
111
|
+
ctx.log.warn("poll: no quote", { symbol, exchange, error: String(err) });
|
|
112
|
+
return [key, createQuoteErrorMessage(symbol ?? "", err)];
|
|
112
113
|
}
|
|
113
114
|
}));
|
|
114
115
|
const quotes = new Map<string, Quote | string>(results);
|
|
@@ -116,7 +117,7 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
116
117
|
let changed = false;
|
|
117
118
|
for (const alert of alerts) {
|
|
118
119
|
if (alert.status !== "active") continue;
|
|
119
|
-
const quote = quotes.get(alert.symbol);
|
|
120
|
+
const quote = quotes.get(`${alert.symbol}\0${alert.exchange ?? ""}`);
|
|
120
121
|
if (quote === undefined) continue;
|
|
121
122
|
if (typeof quote === "string") {
|
|
122
123
|
Object.assign(alert, quoteErrorAlertFields(quote));
|
|
@@ -134,6 +135,10 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
134
135
|
desktop: "always",
|
|
135
136
|
persistent: true,
|
|
136
137
|
sound: "Glass",
|
|
138
|
+
action: {
|
|
139
|
+
label: "Open",
|
|
140
|
+
onClick: () => ctx.showPane("alerts"),
|
|
141
|
+
},
|
|
137
142
|
});
|
|
138
143
|
}
|
|
139
144
|
Object.assign(alert, quoteAlertFields(quote));
|
|
@@ -6,15 +6,20 @@ import {
|
|
|
6
6
|
type DataTableColumn,
|
|
7
7
|
type DataTableKeyEvent,
|
|
8
8
|
} from "../../../components";
|
|
9
|
+
import { TextFieldDialog } from "../../../components/pane-settings-dialog/field-dialogs";
|
|
9
10
|
import { colors } from "../../../theme/colors";
|
|
10
11
|
import { TextAttributes } from "../../../ui";
|
|
12
|
+
import { useDialog, type AlertContext } from "../../../ui/dialog";
|
|
11
13
|
import type { PaneProps } from "../../../types/plugin";
|
|
12
14
|
import { usePluginAppActions, usePluginConfigState } from "../../runtime";
|
|
13
15
|
import {
|
|
14
16
|
deserializeAlerts,
|
|
17
|
+
editAlert,
|
|
18
|
+
rearmAlert as rebuildAlert,
|
|
15
19
|
readAlertsStoreError,
|
|
16
20
|
serializeAlerts,
|
|
17
21
|
} from "./alert-engine";
|
|
22
|
+
import { parseAlertCommandValues } from "./command";
|
|
18
23
|
import { ALERTS_KEY } from "./constants";
|
|
19
24
|
import {
|
|
20
25
|
conditionLabel,
|
|
@@ -59,6 +64,7 @@ const ALERT_TABLE_CONTENT_WIDTH = ALERT_COLUMNS.reduce(
|
|
|
59
64
|
export function AlertsPane({ focused, width, height, close }: PaneProps) {
|
|
60
65
|
const [alertsJson, setAlertsJson] = usePluginConfigState<string>(ALERTS_KEY, "[]");
|
|
61
66
|
const { openPluginCommandWorkflow } = usePluginAppActions();
|
|
67
|
+
const dialog = useDialog();
|
|
62
68
|
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
63
69
|
const showHorizontalScrollbar = ALERT_TABLE_CONTENT_WIDTH > width;
|
|
64
70
|
const storeError = useMemo(() => readAlertsStoreError(alertsJson), [alertsJson]);
|
|
@@ -92,14 +98,7 @@ export function AlertsPane({ focused, width, height, close }: PaneProps) {
|
|
|
92
98
|
|
|
93
99
|
const rearmAlert = useCallback((id: string) => {
|
|
94
100
|
savePaneAlerts(
|
|
95
|
-
alerts.map((a) =>
|
|
96
|
-
a.id === id ? {
|
|
97
|
-
...a,
|
|
98
|
-
status: "active" as const,
|
|
99
|
-
triggeredAt: undefined,
|
|
100
|
-
lastCheckError: undefined,
|
|
101
|
-
} : a,
|
|
102
|
-
),
|
|
101
|
+
alerts.map((a) => (a.id === id ? rebuildAlert(a) : a)),
|
|
103
102
|
);
|
|
104
103
|
}, [alerts, savePaneAlerts]);
|
|
105
104
|
|
|
@@ -112,6 +111,36 @@ export function AlertsPane({ focused, width, height, close }: PaneProps) {
|
|
|
112
111
|
if (selected) deleteAlert(selected.id);
|
|
113
112
|
}, [deleteAlert, rows, selectedIdx]);
|
|
114
113
|
|
|
114
|
+
const editSelectedAlert = useCallback(() => {
|
|
115
|
+
const selected = rows[selectedIdx];
|
|
116
|
+
if (!selected) return;
|
|
117
|
+
void dialog.alert({
|
|
118
|
+
closeOnClickOutside: true,
|
|
119
|
+
content: (context: AlertContext) => (
|
|
120
|
+
<TextFieldDialog
|
|
121
|
+
{...context}
|
|
122
|
+
field={{
|
|
123
|
+
type: "text",
|
|
124
|
+
key: "alert",
|
|
125
|
+
label: "Edit alert",
|
|
126
|
+
description: "SYMBOL above|below|crosses PRICE",
|
|
127
|
+
placeholder: "AAPL above 200",
|
|
128
|
+
}}
|
|
129
|
+
currentValue={`${selected.symbol} ${selected.condition} ${selected.targetPrice}`}
|
|
130
|
+
onApply={async (value) => {
|
|
131
|
+
const parsed = parseAlertCommandValues({ shortcut: value });
|
|
132
|
+
if (!parsed) throw new Error("Use SYMBOL above|below|crosses PRICE.");
|
|
133
|
+
savePaneAlerts((current) => current.map((alert) => (
|
|
134
|
+
alert.id === selected.id
|
|
135
|
+
? editAlert(alert, parsed.symbol, parsed.condition, parsed.price)
|
|
136
|
+
: alert
|
|
137
|
+
)));
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
),
|
|
141
|
+
});
|
|
142
|
+
}, [dialog, rows, savePaneAlerts, selectedIdx]);
|
|
143
|
+
|
|
115
144
|
// Quotes come from the plugin's single background poll, which writes into the
|
|
116
145
|
// same persisted store, so the pane never fetches on its own.
|
|
117
146
|
|
|
@@ -123,6 +152,13 @@ export function AlertsPane({ focused, width, height, close }: PaneProps) {
|
|
|
123
152
|
: [],
|
|
124
153
|
hints: [
|
|
125
154
|
{ id: "add", key: "a", label: "dd alert", onPress: startAddAlert },
|
|
155
|
+
{
|
|
156
|
+
id: "edit",
|
|
157
|
+
key: "e",
|
|
158
|
+
label: "dit",
|
|
159
|
+
onPress: editSelectedAlert,
|
|
160
|
+
disabled: rows.length === 0,
|
|
161
|
+
},
|
|
126
162
|
{
|
|
127
163
|
id: "delete",
|
|
128
164
|
key: "d",
|
|
@@ -133,6 +169,7 @@ export function AlertsPane({ focused, width, height, close }: PaneProps) {
|
|
|
133
169
|
],
|
|
134
170
|
}), [
|
|
135
171
|
deleteSelectedAlert,
|
|
172
|
+
editSelectedAlert,
|
|
136
173
|
quoteError,
|
|
137
174
|
rows.length,
|
|
138
175
|
startAddAlert,
|
|
@@ -154,13 +191,18 @@ export function AlertsPane({ focused, width, height, close }: PaneProps) {
|
|
|
154
191
|
startAddAlert();
|
|
155
192
|
return true;
|
|
156
193
|
}
|
|
194
|
+
if (event.name === "e") {
|
|
195
|
+
event.preventDefault?.();
|
|
196
|
+
editSelectedAlert();
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
157
199
|
if (event.name === "escape") {
|
|
158
200
|
event.preventDefault?.();
|
|
159
201
|
close?.();
|
|
160
202
|
return true;
|
|
161
203
|
}
|
|
162
204
|
return false;
|
|
163
|
-
}, [close, deleteSelectedAlert, startAddAlert]);
|
|
205
|
+
}, [close, deleteSelectedAlert, editSelectedAlert, startAddAlert]);
|
|
164
206
|
|
|
165
207
|
const renderCell = useCallback((
|
|
166
208
|
alert: AlertRule,
|