gloomberb 0.10.0 → 0.10.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/package.json +8 -1
- package/src/api-client/auth.ts +1 -0
- package/src/api-client/index.test.ts +359 -108
- package/src/api-client/index.ts +24 -13
- package/src/api-client/socket.ts +70 -35
- package/src/api-client/types.ts +1 -1
- package/src/app/language-observer.test.tsx +82 -0
- package/src/app/language-observer.tsx +13 -0
- package/src/app/pane-runtime/index.tsx +2 -2
- package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
- package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
- package/src/app/pane-runtime/plugin-bindings.ts +2 -2
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
- package/src/app/runtime/broker-import.ts +2 -2
- package/src/app/runtime/plugin-bindings.ts +2 -2
- package/src/app/runtime/startup.ts +2 -2
- package/src/app.tsx +16 -7
- package/src/brokers/account-cache.ts +6 -6
- package/src/brokers/broker-ticker-sync.ts +3 -3
- package/src/brokers/remote-broker-adapter.ts +2 -2
- package/src/brokers/sync-broker-instance.ts +5 -6
- package/src/cli/commands/api.ts +3 -6
- package/src/cli/commands/automation.ts +7 -15
- package/src/cli/commands/market.ts +26 -61
- package/src/cli/commands/overview.ts +34 -45
- package/src/cli/commands/search.ts +25 -27
- package/src/cli/commands/system.ts +27 -57
- package/src/cli/commands/ticker.ts +62 -63
- package/src/cli/context.test.ts +63 -0
- package/src/cli/context.ts +15 -1
- package/src/cli/index.ts +1 -1
- package/src/cli/pane-functions/data.ts +2 -19
- package/src/cli/pane-functions/index.ts +26 -25
- package/src/cli/pane-functions/report.ts +1 -1
- package/src/cli/pane-functions/screenshot.ts +3 -3
- package/src/cli/scoped-context.ts +41 -0
- package/src/components/chart/composite/composite-chart.test.tsx +615 -18
- package/src/components/chart/composite/composite-chart.tsx +179 -79
- package/src/components/chart/composite/format.test.ts +20 -7
- package/src/components/chart/composite/index.ts +0 -21
- package/src/components/chart/composite/interactions.test.ts +90 -3
- package/src/components/chart/composite/interactions.ts +189 -24
- package/src/components/chart/composite/price-series.ts +0 -2
- package/src/components/chart/composite/rasterizer.ts +10 -9
- package/src/components/chart/composite/renderers.test.ts +108 -2
- package/src/components/chart/composite/scene.test.ts +36 -0
- package/src/components/chart/composite/scene.ts +5 -2
- package/src/components/chart/composite/text-renderer.ts +37 -25
- package/src/components/chart/composite/time-axis.test.ts +243 -0
- package/src/components/chart/composite/time-axis.ts +608 -0
- package/src/components/chart/composite/types.ts +12 -1
- package/src/components/chart/core/data.ts +2 -159
- package/src/components/chart/core/date-window.ts +6 -225
- package/src/components/chart/core/palette.ts +1 -12
- package/src/components/chart/core/pointer.ts +0 -142
- package/src/components/chart/core/types.ts +1 -58
- package/src/components/chart/cursor-motion.ts +2 -4
- package/src/components/chart/indicators/types.ts +0 -9
- package/src/components/chart/market-session.ts +0 -11
- package/src/components/chart/native/kitty/protocol.ts +1 -1
- package/src/components/chart/native/raster/layout.ts +3 -10
- package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
- package/src/components/chart/static/chart/surface.tsx +1 -2
- package/src/components/command-bar/action-runtime.ts +2 -2
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/panel/body.tsx +2 -2
- package/src/components/command-bar/panel/index.tsx +11 -9
- package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
- package/src/components/command-bar/surface/index.tsx +2 -2
- package/src/components/command-bar/workflow/body.tsx +3 -3
- package/src/components/command-bar/workflow/collection-actions.ts +2 -2
- package/src/components/command-bar/workflow/coordinator.ts +2 -2
- package/src/components/command-bar/workflow/field-row.tsx +25 -11
- package/src/components/command-bar/workflow/tickers.ts +2 -2
- package/src/components/feed-data-table/stack-view.test.tsx +28 -36
- package/src/components/feed-data-table/stack-view.tsx +3 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -6
- package/src/components/layout/header.tsx +13 -9
- package/src/components/layout/pane/content.test.tsx +42 -3
- package/src/components/layout/pane/content.tsx +3 -1
- package/src/components/layout/pane/footer/index.test.tsx +45 -2
- package/src/components/layout/pane/footer/registration.tsx +3 -1
- package/src/components/layout/shell/index.tsx +1 -2
- package/src/components/layout/shell/native/surfaces.ts +1 -1
- package/src/components/layout/shell/native/window-state.ts +1 -1
- package/src/components/layout/status-bar.tsx +12 -8
- package/src/components/layout/window-controls.tsx +8 -6
- package/src/components/onboarding/onboarding-wizard.tsx +3 -1
- package/src/components/pane-settings-dialog/desktop.tsx +0 -2
- package/src/components/pane-settings-dialog.tsx +0 -2
- package/src/components/pane-template-wizard.tsx +10 -9
- package/src/components/price-performance.tsx +2 -113
- package/src/components/price-sparkline/model.ts +15 -17
- package/src/components/price-sparkline/view.tsx +8 -7
- package/src/components/speedometer-gauge/desktop.tsx +2 -1
- package/src/components/speedometer-gauge/index.tsx +0 -2
- package/src/components/speedometer-gauge/terminal.tsx +24 -5
- package/src/components/ticker/badge/index.tsx +1 -18
- package/src/components/ui/data-table/opentui/index.tsx +3 -3
- package/src/components/ui/frame.tsx +1 -2
- package/src/components/ui/page-stack-view.test.tsx +126 -0
- package/src/components/ui/page-stack-view.tsx +24 -12
- package/src/components/ui/tabs.tsx +7 -3
- package/src/components/ui/ui.test.tsx +23 -2
- package/src/components/use-external-link-footer.ts +4 -2
- package/src/core/app-service-ports.ts +46 -0
- package/src/core/app-services.test.ts +66 -0
- package/src/core/app-services.ts +17 -15
- package/src/data/config/chart-settings.ts +3 -10
- package/src/data/config/store/index.test.ts +99 -2
- package/src/data/config/store/migrations.ts +278 -0
- package/src/data/config/store/normalize.ts +44 -186
- package/src/data/resource-store.ts +73 -12
- package/src/data/sqlite/cache.test.ts +104 -15
- package/src/i18n/index.test.ts +17 -0
- package/src/i18n/index.ts +23 -6
- package/src/i18n/ja.ts +13 -0
- package/src/i18n/ko.ts +13 -0
- package/src/i18n/react.ts +6 -0
- package/src/i18n/zh-cn.ts +13 -0
- package/src/i18n/zh-tw.ts +13 -0
- package/src/market-data/coordinator/auxiliary.ts +7 -3
- package/src/market-data/coordinator/chart.ts +1 -1
- package/src/market-data/coordinator/index.test.ts +26 -0
- package/src/market-data/coordinator/index.ts +11 -5
- package/src/market-data/coordinator/quotes.ts +4 -40
- package/src/market-data/coordinator/subscriptions.test.ts +89 -1
- package/src/market-data/hooks.tsx +47 -1
- package/src/market-data/market/status.ts +6 -6
- package/src/market-data/performance.ts +1 -1
- package/src/market-data/quote-subscription-target.test.ts +37 -0
- package/src/market-data/quote-subscription-target.ts +60 -0
- package/src/market-data/quotes/freshness.ts +1 -0
- package/src/market-data/quotes/resolution.ts +4 -0
- package/src/market-data/request-types.ts +3 -3
- package/src/news/aggregator.test.ts +97 -1
- package/src/news/aggregator.ts +100 -57
- package/src/news/hooks.test.tsx +64 -0
- package/src/news/hooks.ts +18 -25
- package/src/plugins/builtin/account-management/footer.ts +4 -2
- package/src/plugins/builtin/account-management/pane.tsx +9 -4
- package/src/plugins/builtin/ai/agent-history.ts +11 -7
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
- package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
- package/src/plugins/builtin/ai/index.test.ts +2 -2
- package/src/plugins/builtin/ai/index.tsx +10 -11
- package/src/plugins/builtin/ai/pane-settings.ts +1 -1
- package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
- package/src/plugins/builtin/ai/providers.ts +1 -9
- package/src/plugins/builtin/ai/runner.ts +0 -6
- package/src/plugins/builtin/ai/screener/footer.ts +4 -1
- package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
- package/src/plugins/builtin/ai/workspace/model.ts +2 -27
- package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
- package/src/plugins/builtin/analytics/metrics.ts +1 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
- package/src/plugins/builtin/broker-manager/index.tsx +4 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
- package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
- package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
- package/src/plugins/builtin/chart-composer/index.tsx +0 -6
- package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
- package/src/plugins/builtin/chart-composer/presets.ts +1 -19
- package/src/plugins/builtin/chart-composer/settings.ts +1 -1
- package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
- package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
- package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
- package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
- package/src/plugins/builtin/chat/controller/session.ts +2 -0
- package/src/plugins/builtin/chat/controller/state.ts +7 -0
- package/src/plugins/builtin/chat/controller.test.ts +73 -20
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
- package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
- package/src/plugins/builtin/correlation/compute.ts +4 -10
- package/src/plugins/builtin/econ/index.tsx +17 -19
- package/src/plugins/builtin/holders/types.ts +0 -1
- package/src/plugins/builtin/insider/index.tsx +6 -31
- package/src/plugins/builtin/news/index.tsx +5 -7
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
- package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
- package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
- package/src/plugins/builtin/news/wire/read-state.ts +18 -28
- package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
- package/src/plugins/builtin/options/footer.test.ts +115 -0
- package/src/plugins/builtin/options/footer.ts +110 -0
- package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
- package/src/plugins/builtin/options/live-quotes.ts +147 -0
- package/src/plugins/builtin/options/view.test.tsx +71 -1
- package/src/plugins/builtin/options/view.tsx +39 -24
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
- package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
- package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
- package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
- package/src/plugins/builtin/sec/filing-content.ts +94 -0
- package/src/plugins/builtin/sec/index.tsx +21 -104
- package/src/plugins/builtin/sectors/sector-model.ts +1 -7
- package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
- package/src/plugins/builtin/shared/pane-footer.ts +82 -0
- package/src/plugins/builtin/shared/read-state.ts +64 -3
- package/src/plugins/builtin/substack/read-state.ts +16 -41
- package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
- package/src/plugins/builtin/yield-curve/index.tsx +11 -9
- package/src/plugins/ibkr/dialogs.tsx +3 -3
- package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
- package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
- package/src/plugins/ibkr/gateway/history.ts +2 -2
- package/src/plugins/ibkr/gateway/selection.ts +2 -2
- package/src/plugins/ibkr/gateway/service/index.ts +2 -2
- package/src/plugins/ibkr/gateway/service/native.ts +2 -2
- package/src/plugins/plugin-persistence.ts +3 -4
- package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
- package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
- package/src/plugins/prediction-markets/detail/book.tsx +2 -2
- package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
- package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
- package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
- package/src/plugins/registry/context.ts +3 -4
- package/src/plugins/registry/index.ts +5 -6
- package/src/react/input.test.ts +126 -3
- package/src/react/input.ts +120 -1
- package/src/remote/app-host.tsx +2 -13
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
- package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
- package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
- package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
- package/src/renderers/electrobun/bun/index.ts +123 -83
- package/src/renderers/electrobun/shared/protocol.ts +146 -7
- package/src/renderers/electrobun/view/app-services.ts +5 -8
- package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
- package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
- package/src/renderers/electrobun/view/config-host.ts +1 -1
- package/src/renderers/electrobun/view/context-menu.ts +1 -17
- package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
- package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
- package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
- package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
- package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
- package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
- package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
- package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
- package/src/renderers/electrobun/view/host/box.tsx +0 -1
- package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/input.tsx +0 -1
- package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/native.ts +0 -1
- package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
- package/src/renderers/electrobun/view/host/text.tsx +0 -1
- package/src/renderers/electrobun/view/http-fetch.ts +3 -11
- package/src/renderers/electrobun/view/input-host.tsx +10 -53
- package/src/renderers/electrobun/view/key-event.ts +0 -1
- package/src/renderers/electrobun/view/main.tsx +3 -3
- package/src/renderers/electrobun/view/native-renderer.ts +0 -1
- package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
- package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
- package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
- package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
- package/src/renderers/electrobun/view/toast-host.tsx +0 -1
- package/src/renderers/electrobun/view/ui-host.tsx +1 -2
- package/src/renderers/electrobun/view/update-host.ts +1 -1
- package/src/renderers/opentui/chart-surface.tsx +2 -1
- package/src/renderers/opentui/dialog-host.tsx +20 -4
- package/src/renderers/opentui/image/loader.ts +12 -2
- package/src/renderers/opentui/input-host.tsx +7 -49
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
- package/src/sources/gloomberb-cloud/index.test.ts +173 -0
- package/src/sources/gloomberb-cloud/index.ts +42 -20
- package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
- package/src/sources/provider-router/cache.ts +1 -1
- package/src/sources/provider-router/document-routes.ts +10 -5
- package/src/sources/provider-router/financials.test.ts +14 -0
- package/src/sources/provider-router/financials.ts +6 -1
- package/src/sources/provider-router/history.test.ts +163 -1
- package/src/sources/provider-router/history.ts +278 -347
- package/src/sources/provider-router/index.ts +2 -2
- package/src/sources/provider-router/routing.test.ts +39 -0
- package/src/sources/provider-router/routing.ts +24 -11
- package/src/sources/provider-router/supplemental.ts +37 -17
- package/src/sources/sec-edgar/content.ts +2 -12
- package/src/sources/sec-edgar.ts +2 -13
- package/src/sources/yahoo-finance/history.ts +2 -2
- package/src/sources/yahoo-finance/options.ts +5 -0
- package/src/sources/yahoo-finance.ts +2 -2
- package/src/state/app/bootstrap.ts +2 -2
- package/src/state/app/context/index.tsx +2 -2
- package/src/state/hooks/quote-streaming.ts +66 -1
- package/src/state/hooks/session-snapshot.ts +2 -2
- package/src/state/selectors.ts +0 -9
- package/src/sync/controller.test.ts +98 -0
- package/src/sync/controller.ts +29 -5
- package/src/sync/react.ts +2 -2
- package/src/sync/types.ts +2 -2
- package/src/theme/colors.test.ts +10 -0
- package/src/theme/colors.ts +75 -78
- package/src/theme/theme-context.test.tsx +67 -0
- package/src/theme/theme-context.tsx +9 -5
- package/src/tickers/open-target.ts +2 -2
- package/src/tickers/search/upsert.ts +2 -2
- package/src/time-series/alignment.ts +0 -7
- package/src/time-series/chart-data.ts +145 -0
- package/src/time-series/date-window.ts +44 -0
- package/src/time-series/history-window.ts +21 -0
- package/src/time-series/hooks.test.ts +59 -0
- package/src/time-series/hooks.ts +33 -12
- package/src/time-series/market-resolution.ts +2 -2
- package/src/time-series/market.ts +1 -1
- package/src/time-series/range.ts +9 -0
- package/src/{components/chart/core → time-series}/resolution.ts +3 -102
- package/src/time-series/resolve.test.ts +203 -1
- package/src/time-series/resolve.ts +96 -18
- package/src/time-series/spec-studies.test.ts +15 -0
- package/src/time-series/spec.ts +2 -2
- package/src/time-series/studies.ts +5 -2
- package/src/time-series/types.ts +3 -1
- package/src/time-series/use-chart-resolution.test.tsx +358 -0
- package/src/time-series/use-chart-resolution.ts +46 -4
- package/src/types/broker.ts +2 -2
- package/src/types/data-provider.ts +3 -3
- package/src/types/financials.ts +12 -0
- package/src/types/plugin.ts +2 -2
- package/src/ui/context-menu.tsx +3 -2
- package/src/ui/dialog.tsx +2 -2
- package/src/utils/html-entities.ts +3 -5
- package/src/utils/main-thread-monitor.ts +1 -1
- package/src/utils/price-history.ts +5 -5
- package/src/version.ts +3 -1
- package/src/components/chart/core/viewport.test.ts +0 -8
- package/src/components/chart/core/viewport.ts +0 -24
- package/src/time-series/index.ts +0 -14
- /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
package/src/news/aggregator.ts
CHANGED
|
@@ -17,17 +17,30 @@ export { buildNewsQueryKey } from "./news-model";
|
|
|
17
17
|
|
|
18
18
|
export interface NewsServiceOptions {
|
|
19
19
|
pollIntervalMs?: number;
|
|
20
|
+
inactiveQueryTtlMs?: number;
|
|
21
|
+
maxInactiveQueries?: number;
|
|
22
|
+
now?: () => number;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export type NewsQueryListener = (state: NewsQueryState) => void;
|
|
23
26
|
|
|
24
27
|
const DEFAULT_POLL_INTERVAL_MS = 2 * 60 * 1000;
|
|
28
|
+
const DEFAULT_INACTIVE_QUERY_TTL_MS = 10 * 60 * 1000;
|
|
29
|
+
const DEFAULT_MAX_INACTIVE_QUERIES = 50;
|
|
25
30
|
|
|
26
31
|
interface SourceFetchResult {
|
|
27
32
|
articles: NewsArticle[];
|
|
28
33
|
sourceIds: string[];
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
interface NewsQueryEntry {
|
|
37
|
+
query: NewsQuery;
|
|
38
|
+
state: NewsQueryState;
|
|
39
|
+
inFlight: Promise<NewsQueryState> | null;
|
|
40
|
+
refs: number;
|
|
41
|
+
lastAccessedAt: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
function newsCapabilityPriority(source: NewsCapability): number {
|
|
32
45
|
return source.priority ?? 1000;
|
|
33
46
|
}
|
|
@@ -39,17 +52,20 @@ function newsCapabilitySourceId(source: NewsCapability): string {
|
|
|
39
52
|
export class NewsService {
|
|
40
53
|
private readonly sources = new Map<string, NewsCapability>();
|
|
41
54
|
private readonly listeners = new Set<() => void>();
|
|
42
|
-
private readonly
|
|
43
|
-
private readonly queryByKey = new Map<string, NewsQuery>();
|
|
44
|
-
private readonly watchedQueries = new Map<string, { query: NewsQuery; refs: number }>();
|
|
45
|
-
private readonly inFlight = new Map<string, Promise<NewsQueryState>>();
|
|
55
|
+
private readonly queries = new Map<string, NewsQueryEntry>();
|
|
46
56
|
private articles: NewsArticle[] = [];
|
|
47
57
|
private version = 0;
|
|
48
58
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
49
59
|
private readonly pollIntervalMs: number;
|
|
60
|
+
private readonly inactiveQueryTtlMs: number;
|
|
61
|
+
private readonly maxInactiveQueries: number;
|
|
62
|
+
private readonly now: () => number;
|
|
50
63
|
|
|
51
64
|
constructor(options: NewsServiceOptions = {}) {
|
|
52
65
|
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
66
|
+
this.inactiveQueryTtlMs = Math.max(1, options.inactiveQueryTtlMs ?? DEFAULT_INACTIVE_QUERY_TTL_MS);
|
|
67
|
+
this.maxInactiveQueries = Math.max(1, Math.floor(options.maxInactiveQueries ?? DEFAULT_MAX_INACTIVE_QUERIES));
|
|
68
|
+
this.now = options.now ?? Date.now;
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
register(source: NewsCapability): () => void {
|
|
@@ -89,29 +105,24 @@ export class NewsService {
|
|
|
89
105
|
watchQuery(query: NewsQuery, listener: NewsQueryListener): () => void {
|
|
90
106
|
const normalized = normalizeNewsQuery(query);
|
|
91
107
|
const key = buildNewsQueryKey(normalized);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
query: normalized,
|
|
95
|
-
refs: (currentWatch?.refs ?? 0) + 1,
|
|
96
|
-
});
|
|
108
|
+
const entry = this.getOrCreateQueryEntry(normalized);
|
|
109
|
+
entry.refs++;
|
|
97
110
|
|
|
98
|
-
const emit = () => listener(this.
|
|
111
|
+
const emit = () => listener(this.queries.get(key)?.state ?? createIdleNewsQueryState());
|
|
99
112
|
const unsubscribe = this.subscribe(emit);
|
|
100
113
|
emit();
|
|
101
|
-
void this.refreshQuery(normalized, false
|
|
114
|
+
void this.refreshQuery(normalized, false);
|
|
102
115
|
|
|
103
116
|
let disposed = false;
|
|
104
117
|
return () => {
|
|
105
118
|
if (disposed) return;
|
|
106
119
|
disposed = true;
|
|
107
120
|
unsubscribe();
|
|
108
|
-
const
|
|
109
|
-
if (!
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this.watchedQueries.set(key, { query: watch.query, refs: watch.refs - 1 });
|
|
114
|
-
}
|
|
121
|
+
const current = this.queries.get(key);
|
|
122
|
+
if (!current) return;
|
|
123
|
+
current.refs = Math.max(0, current.refs - 1);
|
|
124
|
+
current.lastAccessedAt = this.now();
|
|
125
|
+
this.pruneInactiveQueries();
|
|
115
126
|
};
|
|
116
127
|
}
|
|
117
128
|
|
|
@@ -128,9 +139,7 @@ export class NewsService {
|
|
|
128
139
|
|
|
129
140
|
getQueryState(query: NewsQuery): NewsQueryState {
|
|
130
141
|
const normalized = normalizeNewsQuery(query);
|
|
131
|
-
|
|
132
|
-
this.queryByKey.set(key, normalized);
|
|
133
|
-
return this.queryStates.get(key) ?? createIdleNewsQueryState();
|
|
142
|
+
return this.getOrCreateQueryEntry(normalized).state;
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
async load(query: NewsQuery): Promise<NewsQueryState> {
|
|
@@ -160,36 +169,28 @@ export class NewsService {
|
|
|
160
169
|
}
|
|
161
170
|
|
|
162
171
|
private async pollActiveQueries(): Promise<void> {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
if (queries.size === 0) return;
|
|
171
|
-
await Promise.allSettled([...queries.values()].map((query) => this.refreshQuery(query, false, { track: false })));
|
|
172
|
+
this.pruneInactiveQueries();
|
|
173
|
+
const queries = [...this.queries.values()]
|
|
174
|
+
.filter((entry) => entry.refs > 0)
|
|
175
|
+
.map((entry) => entry.query);
|
|
176
|
+
if (queries.length === 0) return;
|
|
177
|
+
await Promise.allSettled(queries.map((query) => this.refreshQuery(query, false)));
|
|
172
178
|
}
|
|
173
179
|
|
|
174
180
|
private async refreshQuery(
|
|
175
181
|
query: NewsQuery,
|
|
176
182
|
showLoading: boolean,
|
|
177
|
-
options: { track?: boolean } = {},
|
|
178
183
|
): Promise<NewsQueryState> {
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
this.queryByKey.set(key, query);
|
|
182
|
-
}
|
|
183
|
-
const existingFlight = this.inFlight.get(key);
|
|
184
|
-
if (existingFlight) return existingFlight;
|
|
184
|
+
const entry = this.getOrCreateQueryEntry(query);
|
|
185
|
+
if (entry.inFlight) return entry.inFlight;
|
|
185
186
|
|
|
186
|
-
const current =
|
|
187
|
+
const current = entry.state;
|
|
187
188
|
if (showLoading) {
|
|
188
|
-
|
|
189
|
+
entry.state = {
|
|
189
190
|
...current,
|
|
190
191
|
phase: current.articles.length > 0 ? "refreshing" : "loading",
|
|
191
192
|
error: null,
|
|
192
|
-
}
|
|
193
|
+
};
|
|
193
194
|
this.notify();
|
|
194
195
|
}
|
|
195
196
|
|
|
@@ -201,10 +202,11 @@ export class NewsService {
|
|
|
201
202
|
phase: "ready",
|
|
202
203
|
articles,
|
|
203
204
|
error: null,
|
|
204
|
-
updatedAt:
|
|
205
|
+
updatedAt: this.now(),
|
|
205
206
|
sourceIds: result.sourceIds,
|
|
206
207
|
};
|
|
207
|
-
|
|
208
|
+
entry.state = state;
|
|
209
|
+
entry.lastAccessedAt = this.now();
|
|
208
210
|
this.rebuildArticlePool();
|
|
209
211
|
this.notify();
|
|
210
212
|
return state;
|
|
@@ -214,18 +216,60 @@ export class NewsService {
|
|
|
214
216
|
phase: "error",
|
|
215
217
|
error: error instanceof Error ? error.message : String(error),
|
|
216
218
|
};
|
|
217
|
-
|
|
219
|
+
entry.state = state;
|
|
220
|
+
entry.lastAccessedAt = this.now();
|
|
218
221
|
this.notify();
|
|
219
222
|
return state;
|
|
220
223
|
} finally {
|
|
221
|
-
|
|
224
|
+
entry.inFlight = null;
|
|
225
|
+
this.pruneInactiveQueries();
|
|
222
226
|
}
|
|
223
227
|
})();
|
|
224
228
|
|
|
225
|
-
|
|
229
|
+
entry.inFlight = promise;
|
|
226
230
|
return promise;
|
|
227
231
|
}
|
|
228
232
|
|
|
233
|
+
private getOrCreateQueryEntry(query: NewsQuery): NewsQueryEntry {
|
|
234
|
+
const key = buildNewsQueryKey(query);
|
|
235
|
+
const now = this.now();
|
|
236
|
+
this.pruneInactiveQueries(now);
|
|
237
|
+
const existing = this.queries.get(key);
|
|
238
|
+
if (existing) {
|
|
239
|
+
existing.lastAccessedAt = now;
|
|
240
|
+
return existing;
|
|
241
|
+
}
|
|
242
|
+
const entry: NewsQueryEntry = {
|
|
243
|
+
query,
|
|
244
|
+
state: createIdleNewsQueryState(),
|
|
245
|
+
inFlight: null,
|
|
246
|
+
refs: 0,
|
|
247
|
+
lastAccessedAt: now,
|
|
248
|
+
};
|
|
249
|
+
this.queries.set(key, entry);
|
|
250
|
+
this.pruneInactiveQueries(now);
|
|
251
|
+
return entry;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private pruneInactiveQueries(now = this.now()): void {
|
|
255
|
+
const inactive = [...this.queries.entries()]
|
|
256
|
+
.filter(([, entry]) => entry.refs === 0 && entry.inFlight === null)
|
|
257
|
+
.sort((left, right) => right[1].lastAccessedAt - left[1].lastAccessedAt);
|
|
258
|
+
|
|
259
|
+
let retained = 0;
|
|
260
|
+
let changed = false;
|
|
261
|
+
for (const [key, entry] of inactive) {
|
|
262
|
+
const expired = now - entry.lastAccessedAt >= this.inactiveQueryTtlMs;
|
|
263
|
+
if (expired || retained >= this.maxInactiveQueries) {
|
|
264
|
+
this.queries.delete(key);
|
|
265
|
+
changed = true;
|
|
266
|
+
} else {
|
|
267
|
+
retained++;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (changed) this.rebuildArticlePool();
|
|
271
|
+
}
|
|
272
|
+
|
|
229
273
|
private enabledSources(query: NewsQuery): NewsCapability[] {
|
|
230
274
|
return [...this.sources.values()]
|
|
231
275
|
.filter((source) => source.isEnabled?.() !== false)
|
|
@@ -277,7 +321,7 @@ export class NewsService {
|
|
|
277
321
|
|
|
278
322
|
private seedCachedSource(source: NewsCapability): void {
|
|
279
323
|
const news = source.provider;
|
|
280
|
-
const queries = [...this.
|
|
324
|
+
const queries = [...this.queries.values()].map((entry) => entry.query);
|
|
281
325
|
if (queries.length === 0) queries.push(DEFAULT_GLOBAL_QUERY);
|
|
282
326
|
|
|
283
327
|
let changed = false;
|
|
@@ -286,15 +330,14 @@ export class NewsService {
|
|
|
286
330
|
const cached = (news.getCachedNews?.(query) ?? [])
|
|
287
331
|
.map((article) => markDetailCapableArticle(source, article));
|
|
288
332
|
if (cached.length === 0) continue;
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
this.queryStates.set(key, {
|
|
333
|
+
const entry = this.getOrCreateQueryEntry(query);
|
|
334
|
+
entry.state = {
|
|
292
335
|
phase: "ready",
|
|
293
|
-
articles: filterNewsArticlesForQuery(dedupeNewsArticles([...
|
|
336
|
+
articles: filterNewsArticlesForQuery(dedupeNewsArticles([...entry.state.articles, ...cached]), query),
|
|
294
337
|
error: null,
|
|
295
|
-
updatedAt:
|
|
296
|
-
sourceIds: [...new Set([...
|
|
297
|
-
}
|
|
338
|
+
updatedAt: this.now(),
|
|
339
|
+
sourceIds: [...new Set([...entry.state.sourceIds, newsCapabilitySourceId(source)])],
|
|
340
|
+
};
|
|
298
341
|
changed = true;
|
|
299
342
|
}
|
|
300
343
|
if (changed) {
|
|
@@ -304,21 +347,21 @@ export class NewsService {
|
|
|
304
347
|
}
|
|
305
348
|
|
|
306
349
|
private rebuildArticlePool(): void {
|
|
307
|
-
this.articles = dedupeNewsArticles([...this.
|
|
350
|
+
this.articles = dedupeNewsArticles([...this.queries.values()].flatMap((entry) => entry.state.articles));
|
|
308
351
|
}
|
|
309
352
|
|
|
310
353
|
private mergeStoryDetail(article: NewsArticle): void {
|
|
311
354
|
let changed = false;
|
|
312
|
-
for (const
|
|
355
|
+
for (const entry of this.queries.values()) {
|
|
313
356
|
let stateChanged = false;
|
|
314
|
-
const nextArticles = state.articles.map((existing) => {
|
|
357
|
+
const nextArticles = entry.state.articles.map((existing) => {
|
|
315
358
|
if (existing.id !== article.id) return existing;
|
|
316
359
|
stateChanged = true;
|
|
317
360
|
changed = true;
|
|
318
361
|
return mergeNewsArticle(existing, article);
|
|
319
362
|
});
|
|
320
363
|
if (stateChanged) {
|
|
321
|
-
|
|
364
|
+
entry.state = { ...entry.state, articles: nextArticles };
|
|
322
365
|
}
|
|
323
366
|
}
|
|
324
367
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { act, useState } from "react";
|
|
3
|
+
import { testRender } from "../renderers/opentui/test-utils";
|
|
4
|
+
import type { NewsService } from "./aggregator";
|
|
5
|
+
import { setSharedNewsService, useNewsArticles } from "./hooks";
|
|
6
|
+
import type { NewsQueryState } from "./types";
|
|
7
|
+
|
|
8
|
+
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
9
|
+
let rerenderHarness: (() => void) | null = null;
|
|
10
|
+
|
|
11
|
+
function NewsHookHarness() {
|
|
12
|
+
const [renderCount, setRenderCount] = useState(0);
|
|
13
|
+
rerenderHarness = () => setRenderCount((current) => current + 1);
|
|
14
|
+
const state = useNewsArticles({ feed: "latest", limit: 20 });
|
|
15
|
+
return <text>{state.phase}:{renderCount}</text>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
if (testSetup) {
|
|
20
|
+
await act(async () => testSetup?.renderer.destroy());
|
|
21
|
+
testSetup = undefined;
|
|
22
|
+
}
|
|
23
|
+
rerenderHarness = null;
|
|
24
|
+
setSharedNewsService(null);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("useNewsArticles watches once and unwatches on unmount", async () => {
|
|
28
|
+
let watchCount = 0;
|
|
29
|
+
let unwatchCount = 0;
|
|
30
|
+
const readyState: NewsQueryState = {
|
|
31
|
+
phase: "ready",
|
|
32
|
+
articles: [],
|
|
33
|
+
error: null,
|
|
34
|
+
updatedAt: 1,
|
|
35
|
+
sourceIds: [],
|
|
36
|
+
};
|
|
37
|
+
const service = {
|
|
38
|
+
getQueryState: () => readyState,
|
|
39
|
+
watchQuery: () => {
|
|
40
|
+
watchCount++;
|
|
41
|
+
return () => {
|
|
42
|
+
unwatchCount++;
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
} as unknown as NewsService;
|
|
46
|
+
setSharedNewsService(service);
|
|
47
|
+
|
|
48
|
+
testSetup = await testRender(<NewsHookHarness />, { width: 20, height: 1 });
|
|
49
|
+
await act(async () => testSetup?.renderOnce());
|
|
50
|
+
|
|
51
|
+
expect(watchCount).toBe(1);
|
|
52
|
+
expect(unwatchCount).toBe(0);
|
|
53
|
+
|
|
54
|
+
await act(async () => {
|
|
55
|
+
rerenderHarness?.();
|
|
56
|
+
await Promise.resolve();
|
|
57
|
+
await testSetup?.renderOnce();
|
|
58
|
+
});
|
|
59
|
+
expect(watchCount).toBe(1);
|
|
60
|
+
|
|
61
|
+
await act(async () => testSetup?.renderer.destroy());
|
|
62
|
+
testSetup = undefined;
|
|
63
|
+
expect(unwatchCount).toBe(1);
|
|
64
|
+
});
|
package/src/news/hooks.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback,
|
|
1
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
2
2
|
import { buildNewsQueryKey, type NewsService } from "./aggregator";
|
|
3
3
|
import type { NewsArticle, NewsQuery, NewsQueryState } from "./types";
|
|
4
4
|
|
|
@@ -8,34 +8,27 @@ export function setSharedNewsService(service: NewsService | null): void {
|
|
|
8
8
|
sharedService = service;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function useNewsServiceVersion(): number {
|
|
22
|
-
if (!sharedService) return 0;
|
|
23
|
-
return useSyncExternalStore(
|
|
24
|
-
(cb) => sharedService!.subscribe(cb),
|
|
25
|
-
() => sharedService!.getVersion(),
|
|
26
|
-
);
|
|
27
|
-
}
|
|
11
|
+
const IDLE_NEWS_QUERY_STATE: NewsQueryState = {
|
|
12
|
+
phase: "idle",
|
|
13
|
+
articles: [],
|
|
14
|
+
error: null,
|
|
15
|
+
updatedAt: null,
|
|
16
|
+
sourceIds: [],
|
|
17
|
+
};
|
|
28
18
|
|
|
29
19
|
export function useNewsArticles(query: NewsQuery | null | undefined): NewsQueryState {
|
|
20
|
+
const service = sharedService;
|
|
30
21
|
const key = query ? buildNewsQueryKey(query) : null;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
const subscribe = useCallback((listener: () => void) => {
|
|
23
|
+
if (!query || !service) return () => {};
|
|
24
|
+
return service.watchQuery(query, () => listener());
|
|
25
|
+
}, [key, service]);
|
|
26
|
+
const getSnapshot = useCallback(
|
|
27
|
+
() => query && service ? service.getQueryState(query) : IDLE_NEWS_QUERY_STATE,
|
|
28
|
+
[key, service],
|
|
29
|
+
);
|
|
37
30
|
|
|
38
|
-
return
|
|
31
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
export function useLoadNewsStory(): (storyId: string) => Promise<NewsArticle | null> {
|
|
@@ -3,6 +3,7 @@ import { usePaneFooter, type PaneHint } from "../../../components";
|
|
|
3
3
|
import type { AccountProfile } from "../../../api-client";
|
|
4
4
|
import { formatPlan, type AccountDraft } from "./model";
|
|
5
5
|
import { t } from "../../../i18n";
|
|
6
|
+
import { useAppLanguage } from "../../../i18n/react";
|
|
6
7
|
|
|
7
8
|
export function useAccountManagementFooter({
|
|
8
9
|
busy,
|
|
@@ -19,9 +20,10 @@ export function useAccountManagementFooter({
|
|
|
19
20
|
profile: AccountProfile | null;
|
|
20
21
|
saveProfile: () => Promise<void>;
|
|
21
22
|
}) {
|
|
23
|
+
const language = useAppLanguage();
|
|
22
24
|
const footerHints = useMemo<PaneHint[]>(() => [
|
|
23
25
|
{ id: "save", key: "Ctrl+S", label: t("save"), onPress: () => { void saveProfile(); }, disabled: !!busy || !hasSession },
|
|
24
|
-
], [busy, hasSession, saveProfile]);
|
|
26
|
+
], [busy, hasSession, language, saveProfile]);
|
|
25
27
|
|
|
26
28
|
usePaneFooter("account-management", () => ({
|
|
27
29
|
info: [
|
|
@@ -33,5 +35,5 @@ export function useAccountManagementFooter({
|
|
|
33
35
|
] : []),
|
|
34
36
|
],
|
|
35
37
|
hints: footerHints,
|
|
36
|
-
}), [draft.profilePublic, footerHints, message, profile]);
|
|
38
|
+
}), [draft.profilePublic, footerHints, language, message, profile]);
|
|
37
39
|
}
|
|
@@ -48,15 +48,15 @@ import { useCloudSyncStatus } from "../../../sync/react";
|
|
|
48
48
|
import { cloudSyncController } from "../../../sync/controller";
|
|
49
49
|
import { setSyncedProfileAnalytics } from "../../../sync/profile-analytics";
|
|
50
50
|
import { t } from "../../../i18n";
|
|
51
|
+
import { useAppLanguage } from "../../../i18n/react";
|
|
51
52
|
import {
|
|
52
53
|
consumeRequestedAccountManagementTab,
|
|
53
54
|
subscribeRequestedAccountManagementTab,
|
|
54
55
|
type AccountManagementTab,
|
|
55
56
|
} from "./navigation";
|
|
57
|
+
import { CLOUD_UPGRADE_URL } from "../shared/cloud-upgrade";
|
|
56
58
|
|
|
57
59
|
type AccountBusy = "profile" | "password" | "alerts" | "billing" | "delete" | null;
|
|
58
|
-
const CLOUD_UPGRADE_URL = "https://gloom.sh/cloud?upgrade=pro";
|
|
59
|
-
|
|
60
60
|
const ACCOUNT_TAB_DEFS: Array<{ label: string; value: AccountManagementTab }> = [
|
|
61
61
|
{ label: "Profile", value: "profile" },
|
|
62
62
|
{ label: "Emails", value: "emails" },
|
|
@@ -88,7 +88,8 @@ const ACCOUNT_TAB_FIELD_ORDER: Record<AccountManagementTab, AccountFieldKey[]> =
|
|
|
88
88
|
};
|
|
89
89
|
|
|
90
90
|
const PLAN_COMPARISON_ROWS = [
|
|
91
|
-
{ capability: "
|
|
91
|
+
{ capability: "US equities", free: "15m delay", pro: "Real-time", proTone: "positive" },
|
|
92
|
+
{ capability: "Options data", free: "15m delay", pro: "Real-time", proTone: "positive" },
|
|
92
93
|
{ capability: "News", free: "12h delay", pro: "Real-time wire", proTone: "positive" },
|
|
93
94
|
{ capability: "Cloud sync", free: "Included", pro: "Included", proTone: "neutral" },
|
|
94
95
|
{ capability: "X data", free: "No", pro: "Included", proTone: "positive" },
|
|
@@ -286,6 +287,7 @@ function PlanComparison({
|
|
|
286
287
|
}
|
|
287
288
|
|
|
288
289
|
export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
290
|
+
const language = useAppLanguage();
|
|
289
291
|
const dialog = useDialog();
|
|
290
292
|
const renderer = useRendererHost();
|
|
291
293
|
const isDesktop = useUiHost().kind === "desktop-web";
|
|
@@ -335,7 +337,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
335
337
|
const portfolioHoldingCounts = useMemo(() => countPortfolioHoldings(tickers), [tickers]);
|
|
336
338
|
const portfolioChoices = useMemo(
|
|
337
339
|
() => buildPortfolioChoices(portfolios, portfolioHoldingCounts),
|
|
338
|
-
[portfolioHoldingCounts, portfolios],
|
|
340
|
+
[language, portfolioHoldingCounts, portfolios],
|
|
339
341
|
);
|
|
340
342
|
const selectedAnalyticsPortfolio = useMemo(
|
|
341
343
|
() => portfolios.find((portfolio) => portfolio.id === draft.sharedPortfolioId) ?? null,
|
|
@@ -419,6 +421,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
419
421
|
[
|
|
420
422
|
beta,
|
|
421
423
|
draft.sharedPortfolioId,
|
|
424
|
+
language,
|
|
422
425
|
portfolioTickers,
|
|
423
426
|
selectedAnalyticsPortfolio,
|
|
424
427
|
oneYearReturn,
|
|
@@ -448,6 +451,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
448
451
|
[
|
|
449
452
|
draft.profilePublic,
|
|
450
453
|
draft.sharedPortfolioId,
|
|
454
|
+
language,
|
|
451
455
|
profile,
|
|
452
456
|
selectedAnalyticsPortfolio,
|
|
453
457
|
syncStatus.phase,
|
|
@@ -469,6 +473,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
469
473
|
}, [
|
|
470
474
|
draft.profilePublic,
|
|
471
475
|
draft.sharedPortfolioId,
|
|
476
|
+
language,
|
|
472
477
|
profile,
|
|
473
478
|
syncStatus.phase,
|
|
474
479
|
]);
|
|
@@ -52,18 +52,20 @@ function optionalString(value: unknown): string | undefined {
|
|
|
52
52
|
function normalizeAssistantContent(value: unknown): AiAgentAssistantContent | null {
|
|
53
53
|
if (!isRecord(value)) return null;
|
|
54
54
|
if (value.type === "text" && typeof value.text === "string") {
|
|
55
|
+
const textSignature = optionalString(value.textSignature);
|
|
55
56
|
return {
|
|
56
57
|
type: "text",
|
|
57
58
|
text: value.text,
|
|
58
|
-
...(
|
|
59
|
+
...(textSignature ? { textSignature } : {}),
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
if (value.type === "thinking" && typeof value.thinking === "string") {
|
|
63
|
+
const thinkingSignature = optionalString(value.thinkingSignature);
|
|
62
64
|
return {
|
|
63
65
|
type: "thinking",
|
|
64
66
|
thinking: value.thinking,
|
|
65
|
-
...(
|
|
66
|
-
? { thinkingSignature
|
|
67
|
+
...(thinkingSignature
|
|
68
|
+
? { thinkingSignature }
|
|
67
69
|
: {}),
|
|
68
70
|
...(typeof value.redacted === "boolean" ? { redacted: value.redacted } : {}),
|
|
69
71
|
};
|
|
@@ -74,13 +76,14 @@ function normalizeAssistantContent(value: unknown): AiAgentAssistantContent | nu
|
|
|
74
76
|
&& typeof value.name === "string"
|
|
75
77
|
&& isRecord(value.arguments)
|
|
76
78
|
) {
|
|
79
|
+
const thoughtSignature = optionalString(value.thoughtSignature);
|
|
77
80
|
return {
|
|
78
81
|
type: "toolCall",
|
|
79
82
|
id: value.id,
|
|
80
83
|
name: value.name,
|
|
81
84
|
arguments: { ...value.arguments },
|
|
82
|
-
...(
|
|
83
|
-
? { thoughtSignature
|
|
85
|
+
...(thoughtSignature
|
|
86
|
+
? { thoughtSignature }
|
|
84
87
|
: {}),
|
|
85
88
|
};
|
|
86
89
|
}
|
|
@@ -91,14 +94,15 @@ function normalizeToolResultContent(value: unknown): AiAgentTextContent | null {
|
|
|
91
94
|
if (!isRecord(value) || value.type !== "text" || typeof value.text !== "string") {
|
|
92
95
|
return null;
|
|
93
96
|
}
|
|
97
|
+
const textSignature = optionalString(value.textSignature);
|
|
94
98
|
return {
|
|
95
99
|
type: "text",
|
|
96
100
|
text: value.text,
|
|
97
|
-
...(
|
|
101
|
+
...(textSignature ? { textSignature } : {}),
|
|
98
102
|
};
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
|
|
105
|
+
function normalizeAiAgentHistoryMessage(value: unknown): AiAgentHistoryMessage | null {
|
|
102
106
|
if (!isRecord(value)) return null;
|
|
103
107
|
if (value.role === "user" && typeof value.content === "string") {
|
|
104
108
|
return { role: "user", content: value.content };
|
|
@@ -16,8 +16,6 @@ import { buildTickerAiContext } from "./ticker-context";
|
|
|
16
16
|
import {
|
|
17
17
|
migrateLegacyAiProviderId,
|
|
18
18
|
resolveDefaultAiProviderId,
|
|
19
|
-
__setDetectedProvidersForTests,
|
|
20
|
-
type AiProvider,
|
|
21
19
|
} from "./providers";
|
|
22
20
|
import { runAiPrompt, type AiConversationMessage } from "./runner";
|
|
23
21
|
import { isAiProviderReady, resolveReadyAiRunnerDefault } from "./runner-selection";
|
|
@@ -47,9 +45,6 @@ const LEGACY_HISTORY_PROVIDER_IDS: Readonly<Record<string, string>> = {
|
|
|
47
45
|
google: "gemini",
|
|
48
46
|
};
|
|
49
47
|
|
|
50
|
-
export { __setDetectedProvidersForTests };
|
|
51
|
-
export type { AiProvider };
|
|
52
|
-
|
|
53
48
|
function sameMessages(left: readonly ChatMessage[], right: readonly ChatMessage[]): boolean {
|
|
54
49
|
return left.length === right.length && left.every((message, index) => {
|
|
55
50
|
const other = right[index];
|
|
@@ -12,9 +12,8 @@ import {
|
|
|
12
12
|
AskAiResearchTab as AskAiTab,
|
|
13
13
|
__resetAskAiHistoryForTests,
|
|
14
14
|
__setAskAiHistoryForTests,
|
|
15
|
-
__setDetectedProvidersForTests,
|
|
16
|
-
type AiProvider,
|
|
17
15
|
} from "./ask-ai-detail-tab";
|
|
16
|
+
import { setDetectedProviders, type AiProvider } from "./providers";
|
|
18
17
|
import { setAiRunHost, setAiRuntimeCatalog, type AiRunHost } from "./runner";
|
|
19
18
|
import type { TickerRecord } from "../../../types/ticker";
|
|
20
19
|
|
|
@@ -93,7 +92,7 @@ async function flushFrame() {
|
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
function setProviders(providers: AiProvider[]) {
|
|
96
|
-
|
|
95
|
+
setDetectedProviders(providers);
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
function readyProvider(id: AiProvider["id"] = "anthropic"): AiProvider {
|
|
@@ -113,7 +112,7 @@ afterEach(async () => {
|
|
|
113
112
|
});
|
|
114
113
|
testSetup = undefined;
|
|
115
114
|
}
|
|
116
|
-
|
|
115
|
+
setDetectedProviders(null);
|
|
117
116
|
__resetAskAiHistoryForTests();
|
|
118
117
|
setAiRunHost(null);
|
|
119
118
|
setAiRuntimeCatalog({ providers: [], accounts: [], models: [] });
|
|
@@ -5,7 +5,7 @@ import { aiPlugin } from "./index";
|
|
|
5
5
|
import {
|
|
6
6
|
AI_PROVIDER_IDS,
|
|
7
7
|
getAiProviderDefinition,
|
|
8
|
-
|
|
8
|
+
setDetectedProviders,
|
|
9
9
|
type AiProviderId,
|
|
10
10
|
} from "./providers";
|
|
11
11
|
import {
|
|
@@ -109,7 +109,7 @@ function setupPlugin(config = createDefaultConfig("/tmp/gloomberb-ai-plugin")) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
afterEach(() => {
|
|
112
|
-
|
|
112
|
+
setDetectedProviders(null);
|
|
113
113
|
setAiRunHost(null);
|
|
114
114
|
setAiRuntimeCatalog({ providers: [], accounts: [], models: [] });
|
|
115
115
|
});
|