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
|
@@ -340,6 +340,83 @@ describe("prediction markets pane interactions", () => {
|
|
|
340
340
|
expect(frame).toContain("/ search markets");
|
|
341
341
|
});
|
|
342
342
|
|
|
343
|
+
test("navigates the book and trades tables with arrow keys", async () => {
|
|
344
|
+
installPredictionMarketMocks();
|
|
345
|
+
|
|
346
|
+
testSetup = await testRender(<Harness />, { width: 120, height: 34 });
|
|
347
|
+
await flushFrames(testSetup);
|
|
348
|
+
await emitKeypress(testSetup, { name: "j", sequence: "j" });
|
|
349
|
+
await emitKeypress(testSetup, { name: "enter", sequence: "\r" });
|
|
350
|
+
await flushFrames(testSetup);
|
|
351
|
+
|
|
352
|
+
let rows = testSetup.captureCharFrame().split("\n");
|
|
353
|
+
const detailTabsRow = rows.findIndex((line) =>
|
|
354
|
+
line.includes("Overview") && line.includes("Book") && line.includes("Trades"),
|
|
355
|
+
);
|
|
356
|
+
const bookColumn = rows[detailTabsRow]!.indexOf("Book");
|
|
357
|
+
await act(async () => {
|
|
358
|
+
await testSetup!.mockMouse.click(bookColumn, detailTabsRow);
|
|
359
|
+
await testSetup!.renderOnce();
|
|
360
|
+
});
|
|
361
|
+
await flushFrames(testSetup);
|
|
362
|
+
|
|
363
|
+
rows = testSetup.captureCharFrame().split("\n");
|
|
364
|
+
let headerRow = rows.findIndex((line) =>
|
|
365
|
+
line.includes("OUT") && line.includes("SIDE") && line.includes("PRICE"),
|
|
366
|
+
);
|
|
367
|
+
expect(headerRow).toBeGreaterThanOrEqual(0);
|
|
368
|
+
let spans = testSetup.captureSpans().lines;
|
|
369
|
+
const bookFirstBefore = spans[headerRow + 1]!.spans.find(
|
|
370
|
+
(span) => span.text.trim(),
|
|
371
|
+
)!.bg;
|
|
372
|
+
const bookSecondBefore = spans[headerRow + 2]!.spans.find(
|
|
373
|
+
(span) => span.text.trim(),
|
|
374
|
+
)!.bg;
|
|
375
|
+
expect(bookFirstBefore).not.toEqual(bookSecondBefore);
|
|
376
|
+
|
|
377
|
+
await emitKeypress(testSetup, { name: "down", sequence: "\u001b[B" });
|
|
378
|
+
await flushFrames(testSetup);
|
|
379
|
+
spans = testSetup.captureSpans().lines;
|
|
380
|
+
expect(
|
|
381
|
+
spans[headerRow + 1]!.spans.find((span) => span.text.trim())!.bg,
|
|
382
|
+
).toEqual(bookSecondBefore);
|
|
383
|
+
expect(
|
|
384
|
+
spans[headerRow + 2]!.spans.find((span) => span.text.trim())!.bg,
|
|
385
|
+
).toEqual(bookFirstBefore);
|
|
386
|
+
|
|
387
|
+
rows = testSetup.captureCharFrame().split("\n");
|
|
388
|
+
const tradesColumn = rows[detailTabsRow]!.indexOf("Trades");
|
|
389
|
+
await act(async () => {
|
|
390
|
+
await testSetup!.mockMouse.click(tradesColumn, detailTabsRow);
|
|
391
|
+
await testSetup!.renderOnce();
|
|
392
|
+
});
|
|
393
|
+
await flushFrames(testSetup);
|
|
394
|
+
|
|
395
|
+
rows = testSetup.captureCharFrame().split("\n");
|
|
396
|
+
headerRow = rows.findIndex((line) =>
|
|
397
|
+
line.includes("TIME") && line.includes("SIDE") && line.includes("PRICE"),
|
|
398
|
+
);
|
|
399
|
+
expect(headerRow).toBeGreaterThanOrEqual(0);
|
|
400
|
+
spans = testSetup.captureSpans().lines;
|
|
401
|
+
const tradeFirstBefore = spans[headerRow + 1]!.spans.find(
|
|
402
|
+
(span) => span.text.trim(),
|
|
403
|
+
)!.bg;
|
|
404
|
+
const tradeSecondBefore = spans[headerRow + 2]!.spans.find(
|
|
405
|
+
(span) => span.text.trim(),
|
|
406
|
+
)!.bg;
|
|
407
|
+
expect(tradeFirstBefore).not.toEqual(tradeSecondBefore);
|
|
408
|
+
|
|
409
|
+
await emitKeypress(testSetup, { name: "down", sequence: "\u001b[B" });
|
|
410
|
+
await flushFrames(testSetup);
|
|
411
|
+
spans = testSetup.captureSpans().lines;
|
|
412
|
+
expect(
|
|
413
|
+
spans[headerRow + 1]!.spans.find((span) => span.text.trim())!.bg,
|
|
414
|
+
).toEqual(tradeSecondBefore);
|
|
415
|
+
expect(
|
|
416
|
+
spans[headerRow + 2]!.spans.find((span) => span.text.trim())!.bg,
|
|
417
|
+
).toEqual(tradeFirstBefore);
|
|
418
|
+
});
|
|
419
|
+
|
|
343
420
|
test("supports detail outcome navigation and escape return from the keyboard", async () => {
|
|
344
421
|
attachPredictionMarketsPersistence(new MemoryPersistence());
|
|
345
422
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Spinner,
|
|
6
6
|
Tabs,
|
|
7
7
|
usePaneFooter,
|
|
8
|
+
useTableLoadMore,
|
|
8
9
|
type DataTableKeyEvent,
|
|
9
10
|
type DataTableRootKeyContext,
|
|
10
11
|
} from "../../components";
|
|
@@ -238,6 +239,12 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
|
|
|
238
239
|
watchlistedRowKeys,
|
|
239
240
|
]);
|
|
240
241
|
|
|
242
|
+
const onCatalogScroll = useTableLoadMore(
|
|
243
|
+
controller.scrollRef,
|
|
244
|
+
controller.catalogHasMore && !controller.catalogLoadingMore && !controller.detailOpen,
|
|
245
|
+
() => { void controller.actions.loadMoreCatalog(); },
|
|
246
|
+
);
|
|
247
|
+
|
|
241
248
|
const handleRootKeyDown = useCallback((
|
|
242
249
|
event: DataTableKeyEvent,
|
|
243
250
|
context: DataTableRootKeyContext,
|
|
@@ -255,9 +262,13 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
|
|
|
255
262
|
<Box
|
|
256
263
|
flexDirection="column"
|
|
257
264
|
flexGrow={1}
|
|
265
|
+
flexShrink={1}
|
|
266
|
+
flexBasis={0}
|
|
267
|
+
minHeight={0}
|
|
258
268
|
width={width}
|
|
259
269
|
height={Math.max(height - 1, 1)}
|
|
260
270
|
paddingX={1}
|
|
271
|
+
overflow="hidden"
|
|
261
272
|
backgroundColor={colors.panel}
|
|
262
273
|
>
|
|
263
274
|
<PredictionMarketDetailPane
|
|
@@ -314,6 +325,7 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
|
|
|
314
325
|
scrollRef={controller.scrollRef}
|
|
315
326
|
getItemKey={(row) => row.key}
|
|
316
327
|
virtualize
|
|
328
|
+
onBodyScrollActivity={onCatalogScroll}
|
|
317
329
|
renderCell={renderCell}
|
|
318
330
|
emptyContent={
|
|
319
331
|
rowsLoading ? (
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createDefaultConfig } from "../../types/config";
|
|
3
|
+
import type { CommandDef } from "../../types/plugin";
|
|
2
4
|
import {
|
|
3
5
|
cleanupPredictionTest,
|
|
4
6
|
installPredictionMarketMocks,
|
|
@@ -23,12 +25,46 @@ import {
|
|
|
23
25
|
loadPolymarketDetail,
|
|
24
26
|
normalizePolymarketMarket,
|
|
25
27
|
} from "./services/polymarket/adapter";
|
|
28
|
+
import { predictionMarketsPlugin } from "./index";
|
|
26
29
|
|
|
27
30
|
afterEach(async () => {
|
|
28
31
|
await cleanupPredictionTest();
|
|
29
32
|
});
|
|
30
33
|
|
|
31
34
|
describe("prediction markets plugin registration and services", () => {
|
|
35
|
+
test("routes search state to a materialized pane instance", async () => {
|
|
36
|
+
const config = createDefaultConfig("/tmp/gloomberb-prediction-command-test");
|
|
37
|
+
const instanceId = "prediction-markets:shared-test";
|
|
38
|
+
config.layout.instances.push({ instanceId, paneId: "prediction-markets" });
|
|
39
|
+
const commands: CommandDef[] = [];
|
|
40
|
+
const paneStateUpdates: Array<{ paneId: string; key: string; value: unknown }> = [];
|
|
41
|
+
const focused: string[] = [];
|
|
42
|
+
|
|
43
|
+
predictionMarketsPlugin.setup?.({
|
|
44
|
+
persistence: new MemoryPersistence(),
|
|
45
|
+
getConfig: () => config,
|
|
46
|
+
resume: {
|
|
47
|
+
setPaneState(paneId: string, key: string, value: unknown) {
|
|
48
|
+
paneStateUpdates.push({ paneId, key, value });
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
registerCommand: (command: CommandDef) => commands.push(command),
|
|
52
|
+
focusPane: (paneId: string) => focused.push(paneId),
|
|
53
|
+
} as any);
|
|
54
|
+
|
|
55
|
+
await commands.find((command) => command.id === "prediction-markets-search")?.execute({
|
|
56
|
+
query: "polymarket:fed",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(paneStateUpdates).toEqual([
|
|
60
|
+
{ paneId: instanceId, key: "venueScope", value: "polymarket" },
|
|
61
|
+
{ paneId: instanceId, key: "searchQuery", value: "fed" },
|
|
62
|
+
{ paneId: instanceId, key: "selectedRowKey", value: null },
|
|
63
|
+
{ paneId: instanceId, key: "selectedDetailMarketKey", value: null },
|
|
64
|
+
]);
|
|
65
|
+
expect(focused).toEqual([instanceId]);
|
|
66
|
+
});
|
|
67
|
+
|
|
32
68
|
test("uses fresh prediction resource cache without refetching", async () => {
|
|
33
69
|
const persistence = new MemoryPersistence();
|
|
34
70
|
attachPredictionMarketsPersistence(persistence);
|
|
@@ -39,6 +39,23 @@ export { normalizeKalshiMarket } from "./normalize";
|
|
|
39
39
|
const KALSHI_EVENT_PAGE_LIMIT = 200;
|
|
40
40
|
const DEFAULT_KALSHI_EVENT_MAX_PAGES = 3;
|
|
41
41
|
const SEARCH_KALSHI_EVENT_MAX_PAGES = 3;
|
|
42
|
+
const kalshiCursors = new Map<string, string | null>();
|
|
43
|
+
|
|
44
|
+
function kalshiCursorKey(searchQuery: string, categoryId: PredictionCategoryId): string {
|
|
45
|
+
return `${categoryId}:${searchQuery.trim().toLowerCase()}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function rememberKalshiCursor(
|
|
49
|
+
searchQuery: string,
|
|
50
|
+
categoryId: PredictionCategoryId,
|
|
51
|
+
cursor: string | null,
|
|
52
|
+
): void {
|
|
53
|
+
kalshiCursors.set(kalshiCursorKey(searchQuery, categoryId), cursor);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function kalshiCatalogCursor(searchQuery: string, categoryId: PredictionCategoryId): string | null {
|
|
57
|
+
return kalshiCursors.get(kalshiCursorKey(searchQuery, categoryId)) ?? null;
|
|
58
|
+
}
|
|
42
59
|
|
|
43
60
|
function kalshiSeriesTickerFromEvent(eventTicker: string | undefined): string | undefined {
|
|
44
61
|
const trimmed = eventTicker?.trim().toUpperCase();
|
|
@@ -61,9 +78,10 @@ async function fetchKalshiCatalogEvents(
|
|
|
61
78
|
maxPages = DEFAULT_KALSHI_EVENT_MAX_PAGES,
|
|
62
79
|
limit = KALSHI_EVENT_PAGE_LIMIT,
|
|
63
80
|
signal?: AbortSignal,
|
|
64
|
-
|
|
81
|
+
startCursor?: string,
|
|
82
|
+
): Promise<{ events: KalshiEventRecord[]; nextCursor: string | null }> {
|
|
65
83
|
const events: KalshiEventRecord[] = [];
|
|
66
|
-
let cursor: string | undefined;
|
|
84
|
+
let cursor: string | undefined = startCursor;
|
|
67
85
|
|
|
68
86
|
for (let page = 0; page < maxPages; page += 1) {
|
|
69
87
|
const response = await fetchJson<KalshiEventsResponse>(
|
|
@@ -75,7 +93,7 @@ async function fetchKalshiCatalogEvents(
|
|
|
75
93
|
if (!cursor) break;
|
|
76
94
|
}
|
|
77
95
|
|
|
78
|
-
return events;
|
|
96
|
+
return { events, nextCursor: cursor ?? null };
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
async function fetchKalshiCatalogEventsForCategory(
|
|
@@ -83,13 +101,15 @@ async function fetchKalshiCatalogEventsForCategory(
|
|
|
83
101
|
maxPages = DEFAULT_KALSHI_EVENT_MAX_PAGES,
|
|
84
102
|
limit = KALSHI_EVENT_PAGE_LIMIT,
|
|
85
103
|
signal?: AbortSignal,
|
|
86
|
-
|
|
104
|
+
startCursor?: string,
|
|
105
|
+
): Promise<{ events: KalshiEventRecord[]; nextCursor: string | null }> {
|
|
87
106
|
const categories = getKalshiCategoryNames(categoryId);
|
|
88
|
-
if (categories.length === 0) return await fetchKalshiCatalogEvents(maxPages, limit, signal);
|
|
107
|
+
if (categories.length === 0) return await fetchKalshiCatalogEvents(maxPages, limit, signal, startCursor);
|
|
89
108
|
|
|
90
109
|
const deduped = new Map<string, KalshiEventRecord>();
|
|
110
|
+
let nextCursor: string | null = null;
|
|
91
111
|
for (const category of categories) {
|
|
92
|
-
let cursor: string | undefined;
|
|
112
|
+
let cursor: string | undefined = startCursor;
|
|
93
113
|
for (let page = 0; page < maxPages; page += 1) {
|
|
94
114
|
const response = await fetchJson<KalshiEventsResponse>(
|
|
95
115
|
buildKalshiCatalogUrl(cursor, category, limit),
|
|
@@ -102,9 +122,10 @@ async function fetchKalshiCatalogEventsForCategory(
|
|
|
102
122
|
cursor = response.cursor?.trim() || undefined;
|
|
103
123
|
if (!cursor) break;
|
|
104
124
|
}
|
|
125
|
+
nextCursor = cursor ?? nextCursor;
|
|
105
126
|
}
|
|
106
127
|
|
|
107
|
-
return [...deduped.values()];
|
|
128
|
+
return { events: [...deduped.values()], nextCursor };
|
|
108
129
|
}
|
|
109
130
|
|
|
110
131
|
export async function loadKalshiCatalog(
|
|
@@ -120,15 +141,34 @@ export async function loadKalshiCatalog(
|
|
|
120
141
|
"catalog",
|
|
121
142
|
`${buildPredictionCatalogResourceKey("kalshi", categoryId, normalizedQuery)}:${requestedLimit}`,
|
|
122
143
|
async () => {
|
|
123
|
-
const
|
|
144
|
+
const page = categoryId === "all"
|
|
124
145
|
? await fetchKalshiCatalogEvents(maxPages, pageLimit, options.signal)
|
|
125
146
|
: await fetchKalshiCatalogEventsForCategory(categoryId, maxPages, pageLimit, options.signal);
|
|
126
|
-
|
|
147
|
+
rememberKalshiCursor(normalizedQuery, categoryId, page.nextCursor);
|
|
148
|
+
return normalizeKalshiCatalog(page.events, normalizedQuery, categoryId).slice(0, requestedLimit);
|
|
127
149
|
},
|
|
128
150
|
PREDICTION_CACHE_POLICIES.catalog,
|
|
129
151
|
);
|
|
130
152
|
}
|
|
131
153
|
|
|
154
|
+
export async function loadMoreKalshiCatalog(
|
|
155
|
+
searchQuery: string,
|
|
156
|
+
categoryId: PredictionCategoryId,
|
|
157
|
+
cursor: string,
|
|
158
|
+
signal?: AbortSignal,
|
|
159
|
+
): Promise<{ markets: PredictionMarketSummary[]; nextCursor: string | null; hasMore: boolean }> {
|
|
160
|
+
const normalizedQuery = searchQuery.trim().toLowerCase();
|
|
161
|
+
const page = categoryId === "all"
|
|
162
|
+
? await fetchKalshiCatalogEvents(1, KALSHI_EVENT_PAGE_LIMIT, signal, cursor)
|
|
163
|
+
: await fetchKalshiCatalogEventsForCategory(categoryId, 1, KALSHI_EVENT_PAGE_LIMIT, signal, cursor);
|
|
164
|
+
rememberKalshiCursor(normalizedQuery, categoryId, page.nextCursor);
|
|
165
|
+
return {
|
|
166
|
+
markets: normalizeKalshiCatalog(page.events, normalizedQuery, categoryId),
|
|
167
|
+
nextCursor: page.nextCursor,
|
|
168
|
+
hasMore: !!page.nextCursor,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
132
172
|
async function loadKalshiEvent(
|
|
133
173
|
eventTicker: string | undefined,
|
|
134
174
|
): Promise<KalshiEventResponse | null> {
|
|
@@ -30,6 +30,16 @@ export { loadPolymarketDetail } from "./detail";
|
|
|
30
30
|
|
|
31
31
|
const POLYMARKET_CATALOG_OFFSETS = [0, 200, 400];
|
|
32
32
|
const POLYMARKET_CATEGORY_OFFSETS = [0, 200];
|
|
33
|
+
const POLYMARKET_PAGE_SIZE = 200;
|
|
34
|
+
|
|
35
|
+
export function nextPolymarketCatalogOffset(
|
|
36
|
+
categoryId: PredictionCategoryId,
|
|
37
|
+
searchQuery = "",
|
|
38
|
+
): number | null {
|
|
39
|
+
if (searchQuery.trim()) return null;
|
|
40
|
+
const offsets = categoryId === "all" ? POLYMARKET_CATALOG_OFFSETS : POLYMARKET_CATEGORY_OFFSETS;
|
|
41
|
+
return (offsets.at(-1) ?? 0) + POLYMARKET_PAGE_SIZE;
|
|
42
|
+
}
|
|
33
43
|
|
|
34
44
|
function buildPolymarketCatalogUrl(offset: number, tagSlug?: string, limit = 200): string {
|
|
35
45
|
const url = new URL("https://gamma-api.polymarket.com/events");
|
|
@@ -144,3 +154,24 @@ export async function loadPolymarketCatalog(
|
|
|
144
154
|
PREDICTION_CACHE_POLICIES.catalog,
|
|
145
155
|
);
|
|
146
156
|
}
|
|
157
|
+
|
|
158
|
+
export async function loadMorePolymarketCatalog(
|
|
159
|
+
searchQuery: string,
|
|
160
|
+
categoryId: PredictionCategoryId,
|
|
161
|
+
offset: number,
|
|
162
|
+
signal?: AbortSignal,
|
|
163
|
+
): Promise<{ markets: PredictionMarketSummary[]; hasMore: boolean; nextOffset: number }> {
|
|
164
|
+
if (searchQuery.trim()) {
|
|
165
|
+
return { markets: [], hasMore: false, nextOffset: offset };
|
|
166
|
+
}
|
|
167
|
+
const tagSlugs = categoryId === "all" ? [undefined] : getPolymarketCategoryTagSlugs(categoryId);
|
|
168
|
+
const pages = await Promise.all(
|
|
169
|
+
tagSlugs.map((tagSlug) => loadPolymarketCatalogPages([offset], tagSlug, POLYMARKET_PAGE_SIZE, signal).catch(() => [])),
|
|
170
|
+
);
|
|
171
|
+
const raw = pages.flat();
|
|
172
|
+
return {
|
|
173
|
+
markets: normalizePolymarketCatalog(raw, "", categoryId),
|
|
174
|
+
hasMore: raw.length >= POLYMARKET_PAGE_SIZE,
|
|
175
|
+
nextOffset: offset + POLYMARKET_PAGE_SIZE,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -274,6 +274,15 @@ export function installPredictionMarketMocks() {
|
|
|
274
274
|
count_fp: "50",
|
|
275
275
|
created_time: "2026-04-01T00:00:00Z",
|
|
276
276
|
},
|
|
277
|
+
{
|
|
278
|
+
trade_id: "kal-2",
|
|
279
|
+
ticker: "KAL-1",
|
|
280
|
+
taker_side: "no",
|
|
281
|
+
yes_price_dollars: "0.46",
|
|
282
|
+
no_price_dollars: "0.54",
|
|
283
|
+
count_fp: "40",
|
|
284
|
+
created_time: "2026-04-01T00:01:00Z",
|
|
285
|
+
},
|
|
277
286
|
],
|
|
278
287
|
}),
|
|
279
288
|
{ status: 200 },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { AppPersistencePort, AppTickerRepositoryPort } from "../../core/app-service-ports";
|
|
3
|
+
import type { LayoutMarketplacePayload } from "../../layout-marketplace/payload";
|
|
3
4
|
import {
|
|
4
5
|
connectionHealth as sharedConnectionHealth,
|
|
5
6
|
type ConnectionHealthRegistry,
|
|
@@ -129,6 +130,7 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
129
130
|
showPaneFn: ((paneId: string) => void) = () => {};
|
|
130
131
|
createPaneFromTemplateFn: ((templateId: string, options?: PaneTemplateCreateOptions) => void) = () => {};
|
|
131
132
|
createPaneFromTemplateAsyncFn: ((templateId: string, options?: PaneTemplateCreateOptions) => Promise<void>) = async () => {};
|
|
133
|
+
openPortablePaneShareAsyncFn: ((layout: LayoutMarketplacePayload) => Promise<void>) = async () => {};
|
|
132
134
|
hidePaneFn: ((paneId: string) => void) = () => {};
|
|
133
135
|
focusPaneFn: ((paneId: string) => void) = () => {};
|
|
134
136
|
pinTickerFn: ((symbol: string, options?: PinTickerOptions) => void) = () => {};
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
} from "../types/config";
|
|
8
8
|
import {
|
|
9
9
|
findFixedTickerPaneForSymbol,
|
|
10
|
+
findTickerResearchFollower,
|
|
11
|
+
resolveTickerActivation,
|
|
10
12
|
resolveTickerNavigationReplacementPane,
|
|
11
13
|
shouldFocusTickerNavigationTarget,
|
|
12
14
|
} from "./ticker-navigation";
|
|
@@ -26,6 +28,28 @@ function createLayout(instances: PaneInstanceConfig[]): LayoutConfig {
|
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
describe("ticker source activation", () => {
|
|
32
|
+
test("focuses a bound follower unless the caller asks for a new pane", () => {
|
|
33
|
+
const layout = createLayout([
|
|
34
|
+
createPaneInstance("portfolio-list", {
|
|
35
|
+
instanceId: "portfolio-list:main",
|
|
36
|
+
binding: { kind: "none" },
|
|
37
|
+
}),
|
|
38
|
+
createPaneInstance("ticker-detail", {
|
|
39
|
+
instanceId: "ticker-detail:main",
|
|
40
|
+
binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
|
|
41
|
+
}),
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
expect(findTickerResearchFollower(layout, "portfolio-list:main")?.instanceId).toBe("ticker-detail:main");
|
|
45
|
+
expect(resolveTickerActivation(layout, "portfolio-list:main")).toEqual({
|
|
46
|
+
kind: "focus",
|
|
47
|
+
paneId: "ticker-detail:main",
|
|
48
|
+
});
|
|
49
|
+
expect(resolveTickerActivation(layout, "portfolio-list:main", { newPane: true })).toEqual({ kind: "open" });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
29
53
|
describe("resolveTickerNavigationReplacementPane", () => {
|
|
30
54
|
test("only replaces the Ticker Research pane that opened ticker navigation", () => {
|
|
31
55
|
const layout = createLayout([
|
|
@@ -5,8 +5,48 @@ import {
|
|
|
5
5
|
type LayoutConfig,
|
|
6
6
|
type PaneInstanceConfig,
|
|
7
7
|
} from "../types/config";
|
|
8
|
+
import type { PaneDef } from "../types/plugin";
|
|
8
9
|
import { isPaneInLayout } from "./pane-manager";
|
|
9
10
|
|
|
11
|
+
/** Panes that publish a cursor symbol other panes can follow (`PaneDef.tickerSource`). */
|
|
12
|
+
export function listVisibleTickerSourcePanes(
|
|
13
|
+
layout: LayoutConfig,
|
|
14
|
+
panes: ReadonlyMap<string, PaneDef>,
|
|
15
|
+
): PaneInstanceConfig[] {
|
|
16
|
+
return layout.instances.filter((instance) => (
|
|
17
|
+
panes.get(instance.paneId)?.tickerSource === true
|
|
18
|
+
&& isPaneInLayout(layout, instance.instanceId)
|
|
19
|
+
));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The visible Ticker Research pane that follows `sourceInstanceId`, if any. */
|
|
23
|
+
export function findTickerResearchFollower(
|
|
24
|
+
layout: LayoutConfig,
|
|
25
|
+
sourceInstanceId: string | null | undefined,
|
|
26
|
+
): PaneInstanceConfig | null {
|
|
27
|
+
if (!sourceInstanceId) return null;
|
|
28
|
+
return layout.instances.find((instance) =>
|
|
29
|
+
instance.paneId === TICKER_RESEARCH_PANE_ID
|
|
30
|
+
&& instance.binding?.kind === "follow"
|
|
31
|
+
&& instance.binding.sourceInstanceId === sourceInstanceId
|
|
32
|
+
&& isPaneInLayout(layout, instance.instanceId)
|
|
33
|
+
) ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Enter opens the follower already bound to the source pane; Shift+Enter (`newPane`) and sources
|
|
38
|
+
* without a follower fall back to opening a ticker pane.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveTickerActivation(
|
|
41
|
+
layout: LayoutConfig,
|
|
42
|
+
sourcePaneId: string | null | undefined,
|
|
43
|
+
options?: { newPane?: boolean },
|
|
44
|
+
): { kind: "focus"; paneId: string } | { kind: "open" } {
|
|
45
|
+
if (options?.newPane) return { kind: "open" };
|
|
46
|
+
const follower = findTickerResearchFollower(layout, sourcePaneId);
|
|
47
|
+
return follower ? { kind: "focus", paneId: follower.instanceId } : { kind: "open" };
|
|
48
|
+
}
|
|
49
|
+
|
|
10
50
|
export function resolveTickerNavigationReplacementPane(
|
|
11
51
|
layout: LayoutConfig,
|
|
12
52
|
sourcePaneId: string | null,
|
package/src/remote/schema.ts
CHANGED
|
@@ -43,7 +43,7 @@ export const REMOTE_OPERATIONS: RemoteOperationSchema[] = [
|
|
|
43
43
|
op("layout.delete", "Delete a layout.", "{ index: number }", "local-write"),
|
|
44
44
|
op("layout.undo", "Undo last layout change.", "{}", "local-write"),
|
|
45
45
|
op("layout.redo", "Redo last layout change.", "{}", "local-write"),
|
|
46
|
-
op("layout.gridlock", "
|
|
46
|
+
op("layout.gridlock", "Tidy all panes into a dense layout.", "{}", "local-write"),
|
|
47
47
|
op("layout.closeFloating", "Close all floating panes in the active layout.", "{}", "local-write"),
|
|
48
48
|
op("layout.placePane", "Move a pane to a layout region.", "{ paneId: string, region: 'left' | 'right' | 'top' | 'bottom' | 'floating', relativeTo?: string }", "local-write"),
|
|
49
49
|
op("layout.focusRegion", "Focus a pane by visual layout region.", "{ region: 'left' | 'right' | 'top' | 'bottom' | 'center' }", "local-write"),
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { newsProvider, type NewsCapability } from "../../capabilities";
|
|
2
|
+
import type { AppRuntimeServices, AppServicesFactoryOptions } from "../../core/app-service-ports";
|
|
3
|
+
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
4
|
+
import { NewsService } from "../../news/aggregator";
|
|
5
|
+
import { setSharedNewsService } from "../../news/hooks";
|
|
6
|
+
import { PluginRegistry } from "../../plugins/registry";
|
|
7
|
+
import { createGloomberbCloudCapabilities, createGloomberbCloudProvider } from "../../sources/gloomberb-cloud";
|
|
8
|
+
import { AssetDataRouter } from "../../sources/provider-router";
|
|
9
|
+
import { BrowserPersistence } from "./persistence";
|
|
10
|
+
import { BrowserTickerRepository } from "./ticker-repository";
|
|
11
|
+
|
|
12
|
+
export function createBrowserAppServices({
|
|
13
|
+
config,
|
|
14
|
+
plugins,
|
|
15
|
+
}: AppServicesFactoryOptions): AppRuntimeServices {
|
|
16
|
+
const persistence = new BrowserPersistence(localStorage);
|
|
17
|
+
const tickerRepository = new BrowserTickerRepository(localStorage);
|
|
18
|
+
const cloudProvider = createGloomberbCloudProvider();
|
|
19
|
+
const dataProvider = new AssetDataRouter(null, [cloudProvider]);
|
|
20
|
+
const marketData = new MarketDataCoordinator(dataProvider);
|
|
21
|
+
const cloudNews = createGloomberbCloudCapabilities(cloudProvider).find(
|
|
22
|
+
(capability): capability is NewsCapability => capability.kind === "news",
|
|
23
|
+
);
|
|
24
|
+
const pluginRegistry = new PluginRegistry(dataProvider, tickerRepository, persistence);
|
|
25
|
+
dataProvider.attachRegistry(pluginRegistry);
|
|
26
|
+
const newsService = new NewsService({
|
|
27
|
+
pollIntervalMs: () => Math.max(1, config.refreshIntervalMinutes) * 60_000,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
pluginRegistry.getConfigFn = () => config;
|
|
31
|
+
pluginRegistry.getLayoutFn = () => config.layout;
|
|
32
|
+
pluginRegistry.registerNewsCapabilityFn = (capability) => newsService.register(capability);
|
|
33
|
+
pluginRegistry.watchNewsQueryFn = (query, listener) => newsService.watchQuery(query, listener);
|
|
34
|
+
newsService.register(newsProvider({
|
|
35
|
+
id: dataProvider.id,
|
|
36
|
+
name: dataProvider.name,
|
|
37
|
+
priority: 0,
|
|
38
|
+
provider: { fetchNews: (query) => cloudNews?.provider.fetchNews(query) ?? Promise.resolve([]) },
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
setSharedMarketDataCoordinator(marketData);
|
|
42
|
+
setSharedNewsService(newsService);
|
|
43
|
+
const ready = Promise.all(plugins.map((plugin) => pluginRegistry.register(plugin))).then(() => {});
|
|
44
|
+
newsService.start();
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
persistence,
|
|
48
|
+
tickerRepository,
|
|
49
|
+
dataProvider,
|
|
50
|
+
marketData,
|
|
51
|
+
pluginRegistry,
|
|
52
|
+
ready,
|
|
53
|
+
destroy() {
|
|
54
|
+
setSharedMarketDataCoordinator(null);
|
|
55
|
+
setSharedNewsService(null);
|
|
56
|
+
newsService.stop();
|
|
57
|
+
pluginRegistry.destroy();
|
|
58
|
+
persistence.close();
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { afterEach, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { apiClient } from "../../api-client";
|
|
3
|
+
import {
|
|
4
|
+
browserCredentialedFetch,
|
|
5
|
+
restoreBrowserCloudSession,
|
|
6
|
+
} from "./cloud-transport";
|
|
7
|
+
|
|
8
|
+
const originalFetch = globalThis.fetch;
|
|
9
|
+
afterEach(() => { globalThis.fetch = originalFetch; });
|
|
10
|
+
|
|
11
|
+
test("browser cloud transport uses host cookies without forwarding forbidden headers", async () => {
|
|
12
|
+
let captured: RequestInit | undefined;
|
|
13
|
+
globalThis.fetch = (async (_url: string | URL | Request, init?: RequestInit) => {
|
|
14
|
+
captured = init;
|
|
15
|
+
return new Response("{}", { headers: { "content-type": "application/json" } });
|
|
16
|
+
}) as typeof fetch;
|
|
17
|
+
await browserCredentialedFetch("https://api.gloom.sh/auth/get-session", {
|
|
18
|
+
headers: { Cookie: "must-not-leak", Origin: "https://api.gloom.sh", Accept: "application/json" },
|
|
19
|
+
});
|
|
20
|
+
const headers = new Headers(captured?.headers);
|
|
21
|
+
expect(captured?.credentials).toBe("include");
|
|
22
|
+
expect(headers.has("Cookie")).toBe(false);
|
|
23
|
+
expect(headers.has("Origin")).toBe(false);
|
|
24
|
+
expect(headers.get("Accept")).toBe("application/json");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("browser cloud transport plants session cookies before dropping the Cookie header", async () => {
|
|
28
|
+
const planted: string[] = [];
|
|
29
|
+
const previousDocument = (globalThis as { document?: unknown }).document;
|
|
30
|
+
(globalThis as { document?: { cookie: string } }).document = {
|
|
31
|
+
get cookie() {
|
|
32
|
+
return planted.join("; ");
|
|
33
|
+
},
|
|
34
|
+
set cookie(value: string) {
|
|
35
|
+
planted.push(value);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const previousLocation = (globalThis as { location?: unknown }).location;
|
|
39
|
+
(globalThis as { location?: { protocol: string } }).location = { protocol: "https:" };
|
|
40
|
+
|
|
41
|
+
globalThis.fetch = (async () => {
|
|
42
|
+
return new Response("{}", { headers: { "content-type": "application/json" } });
|
|
43
|
+
}) as typeof fetch;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await browserCredentialedFetch("https://api.gloom.sh/auth/get-session", {
|
|
47
|
+
headers: {
|
|
48
|
+
Cookie: "__Secure-gloomberb.session_token=signed-token.value; gloomberb.session_token=signed-token.value",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
expect(planted).toEqual([
|
|
52
|
+
"__Secure-gloomberb.session_token=signed-token.value; Path=/; SameSite=Lax; Secure",
|
|
53
|
+
"gloomberb.session_token=signed-token.value; Path=/; SameSite=Lax; Secure",
|
|
54
|
+
]);
|
|
55
|
+
} finally {
|
|
56
|
+
if (previousDocument === undefined) delete (globalThis as { document?: unknown }).document;
|
|
57
|
+
else (globalThis as { document?: unknown }).document = previousDocument;
|
|
58
|
+
if (previousLocation === undefined) delete (globalThis as { location?: unknown }).location;
|
|
59
|
+
else (globalThis as { location?: unknown }).location = previousLocation;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("browser boot restores an existing Gloom Cloud cookie session", async () => {
|
|
64
|
+
const getSession = spyOn(apiClient, "getSession").mockResolvedValue(null);
|
|
65
|
+
await restoreBrowserCloudSession();
|
|
66
|
+
expect(getSession).toHaveBeenCalledTimes(1);
|
|
67
|
+
getSession.mockRestore();
|
|
68
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { apiClient, setCloudApiFetchTransport } from "../../api-client";
|
|
2
|
+
import { setHttpFetchTransport } from "../../utils/http-transport";
|
|
3
|
+
|
|
4
|
+
const SESSION_COOKIE_NAMES = ["__Secure-gloomberb.session_token", "gloomberb.session_token"] as const;
|
|
5
|
+
|
|
6
|
+
function plantBrowserSessionCookies(cookieHeader: string): void {
|
|
7
|
+
if (typeof document === "undefined") return;
|
|
8
|
+
const secure = typeof location !== "undefined" && location.protocol === "https:";
|
|
9
|
+
for (const part of cookieHeader.split(";")) {
|
|
10
|
+
const trimmed = part.trim();
|
|
11
|
+
const eq = trimmed.indexOf("=");
|
|
12
|
+
if (eq <= 0) continue;
|
|
13
|
+
const name = trimmed.slice(0, eq).trim();
|
|
14
|
+
const value = trimmed.slice(eq + 1);
|
|
15
|
+
if (!(SESSION_COOKIE_NAMES as readonly string[]).includes(name) || !value) continue;
|
|
16
|
+
const useSecure = secure || name.startsWith("__Secure-");
|
|
17
|
+
document.cookie = `${name}=${value}; Path=/; SameSite=Lax${useSecure ? "; Secure" : ""}`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function browserCredentialedFetch(url: string, init: RequestInit = {}): Promise<Response> {
|
|
22
|
+
const headers = new Headers(init.headers);
|
|
23
|
+
const cookieHeader = headers.get("Cookie");
|
|
24
|
+
if (cookieHeader) plantBrowserSessionCookies(cookieHeader);
|
|
25
|
+
// These are controlled by the browser. Desktop transports may set them, but
|
|
26
|
+
// carrying them into fetch would either fail or misrepresent the web origin.
|
|
27
|
+
headers.delete("Cookie");
|
|
28
|
+
headers.delete("Origin");
|
|
29
|
+
return fetch(url, { ...init, headers, credentials: "include" });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function installBrowserFetchTransports(): void {
|
|
33
|
+
apiClient.setCookieSessionMode(true);
|
|
34
|
+
setCloudApiFetchTransport(browserCredentialedFetch);
|
|
35
|
+
setHttpFetchTransport((url, init) => fetch(url, init));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function restoreBrowserCloudSession(budgetMs = 5_000): Promise<void> {
|
|
39
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
40
|
+
const deadline = new Promise<null>((resolve) => {
|
|
41
|
+
timer = setTimeout(() => resolve(null), budgetMs);
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
await Promise.race([apiClient.getSession().catch(() => null), deadline]);
|
|
45
|
+
} finally {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|