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
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { PluginModule } from "../plugin-module";
|
|
3
3
|
import type { SecFilingDocument, SecFilingItem } from "../../../types/data-provider";
|
|
4
4
|
import { useResolvedEntryValue, useSecFilingDocuments, useSecFilingsQuery } from "../../../market-data/hooks";
|
|
5
5
|
import { instrumentFromTicker } from "../../../market-data/request-types";
|
|
6
6
|
import { useDebouncedPluginPaneState } from "../../runtime";
|
|
7
7
|
import { usePaneTicker } from "../../../state/app/context";
|
|
8
|
-
import {
|
|
8
|
+
import type { ScrollBoxRenderable } from "../../../ui";
|
|
9
|
+
import { EmptyState, FeedDataTableStackView, Spinner, useTableLoadMore, type FeedDataTableItem } from "../../../components";
|
|
9
10
|
import { isUsEquityTicker } from "../../../utils/sec";
|
|
10
11
|
import { parseForm4Xml, transactionTypeLabel } from "../insider/insider-data";
|
|
11
12
|
import { formatCompact, formatCurrency } from "../../../utils/format";
|
|
@@ -27,7 +28,8 @@ import {
|
|
|
27
28
|
} from "./filing-content";
|
|
28
29
|
import { usePaneStatusLinkFooter } from "../shared/pane-footer";
|
|
29
30
|
|
|
30
|
-
const
|
|
31
|
+
const SEC_FILING_FETCH_LIMIT = 20_000;
|
|
32
|
+
const SEC_FILING_PAGE_SIZE = 50;
|
|
31
33
|
const OWNERSHIP_FORMS = new Set(["3", "4", "5"]);
|
|
32
34
|
|
|
33
35
|
function getDisplayFormLabel(form: string): string {
|
|
@@ -250,10 +252,21 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
|
|
|
250
252
|
const instrument = instrumentFromTicker(ticker, ticker?.metadata.ticker ?? null);
|
|
251
253
|
const filingsEntry = useSecFilingsQuery(
|
|
252
254
|
instrument && eligibleTicker
|
|
253
|
-
? { instrument, count:
|
|
255
|
+
? { instrument, count: SEC_FILING_FETCH_LIMIT }
|
|
254
256
|
: null,
|
|
255
257
|
);
|
|
256
258
|
const filings = useResolvedEntryValue(filingsEntry) ?? [];
|
|
259
|
+
const [visibleCount, setVisibleCount] = useState(SEC_FILING_PAGE_SIZE);
|
|
260
|
+
const visibleFilings = useMemo(() => filings.slice(0, visibleCount), [filings, visibleCount]);
|
|
261
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
262
|
+
const loadMore = useTableLoadMore(
|
|
263
|
+
scrollRef,
|
|
264
|
+
visibleCount < filings.length,
|
|
265
|
+
() => setVisibleCount((current) => Math.min(filings.length, current + SEC_FILING_PAGE_SIZE)),
|
|
266
|
+
);
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
setVisibleCount(SEC_FILING_PAGE_SIZE);
|
|
269
|
+
}, [ticker?.metadata.ticker, ticker?.metadata.exchange]);
|
|
257
270
|
const loading = filingsEntry?.phase === "loading" || (filingsEntry?.phase === "refreshing" && filings.length === 0);
|
|
258
271
|
const error = filingsEntry?.phase === "error" ? filingsEntry.error?.message ?? "Failed to load SEC filings" : null;
|
|
259
272
|
|
|
@@ -270,7 +283,7 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
|
|
|
270
283
|
|
|
271
284
|
// Only the filing in view needs its content; queueing every ownership form in
|
|
272
285
|
// the list fires dozens of SEC requests the user never looks at.
|
|
273
|
-
const selectedFiling =
|
|
286
|
+
const selectedFiling = visibleFilings[selectedIdx];
|
|
274
287
|
const contentTargets = useMemo(() => [
|
|
275
288
|
...(openFiling ? [openFiling] : []),
|
|
276
289
|
...buildInlineFilingContentTargets(openFiling, openDocuments),
|
|
@@ -283,10 +296,10 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
|
|
|
283
296
|
const loadingContent = !!openFiling && !contentCache.has(openFiling.accessionNumber);
|
|
284
297
|
|
|
285
298
|
useEffect(() => {
|
|
286
|
-
if (
|
|
287
|
-
setSelectedIdx(Math.max(0,
|
|
299
|
+
if (visibleFilings.length > 0 && selectedIdx >= visibleFilings.length) {
|
|
300
|
+
setSelectedIdx(Math.max(0, visibleFilings.length - 1));
|
|
288
301
|
}
|
|
289
|
-
}, [
|
|
302
|
+
}, [selectedIdx, setSelectedIdx, visibleFilings.length]);
|
|
290
303
|
|
|
291
304
|
usePaneStatusLinkFooter({
|
|
292
305
|
registrationId: "sec",
|
|
@@ -313,7 +326,7 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
|
|
|
313
326
|
height={height}
|
|
314
327
|
focused={focused}
|
|
315
328
|
items={toFeedItems(
|
|
316
|
-
|
|
329
|
+
visibleFilings,
|
|
317
330
|
openFiling?.accessionNumber,
|
|
318
331
|
contentCache,
|
|
319
332
|
loadingContent,
|
|
@@ -326,6 +339,8 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
|
|
|
326
339
|
sourceLabel="Form"
|
|
327
340
|
titleLabel="Filing"
|
|
328
341
|
emptyStateTitle="No SEC filings."
|
|
342
|
+
scrollRef={scrollRef}
|
|
343
|
+
onBodyScrollActivity={loadMore}
|
|
329
344
|
/>
|
|
330
345
|
);
|
|
331
346
|
}
|
|
@@ -342,6 +342,20 @@ export const sectorsModule: PluginModule = {
|
|
|
342
342
|
},
|
|
343
343
|
],
|
|
344
344
|
|
|
345
|
+
setup(ctx) {
|
|
346
|
+
ctx.registerCommand({
|
|
347
|
+
id: "sectors-sp",
|
|
348
|
+
label: "Sector Performance",
|
|
349
|
+
description: "S&P 500 sector and industry performance sorted by daily change.",
|
|
350
|
+
keywords: ["sector", "sectors", "sp", "performance"],
|
|
351
|
+
category: "data",
|
|
352
|
+
shortcut: "SP",
|
|
353
|
+
execute: () => {
|
|
354
|
+
ctx.createPaneFromTemplate("sectors-pane");
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
|
|
345
359
|
paneTemplates: [
|
|
346
360
|
{
|
|
347
361
|
id: "sectors-pane",
|
|
@@ -8,7 +8,7 @@ import { useRendererHost } from "../../../ui";
|
|
|
8
8
|
import type { RendererHost } from "../../../ui";
|
|
9
9
|
import { getSharedRegistry } from "../../registry";
|
|
10
10
|
import { requestAccountManagementTab } from "../account-management/navigation";
|
|
11
|
-
import { usePlanAccess, type PlanAccess } from "./plan-access";
|
|
11
|
+
import { resolvePlanAccess, usePlanAccess, type PlanAccess } from "./plan-access";
|
|
12
12
|
|
|
13
13
|
export const CLOUD_UPGRADE_URL = "https://gloom.sh/cloud?upgrade=pro";
|
|
14
14
|
|
|
@@ -19,17 +19,20 @@ export const CLOUD_UPGRADE_URL = "https://gloom.sh/cloud?upgrade=pro";
|
|
|
19
19
|
let cloudUpgradeOpener: (() => void) | null = null;
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Signed-out users get the public Cloud page. Signed-in free accounts go
|
|
23
|
+
* straight to Stripe checkout, and accounts that already have Pro (paying or
|
|
24
|
+
* trialing) get the billing portal, so nobody re-buys a subscription they
|
|
25
|
+
* already hold. Both URLs are account-bound, so no session handoff is needed.
|
|
25
26
|
*/
|
|
26
27
|
export async function resolveCloudUpgradeUrl(): Promise<string> {
|
|
27
|
-
if (!apiClient.
|
|
28
|
-
const
|
|
29
|
-
|
|
28
|
+
if (!apiClient.isSignedIn()) return CLOUD_UPGRADE_URL;
|
|
29
|
+
const { url } = resolvePlanAccess(apiClient.getCurrentUser()).hasProAccess
|
|
30
|
+
? await apiClient.createBillingPortal()
|
|
31
|
+
: await apiClient.createCloudCheckout();
|
|
32
|
+
return url;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
/** Opens
|
|
35
|
+
/** Opens checkout or the billing portal in the user's browser. */
|
|
33
36
|
export async function openCloudUpgrade(rendererHost: Pick<RendererHost, "openExternal">): Promise<void> {
|
|
34
37
|
await rendererHost.openExternal(await resolveCloudUpgradeUrl());
|
|
35
38
|
}
|
|
@@ -45,9 +48,13 @@ export function openCloudUpgradeUrl(): boolean {
|
|
|
45
48
|
export function useCloudUpgradeAction(): () => void {
|
|
46
49
|
const rendererHost = useRendererHost();
|
|
47
50
|
const openUpgrade = useCallback(() => {
|
|
48
|
-
void openCloudUpgrade(rendererHost).catch(() => {
|
|
49
|
-
// Keep the
|
|
50
|
-
|
|
51
|
+
void openCloudUpgrade(rendererHost).catch(async () => {
|
|
52
|
+
// Keep the Cloud page reachable when checkout cannot be created; a native
|
|
53
|
+
// session still rides along on the server's one-time handoff URL.
|
|
54
|
+
const url = await apiClient.createBrowserHandoff()
|
|
55
|
+
.then((handoff) => handoff.url)
|
|
56
|
+
.catch(() => CLOUD_UPGRADE_URL);
|
|
57
|
+
void rendererHost.openExternal(url);
|
|
51
58
|
});
|
|
52
59
|
}, [rendererHost]);
|
|
53
60
|
useEffect(() => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { createPublicPaneShare } from "./public-pane";
|
|
3
|
+
|
|
4
|
+
test("default public pane shares restore no user-owned state", () => {
|
|
5
|
+
const adapter = createPublicPaneShare("World Equity Indices");
|
|
6
|
+
expect(adapter.serialize({
|
|
7
|
+
pane: { instanceId: "world", paneId: "world-indices", settings: { symbols: ["private"] } },
|
|
8
|
+
paneState: { selectedIndex: 2 },
|
|
9
|
+
})).toEqual({ title: "World Equity Indices", data: {} });
|
|
10
|
+
expect(adapter.restore({})).toEqual({});
|
|
11
|
+
expect(adapter.restore({ token: "secret" })).toBeNull();
|
|
12
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PaneTemplatePublicShareDef } from "../../../types/plugin";
|
|
2
|
+
|
|
3
|
+
/** Shares a public pane whose default template needs no user-owned input. */
|
|
4
|
+
export function createPublicPaneShare(defaultTitle: string): PaneTemplatePublicShareDef {
|
|
5
|
+
return {
|
|
6
|
+
serialize: ({ pane }) => ({ title: pane.title?.trim() || defaultTitle, data: {} }),
|
|
7
|
+
restore: (data) => Object.keys(data).length === 0 ? {} : null,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useRendererHost } from "../../../ui";
|
|
3
|
+
import { createShare, publicShareUrl } from "../../../shares/api";
|
|
4
|
+
import type { SharePayload } from "../../../shares/payload";
|
|
5
|
+
import { usePluginAppActions } from "../../runtime";
|
|
6
|
+
|
|
7
|
+
export function usePublicShare(): (payload: SharePayload) => Promise<void> {
|
|
8
|
+
const renderer = useRendererHost();
|
|
9
|
+
const { notify } = usePluginAppActions();
|
|
10
|
+
|
|
11
|
+
return useCallback(async (payload: SharePayload) => {
|
|
12
|
+
try {
|
|
13
|
+
const { id } = await createShare(payload);
|
|
14
|
+
await renderer.copyText(publicShareUrl(id));
|
|
15
|
+
notify({ body: "Share link copied to clipboard", type: "success" });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
notify({
|
|
18
|
+
body: error instanceof Error ? error.message : "Could not create share.",
|
|
19
|
+
type: "error",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}, [notify, renderer]);
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useAppStateRef, usePaneInstanceId } from "../../../state/app/context";
|
|
3
|
+
import { TICKER_RESEARCH_PANE_ID } from "../../../types/config";
|
|
4
|
+
import { resolveTickerActivation } from "../../ticker-navigation";
|
|
5
|
+
import { usePluginAppActions, usePluginTickerActions } from "../../runtime";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shared Enter routing for panes that publish a cursor symbol: reuse the Ticker Research pane
|
|
9
|
+
* already bound to this one instead of stacking another pane on top of it.
|
|
10
|
+
*/
|
|
11
|
+
export function useTickerSourceActivate(): (
|
|
12
|
+
symbol: string,
|
|
13
|
+
options?: { newPane?: boolean; floating?: boolean },
|
|
14
|
+
) => void {
|
|
15
|
+
const paneId = usePaneInstanceId();
|
|
16
|
+
const stateRef = useAppStateRef();
|
|
17
|
+
const { pinTicker } = usePluginTickerActions();
|
|
18
|
+
const { focusPane } = usePluginAppActions();
|
|
19
|
+
|
|
20
|
+
return useCallback((symbol: string, options?: { newPane?: boolean; floating?: boolean }) => {
|
|
21
|
+
const activation = resolveTickerActivation(stateRef.current.config.layout, paneId, options);
|
|
22
|
+
if (activation.kind === "focus") {
|
|
23
|
+
focusPane(activation.paneId);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
pinTicker(symbol, {
|
|
27
|
+
floating: options?.floating ?? true,
|
|
28
|
+
forceNewPane: options?.newPane,
|
|
29
|
+
paneType: TICKER_RESEARCH_PANE_ID,
|
|
30
|
+
});
|
|
31
|
+
}, [focusPane, paneId, pinTicker, stateRef]);
|
|
32
|
+
}
|
|
@@ -31,6 +31,33 @@ describe("createTickerSurfacePaneTemplate", () => {
|
|
|
31
31
|
}));
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
test("shares only the public ticker binding and restores it through the template", () => {
|
|
35
|
+
const template = createTickerSurfacePaneTemplate({
|
|
36
|
+
id: "financial-analysis-pane",
|
|
37
|
+
paneId: "financial-analysis",
|
|
38
|
+
label: "Financial Analysis",
|
|
39
|
+
description: "Open financial statements.",
|
|
40
|
+
keywords: ["fa"],
|
|
41
|
+
shortcut: "FA",
|
|
42
|
+
publicShare: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const shared = template.publicShare?.serialize({
|
|
46
|
+
pane: {
|
|
47
|
+
instanceId: "financial-analysis:AAPL",
|
|
48
|
+
paneId: "financial-analysis",
|
|
49
|
+
title: "FA AAPL",
|
|
50
|
+
binding: { kind: "fixed", symbol: "AAPL" },
|
|
51
|
+
settings: { cachedPrivateValue: "not shared" },
|
|
52
|
+
},
|
|
53
|
+
paneState: { selectedRow: 4 },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(shared).toEqual({ title: "FA AAPL", data: { symbol: "AAPL" } });
|
|
57
|
+
expect(template.publicShare?.restore(shared!.data)).toEqual({ symbol: "AAPL" });
|
|
58
|
+
expect(template.publicShare?.restore({ symbol: "AAPL", token: "secret" })).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
34
61
|
test("keeps chart shortcut variants separate with view keys", () => {
|
|
35
62
|
const price = createTickerSurfacePaneTemplate({
|
|
36
63
|
id: "graph-price-pane",
|
|
@@ -13,6 +13,8 @@ export interface TickerSurfacePaneTemplateOptions {
|
|
|
13
13
|
description: string;
|
|
14
14
|
keywords: string[];
|
|
15
15
|
shortcut: string;
|
|
16
|
+
/** Opt in only when this template is available in the hosted browser catalog. */
|
|
17
|
+
publicShare?: boolean;
|
|
16
18
|
titlePrefix?: string;
|
|
17
19
|
viewKey?: string | ((
|
|
18
20
|
symbol: string,
|
|
@@ -109,5 +111,20 @@ export function createTickerSurfacePaneTemplate(
|
|
|
109
111
|
resolveViewKey(templateOptions.viewKey, symbol, context, createOptions),
|
|
110
112
|
);
|
|
111
113
|
},
|
|
114
|
+
...(templateOptions.publicShare ? {
|
|
115
|
+
publicShare: {
|
|
116
|
+
serialize: ({ pane }) => pane.binding?.kind === "fixed" && pane.binding.symbol.trim()
|
|
117
|
+
? {
|
|
118
|
+
title: pane.title?.trim() || `${titlePrefix} ${pane.binding.symbol}`,
|
|
119
|
+
data: { symbol: pane.binding.symbol.trim().toUpperCase() },
|
|
120
|
+
}
|
|
121
|
+
: null,
|
|
122
|
+
restore: (data) => {
|
|
123
|
+
if (Object.keys(data).length !== 1 || typeof data.symbol !== "string") return null;
|
|
124
|
+
const symbol = data.symbol.trim().toUpperCase();
|
|
125
|
+
return symbol ? { symbol } : null;
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
} : {}),
|
|
112
129
|
};
|
|
113
130
|
}
|
|
@@ -27,13 +27,17 @@ function cachedDataFromResource<T>(entry: PersistedResourceValue<T> | null): Sub
|
|
|
27
27
|
|
|
28
28
|
export function getCachedSubstackHome(): SubstackHomeData | null {
|
|
29
29
|
const subscriptionsEntry = readResource<SubstackPublication[]>("subscriptions", "me", false);
|
|
30
|
-
const feedEntry = readResource<SubstackArticleSummary[]>("feed", "subscribed", false);
|
|
30
|
+
const feedEntry = readResource<SubstackArticleSummary[] | { items: SubstackArticleSummary[]; nextCursor?: string | null }>("feed", "subscribed", false);
|
|
31
31
|
if (!subscriptionsEntry || !feedEntry) return null;
|
|
32
|
+
const feed = Array.isArray(feedEntry.value) ? feedEntry.value : feedEntry.value.items;
|
|
33
|
+
const nextCursor = Array.isArray(feedEntry.value) ? null : feedEntry.value.nextCursor ?? null;
|
|
32
34
|
return {
|
|
33
|
-
subscriptions: sortSubscriptionsByLatest(subscriptionsEntry.value,
|
|
34
|
-
feed
|
|
35
|
+
subscriptions: sortSubscriptionsByLatest(subscriptionsEntry.value, feed),
|
|
36
|
+
feed,
|
|
35
37
|
fetchedAt: Math.max(subscriptionsEntry.fetchedAt, feedEntry.fetchedAt),
|
|
36
38
|
stale: Boolean(subscriptionsEntry.stale) || Boolean(feedEntry.stale),
|
|
39
|
+
hasMore: !!nextCursor,
|
|
40
|
+
nextCursor,
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -33,13 +33,19 @@ export async function loadSubstackHome(force = false): Promise<SubstackHomeData>
|
|
|
33
33
|
}),
|
|
34
34
|
]);
|
|
35
35
|
return {
|
|
36
|
-
subscriptions: sortSubscriptionsByLatest(subscriptionsEntry.data, feedEntry.data),
|
|
37
|
-
feed: feedEntry.data,
|
|
36
|
+
subscriptions: sortSubscriptionsByLatest(subscriptionsEntry.data, feedEntry.data.items),
|
|
37
|
+
feed: feedEntry.data.items,
|
|
38
38
|
fetchedAt: Math.max(subscriptionsEntry.fetchedAt, feedEntry.fetchedAt),
|
|
39
39
|
stale: subscriptionsEntry.stale || feedEntry.stale,
|
|
40
|
+
hasMore: !!feedEntry.data.nextCursor,
|
|
41
|
+
nextCursor: feedEntry.data.nextCursor,
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
export async function loadSubstackHomeMore(cursor: string): Promise<{ items: SubstackArticleSummary[]; nextCursor: string | null }> {
|
|
46
|
+
return fetchReaderFeedPage(requireAuth(), "subscribed", cursor);
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
function payloadCursor(payload: unknown): string | null {
|
|
44
50
|
const record = payload && typeof payload === "object" && !Array.isArray(payload)
|
|
45
51
|
? payload as Record<string, unknown>
|
|
@@ -48,23 +54,41 @@ function payloadCursor(payload: unknown): string | null {
|
|
|
48
54
|
return typeof cursor === "string" && cursor.trim() ? cursor : null;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
async function
|
|
57
|
+
async function fetchReaderFeedPage(
|
|
58
|
+
auth: ReturnType<typeof requireAuth>,
|
|
59
|
+
tab: string,
|
|
60
|
+
cursor: string | null = null,
|
|
61
|
+
): Promise<{ items: SubstackArticleSummary[]; nextCursor: string | null }> {
|
|
62
|
+
const url = new URL(`${SUBSTACK_ORIGIN}/api/v1/reader/feed`);
|
|
63
|
+
url.searchParams.set("tab", tab);
|
|
64
|
+
if (cursor) url.searchParams.set("cursor", cursor);
|
|
65
|
+
const payload = await fetchJsonAuthenticated<unknown>(url.toString(), auth);
|
|
66
|
+
const nextCursor = payloadCursor(payload);
|
|
67
|
+
return {
|
|
68
|
+
items: normalizeFeedItems(payload),
|
|
69
|
+
nextCursor: nextCursor && nextCursor !== cursor ? nextCursor : null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function fetchReaderFeedItems(
|
|
74
|
+
auth: ReturnType<typeof requireAuth>,
|
|
75
|
+
tab: string,
|
|
76
|
+
): Promise<{ items: SubstackArticleSummary[]; nextCursor: string | null }> {
|
|
52
77
|
const collected = new Map<string, SubstackArticleSummary>();
|
|
53
78
|
let cursor: string | null = null;
|
|
54
79
|
for (let page = 0; page < READER_FEED_MAX_PAGES && collected.size < READER_FEED_TARGET_ITEMS; page += 1) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
if (cursor) url.searchParams.set("cursor", cursor);
|
|
58
|
-
const payload = await fetchJsonAuthenticated<unknown>(url.toString(), auth);
|
|
59
|
-
for (const article of normalizeFeedItems(payload)) {
|
|
80
|
+
const result = await fetchReaderFeedPage(auth, tab, cursor);
|
|
81
|
+
for (const article of result.items) {
|
|
60
82
|
collected.set(article.id, article);
|
|
61
83
|
if (collected.size >= READER_FEED_TARGET_ITEMS) break;
|
|
62
84
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
if (!result.nextCursor) {
|
|
86
|
+
cursor = null;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
cursor = result.nextCursor;
|
|
66
90
|
}
|
|
67
|
-
return [...collected.values()];
|
|
91
|
+
return { items: [...collected.values()], nextCursor: cursor };
|
|
68
92
|
}
|
|
69
93
|
|
|
70
94
|
export async function loadSubstackPublicationFeed(
|
|
@@ -3,6 +3,7 @@ import { Box, useRendererHost, type ScrollBoxRenderable } from "../../../ui";
|
|
|
3
3
|
import {
|
|
4
4
|
EmptyState,
|
|
5
5
|
Spinner,
|
|
6
|
+
useTableLoadMore,
|
|
6
7
|
type DataTableKeyEvent,
|
|
7
8
|
} from "../../../components";
|
|
8
9
|
import type { PaneProps } from "../../../types/plugin";
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
import {
|
|
16
17
|
loadSubstackArticleDetail,
|
|
17
18
|
loadSubstackHome,
|
|
19
|
+
loadSubstackHomeMore,
|
|
18
20
|
loadSubstackPublicationFeed,
|
|
19
21
|
} from "./api/loaders";
|
|
20
22
|
import {
|
|
@@ -126,6 +128,33 @@ export function SubstackPane({ focused, width, height }: PaneProps) {
|
|
|
126
128
|
});
|
|
127
129
|
}, [auth, handleAuthFailure]);
|
|
128
130
|
|
|
131
|
+
const loadHomeMore = useCallback((cursor: string) => {
|
|
132
|
+
if (!auth) return;
|
|
133
|
+
const gen = homeFetchGenRef.current;
|
|
134
|
+
loadSubstackHomeMore(cursor)
|
|
135
|
+
.then((page) => {
|
|
136
|
+
if (homeFetchGenRef.current !== gen) return;
|
|
137
|
+
setHome((current) => {
|
|
138
|
+
if (!current.data) return current;
|
|
139
|
+
const itemsById = new Map(current.data.feed.map((article) => [article.id, article]));
|
|
140
|
+
for (const article of page.items) itemsById.set(article.id, article);
|
|
141
|
+
return {
|
|
142
|
+
...current,
|
|
143
|
+
data: {
|
|
144
|
+
...current.data,
|
|
145
|
+
feed: [...itemsById.values()],
|
|
146
|
+
hasMore: !!page.nextCursor,
|
|
147
|
+
nextCursor: page.nextCursor,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
})
|
|
152
|
+
.catch((loadError) => {
|
|
153
|
+
if (homeFetchGenRef.current !== gen) return;
|
|
154
|
+
handleAuthFailure(loadError);
|
|
155
|
+
});
|
|
156
|
+
}, [auth, handleAuthFailure]);
|
|
157
|
+
|
|
129
158
|
useEffect(() => {
|
|
130
159
|
if (!auth) return;
|
|
131
160
|
loadHome(false);
|
|
@@ -215,18 +244,32 @@ export function SubstackPane({ focused, width, height }: PaneProps) {
|
|
|
215
244
|
home,
|
|
216
245
|
}), [activePublication, activePublicationTabId, home, publicationFeeds]);
|
|
217
246
|
|
|
218
|
-
const
|
|
219
|
-
if (!
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
247
|
+
const loadMoreHome = useCallback(() => {
|
|
248
|
+
if (!auth || home.loading || detailOpen || !home.data?.hasMore || !home.data.nextCursor) return;
|
|
249
|
+
loadHomeMore(home.data.nextCursor);
|
|
250
|
+
}, [auth, detailOpen, home.data?.hasMore, home.data?.nextCursor, home.loading, loadHomeMore]);
|
|
251
|
+
|
|
252
|
+
const loadMoreActivePublicationRows = useTableLoadMore(
|
|
253
|
+
tableScrollRef,
|
|
254
|
+
!detailOpen && (
|
|
255
|
+
activePublication
|
|
256
|
+
? !!(publicationFeeds[activePublicationTabId ?? ""]?.data?.hasMore
|
|
257
|
+
&& publicationFeeds[activePublicationTabId ?? ""]?.data?.nextOffset != null
|
|
258
|
+
&& !publicationFeeds[activePublicationTabId ?? ""]?.loading
|
|
259
|
+
&& !publicationFeeds[activePublicationTabId ?? ""]?.loadingMore
|
|
260
|
+
&& !publicationFeeds[activePublicationTabId ?? ""]?.error)
|
|
261
|
+
: !!(home.data?.hasMore && home.data.nextCursor && !home.loading)
|
|
262
|
+
),
|
|
263
|
+
() => {
|
|
264
|
+
if (activePublication) {
|
|
265
|
+
const page = publicationFeeds[activePublicationTabId ?? ""]?.data;
|
|
266
|
+
if (page?.nextOffset != null) loadPublication(activePublication, false, page.nextOffset);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
loadMoreHome();
|
|
270
|
+
},
|
|
271
|
+
PUBLICATION_LOAD_MORE_THRESHOLD_ROWS,
|
|
272
|
+
);
|
|
230
273
|
|
|
231
274
|
const sortedRows = useMemo(() => (
|
|
232
275
|
sortedSubstackArticles(activeFeedState.data ?? [], sort)
|
|
@@ -26,7 +26,7 @@ describe("13F API", () => {
|
|
|
26
26
|
|
|
27
27
|
const funds = await searchThirteenFFunds("transport-smoke", 1);
|
|
28
28
|
|
|
29
|
-
expect(urls[0]).toContain("
|
|
29
|
+
expect(urls[0]).toContain("/cloud/sec/13f/funds");
|
|
30
30
|
expect(funds).toEqual([{ cik: "0001067983", name: "BERKSHIRE HATHAWAY INC" }]);
|
|
31
31
|
});
|
|
32
32
|
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ThirteenFTopFund,
|
|
8
8
|
} from "./types";
|
|
9
9
|
import type { PluginPersistence } from "../../../types/plugin";
|
|
10
|
+
import { apiClient } from "../../../api-client";
|
|
10
11
|
import { httpFetch } from "../../../utils/http-transport";
|
|
11
12
|
|
|
12
13
|
const FORMS_13F_BASE_URL = "https://forms13f.com/api/v1";
|
|
@@ -124,17 +125,22 @@ async function fetchForms13F<T>(
|
|
|
124
125
|
if (cached) return cached;
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
let value: T;
|
|
129
|
+
try {
|
|
130
|
+
value = await apiClient.getCloudSec13F(path, params) as T;
|
|
131
|
+
} catch {
|
|
132
|
+
const url = `${FORMS_13F_BASE_URL}${path}?${searchParams.toString()}`;
|
|
133
|
+
const response = await httpFetch(url, {
|
|
134
|
+
headers: { Accept: "application/json" },
|
|
135
|
+
signal: options.signal,
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
const stale = options.cache !== false ? readApiCache<T>(key, { allowExpired: true }) : null;
|
|
139
|
+
if (stale) return stale;
|
|
140
|
+
throw new Error(`Forms13F ${response.status} for ${path}`);
|
|
141
|
+
}
|
|
142
|
+
value = await response.json() as T;
|
|
136
143
|
}
|
|
137
|
-
const value = await response.json() as T;
|
|
138
144
|
if (value != null && options.cache !== false) {
|
|
139
145
|
writeApiCache(key, value);
|
|
140
146
|
}
|
|
@@ -277,6 +283,24 @@ export async function listThirteenFForms(
|
|
|
277
283
|
return arrayResponse(raw).map(mapForm).filter((form): form is ThirteenFFormSummary => !!form);
|
|
278
284
|
}
|
|
279
285
|
|
|
286
|
+
export async function listThirteenFFormHoldingsPage(
|
|
287
|
+
cik: string,
|
|
288
|
+
accessionNumber: string,
|
|
289
|
+
signal?: AbortSignal,
|
|
290
|
+
options: { forceRefresh?: boolean; offset?: number; limit?: number } = {},
|
|
291
|
+
): Promise<{ rows: ThirteenFHoldingRecord[]; hasMore: boolean }> {
|
|
292
|
+
const offset = Math.max(0, options.offset ?? 0);
|
|
293
|
+
const limit = Math.max(1, options.limit ?? FORM_PAGE_LIMIT);
|
|
294
|
+
const raw = await fetchForms13F<unknown>("/form", {
|
|
295
|
+
cik: normalizeCik(cik),
|
|
296
|
+
accession_number: accessionNumber,
|
|
297
|
+
limit,
|
|
298
|
+
offset,
|
|
299
|
+
}, { signal, forceRefresh: options.forceRefresh });
|
|
300
|
+
const rows = arrayResponse(raw).map(mapHolding).filter((holding): holding is ThirteenFHoldingRecord => !!holding);
|
|
301
|
+
return { rows, hasMore: rows.length >= limit && offset + rows.length < MAX_FORM_ROWS };
|
|
302
|
+
}
|
|
303
|
+
|
|
280
304
|
export async function listThirteenFFormHoldings(
|
|
281
305
|
cik: string,
|
|
282
306
|
accessionNumber: string,
|
|
@@ -285,15 +309,13 @@ export async function listThirteenFFormHoldings(
|
|
|
285
309
|
): Promise<ThirteenFHoldingRecord[]> {
|
|
286
310
|
const rows: ThirteenFHoldingRecord[] = [];
|
|
287
311
|
for (let offset = 0; offset < MAX_FORM_ROWS; offset += FORM_PAGE_LIMIT) {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
accession_number: accessionNumber,
|
|
291
|
-
limit: FORM_PAGE_LIMIT,
|
|
312
|
+
const page = await listThirteenFFormHoldingsPage(cik, accessionNumber, signal, {
|
|
313
|
+
...options,
|
|
292
314
|
offset,
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
rows.push(...page);
|
|
296
|
-
if (page.
|
|
315
|
+
limit: FORM_PAGE_LIMIT,
|
|
316
|
+
});
|
|
317
|
+
rows.push(...page.rows);
|
|
318
|
+
if (!page.hasMore) break;
|
|
297
319
|
}
|
|
298
320
|
return rows;
|
|
299
321
|
}
|